From 2b5cef9d3644221620a512b1c519419cba960d76 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 28 May 2026 01:33:19 +1000 Subject: [PATCH] fifo2 & startup interim fixes. sqids lib. vfs tidy --- src/bootsupport/modules/metaface-1.2.5.tm | 6411 --------- src/bootsupport/modules/modpod-0.1.4.tm | 673 - src/bootsupport/modules/overtype-1.7.2.tm | 4892 ------- src/bootsupport/modules/overtype-1.7.4.tm | 93 +- src/bootsupport/modules/punk-0.1.tm | 24 +- src/bootsupport/modules/punk/ansi-0.1.1.tm | 14 +- .../modules/punk/ansi/sauce-0.1.0.tm | 1 + src/bootsupport/modules/punk/args-0.2.tm | 10574 --------------- src/bootsupport/modules/punk/console-0.1.1.tm | 16 +- src/bootsupport/modules/punk/du-0.1.0.tm | 18 +- src/bootsupport/modules/punk/lib-0.1.3.tm | 4581 ------- src/bootsupport/modules/punk/lib-0.1.4.tm | 4981 ------- src/bootsupport/modules/punk/lib-0.1.5.tm | 5463 -------- src/bootsupport/modules/punk/lib-0.1.6.tm | 122 +- src/bootsupport/modules/punk/mix-0.2.tm | 37 - src/bootsupport/modules/punk/nav/fs-0.1.0.tm | 38 +- src/bootsupport/modules/punk/ns-0.1.0.tm | 189 +- src/bootsupport/modules/punk/repl-0.1.2.tm | 73 +- .../modules/punk/repl/codethread-0.1.0.tm | 276 - src/bootsupport/modules/punk/winlnk-0.1.0.tm | 836 -- ...llfilter-0.2.1.tm => shellfilter-0.2.2.tm} | 28 +- src/bootsupport/modules/shellthread-1.6.1.tm | 829 -- src/bootsupport/modules/tomlish-1.1.2.tm | 5680 -------- src/bootsupport/modules/tomlish-1.1.3.tm | 6002 --------- src/bootsupport/modules/tomlish-1.1.4.tm | 6199 --------- src/bootsupport/modules/tomlish-1.1.5.tm | 6973 ---------- src/bootsupport/modules/tomlish-1.1.6.tm | 9470 -------------- src/bootsupport/modules/tomlish-1.1.7.tm | 9470 -------------- src/lib/app-punk/repl.tcl | 40 +- src/lib/app-punkshell/punkshell.tcl | 34 +- src/lib/app-shellspy/shellspy.tcl | 251 +- src/lib/app_shell/app_shell.tcl | 2 +- src/lib/app_shellrun/app_shellrun.tcl | 2 +- src/modules/overtype-999999.0a1.0.tm | 93 +- src/modules/poshinfo-999999.0a1.0.tm | 2 +- src/modules/punk-0.1.tm | 34 +- src/modules/punk/aliascore-999999.0a1.0.tm | 1 + src/modules/punk/ansi-999999.0a1.0.tm | 14 +- src/modules/punk/ansi/sauce-999999.0a1.0.tm | 1 + .../punk/args/moduledoc/iocp-999999.0a1.0.tm | 144 + .../punk/args/moduledoc/iocp-buildversion.txt | 3 + .../args/moduledoc/tkcore-999999.0a1.0.tm | 11 +- .../cap/handlers/templates-999999.0a1.0.tm | 2 +- src/modules/punk/cesu-999999.0a1.0.tm | 24 +- src/modules/punk/char-999999.0a1.0.tm | 4 +- src/modules/punk/config-0.1.tm | 2 +- src/modules/punk/console-999999.0a1.0.tm | 16 +- src/modules/punk/du-999999.0a1.0.tm | 36 +- src/modules/punk/lib-999999.0a1.0.tm | 415 +- src/modules/punk/mix/cli-999999.0a1.0.tm | 32 +- .../mix/commandset/module-999999.0a1.0.tm | 16 +- src/modules/punk/mix/util-999999.0a1.0.tm | 30 + src/modules/punk/nav/fs-999999.0a1.0.tm | 38 +- src/modules/punk/ns-999999.0a1.0.tm | 192 +- .../punk/packagepreference-999999.0a1.0.tm | 23 +- src/modules/punk/repl-999999.0a1.0.tm | 206 +- src/modules/punkcheck-0.1.0.tm | 20 - src/modules/shellfilter-999999.0a1.0.tm | 470 +- src/modules/shellfilter-buildversion.txt | 2 +- src/modules/shellthread-999999.0a1.0.tm | 177 +- .../ansi-0.1.1_testsuites/ansi/ansimerge.test | 1 - .../ns-0.1.0_testsuites/ns/corp.test | 42 + src/modules/textblock-999999.0a1.0.tm | 59 +- .../src/bootsupport/modules/overtype-1.7.4.tm | 93 +- .../src/bootsupport/modules/punk-0.1.tm | 60 +- .../bootsupport/modules/punk/ansi-0.1.1.tm | 16 +- .../modules/punk/ansi/sauce-0.1.0.tm | 1 + .../bootsupport/modules/punk/console-0.1.1.tm | 200 +- .../src/bootsupport/modules/punk/du-0.1.0.tm | 20 +- .../src/bootsupport/modules/punk/lib-0.1.6.tm | 176 +- .../bootsupport/modules/punk/mix/cli-0.3.1.tm | 8 +- .../bootsupport/modules/punk/nav/fs-0.1.0.tm | 38 +- .../src/bootsupport/modules/punk/ns-0.1.0.tm | 189 +- .../bootsupport/modules/punk/repl-0.1.2.tm | 101 +- .../bootsupport/modules/punkcheck-0.1.0.tm | 91 +- .../bootsupport/modules/shellfilter-0.2.1.tm | 14 +- .../bootsupport/modules/shellfilter-0.2.2.tm} | 816 +- .../src/bootsupport/modules/overtype-1.7.4.tm | 93 +- .../src/bootsupport/modules/punk-0.1.tm | 60 +- .../bootsupport/modules/punk/ansi-0.1.1.tm | 16 +- .../modules/punk/ansi/sauce-0.1.0.tm | 1 + .../bootsupport/modules/punk/console-0.1.1.tm | 200 +- .../src/bootsupport/modules/punk/du-0.1.0.tm | 20 +- .../src/bootsupport/modules/punk/lib-0.1.6.tm | 176 +- .../bootsupport/modules/punk/mix/cli-0.3.1.tm | 8 +- .../bootsupport/modules/punk/nav/fs-0.1.0.tm | 38 +- .../src/bootsupport/modules/punk/ns-0.1.0.tm | 189 +- .../bootsupport/modules/punk/repl-0.1.2.tm | 101 +- .../bootsupport/modules/punkcheck-0.1.0.tm | 91 +- .../bootsupport/modules/shellfilter-0.2.1.tm | 14 +- .../bootsupport/modules/shellfilter-0.2.2.tm | 3399 +++++ src/runtime/mapvfs.config | 8 +- .../win32-x86_64/Memchan2.3/Memchan23.dll | Bin 0 -> 37376 bytes .../Memchan2.3/libMemchanstub23.a | Bin 0 -> 3420 bytes .../win32-x86_64/Memchan2.3/pkgIndex.tcl | 2 + src/vendormodules/include_modules.config | 67 +- src/vendormodules/sqids-0.3.1.tm | 931 ++ src/vendormodules_tcl9/Thread-3.0b3.tm | Bin 14273 -> 0 bytes .../platform/win32_x86_64_tcl9-3.0b3.tm | Bin 211441 -> 0 bytes src/vfs/_config/punk_main.tcl | 113 +- src/vfs/_vfscommon.vfs/lib/app-punk/repl.tcl | 40 +- .../lib/app-punkshell/punkshell.tcl | 34 +- .../lib/app-shellspy/shellspy.tcl | 251 +- .../lib/app_shell/app_shell.tcl | 2 +- .../lib/app_shellrun/app_shellrun.tcl | 2 +- .../_vfscommon.vfs/modules/gridplus-2.12b0.tm | Bin 51256 -> 51256 bytes .../modules/modpodtest-0.1.0.tm | Bin 9348 -> 0 bytes .../modules/modpodtest-0.1.1.tm | Bin 9410 -> 9410 bytes .../_vfscommon.vfs/modules/overtype-1.7.1.tm | 4772 ------- .../_vfscommon.vfs/modules/overtype-1.7.2.tm | 4892 ------- .../_vfscommon.vfs/modules/overtype-1.7.3.tm | 5019 -------- .../_vfscommon.vfs/modules/overtype-1.7.4.tm | 93 +- .../modules/packagetrace-0.8.tm | 643 - .../_vfscommon.vfs/modules/poshinfo-0.1.0.tm | 2 +- src/vfs/_vfscommon.vfs/modules/punk-0.1.tm | 70 +- .../modules/punk/aliascore-0.1.0.tm | 1 + .../_vfscommon.vfs/modules/punk/ansi-0.1.1.tm | 16 +- .../modules/punk/ansi/sauce-0.1.0.tm | 1 + .../_vfscommon.vfs/modules/punk/args-0.2.tm | 10705 ---------------- .../modules/punk/args/moduledoc/iocp-2.0.2.tm | 144 + .../punk/args/moduledoc/tkcore-0.1.1.tm | 11 +- .../punk/cap/handlers/templates-0.1.0.tm | 2 +- .../_vfscommon.vfs/modules/punk/cesu-0.1.1.tm | 24 +- .../_vfscommon.vfs/modules/punk/char-0.1.0.tm | 4 +- .../_vfscommon.vfs/modules/punk/config-0.1.tm | 2 +- .../modules/punk/console-0.1.1.tm | 200 +- .../_vfscommon.vfs/modules/punk/du-0.1.0.tm | 38 +- .../modules/punk/fileline-0.1.0.tm | 1737 --- .../_vfscommon.vfs/modules/punk/lib-0.1.1.tm | 4237 ------ .../_vfscommon.vfs/modules/punk/lib-0.1.2.tm | 4533 ------- .../_vfscommon.vfs/modules/punk/lib-0.1.3.tm | 4842 ------- .../_vfscommon.vfs/modules/punk/lib-0.1.4.tm | 5008 -------- .../_vfscommon.vfs/modules/punk/lib-0.1.5.tm | 5463 -------- .../_vfscommon.vfs/modules/punk/lib-0.1.6.tm | 469 +- .../modules/punk/mix/cli-0.3.1.tm | 40 +- .../punk/mix/commandset/module-0.1.0.tm | 16 +- .../modules/punk/mix/templates-0.1.3.tm | Bin 72577 -> 72577 bytes .../modules/punk/mix/util-0.1.0.tm | 30 + .../modules/punk/nav/fs-0.1.0.tm | 38 +- .../modules/punk/netbox-0.1.0.tm | 3114 ----- .../modules/punk/netbox-0.1.1.tm | 2 +- .../_vfscommon.vfs/modules/punk/ns-0.1.0.tm | 192 +- .../modules/punk/packagepreference-0.1.0.tm | 23 +- .../_vfscommon.vfs/modules/punk/repl-0.1.1.tm | 3582 ------ .../_vfscommon.vfs/modules/punk/repl-0.1.2.tm | 234 +- .../_vfscommon.vfs/modules/punk/repl-0.1.tm | 3415 ----- .../modules/punk/winlnk-0.1.0.tm | 836 -- .../_vfscommon.vfs/modules/punkcheck-0.1.0.tm | 111 +- .../modules/shellfilter-0.2.1.tm | 14 +- .../modules/shellfilter-0.2.2.tm | 3651 ++++++ .../modules/shellthread-1.6.2.tm | 177 +- src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm | 873 ++ src/vfs/_vfscommon.vfs/modules/sqids-0.3.1.tm | 931 ++ .../modules/test/overtype-1.7.4.tm | Bin 9673 -> 9673 bytes .../modules/test/punk/ansi-0.1.1.tm | Bin 11361 -> 11361 bytes .../modules/test/punk/args-0.1.5.tm | Bin 23388 -> 23388 bytes .../modules/test/punk/lib-0.1.3.tm | Bin 13035 -> 13087 bytes .../modules/test/punk/ns-0.1.0.tm | Bin 10806 -> 11130 bytes .../_vfscommon.vfs/modules/textblock-0.1.3.tm | 59 +- src/vfs/_vfscommon.vfs/modules/zipper-0.14.tm | Bin 9926 -> 9926 bytes .../lib_tcl8/iocp-2.0.2}/LICENSE | 50 +- .../punk86.vfs/lib_tcl8/iocp-2.0.2/README.md | 28 + .../lib_tcl8/iocp-2.0.2}/bt.tcl | 1387 +- .../lib_tcl8/iocp-2.0.2}/btnames.tcl | 766 +- .../lib_tcl8/iocp-2.0.2}/btsdr.tcl | 1924 +-- .../lib_tcl8/iocp-2.0.2}/pkgIndex.tcl | 72 +- .../iocp-2.0.2/win32-ix86/iocp202.dll | Bin 0 -> 125251 bytes .../iocp-2.0.2/win32-ix86/tcl9iocp202.dll | Bin 0 -> 125285 bytes .../iocp-2.0.2/win32-x86_64/iocp202.dll | Bin 0 -> 109642 bytes .../iocp-2.0.2/win32-x86_64/tcl9iocp202.dll | Bin 0 -> 109672 bytes .../lib_tcl8/iocp-2.0.2/LICENSE | 25 + .../lib_tcl8/iocp-2.0.2/README.md | 28 + .../punk86bawt.vfs/lib_tcl8/iocp-2.0.2/bt.tcl | 697 + .../lib_tcl8/iocp-2.0.2/btnames.tcl | 383 + .../lib_tcl8/iocp-2.0.2/btsdr.tcl | 962 ++ .../lib_tcl8/iocp-2.0.2/pkgIndex.tcl | 36 + .../iocp-2.0.2/win32-ix86/iocp202.dll | Bin 0 -> 125251 bytes .../iocp-2.0.2/win32-ix86/tcl9iocp202.dll | Bin 0 -> 125285 bytes .../iocp-2.0.2/win32-x86_64/iocp202.dll | Bin 0 -> 109642 bytes .../iocp-2.0.2/win32-x86_64/tcl9iocp202.dll | Bin 0 -> 109672 bytes .../lib_tcl8/iocp-2.0.2/LICENSE | 25 + .../lib_tcl8/iocp-2.0.2/README.md | 28 + .../lib_tcl8/iocp-2.0.2/bt.tcl | 697 + .../lib_tcl8/iocp-2.0.2/btnames.tcl | 383 + .../lib_tcl8/iocp-2.0.2/btsdr.tcl | 962 ++ .../lib_tcl8/iocp-2.0.2/pkgIndex.tcl | 36 + .../iocp-2.0.2/win32-ix86/iocp202.dll | Bin 0 -> 125251 bytes .../iocp-2.0.2/win32-ix86/tcl9iocp202.dll | Bin 0 -> 125285 bytes .../iocp-2.0.2/win32-x86_64/iocp202.dll | Bin 0 -> 109642 bytes .../iocp-2.0.2/win32-x86_64/tcl9iocp202.dll | Bin 0 -> 109672 bytes .../lib_tcl8/Memchan2.3/Memchan23.dll | Bin 0 -> 37376 bytes .../lib_tcl8/Memchan2.3/libMemchanstub23.a | Bin 0 -> 3420 bytes .../lib_tcl8/Memchan2.3/pkgIndex.tcl | 2 + .../punk8win.vfs/lib_tcl8/iocp-2.0.2/LICENSE | 25 + .../lib_tcl8/iocp-2.0.2/README.md | 28 + .../punk8win.vfs/lib_tcl8/iocp-2.0.2/bt.tcl | 697 + .../lib_tcl8/iocp-2.0.2/btnames.tcl | 383 + .../lib_tcl8/iocp-2.0.2/btsdr.tcl | 962 ++ .../lib_tcl8/iocp-2.0.2/pkgIndex.tcl | 36 + .../iocp-2.0.2/win32-ix86/iocp202.dll | Bin 0 -> 125251 bytes .../iocp-2.0.2/win32-ix86/tcl9iocp202.dll | Bin 0 -> 125285 bytes .../iocp-2.0.2/win32-x86_64/iocp202.dll | Bin 0 -> 109642 bytes .../iocp-2.0.2/win32-x86_64/tcl9iocp202.dll | Bin 0 -> 109672 bytes .../lib_tcl8/tclparser1.8/pkgIndex.tcl | 2 - .../lib_tcl8/tclparser1.8/tclparser18.dll | Bin 19968 -> 0 bytes .../lib_tcl8/tcltls1.7.22/pkgIndex.tcl | 16 + .../lib_tcl8/tcltls1.7.22/tcltls.dll | Bin 0 -> 1584128 bytes .../lib_tcl8/tcltls1.7.22/tls.tcl | 398 + .../punk9cook.vfs/lib_tcl9/iocp-2.0.2/LICENSE | 25 + .../lib_tcl9/iocp-2.0.2/README.md | 28 + .../punk9cook.vfs/lib_tcl9/iocp-2.0.2/bt.tcl | 697 + .../lib_tcl9/iocp-2.0.2/btnames.tcl | 383 + .../lib_tcl9/iocp-2.0.2/btsdr.tcl | 962 ++ .../lib_tcl9/iocp-2.0.2/pkgIndex.tcl | 36 + .../iocp-2.0.2/win32-ix86/iocp202.dll | Bin 0 -> 125251 bytes .../iocp-2.0.2/win32-ix86/tcl9iocp202.dll | Bin 0 -> 125285 bytes .../iocp-2.0.2/win32-x86_64/iocp202.dll | Bin 0 -> 109642 bytes .../iocp-2.0.2/win32-x86_64/tcl9iocp202.dll | Bin 0 -> 109672 bytes .../lib_tcl9/iocp-2.0.2/LICENSE | 25 + .../lib_tcl9/iocp-2.0.2/README.md | 28 + .../lib_tcl9/iocp-2.0.2/bt.tcl | 697 + .../lib_tcl9/iocp-2.0.2/btnames.tcl | 383 + .../lib_tcl9/iocp-2.0.2/btsdr.tcl | 962 ++ .../lib_tcl9/iocp-2.0.2/pkgIndex.tcl | 36 + .../iocp-2.0.2/win32-ix86/iocp202.dll | Bin 0 -> 125251 bytes .../iocp-2.0.2/win32-ix86/tcl9iocp202.dll | Bin 0 -> 125285 bytes .../iocp-2.0.2/win32-x86_64/iocp202.dll | Bin 0 -> 109642 bytes .../iocp-2.0.2/win32-x86_64/tcl9iocp202.dll | Bin 0 -> 109672 bytes .../iocp2.0b1/win32-x86_64/tcl9iocp20b1.dll | Bin 67072 -> 0 bytes 229 files changed, 28904 insertions(+), 150743 deletions(-) delete mode 100644 src/bootsupport/modules/metaface-1.2.5.tm delete mode 100644 src/bootsupport/modules/modpod-0.1.4.tm delete mode 100644 src/bootsupport/modules/overtype-1.7.2.tm delete mode 100644 src/bootsupport/modules/punk/args-0.2.tm delete mode 100644 src/bootsupport/modules/punk/lib-0.1.3.tm delete mode 100644 src/bootsupport/modules/punk/lib-0.1.4.tm delete mode 100644 src/bootsupport/modules/punk/lib-0.1.5.tm delete mode 100644 src/bootsupport/modules/punk/mix-0.2.tm delete mode 100644 src/bootsupport/modules/punk/repl/codethread-0.1.0.tm delete mode 100644 src/bootsupport/modules/punk/winlnk-0.1.0.tm rename src/bootsupport/modules/{shellfilter-0.2.1.tm => shellfilter-0.2.2.tm} (99%) delete mode 100644 src/bootsupport/modules/shellthread-1.6.1.tm delete mode 100644 src/bootsupport/modules/tomlish-1.1.2.tm delete mode 100644 src/bootsupport/modules/tomlish-1.1.3.tm delete mode 100644 src/bootsupport/modules/tomlish-1.1.4.tm delete mode 100644 src/bootsupport/modules/tomlish-1.1.5.tm delete mode 100644 src/bootsupport/modules/tomlish-1.1.6.tm delete mode 100644 src/bootsupport/modules/tomlish-1.1.7.tm create mode 100644 src/modules/punk/args/moduledoc/iocp-999999.0a1.0.tm create mode 100644 src/modules/punk/args/moduledoc/iocp-buildversion.txt rename src/{bootsupport/modules/shellfilter-0.2.tm => project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm} (86%) create mode 100644 src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm create mode 100644 src/vendorlib_tcl8/win32-x86_64/Memchan2.3/Memchan23.dll create mode 100644 src/vendorlib_tcl8/win32-x86_64/Memchan2.3/libMemchanstub23.a create mode 100644 src/vendorlib_tcl8/win32-x86_64/Memchan2.3/pkgIndex.tcl create mode 100644 src/vendormodules/sqids-0.3.1.tm delete mode 100644 src/vendormodules_tcl9/Thread-3.0b3.tm delete mode 100644 src/vendormodules_tcl9/Thread/platform/win32_x86_64_tcl9-3.0b3.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.0.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/overtype-1.7.1.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/overtype-1.7.2.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/overtype-1.7.3.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/packagetrace-0.8.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/args-0.2.tm create mode 100644 src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/iocp-2.0.2.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/fileline-0.1.0.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.1.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.2.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.3.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.4.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.5.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.1.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/punk/winlnk-0.1.0.tm create mode 100644 src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.2.tm create mode 100644 src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm create mode 100644 src/vfs/_vfscommon.vfs/modules/sqids-0.3.1.tm rename src/vfs/{punk9magicsplat.vfs/lib_tcl9/iocp2.0b1 => punk86.vfs/lib_tcl8/iocp-2.0.2}/LICENSE (98%) create mode 100644 src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/README.md rename src/vfs/{punk9magicsplat.vfs/lib_tcl9/iocp2.0b1 => punk86.vfs/lib_tcl8/iocp-2.0.2}/bt.tcl (95%) rename src/vfs/{punk9magicsplat.vfs/lib_tcl9/iocp2.0b1 => punk86.vfs/lib_tcl8/iocp-2.0.2}/btnames.tcl (96%) rename src/vfs/{punk9magicsplat.vfs/lib_tcl9/iocp2.0b1 => punk86.vfs/lib_tcl8/iocp-2.0.2}/btsdr.tcl (97%) rename src/vfs/{punk9magicsplat.vfs/lib_tcl9/iocp2.0b1 => punk86.vfs/lib_tcl8/iocp-2.0.2}/pkgIndex.tcl (82%) create mode 100644 src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll create mode 100644 src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll create mode 100644 src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/iocp202.dll create mode 100644 src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/LICENSE create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/README.md create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/bt.tcl create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/iocp202.dll create mode 100644 src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/LICENSE create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/README.md create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/bt.tcl create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/iocp202.dll create mode 100644 src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/Memchan2.3/Memchan23.dll create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/Memchan2.3/libMemchanstub23.a create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/Memchan2.3/pkgIndex.tcl create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/LICENSE create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/README.md create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/bt.tcl create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/iocp202.dll create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll delete mode 100644 src/vfs/punk8win.vfs/lib_tcl8/tclparser1.8/pkgIndex.tcl delete mode 100644 src/vfs/punk8win.vfs/lib_tcl8/tclparser1.8/tclparser18.dll create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/pkgIndex.tcl create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/tcltls.dll create mode 100644 src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/tls.tcl create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/LICENSE create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/README.md create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/bt.tcl create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/iocp202.dll create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/tcl9iocp202.dll create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/iocp202.dll create mode 100644 src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/LICENSE create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/README.md create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/bt.tcl create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/iocp202.dll create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/tcl9iocp202.dll create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/iocp202.dll create mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll delete mode 100644 src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/win32-x86_64/tcl9iocp20b1.dll diff --git a/src/bootsupport/modules/metaface-1.2.5.tm b/src/bootsupport/modules/metaface-1.2.5.tm deleted file mode 100644 index ebcf579e..00000000 --- a/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/bootsupport/modules/modpod-0.1.4.tm b/src/bootsupport/modules/modpod-0.1.4.tm deleted file mode 100644 index 64e1bd9d..00000000 --- a/src/bootsupport/modules/modpod-0.1.4.tm +++ /dev/null @@ -1,673 +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.4 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin modpod_module_modpod 0 0.1.4] -#[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] - - -#changes -#0.1.4 - when mounting with vfs::zip (because zipfs not available) - mount relative to executable folder instead of module dir -# (given just a module name it's easier to find exepath than look at package ifneeded script to get module path) - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# 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] - - - - #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 exedir [file dirname [file normalize [info nameofexecutable]]] - set mod_and_ver [file rootname [file tail $modfile]] - lassign [split $mod_and_ver -] moduletail version - - #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 $exedir/$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 $exedir/$mount_at] - if {![file exists $exedir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { - vfs::zip::Unmount $fd $exedir/$mount_at - error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" - } - } - } - source $exedir/$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.4 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/overtype-1.7.2.tm b/src/bootsupport/modules/overtype-1.7.2.tm deleted file mode 100644 index aa7405e2..00000000 --- a/src/bootsupport/modules/overtype-1.7.2.tm +++ /dev/null @@ -1,4892 +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.7.2 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.7.2] -#[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 "ANSI capable 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 [expr {$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 - - namespace eval argdoc { - variable PUNKARGS - - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - interp alias "" ::overtype::example "" ::punk::args::helpers::example - } - - - namespace eval argdoc { - lappend PUNKARGS [list { - @id -id ::overtype::renderline - @cmd -name overtype::renderline\ - -summary\ - {Render a line of text/ANSI input over a line of text.}\ - -help\ - {renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode - commandline repl for the Tcl Punk Shell. - It is also a central part of an ansi (micro) virtual terminal-emulator of sorts. - 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. - Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't - affect another. - Calling on the punk::ansi library - it can coalesce codes to keep the size down. - - It is a giant mess of doing exactly what common wisdom says not to do... lots at once. - Renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a - static underlay. - 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. - The overlay couuld be similar - in which case it may often be used to overwrite a column or section of - the underlay. - The overlay could however be a sequence of ANSI-laden text that jumps all over the place. - - 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. - By suppyling the ${$B}-info${$N} 1 option - it can return various fields indicating the state of the render. - The main 3 are: result, overflow_right, and unapplied. - 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. - } - @opts - -etabs -default 0 -type boolean - -width -default \uFFEF -type integer - -expand_right -default 0 -type boolean - -transparent -default 0 -type {literal(0)|literal(1)|regexp} -help\ - "0 to disable transparency processing - 1 to enable space characters in the - overlay to be transparent, or a regex - to match the character(s) required to be - transparent in the overlay." - -startcolumn -default 1 -type integer - -cursor_column -default 1 -type integer -help\ - {First column is 1. Cursor column can be zero or negative} - -cursor_row -default "" -type integer - -insert_mode -default 1 -type boolean - -crm_mode -default 0 -type boolean - -autowrap_mode -default 1 -type boolean - -reverse_mode -default 0 -type boolean - -info -default 0 -type boolean -help\ - "When set to 1, return a dictionary of settings useful for - processing ANSI input in a loop. When zero, the resulting - string will contain the updated line, but not all the - overtext may have been applied." - -exposed1 -default \uFFFD -help\ - {A character of single terminal column width to use - as replacement when first-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the second-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - -exposed2 -default \uFFFD -help\ - {A character of single terminal column width to use - as replacement when second-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the first-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - -cursor_restore_attributes -default "" - -cp437 -default 0 -type boolean - -experimental -default {} - - @values -min 2 -max 2 - undertext -type string -help\ - "A single line of text which may contain pre-rendered ANSI. - 'pre-rendered' in this context means that it may contain - various static ANSI codes such as SGR colours and attributes - but not motion-control or more complex ANSI sequences. - It is an error to supply a newline (lf) character in the - undertext." - overtext -type string -help\ - "ANSI (or plain text) to overlay onto the undertext. - May contain ANSI movement codes even if they would move the - cursor to another line. If -info is zero, the output will - only display up to the point where the cursor moved off-line. - If -info is 1, the line moved to may be reflected in the - cursor_row element of the result. Overtext may contain an lf - which is effectively a form of 'movement control' to increment - the row. - Other ANSI codes may perform operations such as changing the - insert_mode or reverse_mode - and these are reflected in the - result dictionary when '-info 1' is used, so that the state - can then be applied to subsequent lines." - }] - } - - proc renderline {args} { - # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### - # 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? - - - - #*** !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} { - 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 && $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::renderline_transparent {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 -} - - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::overtype ::overtype::argdoc -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [tcl::namespace::eval overtype { - variable version - set version 1.7.2 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.7.4.tm b/src/bootsupport/modules/overtype-1.7.4.tm index 403e971c..6c427f1d 100644 --- a/src/bootsupport/modules/overtype-1.7.4.tm +++ b/src/bootsupport/modules/overtype-1.7.4.tm @@ -401,13 +401,20 @@ tcl::namespace::eval overtype { set opt_console [tcl::dict::get $opts -console] + #-------------------------------------------------------------------------- + #TODO + #REVIEW - punk::console package may not be loaded set cursor_style_overtype {3 underline-blink} set cursor_style_insert {5 beam-blink} if {$opt_insert_mode} { - punk::console::cursor_style -console $opt_console $cursor_style_insert + set initial_cursor_style $cursor_style_insert } else { + set initial_cursor_style $cursor_style_overtype + } + catch { punk::console::cursor_style -console $opt_console $cursor_style_overtype } + #-------------------------------------------------------------------------- # ---------------------------- # -experimental dev flag to set flags etc @@ -695,22 +702,23 @@ tcl::namespace::eval overtype { #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\ - -overtext_type $overtext_type\ + 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 {*}{ + } -overtext_type $overtext_type {*}{ + } ] set rinfo [renderline {*}$renderopts $undertext $overtext] @@ -940,14 +948,15 @@ tcl::namespace::eval overtype { puts stdout ">>>renderspace<<<[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 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..? @@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype { } #JMN - if {[tcl::dict::get $vtstate insert_mode]} { - puts "setting cursor to insert style" - punk::console::cursor_style -console $opt_console $cursor_style_insert - } else { - punk::console::cursor_style -console $opt_console $cursor_style_overtype - } + #REVIEW - we don't want to emit cursor_style ANSI unless it changes. + #if {[tcl::dict::get $vtstate insert_mode]} { + # puts "setting cursor to insert style" + # punk::console::cursor_style -console $opt_console $cursor_style_insert + #} else { + # punk::console::cursor_style -console $opt_console $cursor_style_overtype + #} #puts "renderedrow_max: $renderedrow_max" #check for null lines below renderedrow_max (and at tail) and trim. @@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype { #broken: #todo - renderline -overflow is invalid. # we need renderline to support -expand_left ?? - 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 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} { @@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype { -crm_mode -default 0 -type boolean -autowrap_mode -default 1 -type boolean -reverse_mode -default 0 -type boolean - -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\ + -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\ + -choicelabels\ { 1 "return a dict with raw fields" 2 "return a dict using ansistring VIEW" diff --git a/src/bootsupport/modules/punk-0.1.tm b/src/bootsupport/modules/punk-0.1.tm index 8994be71..a0ae202c 100644 --- a/src/bootsupport/modules/punk-0.1.tm +++ b/src/bootsupport/modules/punk-0.1.tm @@ -283,14 +283,6 @@ namespace eval punk { #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;" - } # ------------------------ #Note that unlike an ordinary Tcl array - the linked ::env behaves differently. @@ -307,6 +299,15 @@ namespace eval punk { } # ------------------------ + 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;" + } + #change2 if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { set lookfor [list $name] @@ -5492,9 +5493,10 @@ namespace eval punk { #ctrl-c propagation also needs to be considered set teehandle punksh - uplevel 1 [list ::catch \ - [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ - ::tcl::UnknownResult ::tcl::UnknownOptions] + uplevel 1 [list ::catch {*}{ + } [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{ + } ::tcl::UnknownResult ::tcl::UnknownOptions + ] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { dict set ::tcl::UnknownOptions -code error diff --git a/src/bootsupport/modules/punk/ansi-0.1.1.tm b/src/bootsupport/modules/punk/ansi-0.1.1.tm index 851dde1b..e8518d0f 100644 --- a/src/bootsupport/modules/punk/ansi-0.1.1.tm +++ b/src/bootsupport/modules/punk/ansi-0.1.1.tm @@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi { @cmd -name punk::ansi::sauce -summary\ "SAUCE info from file"\ -help\ - "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data." + "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data. + + Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format + that was commonly used in old ANSI art files to store information about the file, such as + title, author, group, date, and comments. + It may also have fields to specify the number of columns and rows in the ANSI art, + as well as flags for specific display attributes. + + It may also be used on other types of files such as bitmap, vector, audio, binarytext, + xbin, archive and executable files. + + It is a 128-byte block of data that is typically appended to the end of a file. + https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm" -encoding -default iso8859-1 -type string -help\ "The default iso8859-1 is equivalent to binary ans should work in the usual case. diff --git a/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm b/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm index 79ea5901..e7428d84 100644 --- a/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm +++ b/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm @@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce { variable PUNKARGS variable PUNKARGS_aliases + #https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm lappend PUNKARGS [list { @id -id "(package)punk::ansi::sauce" @package -name "punk::ansi::sauce" -help\ diff --git a/src/bootsupport/modules/punk/args-0.2.tm b/src/bootsupport/modules/punk/args-0.2.tm deleted file mode 100644 index 47338954..00000000 --- a/src/bootsupport/modules/punk/args-0.2.tm +++ /dev/null @@ -1,10574 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::args 0.2 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.2] -#[copyright "2024"] -#[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] -#[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -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 -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -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 "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args::register}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::moduledoc::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - -tcl::namespace::eval ::punk::args {} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %G% \x1b\[32m %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name - -summary - -help - %B%@leaders%N% ?opt val...? - (used for leading args that come before switches/opts) - directive-options: - -min -max (min and max number of leaders) - -unnamed (allow unnamed positional leaders) - -takewhenargsmodulo (assign args to leaders based on modulo - of total number of args. If value is not supplied (or < 2) then - leaders are assigned based on whether configured opts are - encountered, and whether the min number of leaders and values - can be satisfied. In this case optional leaders are assigned if - the type of the argument can be matched.) - (also accepts options as defaults for subsequent leader definitions) - %B%@opts%N% ?opt val...? - directive-options: -any|-arbitrary - (also accepts options as defaults for subsequent flag definitions) - %B%@values%N% ?opt val...? - (used for trailing args that come after switches/opts) - directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent value definitions) - %B%@form%N% ?opt val...? - (used for commands with multiple forms) - directive-options: -form -synopsis - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@formdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (override autogenerated arg info for form) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@examples%N% ?opt val...? - directive-options: -help - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - A typenamelist represents a multi-value clause where each - value must match the specified type in order. This is not - valid for flags - which can only take a single value. - - typename and entries in typenamelist can take 2 forms: - 1) basic form: elements of llength 1 such as a simple type, - or a pipe-delimited set of type-alternates. - e.g for a single typename: - -type int, -type int|char, -type int|literal(abc) - e.g for a typenamelist - -type {int double}, -type {int|char double} - 2) special form: elements of variable length - e.g for a single typename: - -type {{literal |}} - -type {{literal | | literal (}} - e.g for a typenamelist - -type {{literal |} {stringstartswith abc | int}} - The 2 forms can be mixed: - -type {{literal |} {stringstartswith a|c | int} literal(xyz)|int} - - Defaults to string. If no other restrictions - are required, choosing -type any does the least validation. - recognised types: - any - (unvalidated - accepts anything) - unknown - (unvalidated - accepts anything) - none - (used for flags/switches only. Indicates this is - a 'solo' flag ie accepts no value) - Not valid as a member of a clause's typenamelist. - int - integer - number - list - indexexpression - indexset - (as accepted by punk::lib::is_indexset) - dict - double - float - bool - boolean - char - file - directory - ansistring - globstring - (any of the types accepted by 'string is') - - The above all perform some validation checks - - string - (also any of the 'string is' types such as - xdigit, graph, punct, lower etc) - -type string on its own does not need validation, - but still checks for string-related restrictions - such as regexprefail, & minsize - - literal() - (exact match for string) - literalprefix() - (prefix match for string, other literal and literalprefix - entries specified as alternates using | are used in the - calculation) - stringstartswith() - (value must match glob *) - The value of string must not contain pipe char '|' - - Note that types can be combined with | to indicate an 'or' - operation - e.g char|int - e.g literal(xxx)|literal(yyy) - e.g literalprefix(text)|literalprefix(binary) - (when all in the pipe-delimited type-alternates set are - literal or literalprefix - this is similar to the -choices - option) - - - and more.. (todo - document here) - If a typenamelist is supplied and has length > 1 - then -typeranges must be used instead of -range - The number of elements in -typeranges must match - the number of elements specified in -type. - - -typesynopsis - Must be same length as value in -type - This provides and override for synopsis display of types. - Any desired italicization must be applied manually to the - value. - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choiceprefixreservelist {} - These choices are additional values used in prefix calculation. - The values will not be added to the list of available choices. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant - only valid if -type is a single item) - -typeranges (list with same number of elements as -type) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - #text should be a well-formed Tcl list - text -type list -multiple 1 -help\ - {Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - ${[punk::args::moduledoc::tclcore::argdoc::example { - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\ - "Description of command" - - %G%#The following option defines an option-value pair%R% - %G%#It may have aliases by separating them with a pipe |%R% - -fg|-foreground -default blah -type string -help\ - "In the result dict returned by punk::args::parse - the value used in the opts key will always be the last - entry, in this case -foreground" - %G%#The following option defines a flag style option (solo)%R% - -flag1 -default 0 -type none -help\ - "Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars" - - @values -min 1 -max -1 - %G%#Items that don't begin with * or - are value definitions%R% - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } "@doc -name Manpage: -url [myfunc_manpage_geturl myns::myfunc]"}]} - } - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderdirective_defaults [tcl::dict::create\ - -type any\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optdirective_defaults [tcl::dict::create\ - -type any\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - -parsekey ""\ - -group ""\ - ] - #parsekey is name of argument to use as a key in punk::args::parse result dicts - - set valdirective_defaults [tcl::dict::create\ - -type any\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED [list]\ - LEADER_NAMES [list]\ - LEADER_MIN ""\ - LEADER_MAX ""\ - LEADER_TAKEWHENARGSMODULO 0\ - LEADER_UNNAMED false\ - LEADERSPEC_DEFAULTS $leaderdirective_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - OPT_DEFAULTS [tcl::dict::create]\ - OPT_REQUIRED [list]\ - OPT_NAMES [list]\ - OPT_ANY 0\ - OPT_MIN ""\ - OPT_MAX ""\ - OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optdirective_defaults\ - OPT_CHECKS_DEFAULTS {}\ - OPT_GROUPS {}\ - VAL_DEFAULTS [tcl::dict::create]\ - VAL_REQUIRED [list]\ - VAL_NAMES [list]\ - VAL_MIN ""\ - VAL_MAX ""\ - VAL_UNNAMED false\ - VALSPEC_DEFAULTS $valdirective_defaults\ - VAL_CHECKS_DEFAULTS {}\ - FORMDISPLAY [tcl::dict::create]\ - ] - - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set lvl 2 - set id [rawdef_id $args $lvl] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - undefine $id 0 - - ##dict unset argdata_cache $prevraw ;#silently does nothing if key not present - #dict for {k v} $argdata_cache { - # if {[dict get $v id] eq $id} { - # dict unset argdata_cache $k - # } - #} - #dict for {k v} $rawdef_cache { - # if {[dict get $v -id] eq $id} { - # dict unset rawdef_cache $k - # } - #} - #dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - proc undefine {id {quiet 0}} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[id_exists $id]} { - if {!$quiet} { - puts stderr "punk::args::undefine clearing existing data for id:$id" - } - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } else { - if {!$quiet} { - puts stderr "punk::args::undefine unable to find id: '$id'" - } - } - } - #'punk::args::parse $args withdef $deflist' can raise parsing error after an autoid was generated - # In this case we don't see the autoid in order to delete it - #proc undefine_deflist {deflist} { - #} - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus but terribly slow without an accelerator) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - #punk::args::get_by_id ::punk::args::define {} - punk::args::parse {} -errorstyle minimal withid ::punk::args::define - return - } - #if {[lindex $args 0] eq "-dynamic"} { - # set is_dynamic [lindex $args 1] - # set textargs [lrange $args 2 end] - #} - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - #set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -paramindents none -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache [list $optionspecs]]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache [list $optionspecs]] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - - #set opt_defaults [tcl::dict::create] - #set val_defaults [tcl::dict::create] - - #set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - set examples_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - #set opt_any 0 - #set val_min 0 - #set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::resolve @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::moduledoc::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #NOTE - this is switch arm for the literal "default" (@default) - not the default arm of the switch block! - - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - - #foreach fid $record_form_ids { - # #only use elements with matching form id? - # #probably this feature mainly useful for _default anyway so that should be ok - # #cooperative doc sets specified in same file could share via known form ids too - # FORMDISPLAY has keys -header -body - # if {![dict size $F $fid $FORMDISPLAY]} { - # if {[dict exists $copyfrom FORMS $fid FORMDISPLAY]} { - # dict set F $fid FORMDISPLAY [dict get $copyfrom FORMS $fid FORMDISPLAY] - # } - # } - # #TODO - # #create leaders opts vals depending on position of @default line? - # #options on @default line to exclude/include sets??? - #} - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - - # @form "-synopsis" is optional - and only exists in case the user really wants - # to display something different. The system should generate consistent synopses - # with appropriate italics/bracketing etc. - # For manual -synopsis - features such as italics must be manually added. - - #spitballing.. - #The punk::args parser should generally be able to determine the appropriate form based - #on supplied arguments, e.g automatically using argument counts and matching literals. - #We may need to support some hints for forcing more efficient -form discriminators - # - # e.g compare with -takewhenargsmodulo that is available on @leaders - - #the -arities idea below is a rough one; potentially something to consider.. but - #we want to be able to support command completion.. and things like literals should probably - #take preference for partially typed commands.. as flipping to other forms based on argcount - #could be confusing. Need to match partial command to closest form automatically but allow - #user to lock in a form interactively and see mismatches (?) - #Probably the arity-ranges of a form are best calculated automatically rather than explicitly, - #otherwise we have a strong potential for misdefinition.. (conflict with defined leaders,opts,values) - #The way forward might be to calculate some 'arity' structure from the forms to aid in form-discrimination at arg parse time. - #(this is currently covered in some ways by the LEADER_MIN,LEADER_MAX,OPT_MIN,OPT_MAX,VAL_MIN,VAL_MAX members of the FORMS dict.) - - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - #e.g -name - # -summary - # -help - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - formdisplay { - #override the displayed argument table for the form. - #(formdisplay keys -header -body) - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - foreach fid $record_form_ids { - tcl::dict::set F $fid FORMDISPLAY [dict merge [tcl::dict::get $F $fid FORMDISPLAY] $at_specs] - } - } - opts { - foreach fid $record_form_ids { - if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::resolve - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - tcl::dict::set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -any - -arbitrary - - -anyopts { - #set opt_any $v - tcl::dict::set F $fid OPT_ANY $v - } - -min { - dict set F $fid OPT_MIN $v - } - -max { - #if no -max explicitly specified, and llength OPT_NAMES == 0 and OPT_ANY == 0 - -max will be set to 0 below. - dict set F $fid OPT_MAX $v - } - -minsize - -maxsize - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - #v is a typelist - #foreach t $v { - # #validate? - #} - tcl::dict::set tmp_optspec_defaults -type $v - } - -defaultdisplaytype { - #how the -default is displayed - #-default doesn't have to be the same type as -type which validates user input that is not defaulted. - tcl::dict::set tmp_optspec_defaults -defaultdisplaytype $v - } - -parsekey { - tcl::dict::set tmp_optspec_defaults -parsekey $v - - } - -group { - tcl::dict::set tmp_optspec_defaults -group $v - if {$v ne "" && ![tcl::dict::exists $F $fid OPT_GROUPS $v]} { - tcl::dict::set F $fid OPT_GROUPS $v {-parsekey {} -help {}} - } - if {$v ne ""} { - if {[tcl::dict::exists $at_specs -parsekey]} { - tcl::dict::set F $fid OPT_GROUPS $v -parsekey [tcl::dict::get $at_specs -parsekey] - } - } - } - -grouphelp { - if {![tcl::dict::exists $at_specs -group]} { - error "punk::args::resolve Bad @opt line. -group entry is required if -grouphelp is being configured. @id:$DEF_definition_id" - } - set g [tcl::dict::get $at_specs -group] - if {$g eq ""} { - error "punk::args::resolve Bad @opt line. -group non-empty value is required if -grouphelp is being configured. @id:$DEF_definition_id" - } - set groupdict [tcl::dict::get $F $fid OPT_GROUPS] - #set helprecords [tcl::dict::get $F $fid OPT_GROUPS_HELP] - if {![tcl::dict::exists $groupdict $g]} { - tcl::dict::set F $fid OPT_GROUPS $g [dict create -parsekey {} -help $v] - } else { - tcl::dict::set F $fid OPT_GROUPS $g -help $v - } - } - -range { - if {[dict exists $at_specs -type]} { - set tp [dict get $at_specs -type] - } else { - set tp [dict get $tmp_optspec_defaults -type] - } - if {[llength $tp] == 1} { - tcl::dict::set tmp_optspec_defaults -typeranges [list $v] - } else { - error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -range only applies to single-item type. Use -typeranges instead. @id:$DEF_definition_id" - } - } - -typeranges { - if {[dict exists $at_specs -type]} { - set tp [dict get $at_specs -type] - } else { - set tp [dict get $tmp_optspec_defaults -type] - } - if {[llength $tp] != [llength $v]} { - error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -typeranges has length [llength $v]. Lengths must match. @id:$DEF_definition_id" - } - tcl::dict::set tmp_optspec_defaults -typeranges $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -validationtransform { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple - - -prefix { - #check is bool - if {![string is boolean -strict $v]} { - error "punk::args::resolve - Option '$k' has value '$v'of wrong type in @opts line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -parsekey -group -grouphelp\ - -any -anyopts -arbitrary -form -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -type -range -typeranges -default -defaultdisplaytype -typedefaults - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::resolve - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - tcl::dict::set F $fid OPTSPEC_DEFAULTS $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::resolve - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -takewhenargsmodulo { - dict set F $fid LEADER_TAKEWHENARGSMODULO $v - } - -choiceprefix - - -choicerestricted { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo - -choicelabels { - if {[llength $v] %2 != 0} { - error "punk::args::resolve - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - #$v is a list of types - #foreach t $v { - #validate? - #} - #switch -- $v { - # int - integer { - # set v int - # } - # char - character { - # set v char - # } - # bool - boolean { - # set v bool - # } - # dict - dictionary { - # set v dict - # } - # list { - - # } - # index { - # set v indexexpression - # } - # default { - # #todo - disallow unknown types unless prefixed with custom- - # } - #} - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -range { - tcl::dict::set tmp_leaderspec_defaults -range $v - } - -typeranges { - tcl::dict::set tmp_leaderspec_defaults -range $v - } - -minsize - -maxsize - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefixdenylist - -choiceprefixreservelist - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -unnamed { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - dict set F $fid LEADER_UNNAMED $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::resolve - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -unnamed\ - } - error "punk::args::resolve - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid LEADERSPEC_DEFAULTS $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid VALSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is 0. got $v @id:$DEF_definition_id" - } - #set val_min $v - dict set F $fid VAL_MIN $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - #set val_max $v - dict set F $fid VAL_MAX $v - } - -minsize - -maxsize - -choices - -choicemultiple - -choicecolumns - - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::resolve - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -range { - tcl::dict::set tmp_valspec_defaults -range $v - } - -typeranges { - tcl::dict::set tmp_valspec_defaults -typeranges $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform { - tcl::dict::set tmp_valspec_defaults $k $v - } - -unnamed { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" - } - dict set F $fid VAL_UNNAMED $v - } - default { - set known { -type -range -typeranges\ - -min -form -minvalues -max -maxvalues\ - -minsize -maxsize\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -unnamed\ - } - error "punk::args::resolve - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid VALSPEC_DEFAULTS $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - examples { - set examples_info [dict merge $examples_info $at_specs] - } - default { - error "punk::args::resolve - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @examples @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argdef_values $record_values - #Note that we can get options defined with aliases e.g "-x|-suppress" - #Here we store the full string as the argname - but in the resulting dict upon parsing it will have the final - # entry as the key for retrieval e.g {leaders {} opts {-suppress true} values {} ...} - - #we can also have longopts within the list e.g "-f|--filename=" - #This accepts -f or --filename= - # (but not --filename ) - #if the clausemember is optional - then the flag can act as a solo, but a parameter can only be specified on the commandline with an = - #e.g "-x|--something= -type ?string? - #accepts all of: - # -x - # --something - # --something=blah - - - #while most longopts require the = some utilities (e.g fossil) - #accept --longname - #(fossil accepts either --longopt or --longopt=) - #For this reason, "-f|--filename" is different to gnu-style longopt "-f|--filename=" - - #for "--filename=" we can specify an 'optional' clausemember using for example -type ?string? - - #4? cases - #1) - #--longopt - # (not really a longopt - can only parse with --longopt - [optional member not supported, but could be solo if -type none]) - #2) - #--longopt= - # (gnu style longopt - parse with --longopt= - solo allowed if optional member - does not support solo via -type none) - #3) - #--longopt|--longopt= -types int - # (mixed - as fossil does - parse with --longopt= or --longopt [optional member not supported?]) - #4) - # --xxx|--longopt= -types {?int?} - #(repeating such as --longopt --longopt= not valid?) - #redundant? - #ie --longopt|--longopt= -types {?int?} - # equivalent to - # --longopt= -types {?int?} - #allow parsing -xxx only as solo and --longopt as solo or --longopt=n ? - - #the above set would not cover the edge-case where we have an optional member but we don't want --longopt to be allowed solo - #e.g - #-soloname|--longopt= -types ?int? - #allows parsing "-soloname" or "--longopt" or "--longopt=n" - #but what if we want it to mean only accept: - # "-soloname" or "--longopt=n" ?? - - #we deliberately don't support - #--longopt -type ?type? - #or -opt -type ?type? - #as this results in ambiguities and more complexity in parsing depending on where flag occurs in args compared to positionals - - #for these reasons - we can't only look for leading -- here to determine 'longopt' - - - set argname $firstword - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #do some basic validation here - #1 "-type none" would not be valid for "--filename=" - #2 a -type can only be optional (specified as -type ?xxx?) if at least one entry in the argname has a trailing = - #3 require --longopt if has a trailing =. ie disallow -opt= ? - - set has_equal 0 - set optaliases [split $firstword |] - if {[lsearch $optaliases *=] >=0} { - set has_equal 1 - } - #todo - if no -type specified in this flag record, we still need to check the default -type from the @opts record - which could have been - #overridden from just 'string' - if {[tcl::dict::exists $argdef_values -type]} { - set tp [tcl::dict::get $argdef_values -type] - if {[llength $tp] != 1} { - #clauselength > 1 not currently supported for flags - #e.g -myflag -type {list int} - # e.g called on commandline with cmd -myflag {a b c} 3 - #review - seems an unlikely and complicating feature to allow - evidence of tools using/supporting this in the wild not known of. - error "punk::args::resolve - Multiple space-separated arguments (as indicated by -type having multiple entries) for a flag are not supported. flag $argname -type '$tp' @id:$DEF_definition_id" - } - if {$argname eq "--"} { - if {$tp ne "none"} { - #error to explicitly attempt to configure -- as a value-taking option - error "punk::args::resolve - special flag named -- cannot be configured as a value-accepting flag. set -type none or omit -type from definition. @id:$DEF_definition_id" - } - } - if {$tp eq "none"} { - if {$has_equal} { - error "punk::args::resolve - flag type 'none' (indicating non-parameter-taking flag) is not supported when any flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" - } - } elseif {[string match {\?*\?} $tp]} { - #optional flag value - if {!$has_equal} { - error "punk::args::resolve - Optional flag parameter (as indicated by leading & trailing ?) is not supported when no flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" - } - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - ##set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - - tcl::dict::set argdef_values -ARGTYPE option - #set all_choices [_resolve_get_record_choices] - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::resolve - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - #This can happen if the definition has repeated values - error "punk::args::resolve - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - if {[dict get $F $fid LEADER_MAX] < [llength $temp_leadernames]} { - puts stderr "punk::args::resolve warning arg $argname LEADER_MAX == [dict get $F $fid LEADER_MAX] but [llength $temp_leadernames] leader names found @id:$DEF_definition_id" - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - if {$argname ni $temp_valnames} { - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - } else { - error "punk::args::resolve - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - #lappend val_names $argname - if {[dict get $F $fid VAL_MAX] >= 0} { - if {[dict get $F $fid VAL_MAX] < [llength $temp_valnames]} { - puts stderr "punk::args::resolve warning arg $argname VAL_MAX == [dict get $F $fid VAL_MAX] but [llength $temp_valnames] value names found @id:$DEF_definition_id" - dict set F $fid VAL_MAX [llength $temp_valnames] - } - } - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a leader, value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid VALSPEC_DEFAULTS] - } else { - set spec_merged [dict get $F $fid LEADERSPEC_DEFAULTS] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form {} - -type { - #todo - could be a list e.g {any int literal(Test)} - #case must be preserved in literal bracketed part - set typelist [list] - foreach typespec $specval { - if {[string match {\?*\?} $typespec]} { - set tspec [string range $typespec 1 end-1] - set optional_clausemember true - } else { - set tspec $typespec - set optional_clausemember false - } - set type_alternatives [_split_type_expression $tspec] - set normlist [list] - foreach alt $type_alternatives { - set firstword [lindex $alt 0] - set lc_firstword [tcl::string::tolower $firstword] - #normalize here so we don't have to test during actual args parsing in main function - set normtype "" ;#assert - should be overridden in all branches of switch - switch -- $lc_firstword { - int - integer {set normtype int} - double - float { - #review - user may wish to preserve 'float' in help display - consider how best to implement - set normtype double - } - bool - boolean {set normtype bool} - char - character {set normtype char} - dict - dictionary {set normtype dict} - index - indexexpression {set normtype indexexpression} - indexset {set normtype indexset} - "" - none - solo { - if {$is_opt} { - #review - are we allowing clauses for flags? - #e.g {-flag -type {int int}} - #this isn't very tcl like, where we'd normally mark the flag with -multiple true and - # instead require calling as: -flag -flag - #It seems this is a reasonably rare/unlikely requirement in most commandline tools. - - if {[llength $specval] > 1} { - #makes no sense to have 'none' in a clause - error "punk::args::resolve - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" - } - #tcl::dict::set spec_merged -type none - set normtype none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - } else { - #solo only valid for flags - error "punk::args::resolve - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" - } - } - any - anything {set normtype any} - unknown { - #'unspecified' ?? - set normtype unknown - } - ansi - ansistring {set normtype ansistring} - string - globstring {set normtype $lc_firstword} - literal { - #value was split out by _split_type_expression - set normtype literal([lindex $alt 1]) - } - literalprefix { - set normtype literalprefix([lindex $alt 1]) - } - stringstartswith { - set normtype stringstartswith([lindex $alt 1]) - } - stringendswith { - set normtype stringendswith([lindex $alt 1]) - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - #tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - #todo - set normtype $alt - } - } - lappend normlist $normtype - } - set norms [join $normlist |] - if {$optional_clausemember} { - lappend typelist ?$norms? - } else { - lappend typelist $norms - } - } - tcl::dict::set spec_merged -type $typelist - } - -typesynopsis { - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount != [llength $specval]} { - error "punk::args::resolve - invalid -typesynopsis specification for argument '$argname'. -typesynopsis has [llength $specval] entries, but requires $typecount entries (one for each entry in -types. Use empty string list members for default) @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -typesynopsis $specval - } - -parsekey - -group { - tcl::dict::set spec_merged -typesynopsis $specval - } - -solo - - -choices - -choicegroups - -choicemultiple - -choicecolumns - - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -range { - #allow simple case to be specified without additional list wrapping - #only multi-types require full list specification - #arg1 -type int -range {0 4} - #arg2 -type {int string} -range {{0 4} {"" ""}} - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount == 1} { - tcl::dict::set spec_merged -typeranges [list $specval] - } else { - error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -range only applies to single-item type. Use -typeranges instead. @id:$DEF_definition_id" - } - } - -typeranges { - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount != [llength $specval]} { - error "punk::args::resolve - invalid -typeranges specification for argument '$argname'. -typeranges has [llength $specval] entries, but requires $typecount entries (one for each entry in -types) @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -typeranges $specval - } - -default { - #The -default is for when the entire clause is missing - #It doesn't necessarily have to have the same number of elements as the clause {llength $typelist} - #review - tcl::dict::set spec_merged -default $specval - if {![dict exists $argdef_values -optional]} { - tcl::dict::set spec_merged -optional 1 - } - } - -defaultdisplaytype { - tcl::dict::set spec_merged -defaultdisplaytype $specval - } - -typedefaults { - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount != [llength $specval]} { - error "punk::args::resolve - invalid -typedefaults specification for argument '$argname'. -typedefaults has [llength $specval] entries, but requires $typecount entries (one for each entry in -types) @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -typedefaults $specval - } - -optional { - #applies to whole arg - not each -type - tcl::dict::set spec_merged -optional $specval - } - -ensembleparameter { - #applies to whole arg - not each -type - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #applies to whole arg - not each -type - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::resolve - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -command - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::resolve - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - #TODO! - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::resolve argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::resolve argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list\ - -form -type\ - -parsekey -group\ - -range -typeranges\ - -default -defaultdisplaytype -typedefaults\ - -minsize -maxsize -choices -choicegroups\ - -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::resolve - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize - if {$argname eq "--"} { - #force -type none - in case no -type was specified and @opts -type is some other default such as string - tcl::dict::set spec_merged -type none - } - if {[tcl::dict::get $spec_merged -type] eq "none"} { - dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] - } - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - #if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} {} - if {![tcl::dict::get $spec_merged -optional]} { - if {$is_opt} { - set temp_opt_required [dict get $F $fid OPT_REQUIRED] - lappend temp_opt_required $argname - dict set F $fid OPT_REQUIRED $temp_opt_required - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - } else { - set temp_val_required [dict get $F $fid VAL_REQUIRED] - lappend temp_val_required $argname - dict set F $fid VAL_REQUIRED $temp_val_required - } - } - } - - - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - #JJJ - set parsekey [dict get $F $fid ARG_INFO $argname -default] - if {$parsekey eq ""} { - set parsekey $argname - } - tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #now cycle through ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - if {[tcl::dict::get $F $fid OPT_MAX] eq ""} { - if {[llength [tcl::dict::get $F $fid OPT_NAMES]] == 0 && ![tcl::dict::get $F $fid OPT_ANY]} { - tcl::dict::set F $fid OPT_MAX 0 ;#aid in parsing to avoid scanning for opts unnecessarily - #review - when using resolved_def to create a definiation based on another - OPT_MAX may need to be overridden - a bit ugly? - } - } - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - disallow any -multiple == true entries if any leaders have -multiple == true? - #(creates parsing ambiguity) - #ambiguity could be resolved if at least one required option/flag eg -- - #ambiguities could theoretically also be resolved with required literals or choices - or even based on argument type - #(overcomplex? todo see if any core/tcllib commands work like that) - - #only allow a single entry within VAL_NAMES to have -multiple == true - #example of command with non-trailing -multiple == true is core command: 'file copy ?-force? ?--? source ?source?... targetDir - set val_multiples 0 - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - if {$val_multiples > 0} { - error "bad setting -multiple true on argument spec for value '$valname' in command form:'$fid'. Only a single value argument specification can be marked with -multiple true @id:$DEF_definition_id" - } - incr val_multiples - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata LEADERSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata OPTSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata VALSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - seealso_info $seealso_info\ - keywords_info $keywords_info\ - examples_info $examples_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - form_info $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache [list $optionspecs] $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @examples @formdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @examples @formdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - #set fullk [tcl::prefix::match -error "" {-return -form -types -antiglobs -override} $k] - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @examples @seealso} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - - #todo @formdisplay - #todo @ref ? - - - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $directive [dict get $specdict FORMS $formname $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "\"$m\" [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "\"$m\" $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @examples - @seealso { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - #todo @formdisplay - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $type [dict get $specdict FORMS $formname $defaults_key] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "\"$m\" [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "\"$m\" $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$deflist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set deflist [raw_def $id] - if {$deflist eq ""} { - return - } - return [resolve {*}$deflist] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of first 3 blocks - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef {lvl 1}} { - set id "" - set found_id_line 0 - foreach d [lrange $rawdef 0 2] { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - set found_id_line 1 - #review - uplevel 2 would be a call from punk::args::define ?? - set rest [uplevel $lvl [list punk::args::lib::tstr -allowcommands $rest]] - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - } - break - } - } - } - if {$found_id_line} { - break - } - } - if {$id eq "" && $found_id_line} { - #Looked like an @id - but presumable the rest of the line was malformed. - #we won't produce an autoid for such a definition. - set first3blocks "" - foreach b [lrange $rawdef 0 2] { - append first3blocks $b\n - } - error "punk::args::rawdef_id found an @id line in the first 4 lines of one of the 1st 3 blocks - but failed to retrieve a value for it.\nraw_def 1st 3 blocks:\n$first3blocks" - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc aliases {} { - variable aliases - punk::lib::showdict $aliases - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - lappend PUNKARGS [list { - @id -id ::punk::args::update_definitions - @cmd -name punk::args::update_definitions\ - -summary\ - ""\ - -help\ - "" - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - #set id [rawdef_id $definitionlist] - set lvl 1 ;#level at which tstr substitution occurs in @id line - set id [namespace eval $pkgns [list punk::args::rawdef_id $definitionlist $lvl]] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {[string match (autodef)* $pkgns]} { - set pkgns [string range $pkgns 9 end] - } - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - set depth [info level] - set maxd [expr {min($depth,4)}] - set call_level [expr {-1 * $maxd}] - #set call_level -3 ;#for get_dict call - #set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - set maxloop 10 ;#failsafe - while {[string last \n $cmdinfo] >= 1 && $maxloop > -1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - incr maxloop -1 - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -parsedargs -type dict -help\ - "Result of successful punk::pargs::parse - (currently only looks at 'received')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - -form -default 0 -help\ - "Ordinal index or name of command form" - }] ] - - - variable arg_error_CLR - array set arg_error_CLR {} - variable arg_error_CLR_nocolour - array set arg_error_CLR_nocolour {} - variable arg_error_CLR_info - array set arg_error_CLR_info {} - variable arg_error_CLR_error - array set arg_error_CLR_error {} - - proc _argerror_load_colours {{forcereload 0}} { - variable arg_error_CLR - #todo - option for reload/retry? - if {!$forcereload && [array size arg_error_CLR] > 0} { - return - } - - if {[catch {package require punk::ansi} errMsg]} { - puts stderr "punk::args FAILED to load punk::ansi\n$errMsg" - proc ::punk::args::a {args} {} - proc ::punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #array set arg_error_CLR {} - set arg_error_CLR(testsinglecolour) [a+ yellow] ;#A single SGR colour to test current colour on|off state (empty string vs some result - used to determine if forcereload required) - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(linebase) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] - variable arg_error_CLR_nocolour - #array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" - variable arg_error_CLR_info - #array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error - #array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] - } - - - #bas ic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::parse {} withdef {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - #set arg_error_CLR(testsinglecolour) [a+ brightred] - upvar ::punk::args::arg_error_CLR CLR - set forcereload 0 ;#no need for forcereload to be true for initial run - empty array will trigger initial load - if {[info exists CLR(testsinglecolour)]} { - set terminal_colour_is_on [expr {[string length [a+ yellow]]}] - set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] - if {$terminal_colour_is_on ^ $error_colour_is_on} { - #results differ - set forcereload 1 - } - } - _argerror_load_colours $forcereload - - if {[llength $args] %2 != 0} { - set arg_error_isrunning 0 - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - #----------------------- - #todo!! make changeable from config file - #JJJ 2025-07-16 - set returntype table ;#table as string - #set returntype string - #---------------------- - set as_error 1 ;#usual case is to raise an error - set scheme error - set form 0 - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme -form} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -parsedargs { - #todo? - dict for {set setinfo} $v { - switch -- $set { - received { - foreach {r rpos} $setinfo { - if {$r ni $goodargs} { - lappend goodargs $r - } - } - } - } - } - set parsedargs $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - -form { - set form $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - set formnames [dict get $spec_dict form_names] - if {[string is integer -strict $form]} { - if {$form < 0 || $form > [llength $formnames]-1} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - set selected_forms [list [lindex $formnames $form]] - } else { - if {$form eq "*"} { - set selected_forms $formnames - } else { - if {$form in $formnames} { - set selected_forms [list $form] - } else { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - } - } - - - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - - switch -- $scheme { - nocolour { - variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour] - } - info { - variable arg_error_CLR_info - array set CLR [array get arg_error_CLR_info] - } - error { - variable arg_error_CLR_error - array set CLR [array get arg_error_CLR_error] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[0m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdsummary [Dict_getdef $spec_dict cmd_info -summary ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - #review - when can there be more than one selected form? - set argdisplay_header "" - set argdisplay_body "" - if {[llength $selected_forms] == 1} { - set fid [lindex $selected_forms 0] - set FRM [dict get $spec_dict FORMS $fid] - if {[dict size [dict get $FRM FORMDISPLAY]]} { - set argdisplay_header [Dict_getdef $FRM FORMDISPLAY -header ""] - set argdisplay_body [Dict_getdef $FRM FORMDISPLAY -body ""] - } - } - - - # if {![dict size $F $fid $FORMDISPLAY]} {} - #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - #set is_custom_argdisplay 0 - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - #set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - set cmdhelp_display [punk::ansi::ansiwrap_raw $CLR(linebase_header) "" "" $cmdhelp] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "# [Dict_getdef $spec_dict cmd_info -summary {}]\n" - set form_info [dict get $spec_dict form_info] - dict for {fid finfo} $form_info { - set form_synopsis [Dict_getdef $finfo -synopsis ""] - if {$form_synopsis eq ""} { - #todo - set form_synopsis [punk::args::synopsis -noheader -form $fid [dict get $spec_dict id]] - set ansifree_synopsis [punk::ansi::ansistripraw $form_synopsis] - if {[string length $ansifree_synopsis] > 90} { - # - set form_synopsis [punk::args::synopsis -noheader -return summary -form $fid [dict get $spec_dict id]] - } - #review - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - } - if {$fid in $selected_forms} { - set form_synopsis [punk::ansi::a+ underline]$form_synopsis[punk::ansi::a+ nounderline] - } - append synopsis $form_synopsis \n - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - #set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space (zwsp) - set A_PREFIXEND [a+ nounderline] - #review - zwsp problematic on older terminals that print it visibly - #- especially if they also lie about cursor position after it's emitted. - #so although the zwsp fixes the issue where the underline extends to rhs padding if all text was underlined, - #It's probably best fixed in the padding functionality. - } else { - set A_PREFIXEND $RST - } - - #TODO - foreach fid - set fid [lindex $selected_forms 0] - set form_dict [dict get $spec_dict FORMS $fid] - - set opt_names [list] - set opt_names_display [list] - set opt_names_hints [list] ;#comments in first column below name display. - set lookup_optset [dict create] - if {[llength [dict get $form_dict OPT_NAMES]]} { - set all_opts [list] - foreach optionset [dict get $form_dict OPT_NAMES] { - #e.g1 "-alias1|-realname" - #e.g2 "-f|--filename" (fossil longopt style) - #e.g3 "-f|--filename=" (gnu longopt style) - set optmembers [split $optionset |] - lappend all_opts {*}$optmembers - foreach o $optmembers { - dict set lookup_optset $o $optionset - #goodargs - } - } - set full_goodargs [list] - #goodargs may have simplified entries for received opts of form -alias1|-alias2|-realname - #map -realname to full argname - foreach g $goodargs { - if {[string match -* $g] && [dict exists $lookup_optset $g]} { - lappend full_goodargs [dict get $lookup_optset $g] - } else { - lappend full_goodargs $g - } - } - set goodargs $full_goodargs - if {![catch {package require punk::trie}]} { - #todo - reservelist for future options - or just to affect the prefix calculation - # (similar to -choiceprefixreservelist) - - set trie [punk::trie::trieclass new {*}$all_opts --] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach optset [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $optset] - set parsekey [dict get $arginfo -parsekey] - set storageinfo "" - if {$parsekey ne "" && $parsekey ne $optset} { - set storageinfo "(stored as: $parsekey)" - } - if {[dict get $arginfo -prefix]} { - set opt_members [split $optset |] - set odisplay [list] - foreach opt $opt_members { - set id [dict get $idents $opt] - #REVIEW - if {$id eq $opt} { - set prefix $opt - set tail "" - } else { - set idlen [string length $id] - lassign [punk::lib::string_splitbefore $opt $idlen] prefix tail - } - lappend odisplay $A_PREFIX$prefix$A_PREFIXEND$tail - } - #lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - lappend opt_names_display [join $odisplay |] - } else { - lappend opt_names_display $optset - } - lappend opt_names_hints $storageinfo - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $optset - } - } else { - set opt_names [dict get $form_dict OPT_NAMES] - foreach optset [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $optset] - set parsekey [dict get $arginfo -parsekey] - set storageinfo "" - if {$parsekey ne "" && $parsekey ne $optset} { - set storageinfo "(stored as: $parsekey)" - } - lappend opt_names_display $optset - lappend opt_names_hints $storageinfo - } - #set opt_names_display $opt_names - } - } - set leading_val_names [dict get $form_dict LEADER_NAMES] - set trailing_val_names [dict get $form_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $form_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set leading_val_names_hints {} - set trailing_val_names_display $trailing_val_names - set trailing_val_names_hints {} - #puts "--> parsedargs: $parsedargs" - set parsed_leaders [Dict_getdef $parsedargs leaders {}] - set parsed_opts [Dict_getdef $parsedargs opts {}] - set parsed_values [Dict_getdef $parsedargs values {}] - - #display options first then values - foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names $parsed_leaders] [list opts $opt_names_display $opt_names_hints $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames parsedvalues - set lastgroup "" - set lastgroup_parsekey "" - foreach argshow $argnames_display hint $argnames_hints arg $argnames { - set arginfo [dict get $form_dict ARG_INFO $arg] - - if {$argumentclass eq "opts"} { - set thisgroup [dict get $arginfo -group] - if {$thisgroup ne $lastgroup} { - if {[dict exists $form_dict OPT_GROUPS $thisgroup -parsekey]} { - set thisgroup_parsekey [dict get $form_dict OPT_GROUPS $thisgroup -parsekey] - } else { - set thisgroup_parsekey "" - } - - #footer/line? - if {$use_table} { - $t add_row [list " " "" "" "" ""] - } else { - lappend errlines " " - } - - if {$thisgroup eq ""} { - } else { - #SHOW group 'header' (not really a table header - just another row) - set help "" - if {[dict exists $form_dict OPT_GROUPS $thisgroup -help]} { - set help [dict get $form_dict OPT_GROUPS $thisgroup -help] - } - if {$thisgroup_parsekey eq ""} { - set groupinfo "(documentation group)" - } else { - set groupinfo "(common flag group)\nkey:$thisgroup_parsekey" - } - if {$use_table} { - $t add_row [list " $thisgroup" $groupinfo "" "" $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" - set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] - append arghelp \n - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - set lastgroup $thisgroup - set lastgroup_parsekey $thisgroup_parsekey - } - if {[dict exists $arginfo -parsekey]} { - set mypkey [dict get $arginfo -parsekey] - if {$mypkey eq "$lastgroup_parsekey" || $mypkey eq [string trimright [lindex [split $arg |] end] =]} { - set hint "" - } - } - } - - if {[dict exists $arginfo -default]} { - #default isn't necessarily of same type as -type required for validation - #Guessing at the type from the data is likely to be unsatisfactory. - - set defaultdisplaytype [Dict_getdef $arginfo -defaultdisplaytype string] - switch -- $defaultdisplaytype { - dict { - #single level - set rawdefault [dict get $arginfo -default] - set default "{\n" - dict for {k v} $rawdefault { - append default " \"$k\" \"$v\"\n" - } - append default "}" - } - list { - set default "{\n" - foreach v $rawdefault { - append default " \"$v\"\n" - } - append default "}" - } - default { - #set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - set default "'[dict get $arginfo -default]'" - } - } - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - set choiceprefixreservelist [Dict_getdef $arginfo -choiceprefixreservelist {}] ;#names used to calc prefix - but not available as actual choice. - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - #review - does choiceprefixdenylist need to be added? - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_prefixcalc [list {*}[string tolower $allchoices_originalcase] {*}$choiceprefixreservelist] - } else { - set casemsg " (case sensitive)" - set allchoices_prefixcalc [list {*}$allchoices_originalcase {*}$choiceprefixreservelist] - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help "Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_prefixcalc] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - lassign [punk::lib::string_splitbefore $c [string length $shortestid]] prefix tail - #if {$shortestid eq $c} { - # set prefix $c - # set tail "" - #} else { - # set idlen [string length $shortestid] - # set prefix [string range $c 0 $idlen-1] - # set tail [string range $c $idlen end] - #} - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - append help \n[textblock::join -- " " [$obj print]] - #------------- - #todo - tests - #see special case double reset at end of content in textblock class table get_column_by_index - #bug fixed - needed to ensure last two resets were actually concurrent and at end. - #append help "\nbase[a+ green]ab\nc[a]base" ;#ok - #vs - #append help "\nbase[a+ green]a[a]b\nc[a]base" ;#not ok - #------------- - - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -typeranges]} { - set ranges [dict get $arginfo -typeranges] - if {[llength $ranges] == 1} { - append typeshow \n "-range [lindex [dict get $arginfo -typeranges] 0]" - } else { - append typeshow \n "-ranges" - foreach r $ranges { - append typeshow " {$r}" - } - } - } - - if {$use_table} { - if {$hint ne ""} { - set col1 $argshow\n$hint - } else { - set col1 $argshow - } - $t add_row [list $col1 $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set linetail " TYPE:$typeshow DEFAULT:$default MULTI:$multiple" - if {$hint ne ""} { - set arghelp [textblock::join -- "[a+ bold]$argshow\n$hint$RST" $linetail] - } else { - set arghelp "[a+ bold]$argshow$RST $linetail" - } - append arghelp \n - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - - # ------------------------------------------------------------------------------------------------------- - # if the argument class can accept unnamed arguments (or if opts accepts unspecified flags) - display an indication - # ------------------------------------------------------------------------------------------------------- - switch -- $argumentclass { - leaders - values { - if {$argumentclass eq "leaders"} { - set class_unnamed LEADER_UNNAMED - set class_max LEADER_MAX - set class_required LEADER_REQUIRED - set class_directive_defaults LEADERSPEC_DEFAULTS - } else { - set class_unnamed VAL_UNNAMED - set class_max VAL_MAX - set class_required VAL_REQUIRED - set class_directive_defaults VALSPEC_DEFAULTS - } - if {[dict get $form_dict $class_unnamed]} { - set valmax [dict get $form_dict $class_max] - #set valmin [dict get $form_dict VAL_MIN] - if {$valmax eq ""} { - set valmax -1 - } - if {$valmax == -1} { - set possible_unnamed -1 - } else { - set possible_unnamed [expr {$valmax - [llength [dict get $form_dict $class_required]]}] - if {$possible_unnamed < 0} { - set possible_unnamed 0 - } - } - if {$possible_unnamed == -1 || $possible_unnamed > 0} { - #Note 'multiple' is always empty here as each unnamed is assigned to its own positional index - if {$possible_unnamed == 1} { - set argshow ?? - } else { - set argshow ?...? - } - set tp [dict get $form_dict $class_directive_defaults -type] - if {[dict exists $form_dict $class_directive_defaults -default]} { - set default [dict get $form_dict $class_directive_defaults -default] - } else { - set default "" - } - if {$use_table} { - $t add_row [list "$argshow" $tp $default "" ""] - } else { - set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n" - lappend errlines $arghelp - } - } - } - } - opts { - #display row to indicate if -any|-arbitrary true - - #review OPTSPEC_DEFAULTS -multiple ? - if {[dict get $form_dict OPT_ANY]} { - set argshow "?...?" - set tp [dict get $form_dict OPTSPEC_DEFAULTS -type] - if {[dict exists $form_dict OPTSPEC_DEFAULTS -default]} { - set default [dict get $form_dict OPTSPEC_DEFAULTS -default] - } else { - set default "" - } - if {$use_table} { - $t add_row [list "$argshow" $tp $default "" ""] - } else { - set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n" - lappend errlines $arghelp - } - } - } - } - - } ;#end foreach argumentclass - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - catch {$t destroy} - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } else { - #put original error at bottom of table too - append result \n $msg - } - } else { - set result $errmsg - append result \n $msg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::cmdhelp (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - }\ - {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}}\ - {${[punk::args::resolved_def -types opts ::punk::args::resolved_def -form]}}\ - { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict $definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance - #todo - configurable per interp/namespace - -errorstyle -type string -default enhanced -choices {enhanced standard basic minimal} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - @values -max 2 - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - #puts "punk::args::parse --> '$args'" - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - #error "punk::args::parse - invalid call. < 3 args" - punk::args::parse $args withid ::punk::args::parse - } - set opts_and_vals $args - set parseargs [lpop opts_and_vals 0] - - set opts [list] - set values [list] - for {set i 0} {$i < [llength $opts_and_vals]} {incr i} { - if {[string match -* [lindex $opts_and_vals $i]]} { - if {[catch { - lappend opts [lpop opts_and_vals 0] [lpop opts_and_vals 0] - }]} { - #unhappy path - not enough options - #review - which form of punk::args::parse? - #we expect this to always raise error - review - set result [punk::args::parse $args withid ::punk::args::parse] - puts stderr "punk::args::parse unexpected result $result" - return ;#failsafe - } - incr i -1 - #lappend opts $a [lindex $opts_and_vals $i] - } else { - break - } - } - #set values [lrange $opts_and_vals $i end] - set values $opts_and_vals - #puts "---values: $values" - set tailtype [lindex $values 0] - set tailargs [lrange $values 1 end] - - - #set split [lsearch -exact $tailargs withid] - #if {$split < 0} { - # set split [lsearch -exact $tailargs withdef] - # if {$split < 0} { - # #punk::args::usage arg_error? - # #error "punk::args::parse - invalid call. keyword withid|withdef required" - # punk::args::parse $args withid ::punk::args::parse - # } else { - # set tailtype withdef - #} - #} else { - # set tailtype withid - #} - #set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - - - #if {[llength $opts] % 2} { - #error "punk::args::parse Even number of -flag val pairs required after arglist" - #} - - #Default the -errorstyle to standard - # (slow on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle standard\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - #puts "def: $defaultopts opts: $opts" - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - #error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - punk::args::parse $args withid ::punk::args::parse - } - } - } - switch -- $tailtype { - withid { - if {[llength $tailargs] != 1} { - #error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - punk::args::parse $args withid ::punk::args::parse - } - set id [lindex $tailargs 0] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - #puts stdout "punk::args::parse '$parseargs' withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist $tailargs - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - #puts stdout "punk::args::parse '$parseargs' with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist was '$tailtype'. Must be 'withid' or 'withdef'" - } - } - try { - #puts stdout "parse --> get_dict $parseargs -form [dict get $opts -form]" - set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - set ecode [dict get $erroropts -errorcode] - #punk ecode is of form PUNKARGS VALIDATION {description..} -key val ... - set msg [string map [list %caller% [Get_caller]] $msg] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - - #return number of values we can assign to cater for variable length clauses such as {"elseif" expr "?then?" body} - #review - efficiency? each time we call this - we are looking ahead at the same info - proc _get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { - set ARG_INFO [dict get $formdict ARG_INFO] - set all_remaining [lrange $values $idx end] - set thisname [lindex $names $nameidx] - set thistype [dict get $ARG_INFO $thisname -type] - set tailnames [lrange $names $nameidx+1 end] - - #todo - work backwards with any (optional or not) literals at tail that match our values - and remove from assignability. - set ridx 0 - foreach clausename [lreverse $tailnames] { - #puts "=============== clausename:$clausename all_remaining: $all_remaining" - set typelist [dict get $ARG_INFO $clausename -type] - if {[lsearch $typelist literal*] == -1} { - break - } - set max_clause_length [llength $typelist] - if {$max_clause_length == 1} { - #basic case - set alloc_ok 0 - #set v [lindex $values end-$ridx] - set v [lindex $all_remaining end] - set tp [lindex $typelist 0] - # ----------------- - set tp [string trim $tp ?] ;#shouldn't be necessary - #review - ignore ?literal? and ?literal(xxx)? when clause-length == 1? (should raise error during definition instead? - #we shouldn't have an optional clause member if there is only one member - the whole argument should be marked -optional true instead. - # ----------------- - - #todo - support complex type members such as -type {{literal a|b} int OR} - #for now - require llength 1 - simple type such as -type {literal(ab)|int} - if {[llength $tp] !=1} { - error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" - } - - #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { - switch -exact -- [lindex $tp_alternative 0] { - literal { - set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) - set match [lindex $tp_alternative 1] - if {$v eq $match} { - set alloc_ok 1 - ledit all_remaining end end - if {![dict get $ARG_INFO $clausename -multiple]} { - ledit tailnames end end - } - #the type (or one of the possible type alternates) matched a literal - break - } - } - stringstartswith { - set pfx [lindex $tp_alternative 1] - if {[string match "$pfx*" $v} { - set alloc_ok 1 - set alloc_ok 1 - ledit all_remaining end end - if {![dict get $ARG_INFO $clausename -multiple]} { - ledit tailnames end end - } - break - } - - } - stringendswith { - set sfx [lindex $tp_alternative 1] - if {[string match "*$sfx" $v} { - set alloc_ok 1 - set alloc_ok 1 - ledit all_remaining end end - if {![dict get $ARG_INFO $clausename -multiple]} { - ledit tailnames end end - } - break - } - - } - default {} - } - } - if {!$alloc_ok} { - if {![dict get $ARG_INFO $clausename -optional]} { - break - } - } - - } else { - #todo - use _split_type_expression - - - #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) - #This is better caught during definition. - #e.g rn = {elseif expr (?then?) body} typelist = {literal expr ?literal? script} - #set cvals [lrange $values end-$ridx end-[expr {$ridx + $max_clause_length-1}]] - set cvals [lrange $values end-[expr {$ridx + $max_clause_length-1}] end-$ridx] - set rcvals [lreverse $cvals] - set alloc_count 0 - #clause name may have more entries than types - extras at beginning are ignored - set rtypelist [lreverse $typelist] - set alloc_ok 0 - set reverse_type_index 0 - #todo handle type-alternates - # for example: -type {string literal(x)|literal(y)} - foreach tp $rtypelist { - #set rv [lindex $rcvals end-$alloc_count] - set rv [lindex $all_remaining end-$alloc_count] - if {[string match {\?*\?} $tp]} { - set clause_member_optional 1 - } else { - set clause_member_optional 0 - } - set tp [string trim $tp ?] - switch -glob $tp { - literal* { - set litinfo [string range $tp 7 end] - set match [string range $litinfo 1 end-1] - #todo -literalprefix - if {$rv eq $match} { - set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok - incr alloc_count - } else { - if {$clause_member_optional} { - # - } else { - set alloc_ok 0 - break - } - } - } - "stringstartswith(*" { - set pfx [string range $tp 17 end-1] - if {[string match "$pfx*" $tp]} { - set alloc_ok 1 - incr alloc_count - } else { - if {!$clause_member_optional} { - set alloc_ok 0 - break - } - } - } - default { - if {$clause_member_optional} { - #review - optional non-literal makes things harder.. - #we don't want to do full type checking here - but we now risk allocating an item that should actually - #be allocated to the previous value - # todo - lsearch to next literal or non-optional? - set prev_type [lindex $rtypelist $reverse_type_index+1] - if {[string match literal* $prev_type]} { - set litinfo [string range $prev_type 7 end] - #todo -literalprefix - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - set match [lindex $rclausename $reverse_type_index+1] - } - if {$rv ne $match} { - #current val doesn't match previous type - allocate here - incr alloc_count - } - } else { - #no literal to anchor against.. - incr alloc_count - } - } else { - #allocate regardless of type - we're only matching on arity and literal positioning here. - #leave final type-checking for later. - incr alloc_count - } - } - } - incr reverse_type_index - } - if {$alloc_ok && $alloc_count > 0} { - #set n [expr {$alloc_count -1}] - #set all_remaining [lrange $all_remaining end-$n end] - set all_remaining [lrange $all_remaining 0 end-$alloc_count] - #don't lpop if -multiple true - if {![dict get $ARG_INFO $clausename -multiple]} { - #lpop tailnames - ledit tailnames end end - } - } else { - break - } - } - incr ridx - } - set num_remaining [llength $all_remaining] - - if {[dict get $ARG_INFO $thisname -optional] || ([dict get $ARG_INFO $thisname -multiple] && $thisname in $namesreceived)} { - #todo - check -multiple for required min/max (not implemented: make -multiple accept ?) - #thisname already satisfied, or not required - set tail_needs 0 - foreach t $tailnames { - if {![dict get $ARG_INFO $t -optional]} { - set min_clause_length [llength [lsearch -all -not [dict get $ARG_INFO $t -type] {\?*\?}]] - incr tail_needs $min_clause_length - } - } - set all_remaining [lrange $all_remaining 0 end-$tail_needs] - } - - #thistype - set alloc_ok 1 ;#default assumption only - set alloc_count 0 - set resultlist [list] - set n [expr {[llength $thistype]-1}] - set tpidx 0 - set newtypelist $thistype - set has_choices [expr {[tcl::dict::exists $ARG_INFO $thisname -choices] || [tcl::dict::exists $ARG_INFO $thisname -choicegroups]}] - foreach tp $thistype { - #usual case is a single tp (basic length-1 clause) - but tp may commonly have alternates eg int|literal(xxx) - set v [lindex $all_remaining $alloc_count] - if {[string match {\?*\?} $tp]} { - set clause_member_optional 1 - } else { - set clause_member_optional 0 - } - set tp [string trim $tp ?] - - set member_satisfied 0 - if {$has_choices} { - #each tp in the clause is just for validating a value outside the choice-list when -choicerestricted 0 - set member_satisfied 1 - } - - - if {!$member_satisfied} { - #----------------------------------------------------------------------------------- - #first build category lists of any literal,literalprefix,stringstartwith,other - # - set ctg_literals [list] - set ctg_literalprefixes [list] - set ctg_stringstartswith [list] - set ctg_stringendswith [list] - set ctg_other [list] - #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { - #JJJJ - lassign $tp_alternative t textra - switch -exact -- $t { - literal { - lappend ctg_literals $textra - } - literalprefix { - lappend ctg_literalprefixes $textra - } - stringstartswith { - lappend ctg_stringstartswith $textra - } - stringendswith { - lappend ctg_stringendswith $textra - } - default { - lappend ctg_other $tp_alternative - } - } - } - #----------------------------------------------------------------------------------- - if {[llength $ctg_other] > 0} { - #presence of any ordinary type as one of the alternates - means we consider it a match if certain basic types align - #we don't do full validation here -leave main validation for later (review) - foreach tp_alternative $ctg_other { - switch -exact -- $tp_alternative { - int { - if {[string is integer -strict $v]} { - set member_satisfied 1 - break - } - } - double { - if {[string is double -strict $v]} { - set member_satisfied 1 - break - } - } - bool { - if {[string is boolean -strict $v]} { - set member_satisfied 1 - break - } - } - number { - if {[string is integer -strict $v] || [string is double -strict $v]} { - set member_satisfied 1 - break - } - } - dict { - if {[punk::args::lib::string_is_dict $v]} { - set member_satisfied 1 - break - } - } - default { - #REVIEW!!! - #can get infinite loop in get_dict if not satisfied - unstoppable until memory exhausted. - #todo - catch/detect in caller - set member_satisfied 1 - break - } - } - } - } - } - - if {!$member_satisfied && ([llength $ctg_literals] || [llength $ctg_literalprefixes])} { - if {$v in $ctg_literals} { - set member_satisfied 1 - lset newtypelist $tpidx validated-$tp - } else { - #ctg_literals is included in the prefix-calc - but a shortened version of an entry in literals is not allowed - #(exact match would have been caught in other branch of this if) - #review - how does ctg_stringstartswith affect prefix calc for literals? - set full_v [tcl::prefix::match -error "" [list {*}$ctg_literals {*}$ctg_literalprefixes] $v] - if {$full_v ne "" && $full_v ni $ctg_literals} { - #matched prefix must be for one of the entries in ctg_literalprefixes - valid - set member_satisfied 1 - set v $full_v ;#map prefix given as arg to the full literalprefix value - lset newtypelist $tpidx validated-$tp - } - } - } - if {!$member_satisfied && [llength $ctg_stringstartswith]} { - foreach pfx $ctg_stringstartswith { - if {[string match "$pfx*" $v]} { - set member_satisfied 1 - lset newtypelist $tpidx validated-$tp - #review. consider multi-word typespec with RPN? - # {*}$tp_alternative validated - break - } - } - } - if {!$member_satisfied && [llength $ctg_stringendswith]} { - foreach pfx $ctg_stringendswith { - if {[string match "*$pfx" $v]} { - set member_satisfied 1 - lset newtypelist $tpidx validated-$tp - break - } - } - } - - - - if {$member_satisfied} { - if {$clause_member_optional && $alloc_count >= [llength $all_remaining]} { - if {[dict exists $ARG_INFO $thisname -typedefaults]} { - set d [lindex [dict get $ARG_INFO $thisname -typedefaults] $tpidx] - lappend resultlist $d - lset newtypelist $tpidx ?defaulted-$tp? - } else { - lset newtypelist $tpidx ?omitted-$tp? - lappend resultlist "" - } - } else { - #may have satisfied one of the basic type tests above - lappend resultlist $v - incr alloc_count - } - } else { - if {$clause_member_optional} { - if {[dict exists $ARG_INFO $thisname -typedefaults]} { - set d [lindex [dict get $ARG_INFO $thisname -typedefaults] $tpidx] - lappend resultlist $d - lset newtypelist $tpidx ?defaulted-$tp? - } else { - lappend resultlist "" - lset newtypelist $tpidx ?omitted-$tp? - } - } else { - set alloc_ok 0 - } - } - - if {$alloc_count > [llength $all_remaining]} { - set alloc_ok 0 - break - } - incr tpidx - } - - #?omitted-*? and ?defaulted-*? in typelist are a way to know which elements in the clause were missing/defaulted - #so that they are not subject to type validation - #such elements shouldn't be subject to validation - if {$alloc_ok} { - #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" - set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] - } else { - puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" - set d [dict create consumed 0 resultlist {} typelist $thistype] - } - #puts ">>>> _get_dict_can_assign_value $d" - return $d - } - - #_split_type_expression - #only handles toplevel 'or' for type_expression e.g int|char - #we have no mechanism for & - (although it would be useful) - #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) - #or perhaps more performant, RPN to avoid bracket parsing - #if literal(..), literalprefix(..), stringstartswith(..) etc can have pipe symbols and brackets etc - we can't just use split - #if we require -type to always be treated as a list - and if an element is length 1 - require it to - #have properly balanced brackets that don't contain | ( ) etc we can simplify - REVIEW - - #consider: - #1 basic syntax - only OR supported - limits on what chars can be put in 'textn' elements. - #mode -type literalprefix(text1)|literalprefix(text2) -optional 1 - #2 expanded syntax - supports arbitrary chars in 'textn' elements - but still doesn't support more complex OR/AND logic - #mode -type {{literalprefix text1 | literalprefix text2}} - #3 RPN (reverse polish notation) - somewhat unintuitive, but allows arbitrary textn, and complex OR/AND logic without brackets. - #(forth like - stack based definition of types) - #mode -type {literalprefix text1 literalprefix text2 OR} - #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - - proc _split_type_expression {type_expression} { - if {[llength $type_expression] == 1} { - #simple expressions of length one must be splittable on | - #disallowed: things such as literal(|) or literal(x|etc)|int - #these would have to be expressed as {literal |} and {literal x|etc | int} - set or_type_parts [split $type_expression |] - set type_alternatives [list] - foreach t $or_type_parts { - if {[regexp {([^\(^\)]*)\((.*)\)$} $t _ name val]} { - lappend type_alternatives [list $name $val] - } else { - lappend type_alternatives $t - } - } - return $type_alternatives - } else { - error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" - #todo - #RPN reverse polish notation - #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - #equivalent logic: ((stringstartswith(x)|stringstartswith(y))&stringendswith(z))|int - # {int ; stringstartswith x stringstartswith y OR } - - #experimental.. seems like a pointless syntax. - #may as well just use list of lists with |(or) as the intrinsic operator instead of parsing this - #e.g {stringstartswith x | literal | | int} - set type_alternatives [list] - set expect_separator 0 - for {set w 0} {$w < [llength $type_expression]} {incr w} { - set word [lindex $type_expression $w] - if {$expect_separator} { - if {$word eq "|"} { - #pipe could be last entry - not strictly correct, but can ignore - set expect_separator 0 - continue - } else { - error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" - } - } - switch -exact -- $word { - literal - literalprefix - stringstartswith - stringendswith - stringcontains { - if {$w+1 > [llength $type_expression]} { - #premature end - no arg available for type which requires one - error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" - } - lappend type_alternatives [list $word [lindex $type_expression $w+1]] - incr w ;#consume arg - set expect_separator 1 - } - default { - #simple types such as int,double,string - lappend type_alternatives $word - set expect_separator 1 - } - } - } - return $type_alternatives - } - } - - #old version - ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { - ### #set type $type_expression ;#todo - 'split' on | - ### set vlist $clausevalues_raw - ### set vlist_check $clausevalues_check - - ### set type_alternatives [_split_type_expression $type_expression] - ### #each type_alternative is a list of varying length depending on arguments supported by first word. - ### #TODO? - ### #single element types: int double string etc - ### #two element types literal literalprefix stringstartswith stringendswith - ### #TODO - ### set stype [lindex $type_alternatives 0] - ### #e.g int - ### #e.g {literal blah)etc} - ### set type [lindex $stype 0] - ### #switch on first word of each stype - ### # - - ### #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? - ### switch -- $type { - ### any {} - ### literal { - ### foreach clauseval $vlist { - ### set e [lindex $clauseval $clausecolumn] - ### set testval [lindex $stype 1] - ### if {$e ne $testval} { - ### set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### stringstartwith { - ### foreach clauseval $vlist { - ### set e [lindex $clauseval $clausecolumn] - ### set testval [lindex $stype 1] - ### if {![string match $testval* $e]} { - ### set msg "$argclass '$argname' for %caller% requires stringstartswith value '$argname'. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### list { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is list -strict $e_check]} { - ### set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - ### } - ### if {[tcl::dict::size $thisarg_checks]} { - ### tcl::dict::for {checkopt checkval} $thisarg_checks { - ### switch -- $checkopt { - ### -minsize { - ### # -1 for disable is as good as zero - ### if {[llength $e_check] < $checkval} { - ### set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### -maxsize { - ### if {$checkval ne "-1"} { - ### if {[llength $e_check] > $checkval} { - ### set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### } - ### } - ### indexexpression { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[catch {lindex {} $e_check}]} { - ### set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### string - ansistring - globstring { - ### #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - ### #we possibly don't want to always have to regex on things that don't pass the other more basic checks - ### # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - ### # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - ### # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - ### # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - ### # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - ### # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - ### #todo? - way to validate both unstripped and stripped? - ### set pass_quick_list_e [list] - ### set pass_quick_list_e_check [list] - ### set remaining_e $vlist - ### set remaining_e_check $vlist_check - ### #review - order of -regexprepass and -regexprefail in original rawargs significant? - ### #for now -regexprepass always takes precedence - ### set regexprepass [tcl::dict::get $thisarg -regexprepass] - ### set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - ### if {$regexprepass ne ""} { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[regexp [lindex $regexprepass $clausecolumn] $e]} { - ### lappend pass_quick_list_e $clauseval - ### lappend pass_quick_list_e_check $clauseval_check - ### } - ### } - ### set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - ### set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - ### } - ### if {$regexprefail ne ""} { - ### foreach clauseval $remaining_e clauseval_check $remaining_e_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### #puts "----> checking $e vs regex $regexprefail" - ### if {[regexp $regexprefail $e]} { - ### if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - ### #review - %caller% ?? - ### set msg [tcl::dict::get $thisarg -regexprefailmsg] - ### } else { - ### set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - ### } - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### switch -- $type { - ### ansistring { - ### #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - ### #.. so we need to look at the original values in $vlist not $vlist_check - - ### #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - ### #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - ### package require punk::ansi - ### foreach clauseval $remaining_e { - ### set e [lindex $clauseval $clausecolumn] - ### if {![punk::ansi::ta::detect $e]} { - ### set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### globstring { - ### foreach clauseval $remaining_e { - ### set e [lindex $clauseval $clausecolumn] - ### if {![regexp {[*?\[\]]} $e]} { - ### set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - - ### if {[tcl::dict::size $thisarg_checks]} { - ### foreach clauseval $remaining_e_check { - ### set e_check [lindex $clauseval $clausecolumn] - ### if {[dict exists $thisarg_checks -minsize]} { - ### set minsize [dict get $thisarg_checks -minsize] - ### # -1 for disable is as good as zero - ### if {[tcl::string::length $e_check] < $minsize} { - ### set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[dict exists $thisarg_checks -maxsize]} { - ### set maxsize [dict get $thisarg_checks -maxsize] - ### if {$checkval ne "-1"} { - ### if {[tcl::string::length $e_check] > $maxsize} { - ### set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### number { - ### #review - consider effects of Nan and Inf - ### #NaN can be considered as 'technically' a number (or at least a special numeric value) - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::exists $thisarg -typeranges]} { - ### set ranges [tcl::dict::get $thisarg -typeranges] - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set range [lindex $ranges $clausecolumn] - ### lassign {} low high ;#set both empty - ### lassign $range low high - - ### if {"$low$high" ne ""} { - ### if {[::tcl::mathfunc::isnan $e]} { - ### set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### if {$low eq ""} { - ### if {$e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } elseif {$high eq ""} { - ### if {$e_check < $low} { - ### set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } else { - ### if {$e_check < $low || $e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### int { - ### #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is integer -strict $e_check]} { - ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::exists $thisarg -typeranges]} { - ### set ranges [tcl::dict::get $thisarg -typeranges] - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set range [lindex $ranges $clausecolumn] - ### lassign $range low high - ### if {"$low$high" ne ""} { - ### if {$low eq ""} { - ### #lowside unspecified - check only high - ### if {$e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } elseif {$high eq ""} { - ### #highside unspecified - check only low - ### if {$e_check < $low} { - ### set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } else { - ### #high and low specified - ### if {$e_check < $low || $e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### double { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is double -strict $e_check]} { - ### set e [lindex $clauseval $clausecolumn] - ### set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::size $thisarg_checks]} { - ### if {[dict exists $thisarg_checks -typeranges]} { - ### set ranges [dict get $thisarg_checks -typeranges] - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set range [lindex $ranges $clausecolumn] - ### #todo - small-value double comparisons with error-margin? review - ### #todo - empty string for low or high - ### lassign $range low high - ### if {$e_check < $low || $e_check > $high} { - ### set e [lindex $clauseval $clausecolumn] - ### set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### bool { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is boolean -strict $e_check]} { - ### set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### dict { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[llength $e_check] %2 != 0} { - ### set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::size $thisarg_checks]} { - ### if {[dict exists $thisarg_checks -minsize]} { - ### set minsizes [dict get $thisarg_checks -minsize] - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set minsize [lindex $minsizes $clausecolumn] - ### # -1 for disable is as good as zero - ### if {[tcl::dict::size $e_check] < $minsize} { - ### set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### if {[dict exists $thisarg_checks -maxsize]} { - ### set maxsizes [dict get $thisarg_checks -maxsize] - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set maxsize [lindex $maxsizes $clausecolumn] - ### if {$maxsize ne "-1"} { - ### if {[tcl::dict::size $e_check] > $maxsize} { - ### set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### alnum - - ### alpha - - ### ascii - - ### control - - ### digit - - ### graph - - ### lower - - ### print - - ### punct - - ### space - - ### upper - - ### wordchar - - ### xdigit { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is $type -strict $e_check]} { - ### set e [lindex $clauseval $t] - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### file - - ### directory - - ### existingfile - - ### existingdirectory { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### #//review - we may need '?' char on windows - ### if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - ### #what about special file names e.g on windows NUL ? - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### if {$type eq "existingfile"} { - ### if {![file exists $e_check]} { - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - ### } - ### } elseif {$type eq "existingdirectory"} { - ### if {![file isdirectory $e_check]} { - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### char { - ### #review - char vs unicode codepoint vs grapheme? - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[tcl::string::length $e_check] != 1} { - ### set e [lindex $clauseval $clausecolumn] - ### set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### default { - ### } - ### } - - ###} - - #new version - #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname - #common basic case: list of single item being a single value clause. - #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} - proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { - #default_type_expression is for the chosen clausecolumn - #if {$argname eq "frametype"} { - #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" - #puts "--->list_of_clauses_raw : $list_of_clauses_raw" - #puts "--->list_of_clauses_check: $list_of_clauses_check" - #puts "--->$argname -type: [dict get $thisarg -type]" - #} - - set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn - - set default_type_alternatives [_split_type_expression $default_type_expression] - #--------------------- - #pre-calc prefix sets based on the default. - set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] - set literals [lmap v $alt_literals {lindex $v 1}] - set alt_literalprefixes [lsearch -all -inline -index 0 $default_type_alternatives literalprefix] - set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] - #--------------------- - - #each type_alternative is a list of varying length depending on arguments supported by first word. - #TODO? - #single element types: int double string etc - #two element types literal literalprefix stringstartswith stringendswith - #TODO - - #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) - set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $default_type_alternatives] _]] - #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 - #-types {int|char|literal(ok) char double} - #we are checking a and 1 against the defaulttype_expression e.g int|char|literal(ok) (type_alternatives = {int char literal(ok)} - #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} - #review: for a particular clause the active type_expression might be overridden with 'any' if the column has already passed a -choices test - # - - set e_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_raw *] - set check_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] - set typelist_vals_raw [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_types *] - set typelist_vals [lmap v $typelist_vals_raw {string trim $v ?}] - - set c_idx -1 - foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { - incr c_idx - set col_type_alternatives [_split_type_expression $clause_column_type_expression] - set firstany [lsearch -exact $col_type_alternatives any] - if {$firstany > -1} { - lset clause_results $c_idx $firstany 1 - continue - } - set a_idx -1 - foreach typealt $col_type_alternatives { - incr a_idx - lassign $typealt type testval ;#testval will be empty for basic types, but applies to literal, literalprefix, stringstartswith etc. - switch -exact -- $type { - literal { - if {$e ne $testval} { - set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } else { - lset clause_results $c_idx $a_idx 1 - #this clause is satisfied - no need to process it for other typealt - break - } - } - literalprefix { - #this specific literalprefix testval value not relevant - we're testing against all in the set of typealternates - set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] - if {$match ne "" && $match ni $literals} { - lset clause_results $c_idx $a_idx 1 - #this clause is satisfied - no need to process it for other typealt - break - } else { - set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } - } - stringstartswith { - if {[string match $testval* $e]} { - lset clause_results $c_idx $a_idx 1 - break - } else { - set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } - } - stringendswith { - if {[string match *$testval $e]} { - lset clause_results $c_idx $a_idx 1 - break - } else { - set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } - } - list { - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] - continue - } else { - if {[dict exists $thisarg_checks -minsize]} { - # -1 for disable is as good as zero - set minsize [dict get $thisarg_checks -minsize] - if {[llength $e_check] < $minsize} { - set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - continue - } - } - if {[dict exist $thisarg_checks -maxsize]} { - set maxsize [dict get $thisarg_checks -maxsize] - if {$maxsize ne "-1"} { - if {[llength $e_check] > $maxsize} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - indexexpression { - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - indexset { - if {![punk::lib::is_indexset $e_check]} { - set msg "$argclass $argname for %caller% requires type indexset. A comma-delimited set of indexes or index-ranges separated by '..' Received: '$e_check'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - if {$regexprepass ne ""} { - if {[regexp [lindex $regexprepass $clausecolumn] $e]} { - lset clause_results $c_idx $a_idx 1 - break - } - } - if {$regexprefail ne ""} { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] - #review - tests? - continue - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - globstring { - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -minsize]} { - set minsize [dict get $thisarg_checks -minsize] - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - set range [lindex $ranges $clausecolumn] - lassign {} low high ;#set both empty - lassign $range low high - if {"$low$high" ne ""} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - int { - #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - set range [lindex $ranges $clausecolumn] - lassign $range low high - if {"$low$high" ne ""} { - if {$low eq ""} { - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$high eq ""} { - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } else { - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - double { - if {![tcl::string::is double -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {[tcl::dict::exists $thisarg_checks -typeranges]} { - set ranges [dict get $thisarg_checks -typeranges] - set range [lindex $ranges $clausecolumn] - #todo - small-value double comparisons with error-margin? review - lassign $range low high - if {$low$high ne ""} { - if {$low eq ""} { - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass $argname for %caller% must be double less than or equal to $high. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$high eq ""} { - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass $argname for %caller% must be double greater than or equal to $low. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$e_check < $low || $e_check > $high} { - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - bool { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - dict { - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -minsize]} { - set minsizes [dict get $thisarg_checks -minsize] - set minsize [lindex $minsizes $clausecolumn] - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - lset clause_results $c_idx $a_idx [list err [list sizeviolation $type minsize $minsize] msg $msg] - continue - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsize [lindex $maxsizes $clausecolumn] - if {$maxsize ne "-1"} { - if {[tcl::dict::size $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - lset clause_results $c_idx $a_idx [list err [list sizeviolation $type maxsize $maxsize] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - if {![tcl::string::is $type -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - file - - directory - - existingfile - - existingdirectory { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - if {$type eq "existingfile"} { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } elseif {$type eq "existingdirectory"} { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } - lset clause_results $c_idx $a_idx 1 - } - char { - #review - char vs unicode codepoint vs grapheme? - if {[tcl::string::length $e_check] != 1} { - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - tk_screen_units { - switch -exact -- [string index $e_check end] { - c - i - m - p { - set numpart [string range $e_check 0 end-1] - if {![tcl::string::is double $numpart]} { - set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } - default { - if {![tcl::string::is double $e_check]} { - set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - default { - #default pass for unrecognised types - review. - lset clause_results $c_idx $a_idx 1 - break - } - } - } - } - - foreach clauseresult $clause_results { - if {[lsearch $clauseresult 1] == -1} { - #no pass for this clause - fetch first? error and raise - #todo - return error containing clause_indices so we can report more than one failing element at once? - foreach e $clauseresult { - switch -exact [lindex $e 0] { - errorcode { - #errorcode msg checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against type_expression $type_expression" - # puts "--->list_of_clauses_raw : $list_of_clauses_raw" - # puts "--->list_of_clauses_check: $list_of_clauses_check" - # puts "--->$argname -type: [dict get $thisarg -type]" - # } - - # set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just passed type_expression - - # #set vlist [list] - # set clauses_dict [dict create] ;#key is ordinal position, remove entries as they are satsified - # set cidx -1 - # foreach cv $list_of_clauses_raw { - # incr cidx - # #REVIEW - # #if {$clause_size ==1} { - # # lappend vlist [list $cidx [list $cv]] - # #} else { - # #lappend vlist [list $cidx $cv] ;#store the index so we can reduce vlist as we go - # dict set clauses_dict $cidx $cv - # #} - # } - # #set vlist_check [list] - # set clauses_dict_check [dict create] - # set cidx -1 - # foreach cv $list_of_clauses_check { - # incr cidx - # #if {$clause_size == 1} { - # # lappend vlist_check [list $cidx [list $cv]] - # #} else { - # #lappend vlist_check [list $cidx $cv] - # dict set clauses_dict_check $cidx $cv - # #} - # } - - # set type_alternatives [_split_type_expression $type_expression] - # #each type_alternative is a list of varying length depending on arguments supported by first word. - # #TODO? - # #single element types: int double string etc - # #two element types literal literalprefix stringstartswith stringendswith - # #TODO - - # #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) - # set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $type_alternatives] _]] - # #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 - # #-types {int|char|literal(ok) char double} - # #we are checking a and 1 against the type_expression int|char|literal(ok) (type_alternatives = {int char literal(ok)} - # #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} - # # - - - # set a_idx -1 - # foreach typealt $type_alternatives { - # incr a_idx - - # set type [lindex $typealt 0] - # #e.g int - # #e.g {literal blah} - # #e.g {literalprefix abc} - - # #switch on first word of each typealt - # # - - # #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? - # switch -- $type { - # any {} - # literal { - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # set testval [lindex $typealt 1] - # if {$e ne $testval} { - # set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # #this clause is satisfied - no need to process it for other typealt - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # literalprefix { - # set alt_literals [lsearch -all -inline -index 0 $type_alternatives literal] - # set literals [lmap v $alt_literals {lindex $v 1}] - # set alt_literalprefixes [lsearch -all -inline -index 0 $type_alternatives literalprefix] - # set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] - - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # #this specific literalprefix value not relevant - we're testing against all in the set of typealternates - # #set testval [lindex $typealt 1] - # set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] - # if {$match ne "" && $match ni $literals} { - # lset clause_results $c_idx $a_idx 1 - # #this clause is satisfied - no need to process it for other typealt - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } else { - # set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } - # } - # } - # stringstartswith { - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # set testval [lindex $typealt 1] - # if {[string match $testval* $e]} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } else { - # set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } - # } - # } - # stringendswith { - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # set testval [lindex $typealt 1] - # if {[string match *$testval $e]} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } else { - # set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } - # } - # } - # list { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # set passed_checks 1 - # if {![tcl::string::is list -strict $e_check]} { - # set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } else { - # if {[dict exists $thisarg_checks -minsize]} { - # # -1 for disable is as good as zero - # set minsize [dict get $thisarg_checks -minsize] - # if {[llength $e_check] < $minsize} { - # set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks && [dict exist $thisarg_checks -maxsize]} { - # set maxsize [dict get $thisarg_checks -maxsize] - # if {$maxsize ne "-1"} { - # if {[llength $e_check] > $maxsize} { - # set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # indexexpression { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[catch {lindex {} $e_check}]} { - # set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # string - ansistring - globstring { - # #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - # #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - # #todo? - way to validate both unstripped and stripped? - # #review - order of -regexprepass and -regexprefail in original rawargs significant? - # #for now -regexprepass always takes precedence - # #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? - # set regexprepass [tcl::dict::get $thisarg -regexprepass] - # set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - # if {$regexprepass ne ""} { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[regexp [lindex $regexprepass $clausecolumn] $e]} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # if {$regexprefail ne ""} { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # #puts "----> checking $e vs regex $regexprefail" - # if {[regexp $regexprefail $e]} { - # if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - # #review - %caller% ?? - # set msg [tcl::dict::get $thisarg -regexprefailmsg] - # } else { - # set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - # } - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] - # #review - tests? - # } - # } - # } - # switch -- $type { - # ansistring { - # #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - # #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check - - # #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - # #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - # package require punk::ansi - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # if {![punk::ansi::ta::detect $e]} { - # set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # } - # globstring { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # if {![regexp {[*?\[\]]} $e]} { - # set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # } - # } - - # dict for {c_idx clauseval_check} $clauses_dict_check { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # if {[tcl::dict::size $thisarg_checks]} { - # set passed_checks 1 - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[dict exists $thisarg_checks -minsize]} { - # set minsize [dict get $thisarg_checks -minsize] - # # -1 for disable is as good as zero - # if {[tcl::string::length $e_check] < $minsize} { - # set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { - # set maxsize [dict get $thisarg_checks -maxsize] - # if {$checkval ne "-1"} { - # if {[tcl::string::length $e_check] > $maxsize} { - # set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } else { - # if {[lindex $clause_results $c_idx $a_idx] eq "_"} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # } - # number { - # #review - consider effects of Nan and Inf - # #NaN can be considered as 'technically' a number (or at least a special numeric value) - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # if {[tcl::dict::exists $thisarg -typeranges]} { - # set ranges [tcl::dict::get $thisarg -typeranges] - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # set range [lindex $ranges $clausecolumn] - # lassign {} low high ;#set both empty - # lassign $range low high - # set passed_checks 1 - # if {"$low$high" ne ""} { - # if {[::tcl::mathfunc::isnan $e]} { - # set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # if {$passed_checks} { - # if {$low eq ""} { - # if {$e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } elseif {$high eq ""} { - # if {$e_check < $low} { - # set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } else { - # if {$e_check < $low || $e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict usnet clauses_dict_check $c_idx - # } - # } - # } else { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict usnet clauses_dict_check $c_idx - # } - # } - - # } - # int { - # #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is integer -strict $e_check]} { - # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # if {[tcl::dict::exists $thisarg -typeranges]} { - # set ranges [tcl::dict::get $thisarg -typeranges] - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # set range [lindex $ranges $clausecolumn] - # lassign $range low high - # set passed_checks 1 - # if {"$low$high" ne ""} { - # if {$low eq ""} { - # #lowside unspecified - check only high - # if {$e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } elseif {$high eq ""} { - # #highside unspecified - check only low - # if {$e_check < $low} { - # set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } else { - # #high and low specified - # if {$e_check < $low || $e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } else { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # double { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is double -strict $e_check]} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # if {[tcl::dict::exists $thisarg_checks -typeranges]} { - # set ranges [dict get $thisarg_checks -typeranges] - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e_check [lindex $clauseval_check $clausecolumn] - # set range [lindex $ranges $clausecolumn] - # #todo - small-value double comparisons with error-margin? review - # #todo - empty string for low or high - # set passed_checks 1 - # lassign $range low high - # if {$low$high ne ""} { - # if {$e_check < $low || $e_check > $high} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } else { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # bool { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is boolean -strict $e_check]} { - # set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # dict { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # puts "check_clausecolumn2 dict handler: c_idx:$c_idx clausecolumn:$clausecolumn clauseval_check:$clauseval_check" - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[llength $e_check] %2 != 0} { - # set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] - # } - # } - # dict for {c_idx clauseval_check} $clauses_dict_check { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set passed_checks 1 - # if {[tcl::dict::size $thisarg_checks]} { - # if {[dict exists $thisarg_checks -minsize]} { - # set minsizes [dict get $thisarg_checks -minsize] - # set e_check [lindex $clauseval_check $clausecolumn] - # set minsize [lindex $minsizes $clausecolumn] - # # -1 for disable is as good as zero - # if {[tcl::dict::size $e_check] < $minsize} { - # set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { - # set e_check [lindex $clauseval_check $clausecolumn] - # set maxsize [lindex $maxsizes $clausecolumn] - # if {$maxsize ne "-1"} { - # if {[tcl::dict::size $e_check] > $maxsize} { - # set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # } - - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # alnum - - # alpha - - # ascii - - # control - - # digit - - # graph - - # lower - - # print - - # punct - - # space - - # upper - - # wordchar - - # xdigit { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is $type -strict $e_check]} { - # set e [lindex $clauseval $t] - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # file - - # directory - - # existingfile - - # existingdirectory { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - - # #//review - we may need '?' char on windows - # set passed_checks 1 - # if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - # #what about special file names e.g on windows NUL ? - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # if {$passed_checks} { - # if {$type eq "existingfile"} { - # if {![file exists $e_check]} { - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } elseif {$type eq "existingdirectory"} { - # if {![file isdirectory $e_check]} { - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # char { - # #review - char vs unicode codepoint vs grapheme? - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[tcl::string::length $e_check] != 1} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # tk_screen_units { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e_check [lindex $clauseval_check $clausecolumn] - # set passed_checks 1 - # switch -exact -- [string index $e_check end] { - # c - i - m - p { - # set numpart [string range $e_check 0 end-1] - # if {![tcl::string::is double $numpart]} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # default { - # if {![tcl::string::is double $e_check]} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # default { - # #default pass for unrecognised types - review. - # dict for {c_idx clauseval} $clauses_dict { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # } - # foreach clauseresult $clause_results { - # if {[lsearch $clauseresult 1] == -1} { - # #no pass for this clause - fetch first? error and raise - # #todo - return error containing clause_indices so we can report more than one failing element at once? - # foreach e $clauseresult { - # if {[lindex $e 0] eq "errorcode"} { - # #errorcode msg values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional etc - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $proc_opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] - } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list $opt_form] - } - - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - #todo - handle multiple fids? - set fid [lindex $selected_forms 0] - set formdict [dict get $FORMS $fid] - tcl::dict::with formdict {} - #populate vars ARG_INFO,LEADER_MAX,LEADER_NAMES etc - if {$VAL_MIN eq ""} { - set valmin 0 - #set VAL_MIN 0 - foreach v $VAL_REQUIRED { - # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) - # e.g -types {a ?xxx?} - #this has one required and one optional - set typelist [dict get $ARG_INFO $v -type] - set clause_length 0 - foreach t $typelist { - if {![string match {\?*\?} $t]} { - incr clause_length - } - } - incr valmin $clause_length - } - } else { - set valmin $VAL_MIN - } - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - #set optnames [lsearch -all -inline $argnames -*] - #JJJ - set lookup_optset [dict create] - foreach optset $OPT_NAMES { - #optset e.g {-x|--longopt|--longopt=|--otherlongopt} - foreach optdef [split $optset |] { - set opt [string trimright $optdef =] - if {![dict exists $lookup_optset $opt]} { - dict set lookup_optset $opt $optset - } - } - } - set all_opts [dict keys $lookup_optset] - - set ridx 0 - set rawargs_copy $rawargs - set remaining_rawargs $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - - - #consider for example: LEADER_NAMES {"k v" leader2 leader3} with -type {int number} & -type {int int int} & -type string - #(i.e clause-length of 2 3 and 1) - #This will take 6 raw leaders to fill in the basic case that all are -optional 0 and -multiple 0 - #REVIEW - what about optional members in leaders e.g -type {int ?double?} - set named_leader_args_max 0 - foreach ln $LEADER_NAMES { - set typelist [dict get $ARG_INFO $ln -type] - incr named_leader_args_max [llength $typelist] - } - - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" - #} - - set can_have_leaders 1 ;#default assumption - if {$LEADER_MAX == 0 || ([llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED)} { - set can_have_leaders 0 - } - - #REVIEW - this attempt to classify leaders vs opts vs values doesn't account for leaders with clauses containing optional elements - #e.g @leaders {x -type {int ?int?}} - set nameidx 0 - if {$can_have_leaders} { - if {$LEADER_TAKEWHENARGSMODULO} { - #assign set of leaders purely based on number of total args - set take [expr {[llength $remaining_rawargs] % $LEADER_TAKEWHENARGSMODULO}] - set pre_values [lrange $remaining_rawargs 0 $take-1] - set remaining_rawargs [lrange $remaining_rawargs $take end] - } else { - #greedy taking of leaders based on type-matching - - set leadernames_seen [list] - for {set ridx 0} {$ridx < [llength $rawargs]} {incr ridx} { - set raw [lindex $rawargs $ridx] ;#received raw arg - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {[llength $LEADER_NAMES] && $nameidx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $nameidx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > $named_leader_args_max-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string - } - if {$OPT_MAX ne "0" && [string match -* $raw]} { - #all_opts includes end_of_opts marker -- if configured - no need to explicitly check for it separately - set possible_flagname $raw - if {[string match --* $raw]} { - set eposn [string first = $raw] - # --flag=xxx - if {$eposn >=3} { - set possible_flagname [string range $raw 0 $eposn-1] - } - } - set matchopt [::tcl::prefix::match -error {} $all_opts $possible_flagname] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - set leader_type [dict get $ARG_INFO $leader_posn_name -type] - #todo - variable clauselengths e.g 'if' command which has optional 'then' and 'else' "noise words" - set clauselength [llength $leader_type] - set min_clauselength 0 - foreach t $leader_type { - if {![string match {\?*\?} $t]} { - incr min_clauselength - } - } - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] $raw] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop remaining_rawargs 0] - # incr ridx - # continue - # } - #} - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break - } - - #check if enough remaining_rawargs to fill any required values - if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { - break - } - - #leadername may be a 'clause' of arbitrary length (e.g -type {int double} or {int string number}) - set end_leaders 0 - set tentative_pre_values [list] - set tentative_idx $ridx - if {$OPT_MAX ne "0"} { - foreach t $leader_type { - set raw [lindex $rawargs $tentative_idx] - if {[string match {\?*\?} $t] && [string match -* $raw]} { - #review - limitation of optional leaders is they can't be same value as any defined flags/opts - set flagname $raw - if {[string match --* $raw]} { - set eposn [string first = $raw] - # --flag=xxx - if {$eposn >=3} { - set flagname [string range $raw 0 $eposn-1] - } - } - set matchopt [::tcl::prefix::match -error {} $all_opts $flagname] - if {$matchopt ne ""} { - #don't consume if flaglike (and actually matches an opt) - set end_leaders 1 - break ;#break out of looking at -type members in the clause - } else { - #unrecognised flag - treat as value for optional member of the clause - #lappend pre_values [lpop remaining_rawargs 0] - lappend tentative_pre_values $raw - incr tentative_idx - } - } else { - #lappend pre_values [lpop remaining_rawargs 0] - lappend tentative_pre_values $raw - incr tentative_idx - } - } - if {$end_leaders} { - break - } - } else { - foreach t $leader_type { - #JJJ - set raw [lindex $rawargs $tentative_idx] - #lappend pre_values [lpop remaining_rawargs 0] - lappend tentative_pre_values $raw - incr tentative_idx - } - } - set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] - set consumed [dict get $assign_d consumed] - set resultlist [dict get $assign_d resultlist] - set newtypelist [dict get $assign_d typelist] - if {$consumed != 0} { - if {$leader_posn_name ni $leadernames_seen} { - lappend leadernames_seen $leader_posn_name - } - dict incr leader_posn_names_assigned $leader_posn_name - #for {set c 0} {$c < $consumed} {incr c} { - # lappend pre_values [lpop remaining_rawargs 0] - #} - lappend pre_values {*}[lrange $remaining_rawargs 0 $consumed-1] - ledit remaining_rawargs 0 $consumed-1 - - incr ridx $consumed - incr ridx -1 ;#leave ridx at index of last r that we set - } else { - - } - if {!$is_multiple} { - incr nameidx - } - } else { - #clause is required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one complete clause for this name - requirement satisfied - now equivalent to optional - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break - } - - if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but still enough remaining_rawargs for required values - #we still need to check if enough values for the leader itself - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill *required* leader - break - } - - set end_leaders 0 - - #review - are we allowing multivalue leader clauses where the optional members are not at the tail? - #eg @leaders {double -type {?int? char}} - #as we don't type-check here while determining leaders vs opts vs values - this seems impractical. - #for consistency and simplification - we should only allow optional clause members at the tail - # and only for the last defined leader. This should be done in the definition parsing - not here. - foreach t $leader_type { - set raw [lindex $rawargs $ridx] - if {[string match {\?*\?} $t] && [string match -* $raw]} { - #review - limitation of optional leaders is they can't be same value as any defined flags/opts - - set matchopt [::tcl::prefix::match -error {} $all_opts $raw] - if {$matchopt ne ""} { - #don't consume if flaglike (and actually matches an opt) - set end_leaders 1 - break ;#break out of looking at -type members in the clause - } else { - #unrecognised flag - treat as value for optional member of the clause - #ridx must be valid if we matched -* - so lpop will succeed - lappend pre_values [lpop remaining_rawargs 0] - incr ridx - } - } else { - if {[string match {\?*\?} $t]} { - if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { - set end_leaders 1 - break - } - if {[catch { - lappend pre_values [lpop remaining_rawargs 0] - }]} { - set end_leaders 1 - break - } - } else { - if {[catch { - lappend pre_values [lpop remaining_rawargs 0] - }]} { - set end_leaders 1 - break ;#let validation of required leaders report the error? - } - } - incr ridx - } - } - incr ridx -1 - if {$end_leaders} { - break - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break - } else { - if {$valmin > 0} { - if {[llength $remaining_rawargs] > $valmin} { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } else { - break - } - } else { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #review - if is_multiple, keep going if enough remaining_rawargs for values? - break - } - } - - #incr ridx - } ;# end foreach r $rawargs_copy - } - } - #puts "get_dict ================> pre: $pre_values" - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - if {[llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED} { - set leadermax 0 - } else { - set leadermax -1 - } - } else { - set leadermax $LEADER_MAX - } - - if {$VAL_MAX eq ""} { - if {[llength $VAL_NAMES] == 0 && !$VAL_UNNAMED} { - set valmax 0 - } else { - set valmax -1 - } - } else { - set valmax $VAL_MAX - } - - #assert leadermax leadermin are numeric - #assert - remaining_rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to OPT_DEFAULTS here - - set leaders [list] - set arglist {} - set post_values {} - #valmin, valmax - #puts stderr "remaining_rawargs: $remaining_rawargs" - #puts stderr "argstate: $argstate" - if {$OPT_MAX ne "0" && [lsearch $remaining_rawargs -*] >= 0} { - #contains at least one possible flag - set maxidx [expr {[llength $remaining_rawargs] -1}] - if {$valmax == -1} { - set vals_total_possible [llength $remaining_rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $valmax - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set remaining_args_including_this [expr {[llength $remaining_rawargs] - $i}] - #lowest valmin is 0 - if {$remaining_args_including_this <= $valmin} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - set a [lindex $remaining_rawargs $i] - switch -glob -- $a { - -- { - if {$a in $OPT_NAMES} { - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $remaining_rawargs 0 $i] - set post_values [lrange $remaining_rawargs $i+1 end] - } else { - #assume it's a value. - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - } - break - } - --* { - set eposn [string first = $a] - if {$eposn > 2} { - #only allow longopt-style = for double leading dash longopts - #--*= usage - if {$flagname ni $raw_optionset_members} { - # - set msg "Bad options for %caller%. Option $optionset at index [expr {$i-1}] requires a value, but '$flagname' not specified in definition to allow space-separated value." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list badoptionformat $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg - } - } - if {$solo_only} { - #same logic as 'solo' branch below for -type none - if {[tcl::dict::exists $argstate $optionset -typedefaults]} { - set tdflt [tcl::dict::get $argstate $optionset -typedefaults] - } else { - #normal default for a solo is 1 unless overridden by -typedefaults - set tdflt 1 - } - if {[tcl::dict::get $argstate $optionset -multiple]} { - if {$api_opt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $flag_ident $tdflt - } else { - tcl::dict::lappend opts $flag_ident $tdflt - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - if {$flag_ident_is_parsekey} { - lappend opts $flag_ident $tdflt - } else { - tcl::dict::set opts $flag_ident $tdflt - } - } - incr vals_remaining_possible -1 - lappend solosreceived $api_opt ;#dups ok - } else { - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - #review - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - #flagval comes from next remaining rawarg - set flagval [lindex $remaining_rawargs $i+1] - if {[tcl::dict::get $argstate $optionset -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$api_opt ni $flagsreceived} { - tcl::dict::set opts $flag_ident [list $flagval] - } else { - tcl::dict::lappend opts $flag_ident $flagval - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - #tcl::dict::set opts $flag_ident $flagval - if {$flag_ident_is_parsekey} { - #necessary shimmer - lappend opts $flag_ident $flagval - } else { - tcl::dict::set opts $flag_ident $flagval - } - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $optionset at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg - } - } - } - } else { - #none / solo - if {[tcl::dict::exists $argstate $optionset -typedefaults]} { - set tdflt [tcl::dict::get $argstate $optionset -typedefaults] - } else { - #normal default for a solo is 1 unless overridden by -typedefaults - set tdflt 1 - } - if {[tcl::dict::get $argstate $optionset -multiple]} { - if {$api_opt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $flag_ident $tdflt - } else { - tcl::dict::lappend opts $flag_ident $tdflt - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - #test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence} - #tcl::dict::set opts $flag_ident $tdflt - if {$flag_ident_is_parsekey} { - #(shimmer - but required for ordering correctness during override) - lappend opts $flag_ident $tdflt - } else { - tcl::dict::set opts $flag_ident $tdflt - } - } - incr vals_remaining_possible -1 - lappend solosreceived $api_opt ;#dups ok - } - lappend flagsreceived $api_opt ;#dups ok - } else { - #starts with - but unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even if optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - if {!([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$OPT_ANY} { - #exlude argument with whitespace from being a possible option e.g dict - #todo - passthrough of unrecognised --longopt=xxx without looking for following flag-value - set eposn [string first = $a] - if {[string match --* $a] && $eposn > 2} { - #only allow longopt-style = for double leading dash longopts - #--*= $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::exists $argstate $a -typedefaults]} { - set tdflt [tcl::dict::get $argstate $a -typedefaults] - } else { - set tdflt 1 - } - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a $tdflt - } else { - tcl::dict::lappend opts $a $tdflt - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $tdflt - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - } - - lappend flagsreceived $undefined_flagsupplied ;#adhoc flag name (if --x=1 -> --x) - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES (3)" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any|-arbitrary false" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $optionset - } - } else { - #not a flag/option - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - } - - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $remaining_rawargs - #set values [list {*}$pre_values {*}$remaining_rawargs] ;#no -flags detected - set arglist [list] - } - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> arglist: $arglist" - #puts stderr "get_dict--> leaders: $leaders" - #puts stderr "get_dict--> values: $values" - #} - - #--------------------------------------- - #Order the received options by the order in which they are *defined* - #EXCEPT that grouped options using same parsekey must be processed in received order - set ordered_opts [dict create] - - #set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] - ##unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) - ## e.g -fg|-foreground - ## e.g -x|--fullname= - ##Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} - #foreach o $unaliased_opts optset $OPT_NAMES { - # if {[dict exists $opts $o]} { - # dict set ordered_opts $o [dict get $opts $o] - # } elseif {[dict exists $OPT_DEFAULTS $optset]} { - # #JJJ - # set parsekey "" - # if {[tcl::dict::exists $argstate $o -parsekey]} { - # set parsekey [tcl::dict::get $argstate $o -parsekey] - # } - # if {$parsekey eq ""} { - # set parsekey $o - # } - # dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] - # } - #} - - #puts ">>>>==== $opts" - set seen_pks [list] - #treating opts as list for this loop. - foreach optset $OPT_NAMES { - set parsekey "" - set has_parsekey_override 0 - if {[tcl::dict::exists $argstate $optset -parsekey]} { - set parsekey [tcl::dict::get $argstate $optset -parsekey] - } - if {$parsekey eq ""} { - set has_parsekey_override 0 - #fall back to last element of aliased option e.g -fg|-foreground -> "-foreground" - set parsekey [string trimright [lindex [split $optset |] end] =] - } else { - set has_parsekey_override 1 - } - lappend seen_pks $parsekey - set is_found 0 - set foundkey "" - set foundval "" - #no lsearch -stride avail in 8.6 - foreach {k v} $opts { - if {$k eq $parsekey} { - set foundkey $k - set is_found 1 - set foundval $v - #can be multiple - last match wins - don't 'break' out of foreach - } - } ;#avoiding further dict/list shimmering - #if {[dict exists $opts $parsekey]} { - # set found $parsekey - # set foundval [dict get $opts $parsekey] - #} - if {!$is_found && $parsekey ne $optset} { - #.g we may have in opts things like: -decreasing|-SORTDIRECTION -increasing|-SORTDIRECTION - #(where -SORTDIRECTION was configured as -parsekey) - #last entry must win - #NOTE - do not use dict for here. opts is not strictly a dict - dupe keys will cause wrong ordering - foreach {o v} $opts { - if {[string match *|$parsekey $o]} { - set foundkey $o - set is_found 1 - set foundval $v - #last match wins - don't 'break' out of foreach - } - } - } - if {$is_found} { - dict set ordered_opts $foundkey $foundval - } elseif {[tcl::dict::exists $OPT_DEFAULTS $optset]} { - if {$parsekey ne $optset} { - set tailopt [string trimright [lindex [split $optset |] end] =] - if {$tailopt ne $parsekey} { - #defaults for multiple options sharing a -parsekey value ? review - dict set ordered_opts $tailopt|$parsekey [dict get $OPT_DEFAULTS $optset] - } else { - dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] - } - } else { - dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] - } - } - } - - #add in possible arbitrary opts after the defined opts, due to @opts directive flag '-any|-arbitrary true' - #But make sure not to add any repeated parsekey e.g -increasing|-SORT -decreasing|-SORT - #use the seen_pks from the ordered_opts loop above - #keep working with opts only as list here.. - if {[llength $opts] > 2*[dict size $ordered_opts]} { - foreach {o o_val} $opts { - lassign [split $o |] _ parsekey ;#single pipe - 2 elements only | - if {$parsekey ne "" && $parsekey in $seen_pks} { - continue - } - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $o_val - } - } - } - set opts $ordered_opts - #opts is a proper dict now - - #NOTE opts still may contain some entries in non-final form such as -flag|-PARSEKEY - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set leadername_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - - #---------------------------------------- - #Establish firm leaders ordering - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value - - - set start_position $positionalidx - set nameidx 0 - #MAINTENANCE - (*nearly*?) same loop logic as for value - for {set ldridx 0} {$ldridx < [llength $leaders]} {incr ldridx} { - set leadername [lindex $LEADER_NAMES $nameidx] - set ldr [lindex $leaders $ldridx] - if {$leadername ne ""} { - set leadertypelist [tcl::dict::get $argstate $leadername -type] - set leader_clause_size [llength $leadertypelist] - - set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] - set consumed [dict get $assign_d consumed] - set resultlist [dict get $assign_d resultlist] - set newtypelist [dict get $assign_d typelist] - if {[tcl::dict::get $argstate $leadername -optional]} { - if {$consumed == 0} { - puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" - incr ldridx -1 - set leadername_multiple "" - incr nameidx - continue - } - } else { - #required named arg - if {$consumed == 0} { - if {$leadername ni $leadernames_received} { - #puts stderr "_get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" - set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg - } else { - puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" - incr ldridx -1 - set leadername_multiple "" - incr nameidx - continue - } - } - } - - if {$leader_clause_size == 1} { - #set clauseval $ldr - set clauseval [lindex $resultlist 0] - } else { - set clauseval $resultlist - incr ldridx [expr {$consumed - 1}] - - #not quite right.. this sets the -type for all clauses - but they should run independently - #e.g if expr {} elseif 2 {script2} elseif 3 then {script3} (where elseif clause defined as "literal(elseif) expr ?literal(then)? script") - #the elseif 2 {script2} will raise an error because the newtypelist from elseif 3 then {script3} overwrote the newtypelist where then was given the type ?omitted-...? - - tcl::dict::set argstate $leadername -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries - } - - if {[tcl::dict::get $argstate $leadername -multiple]} { - #if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - # #current stored ldr equals defined default - don't include default in the list we build up - # tcl::dict::set leaders_dict $leadername [list $clauseval] ;#important to treat first element as a list - #} else { - # tcl::dict::lappend leaders_dict $leadername $clauseval - #} - if {$leadername in $leadernames_received} { - tcl::dict::lappend leaders_dict $leadername $clauseval - tcl::dict::lappend argument_clause_typestate $leadername $newtypelist - } else { - tcl::dict::set leaders_dict $leadername [list $clauseval] - tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] - } - set leadername_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $clauseval - tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] - set leadername_multiple "" - incr nameidx - } - lappend leadernames_received $leadername - } else { - if {$leadername_multiple ne ""} { - set leadertypelist [tcl::dict::get $argstate $leadername_multiple -type] - if {[llength $leadertypelist] == 1} { - set clauseval $ldr - } else { - set clauseval [list] - incr ldridx -1 - foreach t $leadertypelist { - incr ldridx - if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername_multiple', but requires up to [llength $leadertypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $leadertypelist] ] -argspecs $argspecs]] $msg - } - lappend clauseval [lindex $leaders $ldridx] - } - } - tcl::dict::lappend leaders_dict $leadername_multiple $clauseval - #name already seen - but must add to leadernames_received anyway (as with opts and values) - lappend leadernames_received $leadername_multiple - } else { - if {$LEADER_UNNAMED} { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } else { - set msg "Bad number of leaders for %caller%. Received more leaders than can be assigned to argument names. (set '@leaders -unnamed true' to allow unnamed leaders)" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list toomanyarguments [llength $values] index $positionalidx] -argspecs $argspecs]] $msg - } - } - } - set positionalidx [expr {$start_position + $ldridx + 1}] - } - #----------------------------------------------------- - #satisfy test parse_withdef_leaders_no_phantom_default - #foreach leadername [dict keys $leaders_dict] { - # if {[string is integer -strict $leadername]} { - # #ignore leadername that is a positionalidx - # #review - always trailing - could use break? - # continue - # } - # if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { - # #remove the name with empty-string default we used to establish fixed order of names - # #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - # dict unset leaders_dict $leadername - # } - #} - dict for {leadername _v} $leaders_dict { - if {[string is integer -strict $leadername]} { - #ignore leadername that is a positionalidx - #review - always trailing - could use break? - continue - } - if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { - #remove the name with empty-string default we used to establish fixed order of names - #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - dict unset leaders_dict $leadername - } - } - #----------------------------------------------------- - - - set validx 0 - set valname_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #Establish firm values ordering - ## Don't set values_dict to VAL_DEFAULTS - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - #set ALL valnames to lock in positioning - #note - later we need to unset any optional that had no default and was not received (no phantom default) - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $VAL_DEFAULTS] - #------------------------------------------ - set nameidx 0 - set start_position $positionalidx - #MAINTENANCE - (*nearly*?) same loop logic as for leaders - for {set validx 0} {$validx < [llength $values]} {incr validx} { - set valname [lindex $VAL_NAMES $nameidx] - set val [lindex $values $validx] - if {$valname ne ""} { - set valtypelist [tcl::dict::get $argstate $valname -type] - set clause_size [llength $valtypelist] ;#common case is clause_size == 1 - - set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] - set consumed [dict get $assign_d consumed] - set resultlist [dict get $assign_d resultlist] - set newtypelist [dict get $assign_d typelist] - if {[tcl::dict::get $argstate $valname -optional]} { - if {$consumed == 0} { - #error 333 - puts stderr "get_dict cannot assign val:$val to valname:$valname (333)" - incr validx -1 - set valname_multiple "" - incr nameidx - continue - } - } else { - #required named arg - if {$consumed == 0} { - if {$valname ni $valnames_received} { - #puts stderr "_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" - set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg - } else { - #error 444 - puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" - incr validx -1 - set valname_multiple "" - incr nameidx - continue - } - } - } - #assert can_assign != 0, we have at least one value to assign to clause - - if {$clause_size == 1} { - #set clauseval $val - set clauseval [lindex $resultlist 0] - } else { - #clauseval must contain as many elements as the max length of -types! - #(empty-string/default for optional (?xxx?) clause members) - set clauseval $resultlist - #_get_dict_can_assign has only validated clause-length and literals match - #we assign and leave further validation for main validation loop. - incr validx [expr {$consumed -1}] - if {$validx > [llength $values]-1} { - error "get_dict unreachable" - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to $clause_size values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength $clause_size ] -argspecs $argspecs]] $msg - } - - #incorrect - we shouldn't update the default. see argument_clause_typestate dict of lists of -type - tcl::dict::set argstate $valname -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries - } - - if {[tcl::dict::get $argstate $valname -multiple]} { - #if {[tcl::dict::exists $VAL_DEFAULTS $valname]} { - # #current stored val equals defined default - don't include default in the list we build up - # tcl::dict::set values_dict $valname [list $clauseval] ;#important to treat first element as a list - #} else { - # tcl::dict::lappend values_dict $valname $clauseval - #} - if {$valname in $valnames_received} { - tcl::dict::lappend values_dict $valname $clauseval - tcl::dict::lappend argument_clause_typestate $valname $newtypelist - } else { - tcl::dict::set values_dict $valname [list $clauseval] - tcl::dict::set argument_clause_typestate $valname [list $newtypelist] - } - set valname_multiple $valname - } else { - tcl::dict::set values_dict $valname $clauseval - tcl::dict::set argument_clause_typestate $valname [list $newtypelist] ;#list protect - set valname_multiple "" - incr nameidx - } - lappend valnames_received $valname - } else { - #unnamed - if {$valname_multiple ne ""} { - set valtypelist [tcl::dict::get $argstate $valname_multiple -type] - if {[llength $valname_multiple] == 1} { - set clauseval $val - } else { - set clauseval [list] - incr validx -1 - for {set i 0} {$i < [llength $valtypelist]} {incr i} { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname_multiple', but requires [llength $valtypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valtypelist] ] -argspecs $argspecs]] $msg - } - lappend clauseval [lindex $values $validx] - } - } - tcl::dict::lappend values_dict $valname_multiple $clauseval - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $valname_multiple - } else { - if {$VAL_UNNAMED} { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $VALSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } else { - set msg "Bad number of values for %caller%. Received more values than can be assigned to argument names. (set '@values -unnamed true' to allow unnamed values)" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list toomanyarguments [llength $values] index $positionalidx] -argspecs $argspecs]] $msg - } - } - } - set positionalidx [expr {$start_position + $validx + 1}] - } - #----------------------------------------------------- - #satisfy test parse_withdef_values_no_phantom_default - foreach vname [dict keys $values_dict] { - if {[string is integer -strict $vname]} { - #ignore vname that is a positionalidx - #review - always trailing - could break? - continue - } - if {$vname ni $valnames_received && ![dict exists $VAL_DEFAULTS $vname]} { - #remove the name with empty-string default we used to establish fixed order of names - #The 'values' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - dict unset values_dict $vname - } - } - #----------------------------------------------------- - #JJJJJJ - #if {[dict size $argument_clause_typestate]} { - # puts ">>>>>[dict get $argspecs id] typestate $argument_clause_typestate" - #} - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } - } - - if {$valmax == -1} { - #only check min - if {$num_values < $valmin} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } else { - if {$num_values < $valmin || $num_values > $valmax} { - if {$valmin == $valmax} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $valmin and $valmax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } - } - - #assertion - opts keys are full-length option names if -any|-arbitrary was false or if the supplied option as a shortname matched one of our defined options - #(and may still contain non-final flag_ident entries such as -increasing|-SORTDIRECTION) - - - #opts explicitly marked as -optional 0 must be present - regardless of -any|-arbitrary (which allows us to ignore additional opts to pass on to next call) - #however - if -any|-arbitrary is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $OPT_REQUIRED $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $VAL_REQUIRED $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength $LEADER_REQUIRED]} { - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - } - if {[llength $OPT_REQUIRED]} { - set api_opt_required [lmap v $OPT_REQUIRED {lindex [split $v |] end}] - if {[llength [set missing [punklib_ldiff $api_opt_required $flagsreceived]]]} { - set full_missing [dict get $lookup_optset $missing] - set msg "Required option missing for %caller%. missing flags: '$full_missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $full_missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - } - if {[llength $VAL_REQUIRED]} { - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - } - - #--------------------------------------------------------------------------------------------- - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - #--------------------------------------------------------------------------------------------- - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "get_dict>>>>>>>> ---opts_and_values:$opts_and_values" - #puts " >>>>>>> ---lookup_optset :$lookup_optset" - #puts "---argstate:$argstate" - #JJJ argname_or_ident; ident example: -increasing|-SORTOPTION - tcl::dict::for {argname_or_ident value_group} $opts_and_values { - # - #parsekey: key used in resulting leaders opts values dictionaries - # often distinct from the full argname in the ARG_INFO structure - # - if {[string match -* $argname_or_ident]} { - #ident format only applies to options/flags - if {[string first | $argname_or_ident] > -1} { - #flag_ident style (grouped fullname of option with -parsekey) - lassign [split $argname_or_ident |] fullflag parsekey ;#we expect only a single pipe in ident form | - if {[dict exists $lookup_optset $fullflag]} { - set argname [dict get $lookup_optset $fullflag] - #idents should already have correct parsekey - } else { - puts stderr "punk::args::get_dict unable to find $fullflag in $lookup_optset (parsekey:$parsekey) (value_group: $value_group)" - } - } else { - if {[dict exists $lookup_optset $argname_or_ident]} { - #get full option name such as -fg|-foreground from non-alias name such as -foreground - #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined - set argname [dict get $lookup_optset $argname_or_ident] - set pkoverride [Dict_getdef $argstate -parsekey ""] - if {$pkoverride ne ""} { - set parsekey $pkoverride - } else { - #default parsekey: last element in argname without trailing = - set parsekey [string trimright [lindex [split $argname |] end] =] - } - } else { - puts stderr "punk::args::get_dict unable to find $argname_or_ident in $lookup_optset (value_group: $value_group)" - } - } - } else { - set argname $argname_or_ident - set pkoverride [Dict_getdef $argstate -parsekey ""] - if {$pkoverride ne ""} { - set parsekey $pkoverride - } else { - #leader or value of form x|y has no special meaning and forms the parsekey in entirety by default. - set parsekey $argname - } - } - #assert: argname is the key for the relevant argument info in the FORMS//ARG_INFO dict. (here each member available as $argstate) - #argname is usually the full name as specified in the definition: - #e.g -f|-path|--filename= - # (where the parsekey will be by default --filename, possibly overridden by -parsekey value) - #an example argname_or_compound for the above might be: -path|--filename - # where -path is the expanded form of the actual flag used (could have been for example just -p) and --filename is the parsekey - - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set typelist [tcl::dict::get $thisarg -type] - set clause_size [llength $typelist] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - #JJJJ - #if {$is_multiple} { - # set vlist $value_group - #} else { - # set vlist [list $value_group] - #} - ##JJJJ - #if {$clause_size == 1} { - # set vlist [list $vlist] - #} - - - #JJ 2025-07-25 - set vlist [list] - #vlist is a list of clauses. Each clause is a list of values of length $clause_size. - #The common case is clause_size 1 - but as we need to treat each clause as a list during validation - we need to list protect the clause when clause_size == 1. - if {$is_multiple} { - if {$clause_size == 1} { - foreach c $value_group { - lappend vlist [list $c] - } - } else { - set vlist $value_group - } - } else { - if {$clause_size ==1} { - set vlist [list [list $value_group]] - } else { - set vlist [list $value_group] - } - } - set vlist_typelist [list] - if {[dict exists $argument_clause_typestate $argname]} { - #lookup saved newtypelist (argument_clause_typelist) from can_assign_value result where some optionals were given type ?omitted-? or ?defaulted-? - # args.test: parse_withdef_value_clause_missing_optional_multiple - set vlist_typelist [dict get $argument_clause_typestate $argname] - } else { - foreach v $vlist { - lappend vlist_typelist $typelist - } - } - - - - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach clause_value $vlist { - #lappend vlist_check [punk::ansi::ansistrip $clause_value] - set stripped [list] - foreach element $clause_value { - lappend stripped [punk::ansi::ansistrip $element] - } - lappend vlist_check $stripped - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - switch -- [Dict_getdef $thisarg -ARGTYPE unknown] { - leader { - set dname leaders_dict - set argclass "Leading argument" - } - option { - set dname opts - set argclass Option - } - value { - set dname values_dict - set argclass "Trailing argument" - } - default { - set dname "_unknown_" ;#NA - set argclass "Unknown argument" - } - } - set vlist_validate [list] - set vlist_check_validate [list] - set vlist_typelist_validate [list] - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestricted 0 where some selections match and others don't) - if {$parsekey in $receivednames && $has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choiceprefixreservelist [Dict_getdef $thisarg -choiceprefixreservelist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - set clause_index -1 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - #J2 - #set vlist_validate [list] - #set vlist_check_validate [list] - foreach clause $vlist clause_check $vlist_check clause_typelist $vlist_typelist { - incr clause_index - set element_index -1 ;#element within clause - usually clause size is only 1 - foreach e $clause e_check $clause_check { - incr element_index - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set choices_test $allchoices - set v_test $c_check - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$chosen eq "" || $chosen in $choiceprefixreservelist} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - #our existing values in $dname are not list-protected - so we need to check clause_size - if {$choice_in_list && !$choice_exact_match} { - set existing [tcl::dict::get [set $dname] $argname_or_ident] - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - #single choice allowed per clause-member - if {$is_multiple} { - if {$clause_size == 1} { - #no list wrapping of single element in $dname dict - so don't index into it with element_index - lset existing $element_index $chosen - } else { - lset existing $clause_index $element_index $chosen - } - tcl::dict::set $dname $argname_or_ident $existing - } else { - #test: choice_multielement_clause - lset existing $element_index $chosen - tcl::dict::set $dname $argname_or_ident $existing - } - } else { - if {$is_multiple} { - #puts ">>> existing $existing $choice_idx" - if {$clause_size == 1} { - #no list wrapping of single element in $dname dict - so don't index into it with element_index - lset existing $clause_index $choice_idx $chosen - } else { - lset existing $clause_index $element_index $choice_idx $chosen - } - tcl::dict::set $dname $argname_or_ident $existing - } else { - lset existing $element_index $choice_idx $chosen - tcl::dict::set $dname $argname_or_ident $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $clause_index $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - #JJJ - #lappend vlist_validate $c - #lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } else { - #choice is in list or matches default - no validation for this specific element in the clause - lset clause_typelist $element_index any - } - incr choice_idx - } - - } ;#end foreach e in clause - #jjj 2025-07-16 - #if not all clause_typelist are 'any' - if {[lsearch -not $clause_typelist any] > -1} { - #at least one element still needs validation - lappend vlist_validate $clause - lappend vlist_check_validate $clause_check - lappend vlist_typelist_validate $clause_typelist - } - - - } ;#end foreach clause in vlist - - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - set vlist_typelist $vlist_typelist_validate - } - - #todo - don't add to validation lists if not in receivednames - #if we have an optionset such as "-f|-x|-etc"; the parsekey is -etc (unless it was overridden by -parsekey in definition) - if {$parsekey ni $receivednames} { - set vlist [list] - set vlist_check_validate [list] - } else { - if {[llength $vlist] && $has_default} { - #defaultval here is a value for the entire clause. (clause usually length 1) - #J2 - #set vlist_validate [list] - #set vlist_check_validate [list] - set tp [dict get $thisarg -type] - set clause_size [llength $tp] - foreach clause_value $vlist clause_check $vlist_check clause_typelist $vlist_typelist { - #JJJJ - #REVIEW!!! we're inadvertently adding back in things that may have already been decided in choicelist loop as not requiring validation? - if {$clause_value ni $vlist_validate} { - if {$clause_size ==1} { - #for -choicemultiple with default that could be a list use 'ni' - #?? review! - if {[lindex $clause_check 0] ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check - lappend vlist_typelist_validate $clause_typelist - } - } else { - if {$clause_check ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check - lappend vlist_typelist_validate $clause_typelist - } - } - } - #if {[llength $tp] == 1} { - # if {$clause_value ni $vlist_validate} { - # #for -choicemultiple with default that could be a list use 'ni' - # #?? review! - # if {[lindex $clause_check 0] ne $defaultval} { - # lappend vlist_validate $clause_value - # lappend vlist_check_validate $clause_check - # } - # } - #} else { - # if {$clause_value ni $vlist_validate} { - # if {$clause_check ne $defaultval} { - # lappend vlist_validate $clause_value - # lappend vlist_check_validate $clause_check - # } - # } - #} - #Todo? - #else ??? - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - set vlist_typelist $vlist_typelist_validate - } - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach clause_value $vlist { - foreach e $clause_value { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - #$t = clause column - - #for {set clausecolumn 0} {$clausecolumn < [llength $typelist]} {incr clausecolumn} {} - set clausecolumn -1 - foreach typespec $typelist { - incr clausecolumn - if {[dict exists $thisarg -typedefaults]} { - set tds [dict get $thisarg -typedefaults] - if {[lindex $vlist $clausecolumn] eq [lindex $tds $clausecolumn]} { - continue - } - } - - set type_expression [string trim $typespec ?] - if {$type_expression in {any none unknown}} { - continue - } - #puts "$argname - switch on type_expression: $type_expression v:[lindex $vlist $clausecolumn]" - #set typespec [lindex $typelist $clausecolumn] - #todo - handle type-alternates e.g -type char|double - #------------------------------------------------------------------------------------ - #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist - _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs - #------------------------------------------------------------------------------------ - - - #todo - pass validation if matches an entry in -typedefaults - #has_typedefault? - #set typedefault [lindex $typedefaults $clausecolumn] - - - } - - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname_or_ident $stripped_list - } - option { - tcl::dict::set opts $argname_or_ident $stripped_list - } - value { - tcl::dict::set values_dict $argname_or_ident $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname_or_ident [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname_or_ident [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict $argname_or_ident [lindex $stripped_list 0] - } - } - } - } - } - - set finalopts [dict create] - dict for {o v} $opts { - if {[string first | $o] > -1} { - #set parsekey [lindex [split $o |] end] - dict set finalopts [lindex [split $o |] end] $v - } else { - dict set finalopts $o $v - } - } - set docid [dict get $argspecs id] - return [tcl::dict::create leaders $leaders_dict opts $finalopts values $values_dict received $received_posns solos $solosreceived multis $multisreceived id $docid] - } - - lappend PUNKARGS [list { - @id -id ::punk::args::forms - @cmd -name punk::args::forms\ - -summary\ - "List command forms."\ - -help\ - "Return names for each form of a command identified by 'id'. - Most commands are single-form and will only return the name '_default'." - @leaders -min 0 -max 0 - @opts - @values -min 1 -max 1 - id -multiple 0 -optional 0 -help\ - "Exact id of command" - }] - proc forms {id} { - set spec [get_spec $id] - if {[dict size $spec]} { - return [dict get $spec form_names] - } else { - return [list] - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::eg - @cmd -name punk::args::eg\ - -summary\ - "Command examples."\ - -help\ - "Return command examples from -help in @examples - directive of a command definition." - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc eg {args} { - set argd [punk::args::parse $args withid ::punk::args::eg] - lassign [dict values $argd] leaders opts values received - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - return [dict get $spec examples_info -help] - } - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis\ - -summary\ - "Command synopsis"\ - -help\ - "Return synopsis for each form of a command - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -noheader -type none - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set NI [punk::ansi::a+ noitalic] - #for inner question marks marking optional type - set IS [punk::ansi::a+ italic strike] - set NIS [punk::ansi::a+ noitalic nostrike] - #set RST [punk::ansi::a] - set RST "\x1b\[m" - } else { - set I "" - set NI "" - set IS "" - set NIS "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values received - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {[string is integer -strict $form]} { - set f [lindex $form_names $form] - if {$f ne ""} { - set form_names [list $f] - } else { - set form_names [list] - } - } else { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - } - - set SYND [dict create] - dict set SYND cmd_info [dict get $spec cmd_info] - #leading "# " required (punk::ns::synopsis will pass through) - if {![dict exists $received -noheader]} { - set syn "# [Dict_getdef $spec cmd_info -summary ""]\n" - } - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND FORMS $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - #foreach argname [dict get $forminfo LEADER_NAMES] { - # set arginfo [dict get $forminfo ARG_INFO $argname] - # set ARGD [dict create argname $argname class leader] - # if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display [lindex [dict get $arginfo -choices] 0] - # } elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - # } else { - # set display $I$argname$RST - # } - # if {[dict get $arginfo -optional]} { - # append syn " ?$display?" - # } else { - # append syn " $display" - # } - # dict set ARGD type [dict get $arginfo -type] - # dict set ARGD optional [dict get $arginfo -optional] - # dict set ARGD display $display - # dict lappend SYND $f $ARGD - #} - set FORMARGS [list] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set typelist [dict get $arginfo -type] - if {[llength $typelist] == 1} { - set tp [lindex $typelist 0] - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - #set arg_display [dict get $arginfo -typesynopsis] - set clause $ts - } else { - #set arg_display $argname - set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - set type_alternatives [_split_type_expression $tp] - foreach tp_alternative $type_alternatives { - set firstword [lindex $tp_alternative 0] - switch -exact -- $firstword { - literal { - set match [lindex $tp_alternative 1] - lappend alternates $match - } - literalprefix { - #todo - trie styling on prefix calc - set match [lindex $tp_alternative 1] - lappend alternates $match - } - stringstartswith { - set match [lindex $tp_alternative 1] - lappend alternates $match* - } - stringendswith { - set match [lindex $tp_alternative 1] - lappend alternates *$match - } - default { - lappend alternates $I$argname$NI - } - } - - #if {$tp_alternative eq "literal"} { - # lappend alternates [lindex $argname end] - #} elseif {[string match literal(*) $tp_alternative]} { - # set match [string range $tp_alternative 8 end-1] - # lappend alternates $match - #} elseif {[string match literalprefix(*) $tp_alternative]} { - # set match [string range $tp_alternative 14 end-1] - # lappend alternates $match - #} else { - # lappend alternates $I$argname$NI - #} - } - #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) - #todo - trie prefixes display - set alternates [punk::args::lib::lunique $alternates] - set clause [join $alternates |] - } - } else { - set n [expr {[llength $typelist]-1}] - set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types - set clause "" - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - set tp_displaylist $ts - } else { - set tp_displaylist [lrepeat [llength $typelist] ""] - } - - foreach typespec $typelist td $tp_displaylist elementname $name_tail { - #elementname will commonly be empty - if {[string match {\?*\?} $typespec]} { - set tp [string range $typespec 1 end-1] - set member_optional 1 - } else { - set tp $typespec - set member_optional 0 - } - if {$tp eq "literal"} { - set c $elementname - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set c $match - } else { - if {$td eq ""} { - set c $I$tp$NI - } else { - set c $td - } - } - if {$member_optional} { - append clause " " "(?$c?)" - } else { - append clause " " $c - } - } - set clause [string trimleft $clause] - } - - set ARGD [dict create argname $argname class leader] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - #set display "?$I$argname$NI?..." - set display "?$clause?..." - } else { - set display "?$clause?" - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "?[lindex [dict get $arginfo -choices] 0]?" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display "?$argname?" - #} else { - # set display "?$I$argname$NI?" - #} - } - } else { - if {[dict get $arginfo -multiple]} { - #set display "$I$argname$NI ?$I$argname$NI?..." - set display "$clause ?$clause?..." - } else { - set display $clause - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "[lindex [dict get $arginfo -choices] 0]" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - #} else { - # set display "$I$argname$NI" - #} - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - - #dict lappend SYND $f $ARGD - lappend FORMARGS $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {$tp eq "none"} { - #assert - argname may have aliases delimited by | - but no aliases end with = - #(disallowed in punk::args::define) - set argdisplay $argname - } else { - #assert [llength $tp] == 1 (multiple values for flag unspported in punk::args::define) - if {[string match {\?*\?} $tp]} { - set tp [string range $tp 1 end-1] - set value_is_optional true - } else { - set value_is_optional false - } - - - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - set tp_display $ts - #user may or may not have remembered to match the typesynopsis with the optionality by wrapping with ? - #review - if user wrapped with ?*? and also leading/trailing ANSI - we won't properly strip - #todo - enforce no wrapping '?*?' in define for -typesynopsis? - set tp_display [string trim $tp_display ?] - } else { - - set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_alternative [split $tp |] { - #-type literal not valid for opt - review - if {[string match literal(*) $tp_alternative]} { - set match [string range $tp_alternative 8 end-1] - lappend alternates $match - } elseif {[string match literalprefix(*) $tp_alternative]} { - set match [string range $tp_alternative 14 end-1] - lappend alternates $match - } else { - lappend alternates <$I$tp_alternative$NI> - } - } - #todo - trie prefixes display? - set alternates [punk::args::lib::lunique $alternates] - set tp_display [join $alternates |] - } - if {[string first | $tp_display] >=0} { - #need to bracket alternate-types to distinguish pipes delimiting flag aliases - set tp_display "($tp_display)" - } - set argdisplay "" - foreach aliasflag [split $argname |] { - if {[string match --* $aliasflag]} { - if {[string index $aliasflag end] eq "="} { - set alias [string range $aliasflag 0 end-1] - if {$value_is_optional} { - append argdisplay "$alias$IS?$NIS=$tp_display$IS?$NIS|" - } else { - append argdisplay "$alias=$tp_display|" - } - } else { - if {$value_is_optional} { - append argdisplay "$aliasflag $IS?$NIS$tp_display$IS?$NIS|" - } else { - append argdisplay "$aliasflag $tp_display|" - } - } - } else { - if {$value_is_optional} { - #single dash flag can't accept optional value - append argdisplay "$aliasflag|" - } else { - append argdisplay "$aliasflag $tp_display|" - } - } - } - set argdisplay [string trimright $argdisplay |] - } - - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - set display "?$argdisplay?..." - } else { - set display "?$argdisplay?" - } - } else { - if {[dict get $arginfo -multiple]} { - set display "$argdisplay ?$argdisplay?..." - } else { - set display $argdisplay - } - } - - #if {[string index $argname end] eq "="} { - # set __ "" - #} else { - # set __ " " - #} - #if {[dict get $arginfo -optional]} { - # if {[dict get $arginfo -multiple]} { - # if {$tp eq "none"} { - # set display "?$argname?..." - # } else { - # set display "?$argname$__$tp_display?..." - # } - # } else { - # if {$tp eq "none"} { - # set display "?$argname?" - # } else { - # set display "?$argname$__$tp_display?" - # } - # } - #} else { - # if {[dict get $arginfo -multiple]} { - # if {$tp eq "none"} { - # set display "$argname ?$argname...?" - # } else { - # set display "$argname$__$tp_display ?$argname$__$tp_display?..." - # } - # } else { - # if {$tp eq "none"} { - # set display $argname - # } else { - # set display "$argname$__$tp_display" - # } - # } - #} - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - #dict lappend SYND $f $ARGD - lappend FORMARGS $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set typelist [dict get $arginfo -type] - if {[llength $typelist] == 1} { - set tp [lindex $typelist 0] - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - #set arg_display [dict get $arginfo -typesynopsis] - set clause $ts - } else { - #set arg_display $argname - set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_alternative [split $tp |] { - if {$tp_alternative eq "literal"} { - lappend alternates [lindex $argname end] - } elseif {[string match literal(*) $tp_alternative]} { - set match [string range $tp_alternative 8 end-1] - lappend alternates $match - } elseif {[string match literalprefix(*) $tp_alternative]} { - set match [string range $tp_alternative 14 end-1] - lappend alternates $match - } else { - lappend alternates $I$argname$NI - } - } - #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) - #todo - trie prefixes display - set alternates [punk::args::lib::lunique $alternates] - set clause [join $alternates |] - } - } else { - set n [expr {[llength $typelist]-1}] - set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types - set clause "" - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - set tp_displaylist $ts - } else { - set tp_displaylist [lrepeat [llength $typelist] ""] - } - - foreach typespec $typelist td $tp_displaylist elementname $name_tail { - #elementname will commonly be empty - if {[string match {\?*\?} $typespec]} { - set tp [string range $typespec 1 end-1] - set member_optional 1 - } else { - set tp $typespec - set member_optional 0 - } - #handle alternate-types e.g literal(text)|literal(binary) - set alternates [list] - foreach tp_alternative [split $tp |] { - if {$tp_alternative eq "literal"} { - lappend alternates $elementname - } elseif {[string match literal(*) $tp_alternative]} { - set match [string range $tp_alternative 8 end-1] - lappend alternates $match - } elseif {[string match literalprefix(*) $tp_alternative]} { - set match [string range $tp_alternative 14 end-1] - lappend alternates $match - } else { - if {$td eq ""} { - lappend alternates $I$tp$NI - } else { - lappend alternates $td - } - } - } - set alternates [punk::args::lib::lunique $alternates] - set c [join $alternates |] - if {$member_optional} { - append clause " " "(?$c?)" - } else { - append clause " " $c - } - } - set clause [string trimleft $clause] - } - - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - #set display "?$I$argname$NI?..." - set display "?$clause?..." - } else { - set display "?$clause?" - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "?[lindex [dict get $arginfo -choices] 0]?" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display "?$argname?" - #} else { - # set display "?$I$argname$NI?" - #} - } - } else { - if {[dict get $arginfo -multiple]} { - #set display "$I$argname$NI ?$I$argname$NI?..." - set display "$clause ?$clause?..." - } else { - set display $clause - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "[lindex [dict get $arginfo -choices] 0]" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - #} else { - # set display "$I$argname$NI" - #} - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - #dict lappend SYND $f $ARGD - lappend FORMARGS $ARGD - } - append syn \n - dict set SYND FORMS $f $FORMARGS - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - set FORMS [dict get $SYND FORMS] - dict for {form arglist} $FORMS { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::ensemble_subcommands_definition - @cmd -name ::punk::args::ensemble_subcommands_definition -help\ - "Helper function to return a punk::args definition snippet for ensemble subcommands" - @leaders -max 0 -min 0 - -groupdict -default {} -type dict -help\ - "Dictionary keyed on arbitrary groupname, where value - is a list of known subcommands that should be displayed - by groupname. Each groupname forms the title of a subtable - in the choices list. - Subcommands not assigned to a groupname will appear first - in an untitled subtable." - -columns -default 4 -type integer -help\ - "Max number of columns for all subtables in the choices - display area" - @values -min 1 -max 1 - ensemble -optional 0 -help\ - "Name of ensemble command" - - }] - proc ensemble_subcommands_definition {args} { - #args manually parsed - with use of argdef for unhappy-path only - if {![llength $args]} { - punk::args::parse $args -errorstyle minimal withid ::punk::args::ensemble_subcommands_definition - return - } - set ensemble [lindex $args end] - set optlist [lrange $args 0 end-1] - if {[llength $optlist] % 2} { - punk::args::parse $args -errorstyle minimal withid ::punk::args::ensemble_subcommands_definition - return - } - set defaults [dict create\ - -groupdict {}\ - -columns 4\ - ] - set optlist [dict merge $defaults $optlist] - dict for {k v} $optlist { - switch -- $k { - -groupdict - -columns {} - default { - punk::args::parse $args -errorstyle minimal withid ::punk::args::ensemble_subcommands_definition - return - } - } - } - set opt_groupdict [dict get $optlist -groupdict] - set opt_columns [dict get $optlist -columns] - - #warning - circular package dependency if we try to use this function on punk::ns! - package require punk::ns - set subdict [punk::ns::ensemble_subcommands -return dict $ensemble] - set allsubs [dict keys $subdict] - # ---------------------------------------------- - # manually defined group members may have subcommands that are obsoleted/missing - # we choose to make the situation obvious by re-classifying into a corresponding group with the " - MISSING" suffix - set checked_groupdict [dict create] - dict for {g members} $opt_groupdict { - set validmembers {} - set invalidmembers {} - foreach m $members { - if {$m in $allsubs} { - lappend validmembers $m - } else { - lappend invalidmembers $m - } - } - dict set checked_groupdict $g $validmembers - if {[llength $invalidmembers]} { - dict set checked_groupdict "${g}_MISSING" $invalidmembers - } - } - if {[dict exists $checked_groupdict ""]} { - set others [dict get $checked_groupdict ""] - dict unset checked_groupdict "" - } else { - set others [list] - } - - #REVIEW - set debug 0 - if {$debug} { - puts "punk::args::ensemble_subcommands_definition" - if {[catch { - ::punk::lib::pdict checked_groupdict - } msg]} { - puts stderr "punk::args::ensemble_subcommands_definition Cannot call pdict\n$msg" - } - puts -------------------- - puts "$checked_groupdict" - puts -------------------- - } - - set opt_groupdict $checked_groupdict - # ---------------------------------------------- - set allgrouped [list] - dict for {g members} $opt_groupdict { - lappend allgrouped {*}$members - } - set choiceinfodict [dict create] - foreach {sc cmd} $subdict { - if {$sc ni $allgrouped} { - if {$sc ni $others} { - lappend others $sc - } - } - - #don't use full cmdinfo if $cmd is a single element - if {[llength $cmd] == 1} { - set cinfo [punk::ns::cmdwhich $cmd] - set tp [dict get $cinfo whichtype] - } else { - puts stderr "WARNING ==ensemble_subcommands_definition== cmdinfo $cmd\n$cinfo" - set cinfo [punk::ns::cmdinfo {*}$cmd] - set tp [dict get $cinfo cmdtype] - } - - dict set choiceinfodict $sc [list [list resolved $cmd]] - - switch -- $tp { - ensemble - native { - dict lappend choiceinfodict $sc [list doctype $tp] - } - object { - dict lappend choiceinfodict $sc [list doctype oo] - } - } - - #could be more than one punk::args id - choose a precedence by how we order the id_exists checks. - if {[punk::args::id_exists [list $ensemble $sc]]} { - dict lappend choiceinfodict $sc {doctype punkargs} - dict lappend choiceinfodict $sc [list subhelp {*}$ensemble $sc] - } elseif {[punk::args::id_exists $cmd]} { - dict lappend choiceinfodict $sc {doctype punkargs} - dict lappend choiceinfodict $sc [list subhelp {*}$cmd] - } elseif {[punk::args::id_exists [dict get $cinfo origin]]} { - dict lappend choiceinfodict $sc {doctype punkargs} - dict lappend choiceinfodict $sc [list subhelp {*}[dict get $cinfo origin]] - } else { - #puts stderr "ensemble_subcommands_definition--- NO doc for [list $ensemble $sc] or $cmd or [dict get $cinfo origin]" - } - - #if {[punk::args::id_exists [dict get $cinfo origin]] || [punk::args::id_exists [list $ensemble $sc]]} { - # dict lappend choiceinfodict $sc {doctype punkargs} - #} - } - - set argdef "" - append argdef "subcommand -choicegroups \{" \n - append argdef " \"\" \{$others\}" \n - dict for {g members} $opt_groupdict { - append argdef " \"$g\" \{$members\}" \n - } - append argdef " \} -choicecolumns $opt_columns -choiceinfo {$choiceinfodict}" \n - - #todo -choicelabels - #detect subcommand further info available e.g if oo or ensemble or punk::args id exists.. - #consider a different mechanism to add a label on rhs of same line as choice (for (i) marker) - - return $argdef - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #tcl86 compat for string is dict - but without -strict or -failindex options - if {[catch {string is dict {}} errM]} { - proc string_is_dict {args} { - #ignore opts - set str [lindex $args end] - if {[catch {llength $str} len]} { - return 0 - } - if {$len % 2 == 0} { - return 1 - } - return 0 - } - } else { - proc string_is_dict {args} { - string is dict {*}$args - } - } - - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - classmethod { - lappend marks [punk::ns::Cmark classmethod term-orange1] - } - coremethod { - lappend marks [punk::ns::Cmark coremethod term-plum1] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - objectmethod { - lappend marks [punk::ns::Cmark objectmethod term-orange1] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr\ - -summary\ - "Templating with placeholders such as: \$\{$varName\}"\ - -help\ - "Roughly analogous to js template literals - - Placeholder Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - #JJJ - #REVIEW - #lappend params [subst -nocommands -novariables $expression] - lappend params $expression - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - }] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #order-preserving - #(same as punk::lib) - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::args [tcl::namespace::eval punk::args { - tcl::namespace::path {::punk::args::lib ::punk::args::system} - variable pkg punk::args - variable version - set version 0.2 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/console-0.1.1.tm b/src/bootsupport/modules/punk/console-0.1.1.tm index 3f81d99e..b7c4cd7a 100644 --- a/src/bootsupport/modules/punk/console-0.1.1.tm +++ b/src/bootsupport/modules/punk/console-0.1.1.tm @@ -44,7 +44,14 @@ #[list_begin itemized] package require Tcl 8.6- + +#---------------------------------------------------- +#Although we need to be in an environment with Thread available to use punk::console, +# we don't want to require Thread as a hard dependency in the interp we're running in. +# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp. +#TODO. package require Thread ;#tsv required to sync is_raw +#---------------------------------------------------- package require punk::ansi package require punk::args #*** !doctools @@ -282,7 +289,7 @@ namespace eval punk::console { ignore the regex match 'ok' response and keep going." -return -type string -default payload -choices {payload dict} -choicelabels { - dict\ + dict "dict with keys prefix,response,payload,all" } -help\ "Return format" @@ -290,12 +297,12 @@ namespace eval punk::console { -console -default {stdin stdout} -type list -help\ "console/terminal (currently list of in/out channels) (todo - object?)" -passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels { - none\ + none { ANSI sent without any passthrough wrapping. A terminal multiplexer such as tmux,screen,zellij may not pass the request through to the underlying terminal(s) This is the recommended/normal value for the option.} - tmux\ + tmux { Wrap ANSI sequence with tmux passthrough sequence. \x1bPtmux\;\x1b\\ Note that a tmux session could be connected to multiple @@ -304,7 +311,7 @@ namespace eval punk::console { Passthrough should generally be avoided except for debug/test purposes. } - auto\ + auto { Use existence of ::env(TMUX) to detect tmux and send tmux passthrough sequence. Not recommended except for debug/test purposes. @@ -2779,6 +2786,7 @@ namespace eval punk::console { This allows querying the current style and then re-setting it after temporarily changing it." }] } + proc cursor_style {args} { set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style] lassign [dict values $argd] leaders opts values diff --git a/src/bootsupport/modules/punk/du-0.1.0.tm b/src/bootsupport/modules/punk/du-0.1.0.tm index ab1ed815..5ee354a6 100644 --- a/src/bootsupport/modules/punk/du-0.1.0.tm +++ b/src/bootsupport/modules/punk/du-0.1.0.tm @@ -2114,15 +2114,15 @@ namespace eval punk::du { } proc du_dirlisting_tclvfs {folderpath args} { - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] diff --git a/src/bootsupport/modules/punk/lib-0.1.3.tm b/src/bootsupport/modules/punk/lib-0.1.3.tm deleted file mode 100644 index f6242f76..00000000 --- a/src/bootsupport/modules/punk/lib-0.1.3.tm +++ /dev/null @@ -1,4581 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.3 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.3] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - return true - } else { - return false - } - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - return 0 - } - return [expr {$result ne "a2"}] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - return [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - return $has_bug - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - punk::args::set_alias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - if {"::ledit" ni [info commands ::ledit]} { - interp alias {} ledit {} ::punk::lib::compat::ledit - punk::args::set_alias ::punk::lib::compat::ledit ::ledit - } - proc ledit {lvar first last args} { - upvar $lvar l - #use lindex_resolve to support for example: ledit lst end+1 end+1 h i - set fidx [punk::lib::lindex_resolve [llength $l] $first] - switch -exact -- $fidx { - -3 { - #index below lower bound - set pre [list] - set fidx -1 - } - -2 { - #first index position is greater than index of last element in the list - set pre [lrange $l 0 end] - set fidx [llength $l] - } - default { - #set pre [lrange $l 0 $first-1] - set pre [lrange $l 0 $fidx-1] - } - } - set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -3 { - #index below lower bound - set post [lrange $l 0 end] - } - -2 { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] - set post [lrange $l $lidx+1 end] - } - } - } - set l [list {*}$pre {*}$args {*}$post] - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - proc lswap {lvar a z} { - upvar $lvar l - set len [llength $l] - if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { - #lindex_resolve_basic returns only -1 if out of range - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned) - set a_index [lindex_resolve $len $a] - set a_msg "" - switch -- $a_index { - -2 { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - -3 { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - } - set z_index [lindex_resolve $len $z] - set z_msg "" - switch -- $z_index { - -2 { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - -3 { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [lzip2lists {*}$args]} - 3 {return [lzip3lists {*}$args]} - 4 {return [lzip4lists {*}$args]} - 5 {return [lzip5lists {*}$args]} - 6 {return [lzip6lists {*}$args]} - 7 {return [lzip7lists {*}$args]} - 8 {return [lzip8lists {*}$args]} - 9 {return [lzip9lists {*}$args]} - 10 {return [lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } else { - return [lzipn {*}$args] - } - } - } - } - - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - puts "proc punk::lib::lzip${n}lists {$arglist} \{" - puts "$body" - puts "\}" - proc ::punk::lib::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [punk::lib::check::has_tclbug_lsearch_strideallinline ]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::lzipn_tcl9a] - } - - - namespace import ::punk::args::lib::tstr - - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list array exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list array get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - -- -type none -optional 1 - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-2 for too low, -1 for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == -2} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -3} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve [llength $dval] $b] - if {$upper == -3} { - ##x - #upper bound is below list range - - if {$lower_resolve >=-2} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == -2} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact -- $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - #append result [textblock::join_basic -- $kblock $sblock $vblock] \n - append result [textblock::join_basic_raw $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - if {[llength $small] > [llength $large]} {return 0} - foreach x $large { - ::set ($x) {} - } - foreach x $small { - if {![info exists ($x)]} { - return 0 - } - } - return 1 - } - #v2 generally seems slower - proc is_list_all_in_list2 {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list2] - proc is_list_all_in_list2 {small large} $body - } - - proc is_list_all_ni_list {A B} { - foreach x $B { - ::set ($x) {} - } - foreach x $A { - if {[info exists ($x)]} { - return 0 - } - } - return 1 - } - proc is_list_all_ni_list2 {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list2] - proc is_list_all_ni_list2 {a b} $body - } - - #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist - #struct::set difference may happen to preserve ordering when items are integers, but order can't be relied on, - # especially as struct::set has 2 differing implementations (tcl vs critcl) which return results with different ordering to each other and different deduping behaviour in some cases (e.g empty 2nd arg) - proc ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - #with ledit (also avail in 8.6 using punk::lib::compat::ledit - proc ldiff2 {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - foreach item $removeitems { - set posns [lsearch -all -exact $fromlist $item] - foreach p $posns {ledit fromlist $p $p} - } - return $fromlist - } - proc ldiff3 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - puts stderr "WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - #order-preserving - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {([^+-]*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - punk::args::define { - @id -id ::punk::lib::is_indexset - @cmd -name punk::lib::is_indexset\ - -summary\ - "Validate string is a comma-delimited 'indexset'."\ - -help\ - "Validate that a string is an 'indexset' - - An indexset consists of a comma delimited list of indexes or index-ranges. - The indexes are 0-based. - The normal 'range' specifier is .. - The range specifier can appear at the beginning, middle or end, or even alone to indicate the entire - range of valid values. - e.g the following are all valid ranges - 1.. - (index 1 to max) - ..10 - (index 0 to 10) - 2..11 - (index 2o to 11) - .. - (all indices) - Common whitespace elements space,tab,newlines are ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - see indexset_resolve" - @values -min 2 -max 2 - indexset -type string - } - proc is_indexset {indexset} { - #collapse internal whitespace (for basic whitespace set we allow) - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] - if {![regexp {^[\-\+_end,\.0-9]*$} $indexset]} { - return 0 - } - set ranges [split $indexset ,] - foreach r $ranges { - set validateindices [list] - set rposn [string first .. $r] - if {$rposn >= 0} { - set sepsize 2 - set step 1 - } else { - #check for .n. 'stepped' range - set fdot [string first . $r] - set ldot [string last . $r] - set step [string range $r $fdot+1 $ldot-1] - #todo - allow basic mathops for step: 2+1 2+-1 etc same as tcl lindex, lseq - if {![string is integer -strict $step]} { - } - } - - if {$rposn >= 0} { - lappend validateindices {*}[string range $r 0 $rposn-1] {*}[string range $r $rposn+2 end] - } else { - #'range' is just an index - set validateindices [list $r] - } - foreach v $validateindices { - if {$v eq "" || $v eq "end"} {continue} - if {[string is integer -strict $v]} {continue} - if {[catch {lindex {} $v}]} { - return 0 - } - } - } - return 1 - } - #review - compare to IMAP4 methods of specifying ranges? - punk::args::define { - @id -id ::punk::lib::indexset_resolve - @cmd -name punk::lib::indexset_resolve\ - -summary\ - "Resolve an indexset to a list of integers based on supplied list or string length."\ - -help\ - "Resolve an 'indexset' to a list of actual indices within the range of the provided numitems value. - e.g in a basic case: for a list of 10 items, 'indexset_resolve 10 end' will return the index 9 - - An indexset consists of a comma delimited list of indexes or index-ranges. - The indexes are 0-based. - Ranges must be specified with .. as the separator. - Whitespace is ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - end means the last item. - end-1 means the second last item. - 0.. is the same as 0..end. - indexset examples: - 1,3.. - output the index 1 (2nd item) followed by all from index 3 to the end. - 'indexset_resolve 4 1,3..' -> 1 3 - 'indexset_resolve 10 1,3..' -> 1 3 4 5 6 7 8 9 - 0-2,end - output the first 3 indices, and the last index. - end-1..0 - output the indexes in reverse order from 2nd last item to first item." - @values -min 2 -max 2 - numitems -type integer - indexset -type indexset -help "comma delimited specification for indices to return" - } - proc indexset_resolve {numitems indexset} { - if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { - #use parser on unhappy path only - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] ;#collapse basic whitespace - set index_list [list] ;#list of actual indexes within the range - set iparts [split $indexset ,] - set index_list [list] - foreach ipart $iparts { - set ipart [string trim $ipart] - set rposn [string first .. $ipart] - if {$rposn>=0} { - #range - lassign [punk::lib::string_splitbefore_indices $ipart $rposn $rposn+2] rawa _ rawb - set rawa [string trim $rawa] - set rawb [string trim $rawb] - if {$rawa eq ""} {set rawa 0} - set a [punk::lib::lindex_resolve $numitems $rawa] - if {$a == -3} { - #undershot - leave negative - } elseif {$a == -2 && $rawa ne "-2"} { - #overshot - set a [expr {$numitems}] ;#put it outside the range on the upper side - } - - if {$rawb eq ""} { - if {$a > $numitems-1} { - set rawb $a ;#make sure .. doesn't return last item - should return nothing - } else { - set rawb end - } - } - set b [punk::lib::lindex_resolve $numitems $rawb] - if {$b == -3} { - #undershot - leave negative - } elseif {$b == -2 && $rawb ne "-2"} { - set b [expr {$numitems}] ;#overshot - put it outside the range on the upper side - } - - #e.g make sure .. doesn't return last item - should return nothing as both are above the range. - if {$a >= 0 && $a <= $numitems-1 && $b >=0 && $b <= $numitems-1} { - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - if {$a >= 0 && $a <= $numitems-1} { - #only a is in the range - if {$b < 0} { - set b 0 - } else { - set b [expr {$numitems-1}] - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$b >=0 && $b <= $numitems-1} { - #only b is in the range - if {$a < 0} { - set a 0 - } else { - set a [expr {$numitems-1}] - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - #both outside the range - if {$a < 0 && $b > 0} { - #spans the range in forward order - set a 0 - set b [expr {$numitems-1}] - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$a > 0 && $b < 0} { - #spans the range in reverse order - set a [expr {$numitems-1}] - set b 0 - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } - #both outside of range on same side - } - } - } else { - set idx [punk::lib::lindex_resolve_basic $numitems $ipart] - if {$idx >= 0} { - lappend index_list $idx - } - } - } - return $index_list - } - # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bounds on upper vs lower side - #REVIEW: This shouldn't really need the list itself - just the length would suffice - punk::args::define { - @id -id ::punk::lib::lindex_resolve - @cmd -name punk::lib::lindex_resolve\ - -summary\ - "Resolve an indexexpression to an integer based on supplied list or string length."\ - -help\ - "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 - to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating - whether the index was below or above the range of possible indices for the length supplied. - - Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - This means the proc may be called with something like $x+2 end-$y etc - Sometimes the actual integer index is desired. - - We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - - which could have security risks. - lindex_resolve will parse the index expression and return: - a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) - b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - lindex_resolve never returns -1 - as the similar function lindex_resolve_basic uses this to denote - out of range at either end of the list/string. - Otherwise it will return an integer corresponding to the position in the data. - This is in stark contrast to Tcl list/string function indices which will return empty strings for out of - bounds indices, or in the case of lrange, return results anyway. - Like Tcl list commands - it will produce an error if the form of the index is not acceptable. - For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side - - thus returning -2 - - Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. - We will get something like 10+1 - which can be resolved safely with expr - " - @values -min 2 -max 2 - datalength -type integer - index -type indexexpression - } - proc lindex_resolve {len index} { - #*** !doctools - #[call [fun lindex_resolve] [arg len] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length - #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr - - - #REVIEW - we need compat for 1_000 etc to handle things like toml even in 8.6? - #A basic string map means we aren't properly validating - #todo - be stricter about malformations such as 1000_ - if {![string is integer -strict 1_0]} { - #basic forward compatibility with integers such as 1_000 for 8.6.x - set index [tcl::string::map {_ {}} $index] - set len [tcl::string::map {_ {}} $len] - } - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve len must be a positive integer" - } - - - if {[string is integer -strict $index]} { - #can match +i -i - if {$index < 0} { - return -3 - } elseif {$index >= $len} { - return -2 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - #note - offset could have leading + or - - # 'string is integer -strict +1' ==> true - #e.g end+-1 is valid (end++-1 is not) - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$offset == 0} { - #(offset +0, -0 or 0 or 000 0_0 etc) - #op either + or - is irrelevant - set index [expr {$len-1}] - if {$index < 0} { - return -2 ;#special case - equivalent to 'end', with empty list - treat like a positive number out of bounds - } else { - return $index - } - } - - set index [if {$op eq "+"} {expr {($len-1) + $offset}} else {expr {($len-1) - $offset}}] - if {$index < 0} { - return -3 - } elseif {$index > $len-1} { - return -2 - } else { - return $index - } - } else { - #index is 'end' - if {$len == 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return -2 - } - return [expr {$len - 1}] - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - #regexp must split a++b to a + +b (not a+ + b) ie first +/- is the op - if {[regexp {([^+-]*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < 0} { - return -3 - } elseif {$index >= $len} { - return -2 - } - return $index - } - } - } - proc lindex_resolve_basic {len index} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg len] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -1 for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - if {![string is integer -strict $len]} { - error "lindex_resolve_basic len must be an integer" - } - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= $len)} { - #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. - return -1 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {$len > 0} { - #For large len - this is a wasteful allocation if no true lseq available in Tcl version. - #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) - set testlist [punk::lib::range 0 [expr {$len-1}]] ;# uses lseq if available, has fallback. - } else { - set testlist [list] - #we want to call 'lindex' even in this case - to get the appropriate error message - } - set idx [lindex $testlist $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -1 - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - proc string_splitbefore {str index} { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -2 { - return [list $str ""] - } - -3 { - return [list "" $str] - } - } - } - return [list [string range $str 0 $index-1] [string range $str $index end]] - #scan %s stops at whitespace - not useful here. - #scan $s %${p}s%s - } - proc string_splitbefore_indices {str args} { - set parts [list $str] - set sizes [list [string length $str]] - set s 0 - foreach index $args { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -2 { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - -3 { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - } - } - if {$index <= 0} { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - if {$index >= [string length $str]} { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - set i -1 - set a 0 - foreach sz $sizes { - incr i - if {$a + $sz > $index} { - set p [lindex $parts $i] - #puts "a:$a index:$index" - if {$a == $index} { - break - } - ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] - ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] - break - } - incr a $sz - } - #puts "->parts:$parts" - #puts "->sizes:$sizes" - } - return $parts - } - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - proc lcm {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the lowest common multiple of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para] - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text {max -1}} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - if {$max != -1} { - set len [expr {min($len,$max)}] - } - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - ;#package require punk::ansi - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list/detectcode_in_list will check at first level. (not intended for detecting ansi in deeper structures) - - #we use detectcode_in_list instead of detect_in_list - #detectcode_in_list will detect unclosed (or unopened) paired sequences such as PM (privacy message) - # - but the main reason is it is slightly faster. - if {![punk::ansi::ta::detectcode_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [string trim $ln] - if {!$in_jt} { - if {[string match *jumpTable* $ln]} { - append result $ln \n - set in_jt 1 - } - } else { - if {[string match Command* $tln] || [string match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc valcopy {obj} { - append obj2 $obj {} - } - proc set_valcopy {varname obj} { - #used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_valcopy varnmame $val] - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [valcopy $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [valcopy $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [valcopy $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - #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? - if {$has_twapi} { - interp alias "" ::punk::lib::uuid "" twapi::new_uuid - } else { - catch {package require uuid} - interp alias "" ::punk::lib::uuid "" uuid::uuid generate - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.3 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/lib-0.1.4.tm b/src/bootsupport/modules/punk/lib-0.1.4.tm deleted file mode 100644 index 742223ff..00000000 --- a/src/bootsupport/modules/punk/lib-0.1.4.tm +++ /dev/null @@ -1,4981 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.4 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.4] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - - #NOTE - the extension ns becomes the '-namespace ' for the original routine name, - #with -unknown handling the original subcommands. - #This makes the original ensemble harder to introspect! - #e.g (the original -map or -namespace not visible) - #In this specific case (which, being published on the wiki might be common in the wild) - #we could call {*}[namespace ensemble configure $routine -unknown] $routine - #and then detect that the first resulting word is an ensemble - #For arbitrary '-unknown scripts' - sensible introspection is likely not possible - - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - #These are just a selection of bugs relevant to punk behaviour (or of specific interest to the author) - #Not any sort of comprehensive check of known tcl bugs. - #These are reported in warning output of 'help tcl' - or used for workarounds in some cases. - proc has_tclbug_regexp_emptystring {} { - #The regexp {} [...] trick - code in brackets only runs when non byte-compiled ie in traces - #This was usable as a hack to create low-impact calls that only ran in an execution trace context - handy for debugger logic, - #but as an apparent violation of Tcl's normal parsing rules - was evidently seen as a bug and fixed in: - #https://core.tcl-lang.org/tcl/info/cb03e57a (tcl 9.0.3+ ?) - set bug [expr {![catch {regexp {} [error should_error]}]}] - return [dict create bug $bug bugref cb03e57a description {regexp emptystring first argument over-optimised - difference in compiled vs traced behaviour.} level minor] - } - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - set bug true - } else { - set bug false - } - set description "string rep for list variable in script generated when script changed\n(not an acknowledged/reported bug)" - return [dict create bug $bug bugref "" description $description level minor] - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - set bug 0 - } else { - set bug [expr {$result ne "a2"}] - } - set description "lsearch -stride with -subindices -inline -all and single index - incorrect results." - return [dict create bug $bug bugref 5a1aaa201d description $description level major] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - set bug [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - set description "lists elements not properly quoted in some cases. e.g 'list {*}[lindex {etc #foo} 1] {*}[list]' (#foo not braced)" - return [dict create bug $bug bugref e38dc74e2 description $description level medium] - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - #https://core.tcl-lang.org/tcl/tktview/1095bf7f756f9aed6bde - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - set description "ensemble commands not compiled in safe interps - heavy performance impact in safe interps" - return [dict create bug $has_bug bugref 1095bf7f756f9aed6bde description $description level major] - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - punk::args::set_idalias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - if {"::ledit" ni [info commands ::ledit]} { - interp alias {} ledit {} ::punk::lib::compat::ledit - punk::args::set_idalias ::punk::lib::compat::ledit ::ledit - } - proc ledit {lvar first last args} { - upvar $lvar l - #use lindex_resolve to support for example: ledit lst end+1 end+1 h i - set fidx [punk::lib::lindex_resolve [llength $l] $first] - switch -exact -- $fidx { - -Inf { - #index below lower bound - set pre [list] - set fidx -1 - } - Inf { - #first index position is greater than index of last element in the list - set pre [lrange $l 0 end] - set fidx [llength $l] - } - default { - #set pre [lrange $l 0 $first-1] - set pre [lrange $l 0 $fidx-1] - } - } - set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -Inf { - #index below lower bound - set post [lrange $l 0 end] - } - Inf { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] - set post [lrange $l $lidx+1 end] - } - } - } - set l [list {*}$pre {*}$args {*}$post] - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list ::tcl::info::vars]] - set nscaller [uplevel 1 [list ::tcl::namespace::current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - namespace eval argdoc { - #non-colour SGR codes - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - } - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - proc lswap {lvar a z} { - upvar $lvar l - set len [llength $l] - if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { - #lindex_resolve_basic returns only -Inf if out of range at either bound - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -Inf and Inf special results being lower and upper bound breaches respectively - set a_index [lindex_resolve $len $a] - set a_msg "" - switch -- $a_index { - -Inf { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - Inf { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - } - set z_index [lindex_resolve $len $z] - set z_msg "" - switch -- $z_index { - -Inf { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - Inf { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [lzip2lists {*}$args]} - 3 {return [lzip3lists {*}$args]} - 4 {return [lzip4lists {*}$args]} - 5 {return [lzip5lists {*}$args]} - 6 {return [lzip6lists {*}$args]} - 7 {return [lzip7lists {*}$args]} - 8 {return [lzip8lists {*}$args]} - 9 {return [lzip9lists {*}$args]} - 10 {return [lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } else { - return [lzipn {*}$args] - } - } - } - } - - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - puts "proc punk::lib::lzip${n}lists {$arglist} \{" - puts "$body" - puts "\}" - proc ::punk::lib::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [dict get [punk::lib::check::has_tclbug_lsearch_strideallinline] bug]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::lzipn_tcl9a] - } - - - namespace import ::punk::args::lib::tstr - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::tclscript_to_scriptlist - @cmd -name punk::lib::tclscript_to_scriptlist\ - -summary\ - "Parse tcl script to toplevel list of lists."\ - -help\ - "Get topmost list of tcl language elements in script. - produces a list of lists where each sublist is a commandlist or - a comment string." - @values -min 1 -max 1 - script -type string - }] - } - proc tclscript_to_scriptlist {script} { - set scriptlist [list] - set cmdlist [list] - set scrlen [string length $script] - set token "" - set in_token 0 - set in_cmdlist 0 - set in_comment 0 - set charmap [list \t TB \n LF \r CR \\ BSL] ;#for switch 'jump' preservation - review - may be slower than escapes in switch statement? - for {set i 0} {$i < $scrlen} {incr i} { - set ch [string index $script $i] - set chswitch [string map $charmap $ch] - if {!$in_token} { - switch -- $chswitch { - { } - TB { - #ignore - continue being a non token - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {$in_cmdlist} { - #no active token - newline ends cmdlist - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - incr i - } - } - LF - ";" { - #no active token - newline or semicolon ends cmdlist - if {$in_cmdlist} { - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation of whitespace while no token - boring - incr i - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation of whitespace while no token - boring - incr i 2 - } else { - #an uncommon possibility, a command wth surrounding spaces called in an strange way - # e.g \ cmdname\ arg - set in_token 1 - set token "\\[string index $script $i+1]" - incr i - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - # { - if {$in_cmdlist} { - #ordinary data - set in_token 1 - set token # - } else { - if {!$in_comment} { - set in_token 1 - set in_comment 1 - set token # - } else { - #wnen in comment - all will be a single token until comment ends - append token # - } - } - } - default { - #for completeness.. we should exclude other possible whitespace chars - if {![string is space $ch]} { - set in_token 1 - set token $ch - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - } - } else { - #if we're in a token, we must be in a cmdlist or a comment (single token) - #review - not preserving whitespace in list of commands is ok, but for comments it should ideally be preserved - #note that unbalanced curly in *toplevel* comment will still 'info complete' to true - switch -- $chswitch { - LF { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ends a comment - lappend scriptlist $token ;#single token for comment - set token "" - set in_token 0 - set in_comment 0 - set in_cmdlist 0 ;#shouldn't be necessary, but included for clarity - } - } - ";" { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ordinary char for comment - append token ";" - } - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {[tcl::info::complete $token]} { - #ends token and commandlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \r\n - incr i - } - } else { - append token \r - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation - lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i ;#skip LF - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation - cr-lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i 2 ;#skip CRLF - } else { - append token "\\[string index $script $i+1]" - incr i - } - } - default { - if {![string is space $ch]} { - append token $ch - } else { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token $ch - } - } else { - append token $ch - } - } - } - } - } - } - #eof - if {!$in_comment} { - if {$in_token} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - lappend scriptlist $cmdlist - } else { - error "Eof reached whilst script incomplete. Unbalanced braces?\ntoken: '$token'" - } - } else { - if {$in_cmdlist} { - lappend scriptlist $cmdlist - } - } - } else { - lappend scriptlist $token - } - return $scriptlist - } - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - (todo - change to indexset syntax @1..3 @1..end-1 etc) - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list ::tcl::array::exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list ::tcl::array::get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - -- -type none -optional 1 - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - - #TODO - change to indexset notation 0..1,3..end-1 etc - - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - #TODO - fix terminology. 'lower_resolve' is confusing here as range can be in descending order - #change to start/end terminology? - - set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-Inf for too low, Inf for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == Inf} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -Inf} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve [llength $dval] $b] - if {$upper == -Inf} { - ##x - #upper bound is below list range - - if {$lower_resolve > -Inf} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == Inf} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact -- $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - #append result [textblock::join_basic -- $kblock $sblock $vblock] \n - append result [textblock::join_basic_raw $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - if {[llength $small] > [llength $large]} {return 0} - foreach x $large { - ::set ($x) {} - } - foreach x $small { - if {![info exists ($x)]} { - return 0 - } - } - return 1 - } - #v2 generally seems slower - proc is_list_all_in_list2 {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list2] - proc is_list_all_in_list2 {small large} $body - } - - proc is_list_all_ni_list {A B} { - foreach x $B { - ::set ($x) {} - } - foreach x $A { - if {[info exists ($x)]} { - return 0 - } - } - return 1 - } - proc is_list_all_ni_list2 {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list2] - proc is_list_all_ni_list2 {a b} $body - } - - #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist - #struct::set difference may happen to preserve ordering when items are integers, but order can't be relied on, - # especially as struct::set has 2 differing implementations (tcl vs critcl) which return results with different ordering to each other and different deduping behaviour in some cases (e.g empty 2nd arg) - proc ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - #with ledit (also avail in 8.6 using punk::lib::compat::ledit - proc ldiff2 {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - foreach item $removeitems { - set posns [lsearch -all -exact $fromlist $item] - foreach p $posns {ledit fromlist $p $p} - } - return $fromlist - } - proc ldiff3 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - puts stderr "WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - #order-preserving - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {([^+-]*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - punk::args::define { - @id -id ::punk::lib::is_indexset - @cmd -name punk::lib::is_indexset\ - -summary\ - "Validate string is a comma-delimited 'indexset'."\ - -help\ - "Validate that a string is an 'indexset' - - An indexset consists of a comma delimited list of indexes or index-ranges. - No particular base is assumed for the purposes of validating an indexset here. - While in Tcl, lists are zero-based - an indexset can be applied to lists of any base. - e.g -10..-1 is an indexset that just won't resolve any results for a list with a base >= 0. - To validate if an indexset is strictly within range, both the length of the data and the base would - need to be considered. - - The normal 'range' specifier is .. - The range specifier can appear at the beginning, middle or end, or even alone to indicate the entire - range of valid values. - e.g the following are all valid ranges - 1.. - (index 1 to 'max') - ..10 - (index 'base' to 10) - 2..11 - (index 2 to 11) - .. - (all indices) - Common whitespace elements space,tab,newlines are ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - see indexset_resolve" - @values -min 1 -max 1 - indexset -type string - } - proc is_indexset {indexset} { - #collapse internal whitespace (for basic whitespace set we allow) - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] - if {![regexp {^[\-\+_end,\.0-9]*$} $indexset]} { - return 0 - } - set ranges [split $indexset ,] - foreach r $ranges { - set validateindices [list] - set rposn [string first .. $r] - if {$rposn >= 0} { - set sepsize 2 - set step 1 - } else { - #check for .n. 'stepped' range - set fdot [string first . $r] - set ldot [string last . $r] - set step [string range $r $fdot+1 $ldot-1] - #todo - allow basic mathops for step: 2+1 2+-1 etc same as tcl lindex, lseq - if {![string is integer -strict $step]} { - } - } - - if {$rposn >= 0} { - lappend validateindices {*}[string range $r 0 $rposn-1] {*}[string range $r $rposn+2 end] - } else { - #'range' is just an index - set validateindices [list $r] - } - foreach v $validateindices { - if {$v eq "" || $v eq "end"} {continue} - if {[string is integer -strict $v]} {continue} - if {[catch {lindex {} $v}]} { - return 0 - } - } - } - return 1 - } - #review - compare to IMAP4 methods of specifying ranges? - punk::args::define { - @id -id ::punk::lib::indexset_resolve - @cmd -name punk::lib::indexset_resolve\ - -summary\ - "Resolve an indexset to a list of integers based on supplied list or string length."\ - -help\ - "Resolve an 'indexset' to a list of actual indices within the range of the provided numitems value. - e.g in a basic case: for a list of 10 items, 'indexset_resolve 10 end' will return the index 9 - - An indexset consists of a comma delimited list of indexes or index-ranges. - Ranges must be specified with .. as the separator, with an empty value at either side of the - separator representing beginning and end of the index range respectively. - - The indexes are 0-based by default, but the base can be specified. - indexset_resolve 7 .. - -> 0 1 2 3 4 5 6 - indexset_resolve 7 .. -3 - -> -3 -2 -1 0 1 2 3 - - Whitespace is ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - end means the last item. - end-1 means the second last item. - 0.. is the same as 0..end - - indexset examples: - - These assume the default 0-based indices (base == 0) - - 1,3.. - output the index 1 (2nd item) followed by all from index 3 to the end. - indexset_resolve 4 1,3.. - -> 1 3 - indexset_resolve 10 1,3.. - -> 1 3 4 5 6 7 8 9 - 0..2,end - output the first 3 indices, and the last index. - end-1..0 - output the indexes in reverse order from 2nd last item to first item." - @values -min 2 -max 3 - numitems -type integer - indexset -type indexset -help "comma delimited specification for indices to return" - base -type integer -default 0 -help\ - "This is the starting index. It can be positive, negative or zero. - This affects the start and end calculations, limiting what indices will be - returned. - e.g with base 1 'end' will give a different value from base 0 - - for 10 items 'end' is 10 when 1-based - for 10 items 'end' is 9 when 0-based - - For base 1, index 0 is considered to be below the range. - ie - indexset_resolve 10 0..3 1 - -> 1 2 3 - indexset_resolve 10 0..3 0 - -> 0 1 2 3 - - It does not *convert* integers within the range. - - indexset_resolve 10 5 1 - -> 5 - indexset_resolve 10 5 0 - -> 5 - - ie if you ask for a 1 based indexset the integers that are within the - range will come out the same, so the result needs to be treated as a - 1-based set of indices when performing further operations. - " - } - proc indexset_resolve {numitems indexset {base 0}} { - if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { - #use parser on unhappy path only - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] ;#collapse basic whitespace - set index_list [list] ;#list of actual indexes within the range - set iparts [split $indexset ,] - set based_max [expr {$numitems -1 + $base}] - - foreach ipart $iparts { - set ipart [string trim $ipart] - set rposn [string first .. $ipart] - if {$rposn>=0} { - #range - lassign [punk::lib::string_splitbefore_indices $ipart $rposn $rposn+2] rawa _ rawb - set rawa [string trim $rawa] - set rawb [string trim $rawb] - if {$rawa eq ""} {set rawa $base} - set a [punk::lib::lindex_resolve $numitems $rawa $base] - if {$a == -Inf} { - #(was -3) - #undershot - leave negative - } elseif {$a == Inf} { - #overshot - set a [expr {$based_max + 1}] ;#put it outside the range on the upper side - } - #review - a may be -Inf - - if {$rawb eq ""} { - if {$a > $based_max} { - set rawb $a ;#make sure .. doesn't return last item - should return nothing - } else { - set rawb end - } - } - set b [punk::lib::lindex_resolve $numitems $rawb $base] - if {$b == -Inf} { - #undershot - leave negative - } elseif {$b == Inf} { - #set b [expr {$numitems}] ;#overshot - put it outside the range on the upper side - set b [expr {$based_max + 1}] ;#overshot - put it outside the range on the upper side - } - - #JJJ - - #e.g make sure .. doesn't return last item - should return nothing as both are above the range. - if {$a >= $base && $a <= $based_max && $b >=$base && $b <= $based_max} { - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - if {$a >= $base && $a <= $based_max} { - #only a is in the range - if {$b < $base} { - set b $base - } else { - set b $based_max - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$b >=$base && $b <= $based_max} { - #only b is in the range - if {$a < $base} { - set a $base - } else { - set a $based_max - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - #both outside the range - if {$a < $base && $b > $base} { - #spans the range in forward order - set a $base - set b $based_max - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$a > $base && $b < $base} { - #spans the range in reverse order - set a $based_max - set b $base - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } - #both outside of range on same side - } - } - } else { - set idx [punk::lib::lindex_resolve_basic $numitems $ipart $base] - #returns only -Inf for out of range at either end - if {$idx >= $base} { - #index within the range - lappend index_list $idx - } - } - } - return $index_list - } - # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side - #This doesn't need the list itself - just the length suffices. - punk::args::define { - @id -id ::punk::lib::lindex_resolve - @cmd -name punk::lib::lindex_resolve\ - -summary\ - "Resolve an indexexpression to an integer based on supplied list or string length."\ - -help\ - "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 - to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating - whether the index was below or above the range of possible indices for the length supplied. - - Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - This means the proc may be called with something like $x+2 end-$y etc - Sometimes the actual integer index is desired. - - We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - - which could have security risks. - lindex_resolve will parse the index expression and return: - a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - The similar function lindex_resolve_basic uses -Inf to denote - out of range at either end of the list/string. - Otherwise it will return an integer corresponding to the position in the data. - This is in stark contrast to Tcl list/string function indices which will return empty strings for out of - bounds indices, or in the case of lrange, return results anyway. - Like Tcl list commands - it will produce an error if the form of the index is not acceptable. - For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side - - thus returning -2 - - Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. - We will get something like 10+1 - which can be resolved safely with expr - " - @values -min 2 -max 2 - datalength -type integer - index -type indexexpression - } - proc lindex_resolve {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve] [arg len] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length - #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr - - - #REVIEW - we need compat for 1_000 etc to handle things like toml even in 8.6? - #A basic string map means we aren't properly validating - #todo - be stricter about malformations such as 1000_ - if {![string is integer -strict 1_0]} { - #basic forward compatibility with integers such as 1_000 for 8.6.x - set index [tcl::string::map {_ {}} $index] - set len [tcl::string::map {_ {}} $len] - } - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve len must be a positive integer." - } - set based_max [expr {$len -1 + $base}] - - if {[string is integer -strict $index]} { - #review - base? - #can match +i -i - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - #note - offset could have leading + or - - # 'string is integer -strict +1' ==> true - #e.g end+-1 is valid (end++-1 is not) - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$offset == 0} { - #(offset +0, -0 or 0 or 000 0_0 etc) - #op either + or - is irrelevant - #set index [expr {$len-1}] ;#+ base ? - set index $based_max - if {$index < $base} { - #return -2 ;#special case - equivalent to 'end', with empty list - treat like a positive number out of bounds - return Inf - } else { - return $index - } - } - - #set index [if {$op eq "+"} {expr {($len-1) + $offset}} else {expr {($len-1) - $offset}}] - set index [if {$op eq "+"} {expr {$based_max + $offset}} else {expr {$based_max - $offset}}] - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - return $index - } - } else { - #index is 'end' - if {$len == 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return Inf - } - #return [expr {$len - 1 + $base}] - return $based_max - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - #regexp must split a++b to a + +b (not a+ + b) ie first +/- is the op - if {[regexp {([^+-]*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } - return $index - } - } - } - proc lindex_resolve_basic {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg len] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -Inf for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve_basic len must be an integer greater than or equal to zero" - } - if {![string is integer -strict $base]} { - #base can be negative - error "lindex_resolve_basic base must be an integer" - } - set based_max [expr {$len -1 + $base}] - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < $base || ($index > $based_max)} { - #even though in this case we could return -Inf or Inf like lindex_resolve; - #for consistency we don't return Inf for upper-boudn violation, - #as which bound is violated is not always directly determinable for compound index expressions (such as end-x) using the lseq+lindex mechanism. - return -Inf - } else { - #!NOTE! index within range is unchanged - no matter the base - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {$len > 0} { - #For large len - this is a wasteful allocation if no true lseq available in Tcl version. - #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) - set testlist [punk::lib::range $base $based_max] ;# uses lseq if available, has fallback of creating a potentially large list of numbers. - } else { - set testlist [list] - #we want to call 'lindex' even in this case - to get the appropriate error message - } - set idx [lindex $testlist $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -Inf - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - proc string_splitbefore {str index} { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - return [list "" $str] - } - Inf { - return [list $str ""] - } - } - } - return [list [string range $str 0 $index-1] [string range $str $index end]] - #scan %s stops at whitespace - not useful here. - #scan $s %${p}s%s - } - proc string_splitbefore_indices {str args} { - set parts [list $str] - set sizes [list [string length $str]] - set s 0 - foreach index $args { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - Inf { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - } - } - if {$index <= 0} { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - if {$index >= [string length $str]} { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - set i -1 - set a 0 - foreach sz $sizes { - incr i - if {$a + $sz > $index} { - set p [lindex $parts $i] - #puts "a:$a index:$index" - if {$a == $index} { - break - } - ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] - ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] - break - } - incr a $sz - } - #puts "->parts:$parts" - #puts "->sizes:$sizes" - } - return $parts - } - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::factors - @cmd -name punk::lib::factors\ - -summary\ - "Sorted factors of positive integer x"\ - -help\ - "Return a sorted list of the positive factors of x where x > 0 - For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. - (including zero itself in this context)* - - This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - Comparisons were done with some numbers below 17 digits long - For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - but has the disadvantage of being slower for 'small' numbers and using more memory. - If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get - there than computing the whole list, even for small values of x - * Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - In other mathematical contexts zero may be considered not to divide anything." - @values -min 1 -max 1 - x -type integer - }] - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::gcd - @cmd -name punk::lib::gcd\ - -summary\ - "Gretest common divisor of m and n."\ - -help\ - "Return the greatest common divisor of m and n. - Straight from Lars Hellström's math::numtheory library in Tcllib - - Graphical use: - An a by b rectangle can be covered with square tiles of side-length c, - only if c is a common divisor of a and b" - @values -min 2 -max 2 - m -type integer - n -type integer - }] - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - proc lcm {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the lowest common multiple of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para] - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text {max -1}} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - if {$max != -1} { - set len [expr {min($len,$max)}] - } - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - ;#package require punk::ansi - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list/detectcode_in_list will check at first level. (not intended for detecting ansi in deeper structures) - - #we use detectcode_in_list instead of detect_in_list - #detectcode_in_list will detect unclosed (or unopened) paired sequences such as PM (privacy message) - # - but the main reason is it is slightly faster. - if {![punk::ansi::ta::detectcode_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [::tcl::string::trim $ln] - if {!$in_jt} { - if {[::tcl::string::match *jumpTable* $ln]} { - punk::ns::call_frame - append result $ln \n - set in_jt 1 - } - } else { - if {[::tcl::string::match Command* $tln] || [::tcl::string::match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - #a test - # punk::ns::cmdtracereturn punk::lib::disassemble ::punk::ns::test_switch4 - # Note the different disassemble result when trace is running. - proc disassemble {procname} { - tcl::unsupported::disassemble proc $procname - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc valcopy {obj} { - append obj2 $obj {} - } - proc set_valcopy {varname obj} { - #used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_valcopy varnmame $val] - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [valcopy $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [valcopy $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [valcopy $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - #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? - if {$has_twapi} { - interp alias "" ::punk::lib::uuid "" twapi::new_uuid - } else { - catch {package require uuid} - interp alias "" ::punk::lib::uuid "" uuid::uuid generate - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.4 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/lib-0.1.5.tm b/src/bootsupport/modules/punk/lib-0.1.5.tm deleted file mode 100644 index 5138ac6d..00000000 --- a/src/bootsupport/modules/punk/lib-0.1.5.tm +++ /dev/null @@ -1,5463 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.5 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.5] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - - #NOTE - the extension ns becomes the '-namespace ' for the original routine name, - #with -unknown handling the original subcommands. - #This makes the original ensemble harder to introspect! - #e.g (the original -map or -namespace not visible) - #In this specific case (which, being published on the wiki might be common in the wild) - #we could call {*}[namespace ensemble configure $routine -unknown] $routine - #and then detect that the first resulting word is an ensemble - #For arbitrary '-unknown scripts' - sensible introspection is likely not possible - - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - #These are just a selection of bugs relevant to punk behaviour (or of specific interest to the author) - #Not any sort of comprehensive check of known tcl bugs. - #These are reported in warning output of 'help tcl' - or used for workarounds in some cases. - proc has_tclbug_regexp_emptystring {} { - #The regexp {} [...] trick - code in brackets only runs when non byte-compiled ie in traces - #This was usable as a hack to create low-impact calls that only ran in an execution trace context - handy for debugger logic, - #but as an apparent violation of Tcl's normal parsing rules - was evidently seen as a bug and fixed in: - #https://core.tcl-lang.org/tcl/info/cb03e57a (tcl 9.0.3+ ?) - set bug [expr {![catch {regexp {} [error should_error]}]}] - return [dict create bug $bug bugref cb03e57a description {regexp emptystring first argument over-optimised - difference in compiled vs traced behaviour.} level minor] - } - proc has_tclbug_lsearch_sorted_inline_subindices {} { - if {[catch {lsearch -sorted -subindices -inline -index 0 {{a 1} {a 2} {b 3} {c 4} {c 5}} b} result]} { - #probably tcl version doesn't support all options - set bug 0 - } else { - set bug [expr {$result ne "b"}] - } - set description "lsearch -sorted with -subindices -inline - incorrect result." - return [dict create bug $bug bugref bc4ac0 description $description level minor] - } - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - set bug true - } else { - set bug false - } - set description "string rep for list variable in script generated when script changed\n(not an acknowledged/reported bug)" - return [dict create bug $bug bugref "" description $description level minor] - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - set bug 0 - } else { - set bug [expr {$result ne "a2"}] - } - set description "lsearch -stride with -subindices -inline -all and single index - incorrect results." - return [dict create bug $bug bugref 5a1aaa201d description $description level major] - } - proc has_tclbug_lseq_sign {} { - #https://core.tcl-lang.org/tcl/tktview/999b6966b2 - if {[catch {lseq 1 10}]} { - set bug 0 - } else { - set r1 [lseq 1 10 -9] - set r2 [lseq 1 10 -10] - set bug [expr {$r1 ne $r2}] - } - set description "lseq step sign not matching sequence direction - inconsistent results." - return [dict create bug $bug bugref 999b6966b2 description $description level minor] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - set bug [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - set description "lists elements not properly quoted in some cases. e.g 'list {*}[lindex {etc #foo} 1] {*}[list]' (#foo not braced)" - return [dict create bug $bug bugref e38dc74e2 description $description level medium] - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - #https://core.tcl-lang.org/tcl/tktview/1095bf7f756f9aed6bde - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - set description "ensemble commands not compiled in safe interps - heavy performance impact in safe interps" - return [dict create bug $has_bug bugref 1095bf7f756f9aed6bde description $description level major] - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - punk::args::set_idalias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - if {"::ledit" ni [info commands ::ledit]} { - interp alias {} ledit {} ::punk::lib::compat::ledit - punk::args::set_idalias ::punk::lib::compat::ledit ::ledit - } - proc ledit {lvar first last args} { - upvar $lvar l - #use lindex_resolve to support for example: ledit lst end+1 end+1 h i - set fidx [punk::lib::lindex_resolve [llength $l] $first] - switch -exact -- $fidx { - -Inf { - #index below lower bound - set pre [list] - set fidx -1 - } - Inf { - #first index position is greater than index of last element in the list - set pre [lrange $l 0 end] - set fidx [llength $l] - } - default { - #set pre [lrange $l 0 $first-1] - set pre [lrange $l 0 $fidx-1] - } - } - set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -Inf { - #index below lower bound - set post [lrange $l 0 end] - } - Inf { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] - set post [lrange $l $lidx+1 end] - } - } - } - set l [list {*}$pre {*}$args {*}$post] - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list ::tcl::info::vars]] - set nscaller [uplevel 1 [list ::tcl::namespace::current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - namespace eval argdoc { - #non-colour SGR codes - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::is_main_script - @cmd -name punk::lib::is_main_script\ - -summary\ - "Test if current script was launched directly."\ - -help\ - "The ${$B}main script${$N} is the primary script that is executed - by the interpreter, e.g. tclsh or wish. - (as opposed to being loaded by the 'source' command) - - see https://wiki.tcl-lang.org/page/main+script" - @values -min 0 -max 0 - }] - } - proc is_main_script {} { - #see https://wiki.tcl-lang.org/page/main+script - if {[info script] ne "" && [info exists ::argv0] - && - [file dirname [file normalize [file join [info script] ...]]] - eq - [file dirname [file normalize [file join $::argv0 ...]]] - } { - return true - } else { - return false - } - } - - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lswap - @cmd -name punk::lib::lswap\ - -summary\ - "Swap list values in-place"\ - -help\ - "Similar to struct::list swap, except it fully supports basic - list index expressions such as 7-2 end-1 etc. - - struct::list swap doesn't support 'end' offsets, and only - sometimes appears to support basic expressions, depending on the - expression compared to the list length." - @values -min 3 -max 3 - lvar -type string -help\ - "name of list variable" - a -type indexexpression - z -type indexexpression - }] - } - proc lswap {lvar a z} { - upvar $lvar l - set len [llength $l] - if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { - #lindex_resolve_basic returns only -Inf if out of range at either bound - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -Inf and Inf special results being lower and upper bound breaches respectively - set a_index [lindex_resolve $len $a] - set a_msg "" - switch -- $a_index { - -Inf { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - Inf { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - } - set z_index [lindex_resolve $len $z] - set z_msg "" - switch -- $z_index { - -Inf { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - Inf { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to {by 1}} { - #note inconsistency with lseq 1 10 by -9 vs lseq 1 10 by -10 - #https://core.tcl-lang.org/tcl/tktview/999b6966b2 - lseq $from $to by $by - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - #our range function doesn't support double like lseq does. (deliberate) review - proc range {from to {by ""}} { - if {$by eq "0"} { - #as per lseq, step (by) zero always gives no result - return [list] - } - set to [offset_expr $to] - set from [offset_expr $from] - if {$by ne ""} { - set by [offset_expr $by] - } - #assert $by is now empty string or an integer - if {$to > $from} { - switch -- $by { - "" - 1 { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - } - default { - set count [expr {($to - $from + $by) / $by}] - if {$count <= 0} { - #return [list] - #https://core.tcl-lang.org/tcl/tktview/999b6966b2 - return [list $from] ;#review - } - set result [list] - for {set i $from} {$i <= $to} {incr i $by} { - lappend result $i - } - return $result - - #if we don't have lseq, we probably don't have lsearch -stride, which would make things simpler. - #set count [expr {($to -$from) + 1}] - #if {$from == 0} { - # set fullrange [lsearch -all [lrepeat $count 0] *] - #} else { - # incr from -1 - # set fullrange [lmap v [lrepeat $count 0] {incr from}] - #} - #set result [list] - #for {set i 0} {$i < $count} {incr i} { - # if {$i % $by == 0} { - # lappend result [lindex $fullrange $i] - # } - #} - #return $result - } - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - switch -- $by { - "" - -1 { - set count [expr {$from - $to} + 1] - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - } - default { - set count [expr {($to - $from + $by) / $by}] - if {$count <= 0} { - #return [list] - return [list $from] ;#review - } - set result [list] - for {set i $from} {$i >= $to} {incr i $by} { - lappend result $i - } - return $result - } - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lzip - @cmd -name punk::lib::lzip\ - -summary\ - "zip any number of lists together."\ - -help\ - "Conceptually equivalent to converting a list of rows - to a list of columns. - - The number of returned lists (columns) will be equal to - the length of the longest supplied list (row). - If lengths of supplied lists don't match, empty strings - will be inserted in the resulting lists. - - e.g lzip {a b c d e} {1 2 3 4} {x y z} - -> {a 1 x} {b 2 y} {c 3 z} {d 4 {}} {3 {} {}} - " - @values -min 0 -max -1 - list -type list -multiple 1 -optional 1 - }] - } - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [::punk::lib::system::lzip2lists {*}$args]} - 3 {return [::punk::lib::system::lzip3lists {*}$args]} - 4 {return [::punk::lib::system::lzip4lists {*}$args]} - 5 {return [::punk::lib::system::lzip5lists {*}$args]} - 6 {return [::punk::lib::system::lzip6lists {*}$args]} - 7 {return [::punk::lib::system::lzip7lists {*}$args]} - 8 {return [::punk::lib::system::lzip8lists {*}$args]} - 9 {return [::punk::lib::system::lzip9lists {*}$args]} - 10 {return [::punk::lib::system::lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::system::lzip${n}lists] eq ""} { - #puts "calling ::punk::lib::system::Build_lzipn $n" - ::punk::lib::system::Build_lzipn $n - } - return [::punk::lib::system::lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::system::lzip${n}lists] eq ""} { - #puts "calling ::punk::lib::system::Build_lzipn $n" - ::punk::lib::system::Build_lzipn $n - } - return [::punk::lib::system::lzip${n}lists {*}$args] - } else { - return [::punk::lib::lzipn {*}$args] - } - } - } - } - - namespace eval system { - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - #puts "proc punk::lib::system::lzip${n}lists {$arglist} \{" - #puts "$body" - #puts "\}" - proc ::punk::lib::system::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #For tcl pre 9 (without lsearch -stride) - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #For Tcl 9+ (with lsearch -stride) - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lzipn - @cmd -name punk::lib::lzipn\ - -summary\ - "zip any number of lists together (unoptimised)."\ - -help\ - "Conceptually equivalent to converting a list of rows - to a list of columns. - - See lzip which provides the same functionality but with - optimisations depending on the number of supplied lists. - " - @values -min 1 -max 1 - lvar -type string -help\ - "name of list variable" - a -type indexexpression - z -type indexexpression - }] - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [dict get [punk::lib::check::has_tclbug_lsearch_strideallinline] bug]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::system::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::system::lzipn_tcl9a] - } - - - - namespace import ::punk::args::lib::tstr - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::tclscript_to_scriptlist - @cmd -name punk::lib::tclscript_to_scriptlist\ - -summary\ - "Parse tcl script to toplevel list of lists."\ - -help\ - "Get topmost list of tcl language elements in script. - produces a list of lists where each sublist is a commandlist or - a comment string." - @values -min 1 -max 1 - script -type string - }] - } - proc tclscript_to_scriptlist {script} { - set scriptlist [list] - set cmdlist [list] - set scrlen [string length $script] - set token "" - set in_token 0 - set in_cmdlist 0 - set in_comment 0 - set charmap [list \t TB \n LF \r CR \\ BSL] ;#for switch 'jump' preservation - review - may be slower than escapes in switch statement? - for {set i 0} {$i < $scrlen} {incr i} { - set ch [string index $script $i] - set chswitch [string map $charmap $ch] - if {!$in_token} { - switch -- $chswitch { - { } - TB { - #ignore - continue being a non token - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {$in_cmdlist} { - #no active token - newline ends cmdlist - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - incr i - } - } - LF - ";" { - #no active token - newline or semicolon ends cmdlist - if {$in_cmdlist} { - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation of whitespace while no token - boring - incr i - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation of whitespace while no token - boring - incr i 2 - } else { - #an uncommon possibility, a command wth surrounding spaces called in an strange way - # e.g \ cmdname\ arg - set in_token 1 - set token "\\[string index $script $i+1]" - incr i - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - # { - if {$in_cmdlist} { - #ordinary data - set in_token 1 - set token # - } else { - if {!$in_comment} { - set in_token 1 - set in_comment 1 - set token # - } else { - #wnen in comment - all will be a single token until comment ends - append token # - } - } - } - default { - #for completeness.. we should exclude other possible whitespace chars - if {![string is space $ch]} { - set in_token 1 - set token $ch - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - } - } else { - #if we're in a token, we must be in a cmdlist or a comment (single token) - #review - not preserving whitespace in list of commands is ok, but for comments it should ideally be preserved - #note that unbalanced curly in *toplevel* comment will still 'info complete' to true - switch -- $chswitch { - LF { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ends a comment - lappend scriptlist $token ;#single token for comment - set token "" - set in_token 0 - set in_comment 0 - set in_cmdlist 0 ;#shouldn't be necessary, but included for clarity - } - } - ";" { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ordinary char for comment - append token ";" - } - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {[tcl::info::complete $token]} { - #ends token and commandlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \r\n - incr i - } - } else { - append token \r - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation - lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i ;#skip LF - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation - cr-lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i 2 ;#skip CRLF - } else { - append token "\\[string index $script $i+1]" - incr i - } - } - default { - if {![string is space $ch]} { - append token $ch - } else { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token $ch - } - } else { - append token $ch - } - } - } - } - } - } - #eof - if {!$in_comment} { - if {$in_token} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - lappend scriptlist $cmdlist - } else { - error "Eof reached whilst script incomplete. Unbalanced braces?\ntoken: '$token'" - } - } else { - if {$in_cmdlist} { - lappend scriptlist $cmdlist - } - } - } else { - lappend scriptlist $token - } - return $scriptlist - } - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - (todo - change to indexset syntax @1..3 @1..end-1 etc) - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list ::tcl::array::exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list ::tcl::array::get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - -- -type none -optional 1 - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - - #TODO - change to indexset notation 0..1,3..end-1 etc - - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - #TODO - fix terminology. 'lower_resolve' is confusing here as range can be in descending order - #change to start/end terminology? - - set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-Inf for too low, Inf for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == Inf} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -Inf} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve [llength $dval] $b] - if {$upper == -Inf} { - ##x - #upper bound is below list range - - if {$lower_resolve > -Inf} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == Inf} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact -- $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - #append result [textblock::join_basic -- $kblock $sblock $vblock] \n - append result [textblock::join_basic_raw $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - if {[llength $small] > [llength $large]} {return 0} - foreach x $large { - ::set ($x) {} - } - foreach x $small { - if {![info exists ($x)]} { - return 0 - } - } - return 1 - } - #v2 generally seems slower - proc is_list_all_in_list2 {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list2] - proc is_list_all_in_list2 {small large} $body - } - - proc is_list_all_ni_list {A B} { - foreach x $B { - ::set ($x) {} - } - foreach x $A { - if {[info exists ($x)]} { - return 0 - } - } - return 1 - } - proc is_list_all_ni_list2 {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list2] - proc is_list_all_ni_list2 {a b} $body - } - proc is_cachedlist_all_ni_list {a b} { - upvar 0 ::punk::lib::caches::funcs_ni_list funcs - if {[info exists funcs($a)]} { - return [[set funcs($a)] $b] - } - set keybytes [encoding convertto utf-8 $a] - set key [binary encode base64 $keybytes] ;#one single-line base64 string - - set expression "" - foreach t $a { - #append expression "({$t} ni \$b) && " - append expression "{$t} ni \$b && " - } - set expression [string trimright $expression " &"] ;#trim trailing spaces and ampersands - proc ::punk::lib::caches::ni_list_$key {b} [string map [list @expression@ $expression] { - return [expr {@expression@}] - }] - - set funcs($a) ::punk::lib::caches::ni_list_$key - return [punk::lib::caches::ni_list_$key $b] - } - proc is_cachedlist_all_ni_list2 {a b} { - upvar 0 ::punk::lib::caches::funcs_ni_list funcs - if {[info exists funcs($a)]} { - return [[set funcs($a)] $b] - } - set keybytes [encoding convertto utf-8 $a] - set key [binary encode base64 $keybytes] ;#one single-line base64 string - - set d [dict create] - foreach x $a { - dict set d $x "" - } - #constructing a switch statement could be an option - # - but would need to avoid using escapes in order to get a jump-table - # - this would need runtime mapping of values - unlikely to be a win - proc ::punk::lib::caches::ni_list_$key {b} [string map [list @d@ $d] { - foreach x $b { - if {[::tcl::dict::exists {@d@} $x]} { - return 0 - } - } - return 1 - }] - - set funcs($a) ::punk::lib::caches::ni_list_$key - return [punk::lib::caches::ni_list_$key $b] - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::ldiff - @cmd -name punk::lib::ldiff\ - -summary\ - "Difference consisting of items with removeitems removed."\ - -help\ - "Somewhat like struct::set difference, but order preserving, and doesn't - treat as a 'set' so preserves any duplicates in items. - - struct::set difference may happen to preserve ordering when items are - integers, but order can't be relied on, especially as struct::set has - 2 differening implementations (tcl vs critcl) which return results with - different ordering to each other and different deduping behaviour in - some cases (e.g when 2nd arg is empty)" - @values -min 2 -max 2 - items -type list - removeitems -type list - }] - } - proc ldiff {items removeitems} { - if {[llength $removeitems] == 0} {return $items} - set result {} - foreach item $items { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - #with ledit (also avail in 8.6 using punk::lib::compat::ledit - proc ldiff2 {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - foreach item $removeitems { - set posns [lsearch -all -exact $fromlist $item] - foreach p $posns {ledit fromlist $p $p} - } - return $fromlist - } - proc ldiff3 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lunique_unordered - @cmd -name punk::lib::lunique_unordered\ - -summary\ - "unique values in list"\ - -help\ - "Return unique values in provided list. - This removes duplicates but *may* rearrange the - order of the returned elements compared to the - original list. - - When struct::set is available this will be used - for the implementation, as it can be *slightly* - faster if acceleration is present. When struct::set - is not available it will fallback to lunique and - provide the same functionality with order preserved." - @values -min 1 -max 1 - list -type list - }] - } - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - #struct::set union operates on a 'set' - so this probably won't change, and hopefully is - #consistent across unacelerated versions and those implemented in accelerators, - #but if it ever does change - be a little noisy about it. - puts stderr "punk::lib WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lunique - @cmd -name punk::lib::lunique\ - -summary\ - "Order-preserving unique values in list"\ - -help\ - "Return unique values in provided list. - This removes duplicates whilst preserving the - original order of the provided list. - - When struct::set is available with acceleration, - lunique_unordered may be slightly faster." - @values -min 1 -max 1 - list -type list - }] - } - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] ;#review - this is for 8.6 to understand underscored ints - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {([^+-]*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - punk::args::define { - @id -id ::punk::lib::is_indexset - @cmd -name punk::lib::is_indexset\ - -summary\ - "Validate string is a comma-delimited 'indexset'."\ - -help\ - "Validate that a string is an 'indexset' - - An indexset consists of a comma delimited list of indexes or index-ranges. - No particular base is assumed for the purposes of validating an indexset here. - While in Tcl, lists are zero-based - an indexset can be applied to lists of any base. - e.g -10..-1 is an indexset that just won't resolve any results for a list with a base >= 0. - To validate if an indexset is strictly within range, both the length of the data and the base would - need to be considered. - - The normal 'range' specifier is .. but can be of the form .x. where x is the step value. - The range specifier can appear at the beginning, middle or end, or even alone to indicate the entire - range of valid values. - e.g the following are all valid ranges - 1.. - (index 1 to 'max') - ..10 - (index 'base' to 10) - 2..11 - (index 2 to 11) - .. - (all indices) - .3. - (1st index and every 3rd index thereafter) - - Common whitespace elements space,tab,newlines are ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - see indexset_resolve" - @values -min 1 -max 1 - indexset -type string - } - proc is_indexset {indexset} { - #collapse internal whitespace (for basic whitespace set we allow) - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] - if {![regexp {^[\-\+_end,\.0-9]*$} $indexset]} { - return 0 - } - set ranges [split $indexset ,] - foreach r $ranges { - set validateindices [list] - set rposn [string first .. $r] - if {$rposn >= 0} { - set sepsize 2 - set step 1 - #review - whitespace between ints? - lappend validateindices {*}[string range $r 0 $rposn-1] {*}[string range $r $rposn+2 end] - } elseif {[string first . $r] >= 0} { - set stripped [string map {. ""} $r] - if {[tcl::string::length $stripped] != [tcl::string::length $r]-2} { - #if one dot exists - must be exactly 2 dots in total - possibly separated by positive/negative int (not zero) - return 0 - } - #assert - we have exactly 2 dots separated by something. - #check for .n. 'stepped' range - set fdot [string first . $r] - set ldot [string last . $r] - set step [string range $r $fdot+1 $ldot-1] - #todo - allow basic mathops for step: 2+1 2+-1 etc same as tcl lindex, lseq - #1.0.10 should be valid but behave similarly to lseq 1 0 by 0 ie returns nothing - - #1.end.10 or similar shouldn't be valid - but we need to allow other basic index expressions. - if {[string match *end* $step] || [catch {lindex {} $step}]} { - return 0 - } - #if {![string is integer -strict $step] || $step == 0} { - # return 0 - #} - lappend validateindices {*}[string range $r 0 $fdot-1] {*}[string range $r $ldot+1 end] - } else { - #'range' is just an index - set validateindices [list $r] - } - - foreach v $validateindices { - if {$v eq "" || $v eq "end"} {continue} - if {[string is integer -strict $v]} {continue} - if {[catch {lindex {} $v}]} { - return 0 - } - } - } - return 1 - } - #review - compare to IMAP4 methods of specifying ranges? - #TODO add tests to test::punk::lib indexset_resolve is a little tricky - punk::args::define { - @id -id ::punk::lib::indexset_resolve - @cmd -name punk::lib::indexset_resolve\ - -summary\ - "Resolve an indexset to a list of integers based on supplied list or string length."\ - -help\ - "Resolve an 'indexset' to a list of actual indices within the range of the provided numitems value. - e.g in a basic case: for a list of 10 items, 'indexset_resolve 10 end' will return the index 9 - - An indexset consists of a comma delimited list of indexes or index-ranges. - Ranges must be specified with a range-indicator such as .. as the separator, with an empty value at - either side of the separator representing beginning and end of the index range respectively. - The range-separator can be of the form .x. where x is an integer or basic expression - (single +/- operation) that indicates the step value to use. This is equivalent to the 'by' value - in the tcl9 lseq command. - - When the start index is lower than the end, the step value defaults to 1. - ie indexset_resolve 0..7 is equivalent to indexset_resolve 0.1.7 - When the start index is higher than the end, the step value defaults to -1. - ie indexset_resolve 7..0 is equivalent to indexset_resolve 0.-1.7 - - If start and end are ommitted, increasing order is assumed if the step isn't specified. - eg - .. represents the range from the base to the end - .-1. would represent end to base with step -1 - - If start is omitted and only the end is supplied: - The default step is 1 indicating ascension and the missing start is equivalent to the base. - indexset_resolve 5 ..2 - -> 0 1 2 - The default start is 'end' if the step is negative - indexset_resolve 5 .-1.2 - -> 4 3 2 - If end is omitted and only the start is supplied: - The default step is 1 indicating ascension and the missing end is equivalent to 'end' - indexset_resolve 5 2.. - -> 2 3 4 - The default end is the base if the step is negative - indexset_resolve 5 2.-1. - -> 2 1 0 - - Like the tcl9 lseq command - a step (by) value of zero produces no results. - - The indexes are 0-based by default, but the base can be specified. - indexset_resolve 7 .. - -> 0 1 2 3 4 5 6 - indexset_resolve -base -3 7 .. - -> -3 -2 -1 0 1 2 3 - - Whitespace is ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - end means the last item. - end-1 means the second last item. - 0.. is the same as 0..end - - indexset examples: - - These assume the default 0-based indices (-base 0) - - 1,3.. - output the index 1 (2nd item) followed by all from index 3 to the end. - indexset_resolve 4 1,3.. - -> 1 3 - indexset_resolve 10 1,3.. - -> 1 3 4 5 6 7 8 9 - 0..2,end - output the first 3 indices, and the last index. - end-1..0 - output the indexes in reverse order from 2nd last item to first item." - @leaders -min 0 -max 0 - @opts - -base -type integer -prefix 1 -default 0 -help\ - "This is the starting index. It can be positive, negative or zero. - This affects the start and end calculations, limiting what indices will be - returned. - e.g with base 1 'end' will give a different value from base 0 - - for 10 items 'end' is 10 when 1-based - for 10 items 'end' is 9 when 0-based - - For base 1, index 0 is considered to be below the range. - ie - indexset_resolve -base 1 10 0..3 - -> 1 2 3 - indexset_resolve -base 0 10 0..3 - -> 0 1 2 3 - - It does not *convert* indexes within the range. - - indexset_resolve -base 1 10 5 - -> 5 - indexset_resolve -base 0 10 5 - -> 5 - - ie if you ask for a 1-based resolution of an indexset the integers that are within - the range will come out the same, so the result needs to be treated as a 1-based - set of indices when performing further operations. - " - @values -min 2 -max 3 - numitems -type integer - indexset -type indexset -help "comma delimited specification for indices to return" - } - - #limit punk::args parsing to unhappy paths where possible - proc indexset_resolve {args} { - # -------------------------------------------------- - # Manual parsing of happy path args instead of using punk::args::parse $args withid ::punk::lib::indexset_resolve - # This is because indexset_resolve is *somewhat* low level, has only a few args, and we don't want any overhead. - # for the unhappy path - the punk::args::parse is fine to generate the usage/error information. - # -------------------------------------------------- - if {[llength $args] < 2} { - punk::args::resolve $args withid ::punk::lib::indexset_resolve - } - set indexset [lindex $args end] - set numitems [lindex $args end-1] - if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { - #use parser on unhappy path only - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - #assert we have 2 or more args - set base 0 ;#default - if {[llength $args] > 2} { - #if more than just numitems and indexset - we expect only -base ie 4 args in total - if {[llength $args] != 4} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set optname [lindex $args 0] - set optval [lindex $args 1] - set fulloptname [tcl::prefix::match -error "" -base $optname] - if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set base $optval - } - # -------------------------------------------------- - - - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] ;#collapse basic whitespace - set index_list [list] ;#list of actual indexes within the range - set iparts [split $indexset ,] - set based_max [expr {$numitems -1 + $base}] - - #we already did is_indexset check above, so we can make assumptions about well-formedness of each part - foreach ipart $iparts { - set ipart [string trim $ipart] - #we need to cater for n..m as well as n.s.m where s is 'step' - set rposn [string first . $ipart] - if {$rposn>=0} { - #if we found one dot - there must be exactly 2 dots in the ipart, separated by nothing, or a basic integer-expression - set rposn2 [string last . $ipart] - if {$rposn2 == $rposn+1} { - #.. - set step "default" ;#could be 1 or -1 - } else { - set step [tcl::string::range $ipart $rposn+1 $rposn2-1] - } - lassign [punk::lib::string_splitbefore_indices $ipart $rposn $rposn2+1] rawa _ rawb - - set rawa [string trim $rawa] - set rawb [string trim $rawb] - if {$rawa eq "" && $rawb eq ""} { - if {$step eq "default"} { - set step 1 ;#default ascending when no start and no end - } - if {$step < 0} { - set rawa end - set rawb $base - } else { - set rawa $base - set rawb end - } - #if neither start nor end specified - we won't get out of range results from lindex_resolve - set a [punk::lib::lindex_resolve $numitems $rawa $base] - set b [punk::lib::lindex_resolve $numitems $rawb $base] - } else { - if {$rawa eq ""} { - if {$step eq "default"} { - #when start not specified, but end is - default direction always ascending - #(even if end is base or below range) - set step 1 - } - if {$step < 0} { - set rawa end - } else { - set rawa $base - } - } - set a [punk::lib::lindex_resolve $numitems $rawa $base] - if {$a == -Inf} { - #undershot - leave negative - } elseif {$a == Inf} { - #overshot - set a [expr {$based_max + 1}] ;#put it outside the range on the upper side - } - #review - a may be -Inf - - if {$rawb eq ""} { - if {$step eq "default"} { - set step 1 - } - if {$step < 0} { - if {$a < $base} { - #make sure both - #mathfunc::isinf is tcl9+ - if {[catch { - if {[::tcl::mathfunc::isinf $a]} { - set a [expr {$base -1}] - } - }]} { - if {[string match -nocase *inf* $a]} { - set a [expr {$base -1}] - } - } - set rawb $a - } else { - set rawb $base - } - } else { - if {$a > $based_max} { - set rawb $a ;#make sure .. doesn't return last item - should return nothing - } else { - set rawb end - } - } - } - set b [punk::lib::lindex_resolve $numitems $rawb $base] - if {$b == -Inf} { - #undershot - leave negative - } elseif {$b == Inf} { - #set b [expr {$numitems}] ;#overshot - put it outside the range on the upper side - set b [expr {$based_max + 1}] ;#overshot - put it outside the range on the upper side - } - } - - #JJJ - - #e.g make sure .. doesn't return last item - should return nothing as both are above the range. - if {$a >= $base && $a <= $based_max && $b >=$base && $b <= $based_max} { - #assert a & b are integers within the range - if {$step eq "default"} { - #unspecified step - base direction on order of a & b - if {$a <= $b} { - set step 1 - } else { - set step -1 - } - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - if {$a >= $base && $a <= $based_max} { - #only a is in the range - if {$b < $base} { - set b $base - } else { - set b $based_max - } - if {$step eq "default"} { - if {$a <= $b} { - set step 1 - } else { - set step -1 - } - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$b >=$base && $b <= $based_max} { - #only b is in the range - if {$step eq "default"} { - if {$a <= $b} { - set step 1 - } else { - set step -1 - } - } - if {$step < 0} { - if {$a < $base} { - #negative step from below - doesn't matter if b is in range - recast both to an int below $base - #(a may be -Inf) - set a [expr {$base -1}] - set b $a - set step 0 ;#we should return nothing - } - } else { - if {$a < $base} { - set a $base - } else { - set a $based_max - } - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - #both outside the range - if {$a < $base && $b > $base} { - #spans the range in forward order - set a $base - set b $based_max - if {$step eq "default"} { - set step 1 - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$a > $base && $b < $base} { - #spans the range in reverse order - set a $based_max - set b $base - if {$step eq "default"} { - set step -1 - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } - #both outside of range on same side - } - } - } else { - set idx [punk::lib::lindex_resolve_basic $numitems $ipart $base] - #returns only -Inf for out of range at either end - if {$idx >= $base} { - #index within the range - lappend index_list $idx - } - } - } - return $index_list - } - # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side - #This doesn't need the list itself - just the length suffices. - punk::args::define { - @id -id ::punk::lib::lindex_resolve - @cmd -name punk::lib::lindex_resolve\ - -summary\ - "Resolve an indexexpression to an integer based on supplied list or string length."\ - -help\ - "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 - to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating - whether the index was below or above the range of possible indices for the length supplied. - - Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - This means the proc may be called with something like $x+2 end-$y etc - Sometimes the actual integer index is desired. - - We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - - which could have security risks. - lindex_resolve will parse the index expression and return: - a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - The similar function lindex_resolve_basic uses -Inf to denote - out of range at either end of the list/string. - Otherwise it will return an integer corresponding to the position in the data. - This is in stark contrast to Tcl list/string function indices which will return empty strings for out of - bounds indices, or in the case of lrange, return results anyway. - Like Tcl list commands - it will produce an error if the form of the index is not acceptable. - For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side - - thus returning -2 - - Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. - We will get something like 10+1 - which can be resolved safely with expr - " - @values -min 2 -max 2 - datalength -type integer - index -type indexexpression - } - proc lindex_resolve {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve] [arg len] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length - #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr - - - #REVIEW - we need compat for 1_000 etc to handle things like toml even in 8.6? - #A basic string map means we aren't properly validating - #todo - be stricter about malformations such as 1000_ - if {![string is integer -strict 1_0]} { - #basic forward compatibility with integers such as 1_000 for 8.6.x - set index [tcl::string::map {_ {}} $index] - set len [tcl::string::map {_ {}} $len] - } - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve len must be a positive integer." - } - set based_max [expr {$len -1 + $base}] - - if {[string is integer -strict $index]} { - #review - base? - #can match +i -i - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - #note - offset could have leading + or - - # 'string is integer -strict +1' ==> true - #e.g end+-1 is valid (end++-1 is not) - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$offset == 0} { - #(offset +0, -0 or 0 or 000 0_0 etc) - #op either + or - is irrelevant - #set index [expr {$len-1}] ;#+ base ? - set index $based_max - if {$index < $base} { - #return -2 ;#special case - equivalent to 'end', with empty list - treat like a positive number out of bounds - return Inf - } else { - return $index - } - } - - #set index [if {$op eq "+"} {expr {($len-1) + $offset}} else {expr {($len-1) - $offset}}] - set index [if {$op eq "+"} {expr {$based_max + $offset}} else {expr {$based_max - $offset}}] - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - return $index - } - } else { - #index is 'end' - if {$len == 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return Inf - } - #return [expr {$len - 1 + $base}] - return $based_max - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - #regexp must split a++b to a + +b (not a+ + b) ie first +/- is the op - if {[regexp {([^+-]*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } - return $index - } - } - } - proc lindex_resolve_basic {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg len] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -Inf for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve_basic len must be an integer greater than or equal to zero" - } - if {![string is integer -strict $base]} { - #base can be negative - error "lindex_resolve_basic base must be an integer" - } - set based_max [expr {$len -1 + $base}] - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < $base || ($index > $based_max)} { - #even though in this case we could return -Inf or Inf like lindex_resolve; - #for consistency we don't return Inf for upper-boudn violation, - #as which bound is violated is not always directly determinable for compound index expressions (such as end-x) using the lseq+lindex mechanism. - return -Inf - } else { - #!NOTE! index within range is unchanged - no matter the base - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {$len > 0} { - #For large len - this is a wasteful allocation if no true lseq available in Tcl version. - #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) - set testlist [punk::lib::range $base $based_max] ;# uses lseq if available, has fallback of creating a potentially large list of numbers. - } else { - set testlist [list] - #we want to call 'lindex' even in this case - to get the appropriate error message - } - set idx [lindex $testlist $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -Inf - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - proc string_splitbefore {str index} { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - return [list "" $str] - } - Inf { - return [list $str ""] - } - } - } - return [list [string range $str 0 $index-1] [string range $str $index end]] - #scan %s stops at whitespace - not useful here. - #scan $s %${p}s%s - } - proc string_splitbefore_indices {str args} { - set parts [list $str] - set sizes [list [string length $str]] - set s 0 - foreach index $args { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - Inf { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - } - } - if {$index <= 0} { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - if {$index >= [string length $str]} { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - set i -1 - set a 0 - foreach sz $sizes { - incr i - if {$a + $sz > $index} { - set p [lindex $parts $i] - #puts "a:$a index:$index" - if {$a == $index} { - break - } - ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] - ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] - break - } - incr a $sz - } - #puts "->parts:$parts" - #puts "->sizes:$sizes" - } - return $parts - } - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::factors - @cmd -name punk::lib::factors\ - -summary\ - "Sorted factors of positive integer x"\ - -help\ - "Return a sorted list of the positive factors of x where x > 0 - For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. - (including zero itself in this context)* - - This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - Comparisons were done with some numbers below 17 digits long - For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - but has the disadvantage of being slower for 'small' numbers and using more memory. - If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get - there than computing the whole list, even for small values of x - * Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - In other mathematical contexts zero may be considered not to divide anything." - @values -min 1 -max 1 - x -type integer - }] - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::gcd - @cmd -name punk::lib::gcd\ - -summary\ - "Greatest common divisor of m and n."\ - -help\ - "Return the greatest common divisor of m and n. - Straight from Lars Hellström's math::numtheory library in Tcllib - - Graphical use: - An a by b rectangle can be covered with square tiles of side-length c, - only if c is a common divisor of a and b" - @values -min 2 -max 2 - m -type integer - n -type integer - }] - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lcm - @cmd -name punk::lib::lcm\ - -summary\ - "Lowest common multiple of m and n."\ - -help\ - "Return the lowest common multiple of m and n. - Straight from Lars Hellström's math::numtheory library in Tcllib" - @values -min 2 -max 2 - m -type integer - n -type integer - }] - } - proc lcm {n m} { - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text {max -1}} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - if {$max != -1} { - set len [expr {min($len,$max)}] - } - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - ;#package require punk::ansi - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list/detectcode_in_list will check at first level. (not intended for detecting ansi in deeper structures) - - #we use detectcode_in_list instead of detect_in_list - #detectcode_in_list will detect unclosed (or unopened) paired sequences such as PM (privacy message) - # - but the main reason is it is slightly faster. - if {![punk::ansi::ta::detectcode_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [::tcl::string::trim $ln] - if {!$in_jt} { - if {[::tcl::string::match *jumpTable* $ln]} { - punk::ns::call_frame - append result $ln \n - set in_jt 1 - } - } else { - if {[::tcl::string::match Command* $tln] || [::tcl::string::match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - #a test - # punk::ns::cmdtracereturn punk::lib::disassemble ::punk::ns::test_switch4 - # Note the different disassemble result when trace is running. - proc disassemble {procname} { - tcl::unsupported::disassemble proc $procname - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc valcopy {obj} { - append obj2 $obj {} - } - proc set_valcopy {varname obj} { - #used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_valcopy varnmame $val] - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [valcopy $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [valcopy $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [valcopy $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - #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? - if {$has_twapi} { - interp alias "" ::punk::lib::uuid "" twapi::new_uuid - } else { - catch {package require uuid} - interp alias "" ::punk::lib::uuid "" uuid::uuid generate - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::caches { - -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.5 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/lib-0.1.6.tm b/src/bootsupport/modules/punk/lib-0.1.6.tm index 5c932940..8f25047c 100644 --- a/src/bootsupport/modules/punk/lib-0.1.6.tm +++ b/src/bootsupport/modules/punk/lib-0.1.6.tm @@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check { if {"windows" ne $::tcl_platform(platform)} { set bug 0 } else { - set tmpdir [file tempdir] ;#tcl 9+ - set testfile [file join $tmpdir "bugtest"] + if {![catch {file tempdir} tmpdir]} { + #tcl 9+ has 'file tempdir' + set testfile [file join $tmpdir "bugtest"] + } else { + #fallback for older tcl versions - use env TEMP/TMP or current directory + set tmpdir "" + foreach e {TEMP TMP} { + if {[info exists ::env($e)] && [file isdirectory ::env($e)]} { + set tmpdir ::env($e) + break + } + } + if {$tmpdir eq ""} { + #no env vars - fallback to current directory + set tmpdir [pwd] + } + set testfile [file join $tmpdir "bugtest"] + } + set fd [open $testfile w] puts $fd test close $fd set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*] + if {[file exists $testfile]} { + file delete $testfile + } foreach r $globresult { if {$r ne "bugtest"} { set bug 1 @@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat { #*** !doctools #[call [fun lpop] [arg listvar] [opt {index}]] #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l + #upvar $lvar l + upvar 1 $lvar l if {![llength $args]} { set args [list end] } @@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat { #set newlist [lremove $newlist $tailidx] #set newlist [lreplace $newlist $tailidx $tailidx] set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx] - #don't use ledit here! + #we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin. } else { set sublist [lindex $newlist {*}$sublist_path] #set sublist [lremove $sublist $tailidx] @@ -3467,32 +3488,53 @@ namespace eval punk::lib { showdict {*}$opts $dvalue {*}$patterns } - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + namespace eval argdoc { + variable PUNKARGS + upvar ::punk::lib::has_punk_ansi has_punk_ansi + + + #if {!$has_punk_ansi} { + # set RST "" + # set sep " = " + # set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + #} else { + # set RST [punk::ansi::a] + # #set sep " [a+ Web-seagreen]=[a] " + # set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support + # #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + # #NOTE that \u2260 not suitable for non utf-8 terminals. + # set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + #} + #todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8. + # (safe detection methods for utf-8 support?) + #if colour is disabled we want to refresh this. + #therefore we use @dynamic + proc get_sep {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep " = " + } else { + #set sep " [a+ Web-seagreen]=[a] " + set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support + } + return $sep } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock + proc get_sep_mismatch {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + } else { + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #NOTE that \u2260 not suitable for non utf-8 terminals. + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] " + } + return $sep_mismatch + } + set DYN_SEP {${[get_sep]}} + set DYN_SEP_MISMATCH {${[get_sep_mismatch]}} - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { + lappend PUNKARGS [list { + @dynamic @id -id ::punk::lib::showdict @cmd -name punk::lib::showdict -help "display dictionary keys and values" #todo - table tableobject @@ -3502,10 +3544,8 @@ namespace eval punk::lib { "Trim whitespace off rhs of each line. This can help prevent a single long line that wraps in terminal from making every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" + -separator -default "${$DYN_SEP}" -help "Separator column between keys and values" + -separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch" -roottype -default "dict" -help\ "list,dict,string" -ansibase_keys -default "" -help\ @@ -3524,7 +3564,23 @@ namespace eval punk::lib { "dict or list value" patterns -default "*" -type string -multiple 1 -help\ "key or key glob pattern" - }]] + }] + } + #TODO - much. + #showdict needs to be able to show different branches which share a root path + #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) + # - specify ansi colour per pattern so different branches can be highlighted? + # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc + # - The current version is incomplete but passably usable. + # - Copy proc and attempt rework so we can get back to this as a baseline for functionality + proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) + package require punk::pipe + #package require punk ;#we need pipeline pattern matching features + package require textblock + + set RST [punk::ansi::a] + + set argd [punk::args::parse $args withid ::punk::lib::showdict] #for punk::lib - we want to reduce pkg dependencies. # - so we won't even use the tcllib debug pkg here diff --git a/src/bootsupport/modules/punk/mix-0.2.tm b/src/bootsupport/modules/punk/mix-0.2.tm deleted file mode 100644 index e3f2cb16..00000000 --- a/src/bootsupport/modules/punk/mix-0.2.tm +++ /dev/null @@ -1,37 +0,0 @@ - -package require punk::cap - - -tcl::namespace::eval punk::mix { - proc init {} { - package require punk::cap::handlers::templates ;#handler for templates cap - punk::cap::register_capabilityname punk.templates ::punk::cap::handlers::templates ;#time taken should generally be sub 200us - - #todo: use tcllib pluginmgr to load all modules that provide 'punk.templates' - #review - tcllib pluginmgr 0.5 @2025 has some bugs - esp regarding .tm modules vs packages - #We may also need to better control the order of module and library paths in the safe interps pluginmgr uses. - #todo - develop punk::pluginmgr to fix these issues (bug reports already submitted re tcllib, but the path issues may need customisation) - - package require punk::mix::templates ;#registers as provider pkg for 'punk.templates' capability with punk::cap - set t [time { - if {[catch {punk::mix::templates::provider register *} errM]} { - puts stderr "punk::mix failure during punk::mix::templates::provider register *" - puts stderr $errM - puts stderr "-----" - puts stderr $::errorInfo - } - }] - puts stderr "->punk::mix::templates::provider register * t=$t" - } - init - -} - -package require punk::mix::base -package require punk::mix::cli - -package provide punk::mix [tcl::namespace::eval punk::mix { - variable version - set version 0.2 - -}] diff --git a/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/bootsupport/modules/punk/nav/fs-0.1.0.tm index 5635cd09..7d85e311 100644 --- a/src/bootsupport/modules/punk/nav/fs-0.1.0.tm +++ b/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) if {$linktarget_file_type eq "directory"} { - set linktarget [file readlink $cdtarget] - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { @@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs { link { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) - set linktarget [file readlink $cdtarget] if {$linktarget_file_type eq "directory"} { - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { diff --git a/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/bootsupport/modules/punk/ns-0.1.0.tm index dc8c765c..e89997c5 100644 --- a/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -2167,8 +2167,13 @@ y" {return quirkykeyscript} puts stdout "leaving $target" puts stdout "call $commandstring\x1b\[m" - puts stdout "result:" - puts stdout $result + if {$code == 0} { + puts stdout "result:" + puts stdout $result + } else { + puts stdout "error message:" + puts stdout $::errorInfo + } puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset set cmdtype [dict get $linedict $target cmdtype] @@ -2647,6 +2652,8 @@ y" {return quirkykeyscript} upvar ::punk::ns::linedict linedict set ::punk::ns::linedict [::tcl::dict::create] + set body_cache [tcl::dict::create] + set resolved_targets [list] foreach tgt $targets { set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]] @@ -5018,6 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs + puts "punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -6618,18 +6626,19 @@ y" {return quirkykeyscript} separately calling 'info args ' 'info body ' etc. The body may display with an additional - comment inserted to display information such as the + comment inserted above the proc line to display information such as the namespace origin. Such a comment begins with #corp#. - Returns a list: proc - (as long as any syntax highlighter is written to - avoid breaking the structure. e.g by avoiding the - insertion of ANSI between an escaping backslash and - its target character) + Returns a string: proc If the output is to be used as a script to regenerate a procedure, '-syntax none' should be used to avoid ANSI colours, or the resulting arglist and body should be run through 'ansistrip'. + + (any syntax highlighter should be written to + avoid breaking the structure. e.g by avoiding the + insertion of ANSI between an escaping backslash and + its target character) " @opts #todo - make definition @dynamic - load highlighters as functions? @@ -6647,7 +6656,13 @@ y" {return quirkykeyscript} "Whether to replace tabs in the body with spaces or a visible Unicode symbol." -ranges -type indexset -default "0..end" -help\ "comma delimited set of line ranges. - Restrict output to the specified line ranges of the body. Lines are numbered starting at 1." + Restrict output to the specified line ranges of the body. Lines are numbered starting at 1. + For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body. + The special index 0 is used to specify the line before the first line of the body, + which is where the #corp# info comment is placed if it exists. + So the default range 0..end includes the info comment and all lines of the body. + Specifying -ranges 1..5,0 would include the info comment at the end of the output. + " -syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\ -choicelabels { none @@ -6690,11 +6705,6 @@ y" {return quirkykeyscript} set indent [string repeat " " $tw] ;#match #set indent [string repeat " " $tw] ;#A more sensible default for code - review - if {[info exists ::auto_index($path)]} { - set infoheader "\n${indent}#corp# auto_index $::auto_index($path)" - } else { - set infoheader "" - } #we want to handle edge cases of commands such as "" or :x #various builtins such as 'namespace which' won't work @@ -6753,13 +6763,29 @@ y" {return quirkykeyscript} return [list alias {*}$alias] } } - if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} { - append infoheader \n "${indent}#corp# namespace origin $origin" - } - if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { - append infoheader \n + #-------------------------------------------------------------------------- + if {[info exists ::auto_index($path)]} { + #set infoheader "${indent}#corp# auto_index $::auto_index($path)" + set infoheader "#corp# auto_index $::auto_index($path)" + } else { + set infoheader "" } + #puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'" + #if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {} + if {$origin ne $targetcmd} { + #append infoheader "${indent}#corp# namespace origin $origin" + append infoheader "#corp# namespace origin $origin" + } + if {$infoheader ne "" && $syntax eq "basic"} { + set infoheader [ansiwrap green $infoheader] + } + #if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { + # append infoheader \n + #} + #-------------------------------------------------------------------------- + + set body "" #set bodytext [info body $origin] #relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname @@ -6851,47 +6877,126 @@ y" {return quirkykeyscript} } } - if {$ranges ni {"0..end" ".." "0.."}} { - set lines [split $body \n] - set linecount [llength $lines] + set lines [split $body \n] + set linecount [llength $lines] + + + #------------------------------------------------------------------------------------------------ + #When we resolve our 1-based indexset - the zero index used to specify the info comment is lost. + #we need to search for it manually and add it back in if it's in the specified ranges. + set rangelist [split $ranges ,] + set info_positions [list] + set lnum 0 + foreach range $rangelist { + lassign [split $range ..] start _ end + set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range] + if {$start eq "0"} { + lappend info_positions $lnum + } + incr lnum [llength $r_indices] + #if {$end eq "0"} { + # #ignore + #} + } + #puts "info_positions: $info_positions" + #------------------------------------------------------------------------------------------------ + + + set body "" + if {[lindex $info_positions 0] == 0 && $infoheader ne ""} { + append body "$infoheader" \n + } + + if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} { set w [string length $linecount] set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges] - set body "" + set outputlines [llength $indices] + if {$do_ln} { - set n 0 foreach idx $indices { - append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + if {$idx == 1} { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}" + } else { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } else { foreach idx $indices { - append body [lindex $lines $idx-1] \n + if {$idx == 1} { + set ln1 "proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body [lindex $lines end] "\}" + } else { + append body [lindex $lines $idx-1] \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } + #no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match + if {[string index $body end] eq "\n"} { + set body [string range $body 0 end-1] + } } else { + #range was specified in a standard way to mean 'all lines' + set outputlines $linecount if {$do_ln} { - set linebody "" - set n 0 - set lines [split $body \n] - set linecount [llength $lines] set w [string length $linecount] - foreach ln $lines { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + set n 2 + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$lnc[format %${w}s $n]$lnr $ln" incr n - append linebody "$lnc[format %${w}s $n]$lnr $ln" \n } - set body [string range $linebody 0 end-1] - #set body $linebody + if {$linecount > 1} { + append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody + } else { + set ln1 "proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$ln" + } + if {$linecount > 1} { + append linebody \n "[lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody } } - - - if {$is_highlighted} { - #ansi colourised items in list format may not always have desired string representation (list escaping can occur) - #return as a string - which may not be a proper Tcl list! - return "proc $resolved {$argl} {\n$infoheader$body\n}" - } else { - list proc $resolved $argl $infoheader$body + #ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end? + if {[lindex $info_positions end] > 0} { + if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} { + append body "\n$infoheader" + } } + return $body } diff --git a/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/bootsupport/modules/punk/repl-0.1.2.tm index 8b4b0f0a..a7aff5a6 100644 --- a/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -165,11 +165,11 @@ namespace eval punk::repl { variable frametype set frametype ascii; #conservative default - if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { - if {$testcharwidth == 1} { - set frametype light - } - } + #if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + # if {$testcharwidth == 1} { + # set frametype light + # } + #} variable debug_repl 0 variable signal_control_c 0 @@ -1961,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config } else { set is_vt52 0 } + variable codethread variable loopinstance incr loopinstance @@ -2091,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config set chunk "\b\x7f\b\x7f" } elseif {$chunk eq "\x1c"} { #ctrl-bslash + #This is commonly used in terminals as a 'harder' ctrl-c. #try to brutally terminate process #attempt to leave terminal in a reasonable state - mode line ;#may be aliased to ::repl::interphelpers::mode - after 250 {exit 42} + punk::console::mode line + #for now - exit with small delay for tidyup + after 1000 {exit 43} return } elseif {$chunk eq "\x1a"} { - #for now - exit with small delay for tidyup #ctrl-z #::punk::repl::handler_console_control "ctrl-z_via_rawloop" - if {[catch {punk::console::mode line}]} { - #REVIEW - interp eval code {punk::console::mode line} + #JMN + #set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}] + set iname $::punk::repl::codethread::replthread_interp + #only the highest level subshell has an interp name of empty string (lower levels are named 'code') + if {$iname eq ""} { + punk::console::mode line } - after 1000 {exit 43} + after 250 [list thread::send $codethread [list interp eval code {quit 42}]] return } @@ -2397,6 +2402,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #set commandstr "set ::punk::repl::debug_repl" set commandstr "" } + + + if {$::punk::repl::debug_repl > 100} { proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { set p %p% @@ -2416,12 +2424,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs debugreport $clearance$p[string map [list \n \n$p] $msg] }] set info "" - append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" - append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" + append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" + append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n" set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]] append info "lastrunchunks\n" - append info "chunks: [llength $lastrunchunks]\n" - append info "namespace: $::punk::nav::ns::ns_current" + append info "chunks : [llength $lastrunchunks]\n" + #JMN + set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]] + append info "codethread namespace: $codethread_ns\n" + append info "stdinlines : [llength $stdinlines] lines\n" + foreach ln $stdinlines { + append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n" + } + append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n" + #append info "namespace: $::punk::nav::ns::ns_current" debug_repl_emit $info } else { proc debug_repl_emit {msg} {return} @@ -2466,7 +2482,6 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config # lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] #} - variable codethread variable codethread_cond variable codethread_mutex @@ -2895,8 +2910,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config if {[llength $waiting]} { set c [lindex $waiting end] } else { - #set c " " - set c \u240a + set c \u240a ;#unicode linefeed symbol. } doprompt ">$c " } @@ -3008,16 +3022,17 @@ namespace eval repl { set codethread_mutex [thread::mutex create] - set scriptmap [list %args% [list $opts] \ - %argv0% [list $::argv0] \ - %argv% [list $::argv] \ - %argc% [list $::argc] \ - %replthread% [thread::id] \ - %replthread_cond% $codethread_cond \ - %replthread_interp% [list $opt_callback_interp] \ - %tmlist% [list [tcl::tm::list]] \ - %autopath% [list $::auto_path] \ - %lib_epoch% [list $::punk::libunknown::epoch]\ + set scriptmap [list %args% [list $opts] {*}{ + } %argv0% [list $::argv0] {*}{ + } %argv% [list $::argv] {*}{ + } %argc% [list $::argc] {*}{ + } %replthread% [thread::id] {*}{ + } %replthread_cond% $codethread_cond {*}{ + } %replthread_interp% [list $opt_callback_interp] {*}{ + } %tmlist% [list [tcl::tm::list]] {*}{ + } %autopath% [list $::auto_path] {*}{ + } %lib_epoch% [list $::punk::libunknown::epoch] {*}{ + } ] #scriptmap applied at end to satisfy silly editor highlighting. set init_script { diff --git a/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm b/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm deleted file mode 100644 index bb769b48..00000000 --- a/src/bootsupport/modules/punk/repl/codethread-0.1.0.tm +++ /dev/null @@ -1,276 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: shellspy/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) 2024 -# -# @@ Meta Begin -# Application punk::repl::codethread 0.1.0 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin shellspy_module_punk::repl::codethread 0 0.1.0] -#[copyright "2024"] -#[titledesc {Module repl codethread}] [comment {-- Name section and table of contents description --}] -#[moddesc {codethread for repl - root interpreter}] [comment {-- Description at end of page heading --}] -#[require punk::repl::codethread] -#[keywords module repl] -#[description] -#[para] This is part of the infrastructure required for the punk::repl to operate - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::repl::codethread -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::repl::codethread -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::config -#*** !doctools -#[item] [package {Tcl 8.6}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# oo::class namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#tcl::namespace::eval punk::repl::codethread::class { - #*** !doctools - #[subsection {Namespace punk::repl::codethread::class}] - #[para] class definitions - #if {[info commands [tcl::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 -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::repl::codethread { - tcl::namespace::export * - variable replthread - variable replthread_cond - variable running 0 - - variable output_stdout "" - variable output_stderr "" - - #variable xyz - - #*** !doctools - #[subsection {Namespace punk::repl::codethread}] - #[para] Core API functions for punk::repl::codethread - #[list_begin definitions] - - - - #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" - #} - - variable run_command_cache - - proc is_running {} { - variable running - return $running - } - proc runscript {script} { - - #puts stderr "->runscript" - variable replthread_cond - #variable output_stdout - #set output_stdout "" - #variable output_stderr - #set output_stderr "" - #expecting to be called from a thread::send in parent repl - ie in the toplevel interp so that the sub-interp "code" is available - #if a thread::send is done from the commandline in a codethread - Tcl will - if {"code" ni [interp children] || ![info exists replthread_cond]} { - #in case someone tries calling from codethread directly - don't do anything or change any state - #(direct caller could create an interp named code at the level "" -> "code" -"code" and add a replthread_cond value to avoid this check - but it probably won't do anything useful) - #if called directly - the context will be within the first 'code' interp. - #inappropriate caller could add superfluous entries to shellfilter stack if function errors out - #inappropriate caller could affect tsv vars (if their interp allows that anyway) - puts stderr "runscript is meant to be called from the parent repl thread via a thread::send to the codethread" - return - } - interp eval code [list set ::punk::repl::codethread::output_stdout ""] - interp eval code [list set ::punk::repl::codethread::output_stderr ""] - - set outstack [list] - set errstack [list] - upvar ::punk::config::running running_config - if {[string length [dict get $running_config color_stdout_repl]] && [interp eval code punk::console::colour]} { - lappend outstack [interp eval code [list shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout_repl]]]] - } - lappend outstack [interp eval code [list shellfilter::stack::add stdout tee_to_var -settings {-varname ::punk::repl::codethread::output_stdout}]] - - if {[string length [dict get $running_config color_stderr_repl]] && [interp eval code punk::console::colour]} { - lappend errstack [interp eval code [list shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr_repl]]]] - # #lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour cyan]] - } - lappend errstack [interp eval code [list shellfilter::stack::add stderr tee_to_var -settings {-varname ::punk::repl::codethread::output_stderr}]] - - #an experiment - #set errhandle [shellfilter::stack::item_tophandle stderr] - #interp transfer "" $errhandle code - - set status [catch { - #shennanigans to keep compiled script around after call. - #otherwise when $script goes out of scope - internal rep of vars set in script changes. - #The shimmering may be no big deal(?) - but debug/analysis using tcl::unsupported::representation becomes impossible. - interp eval code [list ::punk::lib::set_clone ::codeinterp::clonescript $script] ;#like objclone - interp eval code { - lappend ::codeinterp::run_command_cache $::codeinterp::clonescript - if {[llength $::codeinterp::run_command_cache] > 2000} { - set ::codeinterp::run_command_cache [lrange $::codeinterp::run_command_cache 1750 end][unset ::codeinterp::run_command_cache] - } - tcl::namespace::inscope $::punk::ns::ns_current $::codeinterp::clonescript - } - } result] - - - flush stdout - flush stderr - - #interp transfer code $errhandle "" - #flush $errhandle - set lastoutchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stdout]] end] - set lasterrchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stderr]] end] - #puts stderr "-->[ansistring VIEW -lf 1 $lastoutchar$lasterrchar]" - - set tid [thread::id] - tsv::set codethread_$tid info [list lastoutchar $lastoutchar lasterrchar $lasterrchar] - tsv::set codethread_$tid status $status - tsv::set codethread_$tid result $result - tsv::set codethread_$tid errorcode $::errorCode - - - #only remove from shellfilter::stack the items we added to stack in this function - foreach s [lreverse $outstack] { - interp eval code [list shellfilter::stack::remove stdout $s] - } - foreach s [lreverse $errstack] { - interp eval code [list shellfilter::stack::remove stderr $s] - } - thread::cond notify $replthread_cond - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::repl::codethread ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::repl::codethread::lib { - tcl::namespace::export * - tcl::namespace::path [tcl::namespace::parent] - #*** !doctools - #[subsection {Namespace punk::repl::codethread::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 punk::repl::codethread::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::repl::codethread::system { - #*** !doctools - #[subsection {Namespace punk::repl::codethread::system}] - #[para] Internal functions that are not part of the API - - - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::repl::codethread [tcl::namespace::eval punk::repl::codethread { - variable pkg punk::repl::codethread - variable version - set version 0.1.0 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/winlnk-0.1.0.tm b/src/bootsupport/modules/punk/winlnk-0.1.0.tm deleted file mode 100644 index 562aad09..00000000 --- a/src/bootsupport/modules/punk/winlnk-0.1.0.tm +++ /dev/null @@ -1,836 +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.3.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) 2024 -# -# @@ Meta Begin -# Application punk::winlnk 0.1.0 -# Meta platform tcl -# Meta license MIT -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::winlnk 0 0.1.0] -#[copyright "2024"] -#[titledesc {windows shortcut .lnk library}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk::winlnk}] [comment {-- Description at end of page heading --}] -#[require punk::winlnk] -#[keywords module shortcut lnk parse windows crossplatform] -#[description] -#[para] Tools for reading windows shortcuts (.lnk files) on any platform - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::winlnk -#[subsection Concepts] -#[para] Windows shortcuts are a binary format file with a .lnk extension -#[para] Shell Link (.LNK) Binary File Format is documented in [lb]MS_SHLLINK[rb].pdf published by Microsoft. -#[para] Revision 8.0 published 2024-04-23 - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::winlnk -#[list_begin itemized] - -package require Tcl 8.6- -#*** !doctools -#[item] [package {Tcl 8.6}] - -#TODO - logger - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::winlnk { - tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - #variable xyz - - #*** !doctools - #[subsection {Namespace punk::winlnk}] - #[para] Core API functions for punk::winlnk - #[list_begin definitions] - - - variable magic_HeaderSize "0000004C" ;#HeaderSize MUST equal this - variable magic_LinkCLSID "00021401-0000-0000-C000-000000000046" ;#LinkCLSID MUST equal this - - proc Get_contents {path {bytes all}} { - if {![file exists $path] || [file type $path] ne "file"} { - error "punk::winlnk::get_contents cannot find a filesystem object of type 'file' at location: $path" - } - set fd [open $path r] - chan configure $fd -translation binary -encoding iso8859-1 - if {$bytes eq "all"} { - set data [read $fd] - } else { - set data [read $fd $bytes] - } - close $fd - return $data - } - proc Contents_check_header {contents} { - variable magic_HeaderSize - variable magic_LinkCLSID - expr {[Header_Get_HeaderSize $contents] eq $magic_HeaderSize && [Header_Get_LinkCLSID $contents] eq $magic_LinkCLSID} - } - - #LinkFlags - 4 bytes - specifies information about the shell link and the presence of optional portions of the structure. - proc Show_LinkFlags {contents} { - set 4bytes [string range $contents 20 23] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - puts "val: $val" - set declist [scan [string reverse $4bytes] %c%c%c%c] - set fmt [string repeat %08b 4] - puts "LinkFlags:[format $fmt {*}$declist]" - - set r [binary scan $4bytes b32 val] - puts "bscan-le: $val" - set r [binary scan [string reverse $4bytes] b32 val] - puts "bscan-2 : $val" - } - variable LinkFlags - set LinkFlags [dict create\ - hasLinkTargetIDList 1\ - HasLinkInfo 2\ - HasName 4\ - HasRelativePath 8\ - HasWorkingDir 16\ - HasArguments 32\ - HasIconLocation 64\ - IsUnicode 128\ - ForceNoLinkInfo 256\ - HasExpString 512\ - RunInSeparateProcess 1024\ - Unused1 2048\ - HasDarwinID 4096\ - RunAsUser 8192\ - HasExpIcon 16394\ - NoPidlAlias 32768\ - Unused2 65536\ - RunWithShimLayer 131072\ - ForceNoLinkTrack 262144\ - EnableTargetMetadata 524288\ - DisableLinkPathTracking 1048576\ - DisableKnownFolderTracking 2097152\ - DisableKnownFolderAlias 4194304\ - AllowLinkToLink 8388608\ - UnaliasOnSave 16777216\ - PreferEnvironmentPath 33554432\ - KeepLocalIDListForUNCTarget 67108864\ - ] - variable LinkFlagLetters [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 AA] - proc Header_Has_LinkFlag {contents flagname} { - variable LinkFlags - variable LinkFlagLetters - if {[string length $flagname] <= 2} { - set idx [lsearch $LinkFlagLetters $flagname] - if {$idx < 0} { - error "punk::winlnk::Header_Has_LinkFlag error - flagname $flagname not known" - } - set binflag [expr {2**$idx}] - set allflags [Header_Get_LinkFlags $contents] - return [expr {$allflags & $binflag}] - } - if {[dict exists $LinkFlags $flagname]} { - set binflag [dict get $LinkFlags $flagname] - set allflags [Header_Get_LinkFlags $contents] - return [expr {$allflags & $binflag}] - } else { - error "punk::winlnk::Header_Has_LinkFlag error - flagname $flagname not known" - } - } - - #MS-SHLLINK.pdf documents the .lnk file format in detail, but here is a brief overview of the structure of a .lnk file: - #protocol revision 10.0 (November 2025) https://winprotocoldocs-bhdugrdyduf5h2e4.b02.azurefd.net/MS-SHLLINK/%5bMS-SHLLINK%5d.pdf - - - #SHELL_LINK_HEADER structure is 76 bytes long and starts at the beginning of the file - #offset hex:0x00 dec:0 4 bytes - #Header size (HeaderSize) (must be 0x0000004C for .lnk files) - proc Header_Get_HeaderSize {contents} { - set 4bytes [split [string range $contents 0 3] ""] - set hex4 "" - foreach b [lreverse $4bytes] { - set dec [scan $b %c] ;# 0-255 decimal - set HH [format %2.2llX $dec] - append hex4 $HH - } - return $hex4 - } - - - #offset hex:0x04 dec:4 16 bytes - #LinkCLSID (must be 00021401-0000-0000-C000-000000000046 for .lnk files) - proc Header_Get_LinkCLSID {contents} { - set 16bytes [string range $contents 4 19] - #CLSID hex textual representation is split as 4-2-2-2-6 bytes(hex pairs) - #e.g We expect 00021401-0000-0000-C000-000000000046 for .lnk files - #for endianness - it is little endian all the way but the split is 4-2-2-1-1-1-1-1-1-1-1 REVIEW - #(so it can appear as mixed endianness if you don't know the splits) - #https://devblogs.microsoft.com/oldnewthing/20220928-00/?p=107221 - #This is based on COM textual representation of GUIDS - #Apparently a CLSID is a GUID that identifies a COM object - set clsid "" - set s1 [tcl::string::range $16bytes 0 3] - set declist [scan [string reverse $s1] %c%c%c%c] - set fmt "%02X%02X%02X%02X" - append clsid [format $fmt {*}$declist] - - append clsid - - set s2 [tcl::string::range $16bytes 4 5] - set declist [scan [string reverse $s2] %c%c] - set fmt "%02X%02X" - append clsid [format $fmt {*}$declist] - - append clsid - - set s3 [tcl::string::range $16bytes 6 7] - set declist [scan [string reverse $s3] %c%c] - append clsid [format $fmt {*}$declist] - - append clsid - - #now treat bytes individually - so no endianness conversion - set declist [scan [tcl::string::range $16bytes 8 9] %c%c] - append clsid [format $fmt {*}$declist] - - append clsid - - set scan [string repeat %c 6] - set fmt [string repeat %02X 6] - set declist [scan [tcl::string::range $16bytes 10 15] $scan] - append clsid [format $fmt {*}$declist] - - return $clsid - } - - - #offset hex:0x14 dec:20 4 bytes - #Link flags (LinkFlags) - bit field specifying information about the shell link and the presence of optional portions of the structure. - #HasLinkTargetIDList bit 0 (0x00000001) - if set, a LinkTargetIDList structure is present immediately following the header - #HasLinkInfo bit 1 (0x00000002) - if set, a LinkInfo structure is present immediately following the header (or the LinkTargetIDList if that is present) - #HasName bit 2 (0x00000004) - if set, a null-terminated string containing the name of the link is present immediately following the header (or the LinkTargetIDList and LinkInfo if they are present) - #HasRelativePath bit 3 (0x00000008) - if set, a null-terminated string containing the relative path of the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo and Name if they are present) - #HasWorkingDir bit 4 (0x00000010) - if set, a null-terminated string containing the working directory of the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name and Relative Path if they are present) - #HasArguments bit 5 (0x00000020) - if set, a null-terminated string containing the command line arguments for the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path and Working Dir if they are present) - #HasIconLocation bit 6 (0x00000040) - if set, a null-terminated string containing the location of the icon for the link is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir and Arguments if they are present) - #IsUnicode bit 7 (0x00000080) - if set, the strings in the link are stored in Unicode (UTF-16LE) format; if not set, the strings are stored in ANSI format (usually the system's default code page) - #ForceNoLinkInfo bit 8 (0x00000100) - if set, the LinkInfo structure is not stored in the file even if the HasLinkInfo bit is set; this can be used to force the link to be resolved using only the information in the header and the optional strings, without using the LinkInfo structure - #HasExpString bit 9 (0x00000200) - if set, a null-terminated string containing an "environment variable" style string is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments and Icon Location if they are present); this string can contain environment variable references (e.g. %USERPROFILE%) that can be expanded to obtain the actual path of the link target - #RunInSeparateProcess bit 10 (0x00000400) - if set, the link target should be run in a separate process; if not set, the link target may be run in the same process as the caller - #Unused1 bit 11 (0x00000800) - reserved for future use; should be set to 0 - #HasDarwinID bit 12 (0x00001000) - if set, a null-terminated string containing a "Darwin ID" is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments, Icon Location and ExpString if they are present); this string can be used to identify the link target in a way that is independent of the file system (e.g. for links to Control Panel items or special folders) - #RunAsUser bit 13 (0x00002000) - if set, the link target should be run with the permissions of the user specified in the HasDarwinID string; if not set, the link target should be run with the permissions of the caller - #HasExpIcon bit 14 (0x00004000) - if set, a null-terminated string containing an "environment variable" style string for the icon location is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments, Icon Location, ExpString and DarwinID if they are present); this string can contain environment variable references that can be expanded to obtain the actual path of the icon for the link - #NoPidlAlias bit 15 (0x00008000) - if set, the link target should not be resolved using the PIDL alias mechanism; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed - #Unused2 bit 16 (0x00010000) - reserved for future use; should be set to 0 - #RunWithShimLayer bit 17 (0x00020000) - if set, the link target should be run with the application compatibility shim layer; if not set, the link target should be run without the shim layer - #ForceNoLinkTrack bit 18 (0x00040000) - if set, the link target should not be tracked by the shell's link tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed - #EnableTargetMetadata bit 19 (0x00080000) - if set, the link target should have metadata enabled; this can be used to allow the link to store additional information about the target (e.g. for links to files, the link can store the file's attributes, creation time, access time and modification time) - #DisableLinkPathTracking bit 20 (0x00100000) - if set, the link target should not be tracked by the shell's link path tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed based on its path - #DisableKnownFolderTracking bit 21 (0x00200000) - if set, the link target should not be tracked by the shell's known folder tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed based on its known folder ID - #DisableKnownFolderAlias bit 22 (0x00400000) - if set, the link target should not be aliased to a known folder; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on its known folder ID - #AllowLinkToLink bit 23 (0x00800000) - if set, the link target can be another link; if not set, the link target should not be another link (i.e. it should be a file or directory); this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on the fact that it is a link - #UnaliasOnSave bit 24 (0x01000000) - if set, the link should be unaliased when it is saved; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on the fact that it is a link - #PreferEnvironmentPath bit 25 (0x02000000) - if set, the link should prefer to resolve the target using environment variable references; this can be used to allow the link to be resolved correctly even if the target is moved or renamed, as long as the environment variable references still point to the correct location - #KeepLocalIDListForUNCTarget bit 26 (0x04000000) - if set, the link should keep the local ID list for UNC targets; this can be used to allow the link to be resolved correctly even if the target is moved or renamed, as long as the local ID list still points to the correct location - # - the presence of these flags indicates the presence of optional structures in the .lnk file and also provides information about how to interpret the data in the file - proc Header_Get_LinkFlags {contents} { - set 4bytes [string range $contents 20 23] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - #offset hex:0x18 dec:24 4 bytes - #File attributes (FileAttributes) - bit field specifying the file attributes of the link target (if the EnableTargetMetadata flag is set in the LinkFlags field); this field is a bitwise combination of the following values: - proc Header_Get_FileAttributes {contents} { - if {![Header_Has_LinkFlag $contents "EnableTargetMetadata"]} { - return {} - } - set 4bytes [string range $contents 24 27] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - set attrlist {} - if {$val & 0x00000001} {lappend attrlist "READONLY"} - if {$val & 0x00000002} {lappend attrlist "HIDDEN"} - if {$val & 0x00000004} {lappend attrlist "SYSTEM"} - if {$val & 0x00000010} {lappend attrlist "DIRECTORY"} - if {$val & 0x00000020} {lappend attrlist "ARCHIVE"} - if {$val & 0x00000040} {lappend attrlist "DEVICE"} - if {$val & 0x00000080} {lappend attrlist "NORMAL"} - if {$val & 0x00000100} {lappend attrlist "TEMPORARY"} - if {$val & 0x00000200} {lappend attrlist "SPARSE_FILE"} - if {$val & 0x00000400} {lappend attrlist "REPARSE_POINT"} - if {$val & 0x00000800} {lappend attrlist "COMPRESSED"} - if {$val & 0x00001000} {lappend attrlist "OFFLINE"} - if {$val & 0x00002000} {lappend attrlist "NOT_CONTENT_INDEXED"} - if {$val & 0x00004000} {lappend attrlist "ENCRYPTED"} - return $attrlist - } - proc Header_Get_FileAttributes_Raw {contents} { - if {![Header_Has_LinkFlag $contents "EnableTargetMetadata"]} { - return 0 - } - set 4bytes [string range $contents 24 27] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - - - - #offset hex:0x1C dec:28 8 bytes - #creation date and time (CreationTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)) - proc Header_Get_CreationTime {contents} { - set 8bytes [string range $contents 28 35] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - #convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC) - #we can convert it to seconds and then to a human readable format - set seconds [expr {$val / 10000000.0}] - set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970 - set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true] - return $human_time - } - proc Header_Get_CreationTime_Raw {contents} { - set 8bytes [string range $contents 28 35] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - return $val - } - - #offset 36 8 bytes - #last access date and time (AccessTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)) - proc Header_Get_AccessTime {contents} { - set 8bytes [string range $contents 36 43] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - #convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC) - #we can convert it to seconds and then to a human readable format - set seconds [expr {$val / 10000000.0}] - set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970 - set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true] - return $human_time - } - proc Header_Get_AccessTime_Raw {contents} { - set 8bytes [string range $contents 36 43] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - return $val - } - - #offset hex:0x2C dec:44 8 bytes - #last modification date and time (WriteTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)) - proc Header_Get_WriteTime {contents} { - set 8bytes [string range $contents 44 51] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - #convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC) - #we can convert it to seconds and then to a human readable format - set seconds [expr {$val / 10000000.0}] - set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970 - set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true] - return $human_time - } - proc Header_Get_WriteTime_Raw {contents} { - set 8bytes [string range $contents 44 51] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - return $val - } - - #offset hex:0x34 dec:52 Bytes:4 - unsigned int - #file size in bytes (of target - low 32 bits if >4GB) - proc Header_Get_FileSize {contents} { - set 4bytes [string range $contents 52 55] - set r [binary scan $4bytes i val] - return $val - } - - #offset hex:0x38 dec:56 Bytes:4 - signed integer - #icon index value - proc Header_Get_IconIndex {contents} { - set 4bytes [string range $contents 56 59] - set r [binary scan $4bytes i val] - return $val - } - - #offset hex:0x3C dec:60 Bytes:4 - unsigned integer - #SW_SHOWNORMAL 0x00000001 - #SW_SHOWMAXIMIZED 0x00000001 - #SW_SHOWMINNOACTIVE 0x00000007 - # - all other values MUST be treated as SW_SHOWNORMAL - proc Header_Get_ShowCommand {contents} { - set 4bytes [string range $contents 60 63] - set r [binary scan $4bytes i val] - return $val - } - - #offset hex:0x40 dec:64 Bytes:2 - #Hot key - proc Header_Get_HotKey {contents} { - # Existing code that extracts the raw 16‑bit hotkey value: - set raw [Header_Get_HotKey_Raw $contents] - # The low byte holds the virtual‑key, high byte holds modifier flags - set vk [expr {$raw & 0xFF}] - set mods [expr {($raw >> 8) & 0xFF}] - set name [_vk_to_name $vk] - set modStr [_modifiers_to_string $mods] - if {$modStr eq ""} { - return $name - } else { - return "${modStr}+${name}" - } - } - proc Header_Get_HotKey_Raw {contents} { - set 2bytes [string range $contents 64 65] - set r [binary scan $2bytes s val] ;#short - return $val - } - proc _modifiers_to_string {mods} { - set parts {} - if {$mods & 0x01} {lappend parts "Shift"} - if {$mods & 0x02} {lappend parts "Ctrl"} - if {$mods & 0x04} {lappend parts "Alt"} - if {$mods & 0x08} {lappend parts "Win"} ;# optional - return [join $parts "+"] - } - proc _vk_to_name {vk} { - # Minimal map – extend as needed - array set vkMap { - 0x00 "No key assigned" - 0x08 Backspace 0x09 Tab 0x0D Return - 0x10 Shift 0x11 Control 0x12 Alt - 0x20 Space 0x21 PageUp 0x22 PageDown - 0x23 End 0x24 Home 0x25 Left - 0x26 Up 0x27 Right 0x28 Down - 0x2D Insert 0x2E Delete - 0x70 F1 0x71 F2 0x72 F3 - 0x73 F4 0x74 F5 0x75 F6 - 0x76 F7 0x77 F8 0x78 F9 - 0x79 F10 0x7A F11 0x7B F12 - 0x7c F13 0x7d F14 0x7e F15 - 0x7f F16 0x80 F17 0x81 F18 - 0x82 F19 0x83 F20 0x84 F21 - 0x85 F22 0x86 F23 0x87 F24 - 0x90 "NUM LOCK" 0x91 "SCROLL LOCK" - } - if {[info exists vkMap($vk)]} { - return $vkMap($vk) - } else { - if {$vk >= 0x30 && $vk <= 0x39} { - return [format "%c" $vk] ;# 0-9 - } elseif {$vk >= 0x41 && $vk <= 0x5A} { - return [format "%c" $vk] ;# A-Z - } - # fallback: hex representation - return [format "0x%02X" $vk] - } - } - - #offset hex:0x42 dec:66 Bytes:2 - reserved1 - proc Header_Get_Reserved1 {contents} { - set 2bytes [string range $contents 66 67] - set r [binary scan $2bytes s val] ;#short - return $val - } - - #offset hex:0x44 dec:68 Bytes:4 - reserved2 - proc Header_Get_Reserved2 {contents} { - set 4bytes [string range $contents 68 71] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - #offset hex:0x48 dec:72 Bytes:4 - reserved3 - proc Header_Get_Reserved3 {contents} { - set 4bytes [string range $contents 72 75] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - #end of 76 byte header - - proc Get_LinkTargetIDList_size {contents} { - if {[Header_Has_LinkFlag $contents "A"]} { - set 2bytes [string range $contents 76 77] - set r [binary scan $2bytes s val] ;#short - #logger - #puts stderr "LinkTargetIDList_size: $val" - return $val - } else { - return 0 - } - } - proc Get_LinkInfo_content {contents} { - set idlist_size [Get_LinkTargetIDList_size $contents] - if {$idlist_size == 0} { - set offset 0 - } else { - set offset [expr {2 + $idlist_size}] ;#LinkTargetIdList IDListSize field + value - } - set linkinfo_start [expr {76 + $offset}] - if {[Header_Has_LinkFlag $contents "B"]} { - #puts stderr "linkinfo_start: $linkinfo_start" - set 4bytes [string range $contents $linkinfo_start $linkinfo_start+3] - binary scan $4bytes i val ;#size *including* these 4 bytes - set linkinfo_content [string range $contents $linkinfo_start [expr {$linkinfo_start + $val -1}]] - return [dict create linkinfo_start $linkinfo_start size $val next_start [expr {$linkinfo_start + $val}] content $linkinfo_content] - } else { - return [dict create linkinfo_start $linkinfo_start size 0 next_start $linkinfo_start content ""] - } - } - - proc LinkInfo_get_fields {linkinfocontent} { - set 4bytes [string range $linkinfocontent 0 3] - binary scan $4bytes i val ;#size *including* these 4 bytes - set bytes_linkinfoheadersize [string range $linkinfocontent 4 7] - set bytes_linkinfoflags [string range $linkinfocontent 8 11] - set r [binary scan $4bytes i flags] ;# i for little endian 32-bit signed int - #puts "linkinfoflags: $flags" - - set localbasepath "" - set commonpathsuffix "" - - #REVIEW - flags problem? - if {$flags & 1} { - #VolumeIDAndLocalBasePath - #logger - #puts stderr "VolumeIDAndLocalBasePath" - } - if {$flags & 2} { - #logger - #puts stderr "CommonNetworkRelativeLinkAndPathSuffix" - } - set bytes_volumeid_offset [string range $linkinfocontent 12 15] - set bytes_localbasepath_offset [string range $linkinfocontent 16 19] ;# a - set bytes_commonnetworkrelativelinkoffset [string range $linkinfocontent 20 23] - set bytes_commonpathsuffix_offset [string range $linkinfocontent 24 27] ;# a - - binary scan $bytes_localbasepath_offset i bp_offset - if {$bp_offset > 0} { - set tail [string range $linkinfocontent $bp_offset end] - set stringterminator 0 - set i 0 - set localbasepath "" - #TODO - while {!$stringterminator & $i < 100} { - set c [string index $tail $i] - if {$c eq "\x00"} { - set stringterminator 1 - } else { - append localbasepath $c - } - incr i - } - } - binary scan $bytes_commonpathsuffix_offset i cps_offset - if {$cps_offset > 0} { - set tail [string range $linkinfocontent $cps_offset end] - set stringterminator 0 - set i 0 - set commonpathsuffix "" - #TODO - while {!$stringterminator && $i < 100} { - set c [string index $tail $i] - if {$c eq "\x00"} { - set stringterminator 1 - } else { - append commonpathsuffix $c - } - incr i - } - } - - - return [dict create localbasepath $localbasepath commonpathsuffix $commonpathsuffix] - } - - proc contents_get_info {contents} { - - #todo - return something like the perl lnk-parse-1.0.pl script? - - #Link File: C:/repo/jn/tclmodules/tomlish/src/modules/test/#modpod-tomlish-0.1.0/suites/all/arrays_1.toml#roundtrip+roundtrip_files+arrays_1.toml.fauxlink.lnk - #Link Flags: HAS SHELLIDLIST | POINTS TO FILE/DIR | NO DESCRIPTION | HAS RELATIVE PATH STRING | HAS WORKING DIRECTORY | NO CMD LINE ARGS | NO CUSTOM ICON | - #File Attributes: ARCHIVE - #Create Time: Sun Jul 14 2024 10:41:34 - #Last Accessed time: Sat Sept 21 2024 02:46:10 - #Last Modified Time: Tue Sept 10 2024 17:16:07 - #Target Length: 479 - #Icon Index: 0 - #ShowWnd: 1 SW_NORMAL - #HotKey: 0 - #(App Path:) Remaining Path: repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-0.1.0\suites\roundtrip\roundtrip_files\arrays_1.toml - #Relative Path: ..\roundtrip\roundtrip_files\arrays_1.toml - #Working Dir: C:\repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-0.1.0\suites\roundtrip\roundtrip_files - - variable LinkFlags - set flags_enabled [list] - dict for {k v} $LinkFlags { - if {[Header_Has_LinkFlag $contents $k] > 0} { - lappend flags_enabled $k - } - } - - set showcommand_val [Header_Get_ShowCommand $contents] - switch -- $showcommand_val { - 1 { - set showwnd [list 1 SW_SHOWNORMAL] - } - 3 { - set showwnd [list 3 SW_SHOWMAXIMIZED] - } - 7 { - set showwnd [list 7 SW_SHOWMINNOACTIVE] - } - default { - set showwnd [list $showcommand_val SW_SHOWNORMAL-effective] - } - } - - set linkinfo_content_dict [Get_LinkInfo_content $contents] - set localbase_path "" - set suffix_path "" - set linkinfocontent [dict get $linkinfo_content_dict content] - set link_target "" - if {$linkinfocontent ne ""} { - set linkfields [LinkInfo_get_fields $linkinfocontent] - set localbase_path [dict get $linkfields localbasepath] - set suffix_path [dict get $linkfields commonpathsuffix] - if {"windows" eq $::tcl_platform(platform)} { - set link_target [file join $localbase_path $suffix_path] - } else { - set suffix_path [string trimleft [string map {\\ /} $suffix_path] /] - if {[regexp {([a-zA-Z]):\\(.*)} $localbase_path _match drive_letter tail]} { - set localbase_path [string map {\\ /} $localbase_path] - set tail [string trimleft [string map {\\ /} $tail] /] - set link_target "" - #shortcut basepath is a windows path with drive letter - try to resolve it on unix by looking for a corresponding mount from fstab or a point under /mnt - set mountinfo [exec mount] - foreach line [split $mountinfo "\n"] { - #review - a more specific mount target might exist that includes the drive letter as part of the mount point name and is a longer prefix of the localbase_path - #- we should probably look for the longest prefix match rather than just the drive letter - if {[regexp -nocase -- [string cat ^$drive_letter {:\\\s+on\s+(\S+)}] $line _match mount_point]} { - set link_target [file join $mount_point $tail $suffix_path] - break - } - } - if {$link_target eq ""} { - #review - under what circumstances could this happen? If the drive letter doesn't match any mount points, then /mnt/drive_letter should generally already have been found above above - # - However, it may be possible for /mnt/drive_Letter to still exist even if it's not reflected in the output of mount or the output of mount is in an unexpected format. - - #nothing in mount result matches the drive letter - try looking for a mount point under /mnt with the drive letter as the name - if {[file exists /mnt/$drive_letter]} { - set link_target [file join /mnt/$drive_letter $tail $suffix_path] - } else { - if {$drive_letter eq [string tolower $drive_letter]]} { - set op_drive_letter [string toupper $drive_letter] - } else { - set op_drive_letter [string tolower $drive_letter] - } - if {[file exists /mnt/$op_drive_letter]} { - set link_target [file join /mnt/$op_drive_letter $tail $suffix_path] - } else { - #leave as is except for backslashes converted to forward - #- probably won't resolve correctly unless the unix system has a folder named drive_letter: in the current folder with a copy of the original filestructure. - set link_target [file join $localbase_path $suffix_path] - } - } - } else { - #shortcut basepath is a windows path with drive letter and we found a matching mount point - link_target is set to the resolved path - } - } else { - #shortcut basepath doesn't match expected windows path format - just join it with the suffix and hope for the best - #could be something like a network path or it could be something else entirely - set link_target [file join $localbase_path $suffix_path] - } - } - } - - set result [dict create\ - link_target $link_target\ - link_flags $flags_enabled\ - file_attributes [Header_Get_FileAttributes $contents]\ - creation_time [Header_Get_CreationTime $contents]\ - access_time [Header_Get_AccessTime $contents]\ - write_time [Header_Get_WriteTime $contents]\ - target_length [Header_Get_FileSize $contents]\ - icon_index ""\ - showwnd "$showwnd"\ - hotkey [Header_Get_HotKey $contents]\ - relative_path "?"\ - ] - } - - proc file_check_header {path} { - #*** !doctools - #[call [fun file_check_header] [arg path] ] - #[para]Return 0|1 - #[para]Determines if the .lnk file specified in path has a valid header for a windows shortcut - set c [Get_contents $path 20] - return [Contents_check_header $c] - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::winlnk::resolve - @cmd -name punk::winlnk::resolve\ - -summary\ - "Return information about a .lnk file (windows shortcut)"\ - -help\ - "Return a dict of info obtained by parsing the binary data in a windows .lnk file. - If the .lnk header check fails, then the .lnk file probably isn't really a shortcut - file and the dictionary will contain an 'error' key." - @values -min 1 -max 1 - path -type string -help "Path to the .lnk file to resolve" - }] - } - proc resolve {path} { - #*** !doctools - #[call [fun resolve] [arg path] ] - #[para] Return a dict of info obtained by parsing the binary data in a windows .lnk file - #[para] If the .lnk header check fails, then the .lnk file probably isn't really a shortcut file and the dictionary will contain an 'error' key - set c [Get_contents $path] - if {[Contents_check_header $c]} { - return [contents_get_info $c] - } else { - return [dict create error "lnk_header_check_failed"] - } - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::winlnk::file_show_info - @cmd -name punk::winlnk::file_show_info\ - -summary\ - "Show information about a .lnk file (windows shortcut)"\ - -help\ - "Print to stdout the information obtained by parsing the binary data in a windows .lnk file, in a human readable format. - If the .lnk header check fails, then the .lnk file probably isn't really a shortcut file and an error message will be printed." - @values -min 1 -max 1 - path -type string -help "Path to the .lnk file to resolve" - }] - } - proc file_show_info {path} { - package require punk::lib - punk::lib::showdict [resolve $path] * - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::winlnk::target - @cmd -name punk::winlnk::target\ - -summary\ - "Return the target path of a .lnk file (windows shortcut)"\ - -help\ - "Return the target path of the .lnk file specified in path. - This is a convenience function that extracts the target path from the .lnk file and returns it directly, - without all the additional information that resolve provides. If the .lnk header check fails, then - the .lnk file probably isn't really a shortcut file and an error message will be returned." - @values -min 1 -max 1 - path -type string -help "Path to the .lnk file to resolve" - }] - } - proc target {path} { - #*** !doctools - #[call [fun target] [arg path] ] - #[para]Return the target path of the .lnk file specified in path - set info [resolve $path] - if {[dict exists $info error]} { - error [dict get $info error] - } else { - return [dict get $info link_target] - } - } - - #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 punk::winlnk ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::winlnk::lib { - tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - tcl::namespace::path [tcl::namespace::parent] - #*** !doctools - #[subsection {Namespace punk::winlnk::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 punk::winlnk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -#tcl::namespace::eval punk::winlnk::system { - #*** !doctools - #[subsection {Namespace punk::winlnk::system}] - #[para] Internal functions that are not part of the API - - - -#} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::winlnk -} -## Ready -package provide punk::winlnk [tcl::namespace::eval punk::winlnk { - variable pkg punk::winlnk - variable version - set version 0.1.0 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/shellfilter-0.2.1.tm b/src/bootsupport/modules/shellfilter-0.2.2.tm similarity index 99% rename from src/bootsupport/modules/shellfilter-0.2.1.tm rename to src/bootsupport/modules/shellfilter-0.2.2.tm index 6d357bd7..a0fa060a 100644 --- a/src/bootsupport/modules/shellfilter-0.2.1.tm +++ b/src/bootsupport/modules/shellfilter-0.2.2.tm @@ -1582,8 +1582,11 @@ namespace eval shellfilter::stack { #include idposn in poplist set poplist [lrange $stack $idposn end] - #set stack [lreplace $stack $idposn end] - set stack [lreplace $stack[set stack {}] $idposn end] + #set stack [lreplace $stack[set stack {}] $idposn end] + # 2026-05-19 + ledit stack $idposn end + + #pop all chans before adding anything back in! foreach p $poplist { chan pop $localchan @@ -1622,8 +1625,9 @@ namespace eval shellfilter::stack { variable pipelines set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] set poplist [lrange $stack $bottom_pop_posn end] - #set stack [lreplace $stack $bottom_pop_posn end] - set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] + #set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] + # 2026-05-19 + ledit stack $bottom_pop_posn end set localchan [dict get $pipelines $pipename device localchan] foreach p [lreverse $poplist] { @@ -2728,13 +2732,13 @@ namespace eval shellfilter { ::shellfilter::log::write $runtag "checking for redirections in $commandlist" #sometimes we see a redirection without a following space e.g >C:/somewhere #normalize - switch -regexp -- $lastitem\ - {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - }\ - {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} @@ -3391,5 +3395,5 @@ namespace eval shellfilter { package provide shellfilter [namespace eval shellfilter { variable version - set version 0.2.1 + set version 0.2.2 }] diff --git a/src/bootsupport/modules/shellthread-1.6.1.tm b/src/bootsupport/modules/shellthread-1.6.1.tm deleted file mode 100644 index 94f70842..00000000 --- a/src/bootsupport/modules/shellthread-1.6.1.tm +++ /dev/null @@ -1,829 +0,0 @@ -#package require logger - - -package require Thread - -namespace eval shellthread { - - proc iso8601 {{tsmicros ""}} { - if {$tsmicros eq ""} { - set tsmicros [tcl::clock::microseconds] - } else { - set microsnow [tcl::clock::microseconds] - if {[tcl::string::length $tsmicros] != [tcl::string::length $microsnow]} { - error "iso8601 requires 'clock micros' or empty string to create timestamp" - } - } - set seconds [expr {$tsmicros / 1000000}] - return [tcl::clock::format $seconds -format "%Y-%m-%d_%H-%M-%S"] - } -} - -namespace eval shellthread::worker { - variable settings - variable sysloghost_port - variable sock - variable logfile "" - variable fd - variable client_ids [list] - variable ts_start_micros - variable errorlist [list] - variable inpipe "" - - proc bgerror {args} { - variable errorlist - lappend errorlist $args - } - proc send_errors_now {tidcli} { - variable errorlist - thread::send -async $tidcli [list shellthread::manager::report_worker_errors [list worker_tid [thread::id] errors $errorlist]] - } - proc add_client_tid {tidcli} { - variable client_ids - if {$tidcli ni $client_ids} { - lappend client_ids $tidcli - } - } - proc init {tidclient start_m settingsdict} { - variable sysloghost_port - variable logfile - variable settings - interp bgerror {} shellthread::worker::bgerror - #package require overtype ;#overtype uses tcllib textutil, punk::char etc - currently too heavyweight in terms of loading time for use in threads. - variable client_ids - variable ts_start_micros - lappend client_ids $tidclient - set ts_start_micros $start_m - - set defaults [list -raw 0 -file "" -syslog "" -direction out] - set settings [dict merge $defaults $settingsdict] - - set syslog [dict get $settings -syslog] - if {[string length $syslog]} { - lassign [split $syslog :] s_host s_port - set sysloghost_port [list $s_host $s_port] - if {[catch {package require udp} errm]} { - #disable rather than bomb and interfere with any -file being written - #review - log/notify? - set sysloghost_port "" - } - } else { - set sysloghost_port "" - } - - set logfile [dict get $settings -file] - } - - proc start_pipe_read {source readchan args} { - #assume 1 inpipe for now - variable inpipe - variable sysloghost_port - variable logfile - - set defaults [dict create -buffering \uFFFF ] - set opts [dict merge $defaults $args] - if {[dict exists $opts -readbuffering]} { - set readbuffering [dict get $opts -readbuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - #get buffering setting from the channel as it was set prior to thread::transfer - set readbuffering [chan configure $readchan -buffering] - } else { - set readbuffering [dict get $opts -buffering] - chan configure $readchan -buffering $readbuffering - } - } - if {[dict exists $opts -writebuffering]} { - set writebuffering [dict get $opts -writebuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - set writebuffering line - #set writebuffering [chan configure $writechan -buffering] - } else { - set writebuffering [dict get $opts -buffering] - #can configure $writechan -buffering $writebuffering - } - } - - chan configure $readchan -translation lf - - if {$readchan ni [chan names]} { - error "shellthread::worker::start_pipe_read - inpipe not configured. Use shellthread::manager::set_pipe_read_from_client to thread::transfer the pipe end" - } - set inpipe $readchan - chan configure $readchan -blocking 0 - set waitvar ::shellthread::worker::wait($inpipe,[clock micros]) - - #tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan - chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] - vwait $waitvar - } - proc pipe_read {chan source waitfor readbuffering writebuffering} { - if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering - } else { - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - } - } else { - set chunk [chan read $chan] - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $chan - } - } - - proc start_pipe_write {source writechan args} { - variable outpipe - set defaults [dict create -buffering \uFFFF ] - set opts [dict merge $defaults $args] - - #todo! - set readchan stdin - - if {[dict exists $opts -readbuffering]} { - set readbuffering [dict get $opts -readbuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - set readbuffering [chan configure $readchan -buffering] - } else { - set readbuffering [dict get $opts -buffering] - chan configure $readchan -buffering $readbuffering - } - } - if {[dict exists $opts -writebuffering]} { - set writebuffering [dict get $opts -writebuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - #nothing explicitly set - take from transferred channel - set writebuffering [chan configure $writechan -buffering] - } else { - set writebuffering [dict get $opts -buffering] - can configure $writechan -buffering $writebuffering - } - } - - if {$writechan ni [chan names]} { - error "shellthread::worker::start_pipe_write - outpipe not configured. Use shellthread::manager::set_pipe_write_to_client to thread::transfer the pipe end" - } - set outpipe $writechan - chan configure $readchan -blocking 0 - chan configure $writechan -blocking 0 - set waitvar ::shellthread::worker::wait($outpipe,[clock micros]) - - chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { - if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - puts $writechan $chunk - } else { - puts -nonewline $writechan $chunk - } - } - } else { - set chunk [chan read $chan] - puts -nonewline $writechan $chunk - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $writechan - if {$chan ne "stdin"} { - chan close $chan - } - } - }} $readchan $writechan $source $waitvar $readbuffering] - - vwait $waitvar - } - - - proc _initsock {} { - variable sysloghost_port - variable sock - if {[string length $sysloghost_port]} { - if {[catch {chan configure $sock} state]} { - set sock [udp_open] - chan configure $sock -buffering none -translation binary - chan configure $sock -remote $sysloghost_port - } - } - } - proc _reconnect {} { - variable sock - catch {close $sock} - _initsock - return [chan configure $sock] - } - - proc send_info {client_tid ts_sent source msg} { - set ts_received [clock micros] - set lag_micros [expr {$ts_received - $ts_sent}] - set lag [expr {$lag_micros / 1000000.0}] ;#lag as x.xxxxxx seconds - log $client_tid $ts_sent $lag $source - info $msg line 1 - } - proc log {client_tid ts_sent lag source service level msg writebuffering {islog 0}} { - variable sock - variable fd - variable sysloghost_port - variable logfile - variable settings - - set logchunk $msg - - if {![dict get $settings -raw]} { - set tail_crlf 0 - set tail_lf 0 - set tail_cr 0 - #for cooked - always remove the trailing newline before splitting.. - # - #note that if we got our data from reading a non-line-buffered binary channel - then this naive line splitting will not split neatly for mixed line-endings. - # - #Possibly not critical as cooked is for logging and we are still preserving all \r and \n chars - but review and consider implementing a better split - #but add it back exactly as it was afterwards - #we can always split on \n - and any adjacent \r will be preserved in the rejoin - set lastchar [string range $logchunk end end] - if {[string range $logchunk end-1 end] eq "\r\n"} { - set tail_crlf 1 - set logchunk [string range $logchunk 0 end-2] - } else { - if {$lastchar eq "\n"} { - set tail_lf 1 - set logchunk [string range $logchunk 0 end-1] - } elseif {$lastchar eq "\r"} { - #\r line-endings are obsolete..and unlikely... and ugly as they can hide characters on the console. but we'll pass through anyway. - set tail_cr 1 - set logchunk [string range $logchunk 0 end-1] - } else { - #possibly a single line with no linefeed.. or has linefeeds only in the middle - } - } - - if {$ts_sent != 0} { - set micros [lindex [split [expr {$ts_sent / 1000000.0}] .] end] - set time_info [::shellthread::iso8601 $ts_sent].$micros - #set time_info "${time_info}+$lag" - set lagfp "+[format %f $lag]" - } else { - #from pipe - no ts_sent/lag info available - set time_info "" - set lagfp "" - } - - set idtail [string range $client_tid end-8 end] ;#enough for display purposes id - mostly zeros anyway - - - #set col0 [string repeat " " 9] - #set col1 [string repeat " " 27] - #set col2 [string repeat " " 11] - #set col3 [string repeat " " 22] - ##do not columnize the final data column or append to tail - or we could muck up the crlf integrity - #lassign [list [overtype::left $col0 $idtail] [overtype::left $col1 $time_info] [overtype::left $col2 $lagfp] [overtype::left $col3 $source]] c0 c1 c2 c3 - - set w0 9 - set w1 27 - set w2 11 - set w3 22 ;#review - this can truncate source name without indication tail is missing - #do not columnize the final data column or append to tail - or we could muck up the crlf integrity - lassign [list \ - [format %-${w0}s $idtail]\ - [format %-${w1}s $time_info]\ - [format %-${w2}s $lagfp]\ - [format %-${w3}s $source]\ - ] c0 c1 c2 c3 - set c2_blank [string repeat " " $w2] - - - #split on \n no matter the actual line-ending in use - #shouldn't matter as long as we don't add anything at the end of the line other than the raw data - #ie - don't quote or add spaces - set lines [split $logchunk \n] - - set i 1 - set outlines [list] - foreach ln $lines { - if {$i == 1} { - lappend outlines "$c0 $c1 $c2 $c3 $ln" - } else { - lappend outlines "$c0 $c1 $c2_blank $c3 $ln" - } - incr i - } - if {$tail_lf} { - set logchunk "[join $outlines \n]\n" - } elseif {$tail_crlf} { - set logchunk "[join $outlines \r\n]\r\n" - } elseif {$tail_cr} { - set logchunk "[join $outlines \r]\r" - } else { - #no trailing linefeed - set logchunk [join $outlines \n] - - } - - #set logchunk "[overtype::left $col0 $idtail] [overtype::left $col1 $time_info] [overtype::left $col2 "+$lagfp"] [overtype::left $col3 $source] $msg" - } - - if {[string length $sysloghost_port]} { - _initsock - catch {puts -nonewline $sock $logchunk} - } - #todo - sockets etc? - if {[string length $logfile]} { - #todo - setting to maintain open filehandle and reduce io. - # possible settings for buffersize - and maybe logrotation, although this could be left to client - #for now - default to safe option of open/close each write despite the overhead. - set fd [open $logfile a] - chan configure $fd -translation auto -buffering $writebuffering - #whether line buffered or not - by now our logchunk includes newlines - puts -nonewline $fd $logchunk - close $fd - } - } - - # - withdraw just this client - proc finish {tidclient} { - variable client_ids - if {($tidclient in $clientids) && ([llength $clientids] == 1)} { - terminate $tidclient - } else { - set posn [lsearch $client_ids $tidclient] - set client_ids [lreplace $clientids $posn $posn] - } - } - - #allow any client to terminate - proc terminate {tidclient} { - variable sock - variable fd - variable client_ids - if {$tidclient in $client_ids} { - catch {close $sock} - catch {close $fd} - set client_ids [list] - #review use of thread::release -wait - #docs indicate deprecated for regular use, and that we should use thread::join - #however.. how can we set a timeout on a thread::join ? - #by telling the thread to release itself - we can wait on the thread::send variable - # This needs review - because it's unclear that -wait even works on self - # (what does it mean to wait for the target thread to exit if the target is self??) - thread::release -wait - return [thread::id] - } else { - return "" - } - } - - -} - - -namespace eval shellthread::manager { - variable workers [dict create] - variable worker_errors [list] - variable timeouts - - variable free_threads [list] - #variable log_threads - - proc dict_getdef {dictValue args} { - if {[llength $args] < 2} { - error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - } - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - #new datastructure regarding workers and sourcetags required. - #one worker can service multiple sourcetags - but each sourcetag may be used by multiple threads too. - #generally each thread will use a specific sourcetag - but we may have pools doing similar things which log to same destination. - # - #As a convention we may use a sourcetag for the thread which started the worker that isn't actually used for logging - but as a common target for joins - #If the thread which started the thread calls leave_worker with that 'primary' sourcetag it means others won't be able to use that target - which seems reasonable. - #If another thread want's to maintain joinability beyond the span provided by the starting client, - #it can join with both the primary tag and a tag it will actually use for logging. - #A thread can join the logger with any existingtag - not just the 'primary' - #(which is arbitrary anyway. It will usually be the first in the list - but may be unsubscribed by clients and disappear) - proc join_worker {existingtag sourcetaglist} { - set client_tid [thread::id] - #todo - allow a source to piggyback on existing worker by referencing one of the sourcetags already using the worker - } - - proc new_pipe_worker {sourcetaglist {settingsdict {}}} { - if {[dict exists $settingsdict -workertype]} { - if {[string tolower [dict get $settingsdict -workertype]] ne "pipe"} { - error "new_pipe_worker error: -workertype ne 'pipe'. Set to 'pipe' or leave empty" - } - } - dict set settingsdict -workertype pipe - new_worker $sourcetaglist $settingsdict - } - - #it is up to caller to use a unique sourcetag (e.g by prefixing with own thread::id etc) - # This allows multiple threads to more easily write to the same named sourcetag if necessary - # todo - change sourcetag for a list of tags which will be handled by the same thread. e.g for multiple threads logging to same file - # - # todo - some protection mechanism for case where target is a file to stop creation of multiple worker threads writing to same file. - # Even if we use open fd,close fd wrapped around writes.. it is probably undesirable to have multiple threads with same target - # On the other hand socket targets such as UDP can happily be written to by multiple threads. - # For now the mechanism is that a call to new_worker (rename to open_worker?) will join the same thread if a sourcetag matches. - # but, as sourcetags can get removed(unsubbed via leave_worker) this doesn't guarantee two threads with same -file settings won't fight. - # Also.. the settingsdict is ignored when joining with a tag that exists.. this is problematic.. e.g logrotation where previous file still being written by existing worker - # todo - rename 'sourcetag' concept to 'targettag' ?? the concept is a mixture of both.. it is somewhat analagous to a syslog 'facility' - # probably new_worker should disallow auto-joining and we allow different workers to handle same tags simultaneously to support overlap during logrotation etc. - proc new_worker {sourcetaglist {settingsdict {}}} { - variable workers - set ts_start [clock micros] - set tidclient [thread::id] - set sourcetag [lindex $sourcetaglist 0] ;#todo - use all - - set defaults [dict create\ - -workertype message\ - ] - set settingsdict [dict merge $defaults $settingsdict] - - set workertype [string tolower [dict get $settingsdict -workertype]] - set known_workertypes [list pipe message] - if {$workertype ni $known_workertypes} { - error "new_worker - unknown -workertype $workertype. Expected one of '$known_workertypes'" - } - - if {[dict exists $workers $sourcetag]} { - set winfo [dict get $workers $sourcetag] - if {[dict get $winfo tid] ne "noop" && [thread::exists [dict get $winfo tid]]} { - #add our client-info to existing worker thread - dict lappend winfo list_client_tids $tidclient - dict set workers $sourcetag $winfo ;#writeback - return [dict get $winfo tid] - } - } - - #noop fake worker for empty syslog and empty file - if {$workertype eq "message"} { - if {[dict_getdef $settingsdict -syslog ""] eq "" && [dict_getdef $settingsdict -file ""] eq ""} { - set winfo [dict create tid noop list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list] workertype "message"] - dict set workers $sourcetag $winfo - return noop - } - } - - #check if there is an existing unsubscribed thread first - #don't use free_threads for pipe workertype for now.. - variable free_threads - if {$workertype ne "pipe"} { - if {[llength $free_threads]} { - #todo - re-use from tail - as most likely to have been doing similar work?? review - - set free_threads [lassign $free_threads tidworker] - #todo - keep track of real ts_start of free threads... kill when too old - set winfo [dict create tid $tidworker list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list] workertype [dict get $settingsdict -workertype]] - #puts stderr "shellfilter::new_worker Re-using free worker thread: $tidworker with tag $sourcetag" - dict set workers $sourcetag $winfo - return $tidworker - } - } - - - #set ts_start [::shellthread::iso8601] - set tidworker [thread::create -preserved] - set init_script [string map [list %ts_start% $ts_start %mp% [tcl::tm::list] %ap% $::auto_path %tidcli% $tidclient %sd% $settingsdict] { - #set tclbase [file dirname [file dirname [info nameofexecutable]]] - #set tcllib $tclbase/lib - #if {$tcllib ni $::auto_path} { - # lappend ::auto_path $tcllib - #} - - set ::settingsinfo [dict create %sd%] - #if the executable running things is something like a tclkit, - # then it's likely we will need to use the caller's auto_path and tcl::tm::list to find things - #The caller can tune the thread's package search by providing a settingsdict - #tcl::tm::add * must add in reverse order to get reulting list in same order as original - if {![dict exists $::settingsinfo tcl_tm_list]} { - #JMN2 - ::tcl::tm::add {*}[lreverse [list %mp%]] - } else { - tcl::tm::remove {*}[tcl::tm::list] - ::tcl::tm::add {*}[lreverse [dict get $::settingsinfo tcl_tm_list]] - } - if {![dict exists $::settingsinfo auto_path]} { - set ::auto_path [list %ap%] - } else { - set ::auto_path [dict get $::settingsinfo auto_path] - } - - package require punk::packagepreference - punk::packagepreference::install - - package require Thread - package require shellthread - if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} { - unset ::settingsinfo - set ::shellthread_init "ok" - } else { - unset ::settingsinfo - set ::shellthread_init "err $errmsg" - } - }] - - thread::send -async $tidworker $init_script - #thread::send $tidworker $init_script - set winfo [dict create tid $tidworker list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list]] - dict set workers $sourcetag $winfo - return $tidworker - } - - proc set_pipe_read_from_client {tag_pipename worker_tid rchan args} { - variable workers - if {![dict exists $workers $tag_pipename]} { - error "workerthread::manager::set_pipe_read_from_client source/pipename $tag_pipename not found" - } - set match_worker_tid [dict get $workers $tag_pipename tid] - if {$worker_tid ne $match_worker_tid} { - error "workerthread::manager::set_pipe_read_from_client source/pipename $tag_pipename workert_tid mismatch '$worker_tid' vs existing:'$match_worker_tid'" - } - #buffering set during channel creation will be preserved on thread::transfer - thread::transfer $worker_tid $rchan - #start_pipe_read will vwait - so we have to send async - thread::send -async $worker_tid [list ::shellthread::worker::start_pipe_read $tag_pipename $rchan] - #client may start writing immediately - but presumably it will buffer in fifo2 - } - - proc set_pipe_write_to_client {tag_pipename worker_tid wchan args} { - variable workers - if {![dict exists $workers $tag_pipename]} { - error "workerthread::manager::set_pipe_write_to_client pipename $tag_pipename not found" - } - set match_worker_tid [dict get $workers $tag_pipename tid] - if {$worker_tid ne $match_worker_tid} { - error "workerthread::manager::set_pipe_write_to_client pipename $tag_pipename workert_tid mismatch '$worker_tid' vs existing:'$match_worker_tid'" - } - #buffering set during channel creation will be preserved on thread::transfer - thread::transfer $worker_tid $wchan - thread::send -async $worker_tid [list ::shellthread::worker::start_pipe_write $tag_pipename $wchan] - } - - proc write_log {source msg args} { - variable workers - set ts_micros_sent [clock micros] - set defaults [list -async 1 -level info] - set opts [dict merge $defaults $args] - - if {[dict exists $workers $source]} { - set tidworker [dict get $workers $source tid] - if {$tidworker eq "noop"} { - return - } - if {![thread::exists $tidworker]} { - # -syslog -file ? - set tidworker [new_worker $source] - } - } else { - #auto create with no requirement to call new_worker.. warn? - # -syslog -file ? - error "write_log no log opened for source: $source" - set tidworker [new_worker $source] - } - set client_tid [thread::id] - if {[dict get $opts -async]} { - thread::send -async $tidworker [list ::shellthread::worker::send_info $client_tid $ts_micros_sent $source $msg] - } else { - thread::send $tidworker [list ::shellthread::worker::send_info $client_tid $ts_micros_sent $source $msg] - } - } - proc report_worker_errors {errdict} { - variable workers - set reporting_tid [dict get $errdict worker_tid] - dict for {src srcinfo} $workers { - if {[dict get $srcinfo tid] eq $reporting_tid} { - dict set srcinfo errors [dict get $errdict errors] - dict set workers $src $srcinfo ;#writeback updated - break - } - } - } - - #aka leave_worker - #Note that the tags may be on separate workertids, or some tags may share workertids - proc unsubscribe {sourcetaglist} { - variable workers - #workers structure example: - #[list sourcetag1 [list tid list_client_tids ] ts_start ts_end_list {}] - variable free_threads - set mytid [thread::id] ;#caller of shellthread::manager::xxx is the client thread - - set subscriberless_tags [list] - foreach source $sourcetaglist { - if {[dict exists $workers $source]} { - set list_client_tids [dict get $workers $source list_client_tids] - if {[set posn [lsearch $list_client_tids $mytid]] >= 0} { - set list_client_tids [lreplace $list_client_tids $posn $posn] - dict set workers $source list_client_tids $list_client_tids - } - if {![llength $list_client_tids]} { - lappend subscriberless_tags $source - } - } - } - - #we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all. - - set subscriberless_workers [list] - set shuttingdown_workers [list] - foreach deadtag $subscriberless_tags { - set workertid [dict get $workers $deadtag tid] - set worker_tags [get_worker_tagstate $workertid] - set subscriber_count 0 - set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed - foreach taginfo $worker_tags { - incr subscriber_count [llength [dict get $taginfo list_client_tids]] - incr kill_count [llength [dict get $taginfo ts_end_list]] - } - if {$subscriber_count == 0} { - lappend subscriberless_workers $workertid - } - if {$kill_count > 0} { - lappend shuttingdown_workers $workertid - } - } - - #if worker isn't shutting down - add it to free_threads list - foreach workertid $subscriberless_workers { - if {$workertid ni $shuttingdown_workers} { - if {$workertid ni $free_threads && $workertid ne "noop"} { - lappend free_threads $workertid - } - } - } - - #todo - #unsub this client_tid from the sourcetags in the sourcetaglist. if no more client_tids exist for sourcetag, remove sourcetag, - #if no more sourcetags - add worker to free_threads - } - proc get_worker_tagstate {workertid} { - variable workers - set taginfo_list [list] - dict for {source sourceinfo} $workers { - if {[dict get $sourceinfo tid] eq $workertid} { - lappend taginfo_list $sourceinfo - } - } - return $taginfo_list - } - - #finalisation - proc shutdown_free_threads {{timeout 2500}} { - variable free_threads - if {![llength $free_threads]} { - return - } - upvar ::shellthread::manager::timeouts timeoutarr - if {[info exists timeoutarr(shutdown_free_threads)]} { - #already called - return false - } - #set timeoutarr(shutdown_free_threads) waiting - #after $timeout [list set timeoutarr(shutdown_free_threads) timed-out] - set ::shellthread::waitfor waiting - #after $timeout [list set ::shellthread::waitfor] - #2025-07 timed-out untested review - set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]] - - set waiting_for [list] - set ended [list] - set timedout 0 - foreach tid $free_threads { - if {[thread::exists $tid]} { - lappend waiting_for $tid - #thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads) - thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor - } - } - if {[llength $waiting_for]} { - for {set i 0} {$i < [llength $waiting_for]} {incr i} { - vwait ::shellthread::waitfor - if {$::shellthread::waitfor eq "timed-out"} { - set timedout 1 - break - } else { - after cancel $cancelid - lappend ended $::shellthread::waitfor - } - } - } - set free_threads [list] - return [dict create existed $waiting_for ended $ended timedout $timedout] - } - - #TODO - important. - #REVIEW! - #since moving to the unsubscribe mechansm - close_worker $source isn't being called - # - we need to set a limit to the number of free threads and shut down excess when detected during unsubscription - #instruction to shut-down the thread that has this source. - #instruction to shut-down the thread that has this source. - proc close_worker {source {timeout 2500}} { - variable workers - variable worker_errors - variable free_threads - upvar ::shellthread::manager::timeouts timeoutarr - set ts_now [clock micros] - #puts stderr "close_worker $source" - if {[dict exists $workers $source]} { - set tidworker [dict get $workers $source tid] - if {$tidworker in $freethreads} { - #make sure a thread that is being closed is removed from the free_threads list - set posn [lsearch $freethreads $tidworker] - set freethreads [lreplace $freethreads $posn $posn] - } - set mytid [thread::id] - set client_tids [dict get $workers $source list_client_tids] - if {[set posn [lsearch $client_tids $mytid]] >= 0} { - set client_tids [lreplace $client_tids $posn $posn] - #remove self from list of clients - dict set workers $source list_client_tids $client_tids - } - set ts_end_list [dict get $workers $source ts_end_list] ;#ts_end_list is just a list of timestamps of closing calls for this source - only one is needed to close, but they may all come in a flurry. - if {[llength $ts_end_list]} { - set last_end_ts [lindex $ts_end_list end] - if {(($tsnow - $last_end_ts) / 1000) >= $timeout} { - lappend ts_end_list $ts_now - dict set workers $source ts_end_list $ts_end_list - } else { - #existing close in progress.. assume it will work - return - } - } - - if {[thread::exists $tidworker]} { - #puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source still running.. terminating" - - #review - timeoutarr is local var (?) - set timeoutarr($source) 0 - after $timeout [list set timeoutarr($source) 2] - - thread::send -async $tidworker [list shellthread::worker::send_errors_now [thread::id]] - thread::send -async $tidworker [list shellthread::worker::terminate [thread::id]] timeoutarr($source) - - #thread::send -async $tidworker [string map [list %tidclient% [thread::id]] { - # shellthread::worker::terminate %tidclient% - #}] timeoutarr($source) - - vwait timeoutarr($source) - #puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source DONE1" - - thread::release $tidworker - #puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source DONE2" - if {[dict exists $workers $source errors]} { - set errlist [dict get $workers $source errors] - if {[llength $errlist]} { - lappend worker_errors [list $source [dict get $workers $source]] - } - } - dict unset workers $source - } else { - #thread may have been closed by call to close_worker with another source with same worker - #clear workers record for this source - #REVIEW - race condition for re-creation of source with new workerid? - #check that record is subscriberless to avoid this - if {[llength [dict get $workers $source list_client_tids]] == 0} { - dict unset workers $source - } - } - } - #puts stdout "close_worker $source - end" - } - - #worker errors only available for a source after close_worker called on that source - #It is possible for there to be multiple entries for a source because new_worker can be called multiple times with same sourcetag, - proc get_and_clear_errors {source} { - variable worker_errors - set source_errors [lsearch -all -inline -index 0 $worker_errors $source] - set worker_errors [lsearch -all -inline -index 0 -not $worker_errors $source] - return $source_errors - } - - -} - -package provide shellthread [namespace eval shellthread { - variable version - set version 1.6.1 -}] - - - - - - - - - diff --git a/src/bootsupport/modules/tomlish-1.1.2.tm b/src/bootsupport/modules/tomlish-1.1.2.tm deleted file mode 100644 index c7da645b..00000000 --- a/src/bootsupport/modules/tomlish-1.1.2.tm +++ /dev/null @@ -1,5680 +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 tomlish 1.1.2 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin tomlish_module_tomlish 0 1.1.2] -#[copyright "2024"] -#[titledesc {tomlish toml parser}] [comment {-- Name section and table of contents description --}] -#[moddesc {tomlish}] [comment {-- Description at end of page heading --}] -#[require tomlish] -#[keywords module parsing toml configuration] -#[description] -#[para] tomlish is an intermediate representation of toml data in a tree structure (tagged lists representing type information) -#[para] The design goals are for tomlish to be whitespace and comment preserving ie byte-for byte preservation during roundtrips from toml to tomlish and back to toml -#[para] The tomlish representation can then be converted to a Tcl dict structure or to other formats such as json, -#[para] although these other formats are generally unlikely to retain whitespace or comments -#[para] A further goal is to allow at least a useful subset of in-place editing operations which also preserve whitespace and comments. -#[para] e.g leaf key value editing, and table reordering/sorting, key-renaming at any level, key insertions/deletions -#[para] The API for editing (tomldoc object?) may require explicit setting of type if accessing an existing key -#[para] e.g setting a key that already exists and is a different type (especially if nested structure such as a table or array) -#[para] will need a -type option (-force ?) to force overriding with another type such as an int. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of tomlish -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by tomlish -#[list_begin itemized] - -package require Tcl 8.6- -package require struct::stack -package require logger - -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {struct::stack}] - -#limit ourselves to clear, destroy, peek, pop, push, rotate, or size (e.g v 1.3 does not implement 'get') - - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish { - namespace export {[a-z]*}; # Convention: export all lowercase - variable types - - #IDEAS: - # since get_toml produces tomlish with whitespace/comments intact: - # tomldoc object - allow (at least basic?) editing of toml whilst preserving comments & whitespace - # - setKey (set leaf only to value) how to specify type? -type option? - whole array vs index into arrays and further nested objects? - option for raw toml additions? - # - separate addKey?? - # - deleteKey (delete leaf) - # - deleteTable (delete table - if only has leaves? - option to delete with child tables?) - # - set/add Table? - position in doc based on existing tables/subtables? - - #The tomlish intermediate representation allows things such as sorting the toml document by table name or other re-ordering of tables - - # because the tables include subkeys, comments and newlines within their structure - those elements all come along with it nicely during reordering. - #The same goes for the first newline following a keyval e.g x=1 \ny=2\n\n - #The newline is part of the keyval structure so makes reordering easier - #example from_toml "a=1\nb=2\n\n\n" - # 0 = TOMLISH - # 1 = KEY a = {INT 1} {NEWLINE lf} - # 2 = NEWLINE lf - # 3 = KEY b = {INT 2} {NEWLINE lf} - # 4 = NEWLINE lf - # 5 = NEWLINE lf - - - #ARRAY is analogous to a Tcl list - #TABLE is analogous to a Tcl dict - #WS = inline whitespace - #KEY = bare key and value - #QKEY = double quoted key and value ;#todo - rename to DQKEY? - #SQKEY = single quoted key and value - #ITABLE = inline table (*can* be anonymous table) - # inline table values immediately create a table with the opening brace - # inline tables are fully defined between their braces, as are dotted-key subtables defined within - # No additional subtables or arrays of tables may be defined within an inline table after the ending brace - they must be entirely self-contained - - set tags [list TOMLISH ARRAY TABLE ITABLE ANONTABLE WS NEWLINE COMMENT DOTTEDKEY KEY QKEY SQKEY STRING STRINGPART MULTISTRING LITERAL LITERALPART MULTILITERAL INT FLOAT BOOL DATETIME] - #tomlish v1.0 should accept arbitrary 64-bit signed ints (from -2^63 to 2^63-1) - #we will restrict to this range for compatibility for now - although Tcl can handle larger (arbitrarily so?) - set min_int -9223372036854775808 ;#-2^63 - set max_int +9223372036854775807 ;#2^63-1 - - proc Dolog {lvl txt} { - #return "$lvl -- $txt" - set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] tomlish '$txt'" - puts stderr $msg - } - logger::initNamespace ::tomlish - foreach lvl [logger::levels] { - interp alias {} tomlish_log_$lvl {} ::tomlish::Dolog $lvl - log::logproc $lvl tomlish_log_$lvl - } - - #*** !doctools - #[subsection {Namespace tomlish}] - #[para] Core API functions for tomlish - #[list_begin definitions] - - proc tags {} { - return $::tomlish::tags - } - - #helper function for to_dict - proc _get_keyval_value {keyval_element} { - log::notice ">>> _get_keyval_value from '$keyval_element'<<<" - set found_value 0 - #find the value - # 3 is the earliest index at which the value could occur (depending on whitespace) - set found_sub [list] - if {[lindex $keyval_element 2] ne "="} { - error "tomlish _get_keyval_value keyval_element doesn't seem to be a properly structured { = } list" - } - foreach sub [lrange $keyval_element 2 end] { - #note that a barekey/quotedkey won't occur directly inside a barekey/quotedkey - switch -exact -- [lindex $sub 0] { - STRING - LITERAL - MULTISTRING - MULTILITERAL - INT - FLOAT - BOOL - DATETIME - TABLE - ARRAY - ITABLE { - set type [lindex $sub 0] - set value [lindex $sub 1] - set found_sub $sub - incr found_value 1 - } - default {} - } - } - if {!$found_value} { - error "tomlish Failed to find value element in KEY. '$keyval_element'" - } - if {$found_value > 1} { - error "tomlish Found multiple value elements in KEY, expected exactly one. '$keyval_element'" - } - - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - #simple (non-container, no-substitution) datatype - set result [list type $type value $value] - } - STRING - STRINGPART { - set result [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL - LITERALPART { - #REVIEW - set result [list type $type value $value] - } - TABLE { - #invalid? - error "tomlish _get_keyval_value invalid to have type TABLE on rhs of =" - } - ITABLE { - #This one should not be returned as a type value structure! - # - set result [::tomlish::to_dict [list $found_sub]] - } - ARRAY { - #we need to recurse to get the corresponding dict for the contained item(s) - #pass in the whole $found_sub - not just the $value! - set result [list type $type value [::tomlish::to_dict [list $found_sub]]] - } - MULTISTRING - MULTILITERAL { - #review - mapping these to STRING might make some conversions harder? - #if we keep the MULTI - we know we have to look for newlines for example when converting to json - #without specific types we'd have to check every STRING - and lose info about how best to map chars within it - set result [list type $type value [::tomlish::to_dict [list $found_sub]]] - } - default { - error "tomlish Unexpected value type '$type' found in keyval '$keyval_element'" - } - } - return $result - } - - proc _get_dottedkey_info {dottedkeyrecord} { - set key_hierarchy [list] - set key_hierarchy_raw [list] - if {[lindex $dottedkeyrecord 0] ne "DOTTEDKEY"} { - error "tomlish _get_dottedkey_info error. Supplied list doesn't appear to be a DOTTEDKEY (tag: [lindex $dottedkeyrecord 0])" - } - set compoundkeylist [lindex $dottedkeyrecord 1] - set expect_sep 0 - foreach part $compoundkeylist { - set parttag [lindex $part 0] - if {$parttag eq "WS"} { - continue - } - if {$expect_sep} { - if {$parttag ne "DOTSEP"} { - error "DOTTEDKEY missing dot separator between parts. '$dottedkeyrecord'" - } - set expect_sep 0 - } else { - set val [lindex $part 1] - switch -exact -- $parttag { - KEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw $val - } - QKEY { - lappend key_hierarchy [::tomlish::utils::unescape_string $val] - lappend key_hierarchy_raw \"$val\" - } - SQKEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw "'$val'" - } - default { - error "DOTTED key unexpected part '$parttag' - ensure dot separator is between key parts. '$item'" - } - } - set expect_sep 1 - } - } - return [dict create keys $key_hierarchy keys_raw $key_hierarchy_raw] - } - - - - #to_dict is a *basic* programmatic datastructure for accessing the data. - # produce a dictionary of keys and values from a tomlish tagged list. - # to_dict is primarily for reading toml data. - #Extraneous (not within quoted sections) whitespace and comments are not preserved in this structure, - # so a roundtrip from toml to this datastructure and back to toml will lose whitespace formatting and comments. - # creating/changing toml values can be done directly on a tomlish list if preserving (or adding) formatting/comments is desired. - #A separate package 'tomlish::object' may be needed to allow easier programmatic creating/updating/deleting of data elements whilst preserving (or adding or selectively deleting/editing) such formatting. - # - #Note, currently we get a plain sub dictionary when an inline table is a direct value for a key, but an ITABLE when it's in an ARRAY - REVIEW - #Namespacing? - #ie note the difference: - #[Data] - #temp = { cpu = 79.5, case = 72.0} - # versus - #[Data] - #temps = [{cpu = 79.5, case = 72.0}] - proc to_dict {tomlish} { - - #keep track of which tablenames have already been directly defined, - # so we can raise an error to satisfy the toml rule: 'You cannot define any key or table more than once. Doing so is invalid' - #Note that [a] and then [a.b] is ok if there are no subkey conflicts - so we are only tracking complete tablenames here. - #we don't error out just because a previous tablename segment has already appeared. - ##variable tablenames_seen [list] - if {[uplevel 1 [list info exists tablenames_seen]]} { - upvar tablenames_seen tablenames_seen - } else { - set tablenames_seen [list] - } - - log::info ">>> processing '$tomlish'<<<" - set items $tomlish - - foreach lst $items { - if {[lindex $lst 0] ni $::tomlish::tags} { - error "supplied string does not appear to be toml parsed into a tomlish tagged list. Run tomlish::decode::toml on the raw toml data to produce a tomlish list" - } - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - #ignore TOMLISH tag at beginning - set items [lrange $tomlish 1 end] - } - - set datastructure [dict create] - foreach item $items { - set tag [lindex $item 0] - #puts "...> item:'$item' tag:'$tag'" - switch -exact -- $tag { - KEY - QKEY - SQKEY { - log::debug "--> processing $tag: $item" - set key [lindex $item 1] - if {$tag eq "QKEY"} { - set key [::tomlish::utils::unescape_string $key] - } - #!todo - normalize key. (may be quoted/doublequoted) - - if {[dict exists $datastructure $key]} { - error "Duplicate key '$key'. The key already exists at this level in the toml data. The toml data is not valid." - } - - #lassign [_get_keyval_value $item] type val - set keyval_dict [_get_keyval_value $item] - dict set datastructure $key $keyval_dict - } - DOTTEDKEY { - log::debug "--> processing $tag: $item" - set dkey_info [_get_dottedkey_info $item] - set dotted_key_hierarchy [dict get $dkey_info keys] - set dotted_key_hierarchy_raw [dict get $dkey_info keys_raw] - - #a.b.c = 1 - #table_key_hierarchy -> a b - #leafkey -> c - if {[llength $dotted_key_hierarchy] == 0} { - #empty?? probably invalid. review - #This is different to '' = 1 or ''.'' = 1 which have lengths 1 and 2 respectively - error "DOTTED key has no parts - invalid? '$item'" - } elseif {[llength $dotted_key_hierarchy] == 1} { - #dottedkey is only a key - no table component - set table_hierarchy [list] - set leafkey [lindex $dotted_key_hierarchy 0] - } else { - set table_hierarchy [lrange $dotted_key_hierarchy 0 end-1] - set table_hierarchy_raw [lrange $dotted_key_hierarchy_raw 0 end-1] - set leafkey [lindex $dotted_key_hierarchy end] - } - - #ensure empty tables are still represented in the datastructure - set pathkeys [list] - foreach k $table_hierarchy { - lappend pathkeys $k - if {![dict exists $datastructure {*}$pathkeys]} { - dict set datastructure {*}$pathkeys [list] - } else { - tomlish::log::notice "to_dict datastructure at key path $pathkeys already had data: [dict get $datastructure {*}$pathkeys]" - } - } - - set keyval_dict [_get_keyval_value $item] - dict set datastructure {*}$pathkeys $leafkey $keyval_dict - - #JMN test 2025 - - } - TABLE { - set tablename [lindex $item 1] - set tablename [::tomlish::utils::tablename_trim $tablename] - - if {$tablename in $tablenames_seen} { - error "Table name '$tablename' has already been directly defined in the toml data. Invalid." - } - - log::debug "--> processing $tag (name: $tablename): $item" - set name_segments [::tomlish::utils::tablename_split $tablename] - set last_seg "" - #toml spec rule - all segments mst be non-empty - #note that the results of tablename_split are 'raw' - ie some segments may be enclosed in single or double quotes. - - set table_key_hierarchy [list] - set table_key_hierarchy_raw [list] - - foreach rawseg $name_segments { - - set seg [::tomlish::utils::normalize_key $rawseg] ;#strips one level of enclosing quotes, and substitutes only toml-specified escapes - set c1 [tcl::string::index $rawseg 0] - set c2 [tcl::string::index $rawseg end] - if {($c1 eq "'") && ($c2 eq "'")} { - #single quoted segment. No escapes are processed within it. - set seg [tcl::string::range $rawseg 1 end-1] - } elseif {($c1 eq "\"") && ($c2 eq "\"")} { - #double quoted segment. Apply escapes. - set seg [::tomlish::utils::unescape_string [tcl::string::range $rawseg 1 end-1]] - } else { - set seg $rawseg - } - - #no need to check for empty segments here - we've already called tablename_split which would have raised an error for empty segments. - #if {$rawseg eq ""} { - # error "Table name '[lindex $item 1]' is not valid. All segments (parts between dots) must be non-empty" - #} - lappend table_key_hierarchy $seg - lappend table_key_hierarchy_raw $rawseg - - if {[dict exists $datastructure {*}$table_key_hierarchy]} { - #It's ok for this key to already exist *if* it was defined by a previous tablename or equivalent - #and if this key is longer - - #consider the following 2 which are legal: - #[table] - #x.y = 3 - #[table.x.z] - #k= 22 - - #equivalent - - #[table] - #[table.x] - #y = 3 - #[table.x.z] - #k=22 - - #illegal - #[table] - #x.y = 3 - #[table.x.y.z] - #k = 22 - ## - we should bfail on encoungerint table.x.y because only table and table.x are effectively tables - ## - we should also fail if - - #illegal - #[table] - #x.y = {p=3} - #[table.x.y.z] - #k = 22 - ## we should fail because y is an inline table which is closed to further entries - - - #TODO! fix - this code is wrong - - set testkey [join $table_key_hierarchy_raw .] - - set testkey_length [llength $table_key_hierarchy_raw] - set found_testkey 0 - if {$testkey in $tablenames_seen} { - set found_testkey 1 - } else { - #see if it was defined by a longer entry - foreach seen $tablenames_seen { - set seen_segments [::tomlish::utils::tablename_split $seen] - #these are raw unnormalized tablename segments. Need to normalize the double-quoted ones, - # and strip the quotes from both single-quoted and double-quoted entries. - - #note: it is not safe to compare normalized tablenames using join! - # e.g a.'b.c'.d is not the same as a.b.c.d - # instead compare {a b.c d} with {a b c d} - # Here is an example where the number of keys is the same, but they must be compared as a list, not a joined string. - #'a.b'.'c.d.e' vs 'a.b.c'.'d.e' - - #VVV the test below is wrong VVV! - #we need to normalize the tablenames seen so that {"x\ty"} matches {"xy"} - - set seen_match [join [lrange $seen_segments 0 [expr {$testkey_length -1}]] .] - puts stderr "testkey:'$testkey' vs seen_match:'$seen_match'" - if {$testkey eq $seen_match} { - set found_testkey 1 - } - } - } - - if {$found_testkey == 0} { - #the raw table_key_hierarchy is better to display in the error message, although it's not the actual dict keyset - set msg "key [join $table_key_hierarchy_raw .] already exists in datastructure, but wasn't defined by a supertable." - append msg \n "tablenames_seen:" \n - foreach ts $tablenames_seen { - append msg " " $ts \n - } - error $msg - } - } - - } - - #ensure empty tables are still represented in the datastructure - set table_keys [list] - foreach k $table_key_hierarchy { - lappend table_keys $k - if {![dict exists $datastructure {*}$table_keys]} { - dict set datastructure {*}$table_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (TABLE) subkey $table_keys already had data: [dict get $datastructure {*}$table_keys]" - } - } - - #We must do this after the key-collision test above! - lappend tablenames_seen $tablename - - - log::debug ">>>>>>>>>>>>>>>>>>>>table_key_hierarchy : $table_key_hierarchy" - log::debug ">>>>>>>>>>>>>>>>>>>>table_key_hierarchy_raw: $table_key_hierarchy_raw" - - #now add the contained elements - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - log::debug "--> $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkey_info [_get_dottedkey_info $element] - #e.g1 keys {x.y y} keys_raw {'x.y' y} - #e.g2 keys {x.y y} keys_raw {{"x.y"} y} - set dotted_key_hierarchy [dict get $dkey_info keys] - set dkeys [lrange $dotted_key_hierarchy 0 end-1] - set leaf_key [lindex $dotted_key_hierarchy end] - set dotted_key_hierarchy_raw [dict get $dkey_info keys_raw] - set dkeys_raw [lrange $dotted_key_hierarchy_raw 0 end-1] - set leaf_key_raw [lindex $dotted_key_hierarchy_raw end] - - #ensure empty keys are still represented in the datastructure - set test_keys $table_keys - foreach k $dkeys { - lappend test_keys $k - if {![dict exists $datastructure {*}$test_keys]} { - dict set datastructure {*}$test_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (DOTTEDKEY) subkey $test_keys already had data: [dict get $datastructure {*}$test_keys]" - } - } - - if {[dict exists $datastructure {*}$table_keys {*}$dkeys $leaf_key]} { - error "Duplicate key '$table_keys $dkeys $leaf_key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - #keyval_dict is either a {type value } - #or the result from parsing an arbitrary dict from an inline table - which could theoretically look the same at the topmost level - #punk::dict::is_tomlish_typeval can distinguish - puts stdout ">>> $keyval_dict" - dict set datastructure {*}$table_keys {*}$dkeys $leaf_key $keyval_dict - #JMN 2025 - #tomlish::utils::normalize_key ?? - lappend tablenames_seen [join [list {*}$table_key_hierarchy_raw {*}$dkeys_raw] .] ;#???? - #if the keyval_dict is not a simple type x value y - then it's an inline table ? - #if so - we should add the path to the leaf_key as a seen table too - as it's not allowed to have more entries added. - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - #the value is either empty or or a dict structure with arbitrary (from-user-data) toplevel keys - # inner structure will contain {type value } if all leaves are not empty ITABLES - lappend tablenames_seen [join [list {*}$table_key_hierarchy_raw {*}$dkeys_raw $leaf_key_raw] .] - } - - } - KEY - QKEY - SQKEY { - #obsolete ? - set keyval_key [lindex $element 1] - if {$type eq "QKEY"} { - set keyval_key [::tomlish::utils::unescape_string $keyval_key] - } - if {[dict exists $datastructure {*}$dotted_key_hierarchy $keyval_key]} { - error "Duplicate key '$dotted_key_hierarchy $key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - dict set datastructure {*}$dotted_key_hierarchy $keyval_key $keyval_dict - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in table context. Expected only KEY,QKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - #now make sure we add an empty value if there were no contained elements! - #!todo. - } - ITABLE { - #SEP??? - set datastructure [list] - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - switch -exact -- $type { - DOTTEDKEY { - set dkey_info [_get_dottedkey_info $element] - set dotted_key_hierarchy [dict get $dkey_info keys] - set dotted_key_hierarchy_raw [dict get $dkey_info keys_raw] - set leaf_key [lindex $dotted_key_hierarchy end] - set dkeys [lrange $dotted_key_hierarchy 0 end-1] - - #ensure empty keys are still represented in the datastructure - set table_keys [list] ;#We don't know the context - next level up will have to check for key collisions? - set test_keys $table_keys - foreach k $dkeys { - lappend test_keys $k - if {![dict exists $datastructure {*}$test_keys]} { - dict set datastructure {*}$test_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (DOTTEDKEY) subkey $test_keys already had data: [dict get $datastructure {*}$test_keys]" - } - } - - if {[dict exists $datastructure {*}$table_keys {*}$dkeys $leaf_key]} { - error "Duplicate key '$table_keys $dkeys $leaf_key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - dict set datastructure {*}$table_keys {*}$dkeys $leaf_key $keyval_dict - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in ITABLE context. Expected only KEY,QKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - } - ARRAY { - #arrays in toml are allowed to contain mixtures of types - set datastructure [list] - log::debug "--> processing array: $item" - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - STRING { - set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - ITABLE - TABLE - ARRAY - MULTISTRING - MULTILITERAL { - set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - } - WS - SEP - NEWLINE - COMMENT { - #ignore whitespace, commas, newlines and comments - } - default { - error "Unexpected value type '$type' found in array" - } - } - } - } - MULTILITERAL { - #triple squoted string - #first newline stripped only if it is the very first element - #(ie *immediately* following the opening delims) - #All whitespace other than newlines is within LITERALPARTS - # ------------------------------------------------------------------------- - #todo - consider extension to toml to allow indent-aware multiline literals - # how - propose as issue in toml github? Use different delim? e.g ^^^ ? - #e.g - # xxx=?'''abc - # def - # etc - # ''' - # - we would like to trimleft each line to the column following the opening delim - # ------------------------------------------------------------------------- - - log::debug "--> processing multiliteral: $item" - set parts [lrange $item 1 end] - if {[lindex $parts 0 0] eq "NEWLINE"} { - set parts [lrange $parts 1 end] ;#skip it - } - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - switch -exact -- $type { - LITERALPART { - append stringvalue [lindex $element 1] - } - NEWLINE { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - MULTISTRING { - #triple dquoted string - log::debug "--> processing multistring: $item" - set stringvalue "" - set idx 0 - set parts [lrange $item 1 end] - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - #We use STRINGPART in the tomlish representation as a distinct element to STRING - which would imply wrapping quotes to be reinserted - switch -exact -- $type { - STRING { - #todo - do away with STRING ? - #we don't build MULTISTRINGS containing STRING - but should we accept it? - tomlish::log::warn "double quoting a STRING found in MULTISTRING - should be STRINGPART?" - append stringvalue "\"[::tomlish::utils::unescape_string [lindex $element 1]]\"" - } - STRINGPART { - append stringvalue [::tomlish::utils::unescape_string [lindex $element 1]] - } - CONT { - #When the last non-whitespace character on a line is an unescaped backslash, - #it will be trimmed along with all whitespace (including newlines) up to the next non-whitespace character or closing delimiter - # review - we allow some whitespace in stringpart elements - can a stringpart ever be all whitespace? - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - append stringvalue "\\" ;#add the sep - } else { - #skip over ws without emitting - set idx [llength $parts] - } - } else { - set parts_til_nl [lrange $parts 0 $next_nl-1] - set non_ws [lsearch -index 0 -start $idx+1 -not $parts_til_nl WS] - if {$non_ws >= 0} { - append stringvalue "\\" - } else { - #skip over ws on this line - set idx $next_nl - #then have to check each subsequent line until we get to first non-whitespace - set trimming 1 - while {$trimming && $idx < [llength $parts]} { - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - } else { - set idx [llength $parts] - } - set trimming 0 - } else { - set non_ws [lsearch -index 0 -start $idx+1 -not [lrange $parts 0 $next_nl-1] WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - set trimming 0 - } else { - set idx $next_nl - #keep trimming - } - } - } - } - } - } - NEWLINE { - #if newline is first element - it is not part of the data of a multistring - if {$idx > 0} { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - } - WS { - append stringvalue [lindex $element 1] - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - WS - COMMENT - NEWLINE { - #ignore - } - default { - error "Unexpected tag '$tag' in Tomlish list '$tomlish'" - } - } - } - return $datastructure - } - - - proc _from_dictval_tomltype {parents tablestack keys typeval} { - set type [dict get $typeval type] - set val [dict get $typeval value] - switch -- $type { - ARRAY { - set subitems [list] - foreach item $val { - lappend subitems [_from_dictval [list {*}$parents ARRAY] $tablestack $keys $item] SEP - } - if {[lindex $subitems end] eq "SEP"} { - set subitems [lrange $subitems 0 end-1] - } - return [list ARRAY {*}$subitems] - } - ITABLE { - if {$val eq ""} { - return ITABLE - } else { - return [_from_dictval [list {*}$parents ITABLE] $tablestack $keys $val] - } - } - MULTISTRING { - #value is a raw string that isn't encoded as tomlish - #create a valid toml snippet with the raw value and decode it to the proper tomlish MULTISTRING format - set tomlpart "x=\"\"\"\\\n" - append tomlpart $val "\"\"\"" - set tomlish [tomlish::decode::toml $tomlpart] - #e.g if val = " etc\nblah" - #TOMLISH {DOTTEDKEY {{KEY x}} = {MULTISTRING CONT {NEWLINE LF} {WS { }} {STRINGPART etc} {NEWLINE lf} {STRINGPART blah} } } - #lindex 1 3 is the MULTISTRING tomlish list - return [lindex $tomlish 1 3] - } - MULTILITERAL { - set tomlpart "x='''\n" - append tomlpart $val ''' - set tomlish [tomlish::decode::toml $tomlpart] - return [lindex $tomlish 1 3] - } - default { - return [list $type $val] - } - } - } - - proc _from_dictval {parents tablestack keys vinfo} { - set k [lindex $keys end] - if {[regexp {\s} $k] || [string first . $k] >= 0} { - #Any dot in the key would have been split by to_dict - so if it's present here it's part of this key - not a level separator! - #todo - QKEY? - set K_PART [list SQKEY $k] - } else { - set K_PART [list KEY $k] - } - puts stderr "---parents:'$parents' keys:'$keys' vinfo: $vinfo---" - puts stderr "---tablestack: $tablestack---" - set result [list] - set lastparent [lindex $parents end] - if {$lastparent in [list "" do_inline]} { - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - set type [dict get $vinfo type] - #treat ITABLE differently? - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result DOTTEDKEY [list $K_PART {WS { }}] = {WS { }} $sublist {NEWLINE lf} - } else { - #set result [list TABLE $k {NEWLINE lf}] - if {$vinfo ne ""} { - - #set result [list DOTTEDKEY [list [list KEY $k]] = ] - #set records [list ITABLE] - - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - - if {$lastparent eq "do_inline"} { - set result [list DOTTEDKEY [list $K_PART] =] - set records [list ITABLE] - } else { - #review - quoted k ?? - set result [list TABLE $k {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $k]] - set records [list] - } - - - - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - dict for {vk vv} $vinfo { - if {[regexp {\s} $vk] || [string first . $vk] >= 0} { - set VK_PART [list SQKEY $vk] - } else { - set VK_PART [list KEY $vk] - } - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART {WS { }}] = {WS { }} $sublist] - } else { - if {$vv eq ""} { - #experimental - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline KEY $vk (tablestack:$tablestack)" - #set tname [tomlish::dict::name_from_tablestack [list {*}$tablestack [list T $vk]]] - set tname [join [list {*}$keys $vk] .] - set record [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - } else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - } - } else { - if { 0 } { - #experiment.. sort of getting there. - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline2 KEYS [list {*}$keys $vk] (tablestack:$tablestack)" - set tname [join [list {*}$keys $vk] .] - set record [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - - #review - todo? - set dottedkey_value [_from_dictval [list {*}$parents TABLE] $tablestack [list {*}$keys $vk] $vv] - lappend record {*}$dottedkey_value - - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - } - if {$dictidx != $lastidx} { - #lappend record SEP - if {$lastparent eq "do_inline"} { - lappend record SEP - } else { - lappend record {NEWLINE lf} - } - } - lappend records $record - incr dictidx - } - if {$lastparent eq "do_inline"} { - lappend result $records {NEWLINE lf} - } else { - lappend result {*}$records {NEWLINE lf} - } - } else { - if {$lastparent eq "do_inline"} { - lappend result DOTTEDKEY [list [list KEY $k]] = ITABLE {NEWLINE lf} - } else { - lappend result TABLE $k {NEWLINE lf} - } - } - } - } else { - #lastparent is not toplevel "" or "do_inline" - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result {*}$sublist - } else { - if {$lastparent eq "TABLE"} { - #review - dict for {vk vv} $vinfo { - set dottedkey_value [_from_dictval [list {*}$parents DOTTEDKEY] $tablestack [list {*}$keys $vk] $vv] - lappend result [list DOTTEDKEY [list [list KEY $vk]] = $dottedkey_value {NEWLINE lf}] - } - } else { - if {$vinfo ne ""} { - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - set sub [list] - set result $lastparent ;#e.g sets ITABLE - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - dict for {vk vv} $vinfo { - if {[regexp {\s} $vk] || [string first . $vk] >=0} { - set VK_PART [list SQKEY $vk] - } else { - set VK_PART [list KEY $vk] - } - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART] = $sublist] - } else { - if {$vv eq ""} { - #can't just uninline at this level - #we need a better method to query main dict for uninlinability at each level - # (including what's been inlined already) - #if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - # puts stderr "_from_dictval uninline2 KEY $keys" - # set tname [join [list {*}$keys $vk] .] - # set record [list TABLE $tname {NEWLINE lf}] - # set tablestack [list {*}$tablestack [list T $vk]] - #} else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - #} - } else { - #set sub [_from_dictval ITABLE $vk $vv] - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - #set record [list DOTTEDKEY [list $VK_PART] = ITABLE $dottedkey_value] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - if {$dictidx != $lastidx} { - lappend record SEP - } - lappend result $record - incr dictidx - } - } else { - puts stderr "table x-1" - lappend result DOTTEDKEY [list $K_PART] = ITABLE - } - } - } - } - return $result - } - - - proc from_dict {d} { - #consider: - # t1={a=1,b=2} - # x = 1 - #If we represent t1 as an expanded table we get - # [t1] - # a=1 - # b=2 - # x=1 - # --- which is incorrect - as x was a toplevel key like t1! - #This issue doesn't occur if x is itself an inline table - # t1={a=1,b=2} - # x= {no="problem"} - # - # (or if we were to reorder x to come before t1) - - #As the dictionary form doesn't distinguish the structure used to create tables {[table1]\nk=v} vs inline {table1={k=v}} - #Without a solution, from_dict would have to always produce the inline form for toplevel tables unless we allowed re-ordering, - #which is unpreferred here. - - #A possible solution: - #scan the top level to see if all (trailing) elements are themselves dicts - # (ie not of form {type XXX value yyy}) - # - # A further point is that if all root level values are at the 'top' - we can treat lower table-like structures as {[table]} elements - #ie we don't need to force do_inline if all the 'simple' keys are before any compound keys - - #set root_has_values 0 - #approach 1) - the naive approach - forces inline when not always necessary - #dict for {k v} $d { - # if {[llength $v] == 4 && [lindex $v 0] eq "type"} { - # set root_has_values 1 - # break - # } - #} - - - #approach 2) - track the position of last {type x value y} in the dictionary built by to_dict - # - still not perfect. Inlines dotted tables unnecessarily - #This means from_dict doesn't produce output optimal for human editing. - set last_simple [tomlish::dict::last_tomltype_posn $d] - - - ## set parent "do_inline" ;#a value used in _from_dictval to distinguish from "" or other context based parent values - #Any keys that are themselves tables - will need to be represented inline - #to avoid reordering, or incorrect assignment of plain values to the wrong table. - - ## set parent "" - #all toplevel keys in the dict structure can represent subtables. - #we are free to use {[tablename]\n} syntax for toplevel elements. - - - set tomlish [list TOMLISH] - set dictposn 0 - set tablestack [list [list T root]] ;#todo - dict for {t tinfo} $d { - if {$last_simple > $dictposn} { - set parents [list do_inline] - } else { - set parents [list ""] - } - set keys [list $t] - set trecord [_from_dictval $parents $tablestack $keys $tinfo] - lappend tomlish $trecord - incr dictposn - } - return $tomlish - } - - proc json_to_toml {json} { - #*** !doctools - #[call [fun json_to_toml] [arg json]] - #[para] - - set tomlish [::tomlish::from_json $json] - set toml [::tomlish::to_toml $tomlish] - } - - #TODO use huddle? - proc from_json {json} { - set jstruct [::tomlish::json_struct $json] - return [::tomlish::from_json_struct $jstruct] - } - - proc from_json_struct {jstruct} { - package require fish::json_toml - return [fish::json_toml::jsonstruct2tomlish $jstruct] - } - - proc toml_to_json {toml} { - set tomlish [::tomlish::from_toml $toml] - return [::tomlish::get_json $tomlish] - } - - proc get_json {tomlish} { - package require fish::json - set d [::tomlish::to_dict $tomlish] - #return [::tomlish::dict_to_json $d] - return [fish::json::from "struct" $d] - } - - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -namespace eval tomlish::encode { - #*** !doctools - #[subsection {Namespace tomlish::encode}] - #[para] - #[list_begin definitions] - - #STRING,INT,FLOAT,BOOL, DATETIME - simple wrappers for completeness - # take a value of the appropriate type and wrap as a tomlish tagged item - proc string {s} { - return [list STRING $s] - } - - proc int {i} { - #whole numbers, may be prefixed with a + or - - #Leading zeros are not allowed - #Hex,octal binary forms are allowed (toml 1.0) - #We will error out on encountering commas, as commas are interpreted differently depending on locale (and don't seem to be supported in the toml spec anyway) - #!todo - Tcl can handle bignums - bigger than a 64bit signed long as specified in toml. - # - We should probably raise an error for number larger than this and suggest the user supply it as a string? - if {[tcl::string::last , $i] > -1} { - error "Unable to interpret '$i' as an integer. Use underscores if you need a thousands separator [::tomlish::parse::report_line]" - } - if {![::tomlish::utils::int_validchars $i]} { - error "Unable to interpret '$i' as an integer. Only 0-9 + 1 _ characters are acceptable. [::tomlish::parse::report_line]" - } - - if {[::tomlish::utils::is_int $i]} { - return [list INT $i] - } else { - error "'$i' is not a valid integer as per the Toml spec. [::tomlish::parse::report_line]" - } - - } - - proc float {f} { - #convert any non-lower case variants of special values to lowercase for Toml - if {[::tcl::string::tolower $f] in {nan +nan -nan inf +inf -inf}} { - return [list FLOAT [tcl::string::tolower $f]] - } - if {[::tomlish::utils::is_float $f]} { - return [list FLOAT $f] - } else { - error "Unable to interpret '$f' as Toml float. Check your input, or check that tomlish is able to handle all Toml floats properly [::tomlish::parse::report_line]" - } - } - - proc datetime {str} { - if {[::tomlish::utils::is_datetime $str]} { - return [list DATETIME $str] - } else { - error "Unable to interpret '$str' as Toml datetime. Check your input, or check that tomlish is able to handle all Toml datetimes properly [::tomlish::parse::report_line]" - } - } - - proc boolean {b} { - #convert any Tcl-acceptable boolean to boolean as accepted by toml - lower case true/false - if {![tcl::string::is boolean -strict $b]} { - error "Unable to convert '$b' to Toml boolean true|false. [::tomlish::parse::report_line]" - } else { - if {[expr {$b && 1}]} { - return [::list BOOL true] - } else { - return [::list BOOL false] - } - } - } - - - #TODO - #Take tablename followed by - # a) *tomlish* name-value pairs e.g table mydata [list KEY item11 = [list STRING "test"]] {KEY item2 = [list INT 1]} - # (accept also key value {STRING }) - # b) simple 2-element tcl lists being name & *simple* value pairs for which basic heuristics will be used to determine types - proc table {name args} { - set pairs [list] - foreach t $args { - if {[llength $t] == 4} { - if {[tcl::string::tolower [lindex $t 0]] ne "key" || [tcl::string::tolower [lindex $t 2]] ni "= value"} { - error "Only items tagged as KEY = currently accepted as name-value pairs for table command" - } - lassign $t _k keystr _eq valuepart - if {[llength $valuepart] != 2} { - error "supplied value must be typed. e.g {INT 1} or {STRING test}" - } - lappend pairs [list KEY $keystr = $valuepart] - } elseif {[llength $t] == 2} { - #!todo - type heuristics - lassign $t n v - lappend pairs [list KEY $n = [list STRING $v]] - } else { - error "'KEY = { toml but - # the first newline is not part of the data. - # we elect instead to maintain a basic LITERALPART that must not contain newlines.. - # and to compose MULTILITERAL of multiple NEWLINE LITERALPART parts, - #with the datastructure representation dropping the first newline (if immediately following opening delim) when building the value. - set literal "" - foreach part [lrange $item 1 end] { - append literal [::tomlish::encode::tomlish [list $part] $nextcontext] - } - append toml '''$literal''' - } - INT - - BOOL - - FLOAT - - DATETIME { - append toml [lindex $item 1] - } - INCOMPLETE { - error "cannot process tomlish term tagged as INCOMPLETE" - } - COMMENT { - append toml "#[lindex $item 1]" - } - BOM { - #Byte Order Mark may appear at beginning of a file. Needs to be preserved. - append toml "\uFEFF" - } - default { - error "Not a properly formed 'tomlish' taggedlist.\n '$list'\n Unknown tag '[lindex $item 0]'. See output of \[tomlish::tags\] command." - } - } - - } - return $toml - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::encode ---}] -} -#fish toml from tomlish - -#(encode tomlish as toml) -interp alias {} tomlish::to_toml {} tomlish::encode::tomlish - -# - - -namespace eval tomlish::decode { - #*** !doctools - #[subsection {Namespace tomlish::decode}] - #[para] - #[list_begin definitions] - - #return a Tcl list of tomlish tokens - #i.e get a standard list of all the toml terms in string $s - #where each element of the list is a *tomlish* term.. i.e a specially 'tagged' Tcl list. - #(simliar to a tcl 'Huddle' - but also supporting whitespace preservation) - # ---------------------------------------------------------------------------------------------- - # NOTE: the production of tomlish from toml source doesn't indicate the toml source was valid!!! - # e.g we deliberately don't check certain things such as duplicate table declarations here. - # ---------------------------------------------------------------------------------------------- - #Part of the justification for this is that as long as the syntax is toml shaped - we can load files which violate certain rules and allow programmatic manipulation. - # (e.g perhaps a toml editor to highlight violations for fixing) - # A further stage is then necessary to load the tomlish tagged list into a data structure more suitable for efficient query/reading. - # e.g dicts or an object oriented structure - #Note also - *no* escapes in quoted strings are processed. This is up to the datastructure stage - #e.g to_dict will substitute \r \n \uHHHH \UHHHHHHH etc - #This is important for tomlish to maintain the ability to perform competely lossless round-trips from toml to tomlish and back to toml. - # (which is handy for testing as well as editing some part of the structure with absolutely no effect on other parts of the document) - #If we were to unescape a tab character for example - # - we have no way of knowing if it was originally specified as \t \u0009 or \U00000009 or directly as a tab character. - # For this reason, we also do absolutely no line-ending transformations based on platform. - # All line-endings are maintained as is, and even a file with mixed cr crlf line-endings will be correctly interpreted and can be 'roundtripped' - - proc toml {args} { - #*** !doctools - #[call [fun toml] [arg arg...]] - #[para] return a Tcl list of tomlish tokens - - set s [join $args \n] - - namespace upvar ::tomlish::parse is_parsing is_parsing - set is_parsing 1 - - - if {[info command ::tomlish::parse::spacestack] eq "::tomlish::parse::spacestack"} { - tomlish::parse::spacestack destroy - } - struct::stack ::tomlish::parse::spacestack - - namespace upvar ::tomlish::parse last_space_action last_space_action - namespace upvar ::tomlish::parse last_space_type last_space_type - - - namespace upvar ::tomlish::parse tok tok - set tok "" - - namespace upvar ::tomlish::parse type type - namespace upvar ::tomlish::parse tokenType tokenType - ::tomlish::parse::set_tokenType "" - namespace upvar ::tomlish::parse tokenType_list tokenType_list - set tokenType [list] ;#Flat (un-nested) list of tokentypes found - - namespace upvar ::tomlish::parse lastChar lastChar - set lastChar "" - - - set result "" - namespace upvar ::tomlish::parse nest nest - set nest 0 - - namespace upvar ::tomlish::parse v v ;#array keyed on nest level - - - set v(0) {TOMLISH} - array set s0 [list] ;#whitespace data to go in {SPACE {}} element. - set parentlevel 0 - - namespace upvar ::tomlish::parse i i - set i 0 - - namespace upvar ::tomlish::parse state state - - namespace upvar ::tomlish::parse braceCount braceCount - set barceCount 0 - namespace upvar ::tomlish::parse bracketCount bracketCount - set bracketCount 0 - - set sep 0 - set r 1 - namespace upvar ::tomlish::parse token_waiting token_waiting - set token_waiting [dict create] ;#if ::tok finds a *complete* second token during a run, it will put the 2nd one here to be returned by the next call. - - - set state "table-space" - ::tomlish::parse::spacestack push {type space state table-space} - namespace upvar ::tomlish::parse linenum linenum;#'line number' of input data. (incremented for each literal linefeed - but not escaped ones in data) - set linenum 1 - - set ::tomlish::parse::state_list [list] - try { - while {$r} { - set r [::tomlish::parse::tok $s] - #puts stdout "got tok: '$tok' while parsing string '$s' " - set next_tokenType_known 0 ;#whether we begin a new token here based on what terminated the token result of 'tok' - - - - #puts "got token: '$tok' tokenType='$tokenType'. while v($nest) = [set v($nest)]" - #puts "-->tok: $tok tokenType='$tokenType'" - set prevstate $state - set transition_info [::tomlish::parse::goNextState $tokenType $tok $state] - #review goNextState could perform more than one space_action - set space_action [dict get $transition_info space_action] - set newstate [dict get $transition_info newstate] ;#use of 'newstate' vs 'state' makes code clearer below - - if {[tcl::string::match "err-*" $state]} { - ::tomlish::log::warn "---- State error in state $prevstate for tokenType: $tokenType token value: $tok. $state aborting parse. [tomlish::parse::report_line]" - lappend v(0) [list ERROR tokentype $tokenType state $prevstate to $state leveldata [set v($nest)]] - return $v(0) - } - # --------------------------------------------------------- - #NOTE there may already be a token_waiting at this point - #set_token_waiting can raise an error here, - # in which case the space_action branch needs to be rewritten to handle the existing token_waiting - # --------------------------------------------------------- - - if {$space_action eq "pop"} { - #pop_trigger_tokens: newline tablename endarray endinlinetable - #note a token is a pop trigger depending on context. e.g first newline during keyval is a pop trigger. - set parentlevel [expr {$nest -1}] - set do_append_to_parent 1 ;#most tokens will leave this alone - but some like squote_seq need to do their own append - switch -exact -- $tokenType { - squote_seq { - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the xxx-squote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - switch -- $tok { - ' { - tomlish::parse::set_token_waiting type startsquote value $tok complete 1 startindex [expr {$i -1}] - } - '' { - #review - we should perhaps return double_squote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_squote value "" complete 1 startindex [expr {$i - 2}] - } - ''' { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 3}] - } - '''' { - switch -exact -- $prevstate { - leading-squote-space { - error "---- 4 squotes from leading-squote-space - shouldn't get here" - #we should have emitted the triple and left the last for next loop - } - trailing-squote-space { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left squote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]'" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "'"] - } - default { - error "--- don't know how to integrate extra trailing squote with data $v($parentlevel)" - } - } - } - default { - error "--- unexpected popped due to squote_seq but came from state '$prevstate' should have been leading-squote-space or trailing-squote-space" - } - } - } - ''''' { - switch -exact -- $prevstate { - leading-squote-space { - error "---- 5 squotes from leading-squote-space - shouldn't get here" - #we should have emitted the triple and left the following squotes for next loop - } - trailing-squote-space { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 squotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]''" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [LITERALPART "''"] - } - default { - error "--- don't know how to integrate extra trailing 2 squotes with data $v($parentlevel)" - } - } - } - default { - error "--- unexpected popped due to squote_seq but came from state '$prevstate' should have been leading-squote-space or trailing-squote-space" - } - } - } - } - puts "---- HERE squote_seq pop <$tok>" - } - triple_squote { - #presumably popping multiliteral-space - ::tomlish::log::debug "---- triple_squote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTILITERAL { - lappend merged $part - } - LITERALPART { - if {$lasttype eq "LITERALPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_squote unhandled part type [lindex $part 0] unable to merge leveldata: $v($next)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - equal { - if {$prevstate eq "dottedkey-space"} { - tomlish::log::debug "---- equal ending dottedkey-space for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - tablename { - #note: a tablename only 'pops' if we are greater than zero - error "---- tablename pop should already have been handled as special case zeropoppushspace in goNextState" - } - tablearrayname { - #!review - tablearrayname different to tablename regarding push/pop? - #note: a tablename only 'pops' if we are greater than zero - error "---- tablearrayname pop should already have been handled as special case zeropoppushspace in goNextState" - } - endarray { - #nothing to do here. - } - comma { - #comma for inline table will pop the keyvalue space - lappend v($nest) "SEP" - } - endinlinetable { - ::tomlish::log::debug "---- endinlinetable for last_space_action pop" - } - endmultiquote { - ::tomlish::log::debug "---- endmultiquote for last_space_action 'pop'" - } - default { - error "---- unexpected tokenType '$tokenType' for last_space_action 'pop'" - } - } - if {$do_append_to_parent} { - #e.g squote_seq does it's own appends as necessary - so won't get here - lappend v($parentlevel) [set v($nest)] - } - - incr nest -1 - - } elseif {$last_space_action eq "push"} { - set prevnest $nest - incr nest 1 - set v($nest) [list] - # push_trigger_tokens: barekey quotedkey startinlinetable startarray tablename tablearrayname - - - switch -exact -- $tokenType { - squote_seq_begin { - #### - if {[dict exists $transition_info starttok] && [dict get $transition_info starttok] ne ""} { - lassign [dict get $transition_info starttok] starttok_type starttok_val - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType $starttok_type - set tok $starttok_val - } - } - squotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - barekey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - set waiting [tomlish::parse::get_token_waiting] - if {[llength $waiting]} { - set i [dict get $waiting startindex] - tomlish::parse::clear_token_waiting - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } else { - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - } - startsquote { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - quotedkey - itablequotedkey { - set v($nest) [list QKEY $tok] ;#$tok is the keyname - } - itablesquotedkey { - set v($nest) [list SQKEY $tok] ;#$tok is the keyname - } - tablename { - #note: we do not use the output of tomlish::tablename_trim to produce a tablename for storage in the tomlish list! - #The tomlish list is intended to preserve all whitespace (and comments) - so a roundtrip from toml file to tomlish - # back to toml file will be identical. - #It is up to the datastructure stage to normalize and interpret tomlish for programmatic access. - # we call tablename_trim here only to to validate that the tablename data is well-formed at the outermost level, - # so we can raise an error at this point rather than create a tomlish list with obviously invalid table names. - - #todo - review! It's arguable that we should not do any validation here, and just store even incorrect raw tablenames, - # so that the tomlish list is more useful for say a toml editor. Consider adding an 'err' tag to the appropriate place in the - # tomlish list? - - set test_only [::tomlish::utils::tablename_trim $tok] - ::tomlish::log::debug "---- trimmed (but not normalized) tablename: '$test_only'" - set v($nest) [list TABLE $tok] ;#$tok is the *raw* table name - #note also that equivalent tablenames may have different toml representations even after being trimmed! - #e.g ["x\t\t"] & ["x "] (tab escapes vs literals) - #These will show as above in the tomlish list, but should normalize to the same tablename when used as keys by the datastructure stage. - } - tablearrayname { - set test_only [::tomlish::utils::tablename_trim $tok] - puts stdout "trimmed (but not normalized) tablearrayname: '$test_only'" - set v($nest) [list TABLEARRAY $tok] ;#$tok is the *raw* tablearray name - } - startarray { - set v($nest) [list ARRAY] ;#$tok is just the opening bracket - don't output. - } - startinlinetable { - set v($nest) [list ITABLE] ;#$tok is just the opening curly brace - don't output. - } - startmultiquote { - ::tomlish::log::debug "---- push trigger tokenType startmultiquote" - set v($nest) [list MULTISTRING] ;#container for STRINGPART, WS, CONT, NEWLINE - } - triple_squote { - ::tomlish::log::debug "---- push trigger tokenType triple_squote" - set v($nest) [list MULTILITERAL] ;#container for NEWLINE,LITERAL - } - default { - error "---- push trigger tokenType '$tokenType' not yet implemented" - } - } - - } else { - #no space level change - switch -exact -- $tokenType { - squotedkey { - puts "---- squotedkey in state $prevstate (no space level change)" - lappend v($nest) [list SQKEY $tok] - } - barekey { - lappend v($nest) [list KEY $tok] - } - dotsep { - lappend v($nest) [list DOTSEP] - } - starttablename { - #$tok is triggered by the opening bracket and sends nothing to output - } - starttablearrayname { - #$tok is triggered by the double opening brackets and sends nothing to output - } - tablename - tablenamearray { - error "---- did not expect 'tablename/tablearrayname' without space level change (no space level change)" - #set v($nest) [list TABLE $tok] - } - endtablename - endtablearrayname { - #no output into the tomlish list for this token - } - startinlinetable { - puts stderr "---- decode::toml error. did not expect startinlinetable without space level change (no space level change)" - } - startquote { - switch -exact -- $newstate { - string-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "string" - set tok "" - } - quoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "quotedkey" - set tok "" - } - itable-quoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "itablequotedkey" - set tok "" - } - default { - error "---- startquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - startsquote { - switch -exact -- $newstate { - literal-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "literal" - set tok "" - } - squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - itable-squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "itablesquotedkey" - set tok "" - } - multiliteral-space { - #false alarm squote returned from squote_seq pop - ::tomlish::log::debug "---- adding lone squote to own LITERALPART nextstate: $newstate (no space level change)" - #(single squote - not terminating space) - lappend v($nest) [list LITERALPART '] - #may need to be joined on pop if there are neighbouring LITERALPARTs - } - default { - error "---- startsquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - startmultiquote { - #review - puts stderr "---- got startmultiquote in state $prevstate (no space level change)" - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "stringpart" - set tok "" - } - endquote { - #nothing to do? - set tok "" - } - endsquote { - set tok "" - } - endmultiquote { - #JMN!! - set tok "" - } - string { - lappend v($nest) [list STRING $tok] ;#directly wrapped in dquotes - } - literal { - lappend v($nest) [list LITERAL $tok] ;#directly wrapped in squotes - } - double_squote { - switch -exact -- $prevstate { - keyval-value-expected { - lappend v($nest) [list LITERAL ""] - } - multiliteral-space { - #multiliteral-space to multiliteral-space - lappend v($nest) [list LITERALPART ''] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - multistring { - #review - lappend v($nest) [list MULTISTRING $tok] - } - stringpart { - lappend v($nest) [list STRINGPART $tok] ;#will not get wrapped in dquotes directly - } - multiliteral { - lappend v($nest) [LIST MULTILITERAL $tok] - } - literalpart { - lappend v($nest) [list LITERALPART $tok] ;#will not get wrapped in squotes directly - } - quotedkey { - #lappend v($nest) [list QKEY $tok] ;#TEST - } - itablequotedkey { - - } - untyped_value { - #we can't determine the type of unquoted values (int,float,datetime,bool) until the entire token was read. - if {$tok in {true false}} { - set tag BOOL - } elseif {[::tomlish::utils::is_int $tok]} { - set tag INT - } elseif {[::tomlish::utils::is_float $tok]} { - set tag FLOAT - } elseif {[::tomlish::utils::is_datetime $tok]} { - set tag DATETIME - } else { - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. [tomlish::parse::report_line] (no space level change)" - } - lappend v($nest) [list $tag $tok] - - } - comment { - #puts stdout "----- comment token returned '$tok'------" - lappend v($nest) [list COMMENT "$tok"] - } - equal { - #we append '=' to the nest so that any surrounding whitespace is retained. - lappend v($nest) = - } - comma { - lappend v($nest) SEP - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - whitespace { - lappend v($nest) [list WS $tok] - } - continuation { - lappend v($nest) CONT - } - bom { - lappend v($nest) BOM - } - eof { - #ok - nothing more to add to the tomlish list. - #!todo - check previous tokens are complete/valid? - } - default { - error "--- unknown tokenType '$tokenType' during state $prevstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - - if {!$next_tokenType_known} { - ::tomlish::log::notice "---- tomlish::decode::toml - current tokenType:$tokenType Next token type not known" - ::tomlish::parse::set_tokenType "" - set tok "" - } - - if {$state eq "end-state"} { - break - } - - - } - - #while {$nest > 0} { - # lappend v([expr {$nest -1}]) [set v($nest)] - # incr nest -1 - #} - while {[::tomlish::parse::spacestack size] > 1} { - ::tomlish::parse::spacestack pop - lappend v([expr {$nest -1}]) [set v($nest)] - incr nest -1 - - #set parent [spacestack peek] ;#the level being appended to - #lassign $parent type state - #if {$type eq "space"} { - # - #} elseif {$type eq "buffer"} { - # lappend v([expr {$nest -1}]) {*}[set v($nest)] - #} else { - # error "invalid spacestack item: $parent" - #} - } - - } finally { - set is_parsing 0 - } - return $v(0) - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::decode ---}] -} -#decode toml to tomlish -interp alias {} tomlish::from_toml {} tomlish::decode::toml - -namespace eval tomlish::utils { - #*** !doctools - #[subsection {Namespace tomlish::utils}] - #[para] - #[list_begin definitions] - - - #tablenames (& tablearraynames) may contain irrelevant leading, trailing and interspersed whitespace - # tablenames can be made up of segments delimited by dots. .eg [ a.b . c ] - #trimmed, the tablename becomes {a.b.c} - # A segment may contain whitespace if it is quoted e.g [a . b . "c etc " ] - #ie whitespace is only irrelevant if it's outside a quoted segment - #trimmed, the tablename becomes {a.b."c etc "} - proc tablename_trim {tablename} { - set segments [tablename_split $tablename false] - set trimmed_segments [list] - foreach seg $segments { - lappend trimmed_segments [::string trim $seg " \t"] - } - return [join $trimmed_segments .] - } - - #basic generic quote matching for single and double quotes - #note for example that {[o'malley]} will return sq - as the single quote is not closed or wrapped in double quotes - proc tok_in_quotedpart {tok} { - set sLen [tcl::string::length $tok] - set quote_type "" - set had_slash 0 - for {set i 0} {$i < $sLen} {incr i} { - set c [tcl::string::index $tok $i] - if {$quote_type eq ""} { - if {$had_slash} { - #don't enter quote mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - set quote_type dq - } - sq { - set quote_type sq - } - bsl { - set had_slash 1 - } - } - } - } else { - if {$had_slash} { - #don't leave quoted mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - if {$quote_type eq "dq"} { - set quote_type "" - } - } - sq { - if {$quote_type eq "sq"} { - set quote_type "" - } - } - bsl { - set had_slash 1 - } - } - } - } - } - return $quote_type ;#dq | sq - } - - #utils::tablename_split - proc tablename_split {tablename {normalize false}} { - #we can't just split on . because we have to handle quoted segments which may contain a dot. - #eg {dog."tater.man"} - set sLen [tcl::string::length $tablename] - set segments [list] - set mode "unknown" ;#5 modes: unknown, quoted,litquoted, unquoted, syntax - #quoted is for double-quotes, litquoted is for single-quotes (string literal) - set seg "" - for {set i 0} {$i < $sLen} {incr i} { - - if {$i > 0} { - set lastChar [tcl::string::index $tablename [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $tablename $i] - - if {$c eq "."} { - switch -exact -- $mode { - unquoted { - #dot marks end of segment. - lappend segments $seg - set seg "" - set mode "unknown" - } - quoted { - append seg $c - } - unknown { - lappend segments $seg - set seg "" - } - litquoted { - append seg $c - } - default { - #mode: syntax - #we got our dot. - the syntax mode is now satisfied. - set mode "unknown" - } - } - } elseif {($c eq "\"") && ($lastChar ne "\\")} { - if {$mode eq "unknown"} { - if {[tcl::string::trim $seg] ne ""} { - #we don't allow a quote in the middle of a bare key - error "tablename_split. character '\"' invalid at this point in tablename. tablename: '$tablename'" - } - set mode "quoted" - set seg "\"" - } elseif {$mode eq "unquoted"} { - append seg $c - } elseif {$mode eq "quoted"} { - append seg $c - lappend segments $seg - set seg "" - set mode "syntax" ;#make sure we only accept a dot or end-of-data now. - } elseif {$mode eq "litquoted"} { - append seg $c - } elseif {$mode eq "syntax"} { - error "tablename_split. expected whitespace or dot, got double quote. tablename: '$tablename'" - } - } elseif {($c eq "\'")} { - if {$mode eq "unknown"} { - append seg $c - set mode "litquoted" - } elseif {$mode eq "unquoted"} { - #single quote inside e.g o'neill - append seg $c - } elseif {$mode eq "quoted"} { - append seg $c - - } elseif {$mode eq "litquoted"} { - append seg $c - lappend segments $seg - set seg "" - set mode "syntax" - } elseif {$mode eq "syntax"} { - error "tablename_split. expected whitespace or dot, got single quote. tablename: '$tablename'" - } - - } elseif {$c in [list " " \t]} { - if {$mode eq "syntax"} { - #ignore - } else { - append seg $c - } - } else { - if {$mode eq "syntax"} { - error "tablename_split. Expected a dot separator. got '$c'. tablename: '$tablename'" - } - if {$mode eq "unknown"} { - set mode "unquoted" - } - append seg $c - } - if {$i == $sLen-1} { - #end of data - ::tomlish::log::debug "End of data: mode='$mode'" - switch -exact -- $mode { - quoted { - if {$c ne "\""} { - error "tablename_split. missing closing double-quote in a segment. tablename: '$tablename'" - } - if {$normalize} { - lappend segments $seg - } else { - lappend segments [::tomlish::utils::unescape_string [tcl::string::range $seg 1 end-1]] - #lappend segments [subst -nocommands -novariables [::string range $seg 1 end-1]] ;#wrong - } - } - litquoted { - set trimmed_seg [tcl::string::trim $seg] - if {[tcl::string::index $trimmed_seg end] ne "\'"} { - error "tablename_split. missing closing single-quote in a segment. tablename: '$tablename'" - } - lappend segments $seg - } - unquoted - unknown { - lappend segments $seg - } - syntax { - #ok - segment already lappended - } - default { - lappend segments $seg - } - } - } - } - foreach seg $segments { - set trimmed [tcl::string::trim $seg " \t"] - #note - we explicitly allow 'empty' quoted strings '' & "" - # (these are 'discouraged' but valid toml keys) - #if {$trimmed in [list "''" "\"\""]} { - # puts stderr "tablename_split. warning - Empty quoted string as tablename segment" - #} - if {$trimmed eq "" } { - error "tablename_split. Empty segment found. tablename: '$tablename' segments [llength $segments] ($segments)" - } - } - return $segments - } - - proc unicode_escape_info {slashu} { - #!todo - # validate that slashu is either a \uxxxx or \Uxxxxxxxx value of the correct length and - # is a valid 'unicode scalar value' - # ie integers in the range 0 to D7FF16 and E00016 to 10FFFF16 inclusive - #expr {(($x >= 0) && ($x <= 0xD7FF16)) || (($x >= 0xE00016) && ($x <= 0x10FFFF16))} - if {[tcl::string::match {\\u*} $slashu]} { - set exp {^\\u([0-9a-fA-F]{4}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %4x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } - } else { - return [list err [list reason "Supplied string not of the form \\uHHHH where H in \[0-9a-fA-F\]"]] - } - } elseif {[tcl::string::match {\\U*} $slashu]} { - set exp {^\\U([0-9a-fA-F]{8}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %8x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - if {(($dec >= 0) && ($dec <= 0xD7FF16)) || (($dec >= 0xE00016) && ($dec <= 0x10FFFF16))} { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } else { - return [list err [list reason "$slashu is not within the 'unicode scalar value' ranges 0 to 0xD7FF16 or 0xE00016 to 0x10FFFF16"]] - } - } - } else { - return [list err [list reason "Supplied string not of the form \\UHHHHHHHH where H in \[0-9a-fA-F\]"]] - } - } else { - return [list err [list reason "Supplied string did not start with \\u or \\U" ]] - } - - } - - proc unescape_string {str} { - #note we can't just use Tcl subst because: - # it also transforms \a (audible bell) and \v (vertical tab) which are not in the toml spec. - # it would strip out backslashes inappropriately: e.g "\j" becomes just j - # it recognizes other escapes which aren't approprite e.g \xhh and octal \nnn - # it replaces\ with a single whitespace - #This means we shouldn't use 'subst' on the whole string, but instead substitute only the toml-specified escapes (\r \n \b \t \f \\ \" \uhhhh & \Uhhhhhhhh - - set buffer "" - set buffer4 "" ;#buffer for 4 hex characters following a \u - set buffer8 "" ;#buffer for 8 hex characters following a \u - - set sLen [tcl::string::length $str] - - #we need to handle arbitrarily long sequences of backslashes. \\\\\ etc - set slash_active 0 - set unicode4_active 0 - set unicode8_active 0 - - - #!todo - check for invalid data in the form of a raw carriage return (decimal 13) without following linefeed? - set i 0 - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $str [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $str $i] - ::tomlish::log::debug "unescape_string. got char $c" - scan $c %c n - if {($n <= 31) && ($n != 9) && ($n != 10) && ($n != 13)} { - #we don't expect unescaped unicode characters from 0000 to 001F - - #*except* for raw tab (which is whitespace) and newlines - error "unescape_string. Invalid data for a toml string. Unescaped control character (decimal $n) [::tomlish::utils::string_to_slashu $c]" - } - incr i ;#must incr here because we do'returns'inside the loop - if {$c eq "\\"} { - if {$slash_active} { - append buffer "\\" - set slash_active 0 - } elseif {$unicode4_active} { - error "unescape_string. unexpected case slash during unicode4 not yet handled" - } elseif {$unicode8_active} { - error "unescape_string. unexpected case slash during unicode8 not yet handled" - } else { - # don't output anything (yet) - set slash_active 1 - } - } else { - if {$unicode4_active} { - if {[tcl::string::length $buffer4] < 4} { - append buffer4 $c - } - if {[tcl::string::length $buffer4] == 4} { - #we have a \uHHHH to test - set unicode4_active 0 - set result [tomlish::utils::unicode_escape_info "\\u$buffer4"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode8_active} { - if {[tcl::string::length $buffer8] < 8} { - append buffer8 $c - } - if {[tcl::string::length $buffer8] == 8} { - #we have a \UHHHHHHHH to test - set unicode8_active 0 - set result [tomlish::utils::unicode_escape_info "\\U$buffer8"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$slash_active} { - set slash_active 0 - set ctest [tcl::string::map {{"} dq} $c] - switch -exact -- $ctest { - dq { - set e "\\\"" - append buffer [subst -nocommand -novariable $e] - } - b - t - n - f - r { - set e "\\$c" - append buffer [subst -nocommand -novariable $e] - } - u { - set unicode4_active 1 - set buffer4 "" - } - U { - set unicode8_active 1 - set buffer8 "" - } - default { - set slash_active 0 - - append buffer "\\" - append buffer $c - } - } - } else { - append buffer $c - } - } - } - #puts stdout "EOF 4:$unicode4_active 8:$unicode8_active slash:$slash_active" - if {$unicode4_active} { - error "End of string reached before complete unicode escape sequence \uHHHH" - } - if {$unicode8_active} { - error "End of string reached before complete unicode escape sequence \UHHHHHHHH" - } - if {$slash_active} { - append buffer "\\" - } - return $buffer - } - - proc normalize_key {rawkey} { - set c1 [tcl::string::index $rawkey 0] - set c2 [tcl::string::index $rawkey end] - if {($c1 eq "'") && ($c2 eq "'")} { - #single quoted segment. No escapes allowed within it. - set key [tcl::string::range $rawkey 1 end-1] - } elseif {($c1 eq "\"") && ($c2 eq "\"")} { - #double quoted segment. Apply escapes. - # - set keydata [tcl::string::range $rawkey 1 end-1] ;#strip outer quotes only - set key [::tomlish::utils::unescape_string $keydata] - #set key [subst -nocommands -novariables $keydata] ;#wrong. Todo - create a string escape substitution function. - } else { - set key $rawkey - } - return $key - } - - proc string_to_slashu {string} { - set rv {} - foreach c [split $string {}] { - scan $c %c c - append rv {\u} - append rv [format %.4X $c] - } - return $rv - } - - #'nonprintable' is conservative here because some systems (e.g windows console) are very limited in what they can display. - proc nonprintable_to_slashu {s} { - set res "" - foreach i [split $s ""] { - scan $i %c c - - set printable 0 - if {($c>31) && ($c<127)} { - set printable 1 - } - if {$printable} {append res $i} else {append res \\u[format %.4X $c]} - } - set res - } ;#RS - - #check if str is valid for use as a toml bare key - #Early toml versions? only allowed letters + underscore + dash - proc is_barekey1 {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } else { - set matches [regexp -all {[a-zA-Z0-9\_\-]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters match the regexp - return 1 - } else { - return 0 - } - } - } - - #from toml.abnf in github.com/toml-lang/toml - #unquoted-key = 1*unquoted-key-char - #unquoted-key-char = ALPHA / DIGIT / %x2D / %x5F ; a-z A-Z 0-9 - _ - #unquoted-key-char =/ %xB2 / %xB3 / %xB9 / %xBC-BE ; superscript digits, fractions - #unquoted-key-char =/ %xC0-D6 / %xD8-F6 / %xF8-37D ; non-symbol chars in Latin block - #unquoted-key-char =/ %x37F-1FFF ; exclude GREEK QUESTION MARK, which is basically a semi-colon - #unquoted-key-char =/ %x200C-200D / %x203F-2040 ; from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - #unquoted-key-char =/ %x2070-218F / %x2460-24FF ; include super-/subscripts, letterlike/numberlike forms, enclosed alphanumerics - #unquoted-key-char =/ %x2C00-2FEF / %x3001-D7FF ; skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - #unquoted-key-char =/ %x2070-21FF / %x2300-24FF ; skip math operators - #unquoted-key-char =/ %x25A0-268B / %x2690-2757 ; skip box drawing, block elements, and some yin-yang symbols - #unquoted-key-char =/ %x2762-2767 / %x2776-27E5 ; skip some Dingbat punctuation - #unquoted-key-char =/ %x2801-297F ; skip some math brackets and arrows, and braille blank - #unquoted-key-char =/ %x2B00-2FFF / %x3001-D7FF ; skip various math operators and symbols, and ideographic space - #unquoted-key-char =/ %xF900-FDCF / %xFDF0-FFFD ; skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - #unquoted-key-char =/ %x10000-EFFFF ; all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - variable re_barekey - set ranges [list] - lappend ranges {a-zA-Z0-9\_\-} - lappend ranges {\u00B2} {\u00B3} {\u00B9} {\u00BC-\u00BE} ;# superscript digits, fractions - lappend ranges {\u00C0-\u00D6} {\u00D8-\u00F6} {\u00F8-\u037D} ;# non-symbol chars in Latin block - lappend ranges {\u037f-\u1FFF} ;# exclude GREEK QUESTION MARK, which is basically a semi-colon - lappend ranges {\u200C-\u200D} {\u203F-\u2040} ;# from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - lappend ranges {\u2070-\u218f} {\u2460-\u24FF} ;# include super-subscripts, letterlike/numberlike forms, enclosed alphanumerics - lappend ranges {\u2C00-\u2FEF} {\u3001-\uD7FF} ;# skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - lappend ranges {\u2070-\u21FF} {\u2300-\u24FF} ;# skip math operators - lappend ranges {\u25A0-\u268B} {\u2690-\u2757} ;# skip box drawing, block elements, and some yin-yang symbols - lappend ranges {\u2762-\u2767} {\u2776-\u27E5} ;# skip some Dingbat punctuation - lappend ranges {\u2801-\u297F} ;# skip some math brackets and arrows, and braille blank - lappend ranges {\u2B00-\u2FFF} {\u3001-\uD7FF} ;# skip various math operators and symbols, and ideographic space - lappend ranges {\uF900-\uFDCF} {\uFDF0-\uFFFD} ;# skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - lappend ranges {\U10000-\UEFFFF} ;# all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - set re_barekey {^[} - foreach r $ranges { - append re_barekey $r - } - append re_barekey {]+$} - - proc is_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } - variable re_barekey - return [regexp $re_barekey $str] - } - - #test only that the characters in str are valid for the toml specified type 'integer'. - proc int_validchars1 {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_\-\+]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - #add support for hex,octal,binary 0x.. 0o.. 0b... - proc int_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - proc is_int {str} { - set matches [regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - - # --------------------------------------- - #check for leading zeroes in non 0x 0b 0o - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - if {([tcl::string::length $check] > 1) && ([tcl::string::index $check 0] eq "0") && ([tcl::string::index $check 1] ni {o x b})} { - return 0 - } - # --------------------------------------- - - #check +,- only occur in the first position. - if {[tcl::string::last - $str] > 0} { - return 0 - } - if {[tcl::string::last + $str] > 0} { - return 0 - } - set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores - #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { - return 0 - } - #!todo - check bounds only based on some config value - #even though Tcl can handle bignums, we won't accept anything outside of toml 1.0 minimum requirements. - #presumably very large numbers would have to be supplied in a toml file as strings. - #Review - toml 1.0 only says that it must handle up to 2^63 - not that this is a max - if {$numeric_value > $::tomlish::max_int} { - return 0 - } - if {$numeric_value < $::tomlish::min_int} { - return 0 - } - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'float'. - proc float_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[eE0-9\_\-\+\.]} $str] == $numchars} { - return 1 - } else { - #only allow lower case for these special values - as per Toml 1.0 spec - if {$str ni {inf +inf -inf nan +nan -nan}} { - return 0 - } else { - return 1 - } - } - } - - proc is_float {str} { - set matches [regexp -all {[eE0-9\_\-\+\.]} $str] - #don't test for case variations - as Toml doesn't allow (whereas Tcl allows Inf NaN etc) - if {$str in {inf +inf -inf nan +nan -nan}} { - return 1 - } - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - #A leading zero is ok, but we should disallow multiple leading zeroes (same rules as toml ints) - #Toml spec also disallows leading zeros in the exponent part - #... but this seems less interoperable (some libraries generate leading zeroes in exponents) - #for now we will allow leading zeros in exponents - #!todo - configure 'strict' option to disallow? - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - set r {([0-9])*} - regexp $r $check intpart ;#intpart holds all numerals before the first .,e or E - set z {([0])*} - regexp $z $intpart leadingzeros - if {[tcl::string::length $leadingzeros] > 1} { - return 0 - } - #for floats, +,- may occur in multiple places - #e.g -2E-22 +3e34 - #!todo - check bounds ? - - #strip underscores for tcl double check - set check [tcl::string::map {_ ""} $str] - #string is double accepts inf nan +NaN etc. - if {![tcl::string::is double $check]} { - return 0 - } - - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'datetime'. - proc datetime_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[zZtT0-9\-\+\.:]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - #review - we - proc is_datetime {str} { - #e.g 1979-05-27 - #e.g 1979-05-27T00:32:00Z - #e.g 1979-05-27 00:32:00-07:00 - #e.g 1979-05-27 00:32:00+10:00 - #e.g 1979-05-27 00:32:00.999999-07:00 - - #review - #minimal datetimes? - # 2024 ok - shortest valid 4 digit year? - # 02:00 ok - # 05-17 ok - if {[string length $str] < 4} { - return 0 - } - - set matches [regexp -all {[zZtT0-9\-\+\.:]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - #!todo - use full RFC 3339 parser? - lassign [split $str T] datepart timepart - #!todo - what if the value is 'time only'? - - #Tcl's free-form clock scan (no -format option) is deprecated - # - #if {[catch {clock scan $datepart} err]} { - # puts stderr "tcl clock scan failed err:'$err'" - # return 0 - #} - - #!todo - verify time part is reasonable - } else { - return 0 - } - return 1 - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::utils ---}] -} - -namespace eval tomlish::parse { - #*** !doctools - #[subsection {Namespace tomlish::parse}] - #[para] - #[list_begin definitions] - - #This is a somewhat curly mix of a statemachine and toml-nesting-stack littered with special cases. - #The code is a pig's-nest - but it should be noted that for example trailing single double quotes in multiline strings are perhaps not so trivial to parse using more standard methods either: - # - e.g some kind of backtracking required if using an ABNF parser? - #I don't know the precise technical name for this sort of parser; probably something like "Dog's Breakfast" - #More seriously, we don't have distinct lex/parse steps - so it is basically a 'fused lexer' or 'scannerless parser' - - #It is also desirable for this system to be useful in 'interactive' use. review - would a separate lexer make this easier or harder? - - #A possible alternative more structured approach might be to use a PEG (Parsing Expression Grammar) - - - variable is_parsing 0 ;#whether we are in the middle of parsing tomlish text - - variable state - # states: - # table-space, itable-space, array-space - # value-expected, keyval-syntax, - # quoted-key, squoted-key - # string-state, literal-state, multistring... - # - # notes: - # only the -space states are also 'spaces' ie a container which is pushed/popped on the spacestack - - # - # value-expected - we also allow for leading whitespace in this state, but once a value is returned we jump to a state based on the containing space. e.g keyval-tail or array-syntax - # - #stateMatrix defines for each state, actions to take for each possible token. - #single-element actions are the name of the next state into which to transition, or a 'POPSPACE' instruction to pop a level off the spacestack and add the data to the parent container. - #dual-element actions are a push instruction and the name of the space to push on the stack. - # - PUSHSPACE is a simple push onto the spacestack, zeropoppushspace also pushes, but will first do a pop *if* the current space level is greater than zero (ie if only if not already in root table-space) - - # -- --- --- --- --- --- - #token/state naming guide - # -- --- --- --- --- --- - #tokens : underscore separated or bare name e.g newline, start_quote, start_squote - #private tokens: always have a leading underscore (These are private 'temporary state' tokens that are never returned as actual tokens e.g _start_squote_sequence - #states : always contain at least one dash e.g err-state, table-space - #instructions - # -- --- --- --- --- --- - - - #stateMatrix dict of elements mapping current state to next state based on returned tokens - # current-state {token-encountered next-state ... } - # where next-state can be a 1 or 2 element list. - #If 2 element - the first item is an instruction (ucase) - #If 1 element - it is either a lowercase dashed state name or an ucase instruction - #e.g {PUSHSPACE } or POPSPACE or SAMESPACE - - - #SAMESPACE - got to same space as parent without popping a level, but has it's own autotransition lookup - strange concept - review usecases - - variable stateMatrix - set stateMatrix [dict create] - - #xxx-space vs xxx-syntax inadequately documented - TODO - - # --------------------------------------------------------------------------------------------------------------# - # incomplete example of some state starting at table-space - # --------------------------------------------------------------------------------------------------------------# - # ( = -> value-expected) - # keyval-syntax (popped -> keyval-space -> keyval-tail) (autotransition on pop) - # keyval-space (autotransition on push ^) - # table-space (barekey^) (startquote -> quoted-key ^) - # --------------------------------------------------------------------------------------------------------------# - - dict set stateMatrix\ - table-space { - bom "table-space"\ - whitespace "table-space"\ - newline "table-space"\ - barekey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - squotedkey {PUSHSPACE "keyval-space" state "keyval-syntax" note ""}\ - startquote "quoted-key"\ - XXXstartsquote "squoted-key"\ - comment "table-space"\ - starttablename "tablename-state"\ - starttablearrayname "tablearrayname-state"\ - startmultiquote "err-state"\ - endquote "err-state"\ - comma "err-state"\ - eof "end-state"\ - equal "err-state"\ - } - - #itable-space/ curly-syntax : itables - dict set stateMatrix\ - itable-space {\ - whitespace "itable-space"\ - newline "itable-space"\ - squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate itable-space starttok {squote_seq "'"}}\ - barekey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - squotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - endinlinetable "POPSPACE"\ - startquote "quoted-key"\ - startsquote {TOSTATE "squoted-key" comment "jn-testing"}\ - comma "itable-space"\ - comment "err-state"\ - eof "err-state"\ - } - - - dict set stateMatrix\ - keyval-space {\ - whitespace "keyval-syntax"\ - equal "keyval-value-expected"\ - } - - # ' = ' portion of keyval - dict set stateMatrix\ - keyval-syntax {\ - whitespace "keyval-syntax"\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - barekey {PUSHSPACE "dottedkey-space"}\ - equal "keyval-value-expected"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - #### - dict set stateMatrix\ - keyval-value-expected {\ - whitespace "keyval-value-expected"\ - untyped_value {TOSTATE "keyval-tail" note ""}\ - squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate keyval-value-expected starttok {squote_seq "'"}}\ - startquote {TOSTATE "string-state" returnstate keyval-tail}\ - startmultiquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - startsquote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - double_squote {TOSTATE "keyval-tail" note "empty literal received when double squote occurs"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - } - #squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate keyval-process-leading-squotes starttok {squote_seq "'"}} - dict set stateMatrix\ - leading-squote-space {\ - squote_seq "POPSPACE"\ - } - #dict set stateMatrix\ - # keyval-process-leading-squotes {\ - # startsquote "literal-state"\ - # triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - # } - - dict set stateMatrix\ - keyval-tail {\ - whitespace "keyval-tail"\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - dict set stateMatrix\ - itable-keyval-syntax {\ - whitespace "itable-keyval-syntax"\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - barekey {PUSHSPACE "dottedkey-space"}\ - equal "itable-keyval-value-expected"\ - newline "err-state"\ - eof "err-state"\ - } - dict set stateMatrix\ - itable-keyval-value-expected {\ - whitespace "itable-keyval-value-expected"\ - untyped_value {TOSTATE "itable-val-tail" note ""}\ - squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate itable-keyval-value-expected starttok {squote_seq "'"}}\ - startquote {TOSTATE "string-state" returnstate itable-val-tail}\ - startmultiquote {PUSHSPACE "multistring-space" returnstate itable-val-tail}\ - startsquote {TOSTATE "literal-state" returnstate itable-val-tail note "usual way a literal is triggered"}\ - double_squote {TOSTATE "itable-val-tail" note "empty literal received when double squote occurs"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate itable-val-tail}\ - startinlinetable {PUSHSPACE "itable-space" returnstate itable-val-tail}\ - startarray {PUSHSPACE "array-space" returnstate itable-val-tail}\ - } - dict set stateMatrix\ - itable-keyval-space {\ - whitespace "itable-keyval-syntax"\ - equal {TOSTATE "itable-keyval-value-expected" note "required"}\ - } - - dict set stateMatrix\ - itable-val-tail {\ - whitespace "itable-val-tail"\ - endinlinetable "POPSPACE"\ - comma "POPSPACE"\ - Xnewline {TOSTATE "itable-val-tail" note "itable-space ??"}\ - newline "err-state"\ - comment "itable-val-tail"\ - eof "err-state"\ - } - #dict set stateMatrix\ - # itable-quoted-key {\ - # whitespace "NA"\ - # itablequotedkey {PUSHSPACE "itable-keyval-space"}\ - # newline "err-state"\ - # endquote "itable-keyval-syntax"\ - # } - #dict set stateMatrix\ - # itable-squoted-key {\ - # whitespace "NA"\ - # itablesquotedkey {PUSHSPACE "itable-keyval-space"}\ - # newline "err-state"\ - # endsquote "itable-keyval-syntax"\ - # } - - - - - - dict set stateMatrix\ - value-expected {\ - whitespace "value-expected"\ - untyped_value {"SAMESPACE" "" replay untyped_value}\ - startquote "string-state"\ - startsquote "literal-state"\ - startmultiquote {PUSHSPACE "multistring-space"}\ - triple_squote {PUSHSPACE "multiliteral-space"}\ - startinlinetable {PUSHSPACE itable-space}\ - startarray {PUSHSPACE array-space}\ - comment "err-state-value-expected-got-comment"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - - #dottedkey-space is not used within [tablename] or [[tablearrayname]] - #it is for keyval ie x.y.z = value - dict set stateMatrix\ - dottedkey-space {\ - whitespace "dottedkey-space"\ - dotsep "dottedkey-space"\ - barekey "dottedkey-space"\ - squotedkey "dottedkey-space"\ - quotedkey "dottedkey-space"\ - equal "POPSPACE"\ - newline "err-state"\ - comma "err-state"\ - comment "err-state"\ - } - #dottedkeyend "POPSPACE" - - - - - #REVIEW - #toml spec looks like heading towards allowing newlines within inline tables - #https://github.com/toml-lang/toml/issues/781 - dict set stateMatrix\ - curly-syntax {\ - whitespace "curly-syntax"\ - newline "curly-syntax"\ - barekey {PUSHSPACE "itable-keyval-space"}\ - itablequotedkey "itable-keyval-space"\ - endinlinetable "POPSPACE"\ - startquote "itable-quoted-key"\ - comma "itable-space"\ - comment "itable-space"\ - eof "err-state"\ - } - #review comment "err-state" vs comment "itable-space" - see if TOML 1.1 comes out and allows comments in multiline ITABLES - #We currently allow multiline ITABLES (also with comments) in the tokenizer. - #if we want to disallow as per TOML 1.0 - we should do so when attempting to get structure? - - - #JMN REVIEW - dict set stateMatrix\ - array-space {\ - whitespace "array-space"\ - newline "array-space"\ - untyped_value "SAMESPACE"\ - startarray {PUSHSPACE "array-space"}\ - endarray "POPSPACE"\ - startmultiquote {PUSHSPACE multistring-space}\ - startinlinetable {PUSHSPACE itable-space}\ - startquote "string-state"\ - startsquote "literal-state"\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax note "seems ok 2024"}\ - comma "array-space"\ - comment "array-space"\ - eof "err-state-array-space-got-eof"\ - } - dict set stateMatrix\ - array-syntax {\ - whitespace "array-syntax"\ - newline "array-syntax"\ - untyped_value "SAMESPACE"\ - startarray {PUSHSPACE array-space}\ - endarray "POPSPACE"\ - startmultiquote {PUSHSPACE multistring-space}\ - startquote "string-state"\ - startsquote "literal-state"\ - comma "array-space"\ - comment "err-state"\ - } - - - - #quoted-key & squoted-key need to PUSHSPACE from own token to keyval-space - dict set stateMatrix\ - quoted-key {\ - whitespace "NA"\ - quotedkey {PUSHSPACE "keyval-space"}\ - newline "err-state"\ - endquote "keyval-syntax"\ - } - dict set stateMatrix\ - squoted-key {\ - whitespace "NA"\ - squotedkey "squoted-key"\ - newline "err-state"\ - } - # endsquote {PUSHSPACE "keyval-space"} - - dict set stateMatrix\ - string-state {\ - whitespace "NA"\ - string "string-state"\ - endquote "SAMESPACE"\ - newline "err-state"\ - eof "err-state"\ - } - dict set stateMatrix\ - literal-state {\ - whitespace "NA"\ - literal "literal-state"\ - endsquote "SAMESPACE"\ - newline "err-state"\ - eof "err-state"\ - } - - - #dict set stateMatrix\ - # stringpart {\ - # continuation "SAMESPACE"\ - # endmultiquote "POPSPACE"\ - # eof "err-state"\ - # } - dict set stateMatrix\ - multistring-space {\ - whitespace "multistring-space"\ - continuation "multistring-space"\ - stringpart "multistring-space"\ - newline "multistring-space"\ - endmultiquote "POPSPACE"\ - eof "err-state"\ - } - - - #only valid subparts are literalpart and newline. other whitespace etc is within literalpart - #todo - treat sole cr as part of literalpart but crlf and lf as newline - dict set stateMatrix\ - multiliteral-space {\ - literalpart "multiliteral-space"\ - newline "multiliteral-space"\ - squote_seq_begin {PUSHSPACE "trailing-squote-space" returnstate multiliteral-space starttok {squote_seq "'"}}\ - triple_squote {POPSPACE note "on popping - we do any necessary concatenation of LITERALPART items due to squote processing"}\ - double_squote {TOSTATE multiliteral-space note "short squote_seq: can occur anywhere in the space e.g emitted at end when 5 squotes occur"}\ - startsquote {TOSTATE multiliteral-space note "short squote_seq: same as double_squote - false alarm"}\ - eof "err-premature-eof-in-multiliteral-space"\ - } - - #trailing because we are looking for possible terminating ''' - but must accept '''' or ''''' and re-integrate the 1st one or 2 extra squotes - dict set stateMatrix\ - trailing-squote-space {\ - squote_seq "POPSPACE"\ - } - - - dict set stateMatrix\ - tablename-state {\ - whitespace "NA"\ - tablename {zeropoppushspace table-space}\ - tablename2 {PUSHSPACE table-space}\ - endtablename "tablename-tail"\ - comma "err-state"\ - newline "err-state"\ - } - dict set stateMatrix\ - tablearrayname-state {\ - whitespace "NA"\ - tablearrayname {zeropoppushspace table-space}\ - tablearrayname2 {PUSHSPACE table-space}\ - endtablearray "tablearrayname-tail"\ - comma "err-state"\ - newline "err-state"\ - } - - dict set stateMatrix\ - tablename-tail {\ - whitespace "tablename-tail"\ - newline "table-space"\ - comment "tablename-tail"\ - eof "end-state"\ - } - dict set stateMatrix\ - tablearrayname-tail {\ - whitespace "tablearrayname-tail"\ - newline "table-space"\ - comment "tablearrayname-tail"\ - eof "end-state"\ - } - dict set stateMatrix\ - end-state {} - - set knowntokens [list] - set knownstates [list] - dict for {state transitions} $stateMatrix { - if {$state ni $knownstates} {lappend knownstates $state} - dict for {tok instructions} $transitions { - if {$tok ni $knowntokens} {lappend knowntokens $tok} - } - } - dict set stateMatrix nostate {} - foreach tok $knowntokens { - dict set stateMatrix nostate $tok "err-nostate-received-token-$tok" - } - - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #purpose - debugging? remove? - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #build a list of 'push triggers' from the stateMatrix - # ie tokens which can push a new space onto spacestack - set push_trigger_tokens [list] - tcl::dict::for {s transitions} $stateMatrix { - tcl::dict::for {token transition_to} $transitions { - set instruction [lindex $transition_to 0] - switch -exact -- $instruction { - PUSHSPACE - zeropoppushspace { - if {$token ni $push_trigger_tokens} { - lappend push_trigger_tokens $token - } - } - } - } - } - ::tomlish::log::debug "push_trigger_tokens: $push_trigger_tokens" - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - - - - #This seems hacky... (deprecate in favour of explicit arguments to the instructions in stateMatrix?) - #spacePopTransitions, spacePushTransitions, spaceSameTransitions below for auto state redirections on POPSPACE,PUSHSPACE,SAMESPACE - - #mainly for the -space states: - #redirect to another state $c based on a state transition from $whatever to $b - # e.g "string {array-space array-syntax}" means when transitioning from string to array-space, jump to array-syntax instead. - #this is useful as we often don't know state $b. e.g when it is decided by 'POPSPACE' - - #Push to, next - #default first states when we push to these spaces - variable spacePushTransitions { - keyval-space keyval-syntax - itable-keyval-space itable-keyval-syntax - array-space array-space - table-space tablename-state - } - #itable-space itable-space - #Pop to, next - variable spacePopTransitions { - array-space array-syntax - } - #itable-space curly-syntax - #itable-keyval-space itable-val-tail - #review - #we pop to keyval-space from dottedkey-space or from value-expected? we don't always want to go to keyval-tail - #leave it out and make the POPSPACE caller explicitly specify it - #keyval-space keyval-tail - - variable spaceSameTransitions { - array-space array-syntax - } - #itable-space curly-syntax - #itable-keyval-space itable-val-tail - - - variable state_list ;#reset every tomlish::decode::toml - - namespace export tomlish toml - namespace ensemble create - - #goNextState has various side-effects e.g pushes and pops spacestack - #REVIEW - setting nest and v elements here is ugly - #todo - make neater, more single-purpose? - proc goNextState {tokentype tok currentstate} { - variable state - variable nest - variable v - - set prevstate $currentstate - - - variable spacePopTransitions - variable spacePushTransitions - variable spaceSameTransitions - - variable last_space_action "none" - variable last_space_type "none" - variable state_list - - set result "" - set starttok "" - - if {[dict exists $::tomlish::parse::stateMatrix $currentstate $tokentype]} { - set transition_to [dict get $::tomlish::parse::stateMatrix $currentstate $tokentype] - ::tomlish::log::debug "--->> goNextState tokentype:$tokentype tok:$tok currentstate:$currentstate : transition_to = $transition_to" - switch -exact -- [lindex $transition_to 0] { - POPSPACE { - spacestack pop - set parent_info [spacestack peek] - set type [dict get $parent_info type] - set parentspace [dict get $parent_info state] - - set last_space_action "pop" - set last_space_type $type - - if {[dict exists $parent_info returnstate]} { - set next [dict get $parent_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> POPSPACE transition to parent space $parentspace redirected to stored returnstate $next <<---" - } else { - ### - #review - do away with spacePopTransitions - which although useful to provide a default.. - # - involve error-prone configurations distant to the main state transition configuration in stateMatrix - if {[dict exists $::tomlish::parse::spacePopTransitions $parentspace]} { - set next [dict get $::tomlish::parse::spacePopTransitions $parentspace] - ::tomlish::log::info "--->> POPSPACE transition to parent space $parentspace redirected state to $next (spacePopTransitions)<<---" - } else { - set next $parentspace - ::tomlish::log::info "--->> POPSPACE transition to parent space $parentspace<<---" - } - } - set result $next - } - SAMESPACE { - set currentspace_info [spacestack peek] - ::tomlish::log::debug "--->> SAMESPACE got current space entry: $currentspace_info <<<<<" - set type [dict get $currentspace_info type] - set currentspace [dict get $currentspace_info state] - - if {[dict exists $currentspace_info returnstate]} { - set next [dict get $currentspace_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected to stored returnstate $next" - } else { - if {[dict exists $::tomlish::parse::spaceSameTransitions $currentspace]} { - set next [dict get $::tomlish::parse::spaceSameTransitions $currentspace] - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected state to $next (spaceSameTransitions)" - } else { - set next $currentspace - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace" - } - } - set result $next - } - zeropoppushspace { - if {$nest > 0} { - #pop back down to the root level (table-space) - spacestack pop - set parentinfo [spacestack peek] - set type [dict get $parentinfo type] - set target [dict get $parentinfo state] - - set last_space_action "pop" - set last_space_type $type - - #----- - #standard pop - set parentlevel [expr {$nest -1}] - lappend v($parentlevel) [set v($nest)] - incr nest -1 - #----- - } - #re-entrancy - - #set next [list PUSHSPACE [lindex $transition_to 1]] - set nexttokentype ${tokentype}2 ;#fake token type e.g tablename2 or tablearrayname2 - #::tomlish::log::notice "goNextState REENTRANCY. calling goNextState $nexttokentype $tokentype" - #set result [::tomlish::parse::goNextState $nexttokentype $tokentype] - ::tomlish::log::debug "--->> zeropoppushspace goNextState REENTRANCY. calling goNextState $nexttokentype $currentstate" - set transition_info [::tomlish::parse::goNextState $nexttokentype $tok $currentstate] - set result [dict get $transition_info newstate] - } - PUSHSPACE { - set original_target [dict get $transition_to PUSHSPACE] - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - if {[dict exists $transition_to starttok]} { - set starttok [dict get $transition_to starttok] - } - spacestack push [dict create type space state $original_target] - - set last_space_action "push" - set last_space_type "space" - - if {[dict exists $transition_to state]} { - #an explicit state in the pushed space was requested in the stateMatrix - override the spacePushTransition (spacePushTransitions can be deprecated if we require explicitness?) - set next [dict get $transition_to state] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next by explicit 'state' entry" - } else { - #puts $::tomlish::parse::spacePushTransitions - if {[dict exists $::tomlish::parse::spacePushTransitions $original_target]} { - set next [dict get $::tomlish::parse::spacePushTransitions $original_target] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next (spacePushTransitions) " - } else { - set next $original_target - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target" - } - } - set result $next - } - TOSTATE { - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - set result [dict get $transition_to TOSTATE] - } - default { - #simplified version of TOSTATE - set result [lindex $transition_to 0] ;#ignore everything but first word - } - } - } else { - ::tomlish::log::error "--->> No state transition defined from state $currentstate when tokentype $tokentype received" - set result "nostate" - } - lappend state_list [list tokentype $tokentype from $currentstate to $result] - set state $result - ::tomlish::log::notice "--->> STATE TRANSITION tokenType: '$tokentype' tok:$tok triggering '$currentstate' -> '$result' last_space_action:$last_space_action " - return [dict create prevstate $prevstate newstate $result space_action $last_space_action starttok $starttok] - } - - proc report_line {{line ""}} { - variable linenum - variable is_parsing - if {$is_parsing} { - if {$line eq ""} { - set line $linenum - } - return "Line Number: $line" - } else { - #not in the middle of parsing tomlish text - return nothing. - return "" - } - } - - #produce a *slightly* more readable string rep of the nest for puts etc. - proc nest_pretty1 {list} { - set prettier "{" - - foreach el $list { - if { [lindex $el 0] eq "NEWLINE"} { - append prettier "[list $el]\n" - } elseif {([llength $el] > 1) && ([lindex $el 0] in {KEY QKEY SQKEY TABLE ARRAY})} { - append prettier [nest_pretty1 $el] - } else { - append prettier "[list $el] " - } - } - append prettier "}" - return $prettier - } - - proc set_tokenType {t} { - variable tokenType - variable tokenType_list - if {![info exists tokenType]} { - set tokenType "" - } - lappend tokenType_list $t - set tokenType $t - } - - proc switch_tokenType {t} { - variable tokenType - variable tokenType_list - lset tokenType_list end $t - set tokenType $t - } - - proc get_tokenType {} { - variable tokenType - return $tokenType - } - - proc _shortcircuit_startquotesequence {} { - variable tok - variable i - set toklen [tcl::string::length $tok] - if {$toklen == 1} { - set_tokenType "startquote" - incr i -1 - return -level 2 1 - } elseif {$toklen == 2} { - puts stderr "_shortcircuit_startquotesequence toklen 2" - set_tokenType "startquote" - set tok "\"" - incr i -2 - return -level 2 1 - } - } - - proc get_token_waiting {} { - variable token_waiting - return [lindex $token_waiting 0] - } - proc clear_token_waiting {} { - variable token_waiting - set token_waiting [list] - } - - #token_waiting is a list - but our standard case is to have only one - #in certain circumstances such as near eof we may have 2 - #the set_token_waiting function only allows setting when there is not already one waiting. - #we want to catch cases of inadvertently trying to set multiple - # - the reason being that the state transition triggered by the previous token may have invalidated the assumptions made when a token was added as waiting. - proc set_token_waiting {args} { - if {[llength $args] %2 != 0} { - error "tomlish set_token_waiting must have args of form: type value complete 0|1" - } - variable token_waiting - - if {[llength $token_waiting] && [dict get [lindex $token_waiting end] type] ne "eof"} { - #tokloop already set a token_waiting - but something (post tokloop processing?) is trying to set another - #we may need to remove the existing token_waiting and reset the tokloop index to the previous char so it's reprocessed in the possibly new context - #rather than attempt to make the right decision here - we raise an error and require the caller to check/handle it - set err "tomlish set_token_waiting already has token_waiting: [lindex $token_waiting 0]" - append err \n " - cannot add token_waiting: $args" - error $err - #set tomlish::parse::i [expr {[dict get $token_waiting startindex] -1}] - #set token_waiting [list] - } - - set waiting [dict create] - dict for {k v} $args { - switch -exact $k { - type - complete { - dict set waiting $k $v - } - value { - dict set waiting tok $v - } - startindex { - dict set waiting startindex $v - } - default { - error "tomlish set_token_waiting error - unrecognised key $k. known keys: [dict keys $args]" - } - } - } - if {![tcl::string::is boolean -strict [dict get $waiting complete]]} { - error "tomlish set_token_waiting error - 'complete' must be a boolean. got [dict get $waiting complete]" - } - if {![llength $token_waiting]} { - set token_waiting [list $waiting] - } else { - #an extra sanity-check that we don't have more than just the eof.. - if {[llength $token_waiting] > 1} { - set err "tomlish Unexpected. Existing token_waiting count > 1.\n" - foreach tw $token_waiting { - append err " $tw" \n - } - append err " - cannot add token_waiting: $waiting" - error $err - } - #last entry must be a waiting eof - set token_waiting [list $waiting [lindex $token_waiting end]] - } - return - } - - #returns 0 or 1 - #tomlish::parse::tok - #we attempt to do this without lookahead (potential use in streaming toml? for what benefit?) todo -final flag - # - the possible benefit is being able to more easily process in arbitrarily split chunks (although we would still have to watch crlf splitting ?) - # - interactive use? - - proc tok {s} { - variable nest - variable v - variable i - variable tok - variable type ;#character type - variable state ;#FSM - - - variable tokenType - variable tokenType_list - - - variable endToken - - variable lastChar - - variable braceCount - variable bracketCount - - - #------------------------------ - #Previous run found another (presumably single-char) token - #The normal case is for there to be only one dict in the list - #multiple is an exception - primarily for eof - variable token_waiting - if {[llength $token_waiting]} { - set waiting [lindex $token_waiting 0] - - set tokenType [dict get $waiting type] - set tok [dict get $waiting tok] - #todo: dict get $token_waiting complete - set token_waiting [lrange $token_waiting 1 end] - return 1 - } - #------------------------------ - - set resultlist [list] - set sLen [tcl::string::length $s] - - set slash_active 0 - set quote 0 - set c "" - set multi_dquote "" - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $s [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $s $i] - set cindex $i - tomlish::log::debug "- tokloop char <$c> index $i tokenType:$tokenType tok:<$tok>" - #puts "got char $c during tokenType '$tokenType'" - incr i ;#must incr here because we do returns inside the loop - - set ctest [tcl::string::map {\{ lc \} rc \[ lb \] rb \" dq ' sq \\ bsl \r cr \n lf \t tab \uFEFF bom} $c] - switch -exact -- $ctest { - # { - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {$had_slash} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - barekey { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed (see tomlish::utils::is_barekey). [tomlish::parse::report_line]" - } - whitespace { - # hash marks end of whitespace token - #do a return for the whitespace, set token_waiting - #set_token_waiting type comment value "" complete 1 - incr i -1 ;#leave comment for next run - return 1 - } - untyped_value { - #REVIEW! the spec isn't clear.. is whitespace after an int,bool etc required before comment? - #we will accept a comment marker as an immediate terminator of the untyped_value. - incr i -1 - return 1 - } - starttablename - starttablearrayname { - #fix! - error "tomlish Character '#' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare parts - but allowed in quoted parts - let tablename parser sort it out - append tok $c - } - default { - #quotedkey, itablequotedkey, string,literal, multistring - append tok $c - } - } - } else { - switch -- $state { - multistring-space { - set_tokenType stringpart - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes#" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "#" - } - default { - #start of token if we're not in a token - set_tokenType comment - set tok "" ;#The hash is not part of the comment data - } - } - } - } - lc { - #left curly brace - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey - itablesquotedkey { - append tok $c - } - string - quotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - starttablename - starttablearrayname { - #*bare* tablename can only contain letters,digits underscores - error "tomlish Invalid tablename first character \{ [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #valid in quoted parts - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - switch -exact -- $state { - itable-keyval-value-expected - keyval-value-expected - value-expected { - #switch last key to tablename?? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - array-space - array-syntax { - #nested anonymous inline table - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\{" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\{" - } - default { - error "tomlish state: '$state'. left brace case not implemented [tomlish::parse::report_line]" - } - } - } - - } - rc { - #right curly brace - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey - itablesquotedkey { - append tok $c - } - string - quotedkey - itablequotedkey - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - starttablename - tablename { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endinlinetable value "" complete 1 startindex $cindex - return 1 - } - starttablearrayname - tablearrayname { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - itable-val-tail { - #review - error "tomlish right-curly in itable-val-tail" - } - default { - #end any other token - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - value-expected { - #invalid - but allow parser statemachine to report it. - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-space { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname-state problem" - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - #invalid - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - curly-syntax { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-val-tail { - set_tokenType "endinlinetable" - set tok "" - #we need to pop the keyval - and then reprocess to pop the inlinetable - so we incr -1 - incr i -1 - return 1 - } - itable-keyval-syntax { - error "tomlish endinlinetable unexpected at this point. Expecting key=val syntax [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\}" - } - multiliteral-space { - set_tokenType "literalpart" ; #review - set tok "\}" - } - default { - #JMN2024b keyval-tail? - error "tomlish state '$state'. endinlinetable case not implemented [tomlish::parse::report_line]" - } - } - } - - } - lb { - #left square bracket - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey - itablesquotedkey { - append tok $c - } - string - quotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - starttablename { - #change the tokenType - switch_tokenType "starttablearrayname" - set tok "" ;#no output into the tomlish list for this token - #any following whitespace is part of the tablearrayname, so return now - return 1 - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\[" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - #invalid at this point - state machine should disallow table -> starttablearrayname - set_token_waiting type starttablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "\[" - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - keyval-value-expected - itable-keyval-value-expected - value-expected { - set_tokenType "startarray" - set tok "\[" - return 1 - } - table-space { - #table name - #assume it's a single bracket - but we need to wait for non-bracket to confirm it's not a tablearray - #note that a starttablearrayname token may contain whitespace between the brackets - # e.g \[ \[ - set_tokenType "starttablename" - set tok "" ;#there is no output into the tomlish list for this token - } - array-space - array-syntax { - #nested array? - set_tokenType "startarray" - set tok "\[" - return 1 - #error "state: array-space. startarray case not implemented [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\[" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\[" - } - itable-space { - #handle state just to give specific error msg - error "tomlish state: '$state'. Left square bracket invalid. Cannot start array in inline table without key. Use key=\[\] syntax. [tomlish::parse::report_line]" - } - default { - error "tomlish state: '$state'. startarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - rb { - #right square bracket - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey - itablesquotedkey { - append tok $c - } - string - quotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - #???? - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } else { - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablename value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - tablearraynames { - #todo? - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - default { - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - value-expected { - #invalid - but allow parser statemachine to report it. - set_tokenType "endarray" - set tok "\]" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endarray" - set tok "\]" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endtablename" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname problem" - set_tokenType "endtablearray" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - set_tokenType "endarray" - set tok "\]" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\]" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\]" - } - default { - error "tomlish state '$state'. endarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - bsl { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - #backslash - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - #end whitespace token - incr i -1 ;#reprocess bsl in next run - return 1 - } else { - error "tomlish Unexpected backslash during whitespace. [tomlish::parse::report_line]" - } - } - literal - literalpart - squotedkey - itablesquotedkey { - #never need to set slash_active true when in single quoted tokens - append tok "\\" - set slash_active 0 - } - string - quotedkey - itablequotedkey - comment { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - stringpart { - if {$slash_active} { - #assert - quotes empty - or we wouldn't have slash_active - set slash_active 0 - append tok "\\\\" - } else { - append tok $dquotes - set slash_active 1 - } - } - starttablename - starttablearrayname { - error "tomlish backslash is invalid as first character of $tokenType [tomlish::parse::report_line]" - } - tablename - tablearrayname { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - barekey { - error "tomlish Unexpected backslash during barekey. [tomlish::parse::report_line]" - } - default { - error "tomlish Backslash unexpected during tokentype: '$tokenType'. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$slash_active} { - set_tokenType "stringpart" - set tok "\\\\" - set slash_active 0 - } else { - if {$dquotes ne ""} { - set_tokenType "stringpart" - set tok $dquotes - } - set slash_active 1 - } - } - multiliteral-space { - #nothing can be escaped in multiliteral-space - not even squotes (?) review - set_tokenType "literalpart" - set tok "\\" - } - default { - error "tomlish tok error: Unexpected backslash when no token is active. [tomlish::parse::report_line]" - } - } - } - } - sq { - #single quote - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - #short squote_seq tokens are returned if active during any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - switch -- $state { - leading-squote-space { - append tok $c - if {$existingtoklen > 2} { - error "tomlish tok error: squote_seq unexpected length $existingtoklen when another received" - } elseif {$existingtoklen == 2} { - return 1 ;#return tok ''' - } - } - trailing-squote-space { - append tok $c - if {$existingtoklen == 4} { - #maxlen to be an squote_seq is multisquote + 2 = 5 - #return tok ''''' - return 1 - } - } - default { - error "tomlish tok error: squote_seq in unexpected state '$state' - expected leading-squote-space or trailing-squote-space" - } - } - } - whitespace { - #end whitespace - incr i -1 ;#reprocess sq - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - #temp token creatable only during value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - append tok $c - } - 2 { - #switch? - append tok $c - set_tokenType triple_squote - return 1 - } - default { - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_squote_sequence'" - } - } - } - literal { - #slash_active always false - #terminate the literal - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - literalpart { - #ended by ''' - but final could be '''' or ''''' (up to 2 squotes allowed directly before ending triple squote sequence) - #todo - # idea: end this literalpart (possibly 'temporarily') - # let the sq be reprocessed in the multiliteral-space to push an end-multiliteral-sequence to state stack - # upon popping end-multiliteral-sequence - stitch quotes back into this literalpart's token (if either too short - or a long ending sequence as shown above) - incr i -1 ;#throw the "'" back to loop - will be added to an squote_seq token for later processing - return 1 - } - itablesquotedkey { - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - squotedkey { - ### - #set_token_waiting type endsquote value "'" complete 1 - return 1 - } - starttablename - starttablearrayname { - #!!! - incr i -1 - return 1 - } - tablename - tablearrayname { - append tok $c - } - default { - append tok $c - } - } - } else { - switch -exact -- $state { - value-expected - array-space { - set_tokenType "_start_squote_sequence" - set tok "'" - } - itable-keyval-value-expected - keyval-value-expected { - set_tokenType "squote_seq_begin" - set tok "'" - return 1 - } - table-space { - ### - set_tokenType "squotedkey" - set tok "" - } - itable-space { - set_tokenType "squote_seq_begin" - set tok "'" - return 1 - } - tablename-state { - #first char in tablename-state/tablearrayname-state - set_tokenType tablename - append tok "'" - } - tablearrayname-state { - set_tokenType tablearrayname - append tok "'" - } - literal-state { - tomlish::log::debug "- tokloop sq during literal-state with no tokentype - empty literal?" - set_tokenType literal - incr -1 - return 1 - } - multistring-space { - error "tomlish unimplemented - squote during state '$state'. [tomlish::parse::report_line]" - } - multiliteral-space { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up an squote_seq to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "squote_seq_begin" - set tok "'" - return 1 - } - dottedkey-space { - set_tokenType squotedkey - } - default { - error "tomlish unhandled squote during state '$state'. [tomlish::parse::report_line]" - } - } - } - - } - dq { - #double quote - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - set toklen [tcl::string::length $tok] - if {$toklen == 1} { - append tok $c - } elseif {$toklen == 2} { - append tok $c - #switch vs set? - set_tokenType "startmultiquote" - return 1 - } else { - error "tomlish unexpected token length $toklen in 'startquotesequence'" - } - } - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - set_tokenType "startsquote" - incr i -1 - return 1 - } - 2 { - set_tokenType "startsquote" - incr i -2 - return 1 - } - default { - error "tomlish unexpected _start_squote_sequence length $toklen" - } - } - } - literal - literalpart { - append tok $c - } - string { - if {$had_slash} { - append tok "\\" $c - } else { - #unescaped quote always terminates a string? - set_token_waiting type endquote value "\"" complete 1 startindex $cindex - return 1 - } - } - stringpart { - #sub element of multistring - if {$had_slash} { - append tok "\\" $c - } else { - #incr i -1 - - if {$multi_dquote eq "\"\""} { - set_token_waiting type endmultiquote value "\"\"\"" complete 1 startindex [expr {$cindex -2}] - set multi_dquote "" - return 1 - } else { - append multi_dquote "\"" - } - } - } - whitespace { - switch -exact -- $state { - multistring-space { - #REVIEW - if {$had_slash} { - incr i -2 - return 1 - } else { - switch -- [tcl::string::length $multi_dquote] { - 2 { - set_token_waiting type endmultiquote value "\"\"\"" complete 1 startindex [expr {$cindex-2}] - set multi_dquote "" - return 1 - } - 1 { - incr i -2 - return 1 - } - 0 { - incr i -1 - return 1 - } - } - } - } - keyval-value-expected - value-expected { - #end whitespace token and reprocess - incr i -1 - return 1 - - #if {$multi_dquote eq "\"\""} { - # set_token_waiting type startmultiquote value "\"\"\"" complete 1 - # set multi_dquote "" - # return 1 - #} else { - # #end whitespace token and reprocess - # incr i -1 - # return 1 - #} - } - default { - set_token_waiting type startquote value "\"" complete 1 startindex $cindex - return 1 - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - quotedkey - itablequotedkey { - if {$had_slash} { - append tok "\\" - append tok $c - } else { - set_token_waiting type endquote value "\"" complete 1 startindex $cindex - return 1 - } - } - squotedkey - itablesquotedkey { - append tok $c - } - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - incr i -1 ;## - return 1 - } - default { - error "tomlish got quote during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - #$slash_active not relevant when no tokenType - #token is string only if we're expecting a value at this point - switch -exact -- $state { - keyval-value-expected - value-expected - array-space { - #!? start looking for possible multistartquote - #set_tokenType startquote - #set tok $c - #return 1 - set_tokenType "startquotesequence" ;#one or more quotes in a row - either startquote or multistartquote - set tok $c - } - itable-keyval-value-expected { - #JMN 2025 - review - set_tokenType "startquotesequence" ;#one or more quotes in a row - either startquote or multistartquote - set tok $c - } - multistring-space { - #TODO - had_slash!!! - #REVIEW - if {$had_slash} { - set_tokenType "stringpart" - set tok "\\\"" - set multi_dquote "" - } else { - if {$multi_dquote eq "\"\""} { - tomlish::log::debug "- tokloop char dq ---> endmultiquote" - set_tokenType "endmultiquote" - set tok "\"\"\"" - return 1 - #set_token_waiting type endmultiquote value "\"\"\"" complete 1 - #set multi_dquote "" - #return 1 - } else { - append multi_dquote "\"" - } - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\"" - } - table-space { - set_tokenType "startquote" - set tok $c - return 1 - } - itable-space { - set_tokenType "startquote" - set tok $c - return 1 - } - tablename-state { - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - dottedkey-space { - set_tokenType dquote_seq_begin - set tok $c - } - default { - error "tomlish Unexpected quote during state '$state' [tomlish::parse::report_line]" - } - } - } - } - = { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey { - #assertion had_slash 0, multi_dquote "" - append tok $c - } - string - comment - quotedkey - itablequotedkey { - #for these tokenTypes an = is just data. - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - whitespace { - if {$state eq "multistring-space"} { - set backlen [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backlen - return 1 - } else { - set_token_waiting type equal value = complete 1 startindex $cindex - return 1 - } - } - barekey { - #set_token_waiting type equal value = complete 1 - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character '=' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare name - but valid in quoted parts - leave for tablename parser to sort out - append tok $c - } - default { - error "tomlish unexpected = character during tokentype $tokenType. case not implemented. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok ${dquotes}= - } - multiliteral-space { - set_tokenType "literalpart" - set tok "=" - } - dottedkey-space { - set_tokenType "equal" - set tok "=" - return 1 - } - default { - set_tokenType "equal" - set tok = - return 1 - } - } - } - } - cr { - #REVIEW! - set dquotes $multi_dquote - set multi_dquote "" ;#!! - # \r carriage return - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal { - append tok $c - } - literalpart { - #part of MLL string (multi-line literal string) - #we need to split out crlf as a separate NEWLINE to be consistent - ::tomlish::log::warn "literalpart ended by cr - needs testing" - #return literalpart temporarily - allow cr to be reprocessed from multiliteral-space - incr i -1 - return 1 - } - stringpart { - #part of MLB string (multi-line basic string) - #jmn2025 - review - #append tok $dquotes$c - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #could in theory be valid in quoted part of name - #review - might be better just to disallow here - append tok $c - } - default { - #!todo - error out if cr inappropriate for tokenType - append tok $c - } - } - } else { - #lf may be appended if next - #review - lone cr as newline? - this is uncommon - but so is lone cr in a string(?) - set_tokenType "newline" - set tok cr - } - } - lf { - # \n newline - set dquotes $multi_dquote - set multi_dquote "" ;#!! - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal { - #nl is not allowed *within* a literal - require multiliteral syntax for any literal containing a newline ''' ''' - #even though we terminate the literal without the closing quote here - the token_waiting newline should trigger a state error - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - literalpart { - #we allow newlines - but store them within the multiliteral as their own element - #This is a legitimate end to the literalpart - but not the whole multiliteral - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - newline { - #review - #this lf is the trailing part of a crlf - append tok lf ;#assert we should now have tok "crlf" - as a previous cr is the only way to have an incomplete newline tok - return 1 - } - stringpart { - if {$dquotes ne ""} { - append tok $dquotes - incr i -1 - return 1 - } else { - if {$had_slash} { - #emit the stringpart (return 1), queue the continuation, go back 1 to reprocess the lf (incr i -1) - set_token_waiting type continuation value \\ complete 1 startindex [expr {$cindex-1}] - incr i -1 - return 1 - } else { - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } - starttablename - tablename - tablearrayname - starttablearrayname { - error "tomlish Character is invalid in $tokenType. [tomlish::parse::report_line]" - } - default { - #newline ends all other tokens. - #note for string: we don't add (raw unescaped) newline to simple string. (must use multi-string for this) - #note for whitespace: - # we will use the convention that \n terminates the current whitespace even if whitespace follows - # ie whitespace is split into separate whitespace tokens at each newline - - #puts "-------------- newline lf during tokenType $tokenType" - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - if {$dquotes ne ""} { - #e.g one or 2 quotes just before nl - set_tokenType "stringpart" - set tok $dquotes - incr i -1 - return 1 - } - set_tokenType "newline" - set tok lf - return 1 - } - } - multiliteral-space { - #assert had_slash 0, multi_dquote "" - set_tokenType "newline" - set tok "lf" - return 1 - } - default { - #ignore slash? error? - set_tokenType "newline" - set tok lf - return 1 - } - } - #if {$had_slash} { - # #CONT directly before newline - allows strings_5_byteequivalent test to pass - # set_tokenType "continuation" - # set tok "\\" - # incr i -1 - # return 1 - #} else { - # set_tokenType newline - # set tok lf - # return 1 - #} - } - } - , { - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - comment - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok , - } - string - quotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - #stringpart can have up to 2 quotes too - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - literal - literalpart - squotedkey - itablesquotedkey { - #assert had_slash always 0, multi_dquote "" - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - set backlen [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backlen - return 1 - } else { - set_token_waiting type comma value "," complete 1 startindex $cindex - return 1 - } - } - default { - set_token_waiting type comma value "," complete 1 startindex $cindex - if {$had_slash} {append tok "\\"} - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "$dquotes," - } - multiliteral-space { - #assert had_slash 0, multi_dquote "" - set_tokenType "literalpart" - set tok "," - } - default { - set_tokenType "comma" - set tok "," - return 1 - } - } - } - } - . { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - comment - untyped_value { - if {$had_slash} {append tok "\\"} - append tok $c - } - string - quotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - literal - literalpart - squotedkey - itablesquotedkey { - #assert had_slash always 0, multi_dquote "" - append tok $c - } - whitespace { - switch -exact -- $state { - multistring-space { - set backchars [expr {[tcl::string::length $dquotes] + 1}] - if {$had_slash} { - incr backchars 1 - } - incr i -$backchars - return 1 - } - dottedkey-space { - incr i -1 - return 1 - } - default { - error "tomlish Received period during tokenType 'whitespace' [tomlish::parse::report_line]" - } - } - } - starttablename - starttablearrayname { - #This would correspond to an empty table name - error "tomlish Character '.' is not allowed as first character ($tokenType). [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #subtable - split later - review - append tok $c - } - barekey { - #e.g x.y = 1 - #we need to transition the barekey to become a structured table name ??? review - #x is the tablename y is the key - set_token_waiting type dotsep value "." complete 1 startindex $cindex - return 1 - } - default { - error "tomlish Received period during tokenType '$tokenType' [tomlish::parse::report_line]" - #set_token_waiting type period value . complete 1 - #return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "$dquotes." - } - multiliteral-space { - set_tokenType "literalpart" - set tok "." - } - dottedkey-space { - ### - set_tokenType "dotsep" - set tok "." - return 1 - } - default { - set_tokenType "untyped_value" - set tok "." - } - } - } - - } - " " { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - if {[tcl::string::length $tokenType]} { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - barekey { - #todo had_slash - emit token or error - #whitespace is a terminator for bare keys - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - comment { - if {$had_slash} { - append tok "\\" - } - append tok $dquotes$c - } - string - quotedkey - itablequotedkey { - if {$had_slash} { append tok "\\" } - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - append tok $dquotes - incr i -1 - return 1 - } - } - literal - literalpart - squotedkey - itablesquotedkey { - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - if {$dquotes ne ""} { - #end whitespace token - #go back by the number of quotes plus this space char - set backchars [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backchars - return 1 - } else { - append tok $c - } - } else { - append tok $c - } - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace space during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - if {$dquotes ne ""} { - set_tokenType "stringpart" - set tok $dquotes - incr i -1 - return 1 - } - set_tokenType "whitespace" - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - if {$had_slash} { - error "tomlish unexpected backslash [tomlish::parse::report_line]" - } - set_tokenType "whitespace" - append tok $c - } - } - } - } - tab { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out (?review) - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - barekey { - #whitespace is a terminator for bare keys - incr i -1 - #set_token_waiting type whitespace value $c complete 1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - quotedkey - itablequotedkey - squotedkey - itablesquotedkey { - append tok $c - } - string - comment - whitespace { - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - append tok $dquotes - incr i -1 - return 1 - } - } - literal - literalpart { - append tok $c - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearraynames { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace tab during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - if {$slash_active} { - set slash_active 0 - } - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - if {$dquotes ne ""} { - set_tokenType stringpart - set tok $dquotes - incr i -1 - return 1 - } else { - set_tokenType whitespace - append tok $c - } - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "whitespace" - append tok $c - } - } - } - } - bom { - #BOM (Byte Order Mark) - ignored by token consumer - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - _start_squote_sequence { - #assert - tok will be one or two squotes only - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart { - append tok $c - } - default { - set_token_waiting type bom value "\uFEFF" complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "bom" - set tok "\uFEFF" - return 1 - } - } - } - } - default { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - if {$dquotes ne ""} { - set backlen [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backlen - return 1 - } else { - incr i -1 - return 1 - } - } else { - #review - incr i -1 ;#We don't have a full token to add to the token_waiting dict - so leave this char for next run. - return 1 - } - } - barekey { - if {[tomlish::utils::is_barekey $c]} { - append tok $c - } else { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed. (see tomlish::utils::is_barekey) [tomlish::parse::report_line]" - } - } - starttablename - starttablearrayname { - incr i -1 - #allow statemachine to set context for subsequent chars - return 1 - } - stringpart { - append tok $dquotes$c - } - default { - #e.g comment/string/literal/literalpart/untyped_value/starttablename/starttablearrayname/tablename/tablearrayname - append tok $c - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - table-space - itable-space { - #if no currently active token - assume another key value pair - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - curly-syntax { - puts stderr "curly-syntax - review" - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - multistring-space { - set_tokenType "stringpart" - if {$had_slash} { - #assert - we don't get had_slash and dquotes at same time - set tok \\$c - } else { - set tok $dquotes$c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - tablename-state { - set_tokenType "tablename" - set tok $c - } - tablearrayname-state { - set_tokenType "tablearrayname" - set tok $c - } - dottedkey-space { - set_tokenType barekey - set tok $c - } - default { - tomlish::log::debug "- tokloop char '$c' setting to untyped_value while state:$state" - set_tokenType "untyped_value" - set tok $c - } - } - } - } - } - - } - - #run out of characters (eof) - if {[tcl::string::length $tokenType]} { - #check for invalid ending tokens - #if {$state eq "err-state"} { - # error "Reached end of data whilst tokenType = '$tokenType'. INVALID" - #} - switch -exact -- $tokenType { - startquotesequence { - set toklen [tcl::string::length $tok] - if {$toklen == 1} { - #invalid - #eof with open string - error "tomlish eof reached without closing quote for string. [tomlish::parse::report_line]" - } elseif {$toklen == 2} { - #valid - #we ended in a double quote, not actually a startquoteseqence - effectively an empty string - switch_tokenType "startquote" - incr i -1 - #set_token_waiting type string value "" complete 1 - return 1 - } - } - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open literal - error "tomlish eof reached without closing single quote for string literal. [tomlish::parse::report_line]" - } - 2 { - #review - set_token_waiting type endsquote value "'" complete 1 startindex [expr {$cindex -1}] - set_tokenType "literal" - set tok "" - return 1 - } - } - } - } - set_token_waiting type eof value eof complete 1 startindex $i ;#review - return 1 - } else { - ::tomlish::log::debug "- No current tokenType, ran out of characters, setting tokenType to 'eof' [tomlish::parse::report_line]" - set tokenType "eof" - set tok "eof" - } - return 0 - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::parse ---}] -} - -namespace eval tomlish::dict { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - - proc is_tomlish_typeval {d} { - #designed to detect {type value } e.g {type INT value 3}, {type STRING value "blah etc"} - #as a sanity check we need to avoid mistaking user data that happens to match same form - #consider x.y={type="spud",value="blah"} - #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. - #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} - } - proc is_tomlish_typeval2 {d} { - upvar ::tomlish::tags tags - expr {[lindex $d 0] eq "type" && [lindex $d 1] in $tags} - } - proc last_tomltype_posn {d} { - set last_simple -1 - set dictposn [expr {[dict size $d] -1}] - foreach k [lreverse [dict keys $d]] { - set dval [dict get $d $k] - if {[is_tomlish_typeval $dval]} { - set last_simple $dictposn - break - } - incr dictposn -1 - } - return $last_simple - } - - - #review - proc name_from_tablestack {tablestack} { - set name "" - foreach tinfo [lrange $tablestack 1 end] { - lassign $tinfo type namepart - switch -- $type { - T { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - I { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - default { - #end at first break in the leading sequence of T & I tablenames - break - } - } - } - return $name - } - -} - -tcl::namespace::eval tomlish::app { - variable applist [list encoder decoder test] - - #*** !doctools - #[subsection {Namespace tomlish::app}] - #[para] - #[list_begin definitions] - - proc decoder {args} { - #*** !doctools - #[call app::[fun decoder] [arg args]] - #[para] read toml on stdin until EOF - #[para] on error - returns non-zero exit code and writes error on stderr - #[para] on success - returns zero exit code and writes JSON encoding of the data on stdout - #[para] This decoder is intended to be compatible with toml-test - - set opts [dict merge [dict create] $args] - #fconfigure stdin -encoding utf-8 - fconfigure stdin -translation binary - #Just slurp it all - presumably we are not handling massive amounts of data on stdin. - # - even if the input is large, we probably don't gain much (aside from possible memory savings?) by attempting to process input as it arrives. - if {[catch { - set toml [read stdin] - }]} { - exit 2 ;#read error - } - try { - set j [::tomlish::toml_to_json $toml] - } on error {em} { - puts stderr "decoding failed: '$em'" - exit 1 - } - puts -nonewline stdout $j - exit 0 - } - - proc encoder {args} { - #*** !doctools - #[call app::[fun encoder] [arg args]] - #[para] read JSON on stdin until EOF - #[para] return non-zero exitcode if JSON data cannot be converted to a valid TOML representation - #[para] return zero exitcode and TOML data on stdout if JSON data can be converted. - #[para] This encoder is intended to be compatible with toml-test - - set opts [dict merge [dict create] $args] - fconfigure stdin -translation binary - if {[catch { - set json [read stdin] - }]} { - exit 2 ;#read error - } - try { - set toml [::tomlish::json_to_toml $json] - } on error {em} { - puts stderr "encoding failed: '$em'" - exit 1 - } - puts -nonewline stdout $toml - exit 0 - } - - proc test {args} { - set opts [dict merge [dict create] $args] - - package require test::tomlish - if {[dict exists $opts -suite]} { - test::tomlish::suite [dict get $opts -suite] - } - test::tomlish::run - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::app ---}] -} - -proc ::tomlish::appnames {} { - set applist [list] - foreach cmd [info commands ::tomlish::app::*] { - lappend applist [namespace tail $cmd] - } - return $applist -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::lib { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace tomlish::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 tomlish::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -if {$argc > 0} { - puts stderr "argc: $argc args: $argv" - - if {($argc == 1)} { - if {[tcl::string::tolower $argv] in {help -help h -h}} { - puts stdout "Usage: -app where appname one of:[tomlish::appnames]" - exit 0 - } else { - puts stderr "Argument '$argv' not understood. Try -help" - exit 1 - } - } - set opts [dict create] - set opts [dict merge $opts $argv] - - set opts_understood [list -app ] - if {"-app" in [dict keys $opts]} { - #Don't vet the remaining opts - as they are interpreted by each app - } else { - foreach key [dict keys $opts] { - if {$key ni $opts_understood} { - puts stderr "Option '$key' not understood" - exit 1 - } - } - } - if {[dict exists $opts -app]} { - set app [dict get $opts -app] - if {$app ni [tomlish::appnames]} { - puts stderr "app '[dict get $opts -app]' not found" - exit 1 - } - tomlish::app::$app {*}$opts - } -} - -## Ready -package provide tomlish [namespace eval tomlish { - variable pkg tomlish - variable version - set version 1.1.2 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/tomlish-1.1.3.tm b/src/bootsupport/modules/tomlish-1.1.3.tm deleted file mode 100644 index 3da39427..00000000 --- a/src/bootsupport/modules/tomlish-1.1.3.tm +++ /dev/null @@ -1,6002 +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 tomlish 1.1.3 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin tomlish_module_tomlish 0 1.1.3] -#[copyright "2024"] -#[titledesc {tomlish toml parser}] [comment {-- Name section and table of contents description --}] -#[moddesc {tomlish}] [comment {-- Description at end of page heading --}] -#[require tomlish] -#[keywords module parsing toml configuration] -#[description] -#[para] tomlish is an intermediate representation of toml data in a tree structure (tagged lists representing type information) -#[para] The design goals are for tomlish to be whitespace and comment preserving ie byte-for byte preservation during roundtrips from toml to tomlish and back to toml -#[para] The tomlish representation can then be converted to a Tcl dict structure or to other formats such as json, -#[para] although these other formats are generally unlikely to retain whitespace or comments -#[para] The other formats also won't preserve roundtripability e.g \t and a literal tab coming from a toml file will be indistinguishable. -#[para] A further goal is to allow at least a useful subset of in-place editing operations which also preserve whitespace and comments. -#[para] e.g leaf key value editing, and table reordering/sorting, key-renaming at any level, key insertions/deletions -#[para] The API for editing (tomldoc object?) may require explicit setting of type if accessing an existing key -#[para] e.g setting a key that already exists and is a different type (especially if nested structure such as a table or array) -#[para] will need a -type option (-force ?) to force overriding with another type such as an int. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of tomlish -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by tomlish -#[list_begin itemized] - -package require Tcl 8.6- -package require struct::stack -package require logger - -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {struct::stack}] - -#limit ourselves to clear, destroy, peek, pop, push, rotate, or size (e.g v 1.3 does not implement 'get') - - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish { - namespace export {[a-z]*}; # Convention: export all lowercase - variable types - - #IDEAS: - # since get_toml produces tomlish with whitespace/comments intact: - # tomldoc object - allow (at least basic?) editing of toml whilst preserving comments & whitespace - # - setKey (set leaf only to value) how to specify type? -type option? - whole array vs index into arrays and further nested objects? - option for raw toml additions? - # - separate addKey?? - # - deleteKey (delete leaf) - # - deleteTable (delete table - if only has leaves? - option to delete with child tables?) - # - set/add Table? - position in doc based on existing tables/subtables? - - #The tomlish intermediate representation allows things such as sorting the toml document by table name or other re-ordering of tables - - # because the tables include subkeys, comments and newlines within their structure - those elements all come along with it nicely during reordering. - #The same goes for the first newline following a keyval e.g x=1 \ny=2\n\n - #The newline is part of the keyval structure so makes reordering easier - #example from_toml "a=1\nb=2\n\n\n" - # 0 = TOMLISH - # 1 = KEY a = {INT 1} {NEWLINE lf} - # 2 = NEWLINE lf - # 3 = KEY b = {INT 2} {NEWLINE lf} - # 4 = NEWLINE lf - # 5 = NEWLINE lf - - - #ARRAY is analogous to a Tcl list - #TABLE is analogous to a Tcl dict - #WS = inline whitespace - #KEY = bare key and value - #DQKEY = double quoted key and value - #SQKEY = single quoted key and value - #ITABLE = inline table (*can* be anonymous table) - # inline table values immediately create a table with the opening brace - # inline tables are fully defined between their braces, as are dotted-key subtables defined within - # No additional subtables or arrays of tables may be defined within an inline table after the ending brace - they must be entirely self-contained - - set tags [list TOMLISH ARRAY TABLE ITABLE ANONTABLE WS NEWLINE COMMENT DOTTEDKEY KEY DQKEY SQKEY STRING STRINGPART MULTISTRING LITERAL LITERALPART MULTILITERAL INT FLOAT BOOL DATETIME] - #tomlish v1.0 should accept arbitrary 64-bit signed ints (from -2^63 to 2^63-1) - #we will restrict to this range for compatibility for now - although Tcl can handle larger (arbitrarily so?) - set min_int -9223372036854775808 ;#-2^63 - set max_int +9223372036854775807 ;#2^63-1 - - proc Dolog {lvl txt} { - #return "$lvl -- $txt" - set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] tomlish '$txt'" - puts stderr $msg - } - logger::initNamespace ::tomlish - foreach lvl [logger::levels] { - interp alias {} tomlish_log_$lvl {} ::tomlish::Dolog $lvl - log::logproc $lvl tomlish_log_$lvl - } - - #*** !doctools - #[subsection {Namespace tomlish}] - #[para] Core API functions for tomlish - #[list_begin definitions] - - proc tags {} { - return $::tomlish::tags - } - - #helper function for to_dict - proc _get_keyval_value {keyval_element} { - log::notice ">>> _get_keyval_value from '$keyval_element'<<<" - set found_value 0 - #find the value - # 3 is the earliest index at which the value could occur (depending on whitespace) - set found_sub [list] - if {[lindex $keyval_element 2] ne "="} { - error "tomlish _get_keyval_value keyval_element doesn't seem to be a properly structured { = } list" - } - - #review - if {[uplevel 1 [list info exists tablenames_seen]]} { - upvar tablenames_seen tablenames_seen - } else { - set tablenames_seen [list] ;#list of lists - } - if {[uplevel 1 [list info exists tablenames_closed]]} { - upvar tablenames_closed tablenames_closed - } else { - set tablenames_closed [list] ;#list of lists - } - - foreach sub [lrange $keyval_element 2 end] { - #note that a barekey/dquotedkey won't occur directly inside a barekey/dquotedkey - switch -exact -- [lindex $sub 0] { - STRING - LITERAL - MULTISTRING - MULTILITERAL - INT - FLOAT - BOOL - DATETIME - TABLE - ARRAY - ITABLE { - set type [lindex $sub 0] - set value [lindex $sub 1] - set found_sub $sub - incr found_value 1 - } - default {} - } - } - if {!$found_value} { - error "tomlish Failed to find value element in KEY. '$keyval_element'" - } - if {$found_value > 1} { - error "tomlish Found multiple value elements in KEY, expected exactly one. '$keyval_element'" - } - - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - #simple (non-container, no-substitution) datatype - set result [list type $type value $value] - } - STRING - STRINGPART { - set result [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL - LITERALPART { - #REVIEW - set result [list type $type value $value] - } - TABLE { - #invalid? - error "tomlish _get_keyval_value invalid to have type TABLE on rhs of =" - } - ITABLE { - #This one should not be returned as a type value structure! - # - set result [::tomlish::to_dict [list $found_sub]] - } - ARRAY { - #we need to recurse to get the corresponding dict for the contained item(s) - #pass in the whole $found_sub - not just the $value! - set prev_tablenames_seen $tablenames_seen - set prev_tablenames_closed $tablenames_closed - set tablenames_seen [list] - set tablenames_closed [list] - set result [list type $type value [::tomlish::to_dict [list $found_sub]]] - set tablenames_seen $prev_tablenames_seen - set tablenames_closed $prev_tablenames_closed - } - MULTISTRING - MULTILITERAL { - #review - mapping these to STRING might make some conversions harder? - #if we keep the MULTI - we know we have to look for newlines for example when converting to json - #without specific types we'd have to check every STRING - and lose info about how best to map chars within it - set result [list type $type value [::tomlish::to_dict [list $found_sub]]] - } - default { - error "tomlish Unexpected value type '$type' found in keyval '$keyval_element'" - } - } - return $result - } - - proc _get_dottedkey_info {dottedkeyrecord} { - set key_hierarchy [list] - set key_hierarchy_raw [list] - if {[lindex $dottedkeyrecord 0] ne "DOTTEDKEY"} { - error "tomlish _get_dottedkey_info error. Supplied list doesn't appear to be a DOTTEDKEY (tag: [lindex $dottedkeyrecord 0])" - } - set compoundkeylist [lindex $dottedkeyrecord 1] - set expect_sep 0 - foreach part $compoundkeylist { - set parttag [lindex $part 0] - if {$parttag eq "WS"} { - continue - } - if {$expect_sep} { - if {$parttag ne "DOTSEP"} { - error "DOTTEDKEY missing dot separator between parts. '$dottedkeyrecord'" - } - set expect_sep 0 - } else { - set val [lindex $part 1] - switch -exact -- $parttag { - KEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw $val - } - DQKEY { - lappend key_hierarchy [::tomlish::utils::unescape_string $val] - lappend key_hierarchy_raw \"$val\" - } - SQKEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw "'$val'" - } - default { - error "DOTTED key unexpected part '$parttag' - ensure dot separator is between key parts. '$item'" - } - } - set expect_sep 1 - } - } - return [dict create keys $key_hierarchy keys_raw $key_hierarchy_raw] - } - - - - #to_dict is a *basic* programmatic datastructure for accessing the data. - # produce a dictionary of keys and values from a tomlish tagged list. - # to_dict is primarily for reading toml data. - #Extraneous (not within quoted sections) whitespace and comments are not preserved in this structure, - # so a roundtrip from toml to this datastructure and back to toml will lose whitespace formatting and comments. - # creating/changing toml values can be done directly on a tomlish list if preserving (or adding) formatting/comments is desired. - #A separate package 'tomlish::object' may be needed to allow easier programmatic creating/updating/deleting of data elements whilst preserving (or adding or selectively deleting/editing) such formatting. - # - - #within an ARRAY, we store a list of items such as plain dicts (possibly empty) and {type value } for simple types - #(ARRAYS can be mixed type) - #This means our dict structure should have only ARRAY and simple types which need to be in {type value } form - #A dict within an array encodeded as a type ITABLE value should also parse - but is the unpreferred form - REVIEW test? - - #Namespacing? - #ie note the difference: - #[Data] - #temp = { cpu = 79.5, case = 72.0} - # versus - #[Data] - #temps = [{cpu = 79.5, case = 72.0}] - proc to_dict {tomlish} { - - #keep track of which tablenames have already been directly defined, - # so we can raise an error to satisfy the toml rule: 'You cannot define any key or table more than once. Doing so is invalid' - #Note that [a] and then [a.b] is ok if there are no subkey conflicts - so we are only tracking complete tablenames here. - #we don't error out just because a previous tablename segment has already appeared. - ##variable tablenames_seen [list] - if {[uplevel 1 [list info exists tablenames_seen]]} { - upvar tablenames_seen tablenames_seen - } else { - set tablenames_seen [list] ;#list of lists - } - if {[uplevel 1 [list info exists tablenames_closed]]} { - upvar tablenames_closed tablenames_closed - } else { - set tablenames_closed [list] ;#list of lists - } - - log::info "---> to_dict processing '$tomlish'<<<" - set items $tomlish - - foreach lst $items { - if {[lindex $lst 0] ni $::tomlish::tags} { - error "supplied string does not appear to be toml parsed into a tomlish tagged list. Run tomlish::decode::toml on the raw toml data to produce a tomlish list" - } - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - #ignore TOMLISH tag at beginning - set items [lrange $tomlish 1 end] - } - - set datastructure [dict create] - foreach item $items { - set tag [lindex $item 0] - #puts "...> item:'$item' tag:'$tag'" - switch -exact -- $tag { - KEY - DQKEY - SQKEY { - log::debug "---> to_dict item: processing $tag: $item" - set key [lindex $item 1] - if {$tag eq "DQKEY"} { - set key [::tomlish::utils::unescape_string $key] - } - #!todo - normalize key. (may be quoted/doublequoted) - - if {[dict exists $datastructure $key]} { - error "Duplicate key '$key'. The key already exists at this level in the toml data. The toml data is not valid." - } - - #lassign [_get_keyval_value $item] type val - set keyval_dict [_get_keyval_value $item] - dict set datastructure $key $keyval_dict - } - DOTTEDKEY { - log::debug "---> to_dict item processing $tag: $item" - set dkey_info [_get_dottedkey_info $item] - set dotted_key_hierarchy [dict get $dkey_info keys] - - #a.b.c = 1 - #table_key_hierarchy -> a b - #leafkey -> c - if {[llength $dotted_key_hierarchy] == 0} { - #empty?? probably invalid. review - #This is different to '' = 1 or ''.'' = 1 which have lengths 1 and 2 respectively - error "DOTTED key has no parts - invalid? '$item'" - } elseif {[llength $dotted_key_hierarchy] == 1} { - #dottedkey is only a key - no table component - set table_hierarchy [list] - set leafkey [lindex $dotted_key_hierarchy 0] - } else { - set table_hierarchy [lrange $dotted_key_hierarchy 0 end-1] - set leafkey [lindex $dotted_key_hierarchy end] - } - - #ensure empty tables are still represented in the datastructure - #review - this seems unnecessary? - set pathkeys [list] - foreach k $table_hierarchy { - lappend pathkeys $k - if {![dict exists $datastructure {*}$pathkeys]} { - dict set datastructure {*}$pathkeys [list] - } else { - tomlish::log::notice "to_dict datastructure at key path $pathkeys already had data: [dict get $datastructure {*}$pathkeys]" - } - } - #review? - if {[dict exists $datastructure {*}$table_hierarchy $leafkey]} { - error "Duplicate key '$table_hierarchy $leafkey'. The key already exists at this level in the toml data. The toml data is not valid." - } - - #JMN test 2025 - if {[llength $table_hierarchy]} { - lappend tablenames_seen $table_hierarchy - } - - set keyval_dict [_get_keyval_value $item] - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - lappend tablenames_seen [list {*}$table_hierarchy $leafkey] - lappend tablenames_closed [list {*}$table_hierarchy $leafkey] - - #review - item is an ITABLE - we recurse here without datastructure context :/ - #overwriting keys? todo ? - dict set datastructure {*}$table_hierarchy $leafkey $keyval_dict - } else { - dict set datastructure {*}$table_hierarchy $leafkey $keyval_dict - } - - } - TABLE { - set tablename [lindex $item 1] - #set tablename [::tomlish::utils::tablename_trim $tablename] - set norm_segments [::tomlish::utils::tablename_split $tablename true] ;#true to normalize - if {$norm_segments in $tablenames_seen} { - error "Table name '$tablename' has already been directly defined in the toml data. Invalid." - } - - log::debug "---> to_dict processing item $tag (name: $tablename): $item" - set name_segments [::tomlish::utils::tablename_split $tablename] ;#unnormalized - set last_seg "" - #toml spec rule - all segments mst be non-empty - #note that the results of tablename_split are 'raw' - ie some segments may be enclosed in single or double quotes. - - set table_key_sublist [list] - - foreach normseg $norm_segments { - lappend table_key_sublist $normseg - if {[dict exists $datastructure {*}$table_key_sublist]} { - #It's ok for this key to already exist *if* it was defined by a previous tablename or equivalent - #and if this key is longer - - #consider the following 2 which are legal: - #[table] - #x.y = 3 - #[table.x.z] - #k= 22 - - #equivalent - - #[table] - #[table.x] - #y = 3 - #[table.x.z] - #k=22 - - #illegal - #[table] - #x.y = 3 - #[table.x.y.z] - #k = 22 - ## - we should fail on encountering table.x.y because only table and table.x are effectively tables - - #illegal - #[table] - #x.y = {p=3} - #[table.x.y.z] - #k = 22 - ## we should fail because y is an inline table which is closed to further entries - - - #note: it is not safe to compare normalized tablenames using join! - # e.g a.'b.c'.d is not the same as a.b.c.d - # instead compare {a b.c d} with {a b c d} - # Here is an example where the number of keys is the same, but they must be compared as a list, not a joined string. - #'a.b'.'c.d.e' vs 'a.b.c'.'d.e' - #we need to normalize the tablenames seen so that {"x\ty"} matches {"xy"} - - set sublist_length [llength $table_key_sublist] - set found_testkey 0 - if {$table_key_sublist in $tablenames_seen} { - set found_testkey 1 - } else { - #see if it was defined by a longer entry - foreach seen_table_segments $tablenames_seen { - if {[llength $seen_table_segments] <= $sublist_length} { - continue - } - #each tablenames_seen entry is already a list of normalized segments - - #we could have [a.b.c.d] early on - # followed by [a.b] - which was still defined by the earlier one. - - set seen_longer [lrange $seen_segments 0 [expr {$sublist_length -1}]] - puts stderr "testkey:'$table_key_sublist' vs seen_match:'$seen_longer'" - if {$table_key_sublist eq $seen_longer} { - set found_testkey 1 - } - } - } - - if {$found_testkey == 0} { - #the raw table_key_hierarchy is better to display in the error message, although it's not the actual dict keyset - set msg "key $table_key_sublist already exists in datastructure, but wasn't defined by a supertable." - append msg \n "tablenames_seen:" \n - foreach ts $tablenames_seen { - append msg " " $ts \n - } - error $msg - } - } - - } - - #ensure empty tables are still represented in the datastructure - set table_keys [list] - foreach k $table_key_hierarchy { - lappend table_keys $k - if {![dict exists $datastructure {*}$table_keys]} { - dict set datastructure {*}$table_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (TABLE) subkey $table_keys already had data: [dict get $datastructure {*}$table_keys]" - } - } - - #We must do this after the key-collision test above! - lappend tablenames_seen $norm_segments - - - log::debug ">>> to_dict >>>>>>>>>>>>>>>>> table_key_hierarchy : $table_key_hierarchy" - - #now add the contained elements - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkey_info [_get_dottedkey_info $element] - #e.g1 keys {x.y y} keys_raw {'x.y' "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - #e.g2 keys {x.y y} keys_raw {{"x.y"} "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - set dotted_key_hierarchy [dict get $dkey_info keys] - set dkeys [lrange $dotted_key_hierarchy 0 end-1] - set leaf_key [lindex $dotted_key_hierarchy end] - - #ensure empty keys are still represented in the datastructure - set test_keys $table_keys - foreach k $dkeys { - lappend test_keys $k - if {![dict exists $datastructure {*}$test_keys]} { - dict set datastructure {*}$test_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (DOTTEDKEY) subkey $test_keys already had data: [dict get $datastructure {*}$test_keys]" - } - } - - if {[dict exists $datastructure {*}$table_keys {*}$dkeys $leaf_key]} { - error "Duplicate key '$table_keys $dkeys $leaf_key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - #keyval_dict is either a {type value } - #or the result from parsing an arbitrary dict from an inline table - which could theoretically look the same at the topmost level - #punk::dict::is_tomlish_typeval can distinguish - puts stdout "to_dict>>> $keyval_dict" - dict set datastructure {*}$table_keys {*}$dkeys $leaf_key $keyval_dict - #JMN 2025 - lappend tablenames_seen [list {*}$table_key_hierarchy {*}$dkeys] - - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - #the value is either empty or or a dict structure with arbitrary (from-user-data) toplevel keys - # inner structure will contain {type value } if all leaves are not empty ITABLES - lappend tablenames_seen [list {*}$table_key_hierarchy {*}$dkeys $leaf_key] - #if the keyval_dict is not a simple type x value y - then it's an inline table ? - #if so - we should add the path to the leaf_key as a closed table too - as it's not allowed to have more entries added. - } - - } - KEY - DQKEY - SQKEY { - #obsolete ? - set keyval_key [lindex $element 1] - if {$type eq "DQKEY"} { - set keyval_key [::tomlish::utils::unescape_string $keyval_key] - } - if {[dict exists $datastructure {*}$dotted_key_hierarchy $keyval_key]} { - error "Duplicate key '$dotted_key_hierarchy $key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - dict set datastructure {*}$dotted_key_hierarchy $keyval_key $keyval_dict - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in table context. Expected only KEY,DQKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - #now make sure we add an empty value if there were no contained elements! - #!todo. - } - ITABLE { - #SEP??? - set datastructure [list] - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkey_info [_get_dottedkey_info $element] - set dotted_key_hierarchy [dict get $dkey_info keys] - set leaf_key [lindex $dotted_key_hierarchy end] - set dkeys [lrange $dotted_key_hierarchy 0 end-1] - - #ensure empty keys are still represented in the datastructure - set table_keys [list] ;#We don't know the context - next level up will have to check for key collisions? - set test_keys $table_keys - foreach k $dkeys { - lappend test_keys $k - if {![dict exists $datastructure {*}$test_keys]} { - dict set datastructure {*}$test_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (DOTTEDKEY) subkey $test_keys already had data: [dict get $datastructure {*}$test_keys]" - } - } - - if {[dict exists $datastructure {*}$table_keys {*}$dkeys $leaf_key]} { - error "Duplicate key '$table_keys $dkeys $leaf_key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - dict set datastructure {*}$table_keys {*}$dkeys $leaf_key $keyval_dict - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in ITABLE context. Expected only KEY,DQKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - } - ARRAY { - #arrays in toml are allowed to contain mixtures of types - set datastructure [list] - log::debug "--> processing array: $item" - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - STRING { - set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - ITABLE { - #anonymous table - #lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - lappend datastructure [::tomlish::to_dict [list $element]] ;#store itables within arrays as raw dicts (possibly empty) - } - TABLE { - #invalid? shouldn't be output from from_dict - but could manually be constructed as such? review - #doesn't make sense as table needs a name? - #take as synonym for ITABLE? - error "to_dict TABLE within array unexpected" - } - ARRAY - MULTISTRING - MULTILITERAL { - #set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - } - WS - SEP - NEWLINE - COMMENT { - #ignore whitespace, commas, newlines and comments - } - default { - error "Unexpected value type '$type' found in array" - } - } - } - } - MULTILITERAL { - #triple squoted string - #first newline stripped only if it is the very first element - #(ie *immediately* following the opening delims) - #All whitespace other than newlines is within LITERALPARTS - # ------------------------------------------------------------------------- - #todo - consider extension to toml to allow indent-aware multiline literals - # how - propose as issue in toml github? Use different delim? e.g ^^^ ? - #e.g - # xxx=?'''abc - # def - # etc - # ''' - # - we would like to trimleft each line to the column following the opening delim - # ------------------------------------------------------------------------- - - log::debug "---> todict processing multiliteral: $item" - set parts [lrange $item 1 end] - if {[lindex $parts 0 0] eq "NEWLINE"} { - set parts [lrange $parts 1 end] ;#skip it - } - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - switch -exact -- $type { - LITERALPART { - append stringvalue [lindex $element 1] - } - NEWLINE { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - MULTISTRING { - #triple dquoted string - log::debug "---> to_dict processing multistring: $item" - set stringvalue "" - set idx 0 - set parts [lrange $item 1 end] - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - #We use STRINGPART in the tomlish representation as a distinct element to STRING - which would imply wrapping quotes to be reinserted - switch -exact -- $type { - STRING { - #todo - do away with STRING ? - #we don't build MULTISTRINGS containing STRING - but should we accept it? - tomlish::log::warn "double quoting a STRING found in MULTISTRING - should be STRINGPART?" - append stringvalue "\"[::tomlish::utils::unescape_string [lindex $element 1]]\"" - } - STRINGPART { - append stringvalue [::tomlish::utils::unescape_string [lindex $element 1]] - } - CONT { - #When the last non-whitespace character on a line is an unescaped backslash, - #it will be trimmed along with all whitespace (including newlines) up to the next non-whitespace character or closing delimiter - # review - we allow some whitespace in stringpart elements - can a stringpart ever be all whitespace? - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - append stringvalue "\\" ;#add the sep - } else { - #skip over ws without emitting - set idx [llength $parts] - } - } else { - set parts_til_nl [lrange $parts 0 $next_nl-1] - set non_ws [lsearch -index 0 -start $idx+1 -not $parts_til_nl WS] - if {$non_ws >= 0} { - append stringvalue "\\" - } else { - #skip over ws on this line - set idx $next_nl - #then have to check each subsequent line until we get to first non-whitespace - set trimming 1 - while {$trimming && $idx < [llength $parts]} { - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - } else { - set idx [llength $parts] - } - set trimming 0 - } else { - set non_ws [lsearch -index 0 -start $idx+1 -not [lrange $parts 0 $next_nl-1] WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - set trimming 0 - } else { - set idx $next_nl - #keep trimming - } - } - } - } - } - } - NEWLINE { - #if newline is first element - it is not part of the data of a multistring - if {$idx > 0} { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - } - WS { - append stringvalue [lindex $element 1] - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - WS - COMMENT - NEWLINE { - #ignore - } - default { - error "Unexpected tag '$tag' in Tomlish list '$tomlish'" - } - } - } - return $datastructure - } - - - proc _from_dictval_tomltype {parents tablestack keys typeval} { - set type [dict get $typeval type] - set val [dict get $typeval value] - switch -- $type { - ARRAY { - set subitems [list] - foreach item $val { - lappend subitems [_from_dictval [list {*}$parents ARRAY] $tablestack $keys $item] SEP - } - if {[lindex $subitems end] eq "SEP"} { - set subitems [lrange $subitems 0 end-1] - } - return [list ARRAY {*}$subitems] - } - ITABLE { - if {$val eq ""} { - return ITABLE - } else { - return [_from_dictval [list {*}$parents ITABLE] $tablestack $keys $val] - } - } - MULTISTRING { - #value is a raw string that isn't encoded as tomlish - #create a valid toml snippet with the raw value and decode it to the proper tomlish MULTISTRING format - #We need to convert controls in $val to escape sequences - except for newlines - # - #consider an *option* to reformat for long lines? (perhaps overcomplex - byte equiv - but may fold in ugly places) - #we could use a line-length limit to decide when to put in a "line ending backslash" - #and even format it with a reasonable indent so that proper CONT and WS entries are made (?) REVIEW - # - #TODO - set tomlpart "x=\"\"\"\\\n" - append tomlpart $val "\"\"\"" - set tomlish [tomlish::decode::toml $tomlpart] - #e.g if val = " etc\nblah" - #TOMLISH {DOTTEDKEY {{KEY x}} = {MULTISTRING CONT {NEWLINE LF} {WS { }} {STRINGPART etc} {NEWLINE lf} {STRINGPART blah} } } - #lindex 1 3 is the MULTISTRING tomlish list - return [lindex $tomlish 1 3] - } - MULTILITERAL { - #MLL string can contain newlines - but still no control chars - #todo - validate - set tomlpart "x='''\n" - append tomlpart $val ''' - set tomlish [tomlish::decode::toml $tomlpart] - return [lindex $tomlish 1 3] - } - LITERAL { - #from v1.0 spec - "Control characters other than tab are not permitted in a literal string" - #(This rules out raw ANSI SGR - which is somewhat restrictive - but perhaps justified for a config format - # as copy-pasting ansi to a config value is probably not always wise, and it's not something that can be - # easily input via a text editor. ANSI can go in Basic strings using the \e escape if that's accepted v1.1?) - #we could choose to change the type to another format here when encountering invalid chars - but that seems - #like too much magic. We elect to error out and require the dict to have valid data for the types it specifies. - if {[string first ' $val] >=0} { - error "_from_dictval_tomltype error: single quote found in LITERAL - cannot encode dict to TOML-VALID TOMLISH" - } - #detect control chars other than tab - #for this we can use rawstring_to_Bstring_with_escaped_controls - even though this isn't a Bstring - #we are just using the map to detect a difference. - set testval [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val] - if {$testval ne $val} { - #some escaping would have to be done if this value was destined for a Bstring... - #therefor this string has controls and isn't suitable for a LITERAL according to the specs. - error "_from_dictval_tomltype error: control chars (other than tab) found in LITERAL value - cannot encode dict to TOML-VALID TOMLISH" - } - return [list LITERAL $val] - } - STRING { - return [list STRING [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val]] - } - INT { - if {![::tomlish::utils::is_int $val]} { - error "_from_dictval_tomltype error: bad INT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list INT $val] - } - FLOAT { - if {![::tomlish::utils::is_float $val]} { - error "_from_dictval_tomltype error: bad FLOAT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list FLOAT $val] - } - default { - if {$type ni [::tomlish::tags]} { - error "_from_dictval_tomltype error: Unrecognised typename '$type' in {type value } - cannot encode dict to TOML-VALID TOMLISH" - } - return [list $type $val] - } - } - } - - proc _from_dictval {parents tablestack keys vinfo} { - set k [lindex $keys end] - if {[regexp {\s} $k] || [string first . $k] >= 0} {} - if {![::tomlish::utils::is_barekey $k]} { - #Any dot in the key would have been split by to_dict - so if it's present here it's part of this key - not a level separator! - #requires quoting - #we'll use a basic mechanism for now to determine the type of quoting - whether it has any single quotes or not. - #todo - more? - #For keys - we currently (2025) are only allowed barekeys,basic strings and literal strings. (no multiline forms) - if {[string first ' $k] >=0} { - #basic string - } else { - #literal string - set K_PART [list SQKEY $k] - } - } else { - set K_PART [list KEY $k] - } - puts stderr "---parents:'$parents' keys:'$keys' vinfo: $vinfo---" - puts stderr "---tablestack: $tablestack---" - set result [list] - set lastparent [lindex $parents end] - if {$lastparent in [list "" do_inline]} { - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - set type [dict get $vinfo type] - #treat ITABLE differently? - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result DOTTEDKEY [list $K_PART {WS { }}] = {WS { }} $sublist {NEWLINE lf} - } else { - #set result [list TABLE $k {NEWLINE lf}] - if {$vinfo ne ""} { - - #set result [list DOTTEDKEY [list [list KEY $k]] = ] - #set records [list ITABLE] - - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - - if {$lastparent eq "do_inline"} { - set result [list DOTTEDKEY [list $K_PART] =] - set records [list ITABLE] - } else { - #review - quoted k ?? - set result [list TABLE $k {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $k]] - set records [list] - } - - - - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - dict for {vk vv} $vinfo { - if {[regexp {\s} $vk] || [string first . $vk] >= 0} { - set VK_PART [list SQKEY $vk] - } else { - set VK_PART [list KEY $vk] - } - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART {WS { }}] = {WS { }} $sublist] - } else { - if {$vv eq ""} { - #experimental - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline KEY $vk (tablestack:$tablestack)" - #set tname [tomlish::dict::name_from_tablestack [list {*}$tablestack [list T $vk]]] - set tname [join [list {*}$keys $vk] .] - set record [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - } else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - } - } else { - if { 0 } { - #experiment.. sort of getting there. - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline2 KEYS [list {*}$keys $vk] (tablestack:$tablestack)" - set tname [join [list {*}$keys $vk] .] - set record [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - - #review - todo? - set dottedkey_value [_from_dictval [list {*}$parents TABLE] $tablestack [list {*}$keys $vk] $vv] - lappend record {*}$dottedkey_value - - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - } - if {$dictidx != $lastidx} { - #lappend record SEP - if {$lastparent eq "do_inline"} { - lappend record SEP - } else { - lappend record {NEWLINE lf} - } - } - lappend records $record - incr dictidx - } - if {$lastparent eq "do_inline"} { - lappend result $records {NEWLINE lf} - } else { - lappend result {*}$records {NEWLINE lf} - } - } else { - if {$lastparent eq "do_inline"} { - lappend result DOTTEDKEY [list [list KEY $k]] = ITABLE {NEWLINE lf} - } else { - lappend result TABLE $k {NEWLINE lf} - } - } - } - } else { - #lastparent is not toplevel "" or "do_inline" - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result {*}$sublist - } else { - if {$lastparent eq "TABLE"} { - #review - dict for {vk vv} $vinfo { - set dottedkey_value [_from_dictval [list {*}$parents DOTTEDKEY] $tablestack [list {*}$keys $vk] $vv] - lappend result [list DOTTEDKEY [list [list KEY $vk]] = $dottedkey_value {NEWLINE lf}] - } - } else { - if {$vinfo ne ""} { - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - set sub [list] - #REVIEW - #set result $lastparent ;#e.g sets ITABLE - set result ITABLE - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - dict for {vk vv} $vinfo { - if {[regexp {\s} $vk] || [string first . $vk] >=0} { - set VK_PART [list SQKEY $vk] - } else { - set VK_PART [list KEY $vk] - } - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART] = $sublist] - } else { - if {$vv eq ""} { - #can't just uninline at this level - #we need a better method to query main dict for uninlinability at each level - # (including what's been inlined already) - #if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - # puts stderr "_from_dictval uninline2 KEY $keys" - # set tname [join [list {*}$keys $vk] .] - # set record [list TABLE $tname {NEWLINE lf}] - # set tablestack [list {*}$tablestack [list T $vk]] - #} else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - #} - } else { - #set sub [_from_dictval ITABLE $vk $vv] - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - #set record [list DOTTEDKEY [list $VK_PART] = ITABLE $dottedkey_value] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - if {$dictidx != $lastidx} { - lappend record SEP - } - lappend result $record - incr dictidx - } - } else { - puts stderr "table x-1" - lappend result DOTTEDKEY [list $K_PART] = ITABLE - } - } - } - } - return $result - } - - - proc from_dict {d} { - #consider: - # t1={a=1,b=2} - # x = 1 - #If we represent t1 as an expanded table we get - # [t1] - # a=1 - # b=2 - # x=1 - # --- which is incorrect - as x was a toplevel key like t1! - #This issue doesn't occur if x is itself an inline table - # t1={a=1,b=2} - # x= {no="problem"} - # - # (or if we were to reorder x to come before t1) - - #As the dictionary form doesn't distinguish the structure used to create tables {[table1]\nk=v} vs inline {table1={k=v}} - #Without a solution, from_dict would have to always produce the inline form for toplevel tables unless we allowed re-ordering, - #which is unpreferred here. - - #A possible solution: - #scan the top level to see if all (trailing) elements are themselves dicts - # (ie not of form {type XXX value yyy}) - # - # A further point is that if all root level values are at the 'top' - we can treat lower table-like structures as {[table]} elements - #ie we don't need to force do_inline if all the 'simple' keys are before any compound keys - - #set root_has_values 0 - #approach 1) - the naive approach - forces inline when not always necessary - #dict for {k v} $d { - # if {[llength $v] == 4 && [lindex $v 0] eq "type"} { - # set root_has_values 1 - # break - # } - #} - - - #approach 2) - track the position of last {type x value y} in the dictionary built by to_dict - # - still not perfect. Inlines dotted tables unnecessarily - #This means from_dict doesn't produce output optimal for human editing. - set last_simple [tomlish::dict::last_tomltype_posn $d] - - - ## set parent "do_inline" ;#a value used in _from_dictval to distinguish from "" or other context based parent values - #Any keys that are themselves tables - will need to be represented inline - #to avoid reordering, or incorrect assignment of plain values to the wrong table. - - ## set parent "" - #all toplevel keys in the dict structure can represent subtables. - #we are free to use {[tablename]\n} syntax for toplevel elements. - - - set tomlish [list TOMLISH] - set dictposn 0 - set tablestack [list [list T root]] ;#todo - dict for {t tinfo} $d { - if {$last_simple > $dictposn} { - set parents [list do_inline] - } else { - set parents [list ""] - } - set keys [list $t] - set trecord [_from_dictval $parents $tablestack $keys $tinfo] - lappend tomlish $trecord - incr dictposn - } - return $tomlish - } - - proc json_to_toml {json} { - #*** !doctools - #[call [fun json_to_toml] [arg json]] - #[para] - - set tomlish [::tomlish::from_json $json] - set toml [::tomlish::to_toml $tomlish] - } - - #TODO use huddle? - proc from_json {json} { - #set jstruct [::tomlish::json_struct $json] - #return [::tomlish::from_json_struct $jstruct] - package require huddle - package require huddle::json - set h [huddle::json::json2huddle parse $json] - - } - - proc from_json_struct {jstruct} { - package require fish::json_toml - return [fish::json_toml::jsonstruct2tomlish $jstruct] - } - - proc toml_to_json {toml} { - set tomlish [::tomlish::from_toml $toml] - return [::tomlish::get_json $tomlish] - } - - proc get_json {tomlish} { - package require fish::json - set d [::tomlish::to_dict $tomlish] - #return [::tomlish::dict_to_json $d] - return [fish::json::from "struct" $d] - } - - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -namespace eval tomlish::build { - #STRING,INT,FLOAT,BOOL, DATETIME - simple wrappers for completeness - # take a value of the appropriate type and wrap as a tomlish tagged item - proc STRING {s} { - return [list STRING [::tomlish::utils::rawstring_to_Bstring_with_escaped_controls $s]] - } - proc LITERAL {litstring} { - - } - - proc INT {i} { - #whole numbers, may be prefixed with a + or - - #Leading zeros are not allowed - #Hex,octal binary forms are allowed (toml 1.0) - #We will error out on encountering commas, as commas are interpreted differently depending on locale (and don't seem to be supported in the toml spec anyway) - #!todo - Tcl can handle bignums - bigger than a 64bit signed long as specified in toml. - # - We should probably raise an error for number larger than this and suggest the user supply it as a string? - if {[tcl::string::last , $i] > -1} { - error "Unable to interpret '$i' as an integer. Use underscores if you need a thousands separator [::tomlish::parse::report_line]" - } - if {![::tomlish::utils::int_validchars $i]} { - error "Unable to interpret '$i' as an integer. Only 0-9 + 1 _ characters are acceptable. [::tomlish::parse::report_line]" - } - - if {[::tomlish::utils::is_int $i]} { - return [list INT $i] - } else { - error "'$i' is not a valid integer as per the Toml spec. [::tomlish::parse::report_line]" - } - - } - - proc FLOAT {f} { - #convert any non-lower case variants of special values to lowercase for Toml - if {[::tcl::string::tolower $f] in {nan +nan -nan inf +inf -inf}} { - return [list FLOAT [tcl::string::tolower $f]] - } - if {[::tomlish::utils::is_float $f]} { - return [list FLOAT $f] - } else { - error "Unable to interpret '$f' as Toml float. Check your input, or check that tomlish is able to handle all Toml floats properly [::tomlish::parse::report_line]" - } - } - - proc DATETIME {str} { - if {[::tomlish::utils::is_datetime $str]} { - return [list DATETIME $str] - } else { - error "Unable to interpret '$str' as Toml datetime. Check your input, or check that tomlish is able to handle all Toml datetimes properly [::tomlish::parse::report_line]" - } - } - - proc BOOLEAN {b} { - #convert any Tcl-acceptable boolean to boolean as accepted by toml - lower case true/false - if {![tcl::string::is boolean -strict $b]} { - error "Unable to convert '$b' to Toml boolean true|false. [::tomlish::parse::report_line]" - } else { - if {$b && 1} { - return [::list BOOL true] - } else { - return [::list BOOL false] - } - } - } - - #REVIEW - #Take tablename followed by - # a) *tomlish* name-value pairs e.g table mydata [list KEY item11 = [list STRING "test"]] {KEY item2 = [list INT 1]} - # (accept also key value {STRING }) - # b) simple 2-element tcl lists being name & *simple* value pairs for which basic heuristics will be used to determine types - proc _table {name args} { - set pairs [list] - foreach t $args { - if {[llength $t] == 4} { - if {[tcl::string::tolower [lindex $t 0]] ne "key" || [tcl::string::tolower [lindex $t 2]] ni "= value"} { - error "Only items tagged as KEY = currently accepted as name-value pairs for table command" - } - lassign $t _k keystr _eq valuepart - if {[llength $valuepart] != 2} { - error "supplied value must be typed. e.g {INT 1} or {STRING test}" - } - lappend pairs [list KEY $keystr = $valuepart] - } elseif {[llength $t] == 2} { - #!todo - type heuristics - lassign $t n v - lappend pairs [list KEY $n = [list STRING $v]] - } else { - error "'KEY = { toml but - # the first newline is not part of the data. - # we elect instead to maintain a basic LITERALPART that must not contain newlines.. - # and to compose MULTILITERAL of multiple NEWLINE LITERALPART parts, - #with the datastructure representation dropping the first newline (if immediately following opening delim) when building the value. - set literal "" - foreach part [lrange $item 1 end] { - append literal [::tomlish::encode::tomlish [list $part] $nextcontext] - } - append toml '''$literal''' - } - INT - - BOOL - - FLOAT - - DATETIME { - append toml [lindex $item 1] - } - INCOMPLETE { - error "cannot process tomlish term tagged as INCOMPLETE" - } - COMMENT { - append toml "#[lindex $item 1]" - } - BOM { - #Byte Order Mark may appear at beginning of a file. Needs to be preserved. - append toml "\uFEFF" - } - default { - error "Not a properly formed 'tomlish' taggedlist.\n '$list'\n Unknown tag '[lindex $item 0]'. See output of \[tomlish::tags\] command." - } - } - - } - return $toml - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::encode ---}] -} -#fish toml from tomlish - -#(encode tomlish as toml) -interp alias {} tomlish::to_toml {} tomlish::encode::tomlish - -# - - -namespace eval tomlish::decode { - #*** !doctools - #[subsection {Namespace tomlish::decode}] - #[para] - #[list_begin definitions] - - #return a Tcl list of tomlish tokens - #i.e get a standard list of all the toml terms in string $s - #where each element of the list is a *tomlish* term.. i.e a specially 'tagged' Tcl list. - #(simliar to a tcl 'Huddle' - but also supporting whitespace preservation) - # ---------------------------------------------------------------------------------------------- - # NOTE: the production of tomlish from toml source doesn't indicate the toml source was valid!!! - # e.g we deliberately don't check certain things such as duplicate table declarations here. - # ---------------------------------------------------------------------------------------------- - #Part of the justification for this is that as long as the syntax is toml shaped - we can load files which violate certain rules and allow programmatic manipulation. - # (e.g perhaps a toml editor to highlight violations for fixing) - # A further stage is then necessary to load the tomlish tagged list into a data structure more suitable for efficient query/reading. - # e.g dicts or an object oriented structure - #Note also - *no* escapes in quoted strings are processed. This is up to the datastructure stage - #e.g to_dict will substitute \r \n \uHHHH \UHHHHHHH etc - #This is important for tomlish to maintain the ability to perform competely lossless round-trips from toml to tomlish and back to toml. - # (which is handy for testing as well as editing some part of the structure with absolutely no effect on other parts of the document) - #If we were to unescape a tab character for example - # - we have no way of knowing if it was originally specified as \t \u0009 or \U00000009 or directly as a tab character. - # For this reason, we also do absolutely no line-ending transformations based on platform. - # All line-endings are maintained as is, and even a file with mixed lf crlf line-endings will be correctly interpreted and can be 'roundtripped' - - proc toml {args} { - #*** !doctools - #[call [fun toml] [arg arg...]] - #[para] return a Tcl list of tomlish tokens - - set s [join $args \n] - - namespace upvar ::tomlish::parse is_parsing is_parsing - set is_parsing 1 - - if {[info command ::tomlish::parse::spacestack] eq "::tomlish::parse::spacestack"} { - tomlish::parse::spacestack destroy - } - struct::stack ::tomlish::parse::spacestack - - namespace upvar ::tomlish::parse last_space_action last_space_action - namespace upvar ::tomlish::parse last_space_type last_space_type - - namespace upvar ::tomlish::parse tok tok - set tok "" - - namespace upvar ::tomlish::parse type type - namespace upvar ::tomlish::parse tokenType tokenType - ::tomlish::parse::set_tokenType "" - namespace upvar ::tomlish::parse tokenType_list tokenType_list - set tokenType [list] ;#Flat (un-nested) list of tokentypes found - - namespace upvar ::tomlish::parse lastChar lastChar - set lastChar "" - - - set result "" - namespace upvar ::tomlish::parse nest nest - set nest 0 - - namespace upvar ::tomlish::parse v v ;#array keyed on nest level - - - set v(0) {TOMLISH} - array set s0 [list] ;#whitespace data to go in {SPACE {}} element. - set parentlevel 0 - - namespace upvar ::tomlish::parse i i - set i 0 - - namespace upvar ::tomlish::parse state state - - namespace upvar ::tomlish::parse braceCount braceCount - set barceCount 0 - namespace upvar ::tomlish::parse bracketCount bracketCount - set bracketCount 0 - - set sep 0 - set r 1 - namespace upvar ::tomlish::parse token_waiting token_waiting - set token_waiting [dict create] ;#if ::tok finds a *complete* second token during a run, it will put the 2nd one here to be returned by the next call. - - - set state "table-space" - ::tomlish::parse::spacestack push {type space state table-space} - namespace upvar ::tomlish::parse linenum linenum;#'line number' of input data. (incremented for each literal linefeed - but not escaped ones in data) - set linenum 1 - - set ::tomlish::parse::state_list [list] - try { - while {$r} { - set r [::tomlish::parse::tok $s] - #puts stdout "got tok: '$tok' while parsing string '$s' " - set next_tokenType_known 0 ;#whether we begin a new token here based on what terminated the token result of 'tok' - - - #puts "got token: '$tok' tokenType='$tokenType'. while v($nest) = [set v($nest)]" - #puts "-->tok: $tok tokenType='$tokenType'" - set prevstate $state - set transition_info [::tomlish::parse::goNextState $tokenType $tok $state] - #review goNextState could perform more than one space_action - set space_action [dict get $transition_info space_action] - set newstate [dict get $transition_info newstate] ;#use of 'newstate' vs 'state' makes code clearer below - - if {[tcl::string::match "err-*" $state]} { - ::tomlish::log::warn "---- State error in state $prevstate for tokenType: $tokenType token value: $tok. $state aborting parse. [tomlish::parse::report_line]" - lappend v(0) [list ERROR tokentype $tokenType state $prevstate to $state leveldata [set v($nest)]] - return $v(0) - } - # --------------------------------------------------------- - #NOTE there may already be a token_waiting at this point - #set_token_waiting can raise an error here, - # in which case the space_action branch needs to be rewritten to handle the existing token_waiting - # --------------------------------------------------------- - - if {$space_action eq "pop"} { - #pop_trigger_tokens: newline tablename endarray endinlinetable - #note a token is a pop trigger depending on context. e.g first newline during keyval is a pop trigger. - set parentlevel [expr {$nest -1}] - set do_append_to_parent 1 ;#most tokens will leave this alone - but some like squote_seq need to do their own append - switch -exact -- $tokenType { - squote_seq { - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the xxx-squote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - switch -- $tok { - ' { - tomlish::parse::set_token_waiting type startsquote value $tok complete 1 startindex [expr {$i -1}] - } - '' { - #review - we should perhaps return double_squote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_squote value "" complete 1 startindex [expr {$i - 2}] - } - ''' { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 3}] - } - '''' { - switch -exact -- $prevstate { - leading-squote-space { - error "---- 4 squotes from leading-squote-space - shouldn't get here" - #we should have emitted the triple and left the last for next loop - } - trailing-squote-space { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left squote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]'" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "'"] - } - default { - error "--- don't know how to integrate extra trailing squote with data $v($parentlevel)" - } - } - } - default { - error "--- unexpected popped due to squote_seq but came from state '$prevstate' should have been leading-squote-space or trailing-squote-space" - } - } - } - ''''' { - switch -exact -- $prevstate { - leading-squote-space { - error "---- 5 squotes from leading-squote-space - shouldn't get here" - #we should have emitted the triple and left the following squotes for next loop - } - trailing-squote-space { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 squotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]''" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [LITERALPART "''"] - } - default { - error "--- don't know how to integrate extra trailing 2 squotes with data $v($parentlevel)" - } - } - } - default { - error "--- unexpected popped due to squote_seq but came from state '$prevstate' should have been leading-squote-space or trailing-squote-space" - } - } - } - } - puts stderr "tomlish::decode::toml ---- HERE squote_seq pop <$tok>" - } - triple_squote { - #presumably popping multiliteral-space - ::tomlish::log::debug "---- triple_squote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTILITERAL { - lappend merged $part - } - LITERALPART { - if {$lasttype eq "LITERALPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_squote unhandled part type [lindex $part 0] unable to merge leveldata: $v($next)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - equal { - #pop caused by = - switch -exact -- $prevstate { - dottedkey-space { - tomlish::log::debug "---- equal ending dottedkey-space for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - dottedkey-space-tail { - #experiment? - tomlish::log::debug "---- equal ending dottedkey-space-tail for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - } - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - tablename { - #note: a tablename only 'pops' if we are greater than zero - error "---- tablename pop should already have been handled as special case zeropoppushspace in goNextState" - } - tablearrayname { - #!review - tablearrayname different to tablename regarding push/pop? - #note: a tablename only 'pops' if we are greater than zero - error "---- tablearrayname pop should already have been handled as special case zeropoppushspace in goNextState" - } - endarray { - #nothing to do here. - } - comma { - #comma for inline table will pop the keyvalue space - lappend v($nest) "SEP" - } - endinlinetable { - ::tomlish::log::debug "---- endinlinetable for last_space_action pop" - } - endmultiquote { - ::tomlish::log::debug "---- endmultiquote for last_space_action 'pop'" - } - default { - error "---- unexpected tokenType '$tokenType' for last_space_action 'pop'" - } - } - if {$do_append_to_parent} { - #e.g squote_seq does it's own appends as necessary - so won't get here - lappend v($parentlevel) [set v($nest)] - } - - incr nest -1 - - } elseif {$last_space_action eq "push"} { - set prevnest $nest - incr nest 1 - set v($nest) [list] - # push_trigger_tokens: barekey dquotedkey startinlinetable startarray tablename tablearrayname - - - switch -exact -- $tokenType { - squote_seq_begin { - #### - if {[dict exists $transition_info starttok] && [dict get $transition_info starttok] ne ""} { - lassign [dict get $transition_info starttok] starttok_type starttok_val - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType $starttok_type - set tok $starttok_val - } - } - squotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - dquotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - XXXdquotedkey - XXXitablequotedkey { - #todo - set v($nest) [list DQKEY $tok] ;#$tok is the keyname - } - barekey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - set waiting [tomlish::parse::get_token_waiting] - if {[llength $waiting]} { - set i [dict get $waiting startindex] - tomlish::parse::clear_token_waiting - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } else { - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - } - startsquote { - #JMN - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - tablename { - #note: we do not use the output of tomlish::tablename_trim to produce a tablename for storage in the tomlish list! - #The tomlish list is intended to preserve all whitespace (and comments) - so a roundtrip from toml file to tomlish - # back to toml file will be identical. - #It is up to the datastructure stage to normalize and interpret tomlish for programmatic access. - # we call tablename_trim here only to to validate that the tablename data is well-formed at the outermost level, - # so we can raise an error at this point rather than create a tomlish list with obviously invalid table names. - - #todo - review! It's arguable that we should not do any validation here, and just store even incorrect raw tablenames, - # so that the tomlish list is more useful for say a toml editor. Consider adding an 'err' tag to the appropriate place in the - # tomlish list? - - set test_only [::tomlish::utils::tablename_trim $tok] - ::tomlish::log::debug "---- trimmed (but not normalized) tablename: '$test_only'" - set v($nest) [list TABLE $tok] ;#$tok is the *raw* table name - #note also that equivalent tablenames may have different toml representations even after being trimmed! - #e.g ["x\t\t"] & ["x "] (tab escapes vs literals) - #These will show as above in the tomlish list, but should normalize to the same tablename when used as keys by the datastructure stage. - } - tablearrayname { - set test_only [::tomlish::utils::tablename_trim $tok] - puts stdout "trimmed (but not normalized) tablearrayname: '$test_only'" - set v($nest) [list TABLEARRAY $tok] ;#$tok is the *raw* tablearray name - } - startarray { - set v($nest) [list ARRAY] ;#$tok is just the opening bracket - don't output. - } - startinlinetable { - set v($nest) [list ITABLE] ;#$tok is just the opening curly brace - don't output. - } - startmultiquote { - ::tomlish::log::debug "---- push trigger tokenType startmultiquote" - set v($nest) [list MULTISTRING] ;#container for STRINGPART, WS, CONT, NEWLINE - } - triple_squote { - ::tomlish::log::debug "---- push trigger tokenType triple_squote" - set v($nest) [list MULTILITERAL] ;#container for NEWLINE,LITERAL - } - default { - error "---- push trigger tokenType '$tokenType' not yet implemented" - } - } - - } else { - #no space level change - switch -exact -- $tokenType { - squotedkey { - puts "---- squotedkey in state $prevstate (no space level change)" - lappend v($nest) [list SQKEY $tok] - } - dquotedkey { - puts "---- dquotedkey in state $prevstate (no space level change)" - lappend v($nest) [list DQKEY $tok] - } - barekey { - lappend v($nest) [list KEY $tok] - } - dotsep { - lappend v($nest) [list DOTSEP] - } - starttablename { - #$tok is triggered by the opening bracket and sends nothing to output - } - starttablearrayname { - #$tok is triggered by the double opening brackets and sends nothing to output - } - tablename - tablenamearray { - error "---- did not expect 'tablename/tablearrayname' without space level change (no space level change)" - #set v($nest) [list TABLE $tok] - } - endtablename - endtablearrayname { - #no output into the tomlish list for this token - } - startinlinetable { - puts stderr "---- decode::toml error. did not expect startinlinetable without space level change (no space level change)" - } - startquote { - switch -exact -- $newstate { - string-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "string" - set tok "" - } - quoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "dquotedkey" - set tok "" - } - XXXitable-quoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "itablequotedkey" - set tok "" - } - default { - error "---- startquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - startsquote { - switch -exact -- $newstate { - literal-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "literal" - set tok "" - } - squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - XXXitable-squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "itablesquotedkey" - set tok "" - } - multiliteral-space { - #false alarm squote returned from squote_seq pop - ::tomlish::log::debug "---- adding lone squote to own LITERALPART nextstate: $newstate (no space level change)" - #(single squote - not terminating space) - lappend v($nest) [list LITERALPART '] - #may need to be joined on pop if there are neighbouring LITERALPARTs - } - default { - error "---- startsquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - startmultiquote { - #review - puts stderr "---- got startmultiquote in state $prevstate (no space level change)" - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "stringpart" - set tok "" - } - endquote { - #nothing to do? - set tok "" - } - endsquote { - set tok "" - } - endmultiquote { - #JMN!! - set tok "" - } - string { - lappend v($nest) [list STRING $tok] ;#directly wrapped in dquotes - } - literal { - lappend v($nest) [list LITERAL $tok] ;#directly wrapped in squotes - } - double_squote { - switch -exact -- $prevstate { - keyval-value-expected { - lappend v($nest) [list LITERAL ""] - } - multiliteral-space { - #multiliteral-space to multiliteral-space - lappend v($nest) [list LITERALPART ''] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - multistring { - #review - lappend v($nest) [list MULTISTRING $tok] - } - stringpart { - lappend v($nest) [list STRINGPART $tok] ;#will not get wrapped in dquotes directly - } - multiliteral { - lappend v($nest) [LIST MULTILITERAL $tok] - } - literalpart { - lappend v($nest) [list LITERALPART $tok] ;#will not get wrapped in squotes directly - } - itablequotedkey { - - } - untyped_value { - #we can't determine the type of unquoted values (int,float,datetime,bool) until the entire token was read. - if {$tok in {true false}} { - set tag BOOL - } elseif {[::tomlish::utils::is_int $tok]} { - set tag INT - } elseif {[::tomlish::utils::is_float $tok]} { - set tag FLOAT - } elseif {[::tomlish::utils::is_datetime $tok]} { - set tag DATETIME - } else { - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. [tomlish::parse::report_line] (no space level change)" - } - lappend v($nest) [list $tag $tok] - - } - comment { - #puts stdout "----- comment token returned '$tok'------" - lappend v($nest) [list COMMENT "$tok"] - } - equal { - #we append '=' to the nest so that any surrounding whitespace is retained. - lappend v($nest) = - } - comma { - lappend v($nest) SEP - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - whitespace { - lappend v($nest) [list WS $tok] - } - continuation { - lappend v($nest) CONT - } - bom { - lappend v($nest) BOM - } - eof { - #ok - nothing more to add to the tomlish list. - #!todo - check previous tokens are complete/valid? - } - default { - error "--- unknown tokenType '$tokenType' during state $prevstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - - if {!$next_tokenType_known} { - ::tomlish::log::notice "---- tomlish::decode::toml - current tokenType:$tokenType Next token type not known" - ::tomlish::parse::set_tokenType "" - set tok "" - } - - if {$state eq "end-state"} { - break - } - - - } - - #while {$nest > 0} { - # lappend v([expr {$nest -1}]) [set v($nest)] - # incr nest -1 - #} - while {[::tomlish::parse::spacestack size] > 1} { - ::tomlish::parse::spacestack pop - lappend v([expr {$nest -1}]) [set v($nest)] - incr nest -1 - - #set parent [spacestack peek] ;#the level being appended to - #lassign $parent type state - #if {$type eq "space"} { - # - #} elseif {$type eq "buffer"} { - # lappend v([expr {$nest -1}]) {*}[set v($nest)] - #} else { - # error "invalid spacestack item: $parent" - #} - } - - } finally { - set is_parsing 0 - } - return $v(0) - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::decode ---}] -} -#decode toml to tomlish -interp alias {} tomlish::from_toml {} tomlish::decode::toml - -namespace eval tomlish::utils { - #*** !doctools - #[subsection {Namespace tomlish::utils}] - #[para] - #[list_begin definitions] - - - #tablenames (& tablearraynames) may contain irrelevant leading, trailing and interspersed whitespace - # tablenames can be made up of segments delimited by dots. .eg [ a.b . c ] - #trimmed, the tablename becomes {a.b.c} - # A segment may contain whitespace if it is quoted e.g [a . b . "c etc " ] - #ie whitespace is only irrelevant if it's outside a quoted segment - #trimmed, the tablename becomes {a.b."c etc "} - proc tablename_trim {tablename} { - set segments [tablename_split $tablename false] - set trimmed_segments [list] - foreach seg $segments { - lappend trimmed_segments [::string trim $seg " \t"] - } - return [join $trimmed_segments .] - } - - #basic generic quote matching for single and double quotes - #note for example that {[o'malley]} will return sq - as the single quote is not closed or wrapped in double quotes - proc tok_in_quotedpart {tok} { - set sLen [tcl::string::length $tok] - set quote_type "" - set had_slash 0 - for {set i 0} {$i < $sLen} {incr i} { - set c [tcl::string::index $tok $i] - if {$quote_type eq ""} { - if {$had_slash} { - #don't enter quote mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - set quote_type dq - } - sq { - set quote_type sq - } - bsl { - set had_slash 1 - } - } - } - } else { - if {$had_slash} { - #don't leave quoted mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - if {$quote_type eq "dq"} { - set quote_type "" - } - } - sq { - if {$quote_type eq "sq"} { - set quote_type "" - } - } - bsl { - set had_slash 1 - } - } - } - } - } - return $quote_type ;#dq | sq - } - - #utils::tablename_split - proc tablename_split {tablename {normalize false}} { - #we can't just split on . because we have to handle quoted segments which may contain a dot. - #eg {dog."tater.man"} - set sLen [tcl::string::length $tablename] - set segments [list] - set mode "unknown" ;#5 modes: unknown, quoted,litquoted, unquoted, syntax - #quoted is for double-quotes, litquoted is for single-quotes (string literal) - set seg "" - for {set i 0} {$i < $sLen} {incr i} { - - if {$i > 0} { - set lastChar [tcl::string::index $tablename [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $tablename $i] - - if {$c eq "."} { - switch -exact -- $mode { - unquoted { - #dot marks end of segment. - lappend segments $seg - set seg "" - set mode "unknown" - } - quoted { - append seg $c - } - unknown { - lappend segments $seg - set seg "" - } - litquoted { - append seg $c - } - default { - #mode: syntax - #we got our dot. - the syntax mode is now satisfied. - set mode "unknown" - } - } - } elseif {($c eq "\"") && ($lastChar ne "\\")} { - if {$mode eq "unknown"} { - if {[tcl::string::trim $seg] ne ""} { - #we don't allow a quote in the middle of a bare key - error "tablename_split. character '\"' invalid at this point in tablename. tablename: '$tablename'" - } - set mode "quoted" - set seg "\"" - } elseif {$mode eq "unquoted"} { - append seg $c - } elseif {$mode eq "quoted"} { - append seg $c - - if {$normalize} { - lappend segments [::tomlish::utils::unescape_string [tcl::string::range $seg 1 end-1]] - } else { - lappend segments $seg - } - - set seg "" - set mode "syntax" ;#make sure we only accept a dot or end-of-data now. - } elseif {$mode eq "litquoted"} { - append seg $c - } elseif {$mode eq "syntax"} { - error "tablename_split. expected whitespace or dot, got double quote. tablename: '$tablename'" - } - } elseif {($c eq "\'")} { - if {$mode eq "unknown"} { - append seg $c - set mode "litquoted" - } elseif {$mode eq "unquoted"} { - #single quote inside e.g o'neill - append seg $c - } elseif {$mode eq "quoted"} { - append seg $c - - } elseif {$mode eq "litquoted"} { - append seg $c - #no normalization to do - lappend segments $seg - set seg "" - set mode "syntax" - } elseif {$mode eq "syntax"} { - error "tablename_split. expected whitespace or dot, got single quote. tablename: '$tablename'" - } - - } elseif {$c in [list " " \t]} { - if {$mode eq "syntax"} { - #ignore - } else { - append seg $c - } - } else { - if {$mode eq "syntax"} { - error "tablename_split. Expected a dot separator. got '$c'. tablename: '$tablename'" - } - if {$mode eq "unknown"} { - set mode "unquoted" - } - append seg $c - } - if {$i == $sLen-1} { - #end of data - ::tomlish::log::debug "End of data: mode='$mode'" - #REVIEW - we can only end up in unquoted or syntax here? are other branches reachable? - switch -exact -- $mode { - quoted { - if {$c ne "\""} { - error "tablename_split. missing closing double-quote in a segment. tablename: '$tablename'" - } - if {$normalize} { - lappend segments [::tomlish::utils::unescape_string [tcl::string::range $seg 1 end-1]] - #lappend segments [subst -nocommands -novariables [::string range $seg 1 end-1]] ;#wrong - } else { - lappend segments $seg - } - } - litquoted { - set trimmed_seg [tcl::string::trim $seg] - if {[tcl::string::index $trimmed_seg end] ne "\'"} { - error "tablename_split. missing closing single-quote in a segment. tablename: '$tablename'" - } - lappend segments $seg - } - unquoted - unknown { - lappend segments $seg - } - syntax { - #ok - segment already lappended - } - default { - lappend segments $seg - } - } - } - } - foreach seg $segments { - set trimmed [tcl::string::trim $seg " \t"] - #note - we explicitly allow 'empty' quoted strings '' & "" - # (these are 'discouraged' but valid toml keys) - #if {$trimmed in [list "''" "\"\""]} { - # puts stderr "tablename_split. warning - Empty quoted string as tablename segment" - #} - if {$trimmed eq "" } { - error "tablename_split. Empty segment found. tablename: '$tablename' segments [llength $segments] ($segments)" - } - } - return $segments - } - - proc unicode_escape_info {slashu} { - #!todo - # validate that slashu is either a \uxxxx or \Uxxxxxxxx value of the correct length and - # is a valid 'unicode scalar value' (any Unicode code point except high-surrogate and low-surrogate code points) - # ie integers in the range 0 to D7FF16 and E00016 to 10FFFF16 inclusive - #expr {(($x >= 0) && ($x <= 0xD7FF16)) || (($x >= 0xE00016) && ($x <= 0x10FFFF16))} - if {[tcl::string::match {\\u*} $slashu]} { - set exp {^\\u([0-9a-fA-F]{4}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %4x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } - } else { - return [list err [list reason "Supplied string not of the form \\uHHHH where H in \[0-9a-fA-F\]"]] - } - } elseif {[tcl::string::match {\\U*} $slashu]} { - set exp {^\\U([0-9a-fA-F]{8}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %8x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - if {(($dec >= 0) && ($dec <= 0xD7FF16)) || (($dec >= 0xE00016) && ($dec <= 0x10FFFF16))} { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } else { - return [list err [list reason "$slashu is not within the 'unicode scalar value' ranges 0 to 0xD7FF16 or 0xE00016 to 0x10FFFF16"]] - } - } - } else { - return [list err [list reason "Supplied string not of the form \\UHHHHHHHH where H in \[0-9a-fA-F\]"]] - } - } else { - return [list err [list reason "Supplied string did not start with \\u or \\U" ]] - } - - } - - #Note that unicode characters don't *have* to be escaped. - #So if we provide a function named 'escape_string', the name implies the inverse of unescape_string which unescapes unicode \u \U values. - #- an inverse of unescape_string would encode all unicode chars unnecessarily. - #- as toml accepts a compact escape sequence for common chars such as tab,backspace,linefeed etc but also allows the full form \u009 etc - #- escape_string and unescape_string would not be reliably roundtrippable inverses anyway. - #REVIEW - provide it anyway? When would it be desirable to use? - - variable Bstring_control_map [list\ - \b {\b}\ - \n {\n}\ - \r {\r}\ - \" {\"}\ - \x1b {\e}\ - \\ "\\\\"\ - ] - #\e for \x1b seems like it might be included - v1.1?? hard to find current state of where toml is going :/ - #for a Bstring (Basic string) tab is explicitly mentioned as not being one that must be escaped. - for {set cdec 0} {$cdec <= 8} {incr cdec} { - set hhhh [format %.4X $cdec] - lappend Bstring_control_map [format %c $cdec] \\u$hhhh - } - for {set cdec [expr {0x0A}]} {$cdec <= 0x1F} {incr cdec} { - set hhhh [format %.4X $cdec] - lappend Bstring_control_map [format %c $cdec] \\u$hhhh - } - # \u007F = 127 - lappend Bstring_control_map [format %c 127] \\u007F - - #Note the inclusion of backslash in the list of controls makes this non idempotent - subsequent runs would keep encoding the backslashes! - #escape only those chars that must be escaped in a Bstring (e.g not tab which can be literal or escaped) - #for example - can be used by from_dict to produce valid Bstring data for a tomlish record - proc rawstring_to_Bstring_with_escaped_controls {str} { - #for the well known chars that have compact escape sequences allowed by toml - we choose that form over the full \u form. - #we'll use a string map with an explicit list rather than algorithmic at runtime - # - the string map is probably more performant than splitting a string, especially if it's large - variable Bstring_control_map - return [string map $Bstring_control_map $str] - } - - #review - unescape what string? Bstring vs MLBstring? - #we should be specific in the function naming here - #used by to_dict - so part of validation? - REVIEW - proc unescape_string {str} { - #note we can't just use Tcl subst because: - # it also transforms \a (audible bell) and \v (vertical tab) which are not in the toml spec. - # it would strip out backslashes inappropriately: e.g "\j" becomes just j - # it recognizes other escapes which aren't approprite e.g \xhh and octal \nnn - # it replaces \ with a single whitespace (trailing backslash) - #This means we shouldn't use 'subst' on the whole string, but instead substitute only the toml-specified escapes (\r \n \b \t \f \\ \" \uhhhh & \Uhhhhhhhh - - set buffer "" - set buffer4 "" ;#buffer for 4 hex characters following a \u - set buffer8 "" ;#buffer for 8 hex characters following a \u - - set sLen [tcl::string::length $str] - - #we need to handle arbitrarily long sequences of backslashes. \\\\\ etc - set slash_active 0 - set unicode4_active 0 - set unicode8_active 0 - - ::tomlish::log::debug "unescape_string. got len [string length str] str $str" - - #!todo - check for invalid data in the form of a raw carriage return (decimal 13) without following linefeed? - set i 0 - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $str [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $str $i] - #::tomlish::log::debug "unescape_string. got char $c" ;#too much? - - #---------------------- - #as we are 'unescaping' - should we really be testing for existing values that should have been escaped? - #this test looks incomplete anyway REVIEW - scan $c %c n - if {($n <= 31) && ($n != 9) && ($n != 10) && ($n != 13)} { - #we don't expect unescaped unicode characters from 0000 to 001F - - #*except* for raw tab (which is whitespace) and newlines - error "unescape_string. Invalid data for a toml string. Unescaped control character (decimal $n) [::tomlish::utils::string_to_slashu $c]" - } - #---------------------- - - incr i ;#must incr here because we do'returns'inside the loop - if {$c eq "\\"} { - if {$slash_active} { - append buffer "\\" - set slash_active 0 - } elseif {$unicode4_active} { - error "unescape_string. unexpected case slash during unicode4 not yet handled" - } elseif {$unicode8_active} { - error "unescape_string. unexpected case slash during unicode8 not yet handled" - } else { - # don't output anything (yet) - set slash_active 1 - } - } else { - if {$unicode4_active} { - if {[tcl::string::length $buffer4] < 4} { - append buffer4 $c - } - if {[tcl::string::length $buffer4] == 4} { - #we have a \uHHHH to test - set unicode4_active 0 - set result [tomlish::utils::unicode_escape_info "\\u$buffer4"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode8_active} { - if {[tcl::string::length $buffer8] < 8} { - append buffer8 $c - } - if {[tcl::string::length $buffer8] == 8} { - #we have a \UHHHHHHHH to test - set unicode8_active 0 - set result [tomlish::utils::unicode_escape_info "\\U$buffer8"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$slash_active} { - set slash_active 0 - set ctest [tcl::string::map {{"} dq} $c] - switch -exact -- $ctest { - dq { - set e "\\\"" - append buffer [subst -nocommand -novariable $e] - } - b - t - n - f - r { - set e "\\$c" - append buffer [subst -nocommand -novariable $e] - } - u { - set unicode4_active 1 - set buffer4 "" - } - U { - set unicode8_active 1 - set buffer8 "" - } - default { - set slash_active 0 - #review - toml spec says all other escapes are reserved - #and if they are used TOML should produce an error. - #we leave detecting this for caller for now - REVIEW - append buffer "\\" - append buffer $c - } - } - } else { - append buffer $c - } - } - } - #puts stdout "EOF 4:$unicode4_active 8:$unicode8_active slash:$slash_active" - if {$unicode4_active} { - error "End of string reached before complete unicode escape sequence \uHHHH" - } - if {$unicode8_active} { - error "End of string reached before complete unicode escape sequence \UHHHHHHHH" - } - if {$slash_active} { - append buffer "\\" - } - return $buffer - } - - #This does not have to do with unicode normal forms - which it seems toml has decided against regarding use in keys (review/references?) - #This is meant for internal use regarding ensuring we match equivalent keys which may have just been specified with different string mechanisms, - #e.g squoted vs dquoted vs barekey. - proc normalize_key {rawkey} { - set c1 [tcl::string::index $rawkey 0] - set c2 [tcl::string::index $rawkey end] - if {($c1 eq "'") && ($c2 eq "'")} { - #single quoted segment. No escapes allowed within it. - set key [tcl::string::range $rawkey 1 end-1] - } elseif {($c1 eq "\"") && ($c2 eq "\"")} { - #double quoted segment. Apply escapes. - # - set keydata [tcl::string::range $rawkey 1 end-1] ;#strip outer quotes only - #e.g key could have mix of \UXXXXXXXX escapes and unicode chars - #or mix of \t and literal tabs. - #unescape to convert all to literal versions for comparison - set key [::tomlish::utils::unescape_string $keydata] - #set key [subst -nocommands -novariables $keydata] ;#wrong. Todo - create a string escape substitution function. - } else { - set key $rawkey - } - return $key - } - - proc string_to_slashu {string} { - set rv {} - foreach c [split $string {}] { - scan $c %c cdec - if {$cdec > 65535} { - append rv {\U} [format %.8X $cdec] - } else { - append rv {\u} [format %.4X $cdec] - } - } - return $rv - } - - #'nonprintable' is conservative here because some systems (e.g windows console) are very limited in what they can display. - #This is used for display purposes only (error msgs) - proc nonprintable_to_slashu {s} { - set res "" - foreach i [split $s ""] { - scan $i %c cdec - - set printable 0 - if {($cdec>31) && ($cdec<127)} { - set printable 1 - } - if {$printable} { - append res $i - } else { - if {$cdec > 65535} { - append res \\U[format %.8X $cdec] - } else { - append res \\u[format %.4X $cdec] - } - } - } - set res - } ;# initial version from tcl wiki RS - - #check if str is valid for use as a toml bare key - #Early toml versions? only allowed letters + underscore + dash - proc is_barekey1 {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } else { - set matches [regexp -all {[a-zA-Z0-9\_\-]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters match the regexp - return 1 - } else { - return 0 - } - } - } - - #from toml.abnf in github.com/toml-lang/toml - #unquoted-key = 1*unquoted-key-char - #unquoted-key-char = ALPHA / DIGIT / %x2D / %x5F ; a-z A-Z 0-9 - _ - #unquoted-key-char =/ %xB2 / %xB3 / %xB9 / %xBC-BE ; superscript digits, fractions - #unquoted-key-char =/ %xC0-D6 / %xD8-F6 / %xF8-37D ; non-symbol chars in Latin block - #unquoted-key-char =/ %x37F-1FFF ; exclude GREEK QUESTION MARK, which is basically a semi-colon - #unquoted-key-char =/ %x200C-200D / %x203F-2040 ; from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - #unquoted-key-char =/ %x2070-218F / %x2460-24FF ; include super-/subscripts, letterlike/numberlike forms, enclosed alphanumerics - #unquoted-key-char =/ %x2C00-2FEF / %x3001-D7FF ; skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - #unquoted-key-char =/ %x2070-21FF / %x2300-24FF ; skip math operators - #unquoted-key-char =/ %x25A0-268B / %x2690-2757 ; skip box drawing, block elements, and some yin-yang symbols - #unquoted-key-char =/ %x2762-2767 / %x2776-27E5 ; skip some Dingbat punctuation - #unquoted-key-char =/ %x2801-297F ; skip some math brackets and arrows, and braille blank - #unquoted-key-char =/ %x2B00-2FFF / %x3001-D7FF ; skip various math operators and symbols, and ideographic space - #unquoted-key-char =/ %xF900-FDCF / %xFDF0-FFFD ; skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - #unquoted-key-char =/ %x10000-EFFFF ; all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - variable re_barekey - set ranges [list] - lappend ranges {a-zA-Z0-9\_\-} - lappend ranges {\u00B2} {\u00B3} {\u00B9} {\u00BC-\u00BE} ;# superscript digits, fractions - lappend ranges {\u00C0-\u00D6} {\u00D8-\u00F6} {\u00F8-\u037D} ;# non-symbol chars in Latin block - lappend ranges {\u037f-\u1FFF} ;# exclude GREEK QUESTION MARK, which is basically a semi-colon - lappend ranges {\u200C-\u200D} {\u203F-\u2040} ;# from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - lappend ranges {\u2070-\u218f} {\u2460-\u24FF} ;# include super-subscripts, letterlike/numberlike forms, enclosed alphanumerics - lappend ranges {\u2C00-\u2FEF} {\u3001-\uD7FF} ;# skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - lappend ranges {\u2070-\u21FF} {\u2300-\u24FF} ;# skip math operators - lappend ranges {\u25A0-\u268B} {\u2690-\u2757} ;# skip box drawing, block elements, and some yin-yang symbols - lappend ranges {\u2762-\u2767} {\u2776-\u27E5} ;# skip some Dingbat punctuation - lappend ranges {\u2801-\u297F} ;# skip some math brackets and arrows, and braille blank - lappend ranges {\u2B00-\u2FFF} {\u3001-\uD7FF} ;# skip various math operators and symbols, and ideographic space - lappend ranges {\uF900-\uFDCF} {\uFDF0-\uFFFD} ;# skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - lappend ranges {\U10000-\UEFFFF} ;# all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - set re_barekey {^[} - foreach r $ranges { - append re_barekey $r - } - append re_barekey {]+$} - - proc is_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } - variable re_barekey - return [regexp $re_barekey $str] - } - - #test only that the characters in str are valid for the toml specified type 'integer'. - proc int_validchars1 {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_\-\+]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - #add support for hex,octal,binary 0x.. 0o.. 0b... - proc int_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - proc is_int {str} { - set matches [regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - - # --------------------------------------- - #check for leading zeroes in non 0x 0b 0o - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - if {([tcl::string::length $check] > 1) && ([tcl::string::index $check 0] eq "0") && ([tcl::string::index $check 1] ni {o x b})} { - return 0 - } - # --------------------------------------- - - #check +,- only occur in the first position. - if {[tcl::string::last - $str] > 0} { - return 0 - } - if {[tcl::string::last + $str] > 0} { - return 0 - } - set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores - #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { - return 0 - } - #!todo - check bounds only based on some config value - #even though Tcl can handle bignums, we won't accept anything outside of toml 1.0 minimum requirements. - #presumably very large numbers would have to be supplied in a toml file as strings. - #Review - toml 1.0 only says that it must handle up to 2^63 - not that this is a max - if {$numeric_value > $::tomlish::max_int} { - return 0 - } - if {$numeric_value < $::tomlish::min_int} { - return 0 - } - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'float'. - proc float_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[eE0-9\_\-\+\.]} $str] == $numchars} { - return 1 - } else { - #only allow lower case for these special values - as per Toml 1.0 spec - if {$str ni {inf +inf -inf nan +nan -nan}} { - return 0 - } else { - return 1 - } - } - } - - proc is_float {str} { - set matches [regexp -all {[eE0-9\_\-\+\.]} $str] - #don't test for case variations - as Toml doesn't allow (whereas Tcl allows Inf NaN etc) - if {$str in {inf +inf -inf nan +nan -nan}} { - return 1 - } - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - #A leading zero is ok, but we should disallow multiple leading zeroes (same rules as toml ints) - #Toml spec also disallows leading zeros in the exponent part - #... but this seems less interoperable (some libraries generate leading zeroes in exponents) - #for now we will allow leading zeros in exponents - #!todo - configure 'strict' option to disallow? - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - set r {([0-9])*} - regexp $r $check intpart ;#intpart holds all numerals before the first .,e or E - set z {([0])*} - regexp $z $intpart leadingzeros - if {[tcl::string::length $leadingzeros] > 1} { - return 0 - } - #for floats, +,- may occur in multiple places - #e.g -2E-22 +3e34 - #!todo - check bounds ? - - #strip underscores for tcl double check - set check [tcl::string::map {_ ""} $str] - #string is double accepts inf nan +NaN etc. - if {![tcl::string::is double $check]} { - return 0 - } - - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'datetime'. - proc datetime_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[zZtT0-9\-\+\.:]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - #review - we - proc is_datetime {str} { - #e.g 1979-05-27 - #e.g 1979-05-27T00:32:00Z - #e.g 1979-05-27 00:32:00-07:00 - #e.g 1979-05-27 00:32:00+10:00 - #e.g 1979-05-27 00:32:00.999999-07:00 - - #review - #minimal datetimes? - # 2024 ok - shortest valid 4 digit year? - # 02:00 ok - # 05-17 ok - if {[string length $str] < 4} { - return 0 - } - - set matches [regexp -all {[zZtT0-9\-\+\.:]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - #!todo - use full RFC 3339 parser? - lassign [split $str T] datepart timepart - #!todo - what if the value is 'time only'? - - #Tcl's free-form clock scan (no -format option) is deprecated - # - #if {[catch {clock scan $datepart} err]} { - # puts stderr "tcl clock scan failed err:'$err'" - # return 0 - #} - - #!todo - verify time part is reasonable - } else { - return 0 - } - return 1 - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::utils ---}] -} - -namespace eval tomlish::parse { - #*** !doctools - #[subsection {Namespace tomlish::parse}] - #[para] - #[list_begin definitions] - - #This is a somewhat curly mix of a statemachine and toml-nesting-stack littered with special cases. - #The code is a pig's-nest - but it should be noted that for example trailing single double quotes in multiline strings are perhaps not so trivial to parse using more standard methods either: - # - e.g some kind of backtracking required if using an ABNF parser? - #I don't know the precise technical name for this sort of parser; probably something like "Dog's Breakfast" - #More seriously, we don't have distinct lex/parse steps - so it is basically a 'fused lexer' or 'scannerless parser' - - #It is also desirable for this system to be useful in 'interactive' use. review - would a separate lexer make this easier or harder? - - #A possible alternative more structured approach might be to use a PEG (Parsing Expression Grammar) - - - variable is_parsing 0 ;#whether we are in the middle of parsing tomlish text - - variable state - # states: - # table-space, itable-space, array-space - # value-expected, keyval-syntax, - # quoted-key, squoted-key - # string-state, literal-state, multistring... - # - # notes: - # only the -space states are also 'spaces' ie a container which is pushed/popped on the spacestack - - # - # value-expected - we also allow for leading whitespace in this state, but once a value is returned we jump to a state based on the containing space. e.g keyval-tail or array-syntax - # - #stateMatrix defines for each state, actions to take for each possible token. - #single-element actions are the name of the next state into which to transition, or a 'POPSPACE' instruction to pop a level off the spacestack and add the data to the parent container. - #dual-element actions are a push instruction and the name of the space to push on the stack. - # - PUSHSPACE is a simple push onto the spacestack, zeropoppushspace also pushes, but will first do a pop *if* the current space level is greater than zero (ie if only if not already in root table-space) - - # -- --- --- --- --- --- - #token/state naming guide - # -- --- --- --- --- --- - #tokens : underscore separated or bare name e.g newline, start_quote, start_squote - #private tokens: always have a leading underscore (These are private 'temporary state' tokens that are never returned as actual tokens e.g _start_squote_sequence - #states : always contain at least one dash e.g err-state, table-space - #instructions - # -- --- --- --- --- --- - - - #stateMatrix dict of elements mapping current state to next state based on returned tokens - # current-state {token-encountered next-state ... } - # where next-state can be a 1 or 2 element list. - #If 2 element - the first item is an instruction (ucase) - #If 1 element - it is either a lowercase dashed state name or an ucase instruction - #e.g {PUSHSPACE } or POPSPACE or SAMESPACE - - - #SAMESPACE - got to same space as parent without popping a level, but has it's own autotransition lookup - strange concept - review usecases - - variable stateMatrix - set stateMatrix [dict create] - - #xxx-space vs xxx-syntax inadequately documented - TODO - - # --------------------------------------------------------------------------------------------------------------# - # incomplete example of some state starting at table-space - # --------------------------------------------------------------------------------------------------------------# - # ( = -> value-expected) - # keyval-syntax (popped -> keyval-space -> keyval-tail) (autotransition on pop) - # keyval-space (autotransition on push ^) - # table-space (barekey^) (startdquote -> dquoted-key ^) - # --------------------------------------------------------------------------------------------------------------# - - dict set stateMatrix\ - table-space { - bom "table-space"\ - whitespace "table-space"\ - newline "table-space"\ - barekey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - squotedkey {PUSHSPACE "keyval-space" state "keyval-syntax" note ""}\ - dquotedkey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - XXXstartquote "quoted-key"\ - XXXstartsquote "squoted-key"\ - comment "table-space"\ - starttablename "tablename-state"\ - starttablearrayname "tablearrayname-state"\ - startmultiquote "err-state"\ - endquote "err-state"\ - comma "err-state"\ - eof "end-state"\ - equal "err-state"\ - cr "err-lonecr"\ - } - - #itable-space/ curly-syntax : itables - dict set stateMatrix\ - itable-space {\ - whitespace "itable-space"\ - newline "itable-space"\ - barekey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - squotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - dquotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - endinlinetable "POPSPACE"\ - XXXstartquote "quoted-key"\ - XXXstartsquote {TOSTATE "squoted-key" comment "jn-testing"}\ - comma "err-state"\ - comment "itable-space"\ - eof "err-state"\ - } - #squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate itable-space starttok {squote_seq "'"}} - - - dict set stateMatrix\ - keyval-space {\ - whitespace "keyval-syntax"\ - equal "keyval-value-expected"\ - } - - # ' = ' portion of keyval - dict set stateMatrix\ - keyval-syntax {\ - whitespace "keyval-syntax"\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal "keyval-value-expected"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - #### - dict set stateMatrix\ - keyval-value-expected {\ - whitespace "keyval-value-expected"\ - untyped_value {TOSTATE "keyval-tail" note ""}\ - squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate keyval-value-expected starttok {squote_seq "'"}}\ - startquote {TOSTATE "string-state" returnstate keyval-tail}\ - startmultiquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - startsquote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - double_squote {TOSTATE "keyval-tail" note "empty literal received when double squote occurs"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - } - #squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate keyval-process-leading-squotes starttok {squote_seq "'"}} - dict set stateMatrix\ - leading-squote-space {\ - squote_seq "POPSPACE"\ - } - #dict set stateMatrix\ - # keyval-process-leading-squotes {\ - # startsquote "literal-state"\ - # triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - # } - - dict set stateMatrix\ - keyval-tail {\ - whitespace "keyval-tail"\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - dict set stateMatrix\ - itable-keyval-syntax {\ - whitespace "itable-keyval-syntax"\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal "itable-keyval-value-expected"\ - newline "err-state"\ - eof "err-state"\ - } - dict set stateMatrix\ - itable-keyval-value-expected {\ - whitespace "itable-keyval-value-expected"\ - untyped_value {TOSTATE "itable-val-tail" note ""}\ - squote_seq_begin {PUSHSPACE "leading-squote-space" returnstate itable-keyval-value-expected starttok {squote_seq "'"}}\ - startquote {TOSTATE "string-state" returnstate itable-val-tail}\ - startmultiquote {PUSHSPACE "multistring-space" returnstate itable-val-tail}\ - startsquote {TOSTATE "literal-state" returnstate itable-val-tail note "usual way a literal is triggered"}\ - double_squote {TOSTATE "itable-val-tail" note "empty literal received when double squote occurs"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate itable-val-tail}\ - startinlinetable {PUSHSPACE "itable-space" returnstate itable-val-tail}\ - startarray {PUSHSPACE "array-space" returnstate itable-val-tail}\ - } - dict set stateMatrix\ - itable-keyval-space {\ - whitespace "itable-keyval-syntax"\ - equal {TOSTATE "itable-keyval-value-expected" note "required"}\ - } - - dict set stateMatrix\ - itable-val-tail {\ - whitespace "itable-val-tail"\ - endinlinetable "POPSPACE"\ - comma "POPSPACE"\ - XXXnewline {TOSTATE "itable-val-tail" note "itable-space ??"}\ - newline "POPSPACE"\ - comment "itable-val-tail"\ - eof "err-state"\ - } - #dict set stateMatrix\ - # itable-quoted-key {\ - # whitespace "NA"\ - # itablequotedkey {PUSHSPACE "itable-keyval-space"}\ - # newline "err-state"\ - # endquote "itable-keyval-syntax"\ - # } - #dict set stateMatrix\ - # itable-squoted-key {\ - # whitespace "NA"\ - # itablesquotedkey {PUSHSPACE "itable-keyval-space"}\ - # newline "err-state"\ - # endsquote "itable-keyval-syntax"\ - # } - - - - - - dict set stateMatrix\ - value-expected {\ - whitespace "value-expected"\ - untyped_value {"SAMESPACE" "" replay untyped_value}\ - startquote "string-state"\ - startsquote "literal-state"\ - startmultiquote {PUSHSPACE "multistring-space"}\ - triple_squote {PUSHSPACE "multiliteral-space"}\ - startinlinetable {PUSHSPACE itable-space}\ - startarray {PUSHSPACE array-space}\ - comment "err-state-value-expected-got-comment"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - - #dottedkey-space is not (currently) used within [tablename] or [[tablearrayname]] - #it is for keyval ie x.y.z = value - - #this is the state after dot - #we are expecting a complete key token or whitespace - #(initial entry to the space is by one of the keys - which will immediately go to dottedkey-space-tail) - dict set stateMatrix\ - dottedkey-space {\ - whitespace "dottedkey-space"\ - dotsep "err-state"\ - barekey "dottedkey-space-tail"\ - squotedkey "dottedkey-space-tail"\ - dquotedkey "dottedkey-space-tail"\ - newline "err-state"\ - comma "err-state"\ - comment "err-state"\ - equal "err-state"\ - } - #dottedkeyend "POPSPACE" - #equal "POPSPACE"\ - - #jmn 2025 - #we have 1 or more dottedkeys so far - need dotsep to add more, whitespace to maintain, equal to pop - dict set stateMatrix\ - dottedkey-space-tail {\ - whitespace "dottedkey-space-tail" - dotsep "dottedkey-space" - equal "POPSPACE"\ - } - - #-------------------------------------------------------------------------- - #scratch area - #from_toml {x=1} - # barekey tok - # table-space PUSHSPACE keyval-space state keyval-syntax - # - - - #-------------------------------------------------------------------------- - - - #REVIEW - #toml spec looks like heading towards allowing newlines within inline tables - #https://github.com/toml-lang/toml/issues/781 - - #2025 - appears to be valid for 1.1 - which we are targeting. - #https://github.com/toml-lang/toml/blob/main/toml.md#inline-table - - #JMN2025 - #dict set stateMatrix\ - # curly-syntax {\ - # whitespace "curly-syntax"\ - # newline "curly-syntax"\ - # barekey {PUSHSPACE "itable-keyval-space"}\ - # itablequotedkey "itable-keyval-space"\ - # endinlinetable "POPSPACE"\ - # startquote "itable-quoted-key"\ - # comma "itable-space"\ - # comment "itable-space"\ - # eof "err-state"\ - # } - #review comment "err-state" vs comment "itable-space" - see if TOML 1.1 comes out and allows comments in multiline ITABLES - #We currently allow multiline ITABLES (also with comments) in the tokenizer. - #if we want to disallow as per TOML 1.0 - we should do so when attempting to get structure? - - - #JMN REVIEW - dict set stateMatrix\ - array-space {\ - whitespace "array-space"\ - newline "array-space"\ - untyped_value "SAMESPACE"\ - startarray {PUSHSPACE "array-space"}\ - endarray "POPSPACE"\ - startmultiquote {PUSHSPACE multistring-space}\ - startinlinetable {PUSHSPACE itable-space}\ - startquote "string-state"\ - startsquote "literal-state"\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax note "seems ok 2024"}\ - comma "array-space"\ - comment "array-space"\ - eof "err-state-array-space-got-eof"\ - } - dict set stateMatrix\ - array-syntax {\ - whitespace "array-syntax"\ - newline "array-syntax"\ - untyped_value "SAMESPACE"\ - startarray {PUSHSPACE array-space}\ - endarray "POPSPACE"\ - startmultiquote {PUSHSPACE multistring-space}\ - startquote "string-state"\ - startsquote "literal-state"\ - comma "array-space"\ - comment "err-state"\ - } - - - - #quoted-key & squoted-key need to PUSHSPACE from own token to keyval-space - dict set stateMatrix\ - quoted-key {\ - whitespace "NA"\ - dquotedkey {PUSHSPACE "keyval-space"}\ - newline "err-state"\ - endquote "keyval-syntax"\ - } - - - #review - dict set stateMatrix\ - dquoted-key {\ - whitespace "NA"\ - dquotedkey "dquoted-key"\ - newline "err-state"\ - } - dict set stateMatrix\ - squoted-key {\ - whitespace "NA"\ - squotedkey "squoted-key"\ - newline "err-state"\ - } - # endsquote {PUSHSPACE "keyval-space"} - - dict set stateMatrix\ - string-state {\ - whitespace "NA"\ - string "string-state"\ - endquote "SAMESPACE"\ - newline "err-state"\ - eof "err-state"\ - } - dict set stateMatrix\ - literal-state {\ - whitespace "NA"\ - literal "literal-state"\ - endsquote "SAMESPACE"\ - newline "err-state"\ - eof "err-state"\ - } - - - #dict set stateMatrix\ - # stringpart {\ - # continuation "SAMESPACE"\ - # endmultiquote "POPSPACE"\ - # eof "err-state"\ - # } - dict set stateMatrix\ - multistring-space {\ - whitespace "multistring-space"\ - continuation "multistring-space"\ - stringpart "multistring-space"\ - newline "multistring-space"\ - endmultiquote "POPSPACE"\ - eof "err-state"\ - } - - - #only valid subparts are literalpart and newline. other whitespace etc is within literalpart - #todo - treat sole cr as part of literalpart but crlf and lf as newline - dict set stateMatrix\ - multiliteral-space {\ - literalpart "multiliteral-space"\ - newline "multiliteral-space"\ - squote_seq_begin {PUSHSPACE "trailing-squote-space" returnstate multiliteral-space starttok {squote_seq "'"}}\ - triple_squote {POPSPACE note "on popping - we do any necessary concatenation of LITERALPART items due to squote processing"}\ - double_squote {TOSTATE multiliteral-space note "short squote_seq: can occur anywhere in the space e.g emitted at end when 5 squotes occur"}\ - startsquote {TOSTATE multiliteral-space note "short squote_seq: same as double_squote - false alarm"}\ - eof "err-premature-eof-in-multiliteral-space"\ - } - - #trailing because we are looking for possible terminating ''' - but must accept '''' or ''''' and re-integrate the 1st one or 2 extra squotes - dict set stateMatrix\ - trailing-squote-space {\ - squote_seq "POPSPACE"\ - } - - - dict set stateMatrix\ - tablename-state {\ - whitespace "NA"\ - tablename {zeropoppushspace table-space}\ - tablename2 {PUSHSPACE table-space}\ - endtablename "tablename-tail"\ - comma "err-state"\ - newline "err-state"\ - } - dict set stateMatrix\ - tablearrayname-state {\ - whitespace "NA"\ - tablearrayname {zeropoppushspace table-space}\ - tablearrayname2 {PUSHSPACE table-space}\ - endtablearray "tablearrayname-tail"\ - comma "err-state"\ - newline "err-state"\ - } - - dict set stateMatrix\ - tablename-tail {\ - whitespace "tablename-tail"\ - newline "table-space"\ - comment "tablename-tail"\ - eof "end-state"\ - } - dict set stateMatrix\ - tablearrayname-tail {\ - whitespace "tablearrayname-tail"\ - newline "table-space"\ - comment "tablearrayname-tail"\ - eof "end-state"\ - } - dict set stateMatrix\ - end-state {} - - set knowntokens [list] - set knownstates [list] - dict for {state transitions} $stateMatrix { - if {$state ni $knownstates} {lappend knownstates $state} - dict for {tok instructions} $transitions { - if {$tok ni $knowntokens} {lappend knowntokens $tok} - } - } - dict set stateMatrix nostate {} - foreach tok $knowntokens { - dict set stateMatrix nostate $tok "err-nostate-received-token-$tok" - } - - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #purpose - debugging? remove? - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #build a list of 'push triggers' from the stateMatrix - # ie tokens which can push a new space onto spacestack - set push_trigger_tokens [list] - tcl::dict::for {s transitions} $stateMatrix { - tcl::dict::for {token transition_to} $transitions { - set instruction [lindex $transition_to 0] - switch -exact -- $instruction { - PUSHSPACE - zeropoppushspace { - if {$token ni $push_trigger_tokens} { - lappend push_trigger_tokens $token - } - } - } - } - } - ::tomlish::log::debug "push_trigger_tokens: $push_trigger_tokens" - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - - - - #This seems hacky... (deprecate in favour of explicit arguments to the instructions in stateMatrix?) - #spacePopTransitions, spacePushTransitions, spaceSameTransitions below for auto state redirections on POPSPACE,PUSHSPACE,SAMESPACE - - #mainly for the -space states: - #redirect to another state $c based on a state transition from $whatever to $b - # e.g "string {array-space array-syntax}" means when transitioning from string to array-space, jump to array-syntax instead. - #this is useful as we often don't know state $b. e.g when it is decided by 'POPSPACE' - - #Push to, next - #default first states when we push to these spaces - variable spacePushTransitions { - keyval-space keyval-syntax - itable-keyval-space itable-keyval-syntax - array-space array-space - table-space tablename-state - } - #itable-space itable-space - #Pop to, next - variable spacePopTransitions { - array-space array-syntax - } - #itable-space curly-syntax - #itable-keyval-space itable-val-tail - #review - #we pop to keyval-space from dottedkey-space or from value-expected? we don't always want to go to keyval-tail - #leave it out and make the POPSPACE caller explicitly specify it - #keyval-space keyval-tail - - variable spaceSameTransitions { - array-space array-syntax - } - #itable-space curly-syntax - #itable-keyval-space itable-val-tail - - - variable state_list ;#reset every tomlish::decode::toml - - namespace export tomlish toml - namespace ensemble create - - #goNextState has various side-effects e.g pushes and pops spacestack - #REVIEW - setting nest and v elements here is ugly - #todo - make neater, more single-purpose? - proc goNextState {tokentype tok currentstate} { - variable state - variable nest - variable v - - set prevstate $currentstate - - - variable spacePopTransitions - variable spacePushTransitions - variable spaceSameTransitions - - variable last_space_action "none" - variable last_space_type "none" - variable state_list - - set result "" - set starttok "" - - if {[dict exists $::tomlish::parse::stateMatrix $currentstate $tokentype]} { - set transition_to [dict get $::tomlish::parse::stateMatrix $currentstate $tokentype] - ::tomlish::log::debug "--->> goNextState tokentype:$tokentype tok:$tok currentstate:$currentstate : transition_to = $transition_to" - switch -exact -- [lindex $transition_to 0] { - POPSPACE { - spacestack pop - set parent_info [spacestack peek] - set type [dict get $parent_info type] - set parentspace [dict get $parent_info state] - - set last_space_action "pop" - set last_space_type $type - - if {[dict exists $parent_info returnstate]} { - set next [dict get $parent_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> POPSPACE transition to parent space $parentspace redirected to stored returnstate $next <<---" - } else { - ### - #review - do away with spacePopTransitions - which although useful to provide a default.. - # - involve error-prone configurations distant to the main state transition configuration in stateMatrix - if {[dict exists $::tomlish::parse::spacePopTransitions $parentspace]} { - set next [dict get $::tomlish::parse::spacePopTransitions $parentspace] - ::tomlish::log::info "--->> POPSPACE transition to parent space $parentspace redirected state to $next (spacePopTransitions)<<---" - } else { - set next $parentspace - ::tomlish::log::info "--->> POPSPACE transition to parent space $parentspace<<---" - } - } - set result $next - } - SAMESPACE { - set currentspace_info [spacestack peek] - ::tomlish::log::debug "--->> SAMESPACE got current space entry: $currentspace_info <<<<<" - set type [dict get $currentspace_info type] - set currentspace [dict get $currentspace_info state] - - if {[dict exists $currentspace_info returnstate]} { - set next [dict get $currentspace_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected to stored returnstate $next" - } else { - if {[dict exists $::tomlish::parse::spaceSameTransitions $currentspace]} { - set next [dict get $::tomlish::parse::spaceSameTransitions $currentspace] - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected state to $next (spaceSameTransitions)" - } else { - set next $currentspace - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace" - } - } - set result $next - } - zeropoppushspace { - if {$nest > 0} { - #pop back down to the root level (table-space) - spacestack pop - set parentinfo [spacestack peek] - set type [dict get $parentinfo type] - set target [dict get $parentinfo state] - - set last_space_action "pop" - set last_space_type $type - - #----- - #standard pop - set parentlevel [expr {$nest -1}] - lappend v($parentlevel) [set v($nest)] - incr nest -1 - #----- - } - #re-entrancy - - #set next [list PUSHSPACE [lindex $transition_to 1]] - set nexttokentype ${tokentype}2 ;#fake token type e.g tablename2 or tablearrayname2 - #::tomlish::log::notice "goNextState REENTRANCY. calling goNextState $nexttokentype $tokentype" - #set result [::tomlish::parse::goNextState $nexttokentype $tokentype] - ::tomlish::log::debug "--->> zeropoppushspace goNextState REENTRANCY. calling goNextState $nexttokentype $currentstate" - set transition_info [::tomlish::parse::goNextState $nexttokentype $tok $currentstate] - set result [dict get $transition_info newstate] - } - PUSHSPACE { - set original_target [dict get $transition_to PUSHSPACE] - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - if {[dict exists $transition_to starttok]} { - set starttok [dict get $transition_to starttok] - } - spacestack push [dict create type space state $original_target] - - set last_space_action "push" - set last_space_type "space" - - if {[dict exists $transition_to state]} { - #an explicit state in the pushed space was requested in the stateMatrix - override the spacePushTransition (spacePushTransitions can be deprecated if we require explicitness?) - set next [dict get $transition_to state] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next by explicit 'state' entry" - } else { - #puts $::tomlish::parse::spacePushTransitions - if {[dict exists $::tomlish::parse::spacePushTransitions $original_target]} { - set next [dict get $::tomlish::parse::spacePushTransitions $original_target] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next (spacePushTransitions) " - } else { - set next $original_target - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target" - } - } - set result $next - } - TOSTATE { - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - set result [dict get $transition_to TOSTATE] - } - default { - #simplified version of TOSTATE - set result [lindex $transition_to 0] ;#ignore everything but first word - } - } - } else { - ::tomlish::log::error "--->> No state transition defined from state $currentstate when tokentype $tokentype received" - set result "nostate" - } - lappend state_list [list tokentype $tokentype from $currentstate to $result] - set state $result - ::tomlish::log::notice "--->> STATE TRANSITION tokenType: '$tokentype' tok:$tok triggering '$currentstate' -> '$result' last_space_action:$last_space_action " - return [dict create prevstate $prevstate newstate $result space_action $last_space_action starttok $starttok] - } - - proc report_line {{line ""}} { - variable linenum - variable is_parsing - if {$is_parsing} { - if {$line eq ""} { - set line $linenum - } - return "Line Number: $line" - } else { - #not in the middle of parsing tomlish text - return nothing. - return "" - } - } - - #produce a *slightly* more readable string rep of the nest for puts etc. - proc nest_pretty1 {list} { - set prettier "{" - - foreach el $list { - if { [lindex $el 0] eq "NEWLINE"} { - append prettier "[list $el]\n" - } elseif {([llength $el] > 1) && ([lindex $el 0] in {KEY DQKEY SQKEY TABLE ARRAY})} { - append prettier [nest_pretty1 $el] - } else { - append prettier "[list $el] " - } - } - append prettier "}" - return $prettier - } - - proc set_tokenType {t} { - variable tokenType - variable tokenType_list - if {![info exists tokenType]} { - set tokenType "" - } - lappend tokenType_list $t - set tokenType $t - } - - proc switch_tokenType {t} { - variable tokenType - variable tokenType_list - lset tokenType_list end $t - set tokenType $t - } - - proc get_tokenType {} { - variable tokenType - return $tokenType - } - - proc _shortcircuit_startquotesequence {} { - variable tok - variable i - set toklen [tcl::string::length $tok] - if {$toklen == 1} { - set_tokenType "startquote" - incr i -1 - return -level 2 1 - } elseif {$toklen == 2} { - puts stderr "_shortcircuit_startquotesequence toklen 2" - set_tokenType "startquote" - set tok "\"" - incr i -2 - return -level 2 1 - } - } - - proc get_token_waiting {} { - variable token_waiting - return [lindex $token_waiting 0] - } - proc clear_token_waiting {} { - variable token_waiting - set token_waiting [list] - } - - #token_waiting is a list - but our standard case is to have only one - #in certain circumstances such as near eof we may have 2 - #the set_token_waiting function only allows setting when there is not already one waiting. - #we want to catch cases of inadvertently trying to set multiple - # - the reason being that the state transition triggered by the previous token may have invalidated the assumptions made when a token was added as waiting. - proc set_token_waiting {args} { - if {[llength $args] %2 != 0} { - error "tomlish set_token_waiting must have args of form: type value complete 0|1" - } - variable token_waiting - - if {[llength $token_waiting] && [dict get [lindex $token_waiting end] type] ne "eof"} { - #tokloop already set a token_waiting - but something (post tokloop processing?) is trying to set another - #we may need to remove the existing token_waiting and reset the tokloop index to the previous char so it's reprocessed in the possibly new context - #rather than attempt to make the right decision here - we raise an error and require the caller to check/handle it - set err "tomlish set_token_waiting already has token_waiting: [lindex $token_waiting 0]" - append err \n " - cannot add token_waiting: $args" - error $err - #set tomlish::parse::i [expr {[dict get $token_waiting startindex] -1}] - #set token_waiting [list] - } - - set waiting [dict create] - dict for {k v} $args { - switch -exact $k { - type - complete { - dict set waiting $k $v - } - value { - dict set waiting tok $v - } - startindex { - dict set waiting startindex $v - } - default { - error "tomlish set_token_waiting error - unrecognised key $k. known keys: [dict keys $args]" - } - } - } - if {![tcl::string::is boolean -strict [dict get $waiting complete]]} { - error "tomlish set_token_waiting error - 'complete' must be a boolean. got [dict get $waiting complete]" - } - if {![llength $token_waiting]} { - set token_waiting [list $waiting] - } else { - #an extra sanity-check that we don't have more than just the eof.. - if {[llength $token_waiting] > 1} { - set err "tomlish Unexpected. Existing token_waiting count > 1.\n" - foreach tw $token_waiting { - append err " $tw" \n - } - append err " - cannot add token_waiting: $waiting" - error $err - } - #last entry must be a waiting eof - set token_waiting [list $waiting [lindex $token_waiting end]] - } - return - } - - #returns 0 or 1 - #tomlish::parse::tok - #we attempt to do this without lookahead (potential use in streaming toml? for what benefit?) todo -final flag - # - the possible benefit is being able to more easily process in arbitrarily split chunks (although we would still have to watch crlf splitting ?) - # - interactive use? - - proc tok {s} { - variable nest - variable v - variable i - variable tok - variable type ;#character type - variable state ;#FSM - - - variable tokenType - variable tokenType_list - - - variable endToken - - variable lastChar - - variable braceCount - variable bracketCount - - - #------------------------------ - #Previous run found another (presumably single-char) token - #The normal case is for there to be only one dict in the list - #multiple is an exception - primarily for eof - variable token_waiting - if {[llength $token_waiting]} { - set waiting [lindex $token_waiting 0] - - set tokenType [dict get $waiting type] - set tok [dict get $waiting tok] - #todo: dict get $token_waiting complete - set token_waiting [lrange $token_waiting 1 end] - return 1 - } - #------------------------------ - - set resultlist [list] - set sLen [tcl::string::length $s] - - set slash_active 0 - set quote 0 - set c "" - set multi_dquote "" - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $s [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $s $i] - set cindex $i - set ctest [tcl::string::map {\{ lc \} rc \[ lb \] rb \" dq ' sq \\ bsl \r cr \n lf \t tab \uFEFF bom} $c] - tomlish::log::debug "- tokloop char <$ctest> index $i tokenType:$tokenType tok:<$tok>" - #puts "got char $c during tokenType '$tokenType'" - incr i ;#must incr here because we do returns inside the loop - - switch -exact -- $ctest { - # { - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {$had_slash} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - barekey { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed (see tomlish::utils::is_barekey). [tomlish::parse::report_line]" - } - whitespace { - # hash marks end of whitespace token - #do a return for the whitespace, set token_waiting - #set_token_waiting type comment value "" complete 1 - incr i -1 ;#leave comment for next run - return 1 - } - untyped_value { - #REVIEW! the spec isn't clear.. is whitespace after an int,bool etc required before comment? - #we will accept a comment marker as an immediate terminator of the untyped_value. - incr i -1 - return 1 - } - starttablename - starttablearrayname { - #fix! - error "tomlish Character '#' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare parts - but allowed in quoted parts - let tablename parser sort it out - append tok $c - } - default { - #dquotedkey, itablequotedkey, string,literal, multistring - append tok $c - } - } - } else { - switch -- $state { - multistring-space { - set_tokenType stringpart - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes#" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "#" - } - default { - #start of token if we're not in a token - set_tokenType comment - set tok "" ;#The hash is not part of the comment data - } - } - } - } - lc { - #left curly brace - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - starttablename - starttablearrayname { - #*bare* tablename can only contain letters,digits underscores - error "tomlish Invalid tablename first character \{ [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #valid in quoted parts - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - switch -exact -- $state { - itable-keyval-value-expected - keyval-value-expected - value-expected { - #switch last key to tablename?? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - array-space - array-syntax { - #nested anonymous inline table - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\{" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\{" - } - default { - error "tomlish state: '$state'. left brace case not implemented [tomlish::parse::report_line]" - } - } - } - - } - rc { - #right curly brace - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - XXXitablesquotedkey { - } - string - dquotedkey - itablequotedkey - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - starttablename - tablename { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endinlinetable value "" complete 1 startindex $cindex - return 1 - } - starttablearrayname - tablearrayname { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - default { - #end any other token - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - value-expected { - #invalid - but allow parser statemachine to report it. - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-space { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname-state problem" - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - #invalid - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - XXXcurly-syntax { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-val-tail { - set_tokenType "endinlinetable" - set tok "" - #we need to pop the keyval - and then reprocess to pop the inlinetable - so we incr -1 - incr i -1 - return 1 - } - itable-keyval-syntax { - error "tomlish endinlinetable unexpected at this point. Expecting key=val syntax [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\}" - } - multiliteral-space { - set_tokenType "literalpart" ; #review - set tok "\}" - } - default { - #JMN2024b keyval-tail? - error "tomlish state '$state'. endinlinetable case not implemented [tomlish::parse::report_line]" - } - } - } - - } - lb { - #left square bracket - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - XXXitablesquotedkey { - } - string - dquotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - starttablename { - #change the tokenType - switch_tokenType "starttablearrayname" - set tok "" ;#no output into the tomlish list for this token - #any following whitespace is part of the tablearrayname, so return now - return 1 - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - #append tok "\\[" - append tok {\[} - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - #invalid at this point - state machine should disallow table -> starttablearrayname - set_token_waiting type starttablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "\[" - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - keyval-value-expected - itable-keyval-value-expected - value-expected { - set_tokenType "startarray" - set tok "\[" - return 1 - } - table-space { - #table name - #assume it's a single bracket - but we need to wait for non-bracket to confirm it's not a tablearray - #note that a starttablearrayname token may contain whitespace between the brackets - # e.g \[ \[ - set_tokenType "starttablename" - set tok "" ;#there is no output into the tomlish list for this token - } - array-space - array-syntax { - #nested array? - set_tokenType "startarray" - set tok "\[" - return 1 - #error "state: array-space. startarray case not implemented [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\[" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\[" - } - itable-space { - #handle state just to give specific error msg - error "tomlish state: '$state'. Left square bracket invalid. Cannot start array in inline table without key. Use key=\[\] syntax. [tomlish::parse::report_line]" - } - default { - error "tomlish state: '$state'. startarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - rb { - #right square bracket - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - XXXitablesquotedkey { - } - string - dquotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - #???? - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } else { - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablename value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - tablearraynames { - #todo? - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - default { - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - value-expected { - #invalid - but allow parser statemachine to report it. - set_tokenType "endarray" - set tok "\]" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endarray" - set tok "\]" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endtablename" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname problem" - set_tokenType "endtablearray" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - set_tokenType "endarray" - set tok "\]" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "$dquotes\]" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\]" - } - default { - error "tomlish state '$state'. endarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - bsl { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - #backslash - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - #end whitespace token - incr i -1 ;#reprocess bsl in next run - return 1 - } else { - error "tomlish Unexpected backslash during whitespace. [tomlish::parse::report_line]" - } - } - literal - literalpart - squotedkey { - #never need to set slash_active true when in single quoted tokens - append tok "\\" - set slash_active 0 - } - XXXitablesquotedkey { - } - string - dquotedkey - itablequotedkey - comment { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - stringpart { - if {$slash_active} { - #assert - quotes empty - or we wouldn't have slash_active - set slash_active 0 - append tok "\\\\" - } else { - append tok $dquotes - set slash_active 1 - } - } - starttablename - starttablearrayname { - error "tomlish backslash is invalid as first character of $tokenType [tomlish::parse::report_line]" - } - tablename - tablearrayname { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - barekey { - error "tomlish Unexpected backslash during barekey. [tomlish::parse::report_line]" - } - default { - error "tomlish Backslash unexpected during tokentype: '$tokenType'. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$slash_active} { - set_tokenType "stringpart" - set tok "\\\\" - set slash_active 0 - } else { - if {$dquotes ne ""} { - set_tokenType "stringpart" - set tok $dquotes - } - set slash_active 1 - } - } - multiliteral-space { - #nothing can be escaped in multiliteral-space - not even squotes (?) review - set_tokenType "literalpart" - set tok "\\" - } - default { - error "tomlish tok error: Unexpected backslash when no token is active. [tomlish::parse::report_line]" - } - } - } - } - sq { - #single quote - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - #short squote_seq tokens are returned if active during any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - switch -- $state { - leading-squote-space { - append tok $c - if {$existingtoklen > 2} { - error "tomlish tok error: squote_seq unexpected length $existingtoklen when another received" - } elseif {$existingtoklen == 2} { - return 1 ;#return tok ''' - } - } - trailing-squote-space { - append tok $c - if {$existingtoklen == 4} { - #maxlen to be an squote_seq is multisquote + 2 = 5 - #return tok ''''' - return 1 - } - } - default { - error "tomlish tok error: squote_seq in unexpected state '$state' - expected leading-squote-space or trailing-squote-space" - } - } - } - whitespace { - #end whitespace - incr i -1 ;#reprocess sq - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - #temp token creatable only during value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - append tok $c - } - 2 { - #switch? - append tok $c - set_tokenType triple_squote - return 1 - } - default { - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_squote_sequence'" - } - } - } - literal { - #slash_active always false - #terminate the literal - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - literalpart { - #ended by ''' - but final could be '''' or ''''' (up to 2 squotes allowed directly before ending triple squote sequence) - #todo - # idea: end this literalpart (possibly 'temporarily') - # let the sq be reprocessed in the multiliteral-space to push an end-multiliteral-sequence to state stack - # upon popping end-multiliteral-sequence - stitch quotes back into this literalpart's token (if either too short - or a long ending sequence as shown above) - incr i -1 ;#throw the "'" back to loop - will be added to an squote_seq token for later processing - return 1 - } - XXXitablesquotedkey { - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - squotedkey { - ### - #set_token_waiting type endsquote value "'" complete 1 - return 1 - } - starttablename - starttablearrayname { - #!!! - incr i -1 - return 1 - } - tablename - tablearrayname { - append tok $c - } - barekey { - #not clear why o'shennanigan shouldn't be a legal barekey - but it seems not to be. - error "tomlish Unexpected single quote during barekey. [tomlish::parse::report_line]" - } - default { - append tok $c - } - } - } else { - switch -exact -- $state { - value-expected - array-space { - set_tokenType "_start_squote_sequence" - set tok "'" - } - itable-keyval-value-expected - keyval-value-expected { - set_tokenType "squote_seq_begin" - set tok "'" - return 1 - } - table-space { - #tests: squotedkey.test - set_tokenType "squotedkey" - set tok "" - } - itable-space { - #tests: squotedkey_itable.test - set_tokenType "squotedkey" - set tok "" - } - XXXitable-space { - #future - could there be multiline keys? - #this would allow arbitrary tcl dicts to be stored in toml - #probably unlikely - as it's perhaps not very 'minimal' or ergonomic for config files - set_tokenType "squote_seq_begin" - set tok "'" - return 1 - } - tablename-state { - #first char in tablename-state/tablearrayname-state - set_tokenType tablename - append tok "'" - } - tablearrayname-state { - set_tokenType tablearrayname - append tok "'" - } - literal-state { - tomlish::log::debug "- tokloop sq during literal-state with no tokentype - empty literal?" - set_tokenType literal - incr -1 - return 1 - } - multistring-space { - error "tomlish unimplemented - squote during state '$state'. [tomlish::parse::report_line]" - } - multiliteral-space { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up an squote_seq to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "squote_seq_begin" - set tok "'" - return 1 - } - dottedkey-space { - set_tokenType squotedkey - } - default { - error "tomlish unhandled squote during state '$state'. [tomlish::parse::report_line]" - } - } - } - - } - dq { - #double quote - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - set toklen [tcl::string::length $tok] - if {$toklen == 1} { - append tok $c - } elseif {$toklen == 2} { - append tok $c - #switch vs set? - set_tokenType "startmultiquote" - return 1 - } else { - error "tomlish unexpected token length $toklen in 'startquotesequence'" - } - } - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - set_tokenType "startsquote" - incr i -1 - return 1 - } - 2 { - set_tokenType "startsquote" - incr i -2 - return 1 - } - default { - error "tomlish unexpected _start_squote_sequence length $toklen" - } - } - } - literal - literalpart { - append tok $c - } - string { - if {$had_slash} { - append tok "\\" $c - } else { - #unescaped quote always terminates a string? - set_token_waiting type endquote value "\"" complete 1 startindex $cindex - return 1 - } - } - stringpart { - #sub element of multistring - if {$had_slash} { - append tok "\\" $c - } else { - #incr i -1 - - if {$multi_dquote eq "\"\""} { - set_token_waiting type endmultiquote value "\"\"\"" complete 1 startindex [expr {$cindex -2}] - set multi_dquote "" - return 1 - } else { - append multi_dquote "\"" - } - } - } - whitespace { - switch -exact -- $state { - multistring-space { - #REVIEW - if {$had_slash} { - incr i -2 - return 1 - } else { - switch -- [tcl::string::length $multi_dquote] { - 2 { - set_token_waiting type endmultiquote value "\"\"\"" complete 1 startindex [expr {$cindex-2}] - set multi_dquote "" - return 1 - } - 1 { - incr i -2 - return 1 - } - 0 { - incr i -1 - return 1 - } - } - } - } - keyval-value-expected - value-expected { - #end whitespace token and reprocess - incr i -1 - return 1 - - #if {$multi_dquote eq "\"\""} { - # set_token_waiting type startmultiquote value "\"\"\"" complete 1 - # set multi_dquote "" - # return 1 - #} else { - # #end whitespace token and reprocess - # incr i -1 - # return 1 - #} - } - table-space - itable-space { - incr i -1 - return 1 - } - default { - set_token_waiting type startquote value "\"" complete 1 startindex $cindex - return 1 - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - XXXdquotedkey - XXXitablequotedkey { - if {$had_slash} { - append tok "\\" - append tok $c - } else { - set_token_waiting type endquote value "\"" complete 1 startindex $cindex - return 1 - } - } - dquotedkey { - ### - if {$had_slash} { - append tok "\\" - append tok $c - } else { - #set_token_waiting type endsquote value "'" complete 1 - return 1 - } - } - squotedkey { - append tok $c - } - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - incr i -1 ;## - return 1 - } - default { - error "tomlish got quote during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - #$slash_active not relevant when no tokenType - #token is string only if we're expecting a value at this point - switch -exact -- $state { - keyval-value-expected - value-expected - array-space { - #!? start looking for possible multistartquote - #set_tokenType startquote - #set tok $c - #return 1 - set_tokenType "startquotesequence" ;#one or more quotes in a row - either startquote or multistartquote - set tok $c - } - itable-keyval-value-expected { - #JMN 2025 - review - set_tokenType "startquotesequence" ;#one or more quotes in a row - either startquote or multistartquote - set tok $c - } - multistring-space { - #TODO - had_slash!!! - #REVIEW - if {$had_slash} { - set_tokenType "stringpart" - set tok "\\\"" - set multi_dquote "" - } else { - if {$multi_dquote eq "\"\""} { - tomlish::log::debug "- tokloop char dq ---> endmultiquote" - set_tokenType "endmultiquote" - set tok "\"\"\"" - return 1 - #set_token_waiting type endmultiquote value "\"\"\"" complete 1 - #set multi_dquote "" - #return 1 - } else { - append multi_dquote "\"" - } - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\"" - } - XXXtable-space { - set_tokenType "startquote" - set tok $c - return 1 - } - XXXitable-space { - set_tokenType "startquote" - set tok $c - } - table-space - itable-space { - set_tokenType "dquotedkey" - set tok "" - } - tablename-state { - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - dottedkey-space { - set_tokenType dquotedkey - set tok "" - - #only if complex keys become a thing - #set_tokenType dquote_seq_begin - #set tok $c - } - default { - error "tomlish Unexpected quote during state '$state' [tomlish::parse::report_line]" - } - } - } - } - = { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart - squotedkey { - #assertion had_slash 0, multi_dquote "" - append tok $c - } - string - comment - dquotedkey - itablequotedkey { - #for these tokenTypes an = is just data. - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - whitespace { - if {$state eq "multistring-space"} { - set backlen [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backlen - return 1 - } else { - set_token_waiting type equal value = complete 1 startindex $cindex - return 1 - } - } - barekey { - #set_token_waiting type equal value = complete 1 - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character '=' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare name - but valid in quoted parts - leave for tablename parser to sort out - append tok $c - } - default { - error "tomlish unexpected = character during tokentype $tokenType. case not implemented. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok ${dquotes}= - } - multiliteral-space { - set_tokenType "literalpart" - set tok "=" - } - dottedkey-space { - set_tokenType "equal" - set tok "=" - return 1 - } - default { - set_tokenType "equal" - set tok = - return 1 - } - } - } - } - cr { - #REVIEW! - set dquotes $multi_dquote - set multi_dquote "" ;#!! - # \r carriage return - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #we have received a double cr - ::tomlish::log::warn "double cr - will generate cr token. needs testing" - set_tokenType "cr" ;#lone cr token will generally raise an error - but let state machine handle it - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal { - append tok $c - } - literalpart { - #part of MLL string (multi-line literal string) - #we need to split out crlf as a separate NEWLINE to be consistent - ::tomlish::log::warn "literalpart ended by cr - needs testing" - #return literalpart temporarily - allow cr to be reprocessed from multiliteral-space - incr i -1 - return 1 - } - stringpart { - #stringpart is a part of MLB string (multi-line basic string) - #throw back the cr - if followed by lf it will become a {NEWLINE crlf} entry within the MULTISTRING list (e.g between STRINGPART entries) - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #could in theory be valid in quoted part of name - #review - might be better just to disallow here - append tok $c - } - whitespace { - #it should technically be part of whitespace if not followed by lf - #but outside of values we are also free to map it to be another NEWLINE instead? REVIEW - incr i -1 - return 1 - } - untyped_value { - incr i -1 - return 1 - } - default { - #!todo - error out if cr inappropriate for tokenType - append tok $c - } - } - } else { - #lf may be appended if next - #review - lone cr as newline? - this is uncommon - but so is lone cr in a string(?) - set_tokenType "newline" - set tok cr - } - } - lf { - # \n newline - set dquotes $multi_dquote - set multi_dquote "" ;#!! - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #this lf is the trailing part of a crlf - append tok lf ;#assert we should now have tok "crlf" - as a previous cr is the only way to have an incomplete newline tok - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal { - #nl is not allowed *within* a literal - require multiliteral syntax for any literal containing a newline ''' ''' - #even though we terminate the literal without the closing quote here - the token_waiting newline should trigger a state error - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - literalpart { - #we allow newlines - but store them within the multiliteral as their own element - #This is a legitimate end to the literalpart - but not the whole multiliteral - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - stringpart { - if {$dquotes ne ""} { - append tok $dquotes - incr i -1 - return 1 - } else { - if {$had_slash} { - #emit the stringpart (return 1), queue the continuation, go back 1 to reprocess the lf (incr i -1) - set_token_waiting type continuation value \\ complete 1 startindex [expr {$cindex-1}] - incr i -1 - return 1 - } else { - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } - starttablename - tablename - tablearrayname - starttablearrayname { - error "tomlish Character is invalid in $tokenType. [tomlish::parse::report_line]" - } - default { - #newline ends all other tokens. - #note for string: we don't add (raw unescaped) newline to simple string. (must use multi-string for this) - #note for whitespace: - # we will use the convention that \n terminates the current whitespace even if whitespace follows - # ie whitespace is split into separate whitespace tokens at each newline - - #puts "-------------- newline lf during tokenType $tokenType" - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - if {$dquotes ne ""} { - #e.g one or 2 quotes just before nl - set_tokenType "stringpart" - set tok $dquotes - incr i -1 - return 1 - } - set_tokenType "newline" - set tok lf - return 1 - } - } - multiliteral-space { - #assert had_slash 0, multi_dquote "" - set_tokenType "newline" - set tok "lf" - return 1 - } - default { - #ignore slash? error? - set_tokenType "newline" - set tok lf - return 1 - } - } - #if {$had_slash} { - # #CONT directly before newline - allows strings_5_byteequivalent test to pass - # set_tokenType "continuation" - # set tok "\\" - # incr i -1 - # return 1 - #} else { - # set_tokenType newline - # set tok lf - # return 1 - #} - } - } - , { - set dquotes $multi_dquote - set multi_dquote "" - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - comment - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok , - } - string - dquotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - #stringpart can have up to 2 quotes too - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - literal - literalpart - squotedkey { - #assert had_slash always 0, multi_dquote "" - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - set backlen [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backlen - return 1 - } else { - set_token_waiting type comma value "," complete 1 startindex $cindex - return 1 - } - } - default { - set_token_waiting type comma value "," complete 1 startindex $cindex - if {$had_slash} {append tok "\\"} - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "$dquotes," - } - multiliteral-space { - #assert had_slash 0, multi_dquote "" - set_tokenType "literalpart" - set tok "," - } - default { - set_tokenType "comma" - set tok "," - return 1 - } - } - } - } - . { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - comment - untyped_value { - if {$had_slash} {append tok "\\"} - append tok $c - } - string - dquotedkey - itablequotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $dquotes$c - } - literal - literalpart - squotedkey { - #assert had_slash always 0, multi_dquote "" - append tok $c - } - whitespace { - switch -exact -- $state { - multistring-space { - set backchars [expr {[tcl::string::length $dquotes] + 1}] - if {$had_slash} { - incr backchars 1 - } - incr i -$backchars - return 1 - } - xxxdottedkey-space { - incr i -1 - return 1 - } - dottedkey-space-tail { - incr i -1 - return 1 - } - default { - error "tomlish Received period during tokenType 'whitespace' [tomlish::parse::report_line]" - } - } - } - starttablename - starttablearrayname { - #This would correspond to an empty table name - error "tomlish Character '.' is not allowed as first character ($tokenType). [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #subtable - split later - review - append tok $c - } - barekey { - #e.g x.y = 1 - #we need to transition the barekey to become a structured table name ??? review - #x is the tablename y is the key - set_token_waiting type dotsep value "." complete 1 startindex $cindex - return 1 - } - default { - error "tomlish Received period during tokenType '$tokenType' [tomlish::parse::report_line]" - #set_token_waiting type period value . complete 1 - #return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "$dquotes." - } - multiliteral-space { - set_tokenType "literalpart" - set tok "." - } - XXXdottedkey-space { - ### obs? - set_tokenType "dotsep" - set tok "." - return 1 - } - dottedkey-space-tail { - ### - set_tokenType "dotsep" - set tok "." - return 1 - } - default { - set_tokenType "untyped_value" - set tok "." - } - } - } - - } - " " { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - if {[tcl::string::length $tokenType]} { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - barekey { - #todo had_slash - emit token or error - #whitespace is a terminator for bare keys - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - comment { - if {$had_slash} { - append tok "\\" - } - append tok $dquotes$c - } - string - dquotedkey - itablequotedkey { - if {$had_slash} { append tok "\\" } - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - append tok $dquotes - incr i -1 - return 1 - } - } - literal - literalpart - squotedkey { - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - if {$dquotes ne ""} { - #end whitespace token - #go back by the number of quotes plus this space char - set backchars [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backchars - return 1 - } else { - append tok $c - } - } else { - append tok $c - } - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace space during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - if {$dquotes ne ""} { - set_tokenType "stringpart" - set tok $dquotes - incr i -1 - return 1 - } - set_tokenType "whitespace" - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - if {$had_slash} { - error "tomlish unexpected backslash [tomlish::parse::report_line]" - } - set_tokenType "whitespace" - append tok $c - } - } - } - } - tab { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out (?review) - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - barekey { - #whitespace is a terminator for bare keys - incr i -1 - #set_token_waiting type whitespace value $c complete 1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - squotedkey { - append tok $c - } - dquotedkey - string - comment - whitespace { - #REVIEW - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - append tok $dquotes - incr i -1 - return 1 - } - } - literal - literalpart { - append tok $c - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearraynames { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace tab during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - if {$slash_active} { - set slash_active 0 - } - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - if {$dquotes ne ""} { - set_tokenType stringpart - set tok $dquotes - incr i -1 - return 1 - } else { - set_tokenType whitespace - append tok $c - } - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "whitespace" - append tok $c - } - } - } - } - bom { - #BOM (Byte Order Mark) - ignored by token consumer - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - _start_squote_sequence { - #assert - tok will be one or two squotes only - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - literal - literalpart { - append tok $c - } - default { - set_token_waiting type bom value "\uFEFF" complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "bom" - set tok "\uFEFF" - return 1 - } - } - } - } - default { - set dquotes $multi_dquote - set multi_dquote "" ;#!! - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - squote_seq { - incr i -1 - return 1 - } - startquotesequence { - _shortcircuit_startquotesequence - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "startsquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - if {$dquotes ne ""} { - set backlen [expr {[tcl::string::length $dquotes] + 1}] - incr i -$backlen - return 1 - } else { - incr i -1 - return 1 - } - } else { - #review - incr i -1 ;#We don't have a full token to add to the token_waiting dict - so leave this char for next run. - return 1 - } - } - barekey { - if {[tomlish::utils::is_barekey $c]} { - append tok $c - } else { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed. (see tomlish::utils::is_barekey) [tomlish::parse::report_line]" - } - } - starttablename - starttablearrayname { - incr i -1 - #allow statemachine to set context for subsequent chars - return 1 - } - stringpart { - append tok $dquotes$c - } - default { - #e.g comment/string/literal/literalpart/untyped_value/starttablename/starttablearrayname/tablename/tablearrayname - append tok $c - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - table-space - itable-space { - #if no currently active token - assume another key value pair - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - XXXcurly-syntax { - puts stderr "curly-syntax - review" - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - multistring-space { - set_tokenType "stringpart" - if {$had_slash} { - #assert - we don't get had_slash and dquotes at same time - set tok \\$c - } else { - set tok $dquotes$c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - tablename-state { - set_tokenType "tablename" - set tok $c - } - tablearrayname-state { - set_tokenType "tablearrayname" - set tok $c - } - dottedkey-space { - set_tokenType barekey - set tok $c - } - default { - #todo - something like ansistring VIEW to show control chars? - set cshow [string map [list \t tab \v vt] $c] - tomlish::log::debug "- tokloop char '$cshow' setting to untyped_value while state:$state [tomlish::parse::report_line]" - set_tokenType "untyped_value" - set tok $c - } - } - } - } - } - - } - - #run out of characters (eof) - if {[tcl::string::length $tokenType]} { - #check for invalid ending tokens - #if {$state eq "err-state"} { - # error "Reached end of data whilst tokenType = '$tokenType'. INVALID" - #} - switch -exact -- $tokenType { - startquotesequence { - set toklen [tcl::string::length $tok] - if {$toklen == 1} { - #invalid - #eof with open string - error "tomlish eof reached without closing quote for string. [tomlish::parse::report_line]" - } elseif {$toklen == 2} { - #valid - #we ended in a double quote, not actually a startquoteseqence - effectively an empty string - switch_tokenType "startquote" - incr i -1 - #set_token_waiting type string value "" complete 1 - return 1 - } - } - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open literal - error "tomlish eof reached without closing single quote for string literal. [tomlish::parse::report_line]" - } - 2 { - #review - set_token_waiting type endsquote value "'" complete 1 startindex [expr {$cindex -1}] - set_tokenType "literal" - set tok "" - return 1 - } - } - } - newline { - #The only newline token that has still not been returned should have a tok value of "cr" - puts "tomlish eof reached - with incomplete newline token '$tok'" - if {$tok eq "cr"} { - #we convert lone cr to it's own "cr" token elsewhere in the document to allow statemachine to handle it. - #(which it should generally do by not handling it ie raising an error - or emitting an ERROR list in the tomlish) - #if trailing char is a lone cr - we should encode it the same way as elsewhere that is outside of values - # ie as it's own token. - switch_tokenType "cr" - return 1 - } else { - #should be unreachable - error "tomlish eof reached - with invalid newline token. value: $tok" - } - } - } - set_token_waiting type eof value eof complete 1 startindex $i ;#review - return 1 - } else { - ::tomlish::log::debug "- No current tokenType, ran out of characters, setting tokenType to 'eof' [tomlish::parse::report_line]" - set tokenType "eof" - set tok "eof" - } - return 0 - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::parse ---}] -} - -namespace eval tomlish::dict { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - - proc is_tomlish_typeval {d} { - #designed to detect {type value } e.g {type INT value 3}, {type STRING value "blah etc"} - #as a sanity check we need to avoid mistaking user data that happens to match same form - #consider x.y={type="spud",value="blah"} - #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. - #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} - } - proc is_tomlish_typeval2 {d} { - upvar ::tomlish::tags tags - expr {[lindex $d 0] eq "type" && [lindex $d 1] in $tags} - } - proc last_tomltype_posn {d} { - set last_simple -1 - set dictposn [expr {[dict size $d] -1}] - foreach k [lreverse [dict keys $d]] { - set dval [dict get $d $k] - if {[is_tomlish_typeval $dval]} { - set last_simple $dictposn - break - } - incr dictposn -1 - } - return $last_simple - } - - - #review - proc name_from_tablestack {tablestack} { - set name "" - foreach tinfo [lrange $tablestack 1 end] { - lassign $tinfo type namepart - switch -- $type { - T { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - I { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - default { - #end at first break in the leading sequence of T & I tablenames - break - } - } - } - return $name - } - -} - -tcl::namespace::eval tomlish::app { - variable applist [list encoder decoder test] - - #*** !doctools - #[subsection {Namespace tomlish::app}] - #[para] - #[list_begin definitions] - - proc decoder {args} { - #*** !doctools - #[call app::[fun decoder] [arg args]] - #[para] read toml on stdin until EOF - #[para] on error - returns non-zero exit code and writes error on stderr - #[para] on success - returns zero exit code and writes JSON encoding of the data on stdout - #[para] This decoder is intended to be compatible with toml-test - - set opts [dict merge [dict create] $args] - #fconfigure stdin -encoding utf-8 - fconfigure stdin -translation binary - #Just slurp it all - presumably we are not handling massive amounts of data on stdin. - # - even if the input is large, we probably don't gain much (aside from possible memory savings?) by attempting to process input as it arrives. - if {[catch { - set toml [read stdin] - }]} { - exit 2 ;#read error - } - try { - set j [::tomlish::toml_to_json $toml] - } on error {em} { - puts stderr "decoding failed: '$em'" - exit 1 - } - puts -nonewline stdout $j - exit 0 - } - - proc encoder {args} { - #*** !doctools - #[call app::[fun encoder] [arg args]] - #[para] read JSON on stdin until EOF - #[para] return non-zero exitcode if JSON data cannot be converted to a valid TOML representation - #[para] return zero exitcode and TOML data on stdout if JSON data can be converted. - #[para] This encoder is intended to be compatible with toml-test - - set opts [dict merge [dict create] $args] - fconfigure stdin -translation binary - if {[catch { - set json [read stdin] - }]} { - exit 2 ;#read error - } - try { - set toml [::tomlish::json_to_toml $json] - } on error {em} { - puts stderr "encoding failed: '$em'" - exit 1 - } - puts -nonewline stdout $toml - exit 0 - } - - proc test {args} { - set opts [dict merge [dict create] $args] - - package require test::tomlish - if {[dict exists $opts -suite]} { - test::tomlish::suite [dict get $opts -suite] - } - test::tomlish::run - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::app ---}] -} - -proc ::tomlish::appnames {} { - set applist [list] - foreach cmd [info commands ::tomlish::app::*] { - lappend applist [namespace tail $cmd] - } - return $applist -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::lib { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace tomlish::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 tomlish::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -if {$argc > 0} { - puts stderr "argc: $argc args: $argv" - - if {($argc == 1)} { - if {[tcl::string::tolower $argv] in {help -help h -h}} { - puts stdout "Usage: -app where appname one of:[tomlish::appnames]" - exit 0 - } else { - puts stderr "Argument '$argv' not understood. Try -help" - exit 1 - } - } - set opts [dict create] - set opts [dict merge $opts $argv] - - set opts_understood [list -app ] - if {"-app" in [dict keys $opts]} { - #Don't vet the remaining opts - as they are interpreted by each app - } else { - foreach key [dict keys $opts] { - if {$key ni $opts_understood} { - puts stderr "Option '$key' not understood" - exit 1 - } - } - } - if {[dict exists $opts -app]} { - set app [dict get $opts -app] - if {$app ni [tomlish::appnames]} { - puts stderr "app '[dict get $opts -app]' not found" - exit 1 - } - tomlish::app::$app {*}$opts - } -} - -## Ready -package provide tomlish [namespace eval tomlish { - variable pkg tomlish - variable version - set version 1.1.3 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/tomlish-1.1.4.tm b/src/bootsupport/modules/tomlish-1.1.4.tm deleted file mode 100644 index 33d5b912..00000000 --- a/src/bootsupport/modules/tomlish-1.1.4.tm +++ /dev/null @@ -1,6199 +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 tomlish 1.1.4 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin tomlish_module_tomlish 0 1.1.4] -#[copyright "2024"] -#[titledesc {tomlish toml parser}] [comment {-- Name section and table of contents description --}] -#[moddesc {tomlish}] [comment {-- Description at end of page heading --}] -#[require tomlish] -#[keywords module parsing toml configuration] -#[description] -#[para] tomlish is an intermediate representation of toml data in a tree structure (tagged lists representing type information) -#[para] The design goals are for tomlish to be whitespace and comment preserving ie byte-for byte preservation during roundtrips from toml to tomlish and back to toml -#[para] The tomlish representation can then be converted to a Tcl dict structure or to other formats such as json, -#[para] although these other formats are generally unlikely to retain whitespace or comments -#[para] The other formats also won't preserve roundtripability e.g \t and a literal tab coming from a toml file will be indistinguishable. -#[para] A further goal is to allow at least a useful subset of in-place editing operations which also preserve whitespace and comments. -#[para] e.g leaf key value editing, and table reordering/sorting, key-renaming at any level, key insertions/deletions -#[para] The API for editing (tomldoc object?) may require explicit setting of type if accessing an existing key -#[para] e.g setting a key that already exists and is a different type (especially if nested structure such as a table or array) -#[para] will need a -type option (-force ?) to force overriding with another type such as an int. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of tomlish -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by tomlish -#[list_begin itemized] - -package require Tcl 8.6- -package require struct::stack -package require logger - -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {struct::stack}] - -#limit ourselves to clear, destroy, peek, pop, push, rotate, or size (e.g v 1.3 does not implement 'get') - - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish { - namespace export {[a-z]*}; # Convention: export all lowercase - variable types - - #IDEAS: - # since get_toml produces tomlish with whitespace/comments intact: - # tomldoc object - allow (at least basic?) editing of toml whilst preserving comments & whitespace - # - setKey (set leaf only to value) how to specify type? -type option? - whole array vs index into arrays and further nested objects? - option for raw toml additions? - # - separate addKey?? - # - deleteKey (delete leaf) - # - deleteTable (delete table - if only has leaves? - option to delete with child tables?) - # - set/add Table? - position in doc based on existing tables/subtables? - - #The tomlish intermediate representation allows things such as sorting the toml document by table name or other re-ordering of tables - - # because the tables include subkeys, comments and newlines within their structure - those elements all come along with it nicely during reordering. - #The same goes for the first newline following a keyval e.g x=1 \ny=2\n\n - #The newline is part of the keyval structure so makes reordering easier - #example from_toml "a=1\nb=2\n\n\n" - # 0 = TOMLISH - # 1 = KEY a = {INT 1} {NEWLINE lf} - # 2 = NEWLINE lf - # 3 = KEY b = {INT 2} {NEWLINE lf} - # 4 = NEWLINE lf - # 5 = NEWLINE lf - - - #ARRAY is analogous to a Tcl list - #TABLE is analogous to a Tcl dict - #WS = inline whitespace - #KEY = bare key and value - #DQKEY = double quoted key and value - #SQKEY = single quoted key and value - #ITABLE = inline table (*can* be anonymous table) - # inline table values immediately create a table with the opening brace - # inline tables are fully defined between their braces, as are dotted-key subtables defined within - # No additional subtables or arrays of tables may be defined within an inline table after the ending brace - they must be entirely self-contained - - set tags [list TOMLISH ARRAY TABLE ITABLE TABLEARRAY WS NEWLINE COMMENT DOTTEDKEY KEY DQKEY SQKEY STRING STRINGPART MULTISTRING LITERAL LITERALPART MULTILITERAL INT FLOAT BOOL DATETIME] - #removed - ANONTABLE - #tomlish v1.0 should accept arbitrary 64-bit signed ints (from -2^63 to 2^63-1) - #we will restrict to this range for compatibility for now - although Tcl can handle larger (arbitrarily so?) - set min_int -9223372036854775808 ;#-2^63 - set max_int +9223372036854775807 ;#2^63-1 - - proc Dolog {lvl txt} { - #return "$lvl -- $txt" - set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] tomlish '$txt'" - puts stderr $msg - } - logger::initNamespace ::tomlish - foreach lvl [logger::levels] { - interp alias {} tomlish_log_$lvl {} ::tomlish::Dolog $lvl - log::logproc $lvl tomlish_log_$lvl - } - - #*** !doctools - #[subsection {Namespace tomlish}] - #[para] Core API functions for tomlish - #[list_begin definitions] - - proc tags {} { - return $::tomlish::tags - } - - #helper function for to_dict - proc _get_keyval_value {keyval_element} { - log::notice ">>> _get_keyval_value from '$keyval_element'<<<" - set found_value 0 - #find the value - # 3 is the earliest index at which the value could occur (depending on whitespace) - set found_sub [list] - if {[lindex $keyval_element 2] ne "="} { - error "tomlish _get_keyval_value keyval_element doesn't seem to be a properly structured { = } list" - } - - #review - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keys are lists {parenttable subtable etc} corresponding to parenttable.subtable.etc - } - - foreach sub [lrange $keyval_element 2 end] { - #note that a barekey/dquotedkey won't occur directly inside a barekey/dquotedkey - switch -exact -- [lindex $sub 0] { - STRING - LITERAL - MULTISTRING - MULTILITERAL - INT - FLOAT - BOOL - DATETIME - TABLE - ARRAY - ITABLE { - set type [lindex $sub 0] - set value [lindex $sub 1] - set found_sub $sub - incr found_value 1 - } - default {} - } - } - if {!$found_value} { - error "tomlish Failed to find value element in KEY. '$keyval_element'" - } - if {$found_value > 1} { - error "tomlish Found multiple value elements in KEY, expected exactly one. '$keyval_element'" - } - - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - #simple (non-container, no-substitution) datatype - set result [list type $type value $value] - } - STRING - STRINGPART { - set result [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL - LITERALPART { - #REVIEW - set result [list type $type value $value] - } - TABLE { - #invalid? - error "tomlish _get_keyval_value invalid to have type TABLE on rhs of =" - } - ITABLE { - #This one should not be returned as a type value structure! - # - set result [::tomlish::to_dict [list $found_sub]] - } - ARRAY { - #we need to recurse to get the corresponding dict for the contained item(s) - #pass in the whole $found_sub - not just the $value! - set prev_tablenames_info $tablenames_info - set tablenames_info [dict create] - set result [list type $type value [::tomlish::to_dict [list $found_sub]]] - set tablenames_info $prev_tablenames_info - } - MULTISTRING - MULTILITERAL { - #review - mapping these to STRING might make some conversions harder? - #if we keep the MULTI - we know we have to look for newlines for example when converting to json - #without specific types we'd have to check every STRING - and lose info about how best to map chars within it - set result [list type $type value [::tomlish::to_dict [list $found_sub]]] - } - default { - error "tomlish Unexpected value type '$type' found in keyval '$keyval_element'" - } - } - return $result - } - - proc _get_dottedkey_info {dottedkeyrecord} { - set key_hierarchy [list] - set key_hierarchy_raw [list] - if {[lindex $dottedkeyrecord 0] ne "DOTTEDKEY"} { - error "tomlish _get_dottedkey_info error. Supplied list doesn't appear to be a DOTTEDKEY (tag: [lindex $dottedkeyrecord 0])" - } - set compoundkeylist [lindex $dottedkeyrecord 1] - set expect_sep 0 - foreach part $compoundkeylist { - set parttag [lindex $part 0] - if {$parttag eq "WS"} { - continue - } - if {$expect_sep} { - if {$parttag ne "DOTSEP"} { - error "DOTTEDKEY missing dot separator between parts. '$dottedkeyrecord'" - } - set expect_sep 0 - } else { - set val [lindex $part 1] - switch -exact -- $parttag { - KEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw $val - } - DQKEY { - lappend key_hierarchy [::tomlish::utils::unescape_string $val] - lappend key_hierarchy_raw \"$val\" - } - SQKEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw "'$val'" - } - default { - error "DOTTED key unexpected part '$parttag' - ensure dot separator is between key parts. '$item'" - } - } - set expect_sep 1 - } - } - return [dict create keys $key_hierarchy keys_raw $key_hierarchy_raw] - } - - - - #to_dict is a *basic* programmatic datastructure for accessing the data. - # produce a dictionary of keys and values from a tomlish tagged list. - # to_dict is primarily for reading toml data. - #Extraneous (not within quoted sections) whitespace and comments are not preserved in this structure, - # so a roundtrip from toml to this datastructure and back to toml will lose whitespace formatting and comments. - # creating/changing toml values can be done directly on a tomlish list if preserving (or adding) formatting/comments is desired. - #A separate package 'tomlish::object' may be needed to allow easier programmatic creating/updating/deleting of data elements whilst preserving (or adding or selectively deleting/editing) such formatting. - # - - #within an ARRAY, we store a list of items such as plain dicts (possibly empty) and {type value } for simple types - #(ARRAYS can be mixed type) - #This means our dict structure should have only ARRAY and simple types which need to be in {type value } form - #A dict within an array encodeded as a type ITABLE value should also parse - but is the unpreferred form - REVIEW test? - - #Namespacing? - #ie note the difference: - #[Data] - #temp = { cpu = 79.5, case = 72.0} - # versus - #[Data] - #temps = [{cpu = 79.5, case = 72.0}] - proc to_dict {tomlish} { - package require dictn - - #keep track of which tablenames have already been directly defined, - # so we can raise an error to satisfy the toml rule: 'You cannot define any key or table more than once. Doing so is invalid' - #Note that [a] and then [a.b] is ok if there are no subkey conflicts - so we are only tracking complete tablenames here. - #we don't error out just because a previous tablename segment has already appeared. - - #Declaring, Creating, and Defining Tables - #https://github.com/toml-lang/toml/issues/795 - #(update - only Creating and Defining are relevant terminology) - - #review - #tablenames_info keys created, defined, createdby, definedby, closedby - - #consider the following 2 which are legal: - #[table] #'table' created, defined=open definedby={header table} - #x.y = 3 - #[table.x.z] #'table' defined=closed closedby={header table.x.z}, 'table.x' created, 'table.x.z' created defined=open definedby={header table.x.z} - #k= 22 - # #'table.x.z' defined=closed closedby={eof eof} - - #equivalent datastructure - - #[table] #'table' created, defined=open definedby={header table} - #[table.x] #'table' defined=closed closedby={header table.x}, 'table.x' created defined=open definedby={header table.x} - #y = 3 - #[table.x.z] #'table.x' defined=closed closedby={header table.x.z}, 'table.x.z' created defined=open definedby={header table.x.z} - #k=22 - - #illegal - #[table] #'table' created and defined=open - #x.y = 3 #'table.x' created first keyval pair defined=open definedby={keyval x.y = 3} - #[table.x.y.z] #'table' defined=closed, 'table.x' closed because parent 'table' closed?, 'table.x.y' cannot be created - #k = 22 - # - ## - we would fail on encountering table.x.y because only table and table.x are effectively tables - but that table.x is closed should be detected (?) - - #illegal - #[table] - #x.y = {p=3} - #[table.x.y.z] - #k = 22 - ## we should fail because y is an inline table which is closed to further entries - - #note: it is not safe to compare normalized tablenames using join! - # e.g a.'b.c'.d is not the same as a.b.c.d - # instead compare {a b.c d} with {a b c d} - # Here is an example where the number of keys is the same, but they must be compared as a list, not a joined string. - #'a.b'.'c.d.e' vs 'a.b.c'.'d.e' - #we need to normalize the tablenames seen so that {"x\ty"} matches {"xy"} - - - - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keyed on tablepath each of which is a list such as {config subgroup etc} (corresponding to config.subgroup.etc) - } - - - log::info "---> to_dict processing '$tomlish'<<<" - set items $tomlish - - foreach lst $items { - if {[lindex $lst 0] ni $::tomlish::tags} { - error "supplied string does not appear to be toml parsed into a tomlish tagged list. Run tomlish::decode::toml on the raw toml data to produce a tomlish list" - } - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - #ignore TOMLISH tag at beginning - set items [lrange $tomlish 1 end] - } - - set datastructure [dict create] - foreach item $items { - set tag [lindex $item 0] - #puts "...> item:'$item' tag:'$tag'" - switch -exact -- $tag { - KEY - DQKEY - SQKEY { - log::debug "---> to_dict item: processing $tag: $item" - set key [lindex $item 1] - if {$tag eq "DQKEY"} { - set key [::tomlish::utils::unescape_string $key] - } - #!todo - normalize key. (may be quoted/doublequoted) - - if {[dict exists $datastructure $key]} { - error "Duplicate key '$key'. The key already exists at this level in the toml data. The toml data is not valid." - } - - #lassign [_get_keyval_value $item] type val - set keyval_dict [_get_keyval_value $item] - dict set datastructure $key $keyval_dict - } - DOTTEDKEY { - log::debug "---> to_dict item processing $tag: $item" - set dkey_info [_get_dottedkey_info $item] - set dotted_key_hierarchy [dict get $dkey_info keys] - - #a.b.c = 1 - #table_key_hierarchy -> a b - #tleaf -> c - if {[llength $dotted_key_hierarchy] == 0} { - #empty?? probably invalid. review - #This is different to '' = 1 or ''.'' = 1 which have lengths 1 and 2 respectively - error "DOTTED key has no parts - invalid? '$item'" - } elseif {[llength $dotted_key_hierarchy] == 1} { - #dottedkey is only a key - no table component - set table_hierarchy [list] - set tleaf [lindex $dotted_key_hierarchy 0] - } else { - set table_hierarchy [lrange $dotted_key_hierarchy 0 end-1] - set tleaf [lindex $dotted_key_hierarchy end] - } - - #ensure empty tables are still represented in the datastructure - #review - this seems unnecessary? - set pathkeys [list] - foreach k $table_hierarchy { - lappend pathkeys $k - if {![dict exists $datastructure {*}$pathkeys]} { - dict set datastructure {*}$pathkeys [list] - } else { - tomlish::log::notice "to_dict datastructure at key path $pathkeys already had data: [dict get $datastructure {*}$pathkeys]" - } - } - #review? - if {[dict exists $datastructure {*}$table_hierarchy $tleaf]} { - error "Duplicate key '$table_hierarchy $tleaf'. The key already exists at this level in the toml data. The toml data is not valid." - } - - #JMN test 2025 - if {[llength $table_hierarchy]} { - dictn incr tablenames_info [list $table_hierarchy seencount] - } - - set keyval_dict [_get_keyval_value $item] - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - set t [list {*}$table_hierarchy $tleaf] - dictn incr tablenames_info [list $t seencount] - dictn set tablenames_info [list $t closed] 1 - - #review - item is an ITABLE - we recurse here without datastructure context :/ - #overwriting keys? todo ? - dict set datastructure {*}$table_hierarchy $tleaf $keyval_dict - } else { - dict set datastructure {*}$table_hierarchy $tleaf $keyval_dict - } - - } - TABLEARRAY { - set tablename [lindex $item 1] - log::debug "---> to_dict processing item TABLENAME (name: $tablename): $item" - set norm_segments [::tomlish::utils::tablename_split $tablename true] ;#true to normalize - #we expect repeated tablearray entries - each adding a sub-object to the value, which is an array/list. - - } - TABLE { - set tablename [lindex $item 1] - log::debug "---> to_dict processing item TABLE (name: $tablename): $item" - #set tablename [::tomlish::utils::tablename_trim $tablename] - set norm_segments [::tomlish::utils::tablename_split $tablename true] ;#true to normalize - - set T_DEFINED [dictn getdef $tablenames_info [list $norm_segments defined] NULL] - if {$T_DEFINED ne "NULL"} { - #our tablename e.g [a.b.c.d] declares a space to 'define' subkeys - but there has already been a definition space for this path - set msg "Table name $tablename has already been directly defined in the toml data. Invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - error $msg - } - - - set name_segments [::tomlish::utils::tablename_split $tablename 0] ;#unnormalized e.g ['a'."b".c.d] -> 'a' "b" c d - #results of tablename_split 0 are 'raw' - ie some segments may be enclosed in single or double quotes. - - - set supertable [list] - ############## - # [a.b.c.d] - # norm_segments = {a b c d} - #check a {a b} {a b c} <---- supertables of a.b.c.d - ############## - foreach normseg [lrange $norm_segments 0 end-1] { - lappend supertable $normseg - if {![dictn exists $tablenames_info [list $supertable type]]} { - #supertable with this path doesn't yet exist - if {[dict exists $datastructure {*}$supertable]} { - #There is data though - so it must have been created as a keyval - set msg "Supertable [join $supertable .] of table name $tablename already has data - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - error $msg - } else { - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - dictn set tablenames_info [list $supertable type] header - #ensure empty tables are still represented in the datastructure - dict set datastructure {*}$supertable [list] - } - } else { - #supertable has already been created - and maybe defined - but even if defined we can add subtables - } - } - #table [a.b.c.d] hasn't been defined - but may have been 'created' already by a longer tablename - # - or may have existing data from a keyval - if {![dictn exists $tablenames_info [list $norm_segments type]]} { - if {[dict exists $datastructure {*}$norm_segments]} { - set msg "Table name $tablename already has data - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - error $msg - } - #no data or previously created table - dictn set tablenames_info [list $norm_segments type] header - - #We are 'defining' this table's keys and values here (even if empty) - dict set datastructure {*}$norm_segments [list] ;#ensure table still represented in datastructure even if we add no keyvals here - } - dictn set tablenames_info [list $norm_segments defined] open - log::debug ">>> to_dict >>>>>>>>>>>>>>>>> normalized table key hierarchy : $norm_segments" - - #now add the contained elements - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - log::debug "----> todict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkey_info [_get_dottedkey_info $element] - #e.g1 keys {x.y y} keys_raw {'x.y' "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - #e.g2 keys {x.y y} keys_raw {{"x.y"} "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - set dotted_key_hierarchy [dict get $dkey_info keys] - set dkeys [lrange $dotted_key_hierarchy 0 end-1] - set leaf_key [lindex $dotted_key_hierarchy end] - - #ensure empty keys are still represented in the datastructure - set test_keys $norm_segments - foreach k $dkeys { - lappend test_keys $k - if {![dict exists $datastructure {*}$test_keys]} { - dict set datastructure {*}$test_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (DOTTEDKEY) subkey $test_keys already had data: [dict get $datastructure {*}$test_keys]" - } - } - - if {[dict exists $datastructure {*}$norm_segments {*}$dkeys $leaf_key]} { - error "Duplicate key '$norm_segments $dkeys $leaf_key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - #keyval_dict is either a {type value } - #or the result from parsing an arbitrary dict from an inline table - which could theoretically look the same at the topmost level - #punk::dict::is_tomlish_typeval can distinguish - puts stdout "to_dict>>> $keyval_dict" - dict set datastructure {*}$norm_segments {*}$dkeys $leaf_key $keyval_dict - #JMN 2025 - #lappend tablenames_info [list {*}$norm_segments {*}$dkeys] - set tkey [list {*}$norm_segments {*}$dkeys] - dictn incr tablenames_info [list $tkey seencount] - - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - #the value is either empty or or a dict structure with arbitrary (from-user-data) toplevel keys - # inner structure will contain {type value } if all leaves are not empty ITABLES - set tkey [list {*}$norm_segments {*}$dkeys $leaf_key] - #lappend tablenames_info [list {*}$norm_segments {*}$dkeys $leaf_key] - dictn incr tablenames_info [list $tkey seencount] - #if the keyval_dict is not a simple type x value y - then it's an inline table ? - #if so - we should add the path to the leaf_key as a closed table too - as it's not allowed to have more entries added. - dictn set tablenames_info [list $tkey closed] 1 - } - - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in table context. Expected only DOTTEDKEY,NEWLINE,COMMENT,WS" - } - } - } - #now make sure we add an empty value if there were no contained elements! - #!todo. - } - ITABLE { - #SEP??? - set datastructure [list] - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkey_info [_get_dottedkey_info $element] - set dotted_key_hierarchy [dict get $dkey_info keys] - set leaf_key [lindex $dotted_key_hierarchy end] - set dkeys [lrange $dotted_key_hierarchy 0 end-1] - - #ensure empty keys are still represented in the datastructure - set table_keys [list] ;#We don't know the context - next level up will have to check for key collisions? - set test_keys $table_keys - foreach k $dkeys { - lappend test_keys $k - if {![dict exists $datastructure {*}$test_keys]} { - dict set datastructure {*}$test_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (DOTTEDKEY) subkey $test_keys already had data: [dict get $datastructure {*}$test_keys]" - } - } - - if {[dict exists $datastructure {*}$table_keys {*}$dkeys $leaf_key]} { - error "Duplicate key '$table_keys $dkeys $leaf_key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - dict set datastructure {*}$table_keys {*}$dkeys $leaf_key $keyval_dict - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in ITABLE context. Expected only KEY,DQKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - } - ARRAY { - #arrays in toml are allowed to contain mixtures of types - set datastructure [list] - log::debug "--> processing array: $item" - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - STRING { - set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - ITABLE { - #anonymous table - #lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - lappend datastructure [::tomlish::to_dict [list $element]] ;#store itables within arrays as raw dicts (possibly empty) - } - TABLE { - #invalid? shouldn't be output from from_dict - but could manually be constructed as such? review - #doesn't make sense as table needs a name? - #take as synonym for ITABLE? - error "to_dict TABLE within array unexpected" - } - ARRAY - MULTISTRING - MULTILITERAL { - #set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - } - WS - SEP - NEWLINE - COMMENT { - #ignore whitespace, commas, newlines and comments - } - default { - error "Unexpected value type '$type' found in array" - } - } - } - } - MULTILITERAL { - #triple squoted string - #first newline stripped only if it is the very first element - #(ie *immediately* following the opening delims) - #All whitespace other than newlines is within LITERALPARTS - # ------------------------------------------------------------------------- - #todo - consider extension to toml to allow indent-aware multiline literals - # how - propose as issue in toml github? Use different delim? e.g ^^^ ? - #e.g - # xxx=?'''abc - # def - # etc - # ''' - # - we would like to trimleft each line to the column following the opening delim - # ------------------------------------------------------------------------- - - log::debug "---> todict processing multiliteral: $item" - set parts [lrange $item 1 end] - if {[lindex $parts 0 0] eq "NEWLINE"} { - set parts [lrange $parts 1 end] ;#skip it - } - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - switch -exact -- $type { - LITERALPART { - append stringvalue [lindex $element 1] - } - NEWLINE { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - MULTISTRING { - #triple dquoted string - log::debug "---> to_dict processing multistring: $item" - set stringvalue "" - set idx 0 - set parts [lrange $item 1 end] - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - #We use STRINGPART in the tomlish representation as a distinct element to STRING - which would imply wrapping quotes to be reinserted - switch -exact -- $type { - STRING { - #todo - do away with STRING ? - #we don't build MULTISTRINGS containing STRING - but should we accept it? - tomlish::log::warn "double quoting a STRING found in MULTISTRING - should be STRINGPART?" - append stringvalue "\"[::tomlish::utils::unescape_string [lindex $element 1]]\"" - } - STRINGPART { - append stringvalue [::tomlish::utils::unescape_string [lindex $element 1]] - } - CONT { - #When the last non-whitespace character on a line is an unescaped backslash, - #it will be trimmed along with all whitespace (including newlines) up to the next non-whitespace character or closing delimiter - # review - we allow some whitespace in stringpart elements - can a stringpart ever be all whitespace? - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - append stringvalue "\\" ;#add the sep - } else { - #skip over ws without emitting - set idx [llength $parts] - } - } else { - set parts_til_nl [lrange $parts 0 $next_nl-1] - set non_ws [lsearch -index 0 -start $idx+1 -not $parts_til_nl WS] - if {$non_ws >= 0} { - append stringvalue "\\" - } else { - #skip over ws on this line - set idx $next_nl - #then have to check each subsequent line until we get to first non-whitespace - set trimming 1 - while {$trimming && $idx < [llength $parts]} { - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - } else { - set idx [llength $parts] - } - set trimming 0 - } else { - set non_ws [lsearch -index 0 -start $idx+1 -not [lrange $parts 0 $next_nl-1] WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - set trimming 0 - } else { - set idx $next_nl - #keep trimming - } - } - } - } - } - } - NEWLINE { - #if newline is first element - it is not part of the data of a multistring - if {$idx > 0} { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - } - WS { - append stringvalue [lindex $element 1] - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - WS - COMMENT - NEWLINE { - #ignore - } - default { - error "Unexpected tag '$tag' in Tomlish list '$tomlish'" - } - } - } - return $datastructure - } - - - proc _from_dictval_tomltype {parents tablestack keys typeval} { - set type [dict get $typeval type] - set val [dict get $typeval value] - switch -- $type { - ARRAY { - set subitems [list] - foreach item $val { - lappend subitems [_from_dictval [list {*}$parents ARRAY] $tablestack $keys $item] SEP - } - if {[lindex $subitems end] eq "SEP"} { - set subitems [lrange $subitems 0 end-1] - } - return [list ARRAY {*}$subitems] - } - ITABLE { - if {$val eq ""} { - return ITABLE - } else { - return [_from_dictval [list {*}$parents ITABLE] $tablestack $keys $val] - } - } - MULTISTRING { - #value is a raw string that isn't encoded as tomlish - #create a valid toml snippet with the raw value and decode it to the proper tomlish MULTISTRING format - #We need to convert controls in $val to escape sequences - except for newlines - # - #consider an *option* to reformat for long lines? (perhaps overcomplex - byte equiv - but may fold in ugly places) - #we could use a line-length limit to decide when to put in a "line ending backslash" - #and even format it with a reasonable indent so that proper CONT and WS entries are made (?) REVIEW - # - #TODO - set tomlpart "x=\"\"\"\\\n" - append tomlpart $val "\"\"\"" - set tomlish [tomlish::decode::toml $tomlpart] - #e.g if val = " etc\nblah" - #TOMLISH {DOTTEDKEY {{KEY x}} = {MULTISTRING CONT {NEWLINE LF} {WS { }} {STRINGPART etc} {NEWLINE lf} {STRINGPART blah} } } - #lindex 1 3 is the MULTISTRING tomlish list - return [lindex $tomlish 1 3] - } - MULTILITERAL { - #MLL string can contain newlines - but still no control chars - #todo - validate - set tomlpart "x='''\n" - append tomlpart $val ''' - set tomlish [tomlish::decode::toml $tomlpart] - return [lindex $tomlish 1 3] - } - LITERAL { - #from v1.0 spec - "Control characters other than tab are not permitted in a literal string" - #(This rules out raw ANSI SGR - which is somewhat restrictive - but perhaps justified for a config format - # as copy-pasting ansi to a config value is probably not always wise, and it's not something that can be - # easily input via a text editor. ANSI can go in Basic strings using the \e escape if that's accepted v1.1?) - #we could choose to change the type to another format here when encountering invalid chars - but that seems - #like too much magic. We elect to error out and require the dict to have valid data for the types it specifies. - if {[string first ' $val] >=0} { - error "_from_dictval_tomltype error: single quote found in LITERAL - cannot encode dict to TOML-VALID TOMLISH" - } - #detect control chars other than tab - #for this we can use rawstring_to_Bstring_with_escaped_controls - even though this isn't a Bstring - #we are just using the map to detect a difference. - set testval [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val] - if {$testval ne $val} { - #some escaping would have to be done if this value was destined for a Bstring... - #therefor this string has controls and isn't suitable for a LITERAL according to the specs. - error "_from_dictval_tomltype error: control chars (other than tab) found in LITERAL value - cannot encode dict to TOML-VALID TOMLISH" - } - return [list LITERAL $val] - } - STRING { - return [list STRING [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val]] - } - INT { - if {![::tomlish::utils::is_int $val]} { - error "_from_dictval_tomltype error: bad INT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list INT $val] - } - FLOAT { - if {![::tomlish::utils::is_float $val]} { - error "_from_dictval_tomltype error: bad FLOAT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list FLOAT $val] - } - default { - if {$type ni [::tomlish::tags]} { - error "_from_dictval_tomltype error: Unrecognised typename '$type' in {type value } - cannot encode dict to TOML-VALID TOMLISH" - } - return [list $type $val] - } - } - } - - proc _from_dictval {parents tablestack keys vinfo} { - set k [lindex $keys end] - if {[regexp {\s} $k] || [string first . $k] >= 0} {} - if {![::tomlish::utils::is_barekey $k]} { - #Any dot in the key would have been split by to_dict - so if it's present here it's part of this key - not a level separator! - #requires quoting - #we'll use a basic mechanism for now to determine the type of quoting - whether it has any single quotes or not. - #todo - more? - #For keys - we currently (2025) are only allowed barekeys,basic strings and literal strings. (no multiline forms) - if {[string first ' $k] >=0} { - #basic string - } else { - #literal string - set K_PART [list SQKEY $k] - } - } else { - set K_PART [list KEY $k] - } - puts stderr "---parents:'$parents' keys:'$keys' vinfo: $vinfo---" - puts stderr "---tablestack: $tablestack---" - set result [list] - set lastparent [lindex $parents end] - if {$lastparent in [list "" do_inline]} { - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - set type [dict get $vinfo type] - #treat ITABLE differently? - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result DOTTEDKEY [list $K_PART {WS { }}] = {WS { }} $sublist {NEWLINE lf} - } else { - #set result [list TABLE $k {NEWLINE lf}] - if {$vinfo ne ""} { - - #set result [list DOTTEDKEY [list [list KEY $k]] = ] - #set records [list ITABLE] - - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - - if {$lastparent eq "do_inline"} { - set result [list DOTTEDKEY [list $K_PART] =] - set records [list ITABLE] - } else { - #review - quoted k ?? - set result [list TABLE $k {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $k]] - set records [list] - } - - - - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - dict for {vk vv} $vinfo { - if {[regexp {\s} $vk] || [string first . $vk] >= 0} { - set VK_PART [list SQKEY $vk] - } else { - set VK_PART [list KEY $vk] - } - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART {WS { }}] = {WS { }} $sublist] - } else { - if {$vv eq ""} { - #experimental - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline KEY $vk (tablestack:$tablestack)" - #set tname [tomlish::dict::name_from_tablestack [list {*}$tablestack [list T $vk]]] - set tname [join [list {*}$keys $vk] .] - set record [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - } else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - } - } else { - if { 0 } { - #experiment.. sort of getting there. - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline2 KEYS [list {*}$keys $vk] (tablestack:$tablestack)" - set tname [join [list {*}$keys $vk] .] - set record [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - - #review - todo? - set dottedkey_value [_from_dictval [list {*}$parents TABLE] $tablestack [list {*}$keys $vk] $vv] - lappend record {*}$dottedkey_value - - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - } - if {$dictidx != $lastidx} { - #lappend record SEP - if {$lastparent eq "do_inline"} { - lappend record SEP - } else { - lappend record {NEWLINE lf} - } - } - lappend records $record - incr dictidx - } - if {$lastparent eq "do_inline"} { - lappend result $records {NEWLINE lf} - } else { - lappend result {*}$records {NEWLINE lf} - } - } else { - if {$lastparent eq "do_inline"} { - lappend result DOTTEDKEY [list [list KEY $k]] = ITABLE {NEWLINE lf} - } else { - lappend result TABLE $k {NEWLINE lf} - } - } - } - } else { - #lastparent is not toplevel "" or "do_inline" - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result {*}$sublist - } else { - if {$lastparent eq "TABLE"} { - #review - dict for {vk vv} $vinfo { - set dottedkey_value [_from_dictval [list {*}$parents DOTTEDKEY] $tablestack [list {*}$keys $vk] $vv] - lappend result [list DOTTEDKEY [list [list KEY $vk]] = $dottedkey_value {NEWLINE lf}] - } - } else { - if {$vinfo ne ""} { - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - set sub [list] - #REVIEW - #set result $lastparent ;#e.g sets ITABLE - set result ITABLE - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - dict for {vk vv} $vinfo { - if {[regexp {\s} $vk] || [string first . $vk] >=0} { - set VK_PART [list SQKEY $vk] - } else { - set VK_PART [list KEY $vk] - } - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART] = $sublist] - } else { - if {$vv eq ""} { - #can't just uninline at this level - #we need a better method to query main dict for uninlinability at each level - # (including what's been inlined already) - #if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - # puts stderr "_from_dictval uninline2 KEY $keys" - # set tname [join [list {*}$keys $vk] .] - # set record [list TABLE $tname {NEWLINE lf}] - # set tablestack [list {*}$tablestack [list T $vk]] - #} else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - #} - } else { - #set sub [_from_dictval ITABLE $vk $vv] - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - #set record [list DOTTEDKEY [list $VK_PART] = ITABLE $dottedkey_value] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - if {$dictidx != $lastidx} { - lappend record SEP - } - lappend result $record - incr dictidx - } - } else { - puts stderr "table x-1" - lappend result DOTTEDKEY [list $K_PART] = ITABLE - } - } - } - } - return $result - } - - - proc from_dict {d} { - #consider: - # t1={a=1,b=2} - # x = 1 - #If we represent t1 as an expanded table we get - # [t1] - # a=1 - # b=2 - # x=1 - # --- which is incorrect - as x was a toplevel key like t1! - #This issue doesn't occur if x is itself an inline table - # t1={a=1,b=2} - # x= {no="problem"} - # - # (or if we were to reorder x to come before t1) - - #As the dictionary form doesn't distinguish the structure used to create tables {[table1]\nk=v} vs inline {table1={k=v}} - #Without a solution, from_dict would have to always produce the inline form for toplevel tables unless we allowed re-ordering, - #which is unpreferred here. - - #A possible solution: - #scan the top level to see if all (trailing) elements are themselves dicts - # (ie not of form {type XXX value yyy}) - # - # A further point is that if all root level values are at the 'top' - we can treat lower table-like structures as {[table]} elements - #ie we don't need to force do_inline if all the 'simple' keys are before any compound keys - - #set root_has_values 0 - #approach 1) - the naive approach - forces inline when not always necessary - #dict for {k v} $d { - # if {[llength $v] == 4 && [lindex $v 0] eq "type"} { - # set root_has_values 1 - # break - # } - #} - - - #approach 2) - track the position of last {type x value y} in the dictionary built by to_dict - # - still not perfect. Inlines dotted tables unnecessarily - #This means from_dict doesn't produce output optimal for human editing. - set last_simple [tomlish::dict::last_tomltype_posn $d] - - - ## set parent "do_inline" ;#a value used in _from_dictval to distinguish from "" or other context based parent values - #Any keys that are themselves tables - will need to be represented inline - #to avoid reordering, or incorrect assignment of plain values to the wrong table. - - ## set parent "" - #all toplevel keys in the dict structure can represent subtables. - #we are free to use {[tablename]\n} syntax for toplevel elements. - - - set tomlish [list TOMLISH] - set dictposn 0 - set tablestack [list [list T root]] ;#todo - dict for {t tinfo} $d { - if {$last_simple > $dictposn} { - set parents [list do_inline] - } else { - set parents [list ""] - } - set keys [list $t] - set trecord [_from_dictval $parents $tablestack $keys $tinfo] - lappend tomlish $trecord - incr dictposn - } - return $tomlish - } - - proc json_to_toml {json} { - #*** !doctools - #[call [fun json_to_toml] [arg json]] - #[para] - - set tomlish [::tomlish::from_json $json] - set toml [::tomlish::to_toml $tomlish] - } - - #TODO use huddle? - proc from_json {json} { - #set jstruct [::tomlish::json_struct $json] - #return [::tomlish::from_json_struct $jstruct] - package require huddle - package require huddle::json - set h [huddle::json::json2huddle parse $json] - - } - - proc from_json_struct {jstruct} { - package require fish::json_toml - return [fish::json_toml::jsonstruct2tomlish $jstruct] - } - - proc toml_to_json {toml} { - set tomlish [::tomlish::from_toml $toml] - return [::tomlish::get_json $tomlish] - } - - proc get_json {tomlish} { - package require fish::json - set d [::tomlish::to_dict $tomlish] - #return [::tomlish::dict_to_json $d] - return [fish::json::from "struct" $d] - } - - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -namespace eval tomlish::build { - #STRING,INT,FLOAT,BOOL, DATETIME - simple wrappers for completeness - # take a value of the appropriate type and wrap as a tomlish tagged item - proc STRING {s} { - return [list STRING [::tomlish::utils::rawstring_to_Bstring_with_escaped_controls $s]] - } - proc LITERAL {litstring} { - error todo - } - - proc INT {i} { - #whole numbers, may be prefixed with a + or - - #Leading zeros are not allowed - #Hex,octal binary forms are allowed (toml 1.0) - #We will error out on encountering commas, as commas are interpreted differently depending on locale (and don't seem to be supported in the toml spec anyway) - #!todo - Tcl can handle bignums - bigger than a 64bit signed long as specified in toml. - # - We should probably raise an error for number larger than this and suggest the user supply it as a string? - if {[tcl::string::last , $i] > -1} { - error "Unable to interpret '$i' as an integer. Use underscores if you need a thousands separator [::tomlish::parse::report_line]" - } - if {![::tomlish::utils::int_validchars $i]} { - error "Unable to interpret '$i' as an integer. Only 0-9 + 1 _ characters are acceptable. [::tomlish::parse::report_line]" - } - - if {[::tomlish::utils::is_int $i]} { - return [list INT $i] - } else { - error "'$i' is not a valid integer as per the Toml spec. [::tomlish::parse::report_line]" - } - - } - - proc FLOAT {f} { - #convert any non-lower case variants of special values to lowercase for Toml - if {[::tcl::string::tolower $f] in {nan +nan -nan inf +inf -inf}} { - return [list FLOAT [tcl::string::tolower $f]] - } - if {[::tomlish::utils::is_float $f]} { - return [list FLOAT $f] - } else { - error "Unable to interpret '$f' as Toml float. Check your input, or check that tomlish is able to handle all Toml floats properly [::tomlish::parse::report_line]" - } - } - - proc DATETIME {str} { - if {[::tomlish::utils::is_datetime $str]} { - return [list DATETIME $str] - } else { - error "Unable to interpret '$str' as Toml datetime. Check your input, or check that tomlish is able to handle all Toml datetimes properly [::tomlish::parse::report_line]" - } - } - - proc BOOLEAN {b} { - #convert any Tcl-acceptable boolean to boolean as accepted by toml - lower case true/false - if {![tcl::string::is boolean -strict $b]} { - error "Unable to convert '$b' to Toml boolean true|false. [::tomlish::parse::report_line]" - } else { - if {$b && 1} { - return [::list BOOL true] - } else { - return [::list BOOL false] - } - } - } - - #REVIEW - #Take tablename followed by - # a) *tomlish* name-value pairs e.g table mydata [list KEY item11 = [list STRING "test"]] {KEY item2 = [list INT 1]} - # (accept also key value {STRING }) - # b) simple 2-element tcl lists being name & *simple* value pairs for which basic heuristics will be used to determine types - proc _table {name args} { - set pairs [list] - foreach t $args { - if {[llength $t] == 4} { - if {[tcl::string::tolower [lindex $t 0]] ne "key" || [tcl::string::tolower [lindex $t 2]] ni "= value"} { - error "Only items tagged as KEY = currently accepted as name-value pairs for table command" - } - lassign $t _k keystr _eq valuepart - if {[llength $valuepart] != 2} { - error "supplied value must be typed. e.g {INT 1} or {STRING test}" - } - lappend pairs [list KEY $keystr = $valuepart] - } elseif {[llength $t] == 2} { - #!todo - type heuristics - lassign $t n v - lappend pairs [list KEY $n = [list STRING $v]] - } else { - error "'KEY = { toml but - # the first newline is not part of the data. - # we elect instead to maintain a basic LITERALPART that must not contain newlines.. - # and to compose MULTILITERAL of multiple NEWLINE LITERALPART parts, - #with the datastructure representation dropping the first newline (if immediately following opening delim) when building the value. - set literal "" - foreach part [lrange $item 1 end] { - append literal [::tomlish::encode::tomlish [list $part] $nextcontext] - } - append toml '''$literal''' - } - INT - - BOOL - - FLOAT - - DATETIME { - append toml [lindex $item 1] - } - INCOMPLETE { - error "cannot process tomlish term tagged as INCOMPLETE" - } - COMMENT { - append toml "#[lindex $item 1]" - } - BOM { - #Byte Order Mark may appear at beginning of a file. Needs to be preserved. - append toml "\uFEFF" - } - default { - error "Not a properly formed 'tomlish' taggedlist.\n '$list'\n Unknown tag '[lindex $item 0]'. See output of \[tomlish::tags\] command." - } - } - - } - return $toml - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::encode ---}] -} -#fish toml from tomlish - -#(encode tomlish as toml) -interp alias {} tomlish::to_toml {} tomlish::encode::tomlish - -# - - -namespace eval tomlish::decode { - #*** !doctools - #[subsection {Namespace tomlish::decode}] - #[para] - #[list_begin definitions] - - #return a Tcl list of tomlish tokens - #i.e get a standard list of all the toml terms in string $s - #where each element of the list is a *tomlish* term.. i.e a specially 'tagged' Tcl list. - #(simliar to a tcl 'Huddle' - but also supporting whitespace preservation) - # ---------------------------------------------------------------------------------------------- - # NOTE: the production of tomlish from toml source doesn't indicate the toml source was valid!!! - # e.g we deliberately don't check certain things such as duplicate table declarations here. - # ---------------------------------------------------------------------------------------------- - #Part of the justification for this is that as long as the syntax is toml shaped - we can load files which violate certain rules and allow programmatic manipulation. - # (e.g perhaps a toml editor to highlight violations for fixing) - # A further stage is then necessary to load the tomlish tagged list into a data structure more suitable for efficient query/reading. - # e.g dicts or an object oriented structure - #Note also - *no* escapes in quoted strings are processed. This is up to the datastructure stage - #e.g to_dict will substitute \r \n \uHHHH \UHHHHHHH etc - #This is important for tomlish to maintain the ability to perform competely lossless round-trips from toml to tomlish and back to toml. - # (which is handy for testing as well as editing some part of the structure with absolutely no effect on other parts of the document) - #If we were to unescape a tab character for example - # - we have no way of knowing if it was originally specified as \t \u0009 or \U00000009 or directly as a tab character. - # For this reason, we also do absolutely no line-ending transformations based on platform. - # All line-endings are maintained as is, and even a file with mixed lf crlf line-endings will be correctly interpreted and can be 'roundtripped' - - proc toml {args} { - #*** !doctools - #[call [fun toml] [arg arg...]] - #[para] return a Tcl list of tomlish tokens - - set s [join $args \n] - - namespace upvar ::tomlish::parse is_parsing is_parsing - set is_parsing 1 - - if {[info command ::tomlish::parse::spacestack] eq "::tomlish::parse::spacestack"} { - tomlish::parse::spacestack destroy - } - struct::stack ::tomlish::parse::spacestack - - namespace upvar ::tomlish::parse last_space_action last_space_action - namespace upvar ::tomlish::parse last_space_type last_space_type - - namespace upvar ::tomlish::parse tok tok - set tok "" - - namespace upvar ::tomlish::parse type type - namespace upvar ::tomlish::parse tokenType tokenType - ::tomlish::parse::set_tokenType "" - namespace upvar ::tomlish::parse tokenType_list tokenType_list - set tokenType [list] ;#Flat (un-nested) list of tokentypes found - - namespace upvar ::tomlish::parse lastChar lastChar - set lastChar "" - - - set result "" - namespace upvar ::tomlish::parse nest nest - set nest 0 - - namespace upvar ::tomlish::parse v v ;#array keyed on nest level - - - set v(0) {TOMLISH} - array set s0 [list] ;#whitespace data to go in {SPACE {}} element. - set parentlevel 0 - - namespace upvar ::tomlish::parse i i - set i 0 - - namespace upvar ::tomlish::parse state state - - namespace upvar ::tomlish::parse braceCount braceCount - set barceCount 0 - namespace upvar ::tomlish::parse bracketCount bracketCount - set bracketCount 0 - - set sep 0 - set r 1 - namespace upvar ::tomlish::parse token_waiting token_waiting - set token_waiting [dict create] ;#if ::tok finds a *complete* second token during a run, it will put the 2nd one here to be returned by the next call. - - - set state "table-space" - ::tomlish::parse::spacestack push {type space state table-space} - namespace upvar ::tomlish::parse linenum linenum;#'line number' of input data. (incremented for each literal linefeed - but not escaped ones in data) - set linenum 1 - - set ::tomlish::parse::state_list [list] - try { - while {$r} { - set r [::tomlish::parse::tok $s] - #puts stdout "got tok: '$tok' while parsing string '$s' " - set next_tokenType_known 0 ;#whether we begin a new token here based on what terminated the token result of 'tok' - - - #puts "got token: '$tok' tokenType='$tokenType'. while v($nest) = [set v($nest)]" - #puts "-->tok: $tok tokenType='$tokenType'" - set prevstate $state - set transition_info [::tomlish::parse::goNextState $tokenType $tok $state] - #review goNextState could perform more than one space_action - set space_action [dict get $transition_info space_action] - set newstate [dict get $transition_info newstate] ;#use of 'newstate' vs 'state' makes code clearer below - - if {[tcl::string::match "err-*" $state]} { - ::tomlish::log::warn "---- State error in state $prevstate for tokenType: $tokenType token value: $tok. $state aborting parse. [tomlish::parse::report_line]" - lappend v(0) [list ERROR tokentype $tokenType state $prevstate to $state leveldata [set v($nest)]] - return $v(0) - } - # --------------------------------------------------------- - #NOTE there may already be a token_waiting at this point - #set_token_waiting can raise an error here, - # in which case the space_action branch needs to be rewritten to handle the existing token_waiting - # --------------------------------------------------------- - - if {$space_action eq "pop"} { - #pop_trigger_tokens: newline tablename endarray endinlinetable - #note a token is a pop trigger depending on context. e.g first newline during keyval is a pop trigger. - set parentlevel [expr {$nest -1}] - set do_append_to_parent 1 ;#most tokens will leave this alone - but some like tentative_accum_squote need to do their own append - switch -exact -- $tokenType { - tentative_accum_squote { - #should only apply within a multiliteral - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the xxx-squote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-squote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-squote-space { - } - default { - error "--- unexpected popped due to tentative_accum_squote but came from state '$prevstate' should have been trailing-squote-space" - } - } - switch -- $tok { - ' { - tomlish::parse::set_token_waiting type single_squote value $tok complete 1 startindex [expr {$i -1}] - } - '' { - #review - we should perhaps return double_squote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_squote value "" complete 1 startindex [expr {$i - 2}] - } - ''' { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 3}] - } - '''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left squote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]'" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "'"] - } - MULTILITERAL { - #empty - lappend v($parentlevel) [list LITERALPART "'"] - } - default { - error "--- don't know how to integrate extra trailing squote with data $v($parentlevel)" - } - } - } - ''''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 squotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]''" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "''"] - } - MULTILITERAL { - lappend v($parentlevel) [list LITERALPART "''"] - } - default { - error "--- don't know how to integrate extra trailing 2 squotes with data $v($parentlevel)" - } - } - } - } - } - triple_squote { - #presumably popping multiliteral-space - ::tomlish::log::debug "---- triple_squote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTILITERAL { - lappend merged $part - } - LITERALPART { - if {$lasttype eq "LITERALPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_squote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - tentative_accum_dquote { - #should only apply within a multistring - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the trailing-dquote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-dquote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-dquote-space { - } - default { - error "--- unexpected popped due to tentative_accum_dquote but came from state '$prevstate' should have been trailing-dquote-space" - } - } - switch -- $tok { - {"} { - tomlish::parse::set_token_waiting type single_dquote value $tok complete 1 startindex [expr {$i -1}] - } - {""} { - #review - we should perhaps return double_dquote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_dquote value "" complete 1 startindex [expr {$i - 2}] - } - {"""} { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 3}] - } - {""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left dquote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {"}] - } - MULTISTRING { - #empty - lappend v($parentlevel) [list STRINGPART {"}] - } - default { - error "--- don't know how to integrate extra trailing dquote with data $v($parentlevel)" - } - } - } - {"""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 dquotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {""}] - } - MULTISTRING { - lappend v($parentlevel) [list STRINGPART {""}] - } - default { - error "--- don't know how to integrate extra trailing 2 dquotes with data $v($parentlevel)" - } - } - } - } - } - triple_dquote { - #presumably popping multistring-space - ::tomlish::log::debug "---- triple_dquote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTISTRING { - lappend merged $part - } - STRINGPART { - if {$lasttype eq "STRINGPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - CONT - WS { - lappend merged $part - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_dquote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - equal { - #pop caused by = - switch -exact -- $prevstate { - dottedkey-space { - tomlish::log::debug "---- equal ending dottedkey-space for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - dottedkey-space-tail { - #experiment? - tomlish::log::debug "---- equal ending dottedkey-space-tail for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - } - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - tablename { - #note: a tablename only 'pops' if we are greater than zero - error "---- tablename pop should already have been handled as special case zeropoppushspace in goNextState" - } - tablearrayname { - #!review - tablearrayname different to tablename regarding push/pop? - #note: a tablename only 'pops' if we are greater than zero - error "---- tablearrayname pop should already have been handled as special case zeropoppushspace in goNextState" - } - endarray { - #nothing to do here. - } - comma { - #comma for inline table will pop the keyvalue space - lappend v($nest) "SEP" - } - endinlinetable { - ::tomlish::log::debug "---- endinlinetable for last_space_action pop" - } - default { - error "---- unexpected tokenType '$tokenType' for last_space_action 'pop'" - } - } - if {$do_append_to_parent} { - #e.g tentative_accum_squote does it's own appends as necessary - so won't get here - lappend v($parentlevel) [set v($nest)] - } - - incr nest -1 - - } elseif {$last_space_action eq "push"} { - set prevnest $nest - incr nest 1 - set v($nest) [list] - # push_trigger_tokens: barekey dquotedkey startinlinetable startarray tablename tablearrayname - - - switch -exact -- $tokenType { - tentative_trigger_squote - tentative_trigger_dquote { - #### this startok will always be tentative_accum_squote/tentative_accum_dquote starting with one accumulated squote/dquote - if {[dict exists $transition_info starttok] && [dict get $transition_info starttok] ne ""} { - lassign [dict get $transition_info starttok] starttok_type starttok_val - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType $starttok_type - set tok $starttok_val - } - } - single_squote { - #JMN - REVIEW - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - triple_squote { - ::tomlish::log::debug "---- push trigger tokenType triple_squote" - set v($nest) [list MULTILITERAL] ;#container for NEWLINE,LITERALPART - } - squotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - triple_dquote { - set v($nest) [list MULTISTRING] ;#container for NEWLINE,STRINGPART,CONT - } - dquotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - XXXdquotedkey { - #todo - set v($nest) [list DQKEY $tok] ;#$tok is the keyname - } - barekey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - set waiting [tomlish::parse::get_token_waiting] - if {[llength $waiting]} { - set i [dict get $waiting startindex] - tomlish::parse::clear_token_waiting - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } else { - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - } - tablename { - #note: we do not use the output of tomlish::tablename_trim to produce a tablename for storage in the tomlish list! - #The tomlish list is intended to preserve all whitespace (and comments) - so a roundtrip from toml file to tomlish - # back to toml file will be identical. - #It is up to the datastructure stage to normalize and interpret tomlish for programmatic access. - # we call tablename_trim here only to to validate that the tablename data is well-formed at the outermost level, - # so we can raise an error at this point rather than create a tomlish list with obviously invalid table names from - # a structural perspective. - - #todo - review! It's arguable that we should not do any validation here, and just store even incorrect raw tablenames, - # so that the tomlish list is more useful for say a toml editor. Consider adding an 'err' tag to the appropriate place in the - # tomlish list? - - #set trimtable [::tomlish::utils::tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablename: '$trimtable'" - set v($nest) [list TABLE $tok] ;#$tok is the *raw* table name - #note also that equivalent tablenames may have different toml representations even after being trimmed! - #e.g ["x\t\t"] & ["x "] (tab escapes vs literals) - #These will show as above in the tomlish list, but should normalize to the same tablename when used as keys by the datastructure stage. - } - tablearrayname { - #set trimtable [::tomlish::utils::tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablearrayname: '$trimtable'" - set v($nest) [list TABLEARRAY $tok] ;#$tok is the *raw* tablearray name - } - startarray { - set v($nest) [list ARRAY] ;#$tok is just the opening bracket - don't output. - } - startinlinetable { - set v($nest) [list ITABLE] ;#$tok is just the opening curly brace - don't output. - } - default { - error "---- push trigger tokenType '$tokenType' not yet implemented" - } - } - - } else { - #no space level change - switch -exact -- $tokenType { - squotedkey { - #puts "---- squotedkey in state $prevstate (no space level change)" - lappend v($nest) [list SQKEY $tok] - } - dquotedkey { - #puts "---- dquotedkey in state $prevstate (no space level change)" - lappend v($nest) [list DQKEY $tok] - } - barekey { - lappend v($nest) [list KEY $tok] - } - dotsep { - lappend v($nest) [list DOTSEP] - } - starttablename { - #$tok is triggered by the opening bracket and sends nothing to output - } - starttablearrayname { - #$tok is triggered by the double opening brackets and sends nothing to output - } - tablename - tablenamearray { - error "---- did not expect 'tablename/tablearrayname' without space level change (no space level change)" - #set v($nest) [list TABLE $tok] - } - endtablename - endtablearrayname { - #no output into the tomlish list for this token - } - startinlinetable { - puts stderr "---- decode::toml error. did not expect startinlinetable without space level change (no space level change)" - } - single_dquote { - switch -exact -- $newstate { - string-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "string" - set tok "" - } - dquoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "dquotedkey" - set tok "" - } - multistring-space { - lappend v($nest) [list STRINGPART {"}] - #may need to be joined on pop if there are neighbouring STRINGPARTS - } - default { - error "---- single_dquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_dquote { - #leading extra quotes - test: toml_multistring_startquote2 - switch -exact -- $prevstate { - itable-keyval-value-expected - keyval-value-expected { - puts stderr "tomlish::decode::toml double_dquote TEST" - #empty string - lappend v($nest) [list STRINGPART ""] - } - multistring-space { - #multistring-space to multistring-space - lappend v($nest) [list STRINGPART {""}] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - - } - single_squote { - switch -exact -- $newstate { - literal-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "literal" - set tok "" - } - squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - multiliteral-space { - #false alarm squote returned from tentative_accum_squote pop - ::tomlish::log::debug "---- adding lone squote to own LITERALPART nextstate: $newstate (no space level change)" - #(single squote - not terminating space) - lappend v($nest) [list LITERALPART '] - #may need to be joined on pop if there are neighbouring LITERALPARTs - } - default { - error "---- single_squote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_squote { - switch -exact -- $prevstate { - keyval-value-expected { - lappend v($nest) [list LITERAL ""] - } - multiliteral-space { - #multiliteral-space to multiliteral-space - lappend v($nest) [list LITERALPART ''] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - enddquote { - #nothing to do? - set tok "" - } - endsquote { - set tok "" - } - string { - lappend v($nest) [list STRING $tok] ;#directly wrapped in dquotes - } - literal { - lappend v($nest) [list LITERAL $tok] ;#directly wrapped in squotes - } - multistring { - #review - lappend v($nest) [list MULTISTRING $tok] - } - stringpart { - lappend v($nest) [list STRINGPART $tok] ;#will not get wrapped in dquotes directly - } - multiliteral { - lappend v($nest) [LIST MULTILITERAL $tok] - } - literalpart { - lappend v($nest) [list LITERALPART $tok] ;#will not get wrapped in squotes directly - } - untyped_value { - #would be better termed unclassified_value - #we can't determine the type of unquoted values (int,float,datetime,bool) until the entire token was read. - if {$tok in {true false}} { - set tag BOOL - } elseif {[::tomlish::utils::is_int $tok]} { - set tag INT - } elseif {[::tomlish::utils::is_float $tok]} { - set tag FLOAT - } elseif {[::tomlish::utils::is_datetime $tok]} { - set tag DATETIME - } else { - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. [tomlish::parse::report_line] (no space level change)" - } - lappend v($nest) [list $tag $tok] - - } - comment { - #puts stdout "----- comment token returned '$tok'------" - lappend v($nest) [list COMMENT "$tok"] - } - equal { - #we append '=' to the nest so that any surrounding whitespace is retained. - lappend v($nest) = - } - comma { - lappend v($nest) SEP - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - whitespace { - lappend v($nest) [list WS $tok] - } - continuation { - lappend v($nest) CONT - } - bom { - lappend v($nest) BOM - } - eof { - #ok - nothing more to add to the tomlish list. - #!todo - check previous tokens are complete/valid? - } - default { - error "--- unknown tokenType '$tokenType' during state $prevstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - - if {!$next_tokenType_known} { - ::tomlish::log::notice "---- tomlish::decode::toml - current tokenType:$tokenType Next token type not known" - ::tomlish::parse::set_tokenType "" - set tok "" - } - - if {$state eq "end-state"} { - break - } - - - } - - #while {$nest > 0} { - # lappend v([expr {$nest -1}]) [set v($nest)] - # incr nest -1 - #} - while {[::tomlish::parse::spacestack size] > 1} { - ::tomlish::parse::spacestack pop - lappend v([expr {$nest -1}]) [set v($nest)] - incr nest -1 - - #set parent [spacestack peek] ;#the level being appended to - #lassign $parent type state - #if {$type eq "space"} { - # - #} elseif {$type eq "buffer"} { - # lappend v([expr {$nest -1}]) {*}[set v($nest)] - #} else { - # error "invalid spacestack item: $parent" - #} - } - - } finally { - set is_parsing 0 - } - return $v(0) - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::decode ---}] -} -#decode toml to tomlish -interp alias {} tomlish::from_toml {} tomlish::decode::toml - -namespace eval tomlish::utils { - #*** !doctools - #[subsection {Namespace tomlish::utils}] - #[para] - #[list_begin definitions] - - - #tablenames (& tablearraynames) may contain irrelevant leading, trailing and interspersed whitespace - # tablenames can be made up of segments delimited by dots. .eg [ a.b . c ] - #trimmed, the tablename becomes {a.b.c} - # A segment may contain whitespace if it is quoted e.g [a . b . "c etc " ] - #ie whitespace is only irrelevant if it's outside a quoted segment - #trimmed, the tablename becomes {a.b."c etc "} - proc tablename_trim {tablename} { - set segments [tablename_split $tablename false] - set trimmed_segments [list] - foreach seg $segments { - lappend trimmed_segments [::string trim $seg " \t"] - } - return [join $trimmed_segments .] - } - - #basic generic quote matching for single and double quotes - #note for example that {[o'malley]} will return sq - as the single quote is not closed or wrapped in double quotes - proc tok_in_quotedpart {tok} { - set sLen [tcl::string::length $tok] - set quote_type "" - set had_slash 0 - for {set i 0} {$i < $sLen} {incr i} { - set c [tcl::string::index $tok $i] - if {$quote_type eq ""} { - if {$had_slash} { - #don't enter quote mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - set quote_type dq - } - sq { - set quote_type sq - } - bsl { - set had_slash 1 - } - } - } - } else { - if {$had_slash} { - #don't leave quoted mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - if {$quote_type eq "dq"} { - set quote_type "" - } - } - sq { - if {$quote_type eq "sq"} { - set quote_type "" - } - } - bsl { - set had_slash 1 - } - } - } - } - } - return $quote_type ;#dq | sq - } - - #utils::tablename_split - proc tablename_split {tablename {normalize false}} { - #we can't just split on . because we have to handle quoted segments which may contain a dot. - #eg {dog."tater.man"} - set sLen [tcl::string::length $tablename] - set segments [list] - set mode "preval" ;#5 modes: preval, quoted,litquoted, unquoted, postval - #quoted is for double-quotes, litquoted is for single-quotes (string literal) - set seg "" - for {set i 0} {$i < $sLen} {incr i} { - - if {$i > 0} { - set lastChar [tcl::string::index $tablename [expr {$i - 1}]] - } else { - set lastChar "" - } - - #todo - track\count backslashes properly - - set c [tcl::string::index $tablename $i] - if {$c eq "\""} { - if {($lastChar eq "\\")} { - #not strictly correct - we could have had an even number prior-backslash sequence - #the toml spec would have us error out immediately on bsl in bad location - but we're - #trying to parse to unvalidated tomlish - set ctest escq - } else { - set ctest dq - } - } else { - set ctest [string map [list " " sp \t tab] $c] - } - - switch -- $ctest { - . { - switch -exact -- $mode { - preval { - error "tablename_split. dot not allowed - expecting a value" - } - unquoted { - #dot marks end of segment. - #if {![is_barekey $seg]} { - # error "tablename_split. dot not allowed - expecting a value" - #} - lappend segments $seg - set seg "" - set mode "preval" - } - quoted { - append seg $c - } - litquoted { - append seg $c - } - postval { - #got dot in an expected location - set mode "preval" - } - } - } - dq { - #unescaped dquote - switch -- $mode { - preval { - set mode "quoted" - set seg "\"" - } - unquoted { - #invalid in barekey - but we are after structure only - append seg $c - } - quoted { - append seg $c - if {$normalize} { - lappend segments [::tomlish::utils::unescape_string [tcl::string::range $seg 1 end-1]] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" ;#make sure we only accept a dot or end-of-data now. - } - litquoted { - append seg $c - } - postval { - error "tablename_split. expected whitespace or dot, got double quote. tablename: '$tablename'" - } - } - } - ' { - switch -- $mode { - preval { - append seg $c - set mode "litquoted" - } - unquoted { - #single quote inside e.g o'neill - ultimately invalid - but we pass through here. - append seg $c - } - quoted { - append seg $c - } - litquoted { - append seg $c - #no normalization to do aside from stripping squotes - if {$normalize} { - lappend segments [tcl::string::range $seg 1 end-1] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" - } - postval { - error "tablename_split. expected whitespace or dot, got single quote. tablename: '$tablename'" - } - } - } - sp - tab { - switch -- $mode { - preval - postval { - #ignore - } - unquoted { - #terminates a barekey - lappend segments $seg - set seg "" - set mode "postval" - } - default { - #append to quoted or litquoted - append seg $c - } - } - } - default { - switch -- $mode { - preval { - set mode unquoted - append seg $c - } - postval { - error "tablename_split. Expected a dot separator. got '$c'. tablename: '$tablename'" - } - default { - append seg $c - } - } - } - } - - if {$i == $sLen-1} { - #end of data - ::tomlish::log::debug "End of data: mode='$mode'" - switch -exact -- $mode { - preval { - error "tablename_split. Expected a value after last dot separator. tablename: '$tablename'" - } - unquoted { - lappend segments $seg - } - quoted { - error "tablename_split. Expected a trailing double quote. tablename: '$tablename'" - } - litquoted { - error "tablename_split. Expected a trailing single quote. tablename: '$tablename'" - } - postval { - #ok - segment already lappended - } - } - } - } - - #note - we must allow 'empty' quoted strings '' & "" - # (these are 'discouraged' but valid toml keys) - - return $segments - } - - proc unicode_escape_info {slashu} { - #!todo - # validate that slashu is either a \uxxxx or \Uxxxxxxxx value of the correct length and - # is a valid 'unicode scalar value' (any Unicode code point except high-surrogate and low-surrogate code points) - # ie integers in the range 0 to D7FF16 and E00016 to 10FFFF16 inclusive - #expr {(($x >= 0) && ($x <= 0xD7FF16)) || (($x >= 0xE00016) && ($x <= 0x10FFFF16))} - if {[tcl::string::match {\\u*} $slashu]} { - set exp {^\\u([0-9a-fA-F]{4}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %4x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } - } else { - return [list err [list reason "Supplied string not of the form \\uHHHH where H in \[0-9a-fA-F\]"]] - } - } elseif {[tcl::string::match {\\U*} $slashu]} { - set exp {^\\U([0-9a-fA-F]{8}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %8x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - if {(($dec >= 0) && ($dec <= 0xD7FF16)) || (($dec >= 0xE00016) && ($dec <= 0x10FFFF16))} { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } else { - return [list err [list reason "$slashu is not within the 'unicode scalar value' ranges 0 to 0xD7FF16 or 0xE00016 to 0x10FFFF16"]] - } - } - } else { - return [list err [list reason "Supplied string not of the form \\UHHHHHHHH where H in \[0-9a-fA-F\]"]] - } - } else { - return [list err [list reason "Supplied string did not start with \\u or \\U" ]] - } - - } - - #Note that unicode characters don't *have* to be escaped. - #So if we provide a function named 'escape_string', the name implies the inverse of unescape_string which unescapes unicode \u \U values. - #- an inverse of unescape_string would encode all unicode chars unnecessarily. - #- as toml accepts a compact escape sequence for common chars such as tab,backspace,linefeed etc but also allows the full form \u009 etc - #- escape_string and unescape_string would not be reliably roundtrippable inverses anyway. - #REVIEW - provide it anyway? When would it be desirable to use? - - variable Bstring_control_map [dict create] - dict set Bstring_control_map \b {\b} - dict set Bstring_control_map \n {\n} - dict set Bstring_control_map \r {\r} - dict set Bstring_control_map \" {\"} - #dict set Bstring_control_map \x1b {\e} ;#should presumably be only be a convenience for decode - going the other way we get \u001B - dict set Bstring_control_map \\ "\\\\" - - #\e for \x1b seems like it might be included - v1.1?? hard to find current state of where toml is going :/ - #for a Bstring (Basic string) tab is explicitly mentioned as not being one that must be escaped. - #8 = \b - already in list. - #built the remainder whilst checking for entries already hardcoded above -in case more are added to the hardcoded list - for {set cdec 0} {$cdec <= 7} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - for {set cdec [expr {0x0A}]} {$cdec <= 0x1F} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - # \u007F = 127 - dict set Bstring_control_map [format %c 127] \\u007F - - #Note the inclusion of backslash in the list of controls makes this non idempotent - subsequent runs would keep encoding the backslashes! - #escape only those chars that must be escaped in a Bstring (e.g not tab which can be literal or escaped) - #for example - can be used by from_dict to produce valid Bstring data for a tomlish record - proc rawstring_to_Bstring_with_escaped_controls {str} { - #for the well known chars that have compact escape sequences allowed by toml - we choose that form over the full \u form. - #we'll use a string map with an explicit list rather than algorithmic at runtime - # - the string map is probably more performant than splitting a string, especially if it's large - variable Bstring_control_map - return [string map $Bstring_control_map $str] - } - - #review - unescape what string? Bstring vs MLBstring? - #we should be specific in the function naming here - #used by to_dict - so part of validation? - REVIEW - proc unescape_string {str} { - #note we can't just use Tcl subst because: - # it also transforms \a (audible bell) and \v (vertical tab) which are not in the toml spec. - # it would strip out backslashes inappropriately: e.g "\j" becomes just j - # it recognizes other escapes which aren't approprite e.g \xhh and octal \nnn - # it replaces \ with a single whitespace (trailing backslash) - #This means we shouldn't use 'subst' on the whole string, but instead substitute only the toml-specified escapes (\r \n \b \t \f \\ \" \uhhhh & \Uhhhhhhhh - #plus \e for \x1b? - - set buffer "" - set buffer4 "" ;#buffer for 4 hex characters following a \u - set buffer8 "" ;#buffer for 8 hex characters following a \u - - set sLen [tcl::string::length $str] - - #we need to handle arbitrarily long sequences of backslashes. \\\\\ etc - set slash_active 0 - set unicode4_active 0 - set unicode8_active 0 - - ::tomlish::log::debug "unescape_string. got len [string length str] str $str" - - #!todo - check for invalid data in the form of a raw carriage return (decimal 13) without following linefeed? - set i 0 - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $str [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $str $i] - #::tomlish::log::debug "unescape_string. got char $c" ;#too much? - - #---------------------- - #as we are 'unescaping' - should we really be testing for existing values that should have been escaped? - #this test looks incomplete anyway REVIEW - scan $c %c n - if {($n <= 31) && ($n != 9) && ($n != 10) && ($n != 13)} { - #we don't expect unescaped unicode characters from 0000 to 001F - - #*except* for raw tab (which is whitespace) and newlines - error "unescape_string. Invalid data for a toml string. Unescaped control character (decimal $n) [::tomlish::utils::string_to_slashu $c]" - } - #---------------------- - - incr i ;#must incr here because we do'returns'inside the loop - if {$c eq "\\"} { - if {$slash_active} { - append buffer "\\" - set slash_active 0 - } elseif {$unicode4_active} { - error "unescape_string. unexpected case slash during unicode4 not yet handled" - } elseif {$unicode8_active} { - error "unescape_string. unexpected case slash during unicode8 not yet handled" - } else { - # don't output anything (yet) - set slash_active 1 - } - } else { - if {$unicode4_active} { - if {[tcl::string::length $buffer4] < 4} { - append buffer4 $c - } - if {[tcl::string::length $buffer4] == 4} { - #we have a \uHHHH to test - set unicode4_active 0 - set result [tomlish::utils::unicode_escape_info "\\u$buffer4"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode8_active} { - if {[tcl::string::length $buffer8] < 8} { - append buffer8 $c - } - if {[tcl::string::length $buffer8] == 8} { - #we have a \UHHHHHHHH to test - set unicode8_active 0 - set result [tomlish::utils::unicode_escape_info "\\U$buffer8"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$slash_active} { - set slash_active 0 - set ctest [tcl::string::map {{"} dq} $c] - switch -exact -- $ctest { - dq { - append buffer {"} - } - b - t - n - f - r { - append buffer [subst -nocommand -novariable "\\$c"] - } - e { - append buffer \x1b - } - u { - set unicode4_active 1 - set buffer4 "" - } - U { - set unicode8_active 1 - set buffer8 "" - } - default { - set slash_active 0 - #review - toml spec says all other escapes are reserved - #and if they are used TOML should produce an error. - #we leave detecting this for caller for now - REVIEW - append buffer "\\$c" - } - } - } else { - append buffer $c - } - } - } - #puts stdout "EOF 4:$unicode4_active 8:$unicode8_active slash:$slash_active" - if {$unicode4_active} { - error "End of string reached before complete unicode escape sequence \uHHHH" - } - if {$unicode8_active} { - error "End of string reached before complete unicode escape sequence \UHHHHHHHH" - } - if {$slash_active} { - append buffer "\\" - } - return $buffer - } - - #This does not have to do with unicode normal forms - which it seems toml has decided against regarding use in keys (review/references?) - #This is meant for internal use regarding ensuring we match equivalent keys which may have just been specified with different string mechanisms, - #e.g squoted vs dquoted vs barekey. - proc normalize_key {rawkey} { - set c1 [tcl::string::index $rawkey 0] - set c2 [tcl::string::index $rawkey end] - if {($c1 eq "'") && ($c2 eq "'")} { - #single quoted segment. No escapes allowed within it. - set key [tcl::string::range $rawkey 1 end-1] - } elseif {($c1 eq "\"") && ($c2 eq "\"")} { - #double quoted segment. Apply escapes. - # - set keydata [tcl::string::range $rawkey 1 end-1] ;#strip outer quotes only - #e.g key could have mix of \UXXXXXXXX escapes and unicode chars - #or mix of \t and literal tabs. - #unescape to convert all to literal versions for comparison - set key [::tomlish::utils::unescape_string $keydata] - #set key [subst -nocommands -novariables $keydata] ;#wrong. Todo - create a string escape substitution function. - } else { - set key $rawkey - } - return $key - } - - proc string_to_slashu {string} { - set rv {} - foreach c [split $string {}] { - scan $c %c cdec - if {$cdec > 65535} { - append rv {\U} [format %.8X $cdec] - } else { - append rv {\u} [format %.4X $cdec] - } - } - return $rv - } - - #'nonprintable' is conservative here because some systems (e.g windows console) are very limited in what they can display. - #This is used for display purposes only (error msgs) - proc nonprintable_to_slashu {s} { - set res "" - foreach i [split $s ""] { - scan $i %c cdec - - set printable 0 - if {($cdec>31) && ($cdec<127)} { - set printable 1 - } - if {$printable} { - append res $i - } else { - if {$cdec > 65535} { - append res \\U[format %.8X $cdec] - } else { - append res \\u[format %.4X $cdec] - } - } - } - set res - } ;# initial version from tcl wiki RS - - #check if str is valid for use as a toml bare key - #Early toml versions? only allowed letters + underscore + dash - proc is_barekey1 {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } else { - set matches [regexp -all {[a-zA-Z0-9\_\-]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters match the regexp - return 1 - } else { - return 0 - } - } - } - - #from toml.abnf in github.com/toml-lang/toml - #unquoted-key = 1*unquoted-key-char - #unquoted-key-char = ALPHA / DIGIT / %x2D / %x5F ; a-z A-Z 0-9 - _ - #unquoted-key-char =/ %xB2 / %xB3 / %xB9 / %xBC-BE ; superscript digits, fractions - #unquoted-key-char =/ %xC0-D6 / %xD8-F6 / %xF8-37D ; non-symbol chars in Latin block - #unquoted-key-char =/ %x37F-1FFF ; exclude GREEK QUESTION MARK, which is basically a semi-colon - #unquoted-key-char =/ %x200C-200D / %x203F-2040 ; from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - #unquoted-key-char =/ %x2070-218F / %x2460-24FF ; include super-/subscripts, letterlike/numberlike forms, enclosed alphanumerics - #unquoted-key-char =/ %x2C00-2FEF / %x3001-D7FF ; skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - #unquoted-key-char =/ %x2070-21FF / %x2300-24FF ; skip math operators - #unquoted-key-char =/ %x25A0-268B / %x2690-2757 ; skip box drawing, block elements, and some yin-yang symbols - #unquoted-key-char =/ %x2762-2767 / %x2776-27E5 ; skip some Dingbat punctuation - #unquoted-key-char =/ %x2801-297F ; skip some math brackets and arrows, and braille blank - #unquoted-key-char =/ %x2B00-2FFF / %x3001-D7FF ; skip various math operators and symbols, and ideographic space - #unquoted-key-char =/ %xF900-FDCF / %xFDF0-FFFD ; skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - #unquoted-key-char =/ %x10000-EFFFF ; all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - variable re_barekey - set ranges [list] - lappend ranges {a-zA-Z0-9\_\-} - lappend ranges {\u00B2} {\u00B3} {\u00B9} {\u00BC-\u00BE} ;# superscript digits, fractions - lappend ranges {\u00C0-\u00D6} {\u00D8-\u00F6} {\u00F8-\u037D} ;# non-symbol chars in Latin block - lappend ranges {\u037f-\u1FFF} ;# exclude GREEK QUESTION MARK, which is basically a semi-colon - lappend ranges {\u200C-\u200D} {\u203F-\u2040} ;# from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - lappend ranges {\u2070-\u218f} {\u2460-\u24FF} ;# include super-subscripts, letterlike/numberlike forms, enclosed alphanumerics - lappend ranges {\u2C00-\u2FEF} {\u3001-\uD7FF} ;# skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - lappend ranges {\u2070-\u21FF} {\u2300-\u24FF} ;# skip math operators - lappend ranges {\u25A0-\u268B} {\u2690-\u2757} ;# skip box drawing, block elements, and some yin-yang symbols - lappend ranges {\u2762-\u2767} {\u2776-\u27E5} ;# skip some Dingbat punctuation - lappend ranges {\u2801-\u297F} ;# skip some math brackets and arrows, and braille blank - lappend ranges {\u2B00-\u2FFF} {\u3001-\uD7FF} ;# skip various math operators and symbols, and ideographic space - lappend ranges {\uF900-\uFDCF} {\uFDF0-\uFFFD} ;# skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - lappend ranges {\U10000-\UEFFFF} ;# all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - set re_barekey {^[} - foreach r $ranges { - append re_barekey $r - } - append re_barekey {]+$} - - proc is_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } - variable re_barekey - return [regexp $re_barekey $str] - } - - #test only that the characters in str are valid for the toml specified type 'integer'. - proc int_validchars1 {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_\-\+]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - #add support for hex,octal,binary 0x.. 0o.. 0b... - proc int_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - proc is_int {str} { - set matches [regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] ;#0b101 etc covered by a-f - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - - # --------------------------------------- - #check for leading zeroes in non 0x 0b 0o - #first strip any +, - or _ (just for this test) - #(but still allowing 0 -0 +0) - set check [tcl::string::map {+ "" - "" _ ""} $str] - if {([tcl::string::length $check] > 1) && ([tcl::string::index $check 0] eq "0") && ([tcl::string::index $check 1] ni {o x b})} { - return 0 - } - # --------------------------------------- - - #check +,- only occur in the first position. (excludes also +++1 etc) - if {[tcl::string::last - $str] > 0} { - return 0 - } - if {[tcl::string::last + $str] > 0} { - return 0 - } - - #------------------------------------------- - #unclear if a 'digit' includes the type specifiers x b o - #we assume the 0x 0b 0o are NOT counted as digits - as underscores here would seem - #to be likely to cause interop issues with other systems - #(e.g tcl allows 0b1_1 but not 0b_11) - #Most of this structure would be unnecessary if we could rely on string::is::integer understanding underscores (9+?) - #we still need to support earlier Tcl for now though. - - #first rule out any case with more than one underscore in a row - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_0xFF - if {[string index $utest 0] eq "_"} { - return 0 - } - if {[string range $utest 0 1] in {0x 0b 0o}} { - set testnum [string range $utest 2 end] - } else { - set testnum $utest - #exclude also things like 0_x 0___b that snuck past our prefix test - if {![string is digit -strict [string map {_ ""} $testnum]]} { - return 0 - } - #assert - only digits and underscores in testnum - #still may have underscores at each end - } - #assert testnum is now the 'digits' portion of a , 0x 0b 0o number - #(+ and - already stripped) - #It may still have chars unsuitable for its type - which will be caught by the string::is::integer test below - if {[string length $testnum] != [string length [string trim $testnum _]]} { - #had non-inner underscores in 'digit' part - return 0 - } - #assert str only has solo inner underscores (if any) between 'digits' - #------------------------------------------- - - set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores - #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { - return 0 - } - #!todo - check bounds only based on some config value - #even though Tcl can handle bignums, we won't accept anything outside of toml 1.0 minimum requirements. - #presumably very large numbers would have to be supplied in a toml file as strings. - #Review - toml 1.0 only says that it must handle up to 2^63 - not that this is a max - if {$numeric_value > $::tomlish::max_int} { - return 0 - } - if {$numeric_value < $::tomlish::min_int} { - return 0 - } - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'float'. - proc float_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[eE0-9\_\-\+\.]} $str] == $numchars} { - return 1 - } else { - #only allow lower case for these special values - as per Toml 1.0 spec - if {$str ni {inf +inf -inf nan +nan -nan}} { - return 0 - } else { - return 1 - } - } - } - - #note - Tcl's string is double will return true also for the subset of float values which are integers - #This function is to determine whether it matches the Toml float concept - so requires a . or e or E - proc is_float {str} { - #vip greenlight known literals, don't test for case variations - as Toml doesn't allow (whereas Tcl allows Inf NaN etc) - if {$str in {inf +inf -inf nan +nan -nan}} { - return 1 - } - #doorcheck the basics for floatiness vs members of that rival gang - ints - if {![regexp {[.eE]} $str]} { - #could be an integer - which isn't specifically a float for Toml purposes. - return 0 - } - - - #patdown for any contraband chars - set matches [regexp -all {[eE0-9\_\-\+\.]} $str] - if {[tcl::string::length $str] != $matches} { - return 0 - } - - #all characters in legal range - - #A leading zero is ok, but we should disallow multiple leading zeroes (same rules as toml ints) - - #Early Toml spec also disallowed leading zeros in the exponent part(?) - #... this seems less interoperable anyway (some libraries generate leading zeroes in exponents) - #we allow leading zeros in exponents here. - - #Check for leading zeros in main part - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - set r {([0-9])*} - regexp $r $check intpart ;#intpart holds all numerals before the first .,e or E - set z {([0])*} - regexp $z $intpart leadingzeros - if {[tcl::string::length $leadingzeros] > 1} { - return 0 - } - - #for floats, +,- may occur in multiple places - #e.g -2E-22 +3e34 - #!todo - check bounds ? - - #----------------------------------------- - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_" || [string index $str end] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_ - if {[string index $utest 0] eq "_"} { - return 0 - } - #----------------------------------------- - - #decimal point, if used must be surrounded by at least one digit on each side - #e.g 3.e+20 also illegal - set dposn [string first . $str] - if {$dposn > -1 } { - set d3 [string range $str $dposn-1 $dposn+1] - if {![string is integer -strict [string index $d3 0]] || ![string is integer -strict [string index $d3 2]]} { - return 0 - } - } - #we've already eliminated leading/trailing underscores - #now ensure each inner underscore is surrounded by digits - if {[regexp {_[^0-9]|[^0-9]_} $str]} { - return 0 - } - - #strip underscores for tcl double check so we can support < tcl 9 versions which didn't allow underscores - set check [tcl::string::map {_ ""} $str] - #string is double accepts inf nan +NaN etc. - if {![tcl::string::is double $check]} { - return 0 - } - - #All good - seems to be a toml-approved float and not an int. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'datetime'. - proc datetime_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[zZtT0-9\-\+\.:]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - #review - we - proc is_datetime {str} { - #e.g 1979-05-27 - #e.g 1979-05-27T00:32:00Z - #e.g 1979-05-27 00:32:00-07:00 - #e.g 1979-05-27 00:32:00+10:00 - #e.g 1979-05-27 00:32:00.999999-07:00 - - #review - #minimal datetimes? - # 2024 ok - shortest valid 4 digit year? - # 02:00 ok - # 05-17 ok - if {[string length $str] < 4} { - return 0 - } - - set matches [regexp -all {[zZtT0-9\-\+\.:]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - #!todo - use full RFC 3339 parser? - lassign [split $str T] datepart timepart - #!todo - what if the value is 'time only'? - - #Tcl's free-form clock scan (no -format option) is deprecated - # - #if {[catch {clock scan $datepart} err]} { - # puts stderr "tcl clock scan failed err:'$err'" - # return 0 - #} - - #!todo - verify time part is reasonable - } else { - return 0 - } - return 1 - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::utils ---}] -} - -namespace eval tomlish::parse { - #*** !doctools - #[subsection {Namespace tomlish::parse}] - #[para] - #[list_begin definitions] - - #This is a somewhat curly mix of a statemachine and toml-nesting-stack littered with special cases. - #The code is a pig's-nest - but it should be noted that for example trailing single double quotes in multiline strings are perhaps not so trivial to parse using more standard methods either: - # - e.g some kind of backtracking required if using an ABNF parser? - #I don't know the precise technical name for this sort of parser; probably something like "Dog's Breakfast" - #More seriously, we don't have distinct lex/parse steps - so it is basically a 'fused lexer' or 'scannerless parser' - - #It is also desirable for this system to be useful in 'interactive' use. review - would a separate lexer make this easier or harder? - - #A possible alternative more structured approach might be to use a PEG (Parsing Expression Grammar) - - - variable is_parsing 0 ;#whether we are in the middle of parsing tomlish text - - variable state - # states: - # table-space, itable-space, array-space - # array-value-expected,keyval-value-expected,itable-keyval-value-expected, keyval-syntax, - # dquoted-key, squoted-key - # string-state, literal-state, multistring... - # - # notes: - # only the -space states are also 'spaces' ie a container which is pushed/popped on the spacestack - - # - # xxx_value-expected - we also allow for leading whitespace in this state, but once a value is returned we jump to a state based on the containing space. e.g keyval-tail or array-syntax - # - #stateMatrix defines for each state, actions to take for each possible token. - #single-element actions are the name of the next state into which to transition, or a 'POPSPACE' instruction to pop a level off the spacestack and add the data to the parent container. - #dual-element actions are a push instruction and the name of the space to push on the stack. - # - PUSHSPACE is a simple push onto the spacestack, zeropoppushspace also pushes, but will first do a pop *if* the current space level is greater than zero (ie if only if not already in root table-space) - - # -- --- --- --- --- --- - #token/state naming guide - # -- --- --- --- --- --- - #tokens : underscore separated or bare name e.g newline, start_quote, start_squote - #private tokens: always have a leading underscore (These are private 'temporary state' tokens that are never returned as actual tokens e.g _start_squote_sequence - #states : always contain at least one dash e.g err-state, table-space - #instructions - # -- --- --- --- --- --- - - - #stateMatrix dict of elements mapping current state to next state based on returned tokens - # current-state {token-encountered next-state ... } - # where next-state can be a 1 or 2 element list. - #If 2 element - the first item is an instruction (ucase) - #If 1 element - it is either a lowercase dashed state name or an ucase instruction - #e.g {PUSHSPACE } or POPSPACE or SAMESPACE - - - #SAMESPACE - got to same space as parent without popping a level, but has it's own autotransition lookup - strange concept - review usecases - - variable stateMatrix - set stateMatrix [dict create] - #--------------------------------------------------------- - #WARNING - #The stateMatrix implementation here is currently messy. - #The code is a mixture of declarative via the stateMatrix and imperative via switch statements during PUSH/POP/SAMESPACE transitions. - #This means the state behaviour has to be reasoned about by looking at both in conjuction. - #--------------------------------------------------------- - - #xxx-space vs xxx-syntax inadequately documented - TODO - - #review - out of date? - # --------------------------------------------------------------------------------------------------------------# - # incomplete example of some state starting at table-space - # --------------------------------------------------------------------------------------------------------------# - # ( = -> keyval-value-expected) - # keyval-syntax (popped -> keyval-space -> keyval-tail) (autotransition on pop) - # keyval-space (autotransition on push ^) - # table-space (barekey^) (startdquote -> dquoted-key ^) - # --------------------------------------------------------------------------------------------------------------# - - dict set stateMatrix\ - table-space { - bom "table-space"\ - whitespace "table-space"\ - newline "table-space"\ - barekey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - squotedkey {PUSHSPACE "keyval-space" state "keyval-syntax" note ""}\ - dquotedkey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - XXXsingle_dquote "quoted-key"\ - XXXsingle_squote "squoted-key"\ - comment "table-space"\ - starttablename "tablename-state"\ - starttablearrayname "tablearrayname-state"\ - enddquote "err-state"\ - endsquote "err-state"\ - comma "err-state"\ - eof "end-state"\ - equal "err-state"\ - cr "err-lonecr"\ - } - - - - dict set stateMatrix\ - keyval-space {\ - whitespace "keyval-syntax"\ - equal "keyval-value-expected"\ - } - - # ' = ' portion of keyval - dict set stateMatrix\ - keyval-syntax {\ - whitespace "keyval-syntax"\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal "keyval-value-expected"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - #### - dict set stateMatrix\ - keyval-value-expected {\ - whitespace "keyval-value-expected"\ - untyped_value {TOSTATE "keyval-tail" note ""}\ - literal {TOSTATE "keyval-tail" note "required for empty literal at EOF"}\ - string {TOSTATE "keyval-tail" note "required for empty string at EOF"}\ - single_dquote {TOSTATE "string-state" returnstate keyval-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - single_squote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - } - #double_squote {TOSTATE "keyval-tail" note "empty literal received when double squote occurs"} - - #2025 - no leading-squote-space - only trailing-squote-space. - - dict set stateMatrix\ - keyval-tail {\ - whitespace "keyval-tail"\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - - #itable-space/ curly-syntax : itables - # x={y=1,} - dict set stateMatrix\ - itable-space {\ - whitespace "itable-space"\ - newline "itable-space"\ - barekey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - squotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - dquotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - endinlinetable "POPSPACE"\ - comma "err-state"\ - comment "itable-space"\ - eof "err-state"\ - } - #we don't get single_squote etc here - instead we get the resulting squotedkey token - - - # ??? review - something like this - # - # x={y =1,} - dict set stateMatrix\ - itable-keyval-syntax {\ - whitespace {TOSTATE "itable-keyval-syntax"}\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal {TOSTATE "itable-keyval-value-expected"}\ - newline "err-state"\ - eof "err-state"\ - } - - # x={y=1} - dict set stateMatrix\ - itable-keyval-space {\ - whitespace "itable-keyval-syntax"\ - equal {TOSTATE "itable-keyval-value-expected" note "required"}\ - } - - dict set stateMatrix\ - itable-keyval-value-expected {\ - whitespace "itable-keyval-value-expected"\ - untyped_value {TOSTATE "itable-val-tail" note ""}\ - single_dquote {TOSTATE "string-state" returnstate itable-val-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate itable-val-tail}\ - single_squote {TOSTATE "literal-state" returnstate itable-val-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate itable-val-tail}\ - startinlinetable {PUSHSPACE "itable-space" returnstate itable-val-tail}\ - startarray {PUSHSPACE "array-space" returnstate itable-val-tail}\ - } - #double_squote not currently generated by _start_squote_sequence - '' processed as single_squote to literal-state just like 'xxx' - # review - # double_squote {TOSTATE "itable-val-tail" note "empty literal received when double squote occurs"} - - - - # x={y=1,z="x"} - #POPSPACE is transition from itable-keyval-space to parent itable-space - dict set stateMatrix\ - itable-val-tail {\ - whitespace "itable-val-tail"\ - endinlinetable "POPSPACE"\ - comma "POPSPACE"\ - newline {TOSTATE "itable-val-tail" note "itable-space ??"}\ - comment "itable-val-tail"\ - eof "err-state"\ - } - # XXXnewline "POPSPACE" - # We shouldn't popspace on newline - as if there was no comma we need to stay in itable-val-tail - # This means the newline and subsequent whitespace, comments etc become part of the preceeding dottedkey record - #e.g - # x = { - # j=1 - # #comment within dottedkey j record - # , # comment unattached - # #comment unattached - # k=2 , #comment unattached - # l=3 #comment within l record - # , m=4 - # #comment associated with m record - # - # #still associated with m record - # } - ## - This doesn't quite correspond to what a user might expect - but seems like a consistent mechanism. - #The awkwardness is because there is no way to put in a comment that doesn't consume a trailing comma - #so we cant do: j= 1 #comment for j1 , - # and have the trailing comma recognised. - # - # To associate: j= 1, #comment for j1 - # we would need some extra processing . (not popping until next key ? extra state itable-sep-tail?) REVIEW - worth doing? - # - # The same issue occurs with multiline arrays. The most natural assumption is that a comment on same line after a comma - # is 'associated' with the previous entry. - # - # These comment issues are independent of the data dictionary being generated for conversion to json etc - as the comments don't carry through anyway, - # but are a potential oddity for manipulating the intermediate tomlish structure whilst attempting to preserve 'associated' comments - # (e.g reordering records within an itable) - #The user's intention for 'associated' isn't always clear and the specs don't really guide on this. - - - #dottedkey-space is not (currently) used within [tablename] or [[tablearrayname]] - #it is for keyval ie x.y.z = value - - #this is the state after dot - #we are expecting a complete key token or whitespace - #(initial entry to the space is by one of the keys - which will immediately go to dottedkey-space-tail) - dict set stateMatrix\ - dottedkey-space {\ - whitespace "dottedkey-space"\ - dotsep "err-state"\ - barekey "dottedkey-space-tail"\ - squotedkey "dottedkey-space-tail"\ - dquotedkey "dottedkey-space-tail"\ - newline "err-state"\ - comma "err-state"\ - comment "err-state"\ - equal "err-state"\ - } - #dottedkeyend "POPSPACE" - #equal "POPSPACE"\ - - #jmn 2025 - #we have 1 or more dottedkeys so far - need dotsep to add more, whitespace to maintain, equal to pop - dict set stateMatrix\ - dottedkey-space-tail {\ - whitespace "dottedkey-space-tail" - dotsep "dottedkey-space" - equal "POPSPACE"\ - eof "err-state"\ - newline "err-state"\ - } - - #-------------------------------------------------------------------------- - #scratch area - #from_toml {x=1} - # barekey tok - # table-space PUSHSPACE keyval-space state keyval-syntax - # - - - #-------------------------------------------------------------------------- - - - #REVIEW - #toml spec looks like heading towards allowing newlines within inline tables - #https://github.com/toml-lang/toml/issues/781 - - #2025 - multiline itables appear to be valid for 1.1 - which we are targeting. - #https://github.com/toml-lang/toml/blob/main/toml.md#inline-table - - #JMN2025 - #review comment "err-state" vs comment "itable-space" - see if TOML 1.1 comes out and allows comments in multiline ITABLES - #We currently allow multiline ITABLES (also with comments) in the tokenizer. - #if we want to disallow as per TOML 1.0 - we should do so when attempting to get structure? - - - #JMN REVIEW - #dict set stateMatrix\ - # array-space {\ - # whitespace "array-space"\ - # newline "array-space"\ - # untyped_value "SAMESPACE"\ - # startarray {PUSHSPACE "array-space"}\ - # endarray "POPSPACE"\ - # startinlinetable {PUSHSPACE itable-space}\ - # single_dquote "string-state"\ - # single_squote "literal-state"\ - # triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax note "seems ok 2024"}\ - # comma "array-space"\ - # comment "array-space"\ - # eof "err-state-array-space-got-eof"\ - # } - - ## array-space ## - set aspace [dict create] - dict set aspace whitespace "array-space" - dict set aspace newline "array-space" - #dict set aspace untyped_value "SAMESPACE" - dict set aspace untyped_value "array-syntax" - dict set aspace startarray {PUSHSPACE "array-space"} - dict set aspace endarray "POPSPACE" - dict set aspace single_dquote {TOSTATE "string-state" returnstate array-syntax} - dict set aspace triple_dquote {PUSHSPACE "multistring-space" returnstate array-syntax} - dict set aspace single_squote {TOSTATE "literal-state" returnstate array-syntax} - dict set aspace triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax} - dict set aspace startinlinetable {PUSHSPACE itable-space} - #dict set aspace comma "array-space" - dict set aspace comment "array-space" - dict set aspace eof "err-state-array-space-got-eof" - dict set stateMatrix array-space $aspace - - #when we pop from an inner array we get to array-syntax - #e.g {x=[[]] ??? - set tarntail [dict create] - dict set tarntail whitespace "tablearrayname-tail" - dict set tarntail newline "err-state" - dict set tarntail comment "err-state" - dict set tarntail eof "err-state" - dict set tarntail endtablename "tablearray-tail" - dict set stateMatrix tablearrayname-tail $tarntail - - #review - somewhat counterintuitive...? - # [(starttablearrayname) (endtablearrayname] - # [(starttablename) (endtablename)] - - # [[xxx]] ??? - set tartail [dict create] - dict set tartail whitespace "tablearray-tail" - dict set tartail newline "table-space" - dict set tartail comment "tablearray-tail" - dict set tartail eof "end-state" - dict set stateMatrix tablearray-tail $tartail - - - - - - - dict set stateMatrix\ - end-state {} - - set knowntokens [list] - set knownstates [list] - dict for {state transitions} $stateMatrix { - if {$state ni $knownstates} {lappend knownstates $state} - dict for {tok instructions} $transitions { - if {$tok ni $knowntokens} {lappend knowntokens $tok} - } - } - dict set stateMatrix nostate {} - foreach tok $knowntokens { - dict set stateMatrix nostate $tok "err-nostate-received-token-$tok" - } - - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #purpose - debugging? remove? - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #build a list of 'push triggers' from the stateMatrix - # ie tokens which can push a new space onto spacestack - set push_trigger_tokens [list] - tcl::dict::for {s transitions} $stateMatrix { - tcl::dict::for {token transition_to} $transitions { - set instruction [lindex $transition_to 0] - switch -exact -- $instruction { - PUSHSPACE - zeropoppushspace { - if {$token ni $push_trigger_tokens} { - lappend push_trigger_tokens $token - } - } - } - } - } - ::tomlish::log::debug "push_trigger_tokens: $push_trigger_tokens" - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - - - - #This seems hacky... (deprecate in favour of explicit arguments to the instructions in stateMatrix?) - #spacePopTransitions, spacePushTransitions, spaceSameTransitions below for auto state redirections on POPSPACE,PUSHSPACE,SAMESPACE - - #mainly for the -space states: - #redirect to another state $c based on a state transition from $whatever to $b - # e.g "string {array-space array-syntax}" means when transitioning from string to array-space, jump to array-syntax instead. - #this is useful as we often don't know state $b. e.g when it is decided by 'POPSPACE' - - #use dict set to add values so we can easily add/remove/comment lines - - #Push to, next - #default first states when we push to these spaces - variable spacePushTransitions [dict create] - dict set spacePushTransitions keyval-space keyval-syntax - dict set spacePushTransitions itable-keyval-space itable-keyval-syntax - dict set spacePushTransitions array-space array-space - dict set spacePushTransitions table-space tablename-state - #dict set spacePushTransitions #itable-space itable-space - - #Pop to, next - variable spacePopTransitions [dict create] - dict set spacePopTransitions array-space array-syntax - - - #itable-keyval-space itable-val-tail - #review - #we pop to keyval-space from dottedkey-space or from keyval-value-expected? we don't always want to go to keyval-tail - #leave it out and make the POPSPACE caller explicitly specify it - #keyval-space keyval-tail - - variable spaceSameTransitions [dict create] - #JMN test - #dict set spaceSameTransitions array-space array-syntax - - #itable-keyval-space itable-val-tail - - - variable state_list ;#reset every tomlish::decode::toml - - namespace export tomlish toml - namespace ensemble create - - #goNextState has various side-effects e.g pushes and pops spacestack - #REVIEW - setting nest and v elements here is ugly - #todo - make neater, more single-purpose? - proc goNextState {tokentype tok currentstate} { - variable state - variable nest - variable v - - set prevstate $currentstate - - - variable spacePopTransitions - variable spacePushTransitions - variable spaceSameTransitions - - variable last_space_action "none" - variable last_space_type "none" - variable state_list - - set result "" - set starttok "" - - if {[dict exists $::tomlish::parse::stateMatrix $currentstate $tokentype]} { - set transition_to [dict get $::tomlish::parse::stateMatrix $currentstate $tokentype] - ::tomlish::log::debug "--->> goNextState tokentype:$tokentype tok:$tok currentstate:$currentstate : transition_to = $transition_to" - switch -exact -- [lindex $transition_to 0] { - POPSPACE { - set popfromspace_info [spacestack peek] - set popfromspace_state [dict get $popfromspace_info state] - spacestack pop - set parent_info [spacestack peek] - set type [dict get $parent_info type] - set parentspace [dict get $parent_info state] - - set last_space_action "pop" - set last_space_type $type - - if {[dict exists $parent_info returnstate]} { - set next [dict get $parent_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected to stored returnstate $next <<---" - } else { - ### - #review - do away with spacePopTransitions - which although useful to provide a default.. - # - involve error-prone configurations distant to the main state transition configuration in stateMatrix - if {[dict exists $::tomlish::parse::spacePopTransitions $parentspace]} { - set next [dict get $::tomlish::parse::spacePopTransitions $parentspace] - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected state to $next (spacePopTransitions)<<---" - } else { - set next $parentspace - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace<<---" - } - } - set result $next - } - SAMESPACE { - set currentspace_info [spacestack peek] - ::tomlish::log::debug "--->> SAMESPACE got current space entry: $currentspace_info <<<<<" - set type [dict get $currentspace_info type] - set currentspace [dict get $currentspace_info state] - - if {[dict exists $currentspace_info returnstate]} { - set next [dict get $currentspace_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected to stored returnstate $next" - } else { - if {[dict exists $::tomlish::parse::spaceSameTransitions $currentspace]} { - set next [dict get $::tomlish::parse::spaceSameTransitions $currentspace] - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected state to $next (spaceSameTransitions)" - } else { - set next $currentspace - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace" - } - } - set result $next - } - zeropoppushspace { - if {$nest > 0} { - #pop back down to the root level (table-space) - spacestack pop - set parentinfo [spacestack peek] - set type [dict get $parentinfo type] - set target [dict get $parentinfo state] - - set last_space_action "pop" - set last_space_type $type - - #----- - #standard pop - set parentlevel [expr {$nest -1}] - lappend v($parentlevel) [set v($nest)] - incr nest -1 - #----- - } - #re-entrancy - - #set next [list PUSHSPACE [lindex $transition_to 1]] - set nexttokentype ${tokentype}2 ;#fake token type e.g tablename2 or tablearrayname2 - #::tomlish::log::notice "goNextState REENTRANCY. calling goNextState $nexttokentype $tokentype" - #set result [::tomlish::parse::goNextState $nexttokentype $tokentype] - ::tomlish::log::debug "--->> zeropoppushspace goNextState REENTRANCY. calling goNextState $nexttokentype $currentstate" - set transition_info [::tomlish::parse::goNextState $nexttokentype $tok $currentstate] - set result [dict get $transition_info newstate] - } - PUSHSPACE { - set original_target [dict get $transition_to PUSHSPACE] - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - if {[dict exists $transition_to starttok]} { - set starttok [dict get $transition_to starttok] - } - spacestack push [dict create type space state $original_target] - - set last_space_action "push" - set last_space_type "space" - - if {[dict exists $transition_to state]} { - #an explicit state in the pushed space was requested in the stateMatrix - override the spacePushTransition (spacePushTransitions can be deprecated if we require explicitness?) - set next [dict get $transition_to state] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next by explicit 'state' entry" - } else { - #puts $::tomlish::parse::spacePushTransitions - if {[dict exists $::tomlish::parse::spacePushTransitions $original_target]} { - set next [dict get $::tomlish::parse::spacePushTransitions $original_target] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next (spacePushTransitions) " - } else { - set next $original_target - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target" - } - } - set result $next - } - TOSTATE { - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - set result [dict get $transition_to TOSTATE] - } - default { - #simplified version of TOSTATE - set result [lindex $transition_to 0] ;#ignore everything but first word - } - } - } else { - ::tomlish::log::error "--->> No state transition defined from state $currentstate when tokentype $tokentype received" - set result "nostate" - } - lappend state_list [list tokentype $tokentype from $currentstate to $result] - set state $result - ::tomlish::log::notice "--->> STATE TRANSITION tokenType: '$tokentype' tok:$tok triggering '$currentstate' -> '$result' last_space_action:$last_space_action " - return [dict create prevstate $prevstate newstate $result space_action $last_space_action starttok $starttok] - } - - proc report_line {{line ""}} { - variable linenum - variable is_parsing - if {$is_parsing} { - if {$line eq ""} { - set line $linenum - } - return "Line Number: $line" - } else { - #not in the middle of parsing tomlish text - return nothing. - return "" - } - } - - #produce a *slightly* more readable string rep of the nest for puts etc. - proc nest_pretty1 {list} { - set prettier "{" - - foreach el $list { - if { [lindex $el 0] eq "NEWLINE"} { - append prettier "[list $el]\n" - } elseif {([llength $el] > 1) && ([lindex $el 0] in {KEY DQKEY SQKEY TABLE ARRAY})} { - append prettier [nest_pretty1 $el] - } else { - append prettier "[list $el] " - } - } - append prettier "}" - return $prettier - } - - proc set_tokenType {t} { - variable tokenType - variable tokenType_list - if {![info exists tokenType]} { - set tokenType "" - } - lappend tokenType_list $t - set tokenType $t - } - - proc switch_tokenType {t} { - variable tokenType - variable tokenType_list - lset tokenType_list end $t - set tokenType $t - } - - proc get_tokenType {} { - variable tokenType - return $tokenType - } - - - proc get_token_waiting {} { - variable token_waiting - return [lindex $token_waiting 0] - } - proc clear_token_waiting {} { - variable token_waiting - set token_waiting [list] - } - - #token_waiting is a list - but our standard case is to have only one - #in certain circumstances such as near eof we may have 2 - #the set_token_waiting function only allows setting when there is not already one waiting. - #we want to catch cases of inadvertently trying to set multiple - # - the reason being that the state transition triggered by the previous token may have invalidated the assumptions made when a token was added as waiting. - proc set_token_waiting {args} { - if {[llength $args] %2 != 0} { - error "tomlish set_token_waiting must have args of form: type value complete 0|1" - } - variable token_waiting - - if {[llength $token_waiting] && [dict get [lindex $token_waiting end] type] ne "eof"} { - #tokloop already set a token_waiting - but something (post tokloop processing?) is trying to set another - #we may need to remove the existing token_waiting and reset the tokloop index to the previous char so it's reprocessed in the possibly new context - #rather than attempt to make the right decision here - we raise an error and require the caller to check/handle it - set err "tomlish set_token_waiting already has token_waiting: [lindex $token_waiting 0]" - append err \n " - cannot add token_waiting: $args" - error $err - #set tomlish::parse::i [expr {[dict get $token_waiting startindex] -1}] - #set token_waiting [list] - } - - set waiting [dict create] - dict for {k v} $args { - switch -exact $k { - type - complete { - dict set waiting $k $v - } - value { - dict set waiting tok $v - } - startindex { - dict set waiting startindex $v - } - default { - error "tomlish set_token_waiting error - unrecognised key $k. known keys: [dict keys $args]" - } - } - } - if {![tcl::string::is boolean -strict [dict get $waiting complete]]} { - error "tomlish set_token_waiting error - 'complete' must be a boolean. got [dict get $waiting complete]" - } - if {![llength $token_waiting]} { - set token_waiting [list $waiting] - } else { - #an extra sanity-check that we don't have more than just the eof.. - if {[llength $token_waiting] > 1} { - set err "tomlish Unexpected. Existing token_waiting count > 1.\n" - foreach tw $token_waiting { - append err " $tw" \n - } - append err " - cannot add token_waiting: $waiting" - error $err - } - #last entry must be a waiting eof - set token_waiting [list $waiting [lindex $token_waiting end]] - } - return - } - - #returns 0 or 1 - #tomlish::parse::tok - #we attempt to do this without lookahead (potential use in streaming toml? for what benefit?) todo -final flag - # - the possible benefit is being able to more easily process in arbitrarily split chunks (although we would still have to watch crlf splitting ?) - # - interactive use? - - proc tok {s} { - variable nest - variable v - variable i - variable tok - variable type ;#character type - variable state ;#FSM - - - variable tokenType - variable tokenType_list - - - variable endToken - - variable lastChar - - variable braceCount - variable bracketCount - - - #------------------------------ - #Previous run found another (presumably single-char) token - #The normal case is for there to be only one dict in the list - #multiple is an exception - primarily for eof - variable token_waiting - if {[llength $token_waiting]} { - set waiting [lindex $token_waiting 0] - - set tokenType [dict get $waiting type] - set tok [dict get $waiting tok] - #todo: dict get $token_waiting complete - set token_waiting [lrange $token_waiting 1 end] - return 1 - } - #------------------------------ - - set resultlist [list] - set sLen [tcl::string::length $s] - - set slash_active 0 - set quote 0 - set c "" - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $s [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $s $i] - set cindex $i - set ctest [tcl::string::map {\{ lc \} rc \[ lb \] rb \" dq ' sq \\ bsl \r cr \n lf \t tab \uFEFF bom} $c] - tomlish::log::debug "- tokloop char <$ctest> index $i tokenType:$tokenType tok:<$tok>" - #puts "got char $c during tokenType '$tokenType'" - incr i ;#must incr here because we do returns inside the loop - - switch -exact -- $ctest { - # { - set had_slash $slash_active - set slash_active 0 - - if {$had_slash} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - #for multiliteral, multistring - data and/or end - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo token beginning with underscore - never returned to state machine - review - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i [tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed (see tomlish::utils::is_barekey). [tomlish::parse::report_line]" - } - whitespace { - # hash marks end of whitespace token - #do a return for the whitespace, set token_waiting - #set_token_waiting type comment value "" complete 1 - incr i -1 ;#leave comment for next run - return 1 - } - untyped_value { - #REVIEW! the spec isn't clear.. is whitespace after an int,bool etc required before comment? - #we will accept a comment marker as an immediate terminator of the untyped_value. - incr i -1 - return 1 - } - starttablename - starttablearrayname { - #fix! - error "tomlish Character '#' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare parts - but allowed in quoted parts - let tablename parser sort it out - append tok $c - } - default { - #dquotedkey, string,literal, multistring - append tok $c - } - } - } else { - switch -- $state { - multistring-space { - set_tokenType stringpart - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "#" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "#" - } - default { - #start of token if we're not in a token - set_tokenType comment - set tok "" ;#The hash is not part of the comment data - } - } - } - } - lc { - #left curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i [tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - #*bare* tablename can only contain letters,digits underscores - error "tomlish Invalid tablename first character \{ [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #valid in quoted parts - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\{" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - switch -exact -- $state { - itable-keyval-value-expected - keyval-value-expected { - #switch last key to tablename?? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - array-space - array-syntax { - #nested anonymous inline table - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\{" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\{" - } - default { - error "tomlish state: '$state'. left brace case not implemented [tomlish::parse::report_line]" - } - } - } - - } - rc { - #right curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - tablename { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endinlinetable value "" complete 1 startindex $cindex - return 1 - } - starttablearrayname - tablearrayname { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - default { - #end any other token - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-space { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname-state problem" - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - #invalid - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-val-tail { - set_tokenType "endinlinetable" - set tok "" - #we need to pop the keyval - and then reprocess to pop the inlinetable - so we incr -1 - incr i -1 - return 1 - } - itable-keyval-syntax { - error "tomlish endinlinetable unexpected at this point. Expecting key=val syntax [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\}" - } - multiliteral-space { - set_tokenType "literalpart" ; #review - set tok "\}" - } - default { - #JMN2024b keyval-tail? - error "tomlish state '$state'. endinlinetable case not implemented [tomlish::parse::report_line]" - } - } - } - - } - lb { - #left square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename { - #change the tokenType - switch_tokenType "starttablearrayname" - set tok "" ;#no output into the tomlish list for this token - #any following whitespace is part of the tablearrayname, so return now - return 1 - } - tablename - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - #append tok "\\[" - append tok {\[} - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - #invalid at this point - state machine should disallow: - # table -> starttablearrayname - # tablearray -> starttablearrayname - set_token_waiting type starttablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "\[" - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - keyval-value-expected - itable-keyval-value-expected { - set_tokenType "startarray" - set tok "\[" - return 1 - } - array-space - array-syntax { - #nested array? - set_tokenType "startarray" - set tok "\[" - return 1 - #error "state: array-space. startarray case not implemented [tomlish::parse::report_line]" - } - table-space { - #table name - #assume it's a single bracket - but we need to wait for non-bracket to confirm it's not a tablearray - #note that a starttablearrayname token may contain whitespace between the brackets - # e.g \[ \[ - set_tokenType "starttablename" - set tok "" ;#there is no output into the tomlish list for this token - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\[" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\[" - } - itable-space { - #handle state just to give specific error msg - error "tomlish state: '$state'. Left square bracket invalid. Cannot start array in inline table without key. Use key=\[\] syntax. [tomlish::parse::report_line]" - } - default { - error "tomlish state: '$state'. startarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - rb { - #right square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - #???? - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } else { - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablename value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - default { - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - array-syntax - array-space { - #invalid - but allow parser statemachine to report it. - set_tokenType "endarray" - set tok "\]" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endarray" - set tok "\]" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endtablename" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname problem" - set_tokenType "endtablearray" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-tail { - #[[xxx] - set_tokenType "endtablename" - #sequence: starttablename -> starttablearrayname -> endtablearrayname -> endtablename - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\]" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\]" - } - default { - error "tomlish state '$state'. endarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - bsl { - #backslash - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - #end whitespace token - incr i -1 ;#reprocess bsl in next run - return 1 - } else { - error "tomlish Unexpected backslash during whitespace. [tomlish::parse::report_line]" - } - } - literal - literalpart - squotedkey { - #never need to set slash_active true when in single quoted tokens - append tok "\\" - set slash_active 0 - } - string - dquotedkey - comment { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - stringpart { - if {$slash_active} { - #assert - quotes empty - or we wouldn't have slash_active - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - starttablename - starttablearrayname { - error "tomlish backslash is invalid as first character of $tokenType [tomlish::parse::report_line]" - } - tablename - tablearrayname { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - barekey { - error "tomlish Unexpected backslash during barekey. [tomlish::parse::report_line]" - } - default { - error "tomlish Backslash unexpected during tokentype: '$tokenType'. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$slash_active} { - set_tokenType "stringpart" - set tok "\\\\" - set slash_active 0 - } else { - set slash_active 1 - } - } - multiliteral-space { - #nothing can be escaped in multiliteral-space - not even squotes (?) review - set_tokenType "literalpart" - set tok "\\" - } - default { - error "tomlish tok error: Unexpected backslash when no token is active. [tomlish::parse::report_line]" - } - } - } - } - sq { - #single quote - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote { - #for within multiliteral - #short tentative_accum_squote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_squote is multisquote + 2 = 5 - #return tok with value ''''' - return 1 - } - } - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multiliteral - #switch? - append tok $c - set_tokenType triple_squote - return 1 - } - default { - #if there are more than 3 leading squotes we also enter multiliteral space and the subsequent ones are handled - #by the tentative_accum_squote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 squotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_squote_sequence'" - } - } - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - #end whitespace - incr i -1 ;#reprocess sq - return 1 - } - literal { - #slash_active always false - #terminate the literal - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - literalpart { - #ended by ''' - but final could be '''' or ''''' (up to 2 squotes allowed directly before ending triple squote sequence) - #todo - # idea: end this literalpart (possibly 'temporarily') - # let the sq be reprocessed in the multiliteral-space to push an end-multiliteral-sequence to state stack - # upon popping end-multiliteral-sequence - stitch quotes back into this literalpart's token (if either too short - or a long ending sequence as shown above) - incr i -1 ;#throw the "'" back to loop - will be added to a tentative_accum_squote token for later processing - return 1 - } - XXXitablesquotedkey { - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - squotedkey { - ### - #set_token_waiting type endsquote value "'" complete 1 - return 1 - } - starttablename - starttablearrayname { - #!!! - incr i -1 - return 1 - } - tablename - tablearrayname { - append tok $c - } - barekey { - #barekeys now support all sorts of unicode letter/number chars for other cultures - #but not punctuation - not even for those of Irish heritage who don't object - #to the anglicised form of some names. - # o'shenanigan seems to not be a legal barekey - #The Irish will have to use an earlier form Ó - which apparently many may prefer anyway. - error "tomlish Unexpected single quote during barekey. [tomlish::parse::report_line]" - } - default { - append tok $c - } - } - } else { - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading squote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_squote token or triple_squote token - #It currently doesn't trigger double_squote token - #(handle '' same as 'x' ie produce a single_squote and go into processing literal) - #review - producing double_squote for empty literal may be slightly more efficient. - #This token is not used to handle squote sequences *within* a multiliteral - set_tokenType "_start_squote_sequence" - set tok "'" - } - multiliteral-space { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_squote" ;#trigger tentative_accum_squote - set tok "'" - return 1 - } - table-space - itable-space { - #tests: squotedkey.test squotedkey_itable.test - set_tokenType "squotedkey" - set tok "" - } - XXXtable-space - XXXitable-space { - #future - could there be multiline keys? MLLKEY, MLBKEY ? - #this would (almost) allow arbitrary tcl dicts to be stored in toml (aside from escaping issues) - #probably unlikely - as it's perhaps not very 'minimal' or ergonomic for config files - #@2025 ABNF for toml mentions key, simple-key, unquoted-key, quoted-key and dotted-key - #where key is simple-key or dotted-key - no MLL or MLB components - #the spec states solution for arbitrary binary data is application specific involving encodings - #such as hex, base64 - set_tokenType "_start_squote_sequence" - set tok "'" - return 1 - } - tablename-state { - #first char in tablename-state/tablearrayname-state - set_tokenType "tablename" - append tok "'" - } - tablearrayname-state { - set_tokenType "tablearrayname" - append tok "'" - } - literal-state { - #shouldn't get here? review - tomlish::log::debug "- tokloop sq during literal-state with no tokentype - empty literal?" - set_tokenType "literal" - incr -1 - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - #error "tomlish unimplemented - squote during state '$state'. [tomlish::parse::report_line]" - } - dottedkey-space { - set_tokenType "squotedkey" - } - default { - error "tomlish unhandled squote during state '$state'. [tomlish::parse::report_line]" - } - } - } - - } - dq { - #double quote - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - tentative_accum_dquote { - #within multistring - #short tentative_accum_dquote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_dquote is multidquote + 2 = 5 - #return tok with value """"" - return 1 - } - } - _start_dquote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multistring - #switch? - append tok $c - set_tokenType triple_dquote - return 1 - } - default { - #if there are more than 3 leading dquotes we also enter multistring space and the subsequent ones are handled - #by the tentative_accum_dquote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 dquotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_dquote_sequence'" - } - } - } - literal - literalpart { - append tok $c - } - string { - if {$had_slash} { - append tok "\\" $c - } else { - #unescaped quote always terminates a string - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - stringpart { - #sub element of multistring - if {$had_slash} { - append tok "\\" $c - } else { - incr i -1 ;#throw the {"} back to loop - will be added to a tentative_accum_dquote token for later processing - return 1 - } - } - whitespace { - #assert: had_slash will only ever be true in multistring-space - if {$had_slash} { - incr i -2 - return 1 - } else { - #end whitespace token - throw dq back for reprocessing - incr i -1 - return 1 - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - XXXdquotedkey { - if {$had_slash} { - append tok "\\" - append tok $c - } else { - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - dquotedkey { - ### - if {$had_slash} { - append tok "\\" - append tok $c - } else { - #set_token_waiting type enddquote value {"} complete 1 - return 1 - } - } - squotedkey { - append tok $c - } - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - incr i -1 ;## - return 1 - } - default { - error "tomlish got quote during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - #$slash_active not relevant when no tokenType - #token is string only if we're expecting a value at this point - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading dquote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_dquote token or triple_dquote token - #It currently doesn't trigger double_dquote token - #(handle "" same as "x" ie produce a single_dquote and go into processing string) - #review - producing double_dquote for empty string may be slightly more efficient. - #This token is not used to handle dquote sequences once *within* a multistring - set_tokenType "_start_dquote_sequence" - set tok {"} - } - multistring-space { - if {$had_slash} { - set_tokenType "stringpart" - set tok "\\\"" - } else { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_dquote" ;#trigger tentative_accum_dquote - set tok {"} - return 1 - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\"" - } - table-space - itable-space { - set_tokenType "dquotedkey" - set tok "" - } - dottedkey-space { - set_tokenType dquotedkey - set tok "" - - #only if complex keys become a thing - #set_tokenType dquote_seq_begin - #set tok $c - } - tablename-state { - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - default { - error "tomlish Unexpected dquote during state '$state' [tomlish::parse::report_line]" - } - } - } - } - = { - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - #assertion had_slash 0 - append tok $c - } - string - comment - dquotedkey { - #for these tokenTypes an = is just data. - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type equal value = complete 1 startindex $cindex - return 1 - } - } - barekey { - #set_token_waiting type equal value = complete 1 - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character '=' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare name - but valid in quoted parts - leave for tablename parser to sort out - append tok $c - } - default { - error "tomlish unexpected = character during tokentype $tokenType. case not implemented. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok = - } - multiliteral-space { - set_tokenType "literalpart" - set tok "=" - } - dottedkey-space { - set_tokenType "equal" - set tok "=" - return 1 - } - default { - set_tokenType "equal" - set tok = - return 1 - } - } - } - } - cr { - #REVIEW! - # \r carriage return - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #we have received a double cr - ::tomlish::log::warn "double cr - will generate cr token. needs testing" - set_tokenType "cr" ;#lone cr token will generally raise an error - but let state machine handle it - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - append tok $c - } - literalpart { - #part of MLL string (multi-line literal string) - #we need to split out crlf as a separate NEWLINE to be consistent - ::tomlish::log::warn "literalpart ended by cr - needs testing" - #return literalpart temporarily - allow cr to be reprocessed from multiliteral-space - incr i -1 - return 1 - } - stringpart { - #stringpart is a part of MLB string (multi-line basic string) - #throw back the cr - if followed by lf it will become a {NEWLINE crlf} entry within the MULTISTRING list (e.g between STRINGPART entries) - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #could in theory be valid in quoted part of name - #review - might be better just to disallow here - append tok $c - } - whitespace { - #it should technically be part of whitespace if not followed by lf - #but outside of values we are also free to map it to be another NEWLINE instead? REVIEW - incr i -1 - return 1 - } - untyped_value { - incr i -1 - return 1 - } - default { - #!todo - error out if cr inappropriate for tokenType - append tok $c - } - } - } else { - #lf may be appended if next - #review - lone cr as newline? - this is uncommon - but so is lone cr in a string(?) - set_tokenType "newline" - set tok cr - } - } - lf { - # \n newline - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #this lf is the trailing part of a crlf - append tok lf ;#assert we should now have tok "crlf" - as a previous cr is the only way to have an incomplete newline tok - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - #multiliteral or multistring - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - #nl is not allowed *within* a literal - require multiliteral syntax for any literal containing a newline ''' ''' - #even though we terminate the literal without the closing quote here - the token_waiting newline should trigger a state error - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - literalpart { - #we allow newlines - but store them within the multiliteral as their own element - #This is a legitimate end to the literalpart - but not the whole multiliteral - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - stringpart { - if {$had_slash} { - #emit the stringpart (return 1), queue the continuation, go back 1 to reprocess the lf (incr i -1) - set_token_waiting type continuation value \\ complete 1 startindex [expr {$cindex-1}] - incr i -1 - return 1 - } else { - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - starttablename - tablename - tablearrayname - starttablearrayname { - error "tomlish Character is invalid in $tokenType. [tomlish::parse::report_line]" - } - default { - #newline ends all other tokens. - #note for string: we don't add (raw unescaped) newline to simple string. (must use multi-string for this) - #note for whitespace: - # we will use the convention that \n terminates the current whitespace even if whitespace follows - # ie whitespace is split into separate whitespace tokens at each newline - - #puts "-------------- newline lf during tokenType $tokenType" - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "newline" - set tok lf - return 1 - } - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "newline" - set tok "lf" - return 1 - } - default { - #ignore slash? error? - set_tokenType "newline" - set tok lf - return 1 - } - } - #if {$had_slash} { - # #CONT directly before newline - allows strings_5_byteequivalent test to pass - # set_tokenType "continuation" - # set tok "\\" - # incr i -1 - # return 1 - #} else { - # set_tokenType newline - # set tok lf - # return 1 - #} - } - } - , { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok , - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - #stringpart can have up to 2 quotes too - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type comma value "," complete 1 startindex $cindex - return 1 - } - } - default { - set_token_waiting type comma value "," complete 1 startindex $cindex - if {$had_slash} {append tok "\\"} - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "literalpart" - set tok "," - } - default { - set_tokenType "comma" - set tok "," - return 1 - } - } - } - } - . { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - untyped_value { - if {$had_slash} {append tok "\\"} - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - switch -exact -- $state { - multistring-space { - #review - if {$had_slash} { - incr i -2 - } else { - incr i -1 - } - return 1 - } - xxxdottedkey-space { - incr i -1 - return 1 - } - dottedkey-space-tail { - incr i -1 - return 1 - } - default { - error "tomlish Received period during tokenType 'whitespace' [tomlish::parse::report_line]" - } - } - } - starttablename - starttablearrayname { - #This would correspond to an empty table name - error "tomlish Character '.' is not allowed as first character ($tokenType). [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #subtable - split later - review - append tok $c - } - barekey { - #e.g x.y = 1 - #we need to transition the barekey to become a structured table name ??? review - #x is the tablename y is the key - set_token_waiting type dotsep value "." complete 1 startindex $cindex - return 1 - } - default { - error "tomlish Received period during tokenType '$tokenType' [tomlish::parse::report_line]" - #set_token_waiting type period value . complete 1 - #return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "." - } - multiliteral-space { - set_tokenType "literalpart" - set tok "." - } - XXXdottedkey-space { - ### obs? - set_tokenType "dotsep" - set tok "." - return 1 - } - dottedkey-space-tail { - ### - set_tokenType "dotsep" - set tok "." - return 1 - } - default { - set_tokenType "untyped_value" - set tok "." - } - } - } - - } - " " { - if {[tcl::string::length $tokenType]} { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #todo had_slash - emit token or error - #whitespace is a terminator for bare keys - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - comment { - if {$had_slash} { - append tok "\\" - } - append tok $c - } - string - dquotedkey { - if {$had_slash} { append tok "\\" } - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART xxx WS " " - incr i -1 - return 1 - } - } - literal - literalpart - squotedkey { - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - append tok $c - } else { - append tok $c - } - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace space during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "whitespace" - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - if {$had_slash} { - error "tomlish unexpected backslash [tomlish::parse::report_line]" - } - set_tokenType "whitespace" - append tok $c - } - } - } - } - tab { - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out (?review) - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #whitespace is a terminator for bare keys - incr i -1 - #set_token_waiting type whitespace value $c complete 1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - squotedkey { - append tok $c - } - dquotedkey - string - comment - whitespace { - #REVIEW - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - incr i -1 - return 1 - } - } - literal - literalpart { - append tok $c - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace tab during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - if {$slash_active} { - set slash_active 0 - } - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType whitespace - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "whitespace" - append tok $c - } - } - } - } - bom { - #BOM (Byte Order Mark) - ignored by token consumer - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #assert - tok will be one or two squotes only - #A toml literal probably isn't allowed to contain this - #but we will parse and let the validator sort it out. - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart { - append tok $c - } - string - stringpart { - append tok $c - } - default { - #state machine will generally not have entry to accept bom - let it crash - set_token_waiting type bom value "\uFEFF" complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - multistring-space { - set_tokenType "stringpart" - set tok $c - } - default { - set_tokenType "bom" - set tok "\uFEFF" - return 1 - } - } - } - } - default { - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - #review - incr i -1 ;#We don't have a full token to add to the token_waiting dict - so leave this char for next run. - return 1 - } - } - barekey { - if {[tomlish::utils::is_barekey $c]} { - append tok $c - } else { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed. (see tomlish::utils::is_barekey) [tomlish::parse::report_line]" - } - } - starttablename - starttablearrayname { - incr i -1 - #allow statemachine to set context for subsequent chars - return 1 - } - stringpart { - append tok $c - } - default { - #e.g comment/string/literal/literalpart/untyped_value/starttablename/starttablearrayname/tablename/tablearrayname - append tok $c - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - table-space - itable-space { - #if no currently active token - assume another key value pair - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - multistring-space { - set_tokenType "stringpart" - if {$had_slash} { - set tok \\$c - } else { - set tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - tablename-state { - set_tokenType "tablename" - set tok $c - } - tablearrayname-state { - set_tokenType "tablearrayname" - set tok $c - } - dottedkey-space { - set_tokenType barekey - set tok $c - } - default { - #todo - something like ansistring VIEW to show control chars? - set cshow [string map [list \t tab \v vt] $c] - tomlish::log::debug "- tokloop char '$cshow' setting to untyped_value while state:$state [tomlish::parse::report_line]" - set_tokenType "untyped_value" - set tok $c - } - } - } - } - } - - } - - #run out of characters (eof) - if {[tcl::string::length $tokenType]} { - #check for invalid ending tokens - #if {$state eq "err-state"} { - # error "Reached end of data whilst tokenType = '$tokenType'. INVALID" - #} - switch -exact -- $tokenType { - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open literal - error "tomlish eof reached without closing single quote for string literal. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "literal" - set tok "" - return 1 - - ##review - #set_token_waiting type endsquote value "'" complete 1 startindex [expr {$cindex -1}] - #set_tokenType "literal" - #set tok "" - #return 1 - } - } - } - _start_dquote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open string - error "tomlish eof reached without closing double quote for string. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "string" - set tok "" - return 1 - } - } - } - newline { - #The only newline token that has still not been returned should have a tok value of "cr" - puts "tomlish eof reached - with incomplete newline token '$tok'" - if {$tok eq "cr"} { - #we convert lone cr to it's own "cr" token elsewhere in the document to allow statemachine to handle it. - #(which it should generally do by not handling it ie raising an error - or emitting an ERROR list in the tomlish) - #if trailing char is a lone cr - we should encode it the same way as elsewhere that is outside of values - # ie as it's own token. - switch_tokenType "cr" - return 1 - } else { - #should be unreachable - error "tomlish eof reached - with invalid newline token. value: $tok" - } - } - } - set_token_waiting type eof value eof complete 1 startindex $i ;#review - return 1 - } else { - ::tomlish::log::debug "- No current tokenType, ran out of characters, setting tokenType to 'eof' [tomlish::parse::report_line]" - set tokenType "eof" - set tok "eof" - } - return 0 - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::parse ---}] -} - -namespace eval tomlish::dict { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - - proc is_tomlish_typeval {d} { - #designed to detect {type value } e.g {type INT value 3}, {type STRING value "blah etc"} - #as a sanity check we need to avoid mistaking user data that happens to match same form - #consider x.y={type="spud",value="blah"} - #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. - #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} - } - proc is_tomlish_typeval2 {d} { - upvar ::tomlish::tags tags - expr {[lindex $d 0] eq "type" && [lindex $d 1] in $tags} - } - proc last_tomltype_posn {d} { - set last_simple -1 - set dictposn [expr {[dict size $d] -1}] - foreach k [lreverse [dict keys $d]] { - set dval [dict get $d $k] - if {[is_tomlish_typeval $dval]} { - set last_simple $dictposn - break - } - incr dictposn -1 - } - return $last_simple - } - - - #review - proc name_from_tablestack {tablestack} { - set name "" - foreach tinfo [lrange $tablestack 1 end] { - lassign $tinfo type namepart - switch -- $type { - T { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - I { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - default { - #end at first break in the leading sequence of T & I tablenames - break - } - } - } - return $name - } - - proc _show_tablenames {tablenames_info} { - append msg \n "tablenames_info:" \n - dict for {tkey tinfo} $tablenames_info { - append msg " " "table: $tkey" \n - dict for {field finfo} $tinfo { - append msg " " "$field $finfo" \n - } - } - return $msg - } -} - -tcl::namespace::eval tomlish::app { - variable applist [list encoder decoder test] - - #*** !doctools - #[subsection {Namespace tomlish::app}] - #[para] - #[list_begin definitions] - - proc decoder {args} { - #*** !doctools - #[call app::[fun decoder] [arg args]] - #[para] read toml on stdin until EOF - #[para] on error - returns non-zero exit code and writes error on stderr - #[para] on success - returns zero exit code and writes JSON encoding of the data on stdout - #[para] This decoder is intended to be compatible with toml-test - - set opts [dict merge [dict create] $args] - #fconfigure stdin -encoding utf-8 - fconfigure stdin -translation binary - #Just slurp it all - presumably we are not handling massive amounts of data on stdin. - # - even if the input is large, we probably don't gain much (aside from possible memory savings?) by attempting to process input as it arrives. - if {[catch { - set toml [read stdin] - }]} { - exit 2 ;#read error - } - try { - set j [::tomlish::toml_to_json $toml] - } on error {em} { - puts stderr "decoding failed: '$em'" - exit 1 - } - puts -nonewline stdout $j - exit 0 - } - - proc encoder {args} { - #*** !doctools - #[call app::[fun encoder] [arg args]] - #[para] read JSON on stdin until EOF - #[para] return non-zero exitcode if JSON data cannot be converted to a valid TOML representation - #[para] return zero exitcode and TOML data on stdout if JSON data can be converted. - #[para] This encoder is intended to be compatible with toml-test - - set opts [dict merge [dict create] $args] - fconfigure stdin -translation binary - if {[catch { - set json [read stdin] - }]} { - exit 2 ;#read error - } - try { - set toml [::tomlish::json_to_toml $json] - } on error {em} { - puts stderr "encoding failed: '$em'" - exit 1 - } - puts -nonewline stdout $toml - exit 0 - } - - proc test {args} { - set opts [dict merge [dict create] $args] - - package require test::tomlish - if {[dict exists $opts -suite]} { - test::tomlish::suite [dict get $opts -suite] - } - test::tomlish::run - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::app ---}] -} - -proc ::tomlish::appnames {} { - set applist [list] - foreach cmd [info commands ::tomlish::app::*] { - lappend applist [namespace tail $cmd] - } - return $applist -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::lib { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace tomlish::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 tomlish::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -if {$argc > 0} { - puts stderr "argc: $argc args: $argv" - - if {($argc == 1)} { - if {[tcl::string::tolower $argv] in {help -help h -h}} { - puts stdout "Usage: -app where appname one of:[tomlish::appnames]" - exit 0 - } else { - puts stderr "Argument '$argv' not understood. Try -help" - exit 1 - } - } - set opts [dict create] - set opts [dict merge $opts $argv] - - set opts_understood [list -app ] - if {"-app" in [dict keys $opts]} { - #Don't vet the remaining opts - as they are interpreted by each app - } else { - foreach key [dict keys $opts] { - if {$key ni $opts_understood} { - puts stderr "Option '$key' not understood" - exit 1 - } - } - } - if {[dict exists $opts -app]} { - set app [dict get $opts -app] - if {$app ni [tomlish::appnames]} { - puts stderr "app '[dict get $opts -app]' not found" - exit 1 - } - tomlish::app::$app {*}$opts - } -} - -## Ready -package provide tomlish [namespace eval tomlish { - variable pkg tomlish - variable version - set version 1.1.4 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/tomlish-1.1.5.tm b/src/bootsupport/modules/tomlish-1.1.5.tm deleted file mode 100644 index 7ff93c3e..00000000 --- a/src/bootsupport/modules/tomlish-1.1.5.tm +++ /dev/null @@ -1,6973 +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 tomlish 1.1.5 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin tomlish_module_tomlish 0 1.1.5] -#[copyright "2024"] -#[titledesc {tomlish toml parser}] [comment {-- Name section and table of contents description --}] -#[moddesc {tomlish}] [comment {-- Description at end of page heading --}] -#[require tomlish] -#[keywords module parsing toml configuration] -#[description] -#[para] tomlish is an intermediate representation of toml data in a tree structure (tagged lists representing type information) -#[para] The design goals are for tomlish to be whitespace and comment preserving ie byte-for byte preservation during roundtrips from toml to tomlish and back to toml -#[para] The tomlish representation can then be converted to a Tcl dict structure or to other formats such as json, -#[para] although these other formats are generally unlikely to retain whitespace or comments -#[para] The other formats also won't preserve roundtripability e.g \t and a literal tab coming from a toml file will be indistinguishable. -#[para] A further goal is to allow at least a useful subset of in-place editing operations which also preserve whitespace and comments. -#[para] e.g leaf key value editing, and table reordering/sorting, key-renaming at any level, key insertions/deletions -#[para] The API for editing (tomldoc object?) may require explicit setting of type if accessing an existing key -#[para] e.g setting a key that already exists and is a different type (especially if nested structure such as a table or array) -#[para] will need a -type option (-force ?) to force overriding with another type such as an int. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of tomlish -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by tomlish -#[list_begin itemized] - -package require Tcl 8.6- -package require struct::stack -package require logger - -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {struct::stack}] - -#limit ourselves to clear, destroy, peek, pop, push, rotate, or size (e.g v 1.3 does not implement 'get') - - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish { - namespace export {[a-z]*}; # Convention: export all lowercase - variable types - - #default interp recursionlimit of 1000 is insufficient to pass 1000 deep nested structures as in certain toml tests. - #e.g https://github.com/iarna/toml-spec-tests/tree/latest/values - #1000 seems deep for a 'configuration' format - but toml sometimes used for other serialisation purposes. - #todo - review - set existing_recursionlimit [interp recursionlimit {}] - if {$existing_recursionlimit < 5000} { - interp recursionlimit {} 5000 - } - - #IDEAS: - # since get_toml produces tomlish with whitespace/comments intact: - # tomldoc object - allow (at least basic?) editing of toml whilst preserving comments & whitespace - # - setKey (set leaf only to value) how to specify type? -type option? - whole array vs index into arrays and further nested objects? - option for raw toml additions? - # - separate addKey?? - # - deleteKey (delete leaf) - # - deleteTable (delete table - if only has leaves? - option to delete with child tables?) - # - set/add Table? - position in doc based on existing tables/subtables? - - #The tomlish intermediate representation allows things such as sorting the toml document by table name or other re-ordering of tables - - # because the tables include subkeys, comments and newlines within their structure - those elements all come along with it nicely during reordering. - #The same goes for the first newline following a keyval e.g x=1 \ny=2\n\n - #The newline is part of the keyval structure so makes reordering easier - #example from_toml "a=1\nb=2\n\n\n" - # 0 = TOMLISH - # 1 = KEY a = {INT 1} {NEWLINE lf} - # 2 = NEWLINE lf - # 3 = KEY b = {INT 2} {NEWLINE lf} - # 4 = NEWLINE lf - # 5 = NEWLINE lf - - #This reordering idea is complicated by the nature of tablearrays - especially as a table header references last tablearrayname, - # and duplicate table headers are allowed in that context. - #e.g - #[[fruits]] - # name="apple" - # [fruits.metadata] - # id=1 - # - #[unrelated1] - # - #[[fruits]] - # name="pear" - # - #[unrelated2] - # silly="ordering" - # - #[fruits.metadata] - #id=2 - #The TABLEARRAY record can't be completely selfcontained on the default parsing mechanism - because it is legal (though not recommended) to have unrelated tables in between. - #If we were to 'insert' later related records (such as the 2nd [fruits.metadata] above) into the TABLEARRAY structure - then, even though it might produce 'nicer' toml, - # we would lose roundtripability toml->tomlish->toml - # ----------------------------------------------------- - #REVIEW - #todo - some sort of 'normalize'/'grouping' function on tomlish that at least makes records self-contained, and perhaps then (optionally) reorders resulting records sensibly. - #such a function on the tomlish may work - although it would be unwise to duplicate the validation aspects of to_dict - #The most practical way might be to use to_dict followed by from_dict - but that would lose comment info and formatting. - #In the above example - The decision by the toml author to put [unrelated1] between related tablearrays should be respected, - #but the positioning of [unrelated2] between a tablearray and one of its contained tables is suspect. - #Both [fruits.metadata] table records should theoretically be added as children to their corresponding [[fruits]] tablearray record in the tomlish. (just as their name keys are) - # ----------------------------------------------------- - - - - #ARRAY is analogous to a Tcl list - #TABLE is analogous to a Tcl dict - #WS = inline whitespace - #KEY = bare key and value - #DQKEY = double quoted key and value - #SQKEY = single quoted key and value - #ITABLE = inline table (*can* be anonymous table) - # inline table values immediately create a table with the opening brace - # inline tables are fully defined between their braces, as are dotted-key subtables defined within - # No additional subtables or arrays of tables may be defined within an inline table after the ending brace - they must be entirely self-contained - - set tags [list TOMLISH ARRAY TABLE ITABLE TABLEARRAY WS NEWLINE COMMENT DOTTEDKEY KEY DQKEY SQKEY STRING STRINGPART MULTISTRING LITERAL LITERALPART MULTILITERAL INT FLOAT BOOL DATETIME] - #removed - ANONTABLE - #tomlish v1.0 should accept arbitrary 64-bit signed ints (from -2^63 to 2^63-1) - #we will restrict to this range for compatibility for now - although Tcl can handle larger (arbitrarily so?) - #todo - configurable - allow empty string for 'unlimited' - set min_int -9223372036854775808 ;#-2^63 - set max_int +9223372036854775807 ;#2^63-1 - - proc Dolog {lvl txt} { - #return "$lvl -- $txt" - set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] tomlish '$txt'" - puts stderr $msg - } - logger::initNamespace ::tomlish - foreach lvl [logger::levels] { - interp alias {} tomlish_log_$lvl {} ::tomlish::Dolog $lvl - log::logproc $lvl tomlish_log_$lvl - } - - #*** !doctools - #[subsection {Namespace tomlish}] - #[para] Core API functions for tomlish - #[list_begin definitions] - - proc tags {} { - return $::tomlish::tags - } - - #helper function for to_dict - proc _get_keyval_value {keyval_element} { - log::notice ">>> _get_keyval_value from '$keyval_element'<<<" - #find the value (or 2 values if space separated datetime - and stitch back into one) - # 3 is the earliest index at which the value could occur (depending on whitespace) - if {[lindex $keyval_element 2] ne "="} { - error "tomlish _get_keyval_value keyval_element doesn't seem to be a properly structured { = } list" - } - - #review - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keys are lists {parenttable subtable etc} corresponding to parenttable.subtable.etc - } - set sublist [lrange $keyval_element 2 end] - - set values [list] - set value_posns [list] - set posn 0 - foreach sub $sublist { - #note that a barekey/dquotedkey won't occur directly inside a barekey/dquotedkey - switch -exact -- [lindex $sub 0] { - STRING - LITERAL - MULTISTRING - MULTILITERAL - INT - FLOAT - BOOL - DATETIME - TABLE - ARRAY - ITABLE { - lappend values $sub - lappend value_posns $posn - } - default {} - } - incr posn - } - switch -- [llength $values] { - 0 { - error "tomlish Failed to find value element in KEY. '$keyval_element'" - } - 1 { - lassign [lindex $values 0] type value - } - 2 { - #validate than exactly single space was between the two values - lassign $value_posns p1 p2 - if {$p2 != $p1 +2} { - #sanity check - #can probably only get here through manual manipulation of the tomlish list to an unprocessable form - error "tomlish KEY appears to have more than one part - but not separated by whitespace - invalid '$keyval_element'" - } - set between_token [lindex $sublist $p1+1] - if {[lindex $between_token 1] ne " "} { - error "tomlish KEY in 2 parts is not separated by a single space - cannot consider for datetime '$keyval_element'" - } - lassign [lindex $values 0] type_d1 value_d1 - lassign [lindex $values 1] type_d2 value_d2 - if {$type_d1 ne "DATETIME" || $type_d2 ne "DATETIME"} { - error "tomlish KEY in 2 parts does not appear to be datetime '$keyval_element'" - } - set type DATETIME - set value "${value_d1}T${value_d2}" - } - default { - error "tomlish Found multiple value elements in KEY, expected one. (or 2 for space-separated datetime) '$keyval_element'" - } - } - - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - #simple (non-container, no-substitution) datatype - set result [list type $type value $value] - } - STRING - STRINGPART { - set result [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL - LITERALPART { - #REVIEW - set result [list type $type value $value] - } - TABLE { - #invalid? - error "tomlish _get_keyval_value invalid to have type TABLE on rhs of =" - } - ITABLE { - #This one should not be returned as a type value structure! - # - set result [::tomlish::to_dict [ list [lindex $values 0] ]] - } - ARRAY { - #we need to recurse to get the corresponding dict for the contained item(s) - #pass in the whole [lindex $values 0] (type val) - not just the $value! - set prev_tablenames_info $tablenames_info - set tablenames_info [dict create] - set result [list type $type value [ ::tomlish::to_dict [ list [lindex $values 0] ] ]] - set tablenames_info $prev_tablenames_info - } - MULTISTRING - MULTILITERAL { - #review - mapping these to STRING might make some conversions harder? - #if we keep the MULTI - we know we have to look for newlines for example when converting to json - #without specific types we'd have to check every STRING - and lose info about how best to map chars within it - set result [list type $type value [ ::tomlish::to_dict [ list [lindex $values 0] ] ]] - } - default { - error "tomlish Unexpected value type '$type' found in keyval '$keyval_element'" - } - } - return $result - } - - - - #to_dict is a *basic* programmatic datastructure for accessing the data. - # produce a dictionary of keys and values from a tomlish tagged list. - # to_dict is primarily for reading toml data. - #Extraneous (not within quoted sections) whitespace and comments are not preserved in this structure, - # so a roundtrip from toml to this datastructure and back to toml will lose whitespace formatting and comments. - # creating/changing toml values can be done directly on a tomlish list if preserving (or adding) formatting/comments is desired. - #A separate package 'tomlish::object' may be needed to allow easier programmatic creating/updating/deleting of data elements whilst preserving (or adding or selectively deleting/editing) such formatting. - # - - #within an ARRAY, we store a list of items such as plain dicts (possibly empty) and {type value } for simple types - #(ARRAYS can be mixed type) - #This means our dict structure should have only ARRAY and simple types which need to be in {type value } form - #A dict within an array encodeded as a type ITABLE value should also parse - but is the unpreferred form - REVIEW test? - - #Namespacing? - #ie note the difference: - #[Data] - #temp = { cpu = 79.5, case = 72.0} - # versus - #[Data] - #temps = [{cpu = 79.5, case = 72.0}] - proc to_dict {tomlish} { - package require dictn - - #keep track of which tablenames have already been directly defined, - # so we can raise an error to satisfy the toml rule: 'You cannot define any key or table more than once. Doing so is invalid' - #Note that [a] and then [a.b] is ok if there are no subkey conflicts - so we are only tracking complete tablenames here. - #we don't error out just because a previous tablename segment has already appeared. - - #Declaring, Creating, and Defining Tables - #https://github.com/toml-lang/toml/issues/795 - #(update - only Creating and Defining are relevant terminology) - - #review - #tablenames_info keys type created, defined, createdby, definedby, closedby ??? review keys - # [tname] = header_table [[tname]] = header_tablearray - - #consider the following 2 which are legal: - #[table] #'table' created, defined=open type header_table - #x.y = 3 - #[table.x.z] #'table' defined=closed closedby={header_table table.x.z}, 'table.x' created, 'table.x.z' created defined=open definedby={header_table table.x.z} - #k= 22 - # #'table.x.z' defined=closed closedby={eof eof} - - #equivalent datastructure - - #[table] #'table' created, defined=open definedby={header_table table} - #[table.x] #'table' defined=closed closedby={header_table table.x}, 'table.x' created defined=open definedby={header_table table.x} - #y = 3 - #[table.x.z] #'table.x' defined=closed closedby={header_table table.x.z}, 'table.x.z' created defined=open definedby={header_table table.x.z} - #k=22 - - #illegal - #[table] #'table' created and defined=open - #x.y = 3 #'table.x' created first keyval pair defined=open definedby={keyval x.y = 3} - #[table.x.y.z] #'table' defined=closed, 'table.x' closed because parent 'table' closed?, 'table.x.y' cannot be created - #k = 22 - # - ## - we would fail on encountering table.x.y because only table and table.x are effectively tables - but that table.x is closed should be detected (?) - - #illegal - #[table] - #x.y = {p=3} - #[table.x.y.z] - #k = 22 - ## we should fail because y is an inline table which is closed to further entries - - #note: it is not safe to compare normalized tablenames using join! - # e.g a.'b.c'.d is not the same as a.b.c.d - # instead compare {a b.c d} with {a b c d} - # Here is an example where the number of keys is the same, but they must be compared as a list, not a joined string. - #'a.b'.'c.d.e' vs 'a.b.c'.'d.e' - #we need to normalize the tablenames seen so that {"x\ty"} matches {"xy"} - - - - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keyed on tablepath each of which is a list such as {config subgroup etc} (corresponding to config.subgroup.etc) - } - - - log::info "---> to_dict processing '$tomlish'<<<" - set items $tomlish - - foreach lst $items { - if {[lindex $lst 0] ni $::tomlish::tags} { - error "supplied string does not appear to be toml parsed into a tomlish tagged list. Run tomlish::decode::toml on the raw toml data to produce a tomlish list" - } - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - #ignore TOMLISH tag at beginning - set items [lrange $tomlish 1 end] - } - - set datastructure [dict create] - foreach item $items { - set tag [lindex $item 0] - #puts "...> item:'$item' tag:'$tag'" - switch -exact -- $tag { - KEY - DQKEY - SQKEY { - log::debug "---> to_dict item: processing $tag: $item" - set key [lindex $item 1] - if {$tag eq "DQKEY"} { - set key [::tomlish::utils::unescape_string $key] - } - #!todo - normalize key. (may be quoted/doublequoted) - - if {[dict exists $datastructure $key]} { - error "Duplicate key '$key'. The key already exists at this level in the toml data. The toml data is not valid." - } - - #lassign [_get_keyval_value $item] type val - set keyval_dict [_get_keyval_value $item] - dict set datastructure $key $keyval_dict - } - DOTTEDKEY { - log::debug "---> to_dict item processing $tag: $item" - set dkey_info [tomlish::to_dict::get_dottedkey_info $item] - set dotted_key_hierarchy [dict get $dkey_info keys] - - #a.b.c = 1 - #table_key_hierarchy -> a b - #tleaf -> c - if {[llength $dotted_key_hierarchy] == 0} { - #empty?? probably invalid. review - #This is different to '' = 1 or ''.'' = 1 which have lengths 1 and 2 respectively - error "DOTTED key has no parts - invalid? '$item'" - } elseif {[llength $dotted_key_hierarchy] == 1} { - #dottedkey is only a key - no table component - set table_hierarchy [list] - set tleaf [lindex $dotted_key_hierarchy 0] - } else { - set table_hierarchy [lrange $dotted_key_hierarchy 0 end-1] - set tleaf [lindex $dotted_key_hierarchy end] - } - - #ensure empty tables are still represented in the datastructure - #review - this seems unnecessary? - set pathkeys [list] - foreach k $table_hierarchy { - lappend pathkeys $k - if {![dict exists $datastructure {*}$pathkeys]} { - dict set datastructure {*}$pathkeys [list] - } else { - tomlish::log::notice "to_dict datastructure at key path $pathkeys already had data: [dict get $datastructure {*}$pathkeys]" - } - } - #review? - if {[dict exists $datastructure {*}$table_hierarchy $tleaf]} { - error "Duplicate key '$table_hierarchy $tleaf'. The key already exists at this level in the toml data. The toml data is not valid." - } - - #JMN test 2025 - if {[llength $table_hierarchy]} { - dictn incr tablenames_info [list $table_hierarchy seencount] - } - - set keyval_dict [_get_keyval_value $item] - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - set t [list {*}$table_hierarchy $tleaf] - dictn incr tablenames_info [list $t seencount] - dictn set tablenames_info [list $t closed] 1 - - #review - item is an ITABLE - we recurse here without datastructure context :/ - #overwriting keys? todo ? - dict set datastructure {*}$table_hierarchy $tleaf $keyval_dict - } else { - dict set datastructure {*}$table_hierarchy $tleaf $keyval_dict - } - - } - TABLEARRAY { - set dottedtables_defined [list] ;#for closing off at end by setting 'defined' - - set NEST_DICT [dict create] ;#first blush attempt at referencing supertable tablearrays - set tablearrayname [lindex $item 1] - log::debug "---> to_dict processing item TABLENAME (name: $tablearrayname): $item" - set norm_segments [::tomlish::to_dict::tablename_split $tablearrayname true] ;#true to normalize - #we expect repeated tablearray entries - each adding a sub-object to the value, which is an array/list. - #tablearrayname is likely to appear multiple times - so unlike a TABLE we don't check for 'defined' for the full name as an indicator of a problem - set supertable [list] - ############## - # [[a.b.c.d]] - # norm_segments = {a b c d} - #check a {a b} {a b c} <---- supertables of a.b.c.d - ############## - foreach normseg [lrange $norm_segments 0 end-1] { - lappend supertable $normseg - if {![dictn exists $tablenames_info [list $supertable type]]} { - #supertable with this path doesn't yet exist - if {[dict exists $datastructure {*}$supertable]} { - #There is data though - so it must have been created as a keyval - set msg "Supertable [join $supertable .] of tablearray name $tablearrayname already has data but doesn't appear to be a table - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablearray_supertable_keycollision - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } else { - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - #REVIEW!! - # what happens with from_toml {[[a.b.c]]} {[[a]]} ??? - dictn set tablenames_info [list $supertable type] header_table ;#how do we know it's not going to be a tablearray? - #ensure empty tables are still represented in the datastructure - dict set datastructure {*}$supertable [list] - } - } else { - #supertable has already been created - and maybe defined - but even if defined we can add subtables unless it is of type itable - #but if it's a tablearray - we need to point to the most 'recently defined table element of the array' - #(another way of saying last member of that array)?? - set supertype [dictn get $tablenames_info [list $supertable type]] - if {$supertype eq "header_tablearray"} { - puts stdout "todict!!! TABLEARRAY nesting required for supertable [join $supertable .]" - puts stdout "todict!!! todo.." - #how to do multilevel nesting?? - set EXISTING_SUPERARRAY_ELEMENTS [dict get $datastructure {*}$supertable value] - dict set NEST_DICT $supertable $EXISTING_SUPERARRAY_ELEMENTS - puts stdout "todict!!! supertable '[join $supertable .]' elements $EXISTING_SUPERARRAY_ELEMENTS" - } - } - } - # - if {![dictn exists $tablenames_info [list $norm_segments type]]} { - #first encounter of this tablearrayname - if {[dict exists $datastructure {*}$norm_segments]} { - #e.g from_toml {a=1} {[[a]]} - set msg "Cannot create tablearray name $tablearrayname. Key already has data but key doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablearray_direct_keycollision_error - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #no collision - we can create the tablearray and the array in the datastructure - dictn set tablenames_info [list $norm_segments type] header_tablearray - dict set datastructure {*}$norm_segments [list type ARRAY value {}] - set ARRAY_ELEMENTS [list] - } else { - #we have a table - but is it a tablearray? - set ttype [dictn get $tablenames_info [list $norm_segments type]] - #use a tabletype_unknown type for previous 'created' only tables? - if {$ttype ne "header_tablearray"} { - set msg "tablearray name $tablearrayname already appears to be already created as a table not a tablearray - invalid?" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #add to array - #error "add_to_array not implemented" - #{type ARRAY value } - set ARRAY_ELEMENTS [dict get $datastructure {*}$norm_segments value] - } - - - set object [dict create] ;#array context equivalent of 'datastructure' - set objectnames_info [dict create] ;#array contex equivalent of tablenames_info - - #add to ARRAY_ELEMENTS and write back in to datastructure. - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - log::debug "----> todict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - #MAINTENANCE: temp copy from TABLE - #convert to function: data needed? element, norm_segments refs to: tablename_info datastructure ??? - set dkey_info [tomlish::to_dict::get_dottedkey_info $element] - #e.g1 keys {x.y y} keys_raw {'x.y' "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - #e.g2 keys {x.y y} keys_raw {{"x.y"} "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - - #[a.b] - #t1.t2.dottedtable.k = "val" - #we have already checked supertables a {a b} - #We need to check {a b t1} & {a b t2} ('creation' only) - #and then 'dottedtable' is 'defined' while k is an ordinary key in dottedtable - - #note we also get here as a 'dottedkey' with a simple - #[a.b] - #k = "val" - - set all_dotted_keys [dict get $dkey_info keys] - set dottedkeyname [join $all_dotted_keys .] - #obsolete - set nonleaf_keys [lrange $all_dotted_keys 0 end-1] ;#may be empty - - if {[llength $all_dotted_keys] > 1} { - #dottedtable.k=1 - #tX.dottedtable.k=1 - #etc - - set defines_a_table 1 - #Wrap in a list so we can detect 'null' equivalent. - #We can't use empty string as that's a valid dotted key segment - set dottedtable_bag [list [lindex $all_dotted_keys end-1]] - set dotparents [lrange $all_dotted_keys 0 end-2] - } else { - #basic case - not really a 'dotted' key - #a = 1 - set defines_a_table 0 - set dottedtable_bag [list] ;#empty bag - set dotparents [list] - } - #assert dottedtable_bag only ever holds 0 or 1 elements - set leaf_key [lindex $all_dotted_keys end] - - #we've already tested the table keys that got us here.. but not the dottedkey segments (if any) prior to dottedtable & leaf_key - #set supertable $norm_segments - set supertable [list] ;#disconnect from main structure - each array element is a new context for key paths! - foreach normkey $dotparents { - lappend supertable $normkey - if {![dictn exists $tablenames_info [list $supertable type]]} { - #supertable with this combined path (table norm_segments plus parts of dottedkey) not yet 'created' - if {[dict exists $datastructure {*}$supertable]} { - #There is data so it must have been created as a keyval - set msg "Supertable [join $supertable .] of dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - dictn set tablenames_info [list $supertable type] unknown_table ;#REVIEW - #ensure empty tables are still represented in the datastructure - dict set datastructure {*}$supertable [list] - } - } - if {[llength $dottedtable_bag] == 1} { - set dottedtable [lindex $dottedtable_bag 0] - set dottedpath [list {*}$norm_segments {*}$dotparents $dottedtable] ;#full path to dottedtable - #our dotted key is attempting to define a table - if {![dictn exists $tablenames_info [list $dottedpath type]]} { - #first one - but check datastructure for collisions - if {[dict exists $datastructure {*}$dottedpath]} { - set msg "Supertable [join $dottedpath .] of dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #'create' the table - dictn set tablenames_info [list $dottedpath type] dottedkey_table - #don't actually set 'defined' here.. use the end of TABLE record to close them off by looking at this list - lappend dottedtables_defined $dottedpath - # - } else { - #exists - but might be from another dottedkey within the current header section - #the table is open for adding keys until the next 'header' section ([tablename] / [[tablearray]]) - #check for 'defined' closed (or just existence) - if {[dictn get $tablenames_info [list $dottedpath type]] eq "dottedkey_table"} { - #right type - but make sure it's from this header section - i.e defined not set - set definedstate [dictn getdef $tablenames_info [list $dottedpath defined] NULL] - if {$definedstate ne "NULL"} { - #collision with some other dottedkey - set msg "Table $dottedpath represented by dottedkey $dottedkeyname has been 'defined' elsewhere (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - } - } - } - #assert - dottedkey represents a key val pair that can be added - - - if {[dict exists $datastructure {*}$norm_segments {*}$all_dotted_keys]} { - set msg "Duplicate key '$norm_segments $all_dotted_keys'. The key already exists at this level in the toml data. The toml data is not valid." - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - - set keyval_dict [_get_keyval_value $element] - #keyval_dict is either a {type value } - #or the result from parsing an arbitrary dict from an inline table - which could theoretically look the same at the topmost level - #punk::dict::is_tomlish_typeval can distinguish - puts stdout "to_dict>>> TABLE/dottedkey '$dottedkeyname' kv: $keyval_dict" - #dict set datastructure {*}$norm_segments {*}$all_dotted_keys $keyval_dict - #wrong - #TODO!!!!!!!!!!!!! - #lappend ARRAY_ELEMENTS [list $dottedkeyname $keyval_dict] - dict set object $dottedkeyname $keyval_dict - - #remove ? - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - #the value is either empty or or a dict structure with arbitrary (from-user-data) toplevel keys - # inner structure will contain {type value } if all leaves are not empty ITABLES - set tkey [list {*}$norm_segments {*}$all_dotted_keys] - #lappend tablenames_info [list {*}$norm_segments {*}$dkeys $leaf_key] - - #by not creating a tablenames_info record - we effectively make it closed anyway? - #it should be detected as a key - #is there any need to store tablenames_info for it?? - #REVIEW - - ##TODO - update? - #dictn incr tablenames_info [list $tkey seencount] - ##if the keyval_dict is not a simple type x value y - then it's an inline table ? - ##if so - we should add the path to the leaf_key as a closed table too - as it's not allowed to have more entries added. - #dictn set tablenames_info [list $tkey closed] 1 - } - - } - NEWLINE - COMMENT - WS { - #ignore - } - TABLE { - #we should be able to process tablearray subtables either as part of the tablearray record, or independently. - #(or even a mixture of both, although that is somewhat an edge case) - #[[fruit]] - #x=1 - # [fruit.metadata] - # [fruit.otherdata] - - #when processing a dict destined for the above - the tomlish generator (e.g from_dict) - #should create as 1 or 3 records (but could create 2 records if there was an unrelated table in between the subtables) - #choices: all in tablearray record, tablearray + 1 or 2 table records. - # - #We are going the other way here - so we just need to realize the list of tables 'belonging' to this tablearray might not be complete. - # - #the subtable names must be prefixed with the tablearray - we should validate that for any contained TABLE records - - } - default { - error "Sub element of type '$type' not understood in tablearray context. Expected only DOTTEDKEY,NEWLINE,COMMENT,WS" - } - } - } - - #todo? - ##end of TABLE record - equivalent of EOF or next header - close off the dottedtables - #foreach dtablepath $dottedtables_defined { - # dictn set tablename_info [list $dtablepath defined] closed - #} - - if {[dict size $NEST_DICT]} { - puts "reintegrate?? $NEST_DICT" - #todo - more - what if multiple in hierarchy? - dict for {superpath existing_elements} $NEST_DICT { - #objects stored directly as dicts in ARRAY value - set lastd [lindex $existing_elements end] - #insufficient.. - #dict set lastd [lindex $norm_segments end] [list type ITABLE value $object] - dict set lastd [lindex $norm_segments end] $object - #set lastd [dict merge $lastd $object] - lset existing_elements end $lastd - dict set datastructure {*}$superpath [list type ARRAY value $existing_elements] - } - } else { - #lappend ARRAY_ELEMENTS [list type ITABLE value $object] - lappend ARRAY_ELEMENTS $object - dict set datastructure {*}$norm_segments [list type ARRAY value $ARRAY_ELEMENTS] - } - } - TABLE { - set tablename [lindex $item 1] - set dottedtables_defined [list] ;#for closing off at end by setting 'defined' - #As our TABLE record contains all it's child DOTTEDKEY records - this should be equivalent to setting them as defined at EOF or next header. - - log::debug "---> to_dict processing item TABLE (name: $tablename): $item" - set norm_segments [::tomlish::to_dict::tablename_split $tablename true] ;#true to normalize - - set T_DEFINED [dictn getdef $tablenames_info [list $norm_segments defined] NULL] - if {$T_DEFINED ne "NULL"} { - #our tablename e.g [a.b.c.d] declares a space to 'define' subkeys - but there has already been a definition space for this path - set msg "Table name $tablename has already been directly defined in the toml data. Invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - - - set name_segments [::tomlish::to_dict::tablename_split $tablename 0] ;#unnormalized e.g ['a'."b".c.d] -> 'a' "b" c d - #results of tablename_split 0 are 'raw' - ie some segments may be enclosed in single or double quotes. - - - set supertable [list] - ############## - # [a.b.c.d] - # norm_segments = {a b c d} - #check a {a b} {a b c} <---- supertables of a.b.c.d - ############## - foreach normseg [lrange $norm_segments 0 end-1] { - lappend supertable $normseg - if {![dictn exists $tablenames_info [list $supertable type]]} { - #supertable with this path doesn't yet exist - if {[dict exists $datastructure {*}$supertable]} { - #There is data though - so it must have been created as a keyval - set msg "Supertable [join $supertable .] of table name $tablename already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - dictn set tablenames_info [list $supertable type] header_table - #ensure empty tables are still represented in the datastructure - dict set datastructure {*}$supertable [list] - } else { - #supertable has already been created - and may be defined - but even if defined we can add subtables unless it is of type itable - - } - } - #table [a.b.c.d] hasn't been defined - but may have been 'created' already by a longer tablename - # - or may have existing data from a keyval - if {![dictn exists $tablenames_info [list $norm_segments type]]} { - if {[dict exists $datastructure {*}$norm_segments]} { - #e.g from_toml {a=1} {[a]} - set msg "Cannot create table name $tablename. Key already has data but key doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablename_keyval_collision_error - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #no data or previously created table - dictn set tablenames_info [list $norm_segments type] header_table - - #We are 'defining' this table's keys and values here (even if empty) - dict set datastructure {*}$norm_segments [list] ;#ensure table still represented in datastructure even if we add no keyvals here - } - dictn set tablenames_info [list $norm_segments defined] open - log::debug ">>> to_dict >>>>>>>>>>>>>>>>> normalized table key hierarchy : $norm_segments" - - #now add the contained elements - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - log::debug "----> todict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - #convert to function: data needed? element, norm_segments refs to: tablename_info datastructure ??? - set dkey_info [tomlish::to_dict::get_dottedkey_info $element] - #e.g1 keys {x.y y} keys_raw {'x.y' "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - #e.g2 keys {x.y y} keys_raw {{"x.y"} "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - - #[a.b] - #t1.t2.dottedtable.k = "val" - #we have already checked supertables a {a b} - #We need to check {a b t1} & {a b t2} ('creation' only) - #and then 'dottedtable' is 'defined' while k is an ordinary key in dottedtable - - #note we also get here as a 'dottedkey' with a simple - #[a.b] - #k = "val" - - set all_dotted_keys [dict get $dkey_info keys] - set dottedkeyname [join $all_dotted_keys .] - #obsolete - set nonleaf_keys [lrange $all_dotted_keys 0 end-1] ;#may be empty - - if {[llength $all_dotted_keys] > 1} { - #dottedtable.k=1 - #tX.dottedtable.k=1 - #etc - - set defines_a_table 1 - #Wrap in a list so we can detect 'null' equivalent. - #We can't use empty string as that's a valid dotted key segment - set dottedtable_bag [list [lindex $all_dotted_keys end-1]] - set dotparents [lrange $all_dotted_keys 0 end-2] - } else { - #basic case - not really a 'dotted' key - #a = 1 - set defines_a_table 0 - set dottedtable_bag [list] ;#empty bag - set dotparents [list] - } - #assert dottedtable_bag only ever holds 0 or 1 elements - set leaf_key [lindex $all_dotted_keys end] - - #we've already tested the table keys that got us here.. but not the dottedkey segments (if any) prior to dottedtable & leaf_key - set supertable $norm_segments - foreach normkey $dotparents { - lappend supertable $normkey - if {![dictn exists $tablenames_info [list $supertable type]]} { - #supertable with this combined path (table norm_segments plus parts of dottedkey) not yet 'created' - if {[dict exists $datastructure {*}$supertable]} { - #There is data so it must have been created as a keyval - set msg "Supertable [join $supertable .] of dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - dictn set tablenames_info [list $supertable type] unknown_table ;#REVIEW - #ensure empty tables are still represented in the datastructure - dict set datastructure {*}$supertable [list] - } - } - if {[llength $dottedtable_bag] == 1} { - set dottedtable [lindex $dottedtable_bag 0] - set dottedpath [list {*}$norm_segments {*}$dotparents $dottedtable] ;#full path to dottedtable - #our dotted key is attempting to define a table - if {![dictn exists $tablenames_info [list $dottedpath type]]} { - #first one - but check datastructure for collisions - if {[dict exists $datastructure {*}$dottedpath]} { - set msg "Supertable [join $dottedpath .] of dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #'create' the table - dictn set tablenames_info [list $dottedpath type] dottedkey_table - #don't actually set 'defined' here.. use the end of TABLE record to close them off by looking at this list - lappend dottedtables_defined $dottedpath - # - } else { - #exists - but might be from another dottedkey within the current header section - #the table is open for adding keys until the next 'header' section ([tablename] / [[tablearray]]) - #check for 'defined' closed (or just existence) - if {[dictn get $tablenames_info [list $dottedpath type]] eq "dottedkey_table"} { - #right type - but make sure it's from this header section - i.e defined not set - set definedstate [dictn getdef $tablenames_info [list $dottedpath defined] NULL] - if {$definedstate ne "NULL"} { - #collision with some other dottedkey - set msg "Table $dottedpath represented by dottedkey $dottedkeyname has been 'defined' elsewhere (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - } - } - } - #assert - dottedkey represents a key val pair that can be added - - - if {[dict exists $datastructure {*}$norm_segments {*}$all_dotted_keys]} { - set msg "Duplicate key '$norm_segments $all_dotted_keys'. The key already exists at this level in the toml data. The toml data is not valid." - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - - set keyval_dict [_get_keyval_value $element] - #keyval_dict is either a {type value } - #or the result from parsing an arbitrary dict from an inline table - which could theoretically look the same at the topmost level - #punk::dict::is_tomlish_typeval can distinguish - puts stdout "to_dict>>> TABLE/dottedkey $dottedkeyname kv: $keyval_dict" - dict set datastructure {*}$norm_segments {*}$all_dotted_keys $keyval_dict - - #remove ? - if {![tomlish::dict::is_tomlish_typeval $keyval_dict]} { - #the value is either empty or or a dict structure with arbitrary (from-user-data) toplevel keys - # inner structure will contain {type value } if all leaves are not empty ITABLES - set tkey [list {*}$norm_segments {*}$all_dotted_keys] - #lappend tablenames_info [list {*}$norm_segments {*}$dkeys $leaf_key] - - #by not creating a tablenames_info record - we effectively make it closed anyway? - #it should be detected as a key - #is there any need to store tablenames_info for it?? - #REVIEW - - ##TODO - update? - #dictn incr tablenames_info [list $tkey seencount] - ##if the keyval_dict is not a simple type x value y - then it's an inline table ? - ##if so - we should add the path to the leaf_key as a closed table too - as it's not allowed to have more entries added. - #dictn set tablenames_info [list $tkey closed] 1 - } - - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in table context. Expected only DOTTEDKEY,NEWLINE,COMMENT,WS" - } - } - } - - #end of TABLE record - equivalent of EOF or next header - close off the dottedtables - foreach dtablepath $dottedtables_defined { - dictn set tablename_info [list $dtablepath defined] closed - } - - - #review??? - #now make sure we add an empty value if there were no contained elements! - #!todo. - } - ITABLE { - #SEP??? - set datastructure [list] - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkey_info [tomlish::to_dict::get_dottedkey_info $element] - set dotted_key_hierarchy [dict get $dkey_info keys] - set leaf_key [lindex $dotted_key_hierarchy end] - set dkeys [lrange $dotted_key_hierarchy 0 end-1] - - #ensure empty keys are still represented in the datastructure - set table_keys [list] ;#We don't know the context - next level up will have to check for key collisions? - set test_keys $table_keys - foreach k $dkeys { - lappend test_keys $k - if {![dict exists $datastructure {*}$test_keys]} { - dict set datastructure {*}$test_keys [list] - } else { - tomlish::log::notice "to_dict datastructure at (DOTTEDKEY) subkey $test_keys already had data: [dict get $datastructure {*}$test_keys]" - } - } - - if {[dict exists $datastructure {*}$table_keys {*}$dkeys $leaf_key]} { - error "Duplicate key '$table_keys $dkeys $leaf_key'. The key already exists at this level in the toml data. The toml data is not valid." - } - set keyval_dict [_get_keyval_value $element] - dict set datastructure {*}$table_keys {*}$dkeys $leaf_key $keyval_dict - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in ITABLE context. Expected only KEY,DQKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - } - ARRAY { - #arrays in toml are allowed to contain mixtures of types - set datastructure [list] - log::debug "--> processing array: $item" - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - STRING { - set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::utils::unescape_string $value]] - } - LITERAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - ITABLE { - #anonymous table - #lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - lappend datastructure [::tomlish::to_dict [list $element]] ;#store itables within arrays as raw dicts (possibly empty) - } - TABLE { - #invalid? shouldn't be output from from_dict - but could manually be constructed as such? review - #doesn't make sense as table needs a name? - #take as synonym for ITABLE? - error "to_dict TABLE within array unexpected" - } - ARRAY - MULTISTRING - MULTILITERAL { - #set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - } - WS - SEP - NEWLINE - COMMENT { - #ignore whitespace, commas, newlines and comments - } - default { - error "Unexpected value type '$type' found in array" - } - } - } - } - MULTILITERAL { - #triple squoted string - #first newline stripped only if it is the very first element - #(ie *immediately* following the opening delims) - #All whitespace other than newlines is within LITERALPARTS - # ------------------------------------------------------------------------- - #todo - consider extension to toml to allow indent-aware multiline literals - # how - propose as issue in toml github? Use different delim? e.g ^^^ ? - #e.g - # xxx=?'''abc - # def - # etc - # ''' - # - we would like to trimleft each line to the column following the opening delim - # ------------------------------------------------------------------------- - - log::debug "---> todict processing multiliteral: $item" - set parts [lrange $item 1 end] - if {[lindex $parts 0 0] eq "NEWLINE"} { - set parts [lrange $parts 1 end] ;#skip it - } - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - switch -exact -- $type { - LITERALPART { - append stringvalue [lindex $element 1] - } - NEWLINE { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - MULTISTRING { - #triple dquoted string - log::debug "---> to_dict processing multistring: $item" - set stringvalue "" - set idx 0 - set parts [lrange $item 1 end] - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - #We use STRINGPART in the tomlish representation as a distinct element to STRING - which would imply wrapping quotes to be reinserted - switch -exact -- $type { - STRING { - #todo - do away with STRING ? - #we don't build MULTISTRINGS containing STRING - but should we accept it? - tomlish::log::warn "double quoting a STRING found in MULTISTRING - should be STRINGPART?" - append stringvalue "\"[::tomlish::utils::unescape_string [lindex $element 1]]\"" - } - STRINGPART { - append stringvalue [::tomlish::utils::unescape_string [lindex $element 1]] - } - CONT { - #When the last non-whitespace character on a line is an unescaped backslash, - #it will be trimmed along with all whitespace (including newlines) up to the next non-whitespace character or closing delimiter - # review - we allow some whitespace in stringpart elements - can a stringpart ever be all whitespace? - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - append stringvalue "\\" ;#add the sep - } else { - #skip over ws without emitting - set idx [llength $parts] - } - } else { - set parts_til_nl [lrange $parts 0 $next_nl-1] - set non_ws [lsearch -index 0 -start $idx+1 -not $parts_til_nl WS] - if {$non_ws >= 0} { - append stringvalue "\\" - } else { - #skip over ws on this line - set idx $next_nl - #then have to check each subsequent line until we get to first non-whitespace - set trimming 1 - while {$trimming && $idx < [llength $parts]} { - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - } else { - set idx [llength $parts] - } - set trimming 0 - } else { - set non_ws [lsearch -index 0 -start $idx+1 -not [lrange $parts 0 $next_nl-1] WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - set trimming 0 - } else { - set idx $next_nl - #keep trimming - } - } - } - } - } - } - NEWLINE { - #if newline is first element - it is not part of the data of a multistring - if {$idx > 0} { - set val [lindex $element 1] - if {$val eq "nl"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - } - WS { - append stringvalue [lindex $element 1] - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - WS - COMMENT - NEWLINE { - #ignore - } - default { - error "Unexpected tag '$tag' in Tomlish list '$tomlish'" - } - } - } - return $datastructure - } - - - proc _from_dictval_tomltype {parents tablestack keys typeval} { - set type [dict get $typeval type] - set val [dict get $typeval value] - switch -- $type { - ARRAY { - set subitems [list] - foreach item $val { - lappend subitems [_from_dictval [list {*}$parents ARRAY] $tablestack $keys $item] SEP - } - if {[lindex $subitems end] eq "SEP"} { - set subitems [lrange $subitems 0 end-1] - } - return [list ARRAY {*}$subitems] - } - ITABLE { - if {$val eq ""} { - return ITABLE - } else { - return [_from_dictval [list {*}$parents ITABLE] $tablestack $keys $val] - } - } - MULTISTRING { - #value is a raw string that isn't encoded as tomlish - #create a valid toml snippet with the raw value and decode it to the proper tomlish MULTISTRING format - #We need to convert controls in $val to escape sequences - except for newlines - # - #consider an *option* to reformat for long lines? (perhaps overcomplex - byte equiv - but may fold in ugly places) - #we could use a line-length limit to decide when to put in a "line ending backslash" - #and even format it with a reasonable indent so that proper CONT and WS entries are made (?) REVIEW - # - #TODO - set tomlpart "x=\"\"\"\\\n" - append tomlpart $val "\"\"\"" - set tomlish [tomlish::decode::toml $tomlpart] - #e.g if val = " etc\nblah" - #TOMLISH {DOTTEDKEY {{KEY x}} = {MULTISTRING CONT {NEWLINE LF} {WS { }} {STRINGPART etc} {NEWLINE lf} {STRINGPART blah} } } - #lindex 1 3 is the MULTISTRING tomlish list - return [lindex $tomlish 1 3] - } - MULTILITERAL { - #MLL string can contain newlines - but still no control chars - #todo - validate - set tomlpart "x='''\n" - append tomlpart $val ''' - set tomlish [tomlish::decode::toml $tomlpart] - return [lindex $tomlish 1 3] - } - LITERAL { - #from v1.0 spec - "Control characters other than tab are not permitted in a literal string" - #(This rules out raw ANSI SGR - which is somewhat restrictive - but perhaps justified for a config format - # as copy-pasting ansi to a config value is probably not always wise, and it's not something that can be - # easily input via a text editor. ANSI can go in Basic strings using the \e escape if that's accepted v1.1?) - #we could choose to change the type to another format here when encountering invalid chars - but that seems - #like too much magic. We elect to error out and require the dict to have valid data for the types it specifies. - if {[string first ' $val] >=0} { - error "_from_dictval_tomltype error: single quote found in LITERAL - cannot encode dict to TOML-VALID TOMLISH" - } - #detect control chars other than tab - #for this we can use rawstring_to_Bstring_with_escaped_controls - even though this isn't a Bstring - #we are just using the map to detect a difference. - set testval [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val] - if {$testval ne $val} { - #some escaping would have to be done if this value was destined for a Bstring... - #therefor this string has controls and isn't suitable for a LITERAL according to the specs. - error "_from_dictval_tomltype error: control chars (other than tab) found in LITERAL value - cannot encode dict to TOML-VALID TOMLISH" - } - return [list LITERAL $val] - } - STRING { - return [list STRING [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val]] - } - INT { - if {![::tomlish::utils::is_int $val]} { - error "_from_dictval_tomltype error: bad INT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list INT $val] - } - FLOAT { - if {![::tomlish::utils::is_float $val]} { - error "_from_dictval_tomltype error: bad FLOAT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list FLOAT $val] - } - default { - if {$type ni [::tomlish::tags]} { - error "_from_dictval_tomltype error: Unrecognised typename '$type' in {type value } - cannot encode dict to TOML-VALID TOMLISH" - } - return [list $type $val] - } - } - } - - #take a raw string and classify: result is a 2 element list comprised of KEY|SQKEY|DQKEY and the value being the appropriate inner string - proc _from_dict_classify_key {rawval} { - if {![::tomlish::utils::is_barekey $rawval]} { - #requires quoting - # - #Any dot in the key would have been split by to_dict - so if it's present here it's part of this key - not a level separator! - # - #we'll use a basic mechanisms for now to determine the type of quoting - # - whether it has any single quotes or not. - # (can't go in an SQKEY) - # - whether it has any chars that require quoting when in a Bstring - # (if so - then its visual representation might be unsuitable for a key in a toml text file, so escape and put in DQKEY instead of literal SQKEY) - #todo - more? - #REVIEW - the backslash might often be in things like a regex or windows path - which is often better expressed in a literal SQKEY - # from literal examples: - # 'c:\Users\nodejs\templates' - # '<\i\c*\s*>' - #If these are in *keys* our basic test will express these as: - # "c:\\Users\\nodejs\\templates" - # "<\\i\\c*\\s*>" - # This still works - but a smarter test might determine when SQKEY is the better form? - #when coming from external systems - can we even know if the value was already escaped? REVIEW - #Probably when coming from json - we know it's already escaped - and so we build our dict converting keys to unescaped - #TODO - clarify in documentation that keys resulting from to_dict are in 'normalized' (unescaped) form - # - #For keys - we currently (2025) are only allowed barekeys,basic strings and literal strings. (no multiline forms) - set k_escaped [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $rawval] - if {[string length $k_escaped] != [string length $rawval]} { - #escaping made a difference - set has_escape_requirement 1 - } else { - set has_escape_requirement 0 - } - if {[string first ' $rawval] >=0 || $has_escape_requirement} { - #basic string - # (any ANSI SGR sequence will end up here in escaped form ) - return [list DQKEY $k_escaped] - } else { - #literal string - return [list SQKEY $rawval] - } - } else { - return [list KEY $rawval] - } - } - - #the quoting implies the necessary escaping for DQKEYs - proc _from_dict_join_and_quote_raw_keys {rawkeylist} { - set result "" - foreach rk $rawkeylist { - lassign [_from_dict_classify_key $rk] type val - switch -- $type { - SQKEY { - append result "'$val'." - } - DQKEY { - append result "\"$val\"." - } - KEY { - append result "$val." - } - } - } - return [string range $result 0 end-1] - } - proc _from_dictval {parents tablestack keys vinfo} { - set k [lindex $keys end] - set K_PART [_from_dict_classify_key $k] ;#get [list SQKEY ] - puts stderr "---parents:'$parents' keys:'$keys' vinfo: $vinfo---" - puts stderr "---tablestack: $tablestack---" - set result [list] - set lastparent [lindex $parents end] - if {$lastparent in [list "" do_inline]} { - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - set type [dict get $vinfo type] - #treat ITABLE differently? - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result DOTTEDKEY [list $K_PART {WS { }}] = {WS { }} $sublist {NEWLINE lf} - } else { - if {$vinfo ne ""} { - - #set result [list DOTTEDKEY [list [list KEY $k]] = ] - #set records [list ITABLE] - - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - - if {$lastparent eq "do_inline"} { - set result [list DOTTEDKEY [list $K_PART] =] - set records [list ITABLE] - } else { - set tname [_from_dict_join_and_quote_raw_keys [list $k]] - set result [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $k]] - set records [list] - } - - - - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - dict for {vk vv} $vinfo { - set VK_PART [_from_dict_classify_key $vk] ;#get [list SQKEY ] - #(SQKEY & DQKEY do not have the enclosing quotes in their returned val) - #if {[regexp {\s} $vk] || [string first . $vk] >= 0} { - # set VK_PART [list SQKEY $vk] - #} else { - # set VK_PART [list KEY $vk] - #} - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - #REVIEW - we could detect if value is an array of objects, - #and depending on parent context - emit a series of TABLEARRAY records instead of a DOTTEDKEY record containing an ARRAY of objects - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART {WS { }}] = {WS { }} $sublist] - } else { - if {$vv eq ""} { - #experimental - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline KEY $vk (tablestack:$tablestack)" - #set tname [tomlish::dict::name_from_tablestack [list {*}$tablestack [list T $vk]]] - - #we can't just join normalized keys - need keys with appropriate quotes and escapes - #set tname [join [list {*}$keys $vk] .] ;#WRONG - set tq [_from_dict_join_and_quote_raw_keys [list {*}$keys $vk]] - set record [list TABLE $tq {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - } else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - } - } else { - if { 0 } { - #experiment.. sort of getting there. - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - puts stderr "_from_dictval could uninline2 KEYS [list {*}$keys $vk] (tablestack:$tablestack)" - set tq [_from_dict_join_and_quote_raw_keys [list {*}$keys $vk]] - set record [list TABLE $tq {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - - #review - todo? - set dottedkey_value [_from_dictval [list {*}$parents TABLE] $tablestack [list {*}$keys $vk] $vv] - lappend record {*}$dottedkey_value - - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - } - if {$dictidx != $lastidx} { - #lappend record SEP - if {$lastparent eq "do_inline"} { - lappend record SEP - } else { - lappend record {NEWLINE lf} - } - } - lappend records $record - incr dictidx - } - if {$lastparent eq "do_inline"} { - lappend result $records {NEWLINE lf} - } else { - lappend result {*}$records {NEWLINE lf} - } - } else { - if {$lastparent eq "do_inline"} { - lappend result DOTTEDKEY [list $K_PART] = ITABLE {NEWLINE lf} - } else { - set tname [_from_dict_join_and_quote_raw_keys [list $k]] - lappend result TABLE $tname {NEWLINE lf} - } - } - } - } else { - #lastparent is not toplevel "" or "do_inline" - if {[tomlish::dict::is_tomlish_typeval $vinfo]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result {*}$sublist - } else { - if {$lastparent eq "TABLE"} { - #review - dict for {vk vv} $vinfo { - set VK_PART [_from_dict_classify_key $vk] ;#get [list SQKEY ] - set dottedkey_value [_from_dictval [list {*}$parents DOTTEDKEY] $tablestack [list {*}$keys $vk] $vv] - lappend result [list DOTTEDKEY [list $VK_PART] = $dottedkey_value {NEWLINE lf}] - } - } else { - if {$vinfo ne ""} { - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - set sub [list] - #REVIEW - #set result $lastparent ;#e.g sets ITABLE - set result ITABLE - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - dict for {vk vv} $vinfo { - set VK_PART [_from_dict_classify_key $vk] ;#get [list SQKEY ] - if {[tomlish::dict::is_tomlish_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART] = $sublist] - } else { - if {$vv eq ""} { - #can't just uninline at this level - #we need a better method to query main dict for uninlinability at each level - # (including what's been inlined already) - #if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - # puts stderr "_from_dictval uninline2 KEY $keys" - # set tname [_from_dict_join_and_quote_raw_keys [list {*}$keys $vk]] - # set record [list TABLE $tname {NEWLINE lf}] - # set tablestack [list {*}$tablestack [list T $vk]] - #} else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - #} - } else { - #set sub [_from_dictval ITABLE $vk $vv] - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - #set record [list DOTTEDKEY [list $VK_PART] = ITABLE $dottedkey_value] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - if {$dictidx != $lastidx} { - lappend record SEP - } - lappend result $record - incr dictidx - } - } else { - puts stderr "table x-1" - lappend result DOTTEDKEY [list $K_PART] = ITABLE - } - } - } - } - return $result - } - - - proc from_dict {d} { - #consider: - # t1={a=1,b=2} - # x = 1 - #If we represent t1 as an expanded table we get - # [t1] - # a=1 - # b=2 - # x=1 - # --- which is incorrect - as x was a toplevel key like t1! - #This issue doesn't occur if x is itself an inline table - # t1={a=1,b=2} - # x= {no="problem"} - # - # (or if we were to reorder x to come before t1) - - #As the dictionary form doesn't distinguish the structure used to create tables {[table1]\nk=v} vs inline {table1={k=v}} - #Without a solution, from_dict would have to always produce the inline form for toplevel tables unless we allowed re-ordering, - #which is unpreferred here. - - #A possible solution: - #scan the top level to see if all (trailing) elements are themselves dicts - # (ie not of form {type XXX value yyy}) - # - # A further point is that if all root level values are at the 'top' - we can treat lower table-like structures as {[table]} elements - #ie we don't need to force do_inline if all the 'simple' keys are before any compound keys - - #set root_has_values 0 - #approach 1) - the naive approach - forces inline when not always necessary - #dict for {k v} $d { - # if {[llength $v] == 4 && [lindex $v 0] eq "type"} { - # set root_has_values 1 - # break - # } - #} - - - #approach 2) - track the position of last {type x value y} in the dictionary built by to_dict - # - still not perfect. Inlines dotted tables unnecessarily - #This means from_dict doesn't produce output optimal for human editing. - set last_simple [tomlish::dict::last_tomltype_posn $d] - - - ## set parent "do_inline" ;#a value used in _from_dictval to distinguish from "" or other context based parent values - #Any keys that are themselves tables - will need to be represented inline - #to avoid reordering, or incorrect assignment of plain values to the wrong table. - - ## set parent "" - #all toplevel keys in the dict structure can represent subtables. - #we are free to use {[tablename]\n} syntax for toplevel elements. - - - set tomlish [list TOMLISH] - set dictposn 0 - set tablestack [list [list T root]] ;#todo - dict for {t tinfo} $d { - if {$last_simple > $dictposn} { - set parents [list do_inline] - } else { - set parents [list ""] - } - set keys [list $t] - #review - where to make decision on - # DOTTEDKEY containing array of objs - #vs - # list of TABLEARRAY records - #At least for the top - set trecord [_from_dictval $parents $tablestack $keys $tinfo] - lappend tomlish $trecord - incr dictposn - } - return $tomlish - } - - proc json_to_toml {json} { - #*** !doctools - #[call [fun json_to_toml] [arg json]] - #[para] - - set tomlish [::tomlish::from_json $json] - set toml [::tomlish::to_toml $tomlish] - } - - #TODO use huddle? - proc from_json {json} { - #set jstruct [::tomlish::json_struct $json] - #return [::tomlish::from_json_struct $jstruct] - package require huddle - package require huddle::json - set h [huddle::json::json2huddle parse $json] - - } - - proc from_json_struct {jstruct} { - package require fish::json_toml - return [fish::json_toml::jsonstruct2tomlish $jstruct] - } - - proc toml_to_json {toml} { - set tomlish [::tomlish::from_toml $toml] - return [::tomlish::get_json $tomlish] - } - - proc get_json {tomlish} { - package require fish::json - set d [::tomlish::to_dict $tomlish] - - #return [::tomlish::dict_to_json $d] - return [fish::json::from "struct" $d] - } - - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -namespace eval tomlish::build { - #STRING,INT,FLOAT,BOOL, DATETIME - simple wrappers for completeness - # take a value of the appropriate type and wrap as a tomlish tagged item - proc STRING {s} { - return [list STRING [::tomlish::utils::rawstring_to_Bstring_with_escaped_controls $s]] - } - proc LITERAL {litstring} { - error todo - } - - proc INT {i} { - #whole numbers, may be prefixed with a + or - - #Leading zeros are not allowed - #Hex,octal binary forms are allowed (toml 1.0) - #We will error out on encountering commas, as commas are interpreted differently depending on locale (and don't seem to be supported in the toml spec anyway) - #!todo - Tcl can handle bignums - bigger than a 64bit signed long as specified in toml. - # - We should probably raise an error for number larger than this and suggest the user supply it as a string? - if {[tcl::string::last , $i] > -1} { - error "Unable to interpret '$i' as an integer. Use underscores if you need a thousands separator [::tomlish::parse::report_line]" - } - if {![::tomlish::utils::int_validchars $i]} { - error "Unable to interpret '$i' as an integer. Only 0-9 + 1 _ characters are acceptable. [::tomlish::parse::report_line]" - } - - if {[::tomlish::utils::is_int $i]} { - return [list INT $i] - } else { - error "'$i' is not a valid integer as per the Toml spec. [::tomlish::parse::report_line]" - } - - } - - proc FLOAT {f} { - #convert any non-lower case variants of special values to lowercase for Toml - if {[::tcl::string::tolower $f] in {nan +nan -nan inf +inf -inf}} { - return [list FLOAT [tcl::string::tolower $f]] - } - if {[::tomlish::utils::is_float $f]} { - return [list FLOAT $f] - } else { - error "Unable to interpret '$f' as Toml float. Check your input, or check that tomlish is able to handle all Toml floats properly [::tomlish::parse::report_line]" - } - } - - proc DATETIME {str} { - if {[::tomlish::utils::is_datetime $str]} { - return [list DATETIME $str] - } else { - error "Unable to interpret '$str' as Toml datetime. Check your input, or check that tomlish is able to handle all Toml datetimes properly [::tomlish::parse::report_line]" - } - } - - proc BOOLEAN {b} { - #convert any Tcl-acceptable boolean to boolean as accepted by toml - lower case true/false - if {![tcl::string::is boolean -strict $b]} { - error "Unable to convert '$b' to Toml boolean true|false. [::tomlish::parse::report_line]" - } else { - if {$b && 1} { - return [::list BOOL true] - } else { - return [::list BOOL false] - } - } - } - - #REVIEW - #Take tablename followed by - # a) *tomlish* name-value pairs e.g table mydata [list KEY item11 = [list STRING "test"]] {KEY item2 = [list INT 1]} - # (accept also key value {STRING }) - # b) simple 2-element tcl lists being name & *simple* value pairs for which basic heuristics will be used to determine types - proc _table {name args} { - set pairs [list] - foreach t $args { - if {[llength $t] == 4} { - if {[tcl::string::tolower [lindex $t 0]] ne "key" || [tcl::string::tolower [lindex $t 2]] ni "= value"} { - error "Only items tagged as KEY = currently accepted as name-value pairs for table command" - } - lassign $t _k keystr _eq valuepart - if {[llength $valuepart] != 2} { - error "supplied value must be typed. e.g {INT 1} or {STRING test}" - } - lappend pairs [list KEY $keystr = $valuepart] - } elseif {[llength $t] == 2} { - #!todo - type heuristics - lassign $t n v - lappend pairs [list KEY $n = [list STRING $v]] - } else { - error "'KEY = { toml but - # the first newline is not part of the data. - # we elect instead to maintain a basic LITERALPART that must not contain newlines.. - # and to compose MULTILITERAL of multiple NEWLINE LITERALPART parts, - #with the datastructure representation dropping the first newline (if immediately following opening delim) when building the value. - set literal "" - foreach part [lrange $item 1 end] { - append literal [::tomlish::encode::tomlish [list $part] $nextcontext] - } - append toml '''$literal''' - } - INT - - BOOL - - FLOAT - - DATETIME { - append toml [lindex $item 1] - } - INCOMPLETE { - error "cannot process tomlish term tagged as INCOMPLETE" - } - COMMENT { - append toml "#[lindex $item 1]" - } - BOM { - #Byte Order Mark may appear at beginning of a file. Needs to be preserved. - append toml "\uFEFF" - } - default { - error "Not a properly formed 'tomlish' taggedlist.\n '$list'\n Unknown tag '[lindex $item 0]'. See output of \[tomlish::tags\] command." - } - } - - } - return $toml - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::encode ---}] -} -#fish toml from tomlish - -#(encode tomlish as toml) -interp alias {} tomlish::to_toml {} tomlish::encode::tomlish - -# - - -namespace eval tomlish::decode { - #*** !doctools - #[subsection {Namespace tomlish::decode}] - #[para] - #[list_begin definitions] - - #return a Tcl list of tomlish tokens - #i.e get a standard list of all the toml terms in string $s - #where each element of the list is a *tomlish* term.. i.e a specially 'tagged' Tcl list. - #(simliar to a tcl 'Huddle' - but also supporting whitespace preservation) - # ---------------------------------------------------------------------------------------------- - # NOTE: the production of tomlish from toml source doesn't indicate the toml source was valid!!! - # e.g we deliberately don't check certain things such as duplicate table declarations here. - # ---------------------------------------------------------------------------------------------- - #Part of the justification for this is that as long as the syntax is toml shaped - we can load files which violate certain rules and allow programmatic manipulation. - # (e.g perhaps a toml editor to highlight violations for fixing) - # A further stage is then necessary to load the tomlish tagged list into a data structure more suitable for efficient query/reading. - # e.g dicts or an object oriented structure - #Note also - *no* escapes in quoted strings are processed. This is up to the datastructure stage - #e.g to_dict will substitute \r \n \uHHHH \UHHHHHHH etc - #This is important for tomlish to maintain the ability to perform competely lossless round-trips from toml to tomlish and back to toml. - # (which is handy for testing as well as editing some part of the structure with absolutely no effect on other parts of the document) - #If we were to unescape a tab character for example - # - we have no way of knowing if it was originally specified as \t \u0009 or \U00000009 or directly as a tab character. - # For this reason, we also do absolutely no line-ending transformations based on platform. - # All line-endings are maintained as is, and even a file with mixed lf crlf line-endings will be correctly interpreted and can be 'roundtripped' - - proc toml {args} { - #*** !doctools - #[call [fun toml] [arg arg...]] - #[para] return a Tcl list of tomlish tokens - - set s [join $args \n] - - namespace upvar ::tomlish::parse is_parsing is_parsing - set is_parsing 1 - - if {[info command ::tomlish::parse::spacestack] eq "::tomlish::parse::spacestack"} { - tomlish::parse::spacestack destroy - } - struct::stack ::tomlish::parse::spacestack - - namespace upvar ::tomlish::parse last_space_action last_space_action - namespace upvar ::tomlish::parse last_space_type last_space_type - - namespace upvar ::tomlish::parse tok tok - set tok "" - - namespace upvar ::tomlish::parse type type - namespace upvar ::tomlish::parse tokenType tokenType - ::tomlish::parse::set_tokenType "" - namespace upvar ::tomlish::parse tokenType_list tokenType_list - set tokenType [list] ;#Flat (un-nested) list of tokentypes found - - namespace upvar ::tomlish::parse lastChar lastChar - set lastChar "" - - - set result "" - namespace upvar ::tomlish::parse nest nest - set nest 0 - - namespace upvar ::tomlish::parse v v ;#array keyed on nest level - - - set v(0) {TOMLISH} - array set s0 [list] ;#whitespace data to go in {SPACE {}} element. - set parentlevel 0 - - namespace upvar ::tomlish::parse i i - set i 0 - - namespace upvar ::tomlish::parse state state - - namespace upvar ::tomlish::parse braceCount braceCount - set barceCount 0 - namespace upvar ::tomlish::parse bracketCount bracketCount - set bracketCount 0 - - set sep 0 - set r 1 - namespace upvar ::tomlish::parse token_waiting token_waiting - set token_waiting [dict create] ;#if ::tok finds a *complete* second token during a run, it will put the 2nd one here to be returned by the next call. - - - set state "table-space" - ::tomlish::parse::spacestack push {type space state table-space} - namespace upvar ::tomlish::parse linenum linenum;#'line number' of input data. (incremented for each literal linefeed - but not escaped ones in data) - set linenum 1 - - set ::tomlish::parse::state_list [list] - try { - while {$r} { - set r [::tomlish::parse::tok $s] - #puts stdout "got tok: '$tok' while parsing string '$s' " - set next_tokenType_known 0 ;#whether we begin a new token here based on what terminated the token result of 'tok' - - - #puts "got token: '$tok' tokenType='$tokenType'. while v($nest) = [set v($nest)]" - #puts "-->tok: $tok tokenType='$tokenType'" - set prevstate $state - set transition_info [::tomlish::parse::goNextState $tokenType $tok $state] - #review goNextState could perform more than one space_action - set space_action [dict get $transition_info space_action] - set newstate [dict get $transition_info newstate] ;#use of 'newstate' vs 'state' makes code clearer below - - if {[tcl::string::match "err-*" $state]} { - ::tomlish::log::warn "---- State error in state $prevstate for tokenType: $tokenType token value: $tok. $state aborting parse. [tomlish::parse::report_line]" - lappend v(0) [list ERROR tokentype $tokenType state $prevstate to $state leveldata [set v($nest)]] - return $v(0) - } - # --------------------------------------------------------- - #NOTE there may already be a token_waiting at this point - #set_token_waiting can raise an error here, - # in which case the space_action branch needs to be rewritten to handle the existing token_waiting - # --------------------------------------------------------- - - if {$space_action eq "pop"} { - #pop_trigger_tokens: newline tablename endarray endinlinetable - #note a token is a pop trigger depending on context. e.g first newline during keyval is a pop trigger. - set parentlevel [expr {$nest -1}] - set do_append_to_parent 1 ;#most tokens will leave this alone - but some like tentative_accum_squote need to do their own append - switch -exact -- $tokenType { - tentative_accum_squote { - #should only apply within a multiliteral - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the xxx-squote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-squote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-squote-space { - } - default { - error "--- unexpected popped due to tentative_accum_squote but came from state '$prevstate' should have been trailing-squote-space" - } - } - switch -- $tok { - ' { - tomlish::parse::set_token_waiting type single_squote value $tok complete 1 startindex [expr {$i -1}] - } - '' { - #review - we should perhaps return double_squote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_squote value "" complete 1 startindex [expr {$i - 2}] - } - ''' { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 3}] - } - '''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left squote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]'" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "'"] - } - MULTILITERAL { - #empty - lappend v($parentlevel) [list LITERALPART "'"] - } - default { - error "--- don't know how to integrate extra trailing squote with data $v($parentlevel)" - } - } - } - ''''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 squotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]''" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "''"] - } - MULTILITERAL { - lappend v($parentlevel) [list LITERALPART "''"] - } - default { - error "--- don't know how to integrate extra trailing 2 squotes with data $v($parentlevel)" - } - } - } - } - } - triple_squote { - #presumably popping multiliteral-space - ::tomlish::log::debug "---- triple_squote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTILITERAL { - lappend merged $part - } - LITERALPART { - if {$lasttype eq "LITERALPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_squote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - tentative_accum_dquote { - #should only apply within a multistring - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the trailing-dquote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-dquote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-dquote-space { - } - default { - error "--- unexpected popped due to tentative_accum_dquote but came from state '$prevstate' should have been trailing-dquote-space" - } - } - switch -- $tok { - {"} { - tomlish::parse::set_token_waiting type single_dquote value $tok complete 1 startindex [expr {$i -1}] - } - {""} { - #review - we should perhaps return double_dquote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_dquote value "" complete 1 startindex [expr {$i - 2}] - } - {"""} { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 3}] - } - {""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left dquote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {"}] - } - MULTISTRING { - #empty - lappend v($parentlevel) [list STRINGPART {"}] - } - default { - error "--- don't know how to integrate extra trailing dquote with data $v($parentlevel)" - } - } - } - {"""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 dquotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {""}] - } - MULTISTRING { - lappend v($parentlevel) [list STRINGPART {""}] - } - default { - error "--- don't know how to integrate extra trailing 2 dquotes with data $v($parentlevel)" - } - } - } - } - } - triple_dquote { - #presumably popping multistring-space - ::tomlish::log::debug "---- triple_dquote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTISTRING { - lappend merged $part - } - STRINGPART { - if {$lasttype eq "STRINGPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - CONT - WS { - lappend merged $part - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_dquote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - equal { - #pop caused by = - switch -exact -- $prevstate { - dottedkey-space { - tomlish::log::debug "---- equal ending dottedkey-space for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - dottedkey-space-tail { - #experiment? - tomlish::log::debug "---- equal ending dottedkey-space-tail for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - } - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - tablename { - #note: a tablename only 'pops' if we are greater than zero - error "---- tablename pop should already have been handled as special case zeropoppushspace in goNextState" - } - tablearrayname { - #!review - tablearrayname different to tablename regarding push/pop? - #note: a tablename only 'pops' if we are greater than zero - error "---- tablearrayname pop should already have been handled as special case zeropoppushspace in goNextState" - } - endarray { - #nothing to do here. - } - comma { - #comma for inline table will pop the keyvalue space - lappend v($nest) "SEP" - } - endinlinetable { - ::tomlish::log::debug "---- endinlinetable for last_space_action pop" - } - default { - error "---- unexpected tokenType '$tokenType' for last_space_action 'pop'" - } - } - if {$do_append_to_parent} { - #e.g tentative_accum_squote does it's own appends as necessary - so won't get here - lappend v($parentlevel) [set v($nest)] - } - - incr nest -1 - - } elseif {$last_space_action eq "push"} { - set prevnest $nest - incr nest 1 - set v($nest) [list] - # push_trigger_tokens: barekey dquotedkey startinlinetable startarray tablename tablearrayname - - - switch -exact -- $tokenType { - tentative_trigger_squote - tentative_trigger_dquote { - #### this startok will always be tentative_accum_squote/tentative_accum_dquote starting with one accumulated squote/dquote - if {[dict exists $transition_info starttok] && [dict get $transition_info starttok] ne ""} { - lassign [dict get $transition_info starttok] starttok_type starttok_val - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType $starttok_type - set tok $starttok_val - } - } - single_squote { - #JMN - REVIEW - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - triple_squote { - ::tomlish::log::debug "---- push trigger tokenType triple_squote" - set v($nest) [list MULTILITERAL] ;#container for NEWLINE,LITERALPART - } - squotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - triple_dquote { - set v($nest) [list MULTISTRING] ;#container for NEWLINE,STRINGPART,CONT - } - dquotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - barekey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - set waiting [tomlish::parse::get_token_waiting] - if {[llength $waiting]} { - set i [dict get $waiting startindex] - tomlish::parse::clear_token_waiting - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } else { - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - } - tablename { - #note: we do not use the output of tomlish::to_dict::tablename_trim to produce a tablename for storage in the tomlish list! - #The tomlish list is intended to preserve all whitespace (and comments) - so a roundtrip from toml file to tomlish - # back to toml file will be identical. - #It is up to the datastructure stage to normalize and interpret tomlish for programmatic access. - # we call tablename_trim here only to to validate that the tablename data is well-formed at the outermost level, - # so we can raise an error at this point rather than create a tomlish list with obviously invalid table names from - # a structural perspective. - - #todo - review! It's arguable that we should not do any validation here, and just store even incorrect raw tablenames, - # so that the tomlish list is more useful for say a toml editor. Consider adding an 'err' tag to the appropriate place in the - # tomlish list? - - #set trimtable [::tomlish::to_dict::tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablename: '$trimtable'" - set v($nest) [list TABLE $tok] ;#$tok is the *raw* table name - #note also that equivalent tablenames may have different toml representations even after being trimmed! - #e.g ["x\t\t"] & ["x "] (tab escapes vs literals) - #These will show as above in the tomlish list, but should normalize to the same tablename when used as keys by the datastructure stage. - } - tablearrayname { - #set trimtable [::tomlish::to_dict::tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablearrayname: '$trimtable'" - set v($nest) [list TABLEARRAY $tok] ;#$tok is the *raw* tablearray name - } - startarray { - set v($nest) [list ARRAY] ;#$tok is just the opening bracket - don't output. - } - startinlinetable { - set v($nest) [list ITABLE] ;#$tok is just the opening curly brace - don't output. - } - default { - error "---- push trigger tokenType '$tokenType' not yet implemented" - } - } - - } else { - #no space level change - switch -exact -- $tokenType { - squotedkey { - #puts "---- squotedkey in state $prevstate (no space level change)" - lappend v($nest) [list SQKEY $tok] - } - dquotedkey { - #puts "---- dquotedkey in state $prevstate (no space level change)" - lappend v($nest) [list DQKEY $tok] - } - barekey { - lappend v($nest) [list KEY $tok] - } - dotsep { - lappend v($nest) [list DOTSEP] - } - starttablename { - #$tok is triggered by the opening bracket and sends nothing to output - } - starttablearrayname { - #$tok is triggered by the double opening brackets and sends nothing to output - } - tablename - tablenamearray { - error "---- did not expect 'tablename/tablearrayname' without space level change (no space level change)" - #set v($nest) [list TABLE $tok] - } - endtablename - endtablearrayname { - #no output into the tomlish list for this token - } - startinlinetable { - puts stderr "---- decode::toml error. did not expect startinlinetable without space level change (no space level change)" - } - single_dquote { - switch -exact -- $newstate { - string-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "string" - set tok "" - } - dquoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "dquotedkey" - set tok "" - } - multistring-space { - lappend v($nest) [list STRINGPART {"}] - #may need to be joined on pop if there are neighbouring STRINGPARTS - } - default { - error "---- single_dquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_dquote { - #leading extra quotes - test: toml_multistring_startquote2 - switch -exact -- $prevstate { - itable-keyval-value-expected - keyval-value-expected { - puts stderr "tomlish::decode::toml double_dquote TEST" - #empty string - lappend v($nest) [list STRINGPART ""] - } - multistring-space { - #multistring-space to multistring-space - lappend v($nest) [list STRINGPART {""}] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - - } - single_squote { - switch -exact -- $newstate { - literal-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "literal" - set tok "" - } - squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - multiliteral-space { - #false alarm squote returned from tentative_accum_squote pop - ::tomlish::log::debug "---- adding lone squote to own LITERALPART nextstate: $newstate (no space level change)" - #(single squote - not terminating space) - lappend v($nest) [list LITERALPART '] - #may need to be joined on pop if there are neighbouring LITERALPARTs - } - default { - error "---- single_squote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_squote { - switch -exact -- $prevstate { - keyval-value-expected { - lappend v($nest) [list LITERAL ""] - } - multiliteral-space { - #multiliteral-space to multiliteral-space - lappend v($nest) [list LITERALPART ''] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - enddquote { - #nothing to do? - set tok "" - } - endsquote { - set tok "" - } - string { - lappend v($nest) [list STRING $tok] ;#directly wrapped in dquotes - } - literal { - lappend v($nest) [list LITERAL $tok] ;#directly wrapped in squotes - } - multistring { - #review - lappend v($nest) [list MULTISTRING $tok] - } - stringpart { - lappend v($nest) [list STRINGPART $tok] ;#will not get wrapped in dquotes directly - } - multiliteral { - lappend v($nest) [LIST MULTILITERAL $tok] - } - literalpart { - lappend v($nest) [list LITERALPART $tok] ;#will not get wrapped in squotes directly - } - untyped_value { - #would be better termed unclassified_value - #we can't determine the type of unquoted values (int,float,datetime,bool) until the entire token was read. - unset -nocomplain tag - if {$tok in {true false}} { - set tag BOOL - } else { - if {[::tomlish::utils::is_int $tok]} { - set tag INT - } else { - if {[string is integer -strict $tok]} { - #didn't qualify as a toml int - but still an int - #probably means is_int is limiting size and not accepting bigints (configurable?) - #or it didn't qualify due to more than 1 leading zero - #or other integer format issue such as repeated underscores - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. (looks close to being an int. Formatting or range issue?) [tomlish::parse::report_line] (no space level change)" - } else { - if {[::tomlish::utils::is_float $tok]} { - set tag FLOAT - } elseif {[::tomlish::utils::is_datetime $tok] || [::tomlish::utils::is_timepart $tok]} { - #Note we must allow lone timepart here (not just is_localtime which doesn't allow tz offsets) in case it followed a localdate - #e.g x= 2025-01-01 02:34Z - #The to_dict validation will catch an invalid standaline timepart, or combine with leading date if applicable. - set tag DATETIME - } else { - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. [tomlish::parse::report_line] (no space level change)" - } - } - } - } - #assert either tag is set, or we errored out. - lappend v($nest) [list $tag $tok] - - } - comment { - #puts stdout "----- comment token returned '$tok'------" - lappend v($nest) [list COMMENT "$tok"] - } - equal { - #we append '=' to the nest so that any surrounding whitespace is retained. - lappend v($nest) = - } - comma { - lappend v($nest) SEP - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - whitespace { - lappend v($nest) [list WS $tok] - } - continuation { - lappend v($nest) CONT - } - bom { - lappend v($nest) BOM - } - eof { - #ok - nothing more to add to the tomlish list. - #!todo - check previous tokens are complete/valid? - } - default { - error "--- unknown tokenType '$tokenType' during state $prevstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - - if {!$next_tokenType_known} { - ::tomlish::log::notice "---- tomlish::decode::toml - current tokenType:$tokenType Next token type not known" - ::tomlish::parse::set_tokenType "" - set tok "" - } - - if {$state eq "end-state"} { - break - } - - - } - - #while {$nest > 0} { - # lappend v([expr {$nest -1}]) [set v($nest)] - # incr nest -1 - #} - while {[::tomlish::parse::spacestack size] > 1} { - ::tomlish::parse::spacestack pop - lappend v([expr {$nest -1}]) [set v($nest)] - incr nest -1 - - #set parent [spacestack peek] ;#the level being appended to - #lassign $parent type state - #if {$type eq "space"} { - # - #} elseif {$type eq "buffer"} { - # lappend v([expr {$nest -1}]) {*}[set v($nest)] - #} else { - # error "invalid spacestack item: $parent" - #} - } - - } finally { - set is_parsing 0 - } - return $v(0) - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::decode ---}] -} -#decode toml to tomlish -interp alias {} tomlish::from_toml {} tomlish::decode::toml - -namespace eval tomlish::utils { - #*** !doctools - #[subsection {Namespace tomlish::utils}] - #[para] - #[list_begin definitions] - - - - #basic generic quote matching for single and double quotes - #note for example that {[o'malley]} will return sq - as the single quote is not closed or wrapped in double quotes - proc tok_in_quotedpart {tok} { - set sLen [tcl::string::length $tok] - set quote_type "" - set had_slash 0 - for {set i 0} {$i < $sLen} {incr i} { - set c [tcl::string::index $tok $i] - if {$quote_type eq ""} { - if {$had_slash} { - #don't enter quote mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - set quote_type dq - } - sq { - set quote_type sq - } - bsl { - set had_slash 1 - } - } - } - } else { - if {$had_slash} { - #don't leave quoted mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - if {$quote_type eq "dq"} { - set quote_type "" - } - } - sq { - if {$quote_type eq "sq"} { - set quote_type "" - } - } - bsl { - set had_slash 1 - } - } - } - } - } - return $quote_type ;#dq | sq - } - - - proc unicode_escape_info {slashu} { - #!todo - # validate that slashu is either a \uxxxx or \Uxxxxxxxx value of the correct length and - # is a valid 'unicode scalar value' (any Unicode code point except high-surrogate and low-surrogate code points) - # ie integers in the range 0 to D7FF16 and E00016 to 10FFFF16 inclusive - #expr {(($x >= 0) && ($x <= 0xD7FF16)) || (($x >= 0xE00016) && ($x <= 0x10FFFF16))} - if {[tcl::string::match {\\u*} $slashu]} { - set exp {^\\u([0-9a-fA-F]{4}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %4x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } - } else { - return [list err [list reason "Supplied string not of the form \\uHHHH where H in \[0-9a-fA-F\]"]] - } - } elseif {[tcl::string::match {\\U*} $slashu]} { - set exp {^\\U([0-9a-fA-F]{8}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %8x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - if {(($dec >= 0) && ($dec <= 0xD7FF16)) || (($dec >= 0xE00016) && ($dec <= 0x10FFFF16))} { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } else { - return [list err [list reason "$slashu is not within the 'unicode scalar value' ranges 0 to 0xD7FF16 or 0xE00016 to 0x10FFFF16"]] - } - } - } else { - return [list err [list reason "Supplied string not of the form \\UHHHHHHHH where H in \[0-9a-fA-F\]"]] - } - } else { - return [list err [list reason "Supplied string did not start with \\u or \\U" ]] - } - - } - - #Note that unicode characters don't *have* to be escaped. - #So if we provide a function named 'escape_string', the name implies the inverse of unescape_string which unescapes unicode \u \U values. - #- an inverse of unescape_string would encode all unicode chars unnecessarily. - #- as toml accepts a compact escape sequence for common chars such as tab,backspace,linefeed etc but also allows the full form \u009 etc - #- escape_string and unescape_string would not be reliably roundtrippable inverses anyway. - #REVIEW - provide it anyway? When would it be desirable to use? - - variable Bstring_control_map [dict create] - dict set Bstring_control_map \b {\b} - dict set Bstring_control_map \n {\n} - dict set Bstring_control_map \r {\r} - dict set Bstring_control_map \" {\"} - dict set Bstring_control_map \x1b {\e} ;#In spec it's included in the list of 'must be escaped', as well as the 'convenience' escapes - so we make it go both ways. - dict set Bstring_control_map \\ "\\\\" - - #\e for \x1b seems like it might be included - v1.1?? hard to find current state of where toml is going :/ - #for a Bstring (Basic string) tab is explicitly mentioned as not being one that must be escaped. - #8 = \b - already in list. - #built the remainder whilst checking for entries already hardcoded above -in case more are added to the hardcoded list - for {set cdec 0} {$cdec <= 7} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - for {set cdec [expr {0x0A}]} {$cdec <= 0x1F} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - # \u007F = 127 - dict set Bstring_control_map [format %c 127] \\u007F - - #Note the inclusion of backslash in the list of controls makes this non idempotent - subsequent runs would keep encoding the backslashes! - #escape only those chars that must be escaped in a Bstring (e.g not tab which can be literal or escaped) - #for example - can be used by from_dict to produce valid Bstring data for a tomlish record - proc rawstring_to_Bstring_with_escaped_controls {str} { - #for the well known chars that have compact escape sequences allowed by toml - we choose that form over the full \u form. - #we'll use a string map with an explicit list rather than algorithmic at runtime - # - the string map is probably more performant than splitting a string, especially if it's large - variable Bstring_control_map - return [string map $Bstring_control_map $str] - } - - #review - unescape what string? Bstring vs MLBstring? - #we should be specific in the function naming here - #used by to_dict - so part of validation? - REVIEW - proc unescape_string {str} { - #note we can't just use Tcl subst because: - # it also transforms \a (audible bell) and \v (vertical tab) which are not in the toml spec. - # it would strip out backslashes inappropriately: e.g "\j" becomes just j - # it recognizes other escapes which aren't approprite e.g \xhh and octal \nnn - # it replaces \ with a single whitespace (trailing backslash) - #This means we shouldn't use 'subst' on the whole string, but instead substitute only the toml-specified escapes (\r \n \b \t \f \\ \" \uhhhh & \Uhhhhhhhh - #plus \e for \x1b? - - set buffer "" - set buffer4 "" ;#buffer for 4 hex characters following a \u - set buffer8 "" ;#buffer for 8 hex characters following a \u - - set sLen [tcl::string::length $str] - - #we need to handle arbitrarily long sequences of backslashes. \\\\\ etc - set slash_active 0 - set unicode4_active 0 - set unicode8_active 0 - - ::tomlish::log::debug "unescape_string. got len [string length str] str $str" - - #!todo - check for invalid data in the form of a raw carriage return (decimal 13) without following linefeed? - set i 0 - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $str [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $str $i] - #::tomlish::log::debug "unescape_string. got char $c" ;#too much? - - #---------------------- - #as we are 'unescaping' - should we really be testing for existing values that should have been escaped? - #this test looks incomplete anyway REVIEW - scan $c %c n - if {($n <= 31) && ($n != 9) && ($n != 10) && ($n != 13)} { - #we don't expect unescaped unicode characters from 0000 to 001F - - #*except* for raw tab (which is whitespace) and newlines - error "unescape_string. Invalid data for a toml string. Unescaped control character (decimal $n) [::tomlish::utils::string_to_slashu $c]" - } - #---------------------- - - incr i ;#must incr here because we do'returns'inside the loop - if {$c eq "\\"} { - if {$slash_active} { - append buffer "\\" - set slash_active 0 - } elseif {$unicode4_active} { - error "unescape_string. unexpected case slash during unicode4 not yet handled" - } elseif {$unicode8_active} { - error "unescape_string. unexpected case slash during unicode8 not yet handled" - } else { - # don't output anything (yet) - set slash_active 1 - } - } else { - if {$unicode4_active} { - if {[tcl::string::length $buffer4] < 4} { - append buffer4 $c - } - if {[tcl::string::length $buffer4] == 4} { - #we have a \uHHHH to test - set unicode4_active 0 - set result [tomlish::utils::unicode_escape_info "\\u$buffer4"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode8_active} { - if {[tcl::string::length $buffer8] < 8} { - append buffer8 $c - } - if {[tcl::string::length $buffer8] == 8} { - #we have a \UHHHHHHHH to test - set unicode8_active 0 - set result [tomlish::utils::unicode_escape_info "\\U$buffer8"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$slash_active} { - set slash_active 0 - set ctest [tcl::string::map {{"} dq} $c] - switch -exact -- $ctest { - dq { - append buffer {"} - } - b - t - n - f - r { - append buffer [subst -nocommand -novariable "\\$c"] - } - e { - append buffer \x1b - } - u { - set unicode4_active 1 - set buffer4 "" - } - U { - set unicode8_active 1 - set buffer8 "" - } - default { - set slash_active 0 - #review - toml spec says all other escapes are reserved - #and if they are used TOML should produce an error. - #we leave detecting this for caller for now - REVIEW - append buffer "\\$c" - } - } - } else { - append buffer $c - } - } - } - #puts stdout "EOF 4:$unicode4_active 8:$unicode8_active slash:$slash_active" - if {$unicode4_active} { - error "End of string reached before complete unicode escape sequence \uHHHH" - } - if {$unicode8_active} { - error "End of string reached before complete unicode escape sequence \UHHHHHHHH" - } - if {$slash_active} { - append buffer "\\" - } - return $buffer - } - - #This does not have to do with unicode normal forms - which it seems toml has decided against regarding use in keys (review/references?) - #This is meant for internal use regarding ensuring we match equivalent keys which may have just been specified with different string mechanisms, - #e.g squoted vs dquoted vs barekey. - proc normalize_key {rawkey} { - set c1 [tcl::string::index $rawkey 0] - set c2 [tcl::string::index $rawkey end] - if {($c1 eq "'") && ($c2 eq "'")} { - #single quoted segment. No escapes allowed within it. - set key [tcl::string::range $rawkey 1 end-1] - } elseif {($c1 eq "\"") && ($c2 eq "\"")} { - #double quoted segment. Apply escapes. - # - set keydata [tcl::string::range $rawkey 1 end-1] ;#strip outer quotes only - #e.g key could have mix of \UXXXXXXXX escapes and unicode chars - #or mix of \t and literal tabs. - #unescape to convert all to literal versions for comparison - set key [::tomlish::utils::unescape_string $keydata] - #set key [subst -nocommands -novariables $keydata] ;#wrong. Todo - create a string escape substitution function. - } else { - set key $rawkey - } - return $key - } - - proc string_to_slashu {string} { - set rv {} - foreach c [split $string {}] { - scan $c %c cdec - if {$cdec > 65535} { - append rv {\U} [format %.8X $cdec] - } else { - append rv {\u} [format %.4X $cdec] - } - } - return $rv - } - - #'nonprintable' is conservative here because some systems (e.g windows console) are very limited in what they can display. - #This is used for display purposes only (error msgs) - proc nonprintable_to_slashu {s} { - set res "" - foreach i [split $s ""] { - scan $i %c cdec - - set printable 0 - if {($cdec>31) && ($cdec<127)} { - set printable 1 - } - if {$printable} { - append res $i - } else { - if {$cdec > 65535} { - append res \\U[format %.8X $cdec] - } else { - append res \\u[format %.4X $cdec] - } - } - } - set res - } ;# initial version from tcl wiki RS - - #check if str is valid for use as a toml bare key - #Early toml versions? only allowed letters + underscore + dash - proc is_barekey1 {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } else { - set matches [regexp -all {[a-zA-Z0-9\_\-]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters match the regexp - return 1 - } else { - return 0 - } - } - } - - #from toml.abnf in github.com/toml-lang/toml - #unquoted-key = 1*unquoted-key-char - #unquoted-key-char = ALPHA / DIGIT / %x2D / %x5F ; a-z A-Z 0-9 - _ - #unquoted-key-char =/ %xB2 / %xB3 / %xB9 / %xBC-BE ; superscript digits, fractions - #unquoted-key-char =/ %xC0-D6 / %xD8-F6 / %xF8-37D ; non-symbol chars in Latin block - #unquoted-key-char =/ %x37F-1FFF ; exclude GREEK QUESTION MARK, which is basically a semi-colon - #unquoted-key-char =/ %x200C-200D / %x203F-2040 ; from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - #unquoted-key-char =/ %x2070-218F / %x2460-24FF ; include super-/subscripts, letterlike/numberlike forms, enclosed alphanumerics - #unquoted-key-char =/ %x2C00-2FEF / %x3001-D7FF ; skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - #unquoted-key-char =/ %x2070-21FF / %x2300-24FF ; skip math operators - #unquoted-key-char =/ %x25A0-268B / %x2690-2757 ; skip box drawing, block elements, and some yin-yang symbols - #unquoted-key-char =/ %x2762-2767 / %x2776-27E5 ; skip some Dingbat punctuation - #unquoted-key-char =/ %x2801-297F ; skip some math brackets and arrows, and braille blank - #unquoted-key-char =/ %x2B00-2FFF / %x3001-D7FF ; skip various math operators and symbols, and ideographic space - #unquoted-key-char =/ %xF900-FDCF / %xFDF0-FFFD ; skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - #unquoted-key-char =/ %x10000-EFFFF ; all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - variable re_barekey - set ranges [list] - lappend ranges {a-zA-Z0-9\_\-} - lappend ranges {\u00B2} {\u00B3} {\u00B9} {\u00BC-\u00BE} ;# superscript digits, fractions - lappend ranges {\u00C0-\u00D6} {\u00D8-\u00F6} {\u00F8-\u037D} ;# non-symbol chars in Latin block - lappend ranges {\u037f-\u1FFF} ;# exclude GREEK QUESTION MARK, which is basically a semi-colon - lappend ranges {\u200C-\u200D} {\u203F-\u2040} ;# from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - lappend ranges {\u2070-\u218f} {\u2460-\u24FF} ;# include super-subscripts, letterlike/numberlike forms, enclosed alphanumerics - lappend ranges {\u2C00-\u2FEF} {\u3001-\uD7FF} ;# skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - lappend ranges {\u2070-\u21FF} {\u2300-\u24FF} ;# skip math operators - lappend ranges {\u25A0-\u268B} {\u2690-\u2757} ;# skip box drawing, block elements, and some yin-yang symbols - lappend ranges {\u2762-\u2767} {\u2776-\u27E5} ;# skip some Dingbat punctuation - lappend ranges {\u2801-\u297F} ;# skip some math brackets and arrows, and braille blank - lappend ranges {\u2B00-\u2FFF} {\u3001-\uD7FF} ;# skip various math operators and symbols, and ideographic space - lappend ranges {\uF900-\uFDCF} {\uFDF0-\uFFFD} ;# skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - lappend ranges {\U10000-\UEFFFF} ;# all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - set re_barekey {^[} - foreach r $ranges { - append re_barekey $r - } - append re_barekey {]+$} - - proc is_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } - variable re_barekey - return [regexp $re_barekey $str] - } - - #test only that the characters in str are valid for the toml specified type 'integer'. - proc int_validchars1 {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_\-\+]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - #add support for hex,octal,binary 0x.. 0o.. 0b... - proc int_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - proc is_int {str} { - set matches [regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] ;#0b101 etc covered by a-f - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - - # --------------------------------------- - #check for leading zeroes in non 0x 0b 0o - #first strip any +, - or _ (just for this test) - #(but still allowing 0 -0 +0) - set check [tcl::string::map {+ "" - "" _ ""} $str] - if {([tcl::string::length $check] > 1) && ([tcl::string::index $check 0] eq "0") && ([tcl::string::index $check 1] ni {o x b})} { - return 0 - } - # --------------------------------------- - - #check +,- only occur in the first position. (excludes also +++1 etc) - if {[tcl::string::last - $str] > 0} { - return 0 - } - if {[tcl::string::last + $str] > 0} { - return 0 - } - - #------------------------------------------- - #unclear if a 'digit' includes the type specifiers x b o - #we assume the 0x 0b 0o are NOT counted as digits - as underscores here would seem - #to be likely to cause interop issues with other systems - #(e.g tcl allows 0b1_1 but not 0b_11) - #Most of this structure would be unnecessary if we could rely on string::is::integer understanding underscores (9+?) - #we still need to support earlier Tcl for now though. - - #first rule out any case with more than one underscore in a row - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_0xFF - if {[string index $utest 0] eq "_"} { - return 0 - } - if {[string range $utest 0 1] in {0x 0b 0o}} { - set testnum [string range $utest 2 end] - } else { - set testnum $utest - #exclude also things like 0_x 0___b that snuck past our prefix test - if {![string is digit -strict [string map {_ ""} $testnum]]} { - return 0 - } - #assert - only digits and underscores in testnum - #still may have underscores at each end - } - #assert testnum is now the 'digits' portion of a , 0x 0b 0o number - #(+ and - already stripped) - #It may still have chars unsuitable for its type - which will be caught by the string::is::integer test below - if {[string length $testnum] != [string length [string trim $testnum _]]} { - #had non-inner underscores in 'digit' part - return 0 - } - #assert str only has solo inner underscores (if any) between 'digits' - #------------------------------------------- - - set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores - #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { - return 0 - } - - - - #!todo - check bounds only based on some config value - #even though Tcl can handle bignums, we won't accept anything outside of toml 1.0 minimum requirements by default (for now) - #presumably very large numbers would have to be supplied in a toml file as strings. - #Review - toml 1.0 only says that it must handle up to 2^63 - not that this is a max - #some question around implementations allowed to use lower values such as 2^31 on some systems? - if {$::tomlish::max_int ne "" && $numeric_value > $::tomlish::max_int} { - return 0 - } - if {$::tomlish::min_int ne "" && $numeric_value < $::tomlish::min_int} { - return 0 - } - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'float'. - proc float_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[eE0-9\_\-\+\.]} $str] == $numchars} { - return 1 - } else { - #only allow lower case for these special values - as per Toml 1.0 spec - if {$str ni {inf +inf -inf nan +nan -nan}} { - return 0 - } else { - return 1 - } - } - } - - #note - Tcl's string is double will return true also for the subset of float values which are integers - #This function is to determine whether it matches the Toml float concept - so requires a . or e or E - proc is_float {str} { - #vip greenlight known literals, don't test for case variations - as Toml doesn't allow (whereas Tcl allows Inf NaN etc) - if {$str in {inf +inf -inf nan +nan -nan}} { - return 1 - } - #doorcheck the basics for floatiness vs members of that rival gang - ints - if {![regexp {[.eE]} $str]} { - #could be an integer - which isn't specifically a float for Toml purposes. - return 0 - } - - - #patdown for any contraband chars - set matches [regexp -all {[eE0-9\_\-\+\.]} $str] - if {[tcl::string::length $str] != $matches} { - return 0 - } - - #all characters in legal range - - #A leading zero is ok, but we should disallow multiple leading zeroes (same rules as toml ints) - - #Early Toml spec also disallowed leading zeros in the exponent part(?) - #... this seems less interoperable anyway (some libraries generate leading zeroes in exponents) - #we allow leading zeros in exponents here. - - #Check for leading zeros in main part - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - set r {([0-9])*} - regexp $r $check intpart ;#intpart holds all numerals before the first .,e or E - set z {([0])*} - regexp $z $intpart leadingzeros - if {[tcl::string::length $leadingzeros] > 1} { - return 0 - } - - #for floats, +,- may occur in multiple places - #e.g -2E-22 +3e34 - #!todo - check bounds ? - - #----------------------------------------- - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_" || [string index $str end] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_ - if {[string index $utest 0] eq "_"} { - return 0 - } - #----------------------------------------- - - #decimal point, if used must be surrounded by at least one digit on each side - #e.g 3.e+20 also illegal - set dposn [string first . $str] - if {$dposn > -1 } { - set d3 [string range $str $dposn-1 $dposn+1] - if {![string is integer -strict [string index $d3 0]] || ![string is integer -strict [string index $d3 2]]} { - return 0 - } - } - #we've already eliminated leading/trailing underscores - #now ensure each inner underscore is surrounded by digits - if {[regexp {_[^0-9]|[^0-9]_} $str]} { - return 0 - } - - #strip underscores for tcl double check so we can support < tcl 9 versions which didn't allow underscores - set check [tcl::string::map {_ ""} $str] - #string is double accepts inf nan +NaN etc. - if {![tcl::string::is double $check]} { - return 0 - } - - #All good - seems to be a toml-approved float and not an int. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'datetime'. - proc datetime_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[zZtT0-9\-\+\.:]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - proc is_datepart {str} { - set matches [regexp -all {[0-9\-]} $str] - if {[tcl::string::length $str] != $matches} { - return 0 - } - #seems to require yyyy-mm-dd (e.g not allowing just yyyy-mm) - if {![regexp {^([0-9]{4})-([0-9]{2})-([0-9]{2})$} $str _match y m d]} { - return 0 - } - if {$m > 12 || $m == 0} { - return 0 - } - switch -- [expr {$m}] { - 1 - 3 - 5 - 7 - 8 - 10 - 12 { - if {$d > 31 || $d == 0} { - return 0 - } - } - 2 { - if {$d > 29 || $d == 0} { - return 0 - } - if {$d == 29} { - #leapyear check - if {[catch {clock scan $str -format %Y-%m-%d} errM]} { - return 0 - } - } - } - 4 - 6 - 9 - 11 { - if {$d > 30 || $d == 0} { - return 0 - } - } - } - return 1 - } - proc is_localdate {str} { - is_datepart $str - } - proc is_timepart {str} { - set numchars [tcl::string::length $str] - #timepart can have negative or positive offsets so - and + must be accepted - if {[regexp -all {[zZt0-9\-\+\.:]} $str] == $numchars} { - #todo - #basic check that we have leading 2dig hr and 2dig min separated by colon - if {![regexp {^[0-9]{2}:[0-9]{2}$|^[0-9]{2}:[0-9]{2}[^0-9]{1}.*$} $str]} { - #nn:nn or nn:nnX.* where X is non digit - return 0 - } - return 1 - } else { - return 0 - } - } - proc is_localtime {str} { - #time of day without any relation to a specific day or any offset or timezone - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\.:]} $str] == $numchars} { - #todo - if {![regexp {^[0-9]{2}:[0-9]{2}$|^[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]*){0,1}$} $str]} { - #hh:mm or hh:mm:ss or hh:mm::ss.nnn - return 0 - } - return 1 - } else { - return 0 - } - } - - #review - proc is_datetime {str} { - #Essentially RFC3339 formatted date-time - but: - #1) allowing seconds to be omitted (:00 assumed) - #2) T may be replaced with a single space character TODO - parser support for space in datetime! - # (RFC 3339 allows space instead of T also - but doesn't specify it *must* be a single space) - - #toml-lint @2025-04 doesn't accept t for T or z for Z - but RFC3339 does - #toml spec doesn't clarify - we will accept - - #e.g 1979-05-27 - #e.g 1979-05-27T00:32:00Z - #e.g 1979-05-27 00:32:00-07:00 - #e.g 1979-05-27 00:32:00+10:00 - #e.g 1979-05-27 00:32:00.999999-07:00 - - #review - #minimal datetimes? - # 2024 not ok - 2024T not accepted by tomlint why? - # 02:00 ok - # 02:00:00.5 ok - # 1:00 - not ok - RFC3339 requires 2-digit hr,min,sec - - #toml-lint.com accepts 2025-01 - - if {[string length $str] < 5} { - return 0 - } - - set matches [regexp -all {[zZtT0-9\ \-\+\.:]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - if {[regexp -all {\ } $str] > 1} { - #only a single space is allowed. - return 0 - } - #If we get a space - it is only valid as a convience to represent the T separator - #we can normalize by converting to T here before more tests - set str [string map {" " T t T} $str] - #a further sanity check on T - if {[regexp -all {T} $str] > 1} { - return 0 - } - - #!todo - use full RFC 3339 parser? - #!todo - what if the value is 'time only'? - - if {[string first T $str] > -1} { - lassign [split $str T] datepart timepart - if {![is_datepart $datepart]} { - return 0 - } - if {![is_timepart $timepart]} { - return 0 - } - } else { - #either a datepart or a localtime - #spec: "If you include only the time portion of an RFC 3339 formatted date-time, it will represent that time of day - # without any relation to a specific day or any offset or timezone." - if {!([is_datepart $str] || [is_localtime $str])} { - return 0 - } - } - - - #Tcl's free-form clock scan (no -format option) is deprecated - # - #if {[catch {clock scan $datepart} err]} { - # puts stderr "tcl clock scan failed err:'$err'" - # return 0 - #} - - } else { - return 0 - } - return 1 - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::utils ---}] -} - -namespace eval tomlish::parse { - #*** !doctools - #[subsection {Namespace tomlish::parse}] - #[para] - #[list_begin definitions] - - #This is a somewhat curly mix of a statemachine and toml-nesting-stack littered with special cases. - #The code is a pig's-nest - but it should be noted that for example trailing single double quotes in multiline strings are perhaps not so trivial to parse using more standard methods either: - # - e.g some kind of backtracking required if using an ABNF parser? - #I don't know the precise technical name for this sort of parser; probably something like "Dog's Breakfast" - #More seriously, we don't have distinct lex/parse steps - so it is basically a 'fused lexer' or 'scannerless parser' - - #It is also desirable for this system to be useful in 'interactive' use. review - would a separate lexer make this easier or harder? - - #A possible alternative more structured approach might be to use a PEG (Parsing Expression Grammar) - - - variable is_parsing 0 ;#whether we are in the middle of parsing tomlish text - - variable state - # states: - # table-space, itable-space, array-space - # array-value-expected,keyval-value-expected,itable-keyval-value-expected, keyval-syntax, - # dquoted-key, squoted-key - # string-state, literal-state, multistring... - # - # notes: - # only the -space states are also 'spaces' ie a container which is pushed/popped on the spacestack - - # - # xxx_value-expected - we also allow for leading whitespace in this state, but once a value is returned we jump to a state based on the containing space. e.g keyval-tail or array-syntax - # - #stateMatrix defines for each state, actions to take for each possible token. - #single-element actions are the name of the next state into which to transition, or a 'POPSPACE' instruction to pop a level off the spacestack and add the data to the parent container. - #dual-element actions are a push instruction and the name of the space to push on the stack. - # - PUSHSPACE is a simple push onto the spacestack, zeropoppushspace also pushes, but will first do a pop *if* the current space level is greater than zero (ie if only if not already in root table-space) - - # -- --- --- --- --- --- - #token/state naming guide - # -- --- --- --- --- --- - #tokens : underscore separated or bare name e.g newline, start_quote, start_squote - #private tokens: always have a leading underscore (These are private 'temporary state' tokens that are never returned as actual tokens e.g _start_squote_sequence - #states : always contain at least one dash e.g err-state, table-space - #instructions - # -- --- --- --- --- --- - - - #stateMatrix dict of elements mapping current state to next state based on returned tokens - # current-state {token-encountered next-state ... } - # where next-state can be a 1 or 2 element list. - #If 2 element - the first item is an instruction (ucase) - #If 1 element - it is either a lowercase dashed state name or an ucase instruction - #e.g {PUSHSPACE } or POPSPACE or SAMESPACE - - - #SAMESPACE - got to same space as parent without popping a level, but has it's own autotransition lookup - strange concept - review usecases - - variable stateMatrix - set stateMatrix [dict create] - #--------------------------------------------------------- - #WARNING - #The stateMatrix implementation here is currently messy. - #The code is a mixture of declarative via the stateMatrix and imperative via switch statements during PUSH/POP/SAMESPACE transitions. - #This means the state behaviour has to be reasoned about by looking at both in conjuction. - #--------------------------------------------------------- - - #xxx-space vs xxx-syntax inadequately documented - TODO - - #review - out of date? - # --------------------------------------------------------------------------------------------------------------# - # incomplete example of some state starting at table-space - # --------------------------------------------------------------------------------------------------------------# - # ( = -> keyval-value-expected) - # keyval-syntax (popped -> keyval-space -> keyval-tail) (autotransition on pop) - # keyval-space (autotransition on push ^) - # table-space (barekey^) (startdquote -> dquoted-key ^) - # --------------------------------------------------------------------------------------------------------------# - - dict set stateMatrix\ - table-space { - bom "table-space"\ - whitespace "table-space"\ - newline "table-space"\ - barekey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - squotedkey {PUSHSPACE "keyval-space" state "keyval-syntax" note ""}\ - dquotedkey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - XXXsingle_dquote "quoted-key"\ - XXXsingle_squote "squoted-key"\ - comment "table-space"\ - starttablename "tablename-state"\ - starttablearrayname "tablearrayname-state"\ - enddquote "err-state"\ - endsquote "err-state"\ - comma "err-state"\ - eof "end-state"\ - equal "err-state"\ - cr "err-lonecr"\ - } - - - - dict set stateMatrix\ - keyval-space {\ - whitespace "keyval-syntax"\ - equal "keyval-value-expected"\ - } - - # ' = ' portion of keyval - dict set stateMatrix\ - keyval-syntax {\ - whitespace "keyval-syntax"\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal "keyval-value-expected"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - #### - dict set stateMatrix\ - keyval-value-expected {\ - whitespace "keyval-value-expected"\ - untyped_value {TOSTATE "keyval-untyped-sequence" note "possible datetime datepart"}\ - literal {TOSTATE "keyval-tail" note "required for empty literal at EOF"}\ - string {TOSTATE "keyval-tail" note "required for empty string at EOF"}\ - single_dquote {TOSTATE "string-state" returnstate keyval-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - single_squote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - } - #double_squote {TOSTATE "keyval-tail" note "empty literal received when double squote occurs"} - - #untyped_value sequences without intervening comma are allowed for datepart timepart - #we will produce tomlish with missing SEPS and to_dict must validate whether 2 adjacent barekeys are valid - dict set stateMatrix\ - keyval-untyped-sequence {\ - whitespace "keyval-untyped-sequence"\ - untyped_value {TOSTATE "keyval-tail"}\ - literal {TOSTATE "keyval-tail" note "required for empty literal at EOF"}\ - string {TOSTATE "keyval-tail" note "required for empty string at EOF"}\ - single_dquote {TOSTATE "string-state" returnstate keyval-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - single_squote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - #2025 - no leading-squote-space - only trailing-squote-space. - - dict set stateMatrix\ - keyval-tail {\ - whitespace "keyval-tail"\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - - #itable-space/ curly-syntax : itables - # x={y=1,} - dict set stateMatrix\ - itable-space {\ - whitespace "itable-space"\ - newline "itable-space"\ - barekey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - squotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - dquotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - endinlinetable "POPSPACE"\ - comma "err-state"\ - comment "itable-space"\ - eof "err-state"\ - } - #we don't get single_squote etc here - instead we get the resulting squotedkey token - - - # ??? review - something like this - # - # x={y =1,} - dict set stateMatrix\ - itable-keyval-syntax {\ - whitespace {TOSTATE "itable-keyval-syntax"}\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal {TOSTATE "itable-keyval-value-expected"}\ - newline "err-state"\ - eof "err-state"\ - } - - # x={y=1} - dict set stateMatrix\ - itable-keyval-space {\ - whitespace "itable-keyval-syntax"\ - equal {TOSTATE "itable-keyval-value-expected" note "required"}\ - } - - dict set stateMatrix\ - itable-keyval-value-expected {\ - whitespace "itable-keyval-value-expected"\ - untyped_value {TOSTATE "itable-val-tail" note ""}\ - single_dquote {TOSTATE "string-state" returnstate itable-val-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate itable-val-tail}\ - single_squote {TOSTATE "literal-state" returnstate itable-val-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate itable-val-tail}\ - startinlinetable {PUSHSPACE "itable-space" returnstate itable-val-tail}\ - startarray {PUSHSPACE "array-space" returnstate itable-val-tail}\ - } - #double_squote not currently generated by _start_squote_sequence - '' processed as single_squote to literal-state just like 'xxx' - # review - # double_squote {TOSTATE "itable-val-tail" note "empty literal received when double squote occurs"} - - - - # x={y=1,z="x"} - #POPSPACE is transition from itable-keyval-space to parent itable-space - dict set stateMatrix\ - itable-val-tail {\ - whitespace "itable-val-tail"\ - endinlinetable "POPSPACE"\ - comma "POPSPACE"\ - newline {TOSTATE "itable-val-tail" note "itable-space ??"}\ - comment "itable-val-tail"\ - eof "err-state"\ - } - # XXXnewline "POPSPACE" - # We shouldn't popspace on newline - as if there was no comma we need to stay in itable-val-tail - # This means the newline and subsequent whitespace, comments etc become part of the preceeding dottedkey record - #e.g - # x = { - # j=1 - # #comment within dottedkey j record - # , # comment unattached - # #comment unattached - # k=2 , #comment unattached - # l=3 #comment within l record - # , m=4 - # #comment associated with m record - # - # #still associated with m record - # } - ## - This doesn't quite correspond to what a user might expect - but seems like a consistent mechanism. - #The awkwardness is because there is no way to put in a comment that doesn't consume a trailing comma - #so we cant do: j= 1 #comment for j1 , - # and have the trailing comma recognised. - # - # To associate: j= 1, #comment for j1 - # we would need some extra processing . (not popping until next key ? extra state itable-sep-tail?) REVIEW - worth doing? - # - # The same issue occurs with multiline arrays. The most natural assumption is that a comment on same line after a comma - # is 'associated' with the previous entry. - # - # These comment issues are independent of the data dictionary being generated for conversion to json etc - as the comments don't carry through anyway, - # but are a potential oddity for manipulating the intermediate tomlish structure whilst attempting to preserve 'associated' comments - # (e.g reordering records within an itable) - #The user's intention for 'associated' isn't always clear and the specs don't really guide on this. - - - #dottedkey-space is not (currently) used within [tablename] or [[tablearrayname]] - #it is for keyval ie x.y.z = value - - #this is the state after dot - #we are expecting a complete key token or whitespace - #(initial entry to the space is by one of the keys - which will immediately go to dottedkey-space-tail) - dict set stateMatrix\ - dottedkey-space {\ - whitespace "dottedkey-space"\ - dotsep "err-state"\ - barekey "dottedkey-space-tail"\ - squotedkey "dottedkey-space-tail"\ - dquotedkey "dottedkey-space-tail"\ - newline "err-state"\ - comma "err-state"\ - comment "err-state"\ - equal "err-state"\ - } - - #dottedkeyend "POPSPACE" - #equal "POPSPACE"\ - - - #jmn 2025 - #we have 1 or more dottedkeys so far - need dotsep to add more, whitespace to maintain, equal to pop - dict set stateMatrix\ - dottedkey-space-tail {\ - whitespace "dottedkey-space-tail" - dotsep "dottedkey-space" - equal "POPSPACE"\ - eof "err-state"\ - newline "err-state"\ - } - - #-------------------------------------------------------------------------- - #scratch area - #from_toml {x=1} - # barekey tok - # table-space PUSHSPACE keyval-space state keyval-syntax - # - - - #-------------------------------------------------------------------------- - - - #REVIEW - #toml spec looks like heading towards allowing newlines within inline tables - #https://github.com/toml-lang/toml/issues/781 - - #2025 - multiline itables appear to be valid for 1.1 - which we are targeting. - #https://github.com/toml-lang/toml/blob/main/toml.md#inline-table - - #JMN2025 - #review comment "err-state" vs comment "itable-space" - see if TOML 1.1 comes out and allows comments in multiline ITABLES - #We currently allow multiline ITABLES (also with comments) in the tokenizer. - #if we want to disallow as per TOML 1.0 - we should do so when attempting to get structure? - - - #JMN REVIEW - #dict set stateMatrix\ - # array-space {\ - # whitespace "array-space"\ - # newline "array-space"\ - # untyped_value "SAMESPACE"\ - # startarray {PUSHSPACE "array-space"}\ - # endarray "POPSPACE"\ - # startinlinetable {PUSHSPACE itable-space}\ - # single_dquote "string-state"\ - # single_squote "literal-state"\ - # triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax note "seems ok 2024"}\ - # comma "array-space"\ - # comment "array-space"\ - # eof "err-state-array-space-got-eof"\ - # } - - ## array-space ## - set aspace [dict create] - dict set aspace whitespace "array-space" - dict set aspace newline "array-space" - #dict set aspace untyped_value "SAMESPACE" - dict set aspace untyped_value "array-syntax" - dict set aspace startarray {PUSHSPACE "array-space"} - dict set aspace endarray "POPSPACE" - dict set aspace single_dquote {TOSTATE "string-state" returnstate array-syntax} - dict set aspace triple_dquote {PUSHSPACE "multistring-space" returnstate array-syntax} - dict set aspace single_squote {TOSTATE "literal-state" returnstate array-syntax} - dict set aspace triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax} - dict set aspace startinlinetable {PUSHSPACE itable-space} - #dict set aspace comma "array-space" - dict set aspace comment "array-space" - dict set aspace eof "err-state-array-space-got-eof" - dict set stateMatrix array-space $aspace - - #when we pop from an inner array we get to array-syntax - #e.g {x=[[]] ??? - set tarntail [dict create] - dict set tarntail whitespace "tablearrayname-tail" - dict set tarntail newline "err-state" - dict set tarntail comment "err-state" - dict set tarntail eof "err-state" - dict set tarntail endtablename "tablearray-tail" - dict set stateMatrix tablearrayname-tail $tarntail - - #review - somewhat counterintuitive...? - # [(starttablearrayname) (endtablearrayname] - # [(starttablename) (endtablename)] - - # [[xxx]] ??? - set tartail [dict create] - dict set tartail whitespace "tablearray-tail" - dict set tartail newline "table-space" - dict set tartail comment "tablearray-tail" - dict set tartail eof "end-state" - dict set stateMatrix tablearray-tail $tartail - - - - - - - dict set stateMatrix\ - end-state {} - - set knowntokens [list] - set knownstates [list] - dict for {state transitions} $stateMatrix { - if {$state ni $knownstates} {lappend knownstates $state} - dict for {tok instructions} $transitions { - if {$tok ni $knowntokens} {lappend knowntokens $tok} - } - } - dict set stateMatrix nostate {} - foreach tok $knowntokens { - dict set stateMatrix nostate $tok "err-nostate-received-token-$tok" - } - - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #purpose - debugging? remove? - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #build a list of 'push triggers' from the stateMatrix - # ie tokens which can push a new space onto spacestack - set push_trigger_tokens [list] - tcl::dict::for {s transitions} $stateMatrix { - tcl::dict::for {token transition_to} $transitions { - set instruction [lindex $transition_to 0] - switch -exact -- $instruction { - PUSHSPACE - zeropoppushspace { - if {$token ni $push_trigger_tokens} { - lappend push_trigger_tokens $token - } - } - } - } - } - ::tomlish::log::debug "push_trigger_tokens: $push_trigger_tokens" - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - - - - #This seems hacky... (deprecate in favour of explicit arguments to the instructions in stateMatrix?) - #spacePopTransitions, spacePushTransitions, spaceSameTransitions below for auto state redirections on POPSPACE,PUSHSPACE,SAMESPACE - - #mainly for the -space states: - #redirect to another state $c based on a state transition from $whatever to $b - # e.g "string {array-space array-syntax}" means when transitioning from string to array-space, jump to array-syntax instead. - #this is useful as we often don't know state $b. e.g when it is decided by 'POPSPACE' - - #use dict set to add values so we can easily add/remove/comment lines - - #Push to, next - #default first states when we push to these spaces - variable spacePushTransitions [dict create] - dict set spacePushTransitions keyval-space keyval-syntax - dict set spacePushTransitions itable-keyval-space itable-keyval-syntax - dict set spacePushTransitions array-space array-space - dict set spacePushTransitions table-space tablename-state - #dict set spacePushTransitions #itable-space itable-space - - #Pop to, next - variable spacePopTransitions [dict create] - dict set spacePopTransitions array-space array-syntax - - - #itable-keyval-space itable-val-tail - #review - #we pop to keyval-space from dottedkey-space or from keyval-value-expected? we don't always want to go to keyval-tail - #leave it out and make the POPSPACE caller explicitly specify it - #keyval-space keyval-tail - - variable spaceSameTransitions [dict create] - #JMN test - #dict set spaceSameTransitions array-space array-syntax - - #itable-keyval-space itable-val-tail - - - variable state_list ;#reset every tomlish::decode::toml - - namespace export tomlish toml - namespace ensemble create - - #goNextState has various side-effects e.g pushes and pops spacestack - #REVIEW - setting nest and v elements here is ugly - #todo - make neater, more single-purpose? - proc goNextState {tokentype tok currentstate} { - variable state - variable nest - variable v - - set prevstate $currentstate - - - variable spacePopTransitions - variable spacePushTransitions - variable spaceSameTransitions - - variable last_space_action "none" - variable last_space_type "none" - variable state_list - - set result "" - set starttok "" - - if {[dict exists $::tomlish::parse::stateMatrix $currentstate $tokentype]} { - set transition_to [dict get $::tomlish::parse::stateMatrix $currentstate $tokentype] - ::tomlish::log::debug "--->> goNextState tokentype:$tokentype tok:$tok currentstate:$currentstate : transition_to = $transition_to" - switch -exact -- [lindex $transition_to 0] { - POPSPACE { - set popfromspace_info [spacestack peek] - set popfromspace_state [dict get $popfromspace_info state] - spacestack pop - set parent_info [spacestack peek] - set type [dict get $parent_info type] - set parentspace [dict get $parent_info state] - - set last_space_action "pop" - set last_space_type $type - - if {[dict exists $parent_info returnstate]} { - set next [dict get $parent_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected to stored returnstate $next <<---" - } else { - ### - #review - do away with spacePopTransitions - which although useful to provide a default.. - # - involve error-prone configurations distant to the main state transition configuration in stateMatrix - if {[dict exists $::tomlish::parse::spacePopTransitions $parentspace]} { - set next [dict get $::tomlish::parse::spacePopTransitions $parentspace] - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected state to $next (spacePopTransitions)<<---" - } else { - set next $parentspace - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace<<---" - } - } - set result $next - } - SAMESPACE { - set currentspace_info [spacestack peek] - ::tomlish::log::debug "--->> SAMESPACE got current space entry: $currentspace_info <<<<<" - set type [dict get $currentspace_info type] - set currentspace [dict get $currentspace_info state] - - if {[dict exists $currentspace_info returnstate]} { - set next [dict get $currentspace_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected to stored returnstate $next" - } else { - if {[dict exists $::tomlish::parse::spaceSameTransitions $currentspace]} { - set next [dict get $::tomlish::parse::spaceSameTransitions $currentspace] - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected state to $next (spaceSameTransitions)" - } else { - set next $currentspace - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace" - } - } - set result $next - } - zeropoppushspace { - if {$nest > 0} { - #pop back down to the root level (table-space) - spacestack pop - set parentinfo [spacestack peek] - set type [dict get $parentinfo type] - set target [dict get $parentinfo state] - - set last_space_action "pop" - set last_space_type $type - - #----- - #standard pop - set parentlevel [expr {$nest -1}] - lappend v($parentlevel) [set v($nest)] - incr nest -1 - #----- - } - #re-entrancy - - #set next [list PUSHSPACE [lindex $transition_to 1]] - set nexttokentype ${tokentype}2 ;#fake token type e.g tablename2 or tablearrayname2 - ::tomlish::log::debug "--->> zeropoppushspace goNextState RECURSE. calling goNextState $nexttokentype $currentstate" - set transition_info [::tomlish::parse::goNextState $nexttokentype $tok $currentstate] - set result [dict get $transition_info newstate] - } - PUSHSPACE { - set original_target [dict get $transition_to PUSHSPACE] - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - if {[dict exists $transition_to starttok]} { - set starttok [dict get $transition_to starttok] - } - spacestack push [dict create type space state $original_target] - - set last_space_action "push" - set last_space_type "space" - - if {[dict exists $transition_to state]} { - #an explicit state in the pushed space was requested in the stateMatrix - override the spacePushTransition (spacePushTransitions can be deprecated if we require explicitness?) - set next [dict get $transition_to state] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next by explicit 'state' entry" - } else { - #puts $::tomlish::parse::spacePushTransitions - if {[dict exists $::tomlish::parse::spacePushTransitions $original_target]} { - set next [dict get $::tomlish::parse::spacePushTransitions $original_target] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next (spacePushTransitions) " - } else { - set next $original_target - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target" - } - } - set result $next - } - TOSTATE { - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - set result [dict get $transition_to TOSTATE] - } - default { - #simplified version of TOSTATE - set result [lindex $transition_to 0] ;#ignore everything but first word - } - } - } else { - ::tomlish::log::error "--->> No state transition defined from state $currentstate when tokentype $tokentype received" - set result "nostate" - } - lappend state_list [list tokentype $tokentype from $currentstate to $result] - set state $result - ::tomlish::log::notice "--->> STATE TRANSITION tokenType: '$tokentype' tok:$tok triggering '$currentstate' -> '$result' last_space_action:$last_space_action " - return [dict create prevstate $prevstate newstate $result space_action $last_space_action starttok $starttok] - } - - proc report_line {{line ""}} { - variable linenum - variable is_parsing - if {$is_parsing} { - if {$line eq ""} { - set line $linenum - } - return "Line Number: $line" - } else { - #not in the middle of parsing tomlish text - return nothing. - return "" - } - } - - #produce a *slightly* more readable string rep of the nest for puts etc. - proc nest_pretty1 {list} { - set prettier "{" - - foreach el $list { - if { [lindex $el 0] eq "NEWLINE"} { - append prettier "[list $el]\n" - } elseif {([llength $el] > 1) && ([lindex $el 0] in {KEY DQKEY SQKEY TABLE ARRAY})} { - append prettier [nest_pretty1 $el] - } else { - append prettier "[list $el] " - } - } - append prettier "}" - return $prettier - } - - proc set_tokenType {t} { - variable tokenType - variable tokenType_list - if {![info exists tokenType]} { - set tokenType "" - } - lappend tokenType_list $t - set tokenType $t - } - - proc switch_tokenType {t} { - variable tokenType - variable tokenType_list - lset tokenType_list end $t - set tokenType $t - } - - proc get_tokenType {} { - variable tokenType - return $tokenType - } - - - proc get_token_waiting {} { - variable token_waiting - return [lindex $token_waiting 0] - } - proc clear_token_waiting {} { - variable token_waiting - set token_waiting [list] - } - - #token_waiting is a list - but our standard case is to have only one - #in certain circumstances such as near eof we may have 2 - #the set_token_waiting function only allows setting when there is not already one waiting. - #we want to catch cases of inadvertently trying to set multiple - # - the reason being that the state transition triggered by the previous token may have invalidated the assumptions made when a token was added as waiting. - proc set_token_waiting {args} { - if {[llength $args] %2 != 0} { - error "tomlish set_token_waiting must have args of form: type value complete 0|1" - } - variable token_waiting - - if {[llength $token_waiting] && [dict get [lindex $token_waiting end] type] ne "eof"} { - #tokloop already set a token_waiting - but something (post tokloop processing?) is trying to set another - #we may need to remove the existing token_waiting and reset the tokloop index to the previous char so it's reprocessed in the possibly new context - #rather than attempt to make the right decision here - we raise an error and require the caller to check/handle it - set err "tomlish set_token_waiting already has token_waiting: [lindex $token_waiting 0]" - append err \n " - cannot add token_waiting: $args" - error $err - #set tomlish::parse::i [expr {[dict get $token_waiting startindex] -1}] - #set token_waiting [list] - } - - set waiting [dict create] - dict for {k v} $args { - switch -exact $k { - type - complete { - dict set waiting $k $v - } - value { - dict set waiting tok $v - } - startindex { - dict set waiting startindex $v - } - default { - error "tomlish set_token_waiting error - unrecognised key $k. known keys: [dict keys $args]" - } - } - } - if {![tcl::string::is boolean -strict [dict get $waiting complete]]} { - error "tomlish set_token_waiting error - 'complete' must be a boolean. got [dict get $waiting complete]" - } - if {![llength $token_waiting]} { - set token_waiting [list $waiting] - } else { - #an extra sanity-check that we don't have more than just the eof.. - if {[llength $token_waiting] > 1} { - set err "tomlish Unexpected. Existing token_waiting count > 1.\n" - foreach tw $token_waiting { - append err " $tw" \n - } - append err " - cannot add token_waiting: $waiting" - error $err - } - #last entry must be a waiting eof - set token_waiting [list $waiting [lindex $token_waiting end]] - } - return - } - - #returns 0 or 1 - #tomlish::parse::tok - #we attempt to do this without lookahead (potential use in streaming toml? for what benefit?) todo -final flag - # - the possible benefit is being able to more easily process in arbitrarily split chunks (although we would still have to watch crlf splitting ?) - # - interactive use? - - proc tok {s} { - variable nest - variable v - variable i - variable tok - variable type ;#character type - variable state ;#FSM - - - variable tokenType - variable tokenType_list - - - variable endToken - - variable lastChar - - variable braceCount - variable bracketCount - - - #------------------------------ - #Previous run found another (presumably single-char) token - #The normal case is for there to be only one dict in the list - #multiple is an exception - primarily for eof - variable token_waiting - if {[llength $token_waiting]} { - set waiting [lindex $token_waiting 0] - - set tokenType [dict get $waiting type] - set tok [dict get $waiting tok] - #todo: dict get $token_waiting complete - set token_waiting [lrange $token_waiting 1 end] - return 1 - } - #------------------------------ - - set resultlist [list] - set sLen [tcl::string::length $s] - - set slash_active 0 - set quote 0 - set c "" - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $s [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $s $i] - set cindex $i - set ctest [tcl::string::map {\{ lc \} rc \[ lb \] rb \" dq ' sq \\ bsl \r cr \n lf \t tab \uFEFF bom} $c] - tomlish::log::debug "- tokloop char <$ctest> index $i tokenType:$tokenType tok:<$tok>" - #puts "got char $c during tokenType '$tokenType'" - incr i ;#must incr here because we do returns inside the loop - - switch -exact -- $ctest { - # { - set had_slash $slash_active - set slash_active 0 - - if {$had_slash} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - #for multiliteral, multistring - data and/or end - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo token beginning with underscore - never returned to state machine - review - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i [tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed (see tomlish::utils::is_barekey). [tomlish::parse::report_line]" - } - whitespace { - # hash marks end of whitespace token - #do a return for the whitespace, set token_waiting - #set_token_waiting type comment value "" complete 1 - incr i -1 ;#leave comment for next run - return 1 - } - untyped_value { - #REVIEW! the spec isn't clear.. is whitespace after an int,bool etc required before comment? - #we will accept a comment marker as an immediate terminator of the untyped_value. - incr i -1 - return 1 - } - starttablename - starttablearrayname { - #fix! - error "tomlish Character '#' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare parts - but allowed in quoted parts - let tablename parser sort it out - append tok $c - } - default { - #dquotedkey, string,literal, multistring - append tok $c - } - } - } else { - switch -- $state { - multistring-space { - set_tokenType stringpart - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "#" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "#" - } - default { - #start of token if we're not in a token - set_tokenType comment - set tok "" ;#The hash is not part of the comment data - } - } - } - } - lc { - #left curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i [tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - #*bare* tablename can only contain letters,digits underscores - error "tomlish Invalid tablename first character \{ [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #valid in quoted parts - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\{" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - switch -exact -- $state { - itable-keyval-value-expected - keyval-value-expected { - #switch last key to tablename?? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - array-space - array-syntax { - #nested anonymous inline table - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\{" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\{" - } - default { - error "tomlish state: '$state'. left brace case not implemented [tomlish::parse::report_line]" - } - } - } - - } - rc { - #right curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - tablename { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endinlinetable value "" complete 1 startindex $cindex - return 1 - } - starttablearrayname - tablearrayname { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - default { - #end any other token - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-space { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname-state problem" - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - #invalid - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-val-tail { - set_tokenType "endinlinetable" - set tok "" - #we need to pop the keyval - and then reprocess to pop the inlinetable - so we incr -1 - incr i -1 - return 1 - } - itable-keyval-syntax { - error "tomlish endinlinetable unexpected at this point. Expecting key=val syntax [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\}" - } - multiliteral-space { - set_tokenType "literalpart" ; #review - set tok "\}" - } - default { - #JMN2024b keyval-tail? - error "tomlish state '$state'. endinlinetable case not implemented [tomlish::parse::report_line]" - } - } - } - - } - lb { - #left square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename { - #change the tokenType - switch_tokenType "starttablearrayname" - set tok "" ;#no output into the tomlish list for this token - #any following whitespace is part of the tablearrayname, so return now - return 1 - } - tablename - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - #append tok "\\[" - append tok {\[} - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - #invalid at this point - state machine should disallow: - # table -> starttablearrayname - # tablearray -> starttablearrayname - set_token_waiting type starttablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "\[" - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - keyval-value-expected - itable-keyval-value-expected { - set_tokenType "startarray" - set tok "\[" - return 1 - } - array-space - array-syntax { - #nested array? - set_tokenType "startarray" - set tok "\[" - return 1 - #error "state: array-space. startarray case not implemented [tomlish::parse::report_line]" - } - table-space { - #table name - #assume it's a single bracket - but we need to wait for non-bracket to confirm it's not a tablearray - #note that a starttablearrayname token may contain whitespace between the brackets - # e.g \[ \[ - set_tokenType "starttablename" - set tok "" ;#there is no output into the tomlish list for this token - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\[" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\[" - } - itable-space { - #handle state just to give specific error msg - error "tomlish state: '$state'. Left square bracket invalid. Cannot start array in inline table without key. Use key=\[\] syntax. [tomlish::parse::report_line]" - } - default { - error "tomlish state: '$state'. startarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - rb { - #right square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - #???? - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } else { - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablename value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - default { - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - array-syntax - array-space { - #invalid - but allow parser statemachine to report it. - set_tokenType "endarray" - set tok "\]" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endarray" - set tok "\]" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endtablename" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname problem" - set_tokenType "endtablearray" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-tail { - #[[xxx] - set_tokenType "endtablename" - #sequence: starttablename -> starttablearrayname -> endtablearrayname -> endtablename - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\]" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\]" - } - default { - error "tomlish state '$state'. endarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - bsl { - #backslash - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - #end whitespace token - incr i -1 ;#reprocess bsl in next run - return 1 - } else { - error "tomlish Unexpected backslash during whitespace. [tomlish::parse::report_line]" - } - } - literal - literalpart - squotedkey { - #never need to set slash_active true when in single quoted tokens - append tok "\\" - set slash_active 0 - } - string - dquotedkey - comment { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - stringpart { - if {$slash_active} { - #assert - quotes empty - or we wouldn't have slash_active - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - starttablename - starttablearrayname { - error "tomlish backslash is invalid as first character of $tokenType [tomlish::parse::report_line]" - } - tablename - tablearrayname { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - barekey { - error "tomlish Unexpected backslash during barekey. [tomlish::parse::report_line]" - } - default { - error "tomlish Backslash unexpected during tokentype: '$tokenType'. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$slash_active} { - set_tokenType "stringpart" - set tok "\\\\" - set slash_active 0 - } else { - set slash_active 1 - } - } - multiliteral-space { - #nothing can be escaped in multiliteral-space - not even squotes (?) review - set_tokenType "literalpart" - set tok "\\" - } - default { - error "tomlish tok error: Unexpected backslash when no token is active. [tomlish::parse::report_line]" - } - } - } - } - sq { - #single quote - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote { - #for within multiliteral - #short tentative_accum_squote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_squote is multisquote + 2 = 5 - #return tok with value ''''' - return 1 - } - } - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multiliteral - #switch? - append tok $c - set_tokenType triple_squote - return 1 - } - default { - #if there are more than 3 leading squotes we also enter multiliteral space and the subsequent ones are handled - #by the tentative_accum_squote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 squotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_squote_sequence'" - } - } - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - #end whitespace - incr i -1 ;#reprocess sq - return 1 - } - literal { - #slash_active always false - #terminate the literal - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - literalpart { - #ended by ''' - but final could be '''' or ''''' (up to 2 squotes allowed directly before ending triple squote sequence) - #todo - # idea: end this literalpart (possibly 'temporarily') - # let the sq be reprocessed in the multiliteral-space to push an end-multiliteral-sequence to state stack - # upon popping end-multiliteral-sequence - stitch quotes back into this literalpart's token (if either too short - or a long ending sequence as shown above) - incr i -1 ;#throw the "'" back to loop - will be added to a tentative_accum_squote token for later processing - return 1 - } - XXXitablesquotedkey { - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - squotedkey { - ### - #set_token_waiting type endsquote value "'" complete 1 - return 1 - } - starttablename - starttablearrayname { - #!!! - incr i -1 - return 1 - } - tablename - tablearrayname { - append tok $c - } - barekey { - #barekeys now support all sorts of unicode letter/number chars for other cultures - #but not punctuation - not even for those of Irish heritage who don't object - #to the anglicised form of some names. - # o'shenanigan seems to not be a legal barekey - #The Irish will have to use an earlier form Ó - which apparently many may prefer anyway. - error "tomlish Unexpected single quote during barekey. [tomlish::parse::report_line]" - } - default { - append tok $c - } - } - } else { - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading squote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_squote token or triple_squote token - #It currently doesn't trigger double_squote token - #(handle '' same as 'x' ie produce a single_squote and go into processing literal) - #review - producing double_squote for empty literal may be slightly more efficient. - #This token is not used to handle squote sequences *within* a multiliteral - set_tokenType "_start_squote_sequence" - set tok "'" - } - multiliteral-space { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_squote" ;#trigger tentative_accum_squote - set tok "'" - return 1 - } - table-space - itable-space { - #tests: squotedkey.test squotedkey_itable.test - set_tokenType "squotedkey" - set tok "" - } - XXXtable-space - XXXitable-space { - #future - could there be multiline keys? MLLKEY, MLBKEY ? - #this would (almost) allow arbitrary tcl dicts to be stored in toml (aside from escaping issues) - #probably unlikely - as it's perhaps not very 'minimal' or ergonomic for config files - #@2025 ABNF for toml mentions key, simple-key, unquoted-key, quoted-key and dotted-key - #where key is simple-key or dotted-key - no MLL or MLB components - #the spec states solution for arbitrary binary data is application specific involving encodings - #such as hex, base64 - set_tokenType "_start_squote_sequence" - set tok "'" - return 1 - } - tablename-state { - #first char in tablename-state/tablearrayname-state - set_tokenType "tablename" - append tok "'" - } - tablearrayname-state { - set_tokenType "tablearrayname" - append tok "'" - } - literal-state { - #shouldn't get here? review - tomlish::log::debug "- tokloop sq during literal-state with no tokentype - empty literal?" - set_tokenType "literal" - incr -1 - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - #error "tomlish unimplemented - squote during state '$state'. [tomlish::parse::report_line]" - } - dottedkey-space { - set_tokenType "squotedkey" - } - default { - error "tomlish unhandled squote during state '$state'. [tomlish::parse::report_line]" - } - } - } - - } - dq { - #double quote - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - tentative_accum_dquote { - #within multistring - #short tentative_accum_dquote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_dquote is multidquote + 2 = 5 - #return tok with value """"" - return 1 - } - } - _start_dquote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multistring - #switch? - append tok $c - set_tokenType triple_dquote - return 1 - } - default { - #if there are more than 3 leading dquotes we also enter multistring space and the subsequent ones are handled - #by the tentative_accum_dquote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 dquotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_dquote_sequence'" - } - } - } - literal - literalpart { - append tok $c - } - string { - if {$had_slash} { - append tok "\\" $c - } else { - #unescaped quote always terminates a string - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - stringpart { - #sub element of multistring - if {$had_slash} { - append tok "\\" $c - } else { - incr i -1 ;#throw the {"} back to loop - will be added to a tentative_accum_dquote token for later processing - return 1 - } - } - whitespace { - #assert: had_slash will only ever be true in multistring-space - if {$had_slash} { - incr i -2 - return 1 - } else { - #end whitespace token - throw dq back for reprocessing - incr i -1 - return 1 - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - XXXdquotedkey { - if {$had_slash} { - append tok "\\" - append tok $c - } else { - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - dquotedkey { - ### - if {$had_slash} { - append tok "\\" - append tok $c - } else { - #set_token_waiting type enddquote value {"} complete 1 - return 1 - } - } - squotedkey { - append tok $c - } - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - incr i -1 ;## - return 1 - } - default { - error "tomlish got quote during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - #$slash_active not relevant when no tokenType - #token is string only if we're expecting a value at this point - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading dquote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_dquote token or triple_dquote token - #It currently doesn't trigger double_dquote token - #(handle "" same as "x" ie produce a single_dquote and go into processing string) - #review - producing double_dquote for empty string may be slightly more efficient. - #This token is not used to handle dquote sequences once *within* a multistring - set_tokenType "_start_dquote_sequence" - set tok {"} - } - multistring-space { - if {$had_slash} { - set_tokenType "stringpart" - set tok "\\\"" - } else { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_dquote" ;#trigger tentative_accum_dquote - set tok {"} - return 1 - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\"" - } - table-space - itable-space { - set_tokenType "dquotedkey" - set tok "" - } - dottedkey-space { - set_tokenType dquotedkey - set tok "" - - #only if complex keys become a thing - #set_tokenType dquote_seq_begin - #set tok $c - } - tablename-state { - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - default { - error "tomlish Unexpected dquote during state '$state' [tomlish::parse::report_line]" - } - } - } - } - = { - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - #assertion had_slash 0 - append tok $c - } - string - comment - dquotedkey { - #for these tokenTypes an = is just data. - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type equal value = complete 1 startindex $cindex - return 1 - } - } - barekey { - #set_token_waiting type equal value = complete 1 - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character '=' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare name - but valid in quoted parts - leave for tablename parser to sort out - append tok $c - } - default { - error "tomlish unexpected = character during tokentype $tokenType. case not implemented. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok = - } - multiliteral-space { - set_tokenType "literalpart" - set tok "=" - } - dottedkey-space { - set_tokenType "equal" - set tok "=" - return 1 - } - default { - set_tokenType "equal" - set tok = - return 1 - } - } - } - } - cr { - #REVIEW! - # \r carriage return - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #we have received a double cr - ::tomlish::log::warn "double cr - will generate cr token. needs testing" - set_tokenType "cr" ;#lone cr token will generally raise an error - but let state machine handle it - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - append tok $c - } - literalpart { - #part of MLL string (multi-line literal string) - #we need to split out crlf as a separate NEWLINE to be consistent - ::tomlish::log::warn "literalpart ended by cr - needs testing" - #return literalpart temporarily - allow cr to be reprocessed from multiliteral-space - incr i -1 - return 1 - } - stringpart { - #stringpart is a part of MLB string (multi-line basic string) - #throw back the cr - if followed by lf it will become a {NEWLINE crlf} entry within the MULTISTRING list (e.g between STRINGPART entries) - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #could in theory be valid in quoted part of name - #review - might be better just to disallow here - append tok $c - } - whitespace { - #it should technically be part of whitespace if not followed by lf - #but outside of values we are also free to map it to be another NEWLINE instead? REVIEW - incr i -1 - return 1 - } - untyped_value { - incr i -1 - return 1 - } - default { - #!todo - error out if cr inappropriate for tokenType - append tok $c - } - } - } else { - #lf may be appended if next - #review - lone cr as newline? - this is uncommon - but so is lone cr in a string(?) - set_tokenType "newline" - set tok cr - } - } - lf { - # \n newline - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #this lf is the trailing part of a crlf - append tok lf ;#assert we should now have tok "crlf" - as a previous cr is the only way to have an incomplete newline tok - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - #multiliteral or multistring - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - #nl is not allowed *within* a literal - require multiliteral syntax for any literal containing a newline ''' ''' - #even though we terminate the literal without the closing quote here - the token_waiting newline should trigger a state error - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - literalpart { - #we allow newlines - but store them within the multiliteral as their own element - #This is a legitimate end to the literalpart - but not the whole multiliteral - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - stringpart { - if {$had_slash} { - #emit the stringpart (return 1), queue the continuation, go back 1 to reprocess the lf (incr i -1) - set_token_waiting type continuation value \\ complete 1 startindex [expr {$cindex-1}] - incr i -1 - return 1 - } else { - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - starttablename - tablename - tablearrayname - starttablearrayname { - error "tomlish Character is invalid in $tokenType. [tomlish::parse::report_line]" - } - default { - #newline ends all other tokens. - #note for string: we don't add (raw unescaped) newline to simple string. (must use multi-string for this) - #note for whitespace: - # we will use the convention that \n terminates the current whitespace even if whitespace follows - # ie whitespace is split into separate whitespace tokens at each newline - - #puts "-------------- newline lf during tokenType $tokenType" - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "newline" - set tok lf - return 1 - } - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "newline" - set tok "lf" - return 1 - } - default { - #ignore slash? error? - set_tokenType "newline" - set tok lf - return 1 - } - } - #if {$had_slash} { - # #CONT directly before newline - allows strings_5_byteequivalent test to pass - # set_tokenType "continuation" - # set tok "\\" - # incr i -1 - # return 1 - #} else { - # set_tokenType newline - # set tok lf - # return 1 - #} - } - } - , { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok , - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - #stringpart can have up to 2 quotes too - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type comma value "," complete 1 startindex $cindex - return 1 - } - } - default { - set_token_waiting type comma value "," complete 1 startindex $cindex - if {$had_slash} {append tok "\\"} - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "literalpart" - set tok "," - } - default { - set_tokenType "comma" - set tok "," - return 1 - } - } - } - } - . { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - untyped_value { - if {$had_slash} {append tok "\\"} - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - switch -exact -- $state { - multistring-space { - #review - if {$had_slash} { - incr i -2 - } else { - incr i -1 - } - return 1 - } - xxxdottedkey-space { - incr i -1 - return 1 - } - dottedkey-space-tail { - incr i -1 - return 1 - } - default { - error "tomlish Received period during tokenType 'whitespace' [tomlish::parse::report_line]" - } - } - } - starttablename - starttablearrayname { - #This would correspond to an empty table name - error "tomlish Character '.' is not allowed as first character ($tokenType). [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #subtable - split later - review - append tok $c - } - barekey { - #e.g x.y = 1 - #we need to transition the barekey to become a structured table name ??? review - #x is the tablename y is the key - set_token_waiting type dotsep value "." complete 1 startindex $cindex - return 1 - } - default { - error "tomlish Received period during tokenType '$tokenType' [tomlish::parse::report_line]" - #set_token_waiting type period value . complete 1 - #return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "." - } - multiliteral-space { - set_tokenType "literalpart" - set tok "." - } - XXXdottedkey-space { - ### obs? - set_tokenType "dotsep" - set tok "." - return 1 - } - dottedkey-space-tail { - ### - set_tokenType "dotsep" - set tok "." - return 1 - } - default { - set_tokenType "untyped_value" - set tok "." - } - } - } - - } - " " { - if {[tcl::string::length $tokenType]} { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #todo had_slash - emit token or error - #whitespace is a terminator for bare keys - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - comment { - if {$had_slash} { - append tok "\\" - } - append tok $c - } - string - dquotedkey { - if {$had_slash} { append tok "\\" } - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART xxx WS " " - incr i -1 - return 1 - } - } - literal - literalpart - squotedkey { - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - append tok $c - } else { - append tok $c - } - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace space during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "whitespace" - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - if {$had_slash} { - error "tomlish unexpected backslash [tomlish::parse::report_line]" - } - set_tokenType "whitespace" - append tok $c - } - } - } - } - tab { - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out (?review) - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #whitespace is a terminator for bare keys - incr i -1 - #set_token_waiting type whitespace value $c complete 1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - squotedkey { - append tok $c - } - dquotedkey - string - comment - whitespace { - #REVIEW - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - incr i -1 - return 1 - } - } - literal - literalpart { - append tok $c - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace tab during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - if {$slash_active} { - set slash_active 0 - } - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType whitespace - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "whitespace" - append tok $c - } - } - } - } - bom { - #BOM (Byte Order Mark) - ignored by token consumer - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #assert - tok will be one or two squotes only - #A toml literal probably isn't allowed to contain this - #but we will parse and let the validator sort it out. - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart { - append tok $c - } - string - stringpart { - append tok $c - } - default { - #state machine will generally not have entry to accept bom - let it crash - set_token_waiting type bom value "\uFEFF" complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - multistring-space { - set_tokenType "stringpart" - set tok $c - } - default { - set_tokenType "bom" - set tok "\uFEFF" - return 1 - } - } - } - } - default { - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - #review - incr i -1 ;#We don't have a full token to add to the token_waiting dict - so leave this char for next run. - return 1 - } - } - barekey { - if {[tomlish::utils::is_barekey $c]} { - append tok $c - } else { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed. (see tomlish::utils::is_barekey) [tomlish::parse::report_line]" - } - } - starttablename - starttablearrayname { - incr i -1 - #allow statemachine to set context for subsequent chars - return 1 - } - stringpart { - append tok $c - } - default { - #e.g comment/string/literal/literalpart/untyped_value/starttablename/starttablearrayname/tablename/tablearrayname - append tok $c - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - table-space - itable-space { - #if no currently active token - assume another key value pair - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - multistring-space { - set_tokenType "stringpart" - if {$had_slash} { - set tok \\$c - } else { - set tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - tablename-state { - set_tokenType "tablename" - set tok $c - } - tablearrayname-state { - set_tokenType "tablearrayname" - set tok $c - } - dottedkey-space { - set_tokenType barekey - set tok $c - } - default { - #todo - something like ansistring VIEW to show control chars? - set cshow [string map [list \t tab \v vt] $c] - tomlish::log::debug "- tokloop char '$cshow' setting to untyped_value while state:$state [tomlish::parse::report_line]" - set_tokenType "untyped_value" - set tok $c - } - } - } - } - } - - } - - #run out of characters (eof) - if {[tcl::string::length $tokenType]} { - #check for invalid ending tokens - #if {$state eq "err-state"} { - # error "Reached end of data whilst tokenType = '$tokenType'. INVALID" - #} - switch -exact -- $tokenType { - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open literal - error "tomlish eof reached without closing single quote for string literal. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "literal" - set tok "" - return 1 - - ##review - #set_token_waiting type endsquote value "'" complete 1 startindex [expr {$cindex -1}] - #set_tokenType "literal" - #set tok "" - #return 1 - } - } - } - _start_dquote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open string - error "tomlish eof reached without closing double quote for string. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "string" - set tok "" - return 1 - } - } - } - newline { - #The only newline token that has still not been returned should have a tok value of "cr" - puts "tomlish eof reached - with incomplete newline token '$tok'" - if {$tok eq "cr"} { - #we convert lone cr to it's own "cr" token elsewhere in the document to allow statemachine to handle it. - #(which it should generally do by not handling it ie raising an error - or emitting an ERROR list in the tomlish) - #if trailing char is a lone cr - we should encode it the same way as elsewhere that is outside of values - # ie as it's own token. - switch_tokenType "cr" - return 1 - } else { - #should be unreachable - error "tomlish eof reached - with invalid newline token. value: $tok" - } - } - } - set_token_waiting type eof value eof complete 1 startindex $i ;#review - return 1 - } else { - ::tomlish::log::debug "- No current tokenType, ran out of characters, setting tokenType to 'eof' [tomlish::parse::report_line]" - set tokenType "eof" - set tok "eof" - } - return 0 - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::parse ---}] -} - -namespace eval tomlish::dict { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - - proc is_tomlish_typeval {d} { - #designed to detect {type value } e.g {type INT value 3}, {type STRING value "blah etc"} - #as a sanity check we need to avoid mistaking user data that happens to match same form - #consider x.y={type="spud",value="blah"} - #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. - #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} - } - proc is_tomlish_typeval2 {d} { - upvar ::tomlish::tags tags - expr {[lindex $d 0] eq "type" && [lindex $d 1] in $tags} - } - proc last_tomltype_posn {d} { - set last_simple -1 - set dictposn [expr {[dict size $d] -1}] - foreach k [lreverse [dict keys $d]] { - set dval [dict get $d $k] - if {[is_tomlish_typeval $dval]} { - set last_simple $dictposn - break - } - incr dictposn -1 - } - return $last_simple - } - - - #review - proc name_from_tablestack {tablestack} { - set name "" - foreach tinfo [lrange $tablestack 1 end] { - lassign $tinfo type namepart - switch -- $type { - T { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - I { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - default { - #end at first break in the leading sequence of T & I tablenames - break - } - } - } - return $name - } - - proc _show_tablenames {tablenames_info} { - append msg \n "tablenames_info:" \n - dict for {tkey tinfo} $tablenames_info { - append msg " " "table: $tkey" \n - dict for {field finfo} $tinfo { - append msg " " "$field $finfo" \n - } - } - return $msg - } -} -tcl::namespace::eval tomlish::to_dict { - proc tablename_split {tablename {normalize false}} { - #we can't just split on . because we have to handle quoted segments which may contain a dot. - #eg {dog."tater.man"} - set sLen [tcl::string::length $tablename] - set segments [list] - set mode "preval" ;#5 modes: preval, quoted,litquoted, unquoted, postval - #quoted is for double-quotes, litquoted is for single-quotes (string literal) - set seg "" - for {set i 0} {$i < $sLen} {incr i} { - - if {$i > 0} { - set lastChar [tcl::string::index $tablename [expr {$i - 1}]] - } else { - set lastChar "" - } - - #todo - track\count backslashes properly - - set c [tcl::string::index $tablename $i] - if {$c eq "\""} { - if {($lastChar eq "\\")} { - #not strictly correct - we could have had an even number prior-backslash sequence - #the toml spec would have us error out immediately on bsl in bad location - but we're - #trying to parse to unvalidated tomlish - set ctest escq - } else { - set ctest dq - } - } else { - set ctest [string map [list " " sp \t tab] $c] - } - - switch -- $ctest { - . { - switch -exact -- $mode { - preval { - error "tablename_split. dot not allowed - expecting a value" - } - unquoted { - #dot marks end of segment. - if {![tomlish::utils::is_barekey $seg]} { - error "tablename_split. unquoted key segment $seg is not a valid toml key" - } - lappend segments $seg - set seg "" - set mode "preval" - } - quoted { - append seg $c - } - litquoted { - append seg $c - } - postval { - #got dot in an expected location - set mode "preval" - } - } - } - dq { - #unescaped dquote - switch -- $mode { - preval { - set mode "quoted" - set seg "\"" - } - unquoted { - #invalid in barekey - but we are after structure only - append seg $c - } - quoted { - append seg $c - if {$normalize} { - lappend segments [::tomlish::utils::unescape_string [tcl::string::range $seg 1 end-1]] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" ;#make sure we only accept a dot or end-of-data now. - } - litquoted { - append seg $c - } - postval { - error "tablename_split. expected whitespace or dot, got double quote. tablename: '$tablename'" - } - } - } - ' { - switch -- $mode { - preval { - append seg $c - set mode "litquoted" - } - unquoted { - #single quote inside e.g o'neill - ultimately invalid - but we pass through here. - append seg $c - } - quoted { - append seg $c - } - litquoted { - append seg $c - #no normalization to do aside from stripping squotes - if {$normalize} { - lappend segments [tcl::string::range $seg 1 end-1] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" - } - postval { - error "tablename_split. expected whitespace or dot, got single quote. tablename: '$tablename'" - } - } - } - sp - tab { - switch -- $mode { - preval - postval { - #ignore - } - unquoted { - #terminates a barekey - lappend segments $seg - set seg "" - set mode "postval" - } - default { - #append to quoted or litquoted - append seg $c - } - } - } - default { - switch -- $mode { - preval { - set mode unquoted - append seg $c - } - postval { - error "tablename_split. Expected a dot separator. got '$c'. tablename: '$tablename'" - } - default { - append seg $c - } - } - } - } - - if {$i == $sLen-1} { - #end of data - ::tomlish::log::debug "End of data: mode='$mode'" - switch -exact -- $mode { - preval { - error "tablename_split. Expected a value after last dot separator. tablename: '$tablename'" - } - unquoted { - if {![tomlish::utils::is_barekey $seg]} { - #e.g toml-test invalid/table/with-pound required to fail for invalid barekey - error "tablename_split. unquoted key segment $seg is not a valid toml key" - } - lappend segments $seg - } - quoted { - error "tablename_split. Expected a trailing double quote. tablename: '$tablename'" - } - litquoted { - error "tablename_split. Expected a trailing single quote. tablename: '$tablename'" - } - postval { - #ok - segment already lappended - } - } - } - } - - #note - we must allow 'empty' quoted strings '' & "" - # (these are 'discouraged' but valid toml keys) - - return $segments - } - - #tablenames (& tablearraynames) may contain irrelevant leading, trailing and interspersed whitespace - # tablenames can be made up of segments delimited by dots. .eg [ a.b . c ] - #trimmed, the tablename becomes {a.b.c} - # A segment may contain whitespace if it is quoted e.g [a . b . "c etc " ] - #ie whitespace is only irrelevant if it's outside a quoted segment - #trimmed, the tablename becomes {a.b."c etc "} - proc tablename_trim {tablename} { - set segments [tablename_split $tablename false] - set trimmed_segments [list] - foreach seg $segments { - lappend trimmed_segments [::string trim $seg " \t"] - } - return [join $trimmed_segments .] - } - - proc get_dottedkey_info {dottedkeyrecord} { - set key_hierarchy [list] - set key_hierarchy_raw [list] - if {[lindex $dottedkeyrecord 0] ne "DOTTEDKEY"} { - error "tomlish::to_dict::get_dottedkey_info error. Supplied list doesn't appear to be a DOTTEDKEY (tag: [lindex $dottedkeyrecord 0])" - } - set compoundkeylist [lindex $dottedkeyrecord 1] - set expect_sep 0 - foreach part $compoundkeylist { - set parttag [lindex $part 0] - if {$parttag eq "WS"} { - continue - } - if {$expect_sep} { - if {$parttag ne "DOTSEP"} { - error "DOTTEDKEY missing dot separator between parts. '$dottedkeyrecord'" - } - set expect_sep 0 - } else { - set val [lindex $part 1] - switch -exact -- $parttag { - KEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw $val - } - DQKEY { - lappend key_hierarchy [::tomlish::utils::unescape_string $val] - lappend key_hierarchy_raw \"$val\" - } - SQKEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw "'$val'" - } - default { - error "tomlish::to_dict::get_dottedkey_info DOTTED key unexpected part '$parttag' - ensure dot separator is between key parts. '$item'" - } - } - set expect_sep 1 - } - } - return [dict create keys $key_hierarchy keys_raw $key_hierarchy_raw] - } - -} - - -tcl::namespace::eval tomlish::app { - #*** !doctools - #[subsection {Namespace tomlish::app}] - #[para] - #[list_begin definitions] - - tcl::namespace::eval argdoc { - proc test_suites {} { - if {[package provide test::tomlish] eq ""} { - return [list] - } - return [test::tomlish::SUITES] - } - } - - package require punk::args - punk::args::define { - @id -id ::tomlish::app::decoder - @cmd -name tomlish::app::decoder -help\ - "Read toml on stdin until EOF - on error - returns non-zero exit code and writes error to - the errorchannel. - on success - returns zero exit code and writes JSON encoding - of the data to the outputchannel. - This decoder is intended to be compatble with toml-test." - @leaders -min 0 -max 0 - @opts - -help -type none -help\ - "Display this usage message" - -inputchannel -default stdin - -outputchannel -default stdout - -errorchannel -default stderr - @values -min 0 -max 0 - } - proc decoder {args} { - set argd [punk::args::parse $args withid ::tomlish::app::decoder] - set ch_input [dict get $argd opts -inputchannel] - set ch_output [dict get $argd opts -outputchannel] - set ch_error [dict get $argd opts -errorchannel] - if {[dict exists $argd received -help]} { - return [punk::args::usage -scheme info ::tomlish::app::decoder] - } - - #fconfigure stdin -encoding utf-8 - fconfigure $ch_input -translation binary - #Just slurp it all - presumably we are not handling massive amounts of data on stdin. - # - even if the input is large, we probably don't gain much (aside from possible memory savings?) by attempting to process input as it arrives. - if {[catch { - set toml [read $ch_input] - }]} { - exit 2 ;#read error - } - try { - set j [::tomlish::toml_to_json $toml] - } on error {em} { - puts $ch_error "decoding failed: '$em'" - exit 1 - } - puts -nonewline $ch_output $j - exit 0 - } - - package require punk::args - punk::args::define { - @id -id ::tomlish::app::encoder - @cmd -name tomlish::app::encoder -help\ - "Read JSON on input until EOF - return non-zero exitcode if JSON data cannot be converted to - a valid TOML representation. - return zero exitcode and TOML data on output if JSON data can - be converted. - This encoder is intended to be compatible with toml-test." - @leaders -min 0 -max 0 - @opts - -help -type none -help \ - "Display this usage message" - -inputchannel -default stdin - -outputchannel -default stdout - -errorchannel -default stderr - @values -min 0 -max 0 - } - proc encoder {args} { - set argd [punk::args::parse $args withid ::tomlish::app::encoder] - set ch_input [dict get $argd opts -inputchannel] - set ch_output [dict get $argd opts -outputchannel] - set ch_error [dict get $argd opts -errorchannel] - if {[dict exists $argd received -help]} { - return [punk::args::usage -scheme info ::tomlish::app::encoder] - } - #review - fconfigure $ch_input -translation binary - if {[catch { - set json [read $ch_input] - }]} { - exit 2 ;#read error - } - try { - set toml [::tomlish::json_to_toml $json] - } on error {em} { - puts $ch_error "encoding failed: '$em'" - exit 1 - } - puts -nonewline $ch_output $toml - exit 0 - } - - punk::args::define { - @dynamic - @id -id ::tomlish::app::test - @cmd -name tomlish::app::test - @leaders - @opts -any 1 - -help -type none -help\ - "Display this usage message - or further info if more args." - -suite -default tests -choices {${[::tomlish::app::argdoc::test_suites]}} - @values -min 0 -max -1 - } - proc test {args} { - package require test::tomlish - set argd [punk::args::parse $args withid ::tomlish::app::test] - set opts [dict get $argd opts] - set values [dict get $argd values] - set received [dict get $argd received] - set solos [dict get $argd solos] - set opt_suite [dict get $opts -suite] - if {[dict exists $received -help] && ![dict exists $received -suite]} { - return [punk::args::usage -scheme info ::tomlish::app::test] - } - - test::tomlish::SUITE $opt_suite - #if {[catch {test::tomlish::SUITE $opt_suite} errM]} { - # puts stderr "Unknown test suite '$opt_suite'. Available suites: [test::tomlish::SUITES]" - # exit 1 - #} - set run_opts [dict remove $opts -suite] - set run_opts [dict remove $run_opts {*}$solos] - set result [test::tomlish::RUN {*}$run_opts {*}$solos {*}$values] - return $result - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::app ---}] -} - -proc ::tomlish::appnames {} { - set applist [list] - foreach cmd [info commands ::tomlish::app::*] { - lappend applist [namespace tail $cmd] - } - return $applist -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::lib { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace tomlish::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 tomlish::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -if {[info exists ::argc] && $::argc > 0} { - #puts stderr "argc: $::argc args: $::argv" - set arglist $::argv - # -------------- - #make sure any dependant packages that are sourced don't get any commandline args - set ::argv {} - set ::argc 0 - # -------------- - package require punk::args - punk::args::define { - @dynamic - @id -id tomlish::cmdline - @cmd -name tomlish -help\ - "toml encoder/decoder written in Tcl" - @opts -any 1 - -help -type none -help\ - "Display this usage message or more specific - help if further arguments provided." - -app -choices {${[tomlish::appnames]}} - } - try { - set argd [punk::args::parse $arglist withid tomlish::cmdline] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - puts stderr $msg - exit 1 - } - - - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received -help] && ![dict exists $received -app]} { - #only emit cmdline help if -app not supplied as well - otherwise app function can act on -help for more specific help - #puts stdout "Usage: -app where appname one of:[tomlish::appnames]" - puts stdout [punk::args::usage -scheme info tomlish::cmdline] - exit 0 - } - if {![dict exists $received -app]} { - puts stderr [punk::args::usage -scheme error tomlish::cmdline] - exit 1 - } - - set app [dict get $opts -app] - set appnames [tomlish::appnames] - set app_opts [dict remove $opts -app {*}$solos] - try { - set result [tomlish::app::$app {*}$app_opts {*}$solos {*}$values] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - #The validation error should fully describe the issue - #no need for errortrace - keep the output cleaner - puts stderr $msg - exit 1 - } trap {} {msg erroropts} { - #unexpected error - uncaught throw will produce error trace - #todo - a support msg? Otherwise we may as well just leave off this trap. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - if {"-help" in $solos} { - puts stderr $result - exit 1 - } else { - if {$result ne ""} { - puts stdout $result - exit 0 - } - } - - #set opts [dict create] - #set opts [dict merge $opts $::argv] - - #set opts_understood [list -app ] - #if {"-app" in [dict keys $opts]} { - # #Don't vet the remaining opts - as they are interpreted by each app - #} else { - # foreach key [dict keys $opts] { - # if {$key ni $opts_understood} { - # puts stderr "Option '$key' not understood" - # exit 1 - # } - # } - #} - #if {[dict exists $opts -app]} { - # set app [dict get $opts -app] - # set appnames [tomlish::appnames] - # if {$app ni $appnames} { - # puts stderr "app '[dict get $opts -app]' not found. Available apps: $appnames" - # exit 1 - # } - # tomlish::app::$app {*}$opts - #} -} - -## Ready -package provide tomlish [namespace eval tomlish { - variable pkg tomlish - variable version - set version 1.1.5 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/tomlish-1.1.6.tm b/src/bootsupport/modules/tomlish-1.1.6.tm deleted file mode 100644 index af632c36..00000000 --- a/src/bootsupport/modules/tomlish-1.1.6.tm +++ /dev/null @@ -1,9470 +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 tomlish 1.1.6 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin tomlish_module_tomlish 0 1.1.6] -#[copyright "2024"] -#[titledesc {tomlish toml parser}] [comment {-- Name section and table of contents description --}] -#[moddesc {tomlish}] [comment {-- Description at end of page heading --}] -#[require tomlish] -#[keywords module parsing toml configuration] -#[description] -#[para] tomlish is an intermediate representation of toml data in a tree structure (tagged lists representing type information) -#[para] The design goals are for tomlish to be whitespace and comment preserving ie byte-for byte preservation during roundtrips from toml to tomlish and back to toml -#[para] The tomlish representation can then be converted to a Tcl dict structure or to other formats such as json, -#[para] although these other formats are generally unlikely to retain whitespace or comments -#[para] The other formats also won't preserve roundtripability e.g \t and a literal tab coming from a toml file will be indistinguishable. -#[para] A further goal is to allow at least a useful subset of in-place editing operations which also preserve whitespace and comments. -#[para] e.g leaf key value editing, and table reordering/sorting, key-renaming at any level, key insertions/deletions -#[para] The API for editing (tomldoc object?) may require explicit setting of type if accessing an existing key -#[para] e.g setting a key that already exists and is a different type (especially if nested structure such as a table or array) -#[para] will need a -type option (-force ?) to force overriding with another type such as an int. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of tomlish -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by tomlish -#[list_begin itemized] - -package require Tcl 8.6- -package require struct::stack -package require logger - -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {struct::stack}] - -#limit ourselves to clear, destroy, peek, pop, push, rotate, or size (e.g v 1.3 does not implement 'get') - - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish { - namespace export {[a-z]*}; # Convention: export all lowercase - variable types - - #*** !doctools - #[subsection {Namespace tomlish}] - #[para] Core API functions for tomlish - #[list_begin definitions] - - #default interp recursionlimit of 1000 is insufficient to pass 1000 deep nested structures as in certain toml tests. - #e.g https://github.com/iarna/toml-spec-tests/tree/latest/values - #1000 seems deep for a 'configuration' format - but toml sometimes used for other serialisation purposes. - #todo - review - set existing_recursionlimit [interp recursionlimit {}] - if {$existing_recursionlimit < 5000} { - interp recursionlimit {} 5000 - } - - #IDEAS: - # since get_toml produces tomlish with whitespace/comments intact: - # tomldoc object - allow (at least basic?) editing of toml whilst preserving comments & whitespace - # - setKey (set leaf only to value) how to specify type? -type option? - whole array vs index into arrays and further nested objects? - option for raw toml additions? - # - separate addKey?? - # - deleteKey (delete leaf) - # - deleteTable (delete table - if only has leaves? - option to delete with child tables?) - # - set/add Table? - position in doc based on existing tables/subtables? - - #The tomlish intermediate representation allows things such as sorting the toml document by table name or other re-ordering of tables - - # because the tables include subkeys, comments and newlines within their structure - those elements all come along with it nicely during reordering. - #The same goes for the first newline following a keyval e.g x=1 \ny=2\n\n - #The newline is part of the keyval structure so makes reordering easier - #example from_toml "a=1\nb=2\n\n\n" - # 0 = TOMLISH - # 1 = KEY a = {INT 1} {NEWLINE lf} - # 2 = NEWLINE lf - # 3 = KEY b = {INT 2} {NEWLINE lf} - # 4 = NEWLINE lf - # 5 = NEWLINE lf - - #This reordering idea is complicated by the nature of tablearrays - especially as a table header references last tablearrayname, - # and duplicate table headers are allowed in that context. - #e.g - #[[fruits]] - # name="apple" - # [fruits.metadata] - # id=1 - # - #[unrelated1] - # - #[[fruits]] - # name="pear" - # - #[unrelated2] - # silly="ordering" - # - #[fruits.metadata] - #id=2 - #The TABLEARRAY record can't be completely selfcontained on the default parsing mechanism - because it is legal (though not recommended) to have unrelated tables in between. - #If we were to 'insert' later related records (such as the 2nd [fruits.metadata] above) into the TABLEARRAY structure - then, even though it might produce 'nicer' toml, - # we would lose roundtripability toml->tomlish->toml - # ----------------------------------------------------- - #REVIEW - #todo - some sort of 'normalize'/'grouping' function on tomlish that at least makes records self-contained, and perhaps then (optionally) reorders resulting records sensibly. - #such a function on the tomlish may work - although it would be unwise to duplicate the validation aspects of dict::from_tomlish - #The most practical way might be to use dict::from_tomlish followed by from_dict - but that would lose comment info and formatting. - #In the above example - The decision by the toml author to put [unrelated1] between related tablearrays should be respected, - #but the positioning of [unrelated2] between a tablearray and one of its contained tables is suspect. - #Both [fruits.metadata] table records should theoretically be added as children to their corresponding [[fruits]] tablearray record in the tomlish. (just as their name keys are) - # ----------------------------------------------------- - - - - #ARRAY is analogous to a Tcl list - #TABLE is analogous to a Tcl dict - #WS = inline whitespace - #KEY = bare key and value - #DQKEY = double quoted key and value - #SQKEY = single quoted key and value - #ITABLE = inline table (*can* be anonymous table) - # inline table values immediately create a table with the opening brace - # inline tables are fully defined between their braces, as are dotted-key subtables defined within - # No additional subtables or arrays of tables may be defined within an inline table after the ending brace - they must be entirely self-contained - - set tags [list TOMLISH BOM ARRAY TABLE ITABLE TABLEARRAY WS NEWLINE COMMENT DOTTEDKEY KEY DQKEY SQKEY STRING STRINGPART MULTISTRING LITERAL LITERALPART MULTILITERAL INT FLOAT BOOL] - #DDDD - lappend tags {*}[list\ - DATETIME\ - DATETIME-LOCAL\ - DATE-LOCAL\ - TIME-LOCAL\ - ] - - #removed - ANONTABLE - #tomlish v1.0 should accept arbitrary 64-bit signed ints (from -2^63 to 2^63-1) - #we will restrict to this range for compatibility for now - although Tcl can handle larger (arbitrarily so?) - #todo - configurable - allow empty string for 'unlimited' - set min_int -9223372036854775808 ;#-2^63 - set max_int +9223372036854775807 ;#2^63-1 - - proc Dolog {lvl txt} { - #return "$lvl -- $txt" - set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] tomlish '$txt'" - puts stderr $msg - } - logger::initNamespace ::tomlish - foreach lvl [logger::levels] { - interp alias {} tomlish_log_$lvl {} ::tomlish::Dolog $lvl - log::logproc $lvl tomlish_log_$lvl - } - - - proc tags {} { - return $::tomlish::tags - } - - proc get_dottedkey_info {dottedkeyrecord} { - set key_hierarchy [list] - set key_hierarchy_raw [list] - if {[lindex $dottedkeyrecord 0] ne "DOTTEDKEY"} { - error "tomlish::get_dottedkey_info error. Supplied list doesn't appear to be a DOTTEDKEY (tag: [lindex $dottedkeyrecord 0])" - } - set compoundkeylist [lindex $dottedkeyrecord 1] - set expect_sep 0 - foreach part $compoundkeylist { - set parttag [lindex $part 0] - if {$parttag eq "WS"} { - continue - } - if {$expect_sep} { - if {$parttag ne "DOTSEP"} { - error "DOTTEDKEY missing dot separator between parts. '$dottedkeyrecord'" - } - set expect_sep 0 - } else { - set val [lindex $part 1] - switch -exact -- $parttag { - KEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw $val - } - DQKEY { - #REVIEW unescape or not? - #JJJJ - lappend key_hierarchy [::tomlish::utils::unescape_string $val] - lappend key_hierarchy_raw \"$val\" - } - SQKEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw "'$val'" - } - default { - error "tomlish::get_dottedkey_info DOTTED key unexpected part '$parttag' - ensure dot separator is between key parts. '$compoundkeylist'" - } - } - set expect_sep 1 - } - } - return [dict create keys $key_hierarchy keys_raw $key_hierarchy_raw] - } - - #helper function for tomlish::dict::from_tomlish - proc _get_keyval_value {keyval_element} { - #e.g - #DOTTEDKEY {{KEY a} {WS { }}} = {WS { }} {ARRAY {INT 1} SEP {ITABLE {DOTTEDKEY {{KEY x}} = {INT 1} SEP} {DOTTEDKEY {{KEY y}} = {INT 2}}}} - - log::notice ">>> _get_keyval_value from '$keyval_element'<<<" - #find the value (or 2 values if space separated datetime - and stitch back into one) - # 3 is the earliest index at which the value could occur (depending on whitespace) - if {[lindex $keyval_element 2] ne "="} { - error "tomlish _get_keyval_value keyval_element doesn't seem to be a properly structured { = } list\n $keyval_element" - } - - #review - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keys are @@ paths {@@parenttable @@arrayable @@etc} corresponding to parenttable.arraytable[].etc - #value is a dict with keys such as ttype, tdefined - } - set sublist [lrange $keyval_element 3 end] ;# rhs of = - - set values [list] - set value_posns [list] - set posn 0 - foreach sub $sublist { - #note that a barekey/dquotedkey won't occur directly inside a barekey/dquotedkey - #DDDD - switch -exact -- [lindex $sub 0] { - STRING - LITERAL - MULTISTRING - MULTILITERAL - INT - FLOAT - BOOL - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL - TIME-TZ - TABLE - ARRAY - ITABLE { - lappend values $sub - lappend value_posns $posn - } - DOTTEDKEY { - #we should never see DOTTEDKEY as a toplevel element on RHS - #sanity check in case manually manipulated tomlish - or something went very wrong - set msg "tomlish::_get_keyval_value Unexpected toplevel value element DOTTEDKEY after =" - return -code error -errorcode {TOMLISH SYNTAX UNEXPECTEDDOTTEDKEYRHS} $msg - } - WS - NEWLINE - COMMENT {} - SEP {} - default { - set msg "tomlish::_get_keyval_value Unexpected toplevel value element [lindex $sub 0] after =" - return -code error -errorcode {TOMLISH SYNTAX UNEXPECTED} $msg - } - } - incr posn - } - switch -- [llength $values] { - 0 { - error "tomlish Failed to find value element in KEY. '$keyval_element'" - } - 1 { - lassign [lindex $values 0] type value - } - 2 { - #we generally expect a single 'value' item on RHS of = - #(ignoring WS,NEWLINE,SEP - #(either a simple type, or a container which has multiple values inside) - #exception for space separated datetime which is two toplevel values - - #validate than exactly single space was between the two values - lassign $value_posns p1 p2 - if {$p2 != $p1 +2} { - #sanity check - #can probably only get here through manual manipulation of the tomlish list to an unprocessable form - error "tomlish KEY appears to have more than one part - but not separated by whitespace - invalid '$keyval_element'" - } - set between_token [lindex $sublist $p1+1] - if {[lindex $between_token 1] ne " "} { - error "tomlish KEY in 2 parts is not separated by a single space - cannot consider for datetime '$keyval_element'" - } - lassign [lindex $values 0] type_d1 value_d1 - lassign [lindex $values 1] type_d2 value_d2 - #DDDD - if {$type_d1 ne "DATE-LOCAL" || $type_d2 ni {TIME-TZ TIME-LOCAL}} { - #we reuse DATETIME tag for standalone time with tz offset (or zZ) - error "tomlish KEY in 2 parts does not appear to be datetime '$keyval_element'" - } - if {$type_d2 eq "TIME-LOCAL"} { - set type DATETIME-LOCAL - } elseif {$type_d2 eq "TIME-TZ"} { - set type DATETIME - } else { - error "tomlish KEY in 2 parts does not appear to be datetime. (part 2 not a time value) '$keyval_element'" - } - set value "${value_d1}T${value_d2}" - } - default { - error "tomlish Found multiple value elements in KEY, expected one. (or 2 for space-separated datetime) '$keyval_element'" - } - } - set sub_tablenames_info [dict create] - switch -exact -- $type { - TIME-TZ { - #This is only valid in tomlish following a DATE-LOCAL - error "tomlish type TIME-TZ was not preceeded by DATE-LOCAL in keyval '$keyval_element'" - } - INT - FLOAT - BOOL - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - #DDDD - #simple (non-container, no-substitution) datatype - set result [list type $type value $value] - } - STRING - STRINGPART { - #JJJ - #!!! review - #set result [list type $type value [::tomlish::utils::unescape_string $value]] - set result [list type $type value $value] - } - LITERAL - LITERALPART { - #REVIEW - set result [list type $type value $value] - } - TABLE { - #invalid? - error "tomlish _get_keyval_value invalid to have type TABLE on rhs of =" - } - ITABLE { - #This one should not be returned as a type value structure! - # - set prev_tablenames_info $tablenames_info - set tablenames_info [dict create] - set result [::tomlish::dict::from_tomlish [ list [lindex $values 0] ]] - set sub_tablenames_info $tablenames_info - set tablenames_info $prev_tablenames_info - } - ARRAY { - #we need to recurse to get the corresponding dict for the contained item(s) - #pass in the whole [lindex $values 0] (type val) - not just the $value! - set prev_tablenames_info $tablenames_info - set tablenames_info [dict create] - set result [list type $type value [ ::tomlish::dict::from_tomlish [ list [lindex $values 0] ] ]] - set sub_tablenames_info $tablenames_info - set tablenames_info $prev_tablenames_info - } - MULTISTRING - MULTILITERAL { - #review - mapping these to STRING might make some conversions harder? - #if we keep the MULTI - we know we have to look for newlines for example when converting to json - #without specific types we'd have to check every STRING - and lose info about how best to map chars within it - set result [list type $type value [ ::tomlish::dict::from_tomlish [ list [lindex $values 0] ] ]] - } - default { - error "tomlish Unexpected value type '$type' found in keyval '$keyval_element'" - } - } - return [dict create result $result tablenames_info $sub_tablenames_info] - } - - - proc to_dict {tomlish {returnextra 0}} { - tomlish::dict::from_tomlish $tomlish $returnextra - } - - - - proc _from_dictval_tomltype {parents tablestack keys typeval} { - set type [dict get $typeval type] - set val [dict get $typeval value] - #These are the restricted sets of typed used in the tomlish::dict representation - #They are a subset of the types in tomlish: data types plus ARRAY, arranged in a dictionary form. - #The container types: ITABLE, TABLE, TABLEARRAY are not used as they are represented as dictionary keys and ARRAY items. - #The WS, COMMENT, and NEWLINE elements are also unrepresented in the dict structure. - switch -- $type { - ARRAY { - set subitems [list] - foreach item $val { - lappend subitems [_from_dictval [list {*}$parents ARRAY] $tablestack $keys $item] SEP - } - if {[lindex $subitems end] eq "SEP"} { - set subitems [lrange $subitems 0 end-1] - } - return [list ARRAY {*}$subitems] - } - ITABLE { - error "not applicable" - if {$val eq ""} { - return ITABLE - } else { - return [_from_dictval [list {*}$parents ITABLE] $tablestack $keys $val] - } - } - STRING { - #JSJS - #if our dict came from json - we have already decided what type of STRING/LITERAL etc to use when building the dict - - #do not validate like this - important that eg json val\\ue -> dict val\ue -> tomlish/toml val\\ue - #see toml-tests - #if {![tomlish::utils::rawstring_is_valid_tomlstring $val]} { - # #todo? - # return -code error -errorcode {TOML SYNTAX INVALIDSTRING} "Unescaped controls in string or invalid escapes" - #} - return [list STRING [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val]] - } - MULTISTRING { - #value is a raw string that isn't encoded as tomlish - #create a valid toml snippet with the raw value and decode it to the proper tomlish MULTISTRING format - #We need to convert controls in $val to escape sequences - except for newlines - # - #consider an *option* to reformat for long lines? (perhaps overcomplex - byte equiv - but may fold in ugly places) - #we could use a line-length limit to decide when to put in a "line ending backslash" - #and even format it with a reasonable indent so that proper CONT and WS entries are made (?) REVIEW - # - #TODO - #set tomlpart "x=\"\"\"\\\n" ;#no need for continuation - set tomlpart "x=\"\"\"\n" - append tomlpart [tomlish::utils::rawstring_to_MultiBstring_with_escaped_controls $val] - append tomlpart "\"\"\"" - set tomlish [tomlish::from_toml $tomlpart] - #e.g if val = " etc\nblah" - #TOMLISH {DOTTEDKEY {{KEY x}} = {MULTISTRING CONT {NEWLINE LF} {WS { }} {STRINGPART etc} {NEWLINE lf} {STRINGPART blah} } } - #lindex 1 3 is the MULTISTRING tomlish list - return [lindex $tomlish 1 3] - } - MULTILITERAL { - #MLL string can contain newlines - but still no control chars - #todo - validate - e.g val can't contain more than 2 squotes in a row - if {[string first ''' $val] >=0} { - set msg "_from_dictval_tomltype error: more than 2 single quotes in a row found in MULTILITERAL - cannot encode dict to TOML-VALID TOMLISH" - return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} $msg - } - - #rawstring_is_valid_multiliteral - allow newlines as lf or crlf - but not bare cr - if {![tomlish::utils::rawstring_is_valid_multiliteral $val]} { - return -code error -errorcode {TOML SYNTAX INVALIDMULTILITERAL} "Controls other than tab or newlines found in multiliteral" - } - - set tomlpart "x='''\n" - append tomlpart $val ''' - set tomlish [tomlish::from_toml $tomlpart] - return [lindex $tomlish 1 3] - } - LITERAL { - #from v1.0 spec - "Control characters other than tab are not permitted in a literal string" - #(This rules out raw ANSI SGR - which is somewhat restrictive - but perhaps justified for a config format - # as copy-pasting ansi to a config value is probably not always wise, and it's not something that can be - # easily input via a text editor. ANSI can go in Basic strings using the \e escape if that's accepted v1.1?) - #we could choose to change the type to another format here when encountering invalid chars - but that seems - #like too much magic. We elect to error out and require the dict to have valid data for the types it specifies. - if {[string first ' $val] >=0} { - set msg "_from_dictval_tomltype error: single quote found in LITERAL - cannot encode dict to TOML-VALID TOMLISH" - return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} $msg - } - #JJJJ - if {![tomlish::utils::rawstring_is_valid_literal $val]} { - #has controls other than tab - #todo - squote? - return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} "Controls other than tab found in literal" - } - return [list LITERAL $val] - } - INT { - if {![::tomlish::utils::is_int $val]} { - error "_from_dictval_tomltype error: bad INT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list INT $val] - } - FLOAT { - if {![::tomlish::utils::is_float $val]} { - error "_from_dictval_tomltype error: bad FLOAT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list FLOAT $val] - } - default { - if {$type ni [::tomlish::tags]} { - error "_from_dictval_tomltype error: Unrecognised typename '$type' in {type value } - cannot encode dict to TOML-VALID TOMLISH" - } - return [list $type $val] - } - } - } - - proc _from_dictval {parents tablestack keys vinfo} { - set k [lindex $keys end] - set K_PART [tomlish::dict::classify_rawkey $k] ;#get [list SQKEY ] - #puts stderr "---parents:'$parents' keys:'$keys' vinfo: $vinfo---" - #puts stderr "---tablestack: $tablestack---" - set result [list] - set lastparent [lindex $parents end] - if {$lastparent in [list "" do_inline]} { - if {[tomlish::dict::is_typeval $vinfo]} { - set type [dict get $vinfo type] - #treat ITABLE differently? - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result DOTTEDKEY [list $K_PART {WS { }}] = {WS { }} $sublist {NEWLINE lf} - } else { - if {$vinfo ne ""} { - if {![tomlish::utils::string_is_dict $vinfo]} { - #e.g tomlish::dict::from_tomlish was called with return_extra 1 - return -code error -errorcode {TOMLISH SYNTAX INVALIDDICT} "tomlish::_from_dictval Supplied dict is not a valid format for converting to tomlish" ;#review - } - - #set result [list DOTTEDKEY [list [list KEY $k]] = ] - #set records [list ITABLE] - - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - - if {$lastparent eq "do_inline"} { - set result [list DOTTEDKEY [list $K_PART] =] - set records [list ITABLE] - } else { - set tname [tomlish::dict::join_and_quote_rawkey_list [list $k]] - set result [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $k]] - set records [list] - } - - - - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - dict for {vk vv} $vinfo { - set VK_PART [tomlish::dict::classify_rawkey $vk] ;#get [list SQKEY ] - #(SQKEY & DQKEY do not have the enclosing quotes in their returned val) - #if {[regexp {\s} $vk] || [string first . $vk] >= 0} { - # set VK_PART [list SQKEY $vk] - #} else { - # set VK_PART [list KEY $vk] - #} - if {[tomlish::dict::is_typeval $vv]} { - #type x value y - #REVIEW - we could detect if value is an array of objects, - #and depending on parent context - emit a series of TABLEARRAY records instead of a DOTTEDKEY record containing an ARRAY of objects - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART {WS { }}] = {WS { }} $sublist] - } else { - if {$vv eq ""} { - #experimental - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - ::tomlish::log::notice "_from_dictval could uninline KEY $vk (tablestack:$tablestack)" - #set tname [tomlish::dict::name_from_tablestack [list {*}$tablestack [list T $vk]]] - - #we can't just join normalized keys - need keys with appropriate quotes and escapes - #set tname [join [list {*}$keys $vk] .] ;#WRONG - set tq [tomlish::dict::join_and_quote_rawkey_list [list {*}$keys $vk]] - - - ##wrong? results in TABLE within TABLE record?? todo pop? - #set record [list TABLE $tq {NEWLINE lf}] - #set tablestack [list {*}$tablestack [list T $vk]] - - #REVIEW!!! - - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - - } else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - } - } else { - if { 0 } { - #experiment.. sort of getting there. - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - ::tomlish::log::notice "_from_dictval could uninline2 KEYS [list {*}$keys $vk] (tablestack:$tablestack)" - set tq [tomlish::dict::join_and_quote_rawkey_list [list {*}$keys $vk]] - set record [list TABLE $tq {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - - #review - todo? - set dottedkey_value [_from_dictval [list {*}$parents TABLE] $tablestack [list {*}$keys $vk] $vv] - lappend record {*}$dottedkey_value - - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - } - if {$dictidx != $lastidx} { - #lappend record SEP - if {$lastparent eq "do_inline"} { - lappend record SEP - } else { - lappend record {NEWLINE lf} - } - } - if {[llength $record]} { - lappend records $record - } - incr dictidx - } - if {$lastparent eq "do_inline"} { - lappend result $records {NEWLINE lf} - } else { - lappend result {*}$records {NEWLINE lf} - } - } else { - if {$lastparent eq "do_inline"} { - lappend result DOTTEDKEY [list $K_PART] = ITABLE {NEWLINE lf} - } else { - set tname [tomlish::dict::join_and_quote_rawkey_list [list $k]] - #REVIEW - lappend result TABLE $tname {NEWLINE lf} - } - } - } - } else { - #lastparent is not toplevel "" or "do_inline" - if {[tomlish::dict::is_typeval $vinfo]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result {*}$sublist - } else { - if {$lastparent eq "TABLE"} { - #review - dict for {vk vv} $vinfo { - set VK_PART [tomlish::dict::classify_rawkey $vk] ;#get [list SQKEY ] - set dottedkey_value [_from_dictval [list {*}$parents DOTTEDKEY] $tablestack [list {*}$keys $vk] $vv] - lappend result [list DOTTEDKEY [list $VK_PART] = $dottedkey_value {NEWLINE lf}] - } - } else { - if {$vinfo ne ""} { - if {![tomlish::utils::string_is_dict $vinfo]} { - #e.g tomlish::dict::from_tomlish was called with return_extra 1 - return -code error -errorcode {TOMLISH SYNTAX INVALIDDICT} "tomlish::_from_dictval Supplied dict is not a valid format for converting to tomlish" ;#review - } - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - set sub [list] - #REVIEW - #set result $lastparent ;#e.g sets ITABLE - set result ITABLE - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - dict for {vk vv} $vinfo { - set VK_PART [tomlish::dict::classify_rawkey $vk] ;#get [list SQKEY ] - if {[tomlish::dict::is_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART] = $sublist] - } else { - if {$vv eq ""} { - #can't just uninline at this level - #we need a better method to query main dict for uninlinability at each level - # (including what's been inlined already) - #if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - # puts stderr "_from_dictval uninline2 KEY $keys" - # set tname [tomlish::dict::join_and_quote_rawkey_list [list {*}$keys $vk]] - # set record [list TABLE $tname {NEWLINE lf}] - # set tablestack [list {*}$tablestack [list T $vk]] - #} else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - #} - } else { - #set sub [_from_dictval ITABLE $vk $vv] - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - #set record [list DOTTEDKEY [list $VK_PART] = ITABLE $dottedkey_value] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - if {$dictidx != $lastidx} { - lappend record SEP - } - lappend result $record - incr dictidx - } - } else { - #e.g x=[{}] - log::debug "---> _from_dictval empty ITABLE x-1" - #lappend result DOTTEDKEY [list $K_PART] = ITABLE ;#wrong - lappend result ITABLE - } - } - } - } - return $result - } - - - proc from_dict {d} { - #consider: - # t1={a=1,b=2} - # x = 1 - - # from_dict gives us: t1 {a {type INT value 1} b {type INT value 2}} x {type INT value 1} - - #If we represent t1 as an expanded table we get - # [t1] - # a=1 - # b=2 - # x=1 - # --- which is incorrect - as x was a toplevel key like t1! - #This issue doesn't occur if x is itself an inline table - # t1={a=1,b=2} - # x= {no="problem"} - # - # (or if we were to reorder x to come before t1) - - #ie the order of the dict elements influences how the toml can be represented. - - #As the dictionary form doesn't distinguish the structure used to create tables {[table1]\nk=v} vs inline {table1={k=v}} - #Without a solution, from_dict would have to always produce the inline form for toplevel tables unless we allowed re-ordering, - #which is unpreferred here. - - #A possible solution: - #scan the top level to see if all (trailing) elements are themselves dicts - # (ie not of form {type XXX value yyy}) - # - # A further point is that if all root level values are at the 'top' - we can treat lower table-like structures as {[table]} elements - #ie we don't need to force do_inline if all the 'simple' keys are before any compound keys - - #set root_has_values 0 - #approach 1) - the naive approach - forces inline when not always necessary - #dict for {k v} $d { - # if {[llength $v] == 4 && [lindex $v 0] eq "type"} { - # set root_has_values 1 - # break - # } - #} - - - #approach 2) - track the position of last {type x value y} in the dictionary built by dict::from_tomlish - # - still not perfect. Inlines dotted tables unnecessarily - #This means from_dict doesn't produce output optimal for human editing. - set last_simple [tomlish::dict::last_tomltype_posn $d] - - - ## set parent "do_inline" ;#a value used in _from_dictval to distinguish from "" or other context based parent values - #Any keys that are themselves tables - will need to be represented inline - #to avoid reordering, or incorrect assignment of plain values to the wrong table. - - ## set parent "" - #all toplevel keys in the dict structure can represent subtables. - #we are free to use {[tablename]\n} syntax for toplevel elements. - - - set tomlish [list TOMLISH] - set dictposn 0 - set tablestack [list [list T root]] ;#todo - dict for {t tinfo} $d { - if {$last_simple > $dictposn} { - set parents [list do_inline] - } else { - set parents [list ""] - } - set keys [list $t] - #review - where to make decision on - # DOTTEDKEY containing array of objs - #vs - # list of TABLEARRAY records - #At least for the top - set trecord [_from_dictval $parents $tablestack $keys $tinfo] - lappend tomlish $trecord - incr dictposn - } - return $tomlish - } - - proc typedjson_to_toml {json} { - #*** !doctools - #[call [fun typedjson_to_toml] [arg json]] - #[para] - - set tomlish [::tomlish::from_dict_from_typedjson $json] - lappend tomlish [list NEWLINE lf] - set toml [::tomlish::to_toml $tomlish] - } - - set json1 {{ "a": {"type": "integer", "value": "42"}}} - set json2 {{ - "a": {"type": "integer", "value": "42"}, - "b": {"type": "string", "value": "test"} - }} - set json3 { -{ - "best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}, - "numtheory": { - "boring": {"type": "bool", "value": "false"}, - "perfection": [ - {"type": "integer", "value": "6"}, - {"type": "integer", "value": "28"}, - {"type": "integer", "value": "496"} - ] - } -} - } - - set json4 { -{ - "best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}, - "numtheory": { - "boring": {"type": "bool", "value": "false"}, - "perfection": [ - {"type": "integer", "value": "6"}, - {"type": "integer", "value": "28"}, - {"type": "integer", "value": "496"} - ] - }, - "emptyobj": {}, - "emptyarray": [] -} - } - - set json5 { -{ - "a": { - " x ": {}, - "b.c": {}, - "d.e": {}, - "b": { - "c": {} - } - } -} - } - - #surrogate pair face emoji - set json6 { -{ - "surrogatepair": {"type": "string", "value": "\uD83D\uDE10"} -} - } - - - set json7 { -{ - "escapes": {"type": "string", "value": "val\\ue"} -} - } - - - proc from_dict_from_typedjson {json} { - set d [tomlish::dict::from_typedjson $json] - tomlish::from_dict $d ;#return tomlish - } - - - proc toml_to_typedjson {toml} { - set tomlish [::tomlish::from_toml $toml] - set d [tomlish::dict::from_tomlish $tomlish] - #full validation only occurs by re-encoding dict to tomlish - set test [tomlish::from_dict $d] - - set h [tomlish::typedhuddle::from_dict $d] - #huddle jsondump $h - tomlish::huddle::jsondumpraw $h - } - - #proc get_json {tomlish} { - # package require fish::json - # set d [::tomlish::dict::from_tomlish $tomlish] - - # #return [::tomlish::dict_to_json $d] - # return [fish::json::from "struct" $d] - #} - - #return a Tcl list of tomlish tokens - #i.e get a standard list of all the toml terms in string $s - #where each element of the list is a *tomlish* term.. i.e a specially 'tagged' Tcl list. - #(simliar to a tcl 'Huddle' - but also supporting whitespace preservation) - # ---------------------------------------------------------------------------------------------- - # NOTE: the production of tomlish from toml source doesn't indicate the toml source was valid!!! - # e.g we deliberately don't check certain things such as duplicate table declarations here. - # ---------------------------------------------------------------------------------------------- - #Part of the justification for this is that as long as the syntax is toml shaped - we can load files which violate certain rules and allow programmatic manipulation. - # (e.g perhaps a toml editor to highlight violations for fixing) - # A further stage is then necessary to load the tomlish tagged list into a data structure more suitable for efficient query/reading. - # e.g dicts or an object oriented structure - #Note also - *no* escapes in quoted strings are processed. This is up to the datastructure stage - #e.g dict::from_tomlish will substitute \r \n \uHHHH \UHHHHHHH etc - #This is important for tomlish to maintain the ability to perform competely lossless round-trips from toml to tomlish and back to toml. - # (which is handy for testing as well as editing some part of the structure with absolutely no effect on other parts of the document) - #If we were to unescape a tab character for example - # - we have no way of knowing if it was originally specified as \t \u0009 or \U00000009 or directly as a tab character. - # For this reason, we also do absolutely no line-ending transformations based on platform. - # All line-endings are maintained as is, and even a file with mixed lf crlf line-endings will be correctly interpreted and can be 'roundtripped' - - proc from_toml {args} { - - namespace upvar ::tomlish::parse s s - set s [join $args \n] - namespace upvar ::tomlish::parse i i - set i 0 ;#index into s - - namespace upvar ::tomlish::parse is_parsing is_parsing - set is_parsing 1 - - if {[info command ::tomlish::parse::spacestack] eq "::tomlish::parse::spacestack"} { - tomlish::parse::spacestack destroy - } - struct::stack ::tomlish::parse::spacestack - - namespace upvar ::tomlish::parse last_space_action last_space_action - namespace upvar ::tomlish::parse last_space_type last_space_type - - namespace upvar ::tomlish::parse tok tok - set tok "" - - namespace upvar ::tomlish::parse type type - namespace upvar ::tomlish::parse tokenType tokenType - ::tomlish::parse::set_tokenType "" - namespace upvar ::tomlish::parse tokenType_list tokenType_list - set tokenType [list] ;#Flat (un-nested) list of tokentypes found - - namespace upvar ::tomlish::parse lastChar lastChar - set lastChar "" - - - set result "" - namespace upvar ::tomlish::parse nest nest - set nest 0 - - namespace upvar ::tomlish::parse v v ;#array keyed on nest level - - - set v(0) {TOMLISH} - array set s0 [list] ;#whitespace data to go in {SPACE {}} element. - set parentlevel 0 - - - namespace upvar ::tomlish::parse state state - - namespace upvar ::tomlish::parse braceCount braceCount - set barceCount 0 - namespace upvar ::tomlish::parse bracketCount bracketCount - set bracketCount 0 - - set sep 0 - set r 1 - namespace upvar ::tomlish::parse token_waiting token_waiting - set token_waiting [dict create] ;#if ::tok finds a *complete* second token during a run, it will put the 2nd one here to be returned by the next call. - - - - set state "table-space" - ::tomlish::parse::spacestack push {type space state table-space} - namespace upvar ::tomlish::parse linenum linenum;#'line number' of input data. (incremented for each literal linefeed - but not escaped ones in data) - set linenum 1 - - set ::tomlish::parse::state_list [list] - try { - while {$r} { - set r [::tomlish::parse::tok] - #puts stdout "got tok: '$tok' while parsing string '$s' " - set next_tokenType_known 0 ;#whether we begin a new token here based on what terminated the token result of 'tok' - - - #puts "got token: '$tok' tokenType='$tokenType'. while v($nest) = [set v($nest)]" - #puts "-->tok: $tok tokenType='$tokenType'" - set prevstate $state - set transition_info [::tomlish::parse::goNextState $tokenType $tok $state] - #review goNextState could perform more than one space_action - set space_action [dict get $transition_info space_action] - set newstate [dict get $transition_info newstate] ;#use of 'newstate' vs 'state' makes code clearer below - - if {[tcl::string::match "err-*" $state]} { - ::tomlish::log::warn "---- State error in state $prevstate for tokenType: $tokenType token value: $tok. $state aborting parse. [tomlish::parse::report_line]" - lappend v(0) [list ERROR tokentype $tokenType state $prevstate to $state leveldata [set v($nest)]] - return $v(0) - } - # --------------------------------------------------------- - #NOTE there may already be a token_waiting at this point - #set_token_waiting can raise an error here, - # in which case the space_action branch needs to be rewritten to handle the existing token_waiting - # --------------------------------------------------------- - - if {$space_action eq "pop"} { - #pop_trigger_tokens: newline tablename endarray endinlinetable - #note a token is a pop trigger depending on context. e.g first newline during keyval is a pop trigger. - set parentlevel [expr {$nest -1}] - set do_append_to_parent 1 ;#most tokens will leave this alone - but some like tentative_accum_squote need to do their own append - switch -exact -- $tokenType { - tentative_accum_squote { - #should only apply within a multiliteral - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the xxx-squote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-squote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-squote-space { - } - default { - error "--- unexpected popped due to tentative_accum_squote but came from state '$prevstate' should have been trailing-squote-space" - } - } - switch -- $tok { - ' { - tomlish::parse::set_token_waiting type single_squote value $tok complete 1 startindex [expr {$i -1}] - } - '' { - #review - we should perhaps return double_squote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_squote value "" complete 1 startindex [expr {$i - 2}] - } - ''' { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 3}] - } - '''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left squote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]'" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "'"] - } - MULTILITERAL { - #empty - lappend v($parentlevel) [list LITERALPART "'"] - } - default { - error "--- don't know how to integrate extra trailing squote with data $v($parentlevel)" - } - } - } - ''''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 squotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]''" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "''"] - } - MULTILITERAL { - lappend v($parentlevel) [list LITERALPART "''"] - } - default { - error "--- don't know how to integrate extra trailing 2 squotes with data $v($parentlevel)" - } - } - } - } - } - triple_squote { - #presumably popping multiliteral-space - ::tomlish::log::debug "---- triple_squote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTILITERAL { - lappend merged $part - } - LITERALPART { - if {$lasttype eq "LITERALPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_squote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - tentative_accum_dquote { - #should only apply within a multistring - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the trailing-dquote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-dquote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-dquote-space { - } - default { - error "--- unexpected popped due to tentative_accum_dquote but came from state '$prevstate' should have been trailing-dquote-space" - } - } - switch -- $tok { - {"} { - tomlish::parse::set_token_waiting type single_dquote value $tok complete 1 startindex [expr {$i -1}] - } - {""} { - #review - we should perhaps return double_dquote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_dquote value "" complete 1 startindex [expr {$i - 2}] - } - {"""} { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 3}] - } - {""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left dquote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {"}] - } - MULTISTRING { - #empty - lappend v($parentlevel) [list STRINGPART {"}] - } - default { - error "--- don't know how to integrate extra trailing dquote with data $v($parentlevel)" - } - } - } - {"""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 dquotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {""}] - } - MULTISTRING { - lappend v($parentlevel) [list STRINGPART {""}] - } - default { - error "--- don't know how to integrate extra trailing 2 dquotes with data $v($parentlevel)" - } - } - } - } - } - triple_dquote { - #presumably popping multistring-space - ::tomlish::log::debug "---- triple_dquote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTISTRING { - lappend merged $part - } - STRINGPART { - if {$lasttype eq "STRINGPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - CONT - WS { - lappend merged $part - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_dquote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - equal { - #pop caused by = - switch -exact -- $prevstate { - dottedkey-space { - tomlish::log::debug "---- equal ending dottedkey-space for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - dottedkey-space-tail { - #experiment? - tomlish::log::debug "---- equal ending dottedkey-space-tail for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - } - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - tablename { - #note: a tablename only 'pops' if we are greater than zero - error "---- tablename pop should already have been handled as special case zeropoppushspace in goNextState" - } - tablearrayname { - #!review - tablearrayname different to tablename regarding push/pop? - #note: a tablename only 'pops' if we are greater than zero - error "---- tablearrayname pop should already have been handled as special case zeropoppushspace in goNextState" - } - endarray { - #nothing to do here. - } - comma { - #comma for inline table will pop the keyvalue space - lappend v($nest) "SEP" - } - endinlinetable { - ::tomlish::log::debug "---- endinlinetable for last_space_action pop" - } - default { - error "---- unexpected tokenType '$tokenType' for last_space_action 'pop'" - } - } - if {$do_append_to_parent} { - #e.g tentative_accum_squote does it's own appends as necessary - so won't get here - lappend v($parentlevel) [set v($nest)] - } - - incr nest -1 - - } elseif {$last_space_action eq "push"} { - set prevnest $nest - incr nest 1 - set v($nest) [list] - # push_trigger_tokens: barekey dquotedkey startinlinetable startarray tablename tablearrayname - - - switch -exact -- $tokenType { - tentative_trigger_squote - tentative_trigger_dquote { - #### this startok will always be tentative_accum_squote/tentative_accum_dquote starting with one accumulated squote/dquote - if {[dict exists $transition_info starttok] && [dict get $transition_info starttok] ne ""} { - lassign [dict get $transition_info starttok] starttok_type starttok_val - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType $starttok_type - set tok $starttok_val - } - } - single_squote { - #JMN - REVIEW - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - triple_squote { - ::tomlish::log::debug "---- push trigger tokenType triple_squote" - set v($nest) [list MULTILITERAL] ;#container for NEWLINE,LITERALPART - } - squotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - triple_dquote { - set v($nest) [list MULTISTRING] ;#container for NEWLINE,STRINGPART,CONT - } - dquotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - barekey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - set waiting [tomlish::parse::get_token_waiting] - if {[llength $waiting]} { - set i [dict get $waiting startindex] - tomlish::parse::clear_token_waiting - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } else { - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - } - tablename { - #note: we do not use the output of tablename_trim to produce a tablename for storage in the tomlish list! - #The tomlish list is intended to preserve all whitespace (and comments) - so a roundtrip from toml file to tomlish - # back to toml file will be identical. - #It is up to the datastructure stage to normalize and interpret tomlish for programmatic access. - # we call tablename_trim here only to to validate that the tablename data is well-formed at the outermost level, - # so we can raise an error at this point rather than create a tomlish list with obviously invalid table names from - # a structural perspective. - - #todo - review! It's arguable that we should not do any validation here, and just store even incorrect raw tablenames, - # so that the tomlish list is more useful for say a toml editor. Consider adding an 'err' tag to the appropriate place in the - # tomlish list? - - #set trimtable [tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablename: '$trimtable'" - set v($nest) [list TABLE $tok] ;#$tok is the *raw* table name - #note also that equivalent tablenames may have different toml representations even after being trimmed! - #e.g ["x\t\t"] & ["x "] (tab escapes vs literals) - #These will show as above in the tomlish list, but should normalize to the same tablename when used as keys by the datastructure stage. - } - tablearrayname { - #set trimtable [tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablearrayname: '$trimtable'" - set v($nest) [list TABLEARRAY $tok] ;#$tok is the *raw* tablearray name - } - startarray { - set v($nest) [list ARRAY] ;#$tok is just the opening bracket - don't output. - } - startinlinetable { - set v($nest) [list ITABLE] ;#$tok is just the opening curly brace - don't output. - } - default { - error "---- push trigger tokenType '$tokenType' not yet implemented" - } - } - - } else { - #no space level change - switch -exact -- $tokenType { - squotedkey { - #puts "---- squotedkey in state $prevstate (no space level change)" - lappend v($nest) [list SQKEY $tok] - } - dquotedkey { - #puts "---- dquotedkey in state $prevstate (no space level change)" - lappend v($nest) [list DQKEY $tok] - } - barekey { - lappend v($nest) [list KEY $tok] - } - dotsep { - lappend v($nest) [list DOTSEP] - } - starttablename { - #$tok is triggered by the opening bracket and sends nothing to output - } - starttablearrayname { - #$tok is triggered by the double opening brackets and sends nothing to output - } - tablename - tablenamearray { - error "---- did not expect 'tablename/tablearrayname' without space level change (no space level change)" - #set v($nest) [list TABLE $tok] - } - endtablename - endtablearrayname { - #no output into the tomlish list for this token - } - startinlinetable { - puts stderr "---- decode::toml error. did not expect startinlinetable without space level change (no space level change)" - } - single_dquote { - switch -exact -- $newstate { - string-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "string" - set tok "" - } - dquoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "dquotedkey" - set tok "" - } - multistring-space { - lappend v($nest) [list STRINGPART {"}] - #may need to be joined on pop if there are neighbouring STRINGPARTS - } - default { - error "---- single_dquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_dquote { - #leading extra quotes - test: toml_multistring_startquote2 - switch -exact -- $prevstate { - itable-keyval-value-expected - keyval-value-expected { - puts stderr "tomlish::decode::toml double_dquote TEST" - #empty string - lappend v($nest) [list STRINGPART ""] - } - multistring-space { - #multistring-space to multistring-space - lappend v($nest) [list STRINGPART {""}] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - - } - single_squote { - switch -exact -- $newstate { - literal-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "literal" - set tok "" - } - squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - multiliteral-space { - #false alarm squote returned from tentative_accum_squote pop - ::tomlish::log::debug "---- adding lone squote to own LITERALPART nextstate: $newstate (no space level change)" - #(single squote - not terminating space) - lappend v($nest) [list LITERALPART '] - #may need to be joined on pop if there are neighbouring LITERALPARTs - } - default { - error "---- single_squote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_squote { - switch -exact -- $prevstate { - keyval-value-expected { - lappend v($nest) [list LITERAL ""] - } - multiliteral-space { - #multiliteral-space to multiliteral-space - lappend v($nest) [list LITERALPART ''] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - enddquote { - #nothing to do? - set tok "" - } - endsquote { - set tok "" - } - string { - #JJJJ - set tok [tomlish::from_Bstring $tok] - lappend v($nest) [list STRING $tok] ;#directly wrapped in dquotes - } - literal { - lappend v($nest) [list LITERAL $tok] ;#directly wrapped in squotes - } - multistring { - #review - #JJJJ ? - lappend v($nest) [list MULTISTRING $tok] - } - stringpart { - #JJJJ - set tok [tomlish::from_Bstring $tok] - lappend v($nest) [list STRINGPART $tok] ;#will not get wrapped in dquotes directly - } - multiliteral { - lappend v($nest) [LIST MULTILITERAL $tok] - } - literalpart { - lappend v($nest) [list LITERALPART $tok] ;#will not get wrapped in squotes directly - } - untyped_value { - #would be better termed unclassified_value - #we can't determine the type of unquoted values (int,float,datetime,bool) until the entire token was read. - unset -nocomplain tag - if {$tok in {true false}} { - set tag BOOL - } else { - if {[::tomlish::utils::is_int $tok]} { - set tag INT - } else { - if {[::tomlish::utils::string_is_integer -strict $tok]} { - #didn't qualify as a toml int - but still an int - #probably means is_int is limiting size and not accepting bigints (configurable?) - #or it didn't qualify due to more than 1 leading zero - #or other integer format issue such as repeated underscores - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. (looks close to being an int. Formatting or range issue?) [tomlish::parse::report_line] (no space level change)" - } else { - #DDDD - if {[::tomlish::utils::is_float $tok]} { - set tag FLOAT - } elseif {[::tomlish::utils::is_time-local $tok]} { - set tag TIME-LOCAL - } elseif {[::tomlish::utils::is_timepart $tok]} { - ###################################### - #Note we must allow lone timepart here (not just is_time-local which doesn't allow tz offsets) in case it followed a previous localdate - #set tag DATETIME ;#PLACEHOLDER tag - review standalone time with tz - no specific tag - only allowed as followup value from DATE-LOCAL - set tag TIME-TZ - #This will become a DATETIME or a DATETIME-LOCAL (or will error) - ###################################### - } elseif {[::tomlish::utils::is_date-local $tok]} { - set tag DATE-LOCAL - } elseif {[::tomlish::utils::is_date_or_time_or_datetime $tok]} { - #not just a date or just a time - #could be either local or have tz offset - #DDDD JJJ - set norm [string map {" " T} $tok];#prob unneeded - we won't get here if there was a space - would arrive as 2 separate tokens review. - lassign [split $norm T] dp tp - if {[::tomlish::utils::is_time-local $tp]} { - set tag DATETIME-LOCAL - } else { - set tag DATETIME - } - } else { - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. [tomlish::parse::report_line] (no space level change)" - } - } - } - } - #assert either tag is set, or we errored out. - lappend v($nest) [list $tag $tok] - - } - comment { - #puts stdout "----- comment token returned '$tok'------" - #JJJJ - set tok [tomlish::from_comment $tok] - lappend v($nest) [list COMMENT "$tok"] - } - equal { - #we append '=' to the nest so that any surrounding whitespace is retained. - lappend v($nest) = - } - comma { - lappend v($nest) SEP - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - whitespace { - lappend v($nest) [list WS $tok] - } - continuation { - lappend v($nest) CONT - } - bom { - lappend v($nest) BOM - } - eof { - #ok - nothing more to add to the tomlish list. - #!todo - check previous tokens are complete/valid? - } - default { - error "--- unknown tokenType '$tokenType' during state $prevstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - - if {!$next_tokenType_known} { - ::tomlish::log::notice "---- tomlish::decode::toml - current tokenType:$tokenType Next token type not known" - ::tomlish::parse::set_tokenType "" - set tok "" - } - - if {$state eq "end-state"} { - break - } - - - } - - #while {$nest > 0} { - # lappend v([expr {$nest -1}]) [set v($nest)] - # incr nest -1 - #} - while {[::tomlish::parse::spacestack size] > 1} { - ::tomlish::parse::spacestack pop - lappend v([expr {$nest -1}]) [set v($nest)] - incr nest -1 - - #set parent [spacestack peek] ;#the level being appended to - #lassign $parent type state - #if {$type eq "space"} { - # - #} elseif {$type eq "buffer"} { - # lappend v([expr {$nest -1}]) {*}[set v($nest)] - #} else { - # error "invalid spacestack item: $parent" - #} - } - - } finally { - set is_parsing 0 - } - return $v(0) - } - - #toml dquoted string to tomlish STRING - # - only allow specified escape sequences - # - allow any unicode except those that must be escaped: dquote, bsl, and control chars(except tab) - proc from_Bstring {bstr} { - #JJJJ - if {[catch { - tomlish::utils::unescape_string $bstr - } errM]} { - return -code error -errorcode {TOML SYNTAX INVALIDESCAPE} "tomlish::from_Bstring toml Bstring contains invalid escape sequence\n$errM" ;#review - } - #assert: all escapes are now valid - - if {[regexp {[\u0000-\u0008\u000A-\u001F\u007f]} $bstr]} { - set msg "tomlish::from_Bstring toml Bstring contains controls that must be escaped" - return -code error -errorcode {TOML SYNTAX BSTRINGUNESCAPEDCONTROLS} $msg ;#review - } - return $bstr - } - #validate toml comment - # - disallow controls that must be escaped - #from spec: - # "Control characters other than tab (U+0000 to U+0008, U+000A to U+001F, U+007F) are not permitted in comments." - proc from_comment {comment} { - if {[regexp {[\u0000-\u0008\u000A-\u001F\u007f]} $comment]} { - set msg "tomlish::from_comment toml comment contains controls that must be escaped" - return -code error -errorcode {TOML SYNTAX COMMENTUNESCAPEDCONTROLS} $msg ;#review - } - return $comment - } - - - #return TOMLISH { value} from new and existing typeval dicts of form {type value value} but - # some such as MULTISTRING can be of form { ...} - # - #Don't validate here - validate in tomlish::dict::path::setleaf - proc _update_tomlish_typeval_convert_to_new_from_existing {new existing} { - #we deliberately don't support container types that can contain comments e.g ARRAY, ITABLE, DOTTEDKEY - #This is also not for higher level constructs such as TABLE, TABLEARRAY - if {!([tomlish::dict::is_typeval $target] && [tomlish::dict_is_typveval $source])} { - error "_update_tomlish_typeval_convert_to: target and source must be of form {type value are contained in the table - foreach tr $tablechildren { - set tr_type [lindex $tr 0] - switch -- $tr_type { - NEWLINE - WS - COMMENT { - lappend updated_tablechildren $tr - } - DOTTEDKEY { - #review - #UUU - set dktomlish [list TOMLISH $tr] - set dkdict [::tomlish::to_dict $dktomlish] - set newdktomlish [update_tomlish_from_dict $dktomlish $subd] - set newrecords [lrange $newdktomlish 1 end];#strip TOMLISH - lappend updated_tablechildren {*}$newrecords - } - default { - error "update_tomlish_from_dict: unexpected table record type $tr_type" - } - } - } - - #todo - add leaves from subd that weren't in the tablechildren list - #ordering? - - lappend output_tomlish [list {*}[lrange $tomlish_record 0 1] {*}$updated_tablechildren] - } - DOTTEDKEY { - #We don't have to check toml table rules regarding created/defined here as dict::from_tomlish has already ensured correctness - #UUU - set dkinfo [tomlish::get_dottedkey_info $tomlish_record] ;#e.g keys {j { k} l} keys_raw {j {' k'} l} - set keys [dict get $dkinfo keys] - set dk_refpath [lmap k $keys {string cat @@ $k}] - - set kvinfo [tomlish::_get_keyval_value $tomlish_record] - set existing_typeval [dict get $kvinfo result] - if {[tomlish::dict::is_typeval $existing_typeval] && [dict get $existing_typeval type] ne "ARRAY"} { - #leaf in supplied tomlish - source dict must also be leaf (invalid to rewrite a branch) - #e.g - #DOTTEDKEY {{KEY j} DOTSEP {SQKEY { k}} DOTSEP {KEY l}} = {INT 0} {WS { }} {COMMENT comment} {NEWLINE lf} - #existing_typeval: {type INT value 0} - #e.g - #DOTTEDKEY {{KEY j} DOTSEP {SQKEY { k}} DOTSEP {KEY l}} = {MULTISTRING {WS { }} {STRINGPART x} {WS { }}} {WS { }} {COMMENT comment} {NEWLINE lf} - #existing_typeval: {type MULTISTRING value { x }} - - #see if source dict has a simple typeval to set - set new_typeval [tomlish::dict::path::get $d $dk_refpath] - if {![tomlish::dict::is_typeval $new_typeval]} { - error "update_tomlish_from_dict - update dictionary has non-leaf data at path $dk_refpath - cannot set" - } - #update if type matches. Todo - flag -allowtypechange ? - set e_type [dict get $existing_typeval type] - set n_type [dict get $new_typeval type] - if {$e_type ne $n_type} { - error "update_tomlish_from_dict - cannot change type $e_type to $n_type at path $dk_refpath" - } - #-start 3 to begin search after = - set valindex [lsearch -start 3 -index 0 $tomlish_record $e_type] - if {$valindex == -1} { - error "update_tomlish_from_dict - unexpected error - failed to find $e_type in record $tomlish_record" - } - set rawval [dict get $new_typeval value] - switch -- $e_type { - MULTISTRING { - #UUU - set newval [tomlish::utils::rawstring_to_MultiBstring_with_escaped_controls $rawval] - set toml "" - append toml "x=\"\"\"" \n - append toml "$newval\"\"\"" \n - set tomlish [lrange [tomlish::from_toml $toml] 1 end] ;#remove TOMLISH keyword - #assert tomlish is a list with a single element - #e.g {DOTTEDKEY {{KEY x}} = {MULTISTRING {NEWLINE lf} {STRINGPART aaa}} {NEWLINE lf}} - set dklist [lindex $tomlish 0] - set msrecord [lindex $dklist 3] - #e.g - #MULTISTRING {NEWLINE lf} {STRINGPART aaa} - - #error "update_tomlish_from_dict MULTISTRING update unimplemented. Todo" - lset tomlish_record $valindex $msrecord - } - MULTILITERAL { - set toml "" - append toml "x='''" \n - append toml "$rawval'''" \n - set tomlish [lrange [tomlish::from_toml $toml] 1 end] ;#remove TOMLISH keyword - set dklist [lindex $tomlish 0] - set msrecord [lindex $dklist 3] - lset tomlish_record $valindex $msrecord - } - default { - switch -- $e_type { - STRING { - #review - set newval [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $rawval] - } - default { - set newval $rawval - } - } - lset tomlish_record $valindex [list $e_type $newval] - } - } - - } elseif {[tomlish::dict::is_typeval $existing_typeval] && [dict get $existing_typeval type] eq "ARRAY"} { - #e.g - #DOTTEDKEY {{KEY a}} = {ARRAY {INT 1} SEP {INT 2} SEP {INT 3}} - #DOTTEDKEY {{KEY a} {WS { }}} = {WS { }} {ARRAY {INT 1} {WS { }} SEP {INT 2} {WS { }} SEP {INT 3}} {WS { }} - #existing_typeval: {type ARRAY value {{type INT value 1} {type INT value 2} {type INT value 3}}} - - #= is always at index 2 (any preceding whitespace is attached to keylist) - set valindex [lsearch -start 3 -index 0 $tomlish_record ARRAY] - if {$valindex == -1} { - error "update_tomlish_from_dict - unexpected error - failed to find ARRAY in record $tomlish_record" - } - - set existing_arraytomlish [lindex $tomlish_record $valindex] - puts "update_tomlish_from_dict: existing_arraytomlish: $existing_arraytomlish" - set subd [tomlish::dict::path::get $d $dk_refpath] - #set existing_items [tomlish::dict::from_tomlish $tomlish_record] ;#utilise fragment processing of dict::from_tomlish - to produce a LIST - #we expect the subdict structure to be something like: - # {type ARRAY value {{type INT value 1} {type INT value 2}}} - # or with untagged subdicts (ITABLE in tomlish) - # {type ARRAY value {{x {type INT value 1}} {type INT value 2}}} - - - #we can only have one ARRAY record - so we can use lset - set newsubrecord_itable [update_tomlish_from_dict [list $existing_arraytomlish] $subd] - lset tomlish_record $valindex [lindex $newsubrecord_itable 0] ;#passed in a single element tomlish list - expect only one back - - } elseif {[tomlish::dict::is_typeval_dict $existing_typeval]} { - #Not actually a {type value } structure. - #sub dict (ITABLE) - #e.g - #DOTTEDKEY {{KEY j} DOTSEP {SQKEY { k}} DOTSEP {KEY l}} = {ITABLE {DOTTEDKEY {{KEY q}} = {INT 1}}} {WS { }} {COMMENT comment} {NEWLINE lf} - #DOTTEDKEY {{KEY x} {WS { }}} = {WS { }} {ITABLE {WS { }} {DOTTEDKEY {{KEY j}} = {INT 1} {WS { }} SEP} {WS { }} {DOTTEDKEY {{KEY k} {WS { }}} = {WS { }} {INT 333}}} {WS { }} {COMMENT {test }} - #existingvaldata: {q {type INT value 1}} - set subd [tomlish::dict::path::get $d $dk_refpath] - #= is always at index 2 (any preceding whitespace is attached to keylist) - set valindex [lsearch -start 3 -index 0 $tomlish_record ITABLE] - if {$valindex == -1} { - error "update_tomlish_from_dict - unexpected error - failed to find ITABLE in record $tomlish_record" - } - #we can only have one ITABLE record - so we can use lset - - set itablerecord [lindex $tomlish_record $valindex] - puts "update_tomlish_from_dict: existing_itabletomlish: $itablerecord" - set newsubrecord_itable [update_tomlish_from_dict [list $itablerecord] $subd] - lset tomlish_record $valindex [lindex $newsubrecord_itable 0] - } else { - #unreachable? - dict::from_tomlish didn't object. - error "update_tomlish_from_dict: Unexpected data in DOTTEDKEY record: $existing_typeval" - } - lappend output_tomlish $tomlish_record - } - ARRAY { - #UUU - #fragment recursion - puts "update_tomlish_from_dict: process ARRAY fragment" - puts "tomlish:\n$tomlish" - puts "updatedict:\n$d" - set source_d_elements [tomlish::dict::path::get $d {[]}] - - set updated_arraychildren [list] - set arrayrecord $tomlish_record - set arraychildren [lrange $arrayrecord 1 end] ;#includes WS, SEP, NEWLINE, COMMENT - set arridx 0 - set childidx 0 - foreach arrchild $arraychildren { - set arrchild_type [lindex $arrchild 0] - switch -- $arrchild_type { - SEP { - #we don't check for proper SEP interspersal here, presuming well-formed tomlish - review - lappend updated_arraychildren $arrchild - } - NEWLINE - WS - COMMENT { - lappend updated_arraychildren $arrchild - } - default { - #updatables - #review - type changes from existing value?? - set sourcedata [lindex $source_d_elements $arridx] - #todo - what happens when less source elements than in existing array? ie sourcedata is empty. - # - switch -- $arrchild_type { - STRING - LITERAL - FLOAT - INT - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - #basic types - no recursion needed - #REVIEW - change of type? flag to allow/disallow? - if {![tomlish::dict::is_typeval $sourcedata]} { - error "update_tomlish_from_dict - update dictionary has non-leaf data at path \[$arridx\] - cannot set" - } - set newval [dict get $sourcedata value] - set newtype [dict get $sourcedata type] - if {$newtype eq "STRING"} { - set newval [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $newval] - } - lappend updated_arraychildren [list $newtype $newval] - } - MULTISTRING { - #no need to recurse - puts stderr "multistring within array update - unimplemented" - } - MULTILITERAL { - #no need to recurse - puts stderr "multiliteral within array update - unimplemented" - } - ITABLE - ARRAY { - #recurse - puts stderr "update $arrchild_type within array" - set nextd [tomlish::dict::path::get $d $arridx] - set subrecord_tomlish [list $arrchild] - set newsubrecord_tomlish [update_tomlish_from_dict $subrecord_tomlish $nextd] - lappend updated_arraychildren {*}$newsubrecord_tomlish - } - default { - error "update_tomlish_from_dict: unexpected array child record type $arrchild_type" - } - } - incr arridx ;#only increment array index for updatables - } - } - } - - lappend output_tomlish [list ARRAY {*}$updated_arraychildren] - } - ITABLE { - #fragment recursion target - #ITABLE {DOTTEDKEY {{KEY j}} = {INT 1}} - #ITABLE {WS { }} {DOTTEDKEY {{KEY j}} = {INT 1} {WS { }} SEP} {WS { }} {DOTTEDKEY {{KEY k} {WS { }}} = {WS { }} {INT 333}} - #ITABLE {NEWLINE lf} {DOTTEDKEY {{KEY j} {WS { }}} = {WS { }} {INT 1} SEP} {WS { }} {COMMENT test} {NEWLINE lf} {WS { }} {DOTTEDKEY {{KEY k}} = {WS { }} {INT 2} {NEWLINE lf}} - puts "update_tomlish_from_dict: process ITABLE fragment" - puts "tomlish:\n$tomlish" - puts "updatedict:\n$d" - set updated_itablechildren [list] - set itablechildren [lrange $tomlish_record 1 end] ;#includes WS, NEWLINE, COMMENT (possibly SEP - though it may be attached to DOTTEDKEY record REVIEW) - #we only expect DOTTEDKEY records for data items within ITABLE - foreach itablechild $tomlish_record { - set itablechild_type [lindex $itablechild 0] - switch -- $itablechild_type { - SEP { - #REVIEW - #we don't necessarily expect a SEP *directly* within ITABLE records as currently when they're created by tomlish::from_toml - #it attaches them (along with intervening WS, COMMENTs) to each DOTTEDKEY record - #This feels somewhat misaligned with ARRAY - where we have no choice but to have SEP, and COMMENTs independent of the array elements. - #Attaching COMMENTs, SEP to the previous DOTTEDKEY has some merit - but perhaps consistency with ARRAY would be preferable. - #This may change - but in any case it should probably be valid/handled gracefully either way. - lappend updated_itablechildren $itablechild - } - COMMENT - WS - NEWLINE { - lappend updated_itablechildren $itablechild - } - DOTTEDKEY { - puts stderr "update dottedkey in itable: tomlish:[list $itablechild] d:$d" - set updatedtomlish [update_tomlish_from_dict [list $itablechild] $d] - set newrecord [lindex $updatedtomlish 0] - lappend updated_itablechildren $newrecord - } - } - } - - lappend output_tomlish [list ITABLE {*}$updated_itablechildren] - } - default { - error "update_tomlish_from_dict: Unexpected toplevel type $tomlish_type record: $tomlish_record" - } - } - } - return $output_tomlish - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -namespace eval tomlish::build { - #STRING,INT,FLOAT,BOOL, DATETIME - simple wrappers for completeness - # take a value of the appropriate type and wrap as a tomlish tagged item - proc STRING {s} { - return [list STRING [::tomlish::utils::rawstring_to_Bstring_with_escaped_controls $s]] - } - proc LITERAL {litstring} { - error todo - } - - proc INT {i} { - #whole numbers, may be prefixed with a + or - - #Leading zeros are not allowed - #Hex,octal binary forms are allowed (toml 1.0) - #We will error out on encountering commas, as commas are interpreted differently depending on locale (and don't seem to be supported in the toml spec anyway) - #!todo - Tcl can handle bignums - bigger than a 64bit signed long as specified in toml. - # - We should probably raise an error for number larger than this and suggest the user supply it as a string? - if {[tcl::string::last , $i] > -1} { - error "Unable to interpret '$i' as an integer. Use underscores if you need a thousands separator [::tomlish::parse::report_line]" - } - if {![::tomlish::utils::int_validchars $i]} { - error "Unable to interpret '$i' as an integer. Only 0-9 + 1 _ characters are acceptable. [::tomlish::parse::report_line]" - } - - if {[::tomlish::utils::is_int $i]} { - return [list INT $i] - } else { - error "'$i' is not a valid integer as per the Toml spec. [::tomlish::parse::report_line]" - } - - } - - proc FLOAT {f} { - #convert any non-lower case variants of special values to lowercase for Toml - if {[::tcl::string::tolower $f] in {nan +nan -nan inf +inf -inf}} { - return [list FLOAT [tcl::string::tolower $f]] - } - if {[::tomlish::utils::is_float $f]} { - return [list FLOAT $f] - } else { - error "Unable to interpret '$f' as Toml float. Check your input, or check that tomlish is able to handle all Toml floats properly [::tomlish::parse::report_line]" - } - } - - proc DATETIME {str} { - if {[::tomlish::utils::is_date_or_time_or_datetime $str]} { - return [list DATETIME $str] - } else { - error "Unable to interpret '$str' as Toml datetime. Check your input, or check that tomlish is able to handle all Toml datetimes properly [::tomlish::parse::report_line]" - } - } - proc DATETIME-LOCAL {str} { - error "build::DATETIME-LOCAL todo" - } - - proc BOOLEAN {b} { - #convert any Tcl-acceptable boolean to boolean as accepted by toml - lower case true/false - if {![tcl::string::is boolean -strict $b]} { - error "Unable to convert '$b' to Toml boolean true|false. [::tomlish::parse::report_line]" - } else { - if {$b && 1} { - return [::list BOOL true] - } else { - return [::list BOOL false] - } - } - } - - #REVIEW - #Take tablename followed by - # a) *tomlish* name-value pairs e.g table mydata [list KEY item11 = [list STRING "test"]] {KEY item2 = [list INT 1]} - # (accept also key value {STRING }) - # b) simple 2-element tcl lists being name & *simple* value pairs for which basic heuristics will be used to determine types - proc _table {name args} { - set pairs [list] - foreach t $args { - if {[llength $t] == 4} { - if {[tcl::string::tolower [lindex $t 0]] ne "key" || [tcl::string::tolower [lindex $t 2]] ni "= value"} { - error "Only items tagged as KEY = currently accepted as name-value pairs for table command" - } - lassign $t _k keystr _eq valuepart - if {[llength $valuepart] != 2} { - error "supplied value must be typed. e.g {INT 1} or {STRING test}" - } - lappend pairs [list KEY $keystr = $valuepart] - } elseif {[llength $t] == 2} { - #!todo - type heuristics - lassign $t n v - lappend pairs [list KEY $n = [list STRING $v]] - } else { - error "'KEY = { toml but - # the first newline is not part of the data. - # we elect instead to maintain a basic LITERALPART that must not contain newlines.. - # and to compose MULTILITERAL of multiple NEWLINE LITERALPART parts, - #with the datastructure representation dropping the first newline (if immediately following opening delim) when building the value. - set literal "" - foreach part [lrange $item 1 end] { - append literal [::tomlish::encode::tomlish [list $part] $nextcontext] - } - append toml '''$literal''' - } - INT - - BOOL - - FLOAT - - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - #DDDD - append toml [lindex $item 1] - } - INCOMPLETE { - error "cannot process tomlish term tagged as INCOMPLETE" - } - COMMENT { - append toml "#[lindex $item 1]" - } - BOM { - #Byte Order Mark may appear at beginning of a file. Needs to be preserved. - append toml "\uFEFF" - } - default { - error "Not a properly formed 'tomlish' taggedlist.\n '$list'\n Unknown tag '[lindex $item 0]'. See output of \[tomlish::tags\] command." - } - } - - } - return $toml - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::encode ---}] -} -#fish toml from tomlish - -#(encode tomlish as toml) -interp alias {} tomlish::to_toml {} tomlish::encode::tomlish - -# - - -namespace eval tomlish::decode { - #*** !doctools - #[subsection {Namespace tomlish::decode}] - #[para] - #[list_begin definitions] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::decode ---}] -} -#decode toml to tomlish -#interp alias {} tomlish::from_toml {} tomlish::decode::toml - -namespace eval tomlish::utils { - #*** !doctools - #[subsection {Namespace tomlish::utils}] - #[para] - #[list_begin definitions] - - #------------------------------------------------------------------------------ - # Tcl 8.6 support - #------------------------------------------------------------------------------ - if {[catch {tcl::string::is dict {}}]} { - proc string_is_dict {str} { - #we don't support -strict or -failindex for this fallback - expr {[::tcl::string::is list $str] && ([llength $str] % 2 == 0)} - } - } else { - proc string_is_dict {str} { - #we don't support -strict or -failindex for this fallback even though underlying supports it - ::tcl::string::is dict $str - } - } - if {![string is integer [expr {2**32}]]} { - proc string_is_integer {args} { - ::tcl::string::is entier {*}$args - } - } else { - proc string_is_integer {args} { - ::tcl::string::is integer {*}$args - } - } - #------------------------------------------------------------------------------ - - #subset of jq syntax for get/set operations on dicts - # no filters or multiple targets - # meant for 'leaf' queries - proc jq_to_path {jq} { - set jq [string trim $jq] ;#don't tokenize any leading/trailing whitespace - set path [list] - set in_arr 0 - set in_dq 0 - set tok "" - set bsl 0 - foreach c [split $jq ""] { - if {$c eq "\\"} { - if {$bsl} { - set bsl 0 - set c "\\" - } else { - set bsl 1 - continue - } - } else { - if {$bsl} { - set c "\\$c" - set bsl 0 - } - } - if {$in_arr} { - switch -- $c { - {]} { - set in_arr 0 - lappend path $tok - set tok "" - } - default { - append tok $c - } - } - } elseif {$in_dq} { - if {$c eq "\""} { - set in_dq 0 - #append tok "\"" - lappend path $tok - set tok "" - } else { - append tok $c - } - } else { - switch -- $c { - . { - if {$tok ne ""} { - lappend path $tok - } - set tok "@@" - } - {[} { - if {$tok ne ""} { - lappend path $tok - } - set in_arr 1 - set tok "" - } - {"} { - if {$tok eq "@@"} { - #set tok "@@\"" - set in_dq 1 - } else { - append tok "\"" - } - } - default { - append tok $c - } - } - } - } - if {$tok ne ""} { - lappend path $tok - } - return $path - } - proc path_to_jq {path} { - set jq "" - foreach p $path { - if {[string match @@* $p]} { - set key [string range $p 2 end] - if {![tomlish::utils::is_barekey $key]} { - set key [subst -nocommands -novariables $key] - set key "\"[tomlish::utils::rawstring_to_Bstring_with_escaped_controls $key]\"" - } - append jq ".$key" - } else { - append jq {[} $p {]} - } - } - return $jq - } - - - - #basic generic quote matching for single and double quotes - #note for example that {[o'malley]} will return sq - as the single quote is not closed or wrapped in double quotes - proc tok_in_quotedpart {tok} { - set sLen [tcl::string::length $tok] - set quote_type "" - set had_slash 0 - for {set i 0} {$i < $sLen} {incr i} { - set c [tcl::string::index $tok $i] - if {$quote_type eq ""} { - if {$had_slash} { - #don't enter quote mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - set quote_type dq - } - sq { - set quote_type sq - } - bsl { - set had_slash 1 - } - } - } - } else { - if {$had_slash} { - #don't leave quoted mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - if {$quote_type eq "dq"} { - set quote_type "" - } - } - sq { - if {$quote_type eq "sq"} { - set quote_type "" - } - } - bsl { - set had_slash 1 - } - } - } - } - } - return $quote_type ;#dq | sq - } - - proc hex_escape_info {slashx} { - set exp {^\\x([0-9a-fA-F]{2}$)} - if {[regexp $exp $slashx match hex]} { - return [list ok [list char [subst -nocommand -novariable $slashx]]] - } else { - return [list err [list reason "Supplied string not of the form \\xHH where H in \[0-9a-fA-F\]"]] - } - } - proc unicode_escape_info {slashu} { - #!todo - # validate that slashu is either a \uxxxx or \Uxxxxxxxx value of the correct length and - # is a valid 'unicode scalar value' (any Unicode code point except high-surrogate and low-surrogate code points) - # ie integers in the range 0 to D7FF16 and E00016 to 10FFFF16 inclusive - #expr {(($x >= 0) && ($x <= 0xD7FF16)) || (($x >= 0xE00016) && ($x <= 0x10FFFF16))} - if {[tcl::string::match {\\u*} $slashu]} { - set exp {^\\u([0-9a-fA-F]{4}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %4x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } - } else { - return [list err [list reason "Supplied string not of the form \\uHHHH where H in \[0-9a-fA-F\]"]] - } - } elseif {[tcl::string::match {\\U*} $slashu]} { - set exp {^\\U([0-9a-fA-F]{8}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %8x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - if {(($dec >= 0) && ($dec <= 0xD7FF16)) || (($dec >= 0xE00016) && ($dec <= 0x10FFFF16))} { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } else { - return [list err [list reason "$slashu is not within the 'unicode scalar value' ranges 0 to 0xD7FF16 or 0xE00016 to 0x10FFFF16"]] - } - } - } else { - return [list err [list reason "Supplied string not of the form \\UHHHHHHHH where H in \[0-9a-fA-F\]"]] - } - } else { - return [list err [list reason "Supplied string did not start with \\u or \\U" ]] - } - - } - - #Note that unicode characters don't *have* to be escaped. - #So if we provide a function named 'escape_string', the name implies the inverse of unescape_string which unescapes unicode \u \U values. - #- an inverse of unescape_string would encode all unicode chars unnecessarily. - #- as toml accepts a compact escape sequence for common chars such as tab,backspace,linefeed etc but also allows the full form \u009 etc - #- escape_string and unescape_string would not be reliably roundtrippable inverses anyway. - #REVIEW - provide it anyway? When would it be desirable to use? - - variable Bstring_control_map [dict create] - dict set Bstring_control_map \b {\b} - dict set Bstring_control_map \n {\n} - dict set Bstring_control_map \r {\r} - dict set Bstring_control_map \" {\"} - dict set Bstring_control_map \x1b {\e} ;#In spec it's included in the list of 'must be escaped', as well as the 'convenience' escapes - so we make it go both ways. - dict set Bstring_control_map \\ "\\\\" - - #\e for \x1b seems like it might be included - v1.1?? hard to find current state of where toml is going :/ - #for a Bstring (Basic string) tab is explicitly mentioned as not being one that must be escaped. - #8 = \b - already in list. - #built the remainder whilst checking for entries already hardcoded above -in case more are added to the hardcoded list - for {set cdec 0} {$cdec <= 7} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - for {set cdec [expr {0x0A}]} {$cdec <= 0x1F} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - # \u007F = 127 - dict set Bstring_control_map [format %c 127] \\u007F - - # ------------------------------------------------------------------ - variable Literal_control_map [dict create] - #controls other than tab - for {set cdec 0} {$cdec <= 8} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Literal_control_map $char]} { - dict set Literal_control_map $char \\u$hhhh - } - } - for {set cdec [expr {0x0A}]} {$cdec <= 0x1F} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Literal_control_map $char]} { - dict set Literal_control_map $char \\u$hhhh - } - } - # \u007F = 127 - dict set Literal_control_map [format %c 127] \\u007F - # ------------------------------------------------------------------ - variable Multiliteral_control_map - set Multiliteral_control_map [dict remove $Literal_control_map \n] - - variable String_control_map - set String_control_map [dict remove $Literal_control_map \\] - - - variable MultiBstring_totoml_map - #'minimally' escaped sequences of double quotes. - #e.g {""\"""\"} vs {\"\"\"\"\"} - #This produces easier to read toml - and in many cases may be more likely to match original format when roundtripped from dict datastructure - # REVIEW - should this be configurable? - set MultiBstring_totoml_map [dict remove $Bstring_control_map {"} \r \n] - dict set MultiBstring_totoml_map {"""} {""\"} ;#" editor hack: commented quote for dumb syntax highlighers - - #Note the inclusion of backslash in the list of controls makes this non idempotent - subsequent runs would keep encoding the backslashes! - #escape only those chars that must be escaped in a Bstring (e.g not tab which can be literal or escaped) - #for example - can be used by from_dict to produce valid Bstring data for a tomlish record - proc rawstring_to_Bstring_with_escaped_controls {str} { - #for the well known chars that have compact escape sequences allowed by toml - we choose that form over the full \u form. - #we'll use a string map with an explicit list rather than algorithmic at runtime - # - the string map is probably more performant than splitting a string, especially if it's large - - upvar ::tomlish::utils::Bstring_control_map map - - return [string map $map $str] - } - proc rawstring_to_MultiBstring_with_escaped_controls {str} { - #for the well known chars that have compact escape sequences allowed by toml - we choose that form over the full \u form. - #we'll use a string map with an explicit list rather than algorithmic at runtime - # - the string map is probably more performant than splitting a string, especially if it's large - - upvar ::tomlish::utils::MultiBstring_totoml_map map - - return [string map $map $str] - } - - #anything is valid in this direction ?? review - #proc rawstring_is_valid_tomlstring {str} { - # #controls are allowed in this direction dict -> toml (they get quoted) - - # #check any existing escapes are valid - # if {[catch { - # unescape_string $str - # } errM]} { - # return 0 - # } - # return 1 - #} - - - #REVIEW - easier way to validate? regex? - #This is not used for the parsing of toml to tomlish, - # but can be used to validate for updating via dict e.g when setting with tomlish::dict::path::setleaf - proc inner_MultiBstring_is_valid_toml {str} { - set without_literal_backslashes [string map [list "\\\\" ""] $str] - #replace only escaped dquotes - use a placeholder - we don't want unescaped runs of dquotes merging. - set without_escaped_dquotes [string map [list "\\\"" ""] $without_literal_backslashes] - - if {[string first "\"\"\"" $without_escaped_dquotes] != -1} { - return 0 - } - #assert - all remaining backslashes are escapes - - #strip remaining dquotes - set dquoteless [string map [list "\"" ""] $without_escaped_dquotes] - #puts stderr "dquoteless: $dquoteless" - - #check any remaining escapes are valid - if {[catch { - #don't use the returned value - just check it - unescape_string $without_literal_backslashes - } errM]} { - return 0 - } - - - variable Bstring_control_map - #remove backslash from control map - we are happy with the remaining escapes (varying length) - set testmap [dict remove $Bstring_control_map "\\" \r \n] - set testval [string map $testmap $dquoteless] - #if they differ - there were raw controls - return [expr {$testval eq $dquoteless}] - } - proc inner_Bstring_is_valid_toml {str} { - set without_literal_backslashes [string map [list "\\\\" ""] $str] - #replace only escaped dquotes - use a placeholder - we don't want unescaped runs of dquotes merging. - set without_escaped_dquotes [string map [list "\\\"" ""] $without_literal_backslashes] - - #plain Bstring can't have unescaped dquotes at tall - if {[string first "\"" $without_escaped_dquotes] != -1} { - return 0 - } - #assert - all remaining backslashes are escapes - - #check any remaining escapes are valid - if {[catch { - #don't use the returned value - just check it - unescape_string $without_literal_backslashes - } errM]} { - return 0 - } - - variable Bstring_control_map - #remove backslash from control map - we are happy with the remaining escapes (varying length) - set testmap [dict remove $Bstring_control_map "\\"] - set testval [string map $testmap $without_escaped_dquotes] - #if they differ - there were raw controls - return [expr {$testval eq $without_escaped_dquotes}] - } - - proc rawstring_is_valid_literal {str} { - #detect control chars other than tab - variable Literal_control_map - set testval [string map $Literal_control_map $str] - return [expr {$testval eq $str}] - } - proc rawstring_is_valid_multiliteral {str} { - #detect control chars other than tab - variable Multiliteral_control_map - - set teststr [string map [list \r\n ok] $str] - - set testval [string map $Multiliteral_control_map $teststr] - return [expr {$testval eq $teststr}] - } - - #review - unescape what string? Bstring vs MLBstring? - #we should be specific in the function naming here - #used by dict::from_tomlish - so part of validation? - REVIEW - proc unescape_string {str} { - #note we can't just use Tcl subst because: - # it also transforms \a (audible bell) and \v (vertical tab) which are not in the toml spec. - # it would strip out backslashes inappropriately: e.g "\j" becomes just j - # it recognizes other escapes which aren't approprite e.g octal \nnn - # it replaces \ with a single whitespace (trailing backslash) - #This means we shouldn't use 'subst' on the whole string, but instead substitute only the toml-specified escapes (\r \n \b \t \f \\ \" \uhhhh & \Uhhhhhhhh - #plus \e for \x1b? - - set buffer "" - set buffer2 "" ;#buffer for 2 hex characters following a \x - set buffer4 "" ;#buffer for 4 hex characters following a \u - set buffer8 "" ;#buffer for 8 hex characters following a \u - - set sLen [tcl::string::length $str] - - #we need to handle arbitrarily long sequences of backslashes. \\\\\ etc - set slash_active 0 - set unicode2_active 0 - set unicode4_active 0 - set unicode8_active 0 - - ::tomlish::log::debug "unescape_string. got len [string length str] str $str" - - #!todo - check for invalid data in the form of a raw carriage return (decimal 13) without following linefeed? - set i 0 - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $str [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $str $i] - #::tomlish::log::debug "unescape_string. got char $c" ;#too much? - - ##---------------------- - ##as we are 'unescaping' - should we really be testing here for existing values that should have been escaped? - ##The answer is probably no - keep this function to a single purpose - test elsewhere for raw controls. - ##this test looks incomplete anyway REVIEW - #scan $c %c n - #if {($n <= 31) && ($n != 9) && ($n != 10) && ($n != 13)} { - # #we don't expect unescaped unicode characters from 0000 to 001F - - # #*except* for raw tab (which is whitespace) and newlines - # error "unescape_string. Invalid data for a toml string. Unescaped control character (decimal $n) [::tomlish::utils::string_to_slashu $c]" - #} - ##---------------------- - - incr i ;#must incr here because we do'returns'inside the loop - if {$c eq "\\"} { - if {$slash_active} { - append buffer "\\" - set slash_active 0 - } elseif {$unicode2_active} { - error "unescape_string. unexpected case slash during unicode2 not yet handled" - } elseif {$unicode4_active} { - error "unescape_string. unexpected case slash during unicode4 not yet handled" - } elseif {$unicode8_active} { - error "unescape_string. unexpected case slash during unicode8 not yet handled" - } else { - # don't output anything (yet) - set slash_active 1 - } - } else { - if {$unicode2_active} { - if {[tcl::string::length $buffer2] < 2} { - append buffer2 $c - } - if {[tcl::string::length $buffer2] == 2} { - #we have a \xHH to test - set unicode2_active 0 - set result [tomlish::utils::hex_escape_info "\\x$buffer2"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode4_active} { - if {[tcl::string::length $buffer4] < 4} { - append buffer4 $c - } - if {[tcl::string::length $buffer4] == 4} { - #we have a \uHHHH to test - set unicode4_active 0 - set result [tomlish::utils::unicode_escape_info "\\u$buffer4"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode8_active} { - if {[tcl::string::length $buffer8] < 8} { - append buffer8 $c - } - if {[tcl::string::length $buffer8] == 8} { - #we have a \UHHHHHHHH to test - set unicode8_active 0 - set result [tomlish::utils::unicode_escape_info "\\U$buffer8"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$slash_active} { - set slash_active 0 - set ctest [tcl::string::map {{"} dq} $c] - switch -exact -- $ctest { - dq { - append buffer {"} - } - b - t - n - f - r { - append buffer [subst -nocommand -novariable "\\$c"] - } - e { - append buffer \x1b - } - x { - #introduced in 1.1.0 \xHH - set unicode2_active 1 - set buffer2 "" - } - u { - set unicode4_active 1 - set buffer4 "" - } - U { - set unicode8_active 1 - set buffer8 "" - } - default { - set slash_active 0 - #review - toml spec says all other escapes are reserved - #and if they are used TOML should produce an error. - #append buffer "\\$c" - set msg "Invalid escape sequence \\ followed by '$c'" - return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} $msg - } - } - } else { - append buffer $c - } - } - } - #puts stdout "EOF 4:$unicode4_active 8:$unicode8_active slash:$slash_active" - if {$unicode2_active} { - error "End of string reached before complete hex escape sequence \xHH" - } - if {$unicode4_active} { - error "End of string reached before complete unicode escape sequence \uHHHH" - } - if {$unicode8_active} { - error "End of string reached before complete unicode escape sequence \UHHHHHHHH" - } - if {$slash_active} { - append buffer "\\" - } - try { - encoding convertto utf-8 $buffer - } trap {} {emsg eopts} { - return -code error -errorcode {TOMLISH SYNTAX ENCODINGERROR} $emsg - } - return $buffer - } - - #This does not have to do with unicode normal forms - which it seems toml has decided against regarding use in keys (review/references?) - #This is meant for internal use regarding ensuring we match equivalent keys which may have just been specified with different string mechanisms, - #e.g squoted vs dquoted vs barekey. - proc normalize_key {rawkey} { - set c1 [tcl::string::index $rawkey 0] - set c2 [tcl::string::index $rawkey end] - if {($c1 eq "'") && ($c2 eq "'")} { - #single quoted segment. No escapes allowed within it. - set key [tcl::string::range $rawkey 1 end-1] - } elseif {($c1 eq "\"") && ($c2 eq "\"")} { - #double quoted segment. Unapply escapes. - # - set keydata [tcl::string::range $rawkey 1 end-1] ;#strip outer quotes only - #e.g key could have mix of \UXXXXXXXX escapes and unicode chars - #or mix of \t and literal tabs. - #unescape to convert all to literal versions for comparison - set key [::tomlish::utils::unescape_string $keydata] - #set key [subst -nocommands -novariables $keydata] ;#wrong. Todo - create a string escape substitution function. - } else { - set key $rawkey - } - return $key - } - - proc string_to_slashu {string} { - set rv {} - foreach c [split $string {}] { - scan $c %c cdec - if {$cdec > 65535} { - append rv {\U} [format %.8X $cdec] - } else { - append rv {\u} [format %.4X $cdec] - } - } - return $rv - } - - #'nonprintable' is conservative here because some systems (e.g windows console) are very limited in what they can display. - #This is used for display purposes only (error msgs) - proc nonprintable_to_slashu {s} { - set res "" - foreach i [split $s ""] { - scan $i %c cdec - - set printable 0 - if {($cdec>31) && ($cdec<127)} { - set printable 1 - } - if {$printable} { - append res $i - } else { - if {$cdec > 65535} { - append res \\U[format %.8X $cdec] - } else { - append res \\u[format %.4X $cdec] - } - } - } - set res - } ;# initial version from tcl wiki RS - - proc rawstring_to_jsonstring {s} { - #like nonprintable_to_slashu - # - also escape every dquote - # - escape newlines - set res "" - foreach i [split $s ""] { - scan $i %c cdec - switch -- $cdec { - 34 { - #double quote - append res \\\" - } - 13 { - #carriage return - append res \\r - } - 8 { - append res \\b - } - 9 { - append res \\t - } - 10 { - #linefeed - append res \\n - } - 92 { - append res \\\\ - } - default { - set printable 0 - if {($cdec>31) && ($cdec<127)} { - set printable 1 - } - if {$printable} { - append res $i - } else { - if {$cdec > 65535} { - #append res $i - #append res \\U[format %.8X $cdec] ;#wrong - #append res "\\U{[format %.8x $cdec]}" ;#some variation of json? - package require punk::cesu - #e.g \U0001f610 emoticon face - #surrogate pair: \uD83D\uDE10 - set surrogatepair [punk::cesu::to_surrogatestring -format escape $i] - append res $surrogatepair - } else { - append res \\u[format %.4X $cdec] - } - } - } - } - } - set res - - } - - #check if str is valid for use as a toml bare key - #Early toml versions only allowed letters + underscore + dash - proc is_basic_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } else { - set matches [regexp -all {[a-zA-Z0-9\_\-]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters match the regexp - return 1 - } else { - return 0 - } - } - } - - #from toml.abnf in github.com/toml-lang/toml - #unquoted-key = 1*unquoted-key-char - #unquoted-key-char = ALPHA / DIGIT / %x2D / %x5F ; a-z A-Z 0-9 - _ - #unquoted-key-char =/ %xB2 / %xB3 / %xB9 / %xBC-BE ; superscript digits, fractions - #unquoted-key-char =/ %xC0-D6 / %xD8-F6 / %xF8-37D ; non-symbol chars in Latin block - #unquoted-key-char =/ %x37F-1FFF ; exclude GREEK QUESTION MARK, which is basically a semi-colon - #unquoted-key-char =/ %x200C-200D / %x203F-2040 ; from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - #unquoted-key-char =/ %x2070-218F / %x2460-24FF ; include super-/subscripts, letterlike/numberlike forms, enclosed alphanumerics - #unquoted-key-char =/ %x2C00-2FEF / %x3001-D7FF ; skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - #unquoted-key-char =/ %x2070-21FF / %x2300-24FF ; skip math operators - #unquoted-key-char =/ %x25A0-268B / %x2690-2757 ; skip box drawing, block elements, and some yin-yang symbols - #unquoted-key-char =/ %x2762-2767 / %x2776-27E5 ; skip some Dingbat punctuation - #unquoted-key-char =/ %x2801-297F ; skip some math brackets and arrows, and braille blank - #unquoted-key-char =/ %x2B00-2FFF / %x3001-D7FF ; skip various math operators and symbols, and ideographic space - #unquoted-key-char =/ %xF900-FDCF / %xFDF0-FFFD ; skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - #unquoted-key-char =/ %x10000-EFFFF ; all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - variable re_barekey - set ranges [list] - lappend ranges {a-zA-Z0-9\_\-} - lappend ranges {\u00B2} {\u00B3} {\u00B9} {\u00BC-\u00BE} ;# superscript digits, fractions - lappend ranges {\u00C0-\u00D6} {\u00D8-\u00F6} {\u00F8-\u037D} ;# non-symbol chars in Latin block - lappend ranges {\u037f-\u1FFF} ;# exclude GREEK QUESTION MARK, which is basically a semi-colon - lappend ranges {\u200C-\u200D} {\u203F-\u2040} ;# from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - lappend ranges {\u2070-\u218f} {\u2460-\u24FF} ;# include super-subscripts, letterlike/numberlike forms, enclosed alphanumerics - lappend ranges {\u2C00-\u2FEF} {\u3001-\uD7FF} ;# skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - lappend ranges {\u2070-\u21FF} {\u2300-\u24FF} ;# skip math operators - lappend ranges {\u25A0-\u268B} {\u2690-\u2757} ;# skip box drawing, block elements, and some yin-yang symbols - lappend ranges {\u2762-\u2767} {\u2776-\u27E5} ;# skip some Dingbat punctuation - lappend ranges {\u2801-\u297F} ;# skip some math brackets and arrows, and braille blank - lappend ranges {\u2B00-\u2FFF} {\u3001-\uD7FF} ;# skip various math operators and symbols, and ideographic space - lappend ranges {\uF900-\uFDCF} {\uFDF0-\uFFFD} ;# skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - lappend ranges {\U10000-\UEFFFF} ;# all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - set re_barekey {^[} - foreach r $ranges { - append re_barekey $r - } - append re_barekey {]+$} - - proc is_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } - variable re_barekey - return [regexp $re_barekey $str] - } - - #test only that the characters in str are valid for the toml specified type 'integer'. - proc int_validchars1 {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_\-\+]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - #add support for hex,octal,binary 0x.. 0o.. 0b... - proc int_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - proc is_int {str} { - set matches [regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] ;#0b101 etc covered by a-f - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - - # --------------------------------------- - #check for leading zeroes in non 0x 0b 0o - #first strip any +, - or _ (just for this test) - #(but still allowing 0 -0 +0) - set check [tcl::string::map {+ "" - "" _ ""} $str] - if {([tcl::string::length $check] > 1) && ([tcl::string::index $check 0] eq "0") && ([tcl::string::index $check 1] ni {o x b})} { - return 0 - } - # --------------------------------------- - - #check +,- only occur in the first position. (excludes also +++1 etc) - if {[tcl::string::last - $str] > 0} { - return 0 - } - if {[tcl::string::last + $str] > 0} { - return 0 - } - - #------------------------------------------- - #unclear if a 'digit' includes the type specifiers x b o - #we assume the 0x 0b 0o are NOT counted as digits - as underscores here would seem - #to be likely to cause interop issues with other systems - #(e.g tcl allows 0b1_1 but not 0b_11) - #Most of this structure would be unnecessary if we could rely on string::is::integer understanding underscores (9+?) - #we still need to support earlier Tcl for now though. - - #first rule out any case with more than one underscore in a row - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_0xFF - if {[string index $utest 0] eq "_"} { - return 0 - } - if {[string range $utest 0 1] in {0x 0b 0o}} { - set testnum [string range $utest 2 end] - #spec says *non-negative* integers may *also* be expressed in hex, octal or binary - #and also explicitly states + not allowed - #presumed to mean negative not allowed. - if {[string index $str 0] in {- +}} { - return 0 - } - } else { - set testnum $utest - #exclude also things like 0_x 0___b that snuck past our prefix test - if {![string is digit -strict [string map {_ ""} $testnum]]} { - return 0 - } - #assert - only digits and underscores in testnum - #still may have underscores at each end - } - #assert testnum is now the 'digits' portion of a , 0x 0b 0o number - #(+ and - already stripped) - #It may still have chars unsuitable for its type - which will be caught by the string::is::integer test below - if {[string length $testnum] != [string length [string trim $testnum _]]} { - #had non-inner underscores in 'digit' part - return 0 - } - #assert str only has solo inner underscores (if any) between 'digits' - #------------------------------------------- - - set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores - #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![::tomlish::utils::string_is_integer -strict $numeric_value]} { - return 0 - } - - - - #!todo - check bounds only based on some config value - #even though Tcl can handle bignums, we won't accept anything outside of toml 1.0 minimum requirements by default (for now) - #presumably very large numbers would have to be supplied in a toml file as strings. - #Review - toml 1.0 only says that it must handle up to 2^63 - not that this is a max - #some question around implementations allowed to use lower values such as 2^31 on some systems? - if {$::tomlish::max_int ne "" && $numeric_value > $::tomlish::max_int} { - return 0 - } - if {$::tomlish::min_int ne "" && $numeric_value < $::tomlish::min_int} { - return 0 - } - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'float'. - proc float_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[eE0-9\_\-\+\.]} $str] == $numchars} { - return 1 - } else { - #only allow lower case for these special values - as per Toml 1.0 spec - if {$str ni {inf +inf -inf nan +nan -nan}} { - return 0 - } else { - return 1 - } - } - } - - #note - Tcl's string is double will return true also for the subset of float values which are integers - #This function is to determine whether it matches the Toml float concept - so requires a . or e or E - proc is_float {str} { - #vip greenlight known literals, don't test for case variations - as Toml doesn't allow (whereas Tcl allows Inf NaN etc) - if {$str in {inf +inf -inf nan +nan -nan}} { - return 1 - } - #doorcheck the basics for floatiness vs members of that rival gang - ints - if {![regexp {[.eE]} $str]} { - #could be an integer - which isn't specifically a float for Toml purposes. - return 0 - } - - - #patdown for any contraband chars - set matches [regexp -all {[eE0-9\_\-\+\.]} $str] - if {[tcl::string::length $str] != $matches} { - return 0 - } - - #all characters in legal range - - #A leading zero is ok, but we should disallow multiple leading zeroes (same rules as toml ints) - - #Early Toml spec also disallowed leading zeros in the exponent part(?) - #... this seems less interoperable anyway (some libraries generate leading zeroes in exponents) - #we allow leading zeros in exponents here. - - #Check for leading zeros in main part - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - set r {([0-9])*} - regexp $r $check intpart ;#intpart holds all numerals before the first .,e or E - #leading zero only if exactly one zero - if {$intpart ne "0" && [string match 0* $intpart]} { - return 0 - } - - #for floats, +,- may occur in multiple places - #e.g -2E-22 +3e34 - #!todo - check bounds ? - - #----------------------------------------- - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_" || [string index $str end] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_ - if {[string index $utest 0] eq "_"} { - return 0 - } - #----------------------------------------- - - #decimal point, if used must be surrounded by at least one digit on each side - #e.g 3.e+20 also illegal - set dposn [string first . $str] - if {$dposn > -1 } { - set d3 [string range $str $dposn-1 $dposn+1] - if {![::tomlish::utils::string_is_integer -strict [string index $d3 0]] || ![::tomlish::utils::string_is_integer -strict [string index $d3 2]]} { - return 0 - } - } - #we've already eliminated leading/trailing underscores - #now ensure each inner underscore is surrounded by digits - if {[regexp {_[^0-9]|[^0-9]_} $str]} { - return 0 - } - - #strip underscores for tcl double check so we can support < tcl 9 versions which didn't allow underscores - set check [tcl::string::map {_ ""} $str] - #string is double accepts inf nan +NaN etc. - if {![tcl::string::is double $check]} { - return 0 - } - - #All good - seems to be a toml-approved float and not an int. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'datetime'. - proc datetime_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[zZtT0-9\-\+\.:]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - - #allow only hh:mm:ss or hh:mm (no subseconds) - #return 2 when missing seconds - proc _is_hms_or_hm_time {val} { - set numchars [tcl::string::length $val] - if {[regexp -all {[0-9:]} $val] != $numchars} { - return 0 - } - #assert now digits and colons only - set hms_cparts [split $val :] - #2 or 3 parts only are valid - check contents of each part - if {[llength $hms_cparts] == 2} { - lassign $hms_cparts hr min - if {[string length $hr] != 2 || [string length $min] != 2} { - return 0 - } - if {$hr > 23 || $min > 59} { - return 0 - } - return 2 ;#missing seconds indicator (can still be used as boolean for true in tcl if we don't care whether hh::mm::ss or hh:mm - } elseif {[llength $hms_cparts] == 3} { - lassign $hms_cparts hr min sec - if {[string length $hr] != 2 || [string length $min] != 2 || [string length $sec] !=2} { - return 0 - } - #possible for sec to be 60 - leap second RFC 3339 - if {$hr > 23 || $min > 59 || $sec > 60} { - return 0 - } - return 1 - } else { - return 0 - } - } - proc is_timepart {str} { - #validate the part after the T (or space) - #we receive only that trailing part here. - - #odt1 = 1979-05-27T07:32:00Z - #odt2 = 1979-05-27T00:32:00-07:00 - #odt3 = 1979-05-27T00:32:00.5-07:00 - #odt4 = 1979-05-27T00:32:00.999999-07:00 - - set numchars [tcl::string::length $str] - #timepart can have negative or positive offsets so - and + must be accepted - if {[regexp -all {[zZt0-9\-\+\.:]} $str] == $numchars} { - #todo - #basic check that we have leading 2dig hr and 2dig min separated by colon - if {![regexp {^[0-9]{2}:[0-9]{2}$|^[0-9]{2}:[0-9]{2}[^0-9]{1}.*$} $str]} { - #nn:nn or nn:nnX.* where X is non digit - return 0 - } - set dotparts [split $str .] - if {[llength $dotparts] ni {1 2}} { - return 0 - } - if {[llength $dotparts] == 2} { - lassign $dotparts hms tail - if {[_is_hms_or_hm_time $hms] == 2} { - #If we have a dot - assume hh::mm::ss required - #toml spec is unclear on this but hh:mm. doesn't seem sensible - REVIEW - return 0 - } - #validate tail - which might have +- offset - if {[string index $tail end] ni {z Z}} { - #from hh:mm:??. - #check for +/- something - if {[regexp {(.*)[+-](.*)} $tail _match fraction offset]} { - if {![string is digit -strict $fraction]} { - return 0 - } - if {[_is_hms_or_hm_time $offset] != 2} { - #RFC3339 indicates offset can be specified as hh:mm or Z - not hh:mm:ss - return 0 - } - } else { - #tail has no +/-, only valid if fraction digits - #toml-test invalid/datetime/second-trailing-dot - if {![string is digit -strict $tail]} { - return 0 - } - } - } else { - set tail [string range $tail 0 end-1] - #expect tail nnnn (from hh:mm::ss.nnnnZ) - #had a dot and a zZ - if {![string is digit -strict $tail]} { - return 0 - } - } - - } else { - #no dot (fraction of second) - if {[regexp {(.*)[+-](.*)} $str _match hms offset]} { - #validate offset - #offset of +Z or -Z not valid - if {[_is_hms_or_hm_time $offset] != 2} { - #offset is not of required form hh:mm - return 0 - } - } else { - set hms $str - set offset "" - #trim a *single* z or Z off hms if present - multiple should error later - if {[string index $hms end] in {z Z}} { - set hms [string range $hms 0 end-1] - } - } - } - #hms is allowed in toml to be hh:mm:ss or hh:mm - #validate we have hh:mm:ss or hh:mm - exactly 2 digits each - if {![_is_hms_or_hm_time $hms]} { - return 0 - } - - return 1 - } else { - return 0 - } - } - - proc is_date-local {str} { - set matches [regexp -all {[0-9\-]} $str] - if {[tcl::string::length $str] != $matches} { - return 0 - } - #seems to require yyyy-mm-dd (e.g not allowing just yyyy-mm) - if {![regexp {^([0-9]{4})-([0-9]{2})-([0-9]{2})$} $str _match y m d]} { - return 0 - } - if {$m > 12 || $m == 0} { - return 0 - } - switch -- [expr {$m}] { - 1 - 3 - 5 - 7 - 8 - 10 - 12 { - if {$d > 31 || $d == 0} { - return 0 - } - } - 2 { - if {$d > 29 || $d == 0} { - return 0 - } - if {$d == 29} { - #leapyear check - if {[catch {clock scan $str -format %Y-%m-%d} errM]} { - return 0 - } - } - } - 4 - 6 - 9 - 11 { - if {$d > 30 || $d == 0} { - return 0 - } - } - } - return 1 - } - proc is_time-local {str} { - #time of day without any relation to a specific day or any offset or timezone - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\.:]} $str] == $numchars} { - #todo - if {![regexp {^[0-9]{2}:[0-9]{2}$|^[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]+){0,1}$} $str]} { - #hh:mm or hh:mm:ss or hh:mm::ss.nnn - return 0 - } - set dotparts [split $str .] - if {[llength $dotparts] ni {1 2}} { - return 0 - } - if {[llength $dotparts] == 2} { - lassign $dotparts hms _tail - #validate tail - just fractional seconds - regex has confirmed at least one digit and only digits - #nothing todo? max length? - } else { - #no fractional seconds - set hms $str - } - if {![_is_hms_or_hm_time $hms]} { - return 0 - } - return 1 - } else { - return 0 - } - } - proc is_datetime-local {str} { - set norm [string map {" " T} $str] - lassign [split $norm T] dp tp - if {$dp eq "" || $tp eq ""} {return 0} - if {![is_date-local $dp]} {return 0} - if {![is_timepart $tp]} {return 0} - if {![is_time-local $tp]} {return 0} - return 1 - } - proc is_datetime {str} { - set norm [string map {" " T} $str] - lassign [split $norm T] dp tp - if {$dp eq "" || $tp eq ""} {return 0} - if {![is_date-local $dp]} {return 0} - if {![is_timepart $tp]} {return 0} - if {[is_time-local $tp]} {return 0} - return 1 - } - #review - proc is_date_or_time_or_datetime {str} { - #Essentially RFC3339 formatted date-time - but: - #1) allowing seconds to be omitted (:00 assumed) - #2) T may be replaced with a single space character TODO - parser support for space in datetime! - # (RFC 3339 allows space instead of T also - but doesn't specify it *must* be a single space) - - #toml-lint @2025-04 doesn't accept t for T or z for Z - but RFC3339 does - #toml spec doesn't clarify - we will accept - - #e.g 1979-05-27 - #e.g 1979-05-27T00:32:00Z - #e.g 1979-05-27 00:32:00-07:00 - #e.g 1979-05-27 00:32:00+10:00 - #e.g 1979-05-27 00:32:00.999999-07:00 - - #review - #minimal datetimes? - # 2024 not ok - 2024T not accepted by tomlint why? - # 02:00 ok - # 02:00:00.5 ok - # 1:00 - not ok - RFC3339 requires 2-digit hr,min,sec - - #toml-lint.com accepts 2025-01 - - if {[string length $str] < 5} { - return 0 - } - - set matches [regexp -all {[zZtT0-9\ \-\+\.:]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - if {[regexp -all {\ } $str] > 1} { - #only a single space is allowed. - return 0 - } - #If we get a space - it is only valid as a convience to represent the T separator - #we can normalize by converting to T here before more tests - set str [string map {" " T t T} $str] - #a further sanity check on T - if {[regexp -all {T} $str] > 1} { - return 0 - } - - #!todo - use full RFC 3339 parser? - #!todo - what if the value is 'time only'? - - if {[string first T $str] > -1} { - lassign [split $str T] datepart timepart - if {![is_date-local $datepart]} { - return 0 - } - if {![is_timepart $timepart]} { - return 0 - } - } else { - #either a datepart or a localtime - #spec: "If you include only the time portion of an RFC 3339 formatted date-time, it will represent that time of day - # without any relation to a specific day or any offset or timezone." - if {!([is_date-local $str] || [is_time-local $str])} { - return 0 - } - } - - - #Tcl's free-form clock scan (no -format option) is deprecated - # - #if {[catch {clock scan $datepart} err]} { - # puts stderr "tcl clock scan failed err:'$err'" - # return 0 - #} - - } else { - return 0 - } - return 1 - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::utils ---}] -} - -namespace eval tomlish::parse { - #*** !doctools - #[subsection {Namespace tomlish::parse}] - #[para] - #[list_begin definitions] - - #This is a somewhat curly mix of a statemachine and toml-nesting-stack littered with special cases. - #The code is a pig's-nest - but it should be noted that for example trailing single double quotes in multiline strings are perhaps not so trivial to parse using more standard methods either: - # - e.g some kind of backtracking required if using an ABNF parser? - #I don't know the precise technical name for this sort of parser; probably something like "Dog's Breakfast" - #More seriously, we don't have distinct lex/parse steps - so it is basically a 'fused lexer' or 'scannerless parser' - - #It is also desirable for this system to be useful in 'interactive' use. review - would a separate lexer make this easier or harder? - - #A possible alternative more structured approach might be to use a PEG (Parsing Expression Grammar) - - - variable is_parsing 0 ;#whether we are in the middle of parsing tomlish text - - variable state - # states: - # table-space, itable-space, array-space - # array-value-expected,keyval-value-expected,itable-keyval-value-expected, keyval-syntax, - # dquoted-key, squoted-key - # string-state, literal-state, multistring... - # - # notes: - # only the -space states are also 'spaces' ie a container which is pushed/popped on the spacestack - - # - # xxx_value-expected - we also allow for leading whitespace in this state, but once a value is returned we jump to a state based on the containing space. e.g keyval-tail or array-syntax - # - #stateMatrix defines for each state, actions to take for each possible token. - #single-element actions are the name of the next state into which to transition, or a 'POPSPACE' instruction to pop a level off the spacestack and add the data to the parent container. - #dual-element actions are a push instruction and the name of the space to push on the stack. - # - PUSHSPACE is a simple push onto the spacestack, zeropoppushspace also pushes, but will first do a pop *if* the current space level is greater than zero (ie if only if not already in root table-space) - - # -- --- --- --- --- --- - #token/state naming guide - # -- --- --- --- --- --- - #tokens : underscore separated or bare name e.g newline, start_quote, start_squote - #private tokens: always have a leading underscore (These are private 'temporary state' tokens that are never returned as actual tokens e.g _start_squote_sequence - #states : always contain at least one dash e.g err-state, table-space - #instructions - # -- --- --- --- --- --- - - - #stateMatrix dict of elements mapping current state to next state based on returned tokens - # current-state {token-encountered next-state ... } - # where next-state can be a 1 or 2 element list. - #If 2 element - the first item is an instruction (ucase) - #If 1 element - it is either a lowercase dashed state name or an ucase instruction - #e.g {PUSHSPACE } or POPSPACE or SAMESPACE - - - #SAMESPACE - got to same space as parent without popping a level, but has it's own autotransition lookup - strange concept - review usecases - - variable stateMatrix - set stateMatrix [dict create] - #--------------------------------------------------------- - #WARNING - #The stateMatrix implementation here is currently messy. - #The code is a mixture of declarative via the stateMatrix and imperative via switch statements during PUSH/POP/SAMESPACE transitions. - #This means the state behaviour has to be reasoned about by looking at both in conjuction. - #--------------------------------------------------------- - - #xxx-space vs xxx-syntax inadequately documented - TODO - - #review - out of date? - # --------------------------------------------------------------------------------------------------------------# - # incomplete example of some state starting at table-space - # --------------------------------------------------------------------------------------------------------------# - # ( = -> keyval-value-expected) - # keyval-syntax (popped -> keyval-space -> keyval-tail) (autotransition on pop) - # keyval-space (autotransition on push ^) - # table-space (barekey^) (startdquote -> dquoted-key ^) - # --------------------------------------------------------------------------------------------------------------# - - dict set stateMatrix\ - table-space { - bom "table-space"\ - whitespace "table-space"\ - newline "table-space"\ - barekey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - squotedkey {PUSHSPACE "keyval-space" state "keyval-syntax" note ""}\ - dquotedkey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - XXXsingle_dquote "quoted-key"\ - XXXsingle_squote "squoted-key"\ - comment "table-space"\ - starttablename "tablename-state"\ - starttablearrayname "tablearrayname-state"\ - enddquote "err-state"\ - endsquote "err-state"\ - comma "err-state"\ - eof "end-state"\ - equal "err-state"\ - cr "err-lonecr"\ - } - - - - dict set stateMatrix\ - keyval-space {\ - whitespace "keyval-syntax"\ - equal "keyval-value-expected"\ - } - - # ' = ' portion of keyval - dict set stateMatrix\ - keyval-syntax {\ - whitespace "keyval-syntax"\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal "keyval-value-expected"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - #### - dict set stateMatrix\ - keyval-value-expected {\ - whitespace "keyval-value-expected"\ - untyped_value {TOSTATE "keyval-untyped-sequence" note "possible datetime datepart"}\ - literal {TOSTATE "keyval-tail" note "required for empty literal at EOF"}\ - string {TOSTATE "keyval-tail" note "required for empty string at EOF"}\ - single_dquote {TOSTATE "string-state" returnstate keyval-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - single_squote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - } - #double_squote {TOSTATE "keyval-tail" note "empty literal received when double squote occurs"} - - #untyped_value sequences without intervening comma are allowed for datepart timepart - #we will produce tomlish with missing SEPS and to_dict must validate whether 2 adjacent barekeys are valid - dict set stateMatrix\ - keyval-untyped-sequence {\ - whitespace "keyval-untyped-sequence"\ - untyped_value {TOSTATE "keyval-tail"}\ - literal {TOSTATE "keyval-tail" note "required for empty literal at EOF"}\ - string {TOSTATE "keyval-tail" note "required for empty string at EOF"}\ - single_dquote {TOSTATE "string-state" returnstate keyval-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - single_squote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - #2025 - no leading-squote-space - only trailing-squote-space. - - dict set stateMatrix\ - keyval-tail {\ - whitespace "keyval-tail"\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - - #itable-space/ curly-syntax : itables - # x={y=1,} - dict set stateMatrix\ - itable-space {\ - whitespace "itable-space"\ - newline "itable-space"\ - barekey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - squotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - dquotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - endinlinetable "POPSPACE"\ - comma "err-state"\ - comment "itable-space"\ - eof "err-state"\ - } - #we don't get single_squote etc here - instead we get the resulting squotedkey token - - - # ??? review - something like this - # - # x={y =1,} - dict set stateMatrix\ - itable-keyval-syntax {\ - whitespace {TOSTATE "itable-keyval-syntax"}\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal {TOSTATE "itable-keyval-value-expected"}\ - newline "err-state"\ - eof "err-state"\ - } - - # x={y=1} - dict set stateMatrix\ - itable-keyval-space {\ - whitespace "itable-keyval-syntax"\ - equal {TOSTATE "itable-keyval-value-expected" note "required"}\ - } - - dict set stateMatrix\ - itable-keyval-value-expected {\ - whitespace "itable-keyval-value-expected"\ - untyped_value {TOSTATE "itable-val-tail" note ""}\ - single_dquote {TOSTATE "string-state" returnstate itable-val-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate itable-val-tail}\ - single_squote {TOSTATE "literal-state" returnstate itable-val-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate itable-val-tail}\ - startinlinetable {PUSHSPACE "itable-space" returnstate itable-val-tail}\ - startarray {PUSHSPACE "array-space" returnstate itable-val-tail}\ - } - #double_squote not currently generated by _start_squote_sequence - '' processed as single_squote to literal-state just like 'xxx' - # review - # double_squote {TOSTATE "itable-val-tail" note "empty literal received when double squote occurs"} - - - - # x={y=1,z="x"} - #POPSPACE is transition from itable-keyval-space to parent itable-space - dict set stateMatrix\ - itable-val-tail {\ - whitespace "itable-val-tail"\ - endinlinetable "POPSPACE"\ - comma "POPSPACE"\ - newline {TOSTATE "itable-val-tail" note "itable-space ??"}\ - comment "itable-val-tail"\ - eof "err-state"\ - } - # XXXnewline "POPSPACE" - # We shouldn't popspace on newline - as if there was no comma we need to stay in itable-val-tail - # This means the newline and subsequent whitespace, comments etc become part of the preceeding dottedkey record - #e.g - # x = { - # j=1 - # #comment within dottedkey j record - # , # comment unattached - # #comment unattached - # k=2 , #comment unattached - # l=3 #comment within l record - # , m=4 - # #comment associated with m record - # - # #still associated with m record - # } - ## - This doesn't quite correspond to what a user might expect - but seems like a consistent mechanism. - #The awkwardness is because there is no way to put in a comment that doesn't consume a trailing comma - #so we cant do: j= 1 #comment for j1 , - # and have the trailing comma recognised. - # - # To associate: j= 1, #comment for j1 - # we would need some extra processing . (not popping until next key ? extra state itable-sep-tail?) REVIEW - worth doing? - # - # The same issue occurs with multiline arrays. The most natural assumption is that a comment on same line after a comma - # is 'associated' with the previous entry. - # - # These comment issues are independent of the data dictionary being generated for conversion to json etc - as the comments don't carry through anyway, - # but are a potential oddity for manipulating the intermediate tomlish structure whilst attempting to preserve 'associated' comments - # (e.g reordering records within an itable) - #The user's intention for 'associated' isn't always clear and the specs don't really guide on this. - - - #dottedkey-space is not (currently) used within [tablename] or [[tablearrayname]] - #it is for keyval ie x.y.z = value - - #this is the state after dot - #we are expecting a complete key token or whitespace - #(initial entry to the space is by one of the keys - which will immediately go to dottedkey-space-tail) - dict set stateMatrix\ - dottedkey-space {\ - whitespace "dottedkey-space"\ - dotsep "err-state"\ - barekey "dottedkey-space-tail"\ - squotedkey "dottedkey-space-tail"\ - dquotedkey "dottedkey-space-tail"\ - newline "err-state"\ - comma "err-state"\ - comment "err-state"\ - equal "err-state"\ - } - - #dottedkeyend "POPSPACE" - #equal "POPSPACE"\ - - - #jmn 2025 - #we have 1 or more dottedkeys so far - need dotsep to add more, whitespace to maintain, equal to pop - dict set stateMatrix\ - dottedkey-space-tail {\ - whitespace "dottedkey-space-tail" - dotsep "dottedkey-space" - equal "POPSPACE"\ - eof "err-state"\ - newline "err-state"\ - } - - #-------------------------------------------------------------------------- - #scratch area - #from_toml {x=1} - # barekey tok - # table-space PUSHSPACE keyval-space state keyval-syntax - # - - - #-------------------------------------------------------------------------- - - - #REVIEW - #toml spec looks like heading towards allowing newlines within inline tables - #https://github.com/toml-lang/toml/issues/781 - - #2025 - multiline itables appear to be valid for 1.1 - which we are targeting. - #https://github.com/toml-lang/toml/blob/main/toml.md#inline-table - - #JMN2025 - #review comment "err-state" vs comment "itable-space" - see if TOML 1.1 comes out and allows comments in multiline ITABLES - #We currently allow multiline ITABLES (also with comments) in the tokenizer. - #if we want to disallow as per TOML 1.0 - we should do so when attempting to get structure? - - - #JMN REVIEW - #dict set stateMatrix\ - # array-space {\ - # whitespace "array-space"\ - # newline "array-space"\ - # untyped_value "SAMESPACE"\ - # startarray {PUSHSPACE "array-space"}\ - # endarray "POPSPACE"\ - # startinlinetable {PUSHSPACE itable-space}\ - # single_dquote "string-state"\ - # single_squote "literal-state"\ - # triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax note "seems ok 2024"}\ - # comma "array-space"\ - # comment "array-space"\ - # eof "err-state-array-space-got-eof"\ - # } - - ## array-space ## - set aspace [dict create] - dict set aspace whitespace "array-space" - dict set aspace newline "array-space" - #dict set aspace untyped_value "SAMESPACE" - dict set aspace untyped_value "array-syntax" - dict set aspace startarray {PUSHSPACE "array-space"} - dict set aspace endarray "POPSPACE" - dict set aspace single_dquote {TOSTATE "string-state" returnstate array-syntax} - dict set aspace triple_dquote {PUSHSPACE "multistring-space" returnstate array-syntax} - dict set aspace single_squote {TOSTATE "literal-state" returnstate array-syntax} - dict set aspace triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax} - dict set aspace startinlinetable {PUSHSPACE itable-space} - #dict set aspace comma "array-space" - dict set aspace comment "array-space" - dict set aspace eof "err-state-array-space-got-eof" - dict set stateMatrix array-space $aspace - - #when we pop from an inner array we get to array-syntax - #e.g {x=[[]] ??? - set tarntail [dict create] - dict set tarntail whitespace "err-state" ;#"tablearrayname-tail" ;#spec doesn't allow whitespace here - dict set tarntail newline "err-state" - dict set tarntail comment "err-state" - dict set tarntail eof "err-state" - dict set tarntail endtablename "tablearray-tail" - dict set stateMatrix tablearrayname-tail $tarntail - - #review - somewhat counterintuitive...? - # [(starttablearrayname) (endtablearrayname] - # [(starttablename) (endtablename)] - - # [[xxx]] ??? - set tartail [dict create] - dict set tartail whitespace "tablearray-tail" - dict set tartail newline "table-space" - dict set tartail comment "tablearray-tail" - dict set tartail eof "end-state" - dict set stateMatrix tablearray-tail $tartail - - - - - - - dict set stateMatrix\ - end-state {} - - set knowntokens [list] - set knownstates [list] - dict for {state transitions} $stateMatrix { - if {$state ni $knownstates} {lappend knownstates $state} - dict for {tok instructions} $transitions { - if {$tok ni $knowntokens} {lappend knowntokens $tok} - } - } - dict set stateMatrix nostate {} - foreach tok $knowntokens { - dict set stateMatrix nostate $tok "err-nostate-received-token-$tok" - } - - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #purpose - debugging? remove? - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #build a list of 'push triggers' from the stateMatrix - # ie tokens which can push a new space onto spacestack - set push_trigger_tokens [list] - tcl::dict::for {s transitions} $stateMatrix { - tcl::dict::for {token transition_to} $transitions { - set instruction [lindex $transition_to 0] - switch -exact -- $instruction { - PUSHSPACE - zeropoppushspace { - if {$token ni $push_trigger_tokens} { - lappend push_trigger_tokens $token - } - } - } - } - } - ::tomlish::log::debug "push_trigger_tokens: $push_trigger_tokens" - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - - - - #This seems hacky... (deprecate in favour of explicit arguments to the instructions in stateMatrix?) - #spacePopTransitions, spacePushTransitions, spaceSameTransitions below for auto state redirections on POPSPACE,PUSHSPACE,SAMESPACE - - #mainly for the -space states: - #redirect to another state $c based on a state transition from $whatever to $b - # e.g "string {array-space array-syntax}" means when transitioning from string to array-space, jump to array-syntax instead. - #this is useful as we often don't know state $b. e.g when it is decided by 'POPSPACE' - - #use dict set to add values so we can easily add/remove/comment lines - - #Push to, next - #default first states when we push to these spaces - variable spacePushTransitions [dict create] - dict set spacePushTransitions keyval-space keyval-syntax - dict set spacePushTransitions itable-keyval-space itable-keyval-syntax - dict set spacePushTransitions array-space array-space - dict set spacePushTransitions table-space tablename-state - #dict set spacePushTransitions #itable-space itable-space - - #Pop to, next - variable spacePopTransitions [dict create] - dict set spacePopTransitions array-space array-syntax - - - #itable-keyval-space itable-val-tail - #review - #we pop to keyval-space from dottedkey-space or from keyval-value-expected? we don't always want to go to keyval-tail - #leave it out and make the POPSPACE caller explicitly specify it - #keyval-space keyval-tail - - variable spaceSameTransitions [dict create] - #JMN test - #dict set spaceSameTransitions array-space array-syntax - - #itable-keyval-space itable-val-tail - - - variable state_list ;#reset every tomlish::decode::toml - - namespace export tomlish toml - namespace ensemble create - - #goNextState has various side-effects e.g pushes and pops spacestack - #REVIEW - setting nest and v elements here is ugly - #todo - make neater, more single-purpose? - proc goNextState {tokentype tok currentstate} { - variable state - variable nest - variable v - - set prevstate $currentstate - - - variable spacePopTransitions - variable spacePushTransitions - variable spaceSameTransitions - - variable last_space_action "none" - variable last_space_type "none" - variable state_list - - set result "" - set starttok "" - - if {[dict exists $::tomlish::parse::stateMatrix $currentstate $tokentype]} { - set transition_to [dict get $::tomlish::parse::stateMatrix $currentstate $tokentype] - ::tomlish::log::debug "--->> goNextState tokentype:$tokentype tok:$tok currentstate:$currentstate : transition_to = $transition_to" - switch -exact -- [lindex $transition_to 0] { - POPSPACE { - set popfromspace_info [spacestack peek] - set popfromspace_state [dict get $popfromspace_info state] - spacestack pop - set parent_info [spacestack peek] - set type [dict get $parent_info type] - set parentspace [dict get $parent_info state] - - set last_space_action "pop" - set last_space_type $type - - if {[dict exists $parent_info returnstate]} { - set next [dict get $parent_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected to stored returnstate $next <<---" - } else { - ### - #review - do away with spacePopTransitions - which although useful to provide a default.. - # - involve error-prone configurations distant to the main state transition configuration in stateMatrix - if {[dict exists $::tomlish::parse::spacePopTransitions $parentspace]} { - set next [dict get $::tomlish::parse::spacePopTransitions $parentspace] - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected state to $next (spacePopTransitions)<<---" - } else { - set next $parentspace - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace<<---" - } - } - set result $next - } - SAMESPACE { - set currentspace_info [spacestack peek] - ::tomlish::log::debug "--->> SAMESPACE got current space entry: $currentspace_info <<<<<" - set type [dict get $currentspace_info type] - set currentspace [dict get $currentspace_info state] - - if {[dict exists $currentspace_info returnstate]} { - set next [dict get $currentspace_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected to stored returnstate $next" - } else { - if {[dict exists $::tomlish::parse::spaceSameTransitions $currentspace]} { - set next [dict get $::tomlish::parse::spaceSameTransitions $currentspace] - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected state to $next (spaceSameTransitions)" - } else { - set next $currentspace - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace" - } - } - set result $next - } - zeropoppushspace { - if {$nest > 0} { - #pop back down to the root level (table-space) - spacestack pop - set parentinfo [spacestack peek] - set type [dict get $parentinfo type] - set target [dict get $parentinfo state] - - set last_space_action "pop" - set last_space_type $type - - #----- - #standard pop - set parentlevel [expr {$nest -1}] - lappend v($parentlevel) [set v($nest)] - incr nest -1 - #----- - } - #re-entrancy - - #set next [list PUSHSPACE [lindex $transition_to 1]] - set nexttokentype ${tokentype}2 ;#fake token type e.g tablename2 or tablearrayname2 - ::tomlish::log::debug "--->> zeropoppushspace goNextState RECURSE. calling goNextState $nexttokentype $currentstate" - set transition_info [::tomlish::parse::goNextState $nexttokentype $tok $currentstate] - set result [dict get $transition_info newstate] - } - PUSHSPACE { - set original_target [dict get $transition_to PUSHSPACE] - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - if {[dict exists $transition_to starttok]} { - set starttok [dict get $transition_to starttok] - } - spacestack push [dict create type space state $original_target] - - set last_space_action "push" - set last_space_type "space" - - if {[dict exists $transition_to state]} { - #an explicit state in the pushed space was requested in the stateMatrix - override the spacePushTransition (spacePushTransitions can be deprecated if we require explicitness?) - set next [dict get $transition_to state] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next by explicit 'state' entry" - } else { - #puts $::tomlish::parse::spacePushTransitions - if {[dict exists $::tomlish::parse::spacePushTransitions $original_target]} { - set next [dict get $::tomlish::parse::spacePushTransitions $original_target] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next (spacePushTransitions) " - } else { - set next $original_target - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target" - } - } - set result $next - } - TOSTATE { - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - set result [dict get $transition_to TOSTATE] - } - default { - #simplified version of TOSTATE - set result [lindex $transition_to 0] ;#ignore everything but first word - } - } - } else { - ::tomlish::log::error "--->> No state transition defined from state $currentstate when tokentype $tokentype received" - set result "nostate" - } - lappend state_list [list tokentype $tokentype from $currentstate to $result] - set state $result - ::tomlish::log::notice "--->> STATE TRANSITION tokenType: '$tokentype' tok:$tok triggering '$currentstate' -> '$result' last_space_action:$last_space_action " - return [dict create prevstate $prevstate newstate $result space_action $last_space_action starttok $starttok] - } - - proc report_line {{line ""}} { - variable linenum - variable is_parsing - if {$is_parsing} { - if {$line eq ""} { - set line $linenum - } - return "Line Number: $line" - } else { - #not in the middle of parsing tomlish text - return nothing. - return "" - } - } - - #produce a *slightly* more readable string rep of the nest for puts etc. - proc nest_pretty1 {list} { - set prettier "{" - - foreach el $list { - if { [lindex $el 0] eq "NEWLINE"} { - append prettier "[list $el]\n" - } elseif {([llength $el] > 1) && ([lindex $el 0] in {KEY DQKEY SQKEY TABLE ARRAY})} { - append prettier [nest_pretty1 $el] - } else { - append prettier "[list $el] " - } - } - append prettier "}" - return $prettier - } - - proc set_tokenType {t} { - variable tokenType - variable tokenType_list - if {![info exists tokenType]} { - set tokenType "" - } - lappend tokenType_list $t - set tokenType $t - } - - proc switch_tokenType {t} { - variable tokenType - variable tokenType_list - lset tokenType_list end $t - set tokenType $t - } - - proc get_tokenType {} { - variable tokenType - return $tokenType - } - - - proc get_token_waiting {} { - variable token_waiting - return [lindex $token_waiting 0] - } - proc clear_token_waiting {} { - variable token_waiting - set token_waiting [list] - } - - #token_waiting is a list - but our standard case is to have only one - #in certain circumstances such as near eof we may have 2 - #the set_token_waiting function only allows setting when there is not already one waiting. - #we want to catch cases of inadvertently trying to set multiple - # - the reason being that the state transition triggered by the previous token may have invalidated the assumptions made when a token was added as waiting. - proc set_token_waiting {args} { - if {[llength $args] %2 != 0} { - error "tomlish set_token_waiting must have args of form: type value complete 0|1" - } - variable token_waiting - - if {[llength $token_waiting] && [dict get [lindex $token_waiting end] type] ne "eof"} { - #tokloop already set a token_waiting - but something (post tokloop processing?) is trying to set another - #we may need to remove the existing token_waiting and reset the tokloop index to the previous char so it's reprocessed in the possibly new context - #rather than attempt to make the right decision here - we raise an error and require the caller to check/handle it - set err "tomlish set_token_waiting already has token_waiting: [lindex $token_waiting 0]" - append err \n " - cannot add token_waiting: $args" - error $err - #set tomlish::parse::i [expr {[dict get $token_waiting startindex] -1}] - #set token_waiting [list] - } - - set waiting [dict create] - dict for {k v} $args { - switch -exact $k { - type - complete { - dict set waiting $k $v - } - value { - dict set waiting tok $v - } - startindex { - dict set waiting startindex $v - } - default { - error "tomlish set_token_waiting error - unrecognised key $k. known keys: [dict keys $args]" - } - } - } - if {![tcl::string::is boolean -strict [dict get $waiting complete]]} { - error "tomlish set_token_waiting error - 'complete' must be a boolean. got [dict get $waiting complete]" - } - if {![llength $token_waiting]} { - set token_waiting [list $waiting] - } else { - #an extra sanity-check that we don't have more than just the eof.. - if {[llength $token_waiting] > 1} { - set err "tomlish Unexpected. Existing token_waiting count > 1.\n" - foreach tw $token_waiting { - append err " $tw" \n - } - append err " - cannot add token_waiting: $waiting" - error $err - } - #last entry must be a waiting eof - set token_waiting [list $waiting [lindex $token_waiting end]] - } - return - } - - #returns 0 or 1 - #tomlish::parse::tok - #we attempt to do this without lookahead (potential use in streaming toml? for what benefit?) todo -final flag - # - the possible benefit is being able to more easily process in arbitrarily split chunks (although we would still have to watch crlf splitting ?) - # - interactive use? - - proc tok {} { - variable nest - variable s - variable v - variable i - variable tok - variable type ;#character type - variable state ;#FSM - - - variable tokenType - variable tokenType_list - - - variable endToken - - variable lastChar - - variable braceCount - variable bracketCount - - - #------------------------------ - #Previous run found another (presumably single-char) token - #The normal case is for there to be only one dict in the list - #multiple is an exception - primarily for eof - variable token_waiting - if {[llength $token_waiting]} { - set waiting [lindex $token_waiting 0] - - set tokenType [dict get $waiting type] - set tok [dict get $waiting tok] - #todo: dict get $token_waiting complete - set token_waiting [lrange $token_waiting 1 end] - return 1 - } - #------------------------------ - - set resultlist [list] - set sLen [tcl::string::length $s] - - set slash_active 0 - set quote 0 - set c "" - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $s [expr {$i - 1}]] - set start_of_data h - } else { - set lastChar "" - set start_of_data 1 - #bom-handling - if {[tcl::string::index $s 0] eq "\uFEFF"} { - #bom (could be from various encodings - now decoded as single unicode char FEFF) - #incr i 1 ;#skip over initial bom? - } - } - - - set c [tcl::string::index $s $i] - set cindex $i - - set ctest [tcl::string::map {\{ lc \} rc \[ lb \] rb \" dq ' sq \\ bsl \r cr \n lf \t tab \uFEFF bom} $c] - tomlish::log::debug "- tokloop char <$ctest> index $i tokenType:$tokenType tok:<$tok>" - #puts "got char $c during tokenType '$tokenType'" - incr i ;#must incr here because we do returns inside the loop - - - - switch -exact -- $ctest { - # { - set had_slash $slash_active - set slash_active 0 - - if {$had_slash} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - #for multiliteral, multistring - data and/or end - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo token beginning with underscore - never returned to state machine - review - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed (see tomlish::utils::is_barekey). [tomlish::parse::report_line]" - } - whitespace { - # hash marks end of whitespace token - #do a return for the whitespace, set token_waiting - #set_token_waiting type comment value "" complete 1 - incr i -1 ;#leave comment for next run - return 1 - } - untyped_value { - #REVIEW! the spec isn't clear.. is whitespace after an int,bool etc required before comment? - #we will accept a comment marker as an immediate terminator of the untyped_value. - incr i -1 - return 1 - } - starttablename - starttablearrayname { - #fix? - error "tomlish Character '#' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare parts - but allowed in quoted parts - let tablename parser sort it out - append tok $c - } - default { - #dquotedkey, string,literal, multistring - append tok $c - } - } - } else { - switch -- $state { - multistring-space { - set_tokenType stringpart - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "#" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "#" - } - default { - #start of token if we're not in a token - set_tokenType comment - set tok "" ;#The hash is not part of the comment data - } - } - } - } - lc { - #left curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i [tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - #*bare* tablename can only contain letters,digits underscores - error "tomlish Invalid tablename first character \{ [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #valid in quoted parts - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\{" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - switch -exact -- $state { - itable-keyval-value-expected - keyval-value-expected { - #switch last key to tablename?? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - array-space - array-syntax { - #nested anonymous inline table - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\{" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\{" - } - default { - error "tomlish state: '$state'. left brace case not implemented [tomlish::parse::report_line]" - } - } - } - - } - rc { - #right curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - tablename { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endinlinetable value "" complete 1 startindex $cindex - return 1 - } - starttablearrayname - tablearrayname { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - default { - #end any other token - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-space { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname-state problem" - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - #invalid - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-val-tail { - set_tokenType "endinlinetable" - set tok "" - #we need to pop the keyval - and then reprocess to pop the inlinetable - so we incr -1 - incr i -1 - return 1 - } - itable-keyval-syntax { - error "tomlish endinlinetable unexpected at this point. Expecting key=val syntax [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\}" - } - multiliteral-space { - set_tokenType "literalpart" ; #review - set tok "\}" - } - default { - #JMN2024b keyval-tail? - error "tomlish state '$state'. endinlinetable case not implemented [tomlish::parse::report_line]" - } - } - } - - } - lb { - #left square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename { - #change the tokenType - switch_tokenType "starttablearrayname" - set tok "" ;#no output into the tomlish list for this token - #any following whitespace is part of the tablearrayname, so return now - return 1 - } - tablename - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - #append tok "\\[" - append tok {\[} - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - #invalid at this point - state machine should disallow: - # table -> starttablearrayname - # tablearray -> starttablearrayname - set_token_waiting type starttablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "\[" - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - keyval-value-expected - itable-keyval-value-expected { - set_tokenType "startarray" - set tok "\[" - return 1 - } - array-space - array-syntax { - #nested array? - set_tokenType "startarray" - set tok "\[" - return 1 - #error "state: array-space. startarray case not implemented [tomlish::parse::report_line]" - } - table-space { - #table name - #assume it's a single bracket - but we need to wait for non-bracket to confirm it's not a tablearray - #note that a starttablearrayname token may contain whitespace between the brackets - # e.g \[ \[ - set_tokenType "starttablename" - set tok "" ;#there is no output into the tomlish list for this token - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\[" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\[" - } - itable-space { - #handle state just to give specific error msg - error "tomlish state: '$state'. Left square bracket invalid. Cannot start array in inline table without key. Use key=\[\] syntax. [tomlish::parse::report_line]" - } - default { - error "tomlish state: '$state'. startarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - rb { - #right square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - #???? - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } else { - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } - } - starttablename { - #toml-test invalid/table/empty - - set_token_waiting type tablename value "" complete 1 startindex $cindex - incr i -1 - return 1 - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablename value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - default { - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - array-syntax - array-space { - #invalid - but allow parser statemachine to report it. - set_tokenType "endarray" - set tok "\]" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endarray" - set tok "\]" - return 1 - } - tablename-state { - #e.g [] - empty tablename - #tomltest 1.1.0 invalid/table/empty - #should be invalid - #we parse it and let dict::from_tomlish error when it tries to split table - - set_tokenType "endtablename" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname problem" - set_tokenType "endtablearray" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-tail { - #[[xxx] - set_tokenType "endtablename" - #sequence: starttablename -> starttablearrayname -> endtablearrayname -> endtablename - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\]" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\]" - } - default { - error "tomlish state '$state'. endarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - bsl { - #backslash - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - #end whitespace token - incr i -1 ;#reprocess bsl in next run - return 1 - } else { - error "tomlish Unexpected backslash during whitespace. [tomlish::parse::report_line]" - } - } - literal - literalpart - squotedkey { - #never need to set slash_active true when in single quoted tokens - append tok "\\" - set slash_active 0 - } - string - dquotedkey - comment { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - stringpart { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - starttablename - starttablearrayname { - error "tomlish backslash is invalid as first character of $tokenType [tomlish::parse::report_line]" - } - tablename - tablearrayname { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - barekey { - error "tomlish Unexpected backslash during barekey. [tomlish::parse::report_line]" - } - default { - error "tomlish Backslash unexpected during tokentype: '$tokenType'. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$slash_active} { - set_tokenType "stringpart" - set tok "\\\\" - set slash_active 0 - } else { - set slash_active 1 - } - } - multiliteral-space { - #nothing can be escaped in multiliteral-space - not even squotes (?) review - set_tokenType "literalpart" - set tok "\\" - } - default { - error "tomlish tok error: Unexpected backslash when no token is active. [tomlish::parse::report_line]" - } - } - } - } - sq { - #single quote - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote { - #for within multiliteral - #short tentative_accum_squote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_squote is multisquote + 2 = 5 - #return tok with value ''''' - return 1 - } - } - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multiliteral - #switch? - append tok $c - set_tokenType triple_squote - return 1 - } - default { - #if there are more than 3 leading squotes we also enter multiliteral space and the subsequent ones are handled - #by the tentative_accum_squote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 squotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_squote_sequence'" - } - } - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - #end whitespace - incr i -1 ;#reprocess sq - return 1 - } - literal { - #slash_active always false - #terminate the literal - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - literalpart { - #ended by ''' - but final could be '''' or ''''' (up to 2 squotes allowed directly before ending triple squote sequence) - #todo - # idea: end this literalpart (possibly 'temporarily') - # let the sq be reprocessed in the multiliteral-space to push an end-multiliteral-sequence to state stack - # upon popping end-multiliteral-sequence - stitch quotes back into this literalpart's token (if either too short - or a long ending sequence as shown above) - incr i -1 ;#throw the "'" back to loop - will be added to a tentative_accum_squote token for later processing - return 1 - } - XXXitablesquotedkey { - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - squotedkey { - ### - #set_token_waiting type endsquote value "'" complete 1 - return 1 - } - starttablename - starttablearrayname { - #!!! - incr i -1 - return 1 - } - tablename - tablearrayname { - append tok $c - } - barekey { - #barekeys now support all sorts of unicode letter/number chars for other cultures - #but not punctuation - not even for those of Irish heritage who don't object - #to the anglicised form of some names. - # o'shenanigan seems to not be a legal barekey - #The Irish will have to use an earlier form Ó - which apparently many may prefer anyway. - error "tomlish Unexpected single quote during barekey. [tomlish::parse::report_line]" - } - default { - append tok $c - } - } - } else { - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading squote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_squote token or triple_squote token - #It currently doesn't trigger double_squote token - #(handle '' same as 'x' ie produce a single_squote and go into processing literal) - #review - producing double_squote for empty literal may be slightly more efficient. - #This token is not used to handle squote sequences *within* a multiliteral - set_tokenType "_start_squote_sequence" - set tok "'" - } - multiliteral-space { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_squote" ;#trigger tentative_accum_squote - set tok "'" - return 1 - } - table-space - itable-space { - #tests: squotedkey.test squotedkey_itable.test - set_tokenType "squotedkey" - set tok "" - } - XXXtable-space - XXXitable-space { - #future - could there be multiline keys? MLLKEY, MLBKEY ? - #this would (almost) allow arbitrary tcl dicts to be stored in toml (aside from escaping issues) - #probably unlikely - as it's perhaps not very 'minimal' or ergonomic for config files - #@2025 ABNF for toml mentions key, simple-key, unquoted-key, quoted-key and dotted-key - #where key is simple-key or dotted-key - no MLL or MLB components - #the spec states solution for arbitrary binary data is application specific involving encodings - #such as hex, base64 - set_tokenType "_start_squote_sequence" - set tok "'" - return 1 - } - tablename-state { - #first char in tablename-state/tablearrayname-state - set_tokenType "tablename" - append tok "'" - } - tablearrayname-state { - set_tokenType "tablearrayname" - append tok "'" - } - literal-state { - #shouldn't get here? review - tomlish::log::debug "- tokloop sq during literal-state with no tokentype - empty literal?" - set_tokenType "literal" - incr -1 - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - #error "tomlish unimplemented - squote during state '$state'. [tomlish::parse::report_line]" - } - dottedkey-space { - set_tokenType "squotedkey" - } - default { - error "tomlish unhandled squote during state '$state'. [tomlish::parse::report_line]" - } - } - } - - } - dq { - #double quote - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - tentative_accum_dquote { - #within multistring - #short tentative_accum_dquote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_dquote is multidquote + 2 = 5 - #return tok with value """"" - return 1 - } - } - _start_dquote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multistring - #switch? - append tok $c - set_tokenType triple_dquote - return 1 - } - default { - #if there are more than 3 leading dquotes we also enter multistring space and the subsequent ones are handled - #by the tentative_accum_dquote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 dquotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_dquote_sequence'" - } - } - } - literal - literalpart { - append tok $c - } - string { - if {$had_slash} { - append tok "\\" $c - } else { - #unescaped quote always terminates a string - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - stringpart { - #sub element of multistring - if {$had_slash} { - append tok "\\" $c - } else { - incr i -1 ;#throw the {"} back to loop - will be added to a tentative_accum_dquote token for later processing - return 1 - } - } - whitespace { - #assert: had_slash will only ever be true in multistring-space - if {$had_slash} { - incr i -2 - return 1 - } else { - #end whitespace token - throw dq back for reprocessing - incr i -1 - return 1 - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - XXXdquotedkey { - if {$had_slash} { - append tok "\\" - append tok $c - } else { - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - dquotedkey { - ### - if {$had_slash} { - append tok "\\" - append tok $c - } else { - #set_token_waiting type enddquote value {"} complete 1 - return 1 - } - } - squotedkey { - append tok $c - } - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - incr i -1 ;## - return 1 - } - default { - error "tomlish got quote during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - #$slash_active not relevant when no tokenType - #token is string only if we're expecting a value at this point - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading dquote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_dquote token or triple_dquote token - #It currently doesn't trigger double_dquote token - #(handle "" same as "x" ie produce a single_dquote and go into processing string) - #review - producing double_dquote for empty string may be slightly more efficient. - #This token is not used to handle dquote sequences once *within* a multistring - set_tokenType "_start_dquote_sequence" - set tok {"} - } - multistring-space { - if {$had_slash} { - set_tokenType "stringpart" - set tok "\\\"" - } else { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_dquote" ;#trigger tentative_accum_dquote - set tok {"} - return 1 - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\"" - } - table-space - itable-space { - set_tokenType "dquotedkey" - set tok "" - } - dottedkey-space { - set_tokenType dquotedkey - set tok "" - - #only if complex keys become a thing - #set_tokenType dquote_seq_begin - #set tok $c - } - tablename-state { - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - default { - error "tomlish Unexpected dquote during state '$state' [tomlish::parse::report_line]" - } - } - } - } - = { - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - #assertion had_slash 0 - append tok $c - } - string - comment - dquotedkey { - #for these tokenTypes an = is just data. - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type equal value = complete 1 startindex $cindex - return 1 - } - } - barekey { - #set_token_waiting type equal value = complete 1 - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character '=' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare name - but valid in quoted parts - leave for tablename parser to sort out - append tok $c - } - default { - error "tomlish unexpected = character during tokentype $tokenType. case not implemented. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok = - } - multiliteral-space { - set_tokenType "literalpart" - set tok "=" - } - dottedkey-space { - set_tokenType "equal" - set tok "=" - return 1 - } - default { - set_tokenType "equal" - set tok = - return 1 - } - } - } - } - cr { - #REVIEW! - # \r carriage return - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #we have received a double cr - ::tomlish::log::warn "double cr - will generate cr token. needs testing" - set_tokenType "cr" ;#lone cr token will generally raise an error - but let state machine handle it - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - append tok $c - } - literalpart { - #part of MLL string (multi-line literal string) - #we need to split out crlf as a separate NEWLINE to be consistent - ::tomlish::log::warn "literalpart ended by cr - needs testing" - #return literalpart temporarily - allow cr to be reprocessed from multiliteral-space - incr i -1 - return 1 - } - stringpart { - #stringpart is a part of MLB string (multi-line basic string) - #throw back the cr - if followed by lf it will become a {NEWLINE crlf} entry within the MULTISTRING list (e.g between STRINGPART entries) - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #could in theory be valid in quoted part of name - #review - might be better just to disallow here - append tok $c - } - whitespace { - #it should technically be part of whitespace if not followed by lf - #but outside of values we are also free to map it to be another NEWLINE instead? REVIEW - incr i -1 - return 1 - } - untyped_value { - incr i -1 - return 1 - } - comment { - #JJJJ - #review - incr i -1 - return 1 - } - default { - #!todo - error out if cr inappropriate for tokenType - append tok $c - } - } - } else { - #lf may be appended if next - #review - lone cr as newline? - this is uncommon - but so is lone cr in a string(?) - set_tokenType "newline" - set tok cr - } - } - lf { - # \n newline - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #this lf is the trailing part of a crlf - append tok lf ;#assert we should now have tok "crlf" - as a previous cr is the only way to have an incomplete newline tok - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - #multiliteral or multistring - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - #nl is not allowed *within* a literal - require multiliteral syntax for any literal containing a newline ''' ''' - #even though we terminate the literal without the closing quote here - the token_waiting newline should trigger a state error - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - literalpart { - #we allow newlines - but store them within the multiliteral as their own element - #This is a legitimate end to the literalpart - but not the whole multiliteral - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - stringpart { - if {$had_slash} { - #emit the stringpart (return 1), queue the continuation, go back 1 to reprocess the lf (incr i -1) - set_token_waiting type continuation value \\ complete 1 startindex [expr {$cindex-1}] - incr i -1 - return 1 - } else { - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - starttablename - tablename - tablearrayname - starttablearrayname { - error "tomlish Character is invalid in $tokenType. [tomlish::parse::report_line]" - } - default { - #newline ends all other tokens. - #note for string: we don't add (raw unescaped) newline to simple string. (must use multi-string for this) - #note for whitespace: - # we will use the convention that \n terminates the current whitespace even if whitespace follows - # ie whitespace is split into separate whitespace tokens at each newline - - #puts "-------------- newline lf during tokenType $tokenType" - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "newline" - set tok lf - return 1 - } - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "newline" - set tok "lf" - return 1 - } - default { - #ignore slash? error? - set_tokenType "newline" - set tok lf - return 1 - } - } - #if {$had_slash} { - # #CONT directly before newline - allows strings_5_byteequivalent test to pass - # set_tokenType "continuation" - # set tok "\\" - # incr i -1 - # return 1 - #} else { - # set_tokenType newline - # set tok lf - # return 1 - #} - } - } - , { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok , - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - #stringpart can have up to 2 quotes too - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type comma value "," complete 1 startindex $cindex - return 1 - } - } - default { - set_token_waiting type comma value "," complete 1 startindex $cindex - if {$had_slash} {append tok "\\"} - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "literalpart" - set tok "," - } - default { - set_tokenType "comma" - set tok "," - return 1 - } - } - } - } - . { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - untyped_value { - if {$had_slash} {append tok "\\"} - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - switch -exact -- $state { - multistring-space { - #review - if {$had_slash} { - incr i -2 - } else { - incr i -1 - } - return 1 - } - xxxdottedkey-space { - incr i -1 - return 1 - } - dottedkey-space-tail { - incr i -1 - return 1 - } - default { - error "tomlish Received period during tokenType 'whitespace' [tomlish::parse::report_line]" - } - } - } - starttablename - starttablearrayname { - #This would correspond to an empty table name - error "tomlish Character '.' is not allowed as first character ($tokenType). [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #subtable - split later - review - append tok $c - } - barekey { - #e.g x.y = 1 - #we need to transition the barekey to become a structured table name ??? review - #x is the tablename y is the key - set_token_waiting type dotsep value "." complete 1 startindex $cindex - return 1 - } - default { - error "tomlish Received period during tokenType '$tokenType' [tomlish::parse::report_line]" - #set_token_waiting type period value . complete 1 - #return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "." - } - multiliteral-space { - set_tokenType "literalpart" - set tok "." - } - XXXdottedkey-space { - ### obs? - set_tokenType "dotsep" - set tok "." - return 1 - } - dottedkey-space-tail { - ### - set_tokenType "dotsep" - set tok "." - return 1 - } - default { - set_tokenType "untyped_value" - set tok "." - } - } - } - - } - " " - tab { - if {[tcl::string::length $tokenType]} { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #todo had_slash - emit token or error - #whitespace is a terminator for bare keys - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - comment { - if {$had_slash} { - append tok "\\" - } - append tok $c - } - string - dquotedkey { - if {$had_slash} { append tok "\\" } - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART xxx WS " " - incr i -1 - return 1 - } - } - literal - literalpart - squotedkey { - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - append tok $c - } else { - append tok $c - } - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace space during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "whitespace" - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - if {$had_slash} { - error "tomlish unexpected backslash [tomlish::parse::report_line]" - } - set_tokenType "whitespace" - append tok $c - } - } - } - } - tabX { - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out (?review) - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #whitespace is a terminator for bare keys - incr i -1 - #set_token_waiting type whitespace value $c complete 1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - squotedkey { - append tok $c - } - dquotedkey - string - comment - whitespace { - #REVIEW - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - incr i -1 - return 1 - } - } - literal - literalpart { - append tok $c - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace tab during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - if {$slash_active} { - set slash_active 0 - } - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType whitespace - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "whitespace" - append tok $c - } - } - } - } - bom { - #bom encoded as single unicode codepoint \uFFEF - #BOM (Byte Order Mark) - ignored by token consumer - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #assert - tok will be one or two squotes only - #A toml literal probably isn't allowed to contain this - #but we will parse and let the validator sort it out. - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart { - append tok $c - } - string - stringpart { - append tok $c - } - default { - #state machine will generally not have entry to accept bom - let it crash - set_token_waiting type bom value "\uFEFF" complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - multistring-space { - set_tokenType "stringpart" - set tok $c - } - default { - set_tokenType "bom" - set tok "\uFEFF" - return 1 - } - } - } - } - default { - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - #review - incr i -1 ;#We don't have a full token to add to the token_waiting dict - so leave this char for next run. - return 1 - } - } - barekey { - if {[tomlish::utils::is_barekey $c]} { - append tok $c - } else { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed. (see tomlish::utils::is_barekey) [tomlish::parse::report_line]" - } - } - starttablename - starttablearrayname { - incr i -1 - #allow statemachine to set context for subsequent chars - return 1 - } - string - stringpart { - append tok $c - } - default { - #e.g comment/literal/literalpart/untyped_value/starttablename/starttablearrayname/tablename/tablearrayname - append tok $c - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - table-space - itable-space { - #if no currently active token - assume another key value pair - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - multistring-space { - set_tokenType "stringpart" - if {$had_slash} { - set tok \\$c - } else { - set tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - tablename-state { - set_tokenType "tablename" - set tok $c - } - tablearrayname-state { - set_tokenType "tablearrayname" - set tok $c - } - dottedkey-space { - set_tokenType barekey - set tok $c - } - default { - #todo - something like ansistring VIEW to show control chars? - set cshow [string map [list \t tab \v vt] $c] - tomlish::log::debug "- tokloop char '$cshow' setting to untyped_value while state:$state [tomlish::parse::report_line]" - set_tokenType "untyped_value" - set tok $c - } - } - } - } - } - - } - - #run out of characters (eof) - if {[tcl::string::length $tokenType]} { - #check for invalid ending tokens - #if {$state eq "err-state"} { - # error "Reached end of data whilst tokenType = '$tokenType'. INVALID" - #} - switch -exact -- $tokenType { - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open literal - error "tomlish eof reached without closing single quote for string literal. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "literal" - set tok "" - return 1 - - ##review - #set_token_waiting type endsquote value "'" complete 1 startindex [expr {$cindex -1}] - #set_tokenType "literal" - #set tok "" - #return 1 - } - } - } - _start_dquote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open string - error "tomlish eof reached without closing double quote for string. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "string" - set tok "" - return 1 - } - } - } - newline { - #The only newline token that has still not been returned should have a tok value of "cr" - puts "tomlish eof reached - with incomplete newline token '$tok'" - if {$tok eq "cr"} { - #we convert lone cr to it's own "cr" token elsewhere in the document to allow statemachine to handle it. - #(which it should generally do by not handling it ie raising an error - or emitting an ERROR list in the tomlish) - #if trailing char is a lone cr - we should encode it the same way as elsewhere that is outside of values - # ie as it's own token. - switch_tokenType "cr" - return 1 - } else { - #should be unreachable - error "tomlish eof reached - with invalid newline token. value: $tok" - } - } - } - set_token_waiting type eof value eof complete 1 startindex $i ;#review - return 1 - } else { - ::tomlish::log::debug "- No current tokenType, ran out of characters, setting tokenType to 'eof' [tomlish::parse::report_line]" - set tokenType "eof" - set tok "eof" - } - return 0 - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::parse ---}] -} -namespace eval tomlish::huddle { - proc from_json {json} { - package require huddle - package require huddle::json - #note - huddle may now contain raw surrogate pair - which cannot be emitted to stdout - set h [huddle::json::json2huddle parse $json] - } - proc from_dict {d} { - error "tomlish::huddle::from_dict unimplemented" - } - - #raw - strings must already be processed into values suitable for json e.g surrogate pair escaping - proc jsondumpraw {huddle_object {offset " "} {newline "\n"} {begin ""}} { - upvar ::huddle::types types - set nextoff "$begin$offset" - set nlof "$newline$nextoff" - set sp " " - if {[string equal $offset ""]} {set sp ""} - - set type [huddle type $huddle_object] - - switch -- $type { - boolean - - number { - return [huddle get_stripped $huddle_object] - } - null { - return null - } - string { - set data [huddle get_stripped $huddle_object] - - # JSON permits only oneline string - #set data [string map { - # \n \\n - # \t \\t - # \r \\r - # \b \\b - # \f \\f - # \\ \\\\ - # \" \\\" - # / \\/ - # } $data - #] - return "\"$data\"" - } - list { - set inner {} - set len [huddle llength $huddle_object] - for {set i 0} {$i < $len} {incr i} { - set subobject [huddle get $huddle_object $i] - lappend inner [jsondumpraw $subobject $offset $newline $nextoff] - } - if {[llength $inner] == 1} { - return "\[[lindex $inner 0]\]" - } - return "\[$nlof[join $inner ,$nlof]$newline$begin\]" - } - dict { - set inner {} - foreach {key} [huddle keys $huddle_object] { - lappend inner [subst {"$key":$sp[jsondumpraw [huddle get $huddle_object $key] $offset $newline $nextoff]}] - } - #if {[llength $inner] == 1} { - # return $inner ;#wrong - breaks with quoted list representation - # #FAILS: toml-test valid/comment/tricky - #} - - return "\{$nlof[join $inner ,$nlof]$newline$begin\}" - } - default { - set node [unwrap $huddle_object] - #foreach {tag src} $node break - lassign $node tag src - return [$types(callback:$tag) jsondumpraw $huddle_object $offset $newline $nextoff] - } - } - } -} - -#typed as per toml-test types -namespace eval tomlish::typedhuddle { - proc from_json {json} { - set plainhuddle [tomlish::huddle::from_json $json] - - error "tomlish::typedhuddle::from_json unimplemented" - } - proc from_dict {d} { - package require huddle - set h [huddle create] - if {[tomlish::dict::is_typeval $d]} { - set dtype [dict get $d type] - switch -- $dtype { - ARRAY { - #error "typedhuddle::from_dict ARRAY not yet handled" - set h_list [huddle list] - set elements [dict get $d value] - foreach el $elements { - set sub [from_dict $el] - huddle append h_list $sub - } - return $h_list - } - default { - set tinfo [tomlish::dict::convert_typeval_to_tomltest $d] - #basic non-container types - set h_tdict [huddle create] - huddle set h_tdict type [huddle string [dict get $tinfo type]] - huddle set h_tdict value [huddle string [dict get $tinfo value]] - return $h_tdict - } - } - } else { - dict for {dictkey dictval} $d { - set jsonkey [tomlish::utils::rawstring_to_jsonstring $dictkey] - if {[tomlish::dict::is_typeval $dictval]} { - set dtype [dict get $dictval type] - switch -- $dtype { - ARRAY { - #error "typedhuddle::from_dict ARRAY not yet handled" - set h_next [huddle list] - set elements [dict get $dictval value] - foreach el $elements { - set sub [from_dict $el] - huddle append h_next $sub - } - } - default { - set tinfo [tomlish::dict::convert_typeval_to_tomltest $dictval] - set tp [dict get $tinfo type] - #basic non-container types - set h_next [huddle create] ;#dict - huddle set h_next type [huddle string [dict get $tinfo type]] - huddle set h_next value [huddle string [dict get $tinfo value]] - } - } - huddle set h $jsonkey $h_next - } else { - #dict - set sub [from_dict $dictval] - huddle set h $jsonkey $sub - } - } - } - return $h - } - proc is_typeval {huddled} { - set htype [huddle type $huddled] - if {$htype ne "dict"} { - return 0 - } - if {[huddle keys $huddled] ne {type value}} { - return 0 - } - set tp [huddle type $huddled type] - switch -- $tp { - string - integer - float - bool - datetime - datetime-local - date-local - time-local { - return 1 - } - } - return 0 - } - - #direction from typed json towards toml - proc convert_typeval_to_tomlish {huddled} { - set htype [huddle get_stripped $huddled type] - set hval [huddle get_stripped $huddled value] - switch -- $htype { - string { - #we need to decide here the type of string element to use in toml/tomlish - #STRING,MULTISTRING,LITERAL,MULTILITERAL - #set unesc [tomlish::utils::unescape_jsonstring $hval] ;#no need - json parser unescaped when creating the huddle - set unesc $hval - #(huddle::json::json2huddle parse $json) - #since it was unescaped any backslashes remaining represent themselves - reapply escape - REVIEW - #set hval [string map [list \\ \\\ ] $hval] - #JSJS - if {[string first \n $unesc] >= 0} { - #always use a MULTI - if {[string first ' $unesc] >=0} { - if {[string first ''' $unesc] >=0} { - set dtype MULTISTRING - } else { - set dtype MULTILITERAL - } - } else { - if {[string first \"\"\" $unesc] >=0} { - set dtype MULTILITERAL - } else { - set dtype MULTISTRING - } - } - } else { - #use multi if needed? - if {[string first '' $hval] >=0} { - if {[string first ''' $unesc] >=0} { - set dtype STRING - } else { - set dtype MULTILITERAL - } - } elseif {[string first ' $unesc] >= 0} { - set dtype STRING - } elseif {[string first \"\"\" $unesc] >= 0} { - set dtype LITERAL - } else { - #STRING or LITERAL? - set dtype STRING - } - } - - } - datetime - bool { - set dtype [string toupper $htype] - } - float { - set dtype FLOAT - if {[::tomlish::utils::string_is_integer -strict $hval]} { - #json FLOAT specified as integer - must have dot for toml - set hval [expr {double($hval)}] - } - } - integer { - set dtype INT - } - datetime - datetime-local - date-local - time-local { - #DDDD - #set dtype DATETIME - set dtype [string toupper $htype] - } - default { - error "tomlish::typedhuddle::convert_typeval_to_tomlish unrecognised type $htype" - } - } - return [list type $dtype value $hval] - } - -} -namespace eval tomlish::toml { - proc from_binary {bindata} { - set bom "" - set b12 [tcl::string::range $bindata 0 1] - set b12test [string map [list \xEF\xBB utf8_12 \xFE\xFF bom16be \xFF\xFE utf32le_12 \x00\x00 utf32be_12] $b12] - switch -- $b12test { - bom16be { - #FEFF - set bom utf-16be - } - utf32le_12 { - #FFFE - set b34 [tcl::string::range $bindata 2 3] - if {$b34 eq "\x00\x00"} { - set bom utf-32le - } else { - set bom utf-16le - } - } - utf32be_12 { - #0000 - set b34 [tcl::string::range $bindata 2 3] - if {$b34 eq "\xFE\xFF"} { - set bom utf-32be - } - } - utf8_12 { - set b3 [tcl::string::index $bindata 2] - if {$b3 eq "\xBF"} { - set bom utf-8 - } - } - } - if {$bom eq ""} { - #no bom - assume utf8 - but we read in as binary - #if data wasn't actually utf8 we may error here depending on content - or we may just get wrongly encoded chars - set tomldata [encoding convertfrom utf-8 $bindata] - } elseif {$bom eq "utf-8"} { - #utf-8 bom read in as binary - set tomldata [encoding convertfrom utf-8 $bindata] - #bom now encoded as single unicode char \uFFEF - } else { - return -code error -errorcode {TOML ENCODING NOTUTF8} "Input not UTF8 encoded according to BOM. Indicated encoding is '$bom' - invalid for toml" - } - return $tomldata - } - proc from_tomlish {tomlish} { - return [tomlish::encode::tomlish $tomlish] - } - - #todo - rename to taggedjson - proc from_tomlish_from_dict_from_typedjson {json} { - set d [tomlish::dict::from_typedjson $json] - from_tomlish [tomlish::from_dict $d] ;#return tomlish - } - - proc tablename_split {tablename {normalize false}} { - #we can't just split on . because we have to handle quoted segments which may contain a dot. - #eg {dog."tater.man"} - if {$tablename eq ""} { - error "tablename_split. No table name segments found. empty tablename" - } - set sLen [tcl::string::length $tablename] - set segments [list] - set mode "preval" ;#5 modes: preval, quoted,litquoted, unquoted, postval - #quoted is for double-quotes, litquoted is for single-quotes (string literal) - set seg "" - for {set i 0} {$i < $sLen} {incr i} { - - if {$i > 0} { - set lastChar [tcl::string::index $tablename [expr {$i - 1}]] - } else { - set lastChar "" - } - - #todo - track\count backslashes properly - - set c [tcl::string::index $tablename $i] - if {$c eq "\""} { - if {($lastChar eq "\\")} { - #not strictly correct - we could have had an even number prior-backslash sequence - #the toml spec would have us error out immediately on bsl in bad location - but we're - #trying to parse to unvalidated tomlish - set ctest escq - } else { - set ctest dq - } - } else { - set ctest [string map [list " " sp \t tab] $c] - } - - switch -- $ctest { - . { - switch -exact -- $mode { - preval { - error "tablename_split. dot not allowed - expecting a value" - } - unquoted { - #dot marks end of segment. - if {![tomlish::utils::is_barekey $seg]} { - error "tablename_split. unquoted key segment $seg is not a valid toml key" - } - lappend segments $seg - set seg "" - set mode "preval" - } - quoted { - append seg $c - } - litquoted { - append seg $c - } - postval { - #got dot in an expected location - set mode "preval" - } - } - } - dq { - #unescaped dquote - switch -- $mode { - preval { - set mode "quoted" - set seg "\"" - } - unquoted { - #invalid in barekey - but we are after structure only - append seg $c - } - quoted { - append seg $c - #JJJJ - if {$normalize} { - lappend segments [::tomlish::utils::unescape_string [tcl::string::range $seg 1 end-1]] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" ;#make sure we only accept a dot or end-of-data now. - } - litquoted { - append seg $c - } - postval { - error "tablename_split. expected whitespace or dot, got double quote. tablename: '$tablename'" - } - } - } - ' { - switch -- $mode { - preval { - append seg $c - set mode "litquoted" - } - unquoted { - #single quote inside e.g o'neill - ultimately invalid - but we pass through here. - append seg $c - } - quoted { - append seg $c - } - litquoted { - append seg $c - #no normalization to do aside from stripping squotes - if {$normalize} { - lappend segments [tcl::string::range $seg 1 end-1] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" - } - postval { - error "tablename_split. expected whitespace or dot, got single quote. tablename: '$tablename'" - } - } - } - sp - tab { - switch -- $mode { - preval - postval { - #ignore - } - unquoted { - #terminates a barekey - lappend segments $seg - set seg "" - set mode "postval" - } - default { - #append to quoted or litquoted - append seg $c - } - } - } - default { - switch -- $mode { - preval { - set mode unquoted - append seg $c - } - postval { - error "tablename_split. Expected a dot separator. got '$c'. tablename: '$tablename'" - } - default { - append seg $c - } - } - } - } - - if {$i == $sLen-1} { - #end of data - ::tomlish::log::debug "End of data: mode='$mode'" - switch -exact -- $mode { - preval { - if {[llength $segments]} { - error "tablename_split. Expected a value after last dot separator. tablename: '$tablename'" - } else { - error "tablename_split. Whitespace only? No table name segments found. tablename: '$tablename'" - } - } - unquoted { - if {![tomlish::utils::is_barekey $seg]} { - #e.g toml-test invalid/table/with-pound required to fail for invalid barekey - error "tablename_split. unquoted key segment $seg is not a valid toml key" - } - lappend segments $seg - } - quoted { - error "tablename_split. Expected a trailing double quote. tablename: '$tablename'" - } - litquoted { - error "tablename_split. Expected a trailing single quote. tablename: '$tablename'" - } - postval { - #ok - segment already lappended - } - } - } - } - - #note - we must allow 'empty' quoted strings '' & "" - # (these are 'discouraged' but valid toml keys) - - return $segments - } - - #tablenames (& tablearraynames) may contain irrelevant leading, trailing and interspersed whitespace - # tablenames can be made up of segments delimited by dots. .eg [ a.b . c ] - #trimmed, the tablename becomes {a.b.c} - # A segment may contain whitespace if it is quoted e.g [a . b . "c etc " ] - #ie whitespace is only irrelevant if it's outside a quoted segment - #trimmed, the tablename becomes {a.b."c etc "} - proc tablename_trim {tablename} { - set segments [tomlish::toml::tablename_split $tablename false] - set trimmed_segments [list] - foreach seg $segments { - lappend trimmed_segments [::string trim $seg " \t"] - } - return [join $trimmed_segments .] - } -} - -namespace eval tomlish::dict { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - - #from_taggedjson - proc from_typedjson {json} { - package require huddle - package require huddle::json - set h [huddle::json::json2huddle parse $json] - #json2huddle parse unescapes the basic json escapes \n \\ etc - #$h could now contain raw form of surrogate pair (json2huddle parse as at 2025-014 doesn't convert the surrogates - just unescapes?) - if {[catch {encoding convertto utf-8 $h} errM]} { - #This test suggests we have raw surrogate pairs - REVIEW - package require punk::cesu - set h [punk::cesu::from_surrogatestring $h] - } - tomlish::dict::from_typedhuddle $h - } - proc from_typedhuddle {h} { - set resultd [dict create] - switch -- [huddle type $h] { - dict { - foreach k [huddle keys $h] { - switch -- [huddle type $h $k] { - dict { - set huddle_d [huddle get $h $k] - #puts stderr "huddle_d: $huddle_d" - #set v [huddle get_stripped $h $k] - if {[tomlish::typedhuddle::is_typeval $huddle_d]} { - dict set resultd $k [tomlish::typedhuddle::convert_typeval_to_tomlish $huddle_d] - } else { - dict set resultd $k [from_typedhuddle $huddle_d] - } - } - list { - set items [huddle get $h $k] - - set numitems [huddle llength $items] - if {$numitems == 0} { - dict set resultd $k [list type ARRAY value {}] - } else { - set arritems [list] - for {set i 0} {$i < $numitems} {incr i} { - set item [huddle get $items $i] - #puts stderr "item: $item" - #set v [huddle get $item] - if {[tomlish::typedhuddle::is_typeval $item]} { - lappend arritems [tomlish::typedhuddle::convert_typeval_to_tomlish $item] - } else { - lappend arritems [from_typedhuddle $item] - } - } - dict set resultd $k [list type ARRAY value $arritems] - } - } - default { - error "dict_from_json unexpected subtype [huddle type $h $k] in dict" - } - } - } - } - list { - set items [huddle get $h] - set numitems [huddle llength $items] - if {$numitems == 0} { - return [list type ARRAY value {}] - } else { - set arritems [list] - for {set i 0} {$i < $numitems} {incr i} { - set item [huddle get $items $i] - #puts stderr "item: $item" - #set v [huddle get $item] - if {[tomlish::typedhuddle::is_typeval $item]} { - lappend arritems [tomlish::typedhuddle::convert_typeval_to_tomlish $item] - } else { - lappend arritems [from_typedhuddle $item] - } - } - return [list type ARRAY value $arritems] - } - - } - } - return $resultd - } - - proc is_typeval {d} { - #designed to detect {type value } e.g {type INT value 3}, {type STRING value "blah etc"} - #as a sanity check we need to avoid mistaking user data that happens to match same form - #consider x.y={type="spud",value="blah"} - #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. - #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[::tomlish::utils::string_is_dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} - } - - #simple types only - not containers? - proc convert_typeval_to_tomltest {d} { - set dtype [dict get $d type] - set dval [dict get $d value] - switch -- $dtype { - INT { - set testtype integer - set dval [expr {$dval}] ;#convert e.g 0xDEADBEEF to 3735928559 - } - FLOAT - BOOL { - set testtype [string tolower $dtype] - } - DATE-LOCAL { - set testtype date-local - } - TIME-LOCAL { - if {[tomlish::utils::_is_hms_or_hm_time $dval] == 2} { - #add seconds for sending to json - set dval "${dval}:00" - } - set testtype time-local - } - DATETIME - DATETIME-LOCAL { - #we expect it to be basically well formed here - this is not validation - just adding possible missing seconds - if {![regexp {([tT\ ])} $dval _ dsep]} { - return -code error -errorcode {TOJSON SYNTAX INVALIDDATE} "Unable to process $dtype '$dval' - missing RFC3339 separator space or T" - } - lassign [split $dval $dsep] dp tail - - #toml allows HH:MM without seconds - but we need to add seconds 00 when passing to external systems - if {![tomlish::utils::is_time-local $tail]} { - #there is some offset component. We aren't checking its syntax here (presumed done when dict building) - regexp {([\+\-zZ])} $tail _ tsep ;#keep tsep for rebuilding - lassign [split $tail $tsep] tp offset ;#offset may be empty if z or Z - } else { - set tp $tail - set tsep "" - set offset "" - } - if {[tomlish::utils::_is_hms_or_hm_time $tp] == 2} { - #need to add seconds - set dval "${dp}${dsep}${tp}:00${tsep}${offset}" - } - set testtype [string tolower $dtype] - } - STRING - MULTISTRING { - set testtype string - #JJJJ - set dval [tomlish::utils::unescape_string $dval] - set dval [tomlish::utils::rawstring_to_jsonstring $dval] - } - LITERAL - MULTILITERAL { - set testtype string - #don't validate on way out to json here? - #decoder should validate by calling tomlish::from_dict - #if {![tomlish::utils::rawstring_is_valid_literal $dval]} { - # return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} $msg - #} - set dval [tomlish::utils::rawstring_to_jsonstring $dval] - } - default { - error "convert_typeval_to_tomltest unhandled type $dtype" - } - } - return [list type $testtype value $dval] - } - - # Check that each leaf is a typeval or typeval dict - #importantly: must accept empty dict leaves e.g {x {}} - proc is_typeval_dict {d {checkarrays 0}} { - if {![::tomlish::utils::string_is_dict $d]} { - return 0 - } - dict for {k v} $d { - set is_d 0 - if {!([is_typeval $v] || [set is_d [is_typeval_dict $v $checkarrays]])} { - return 0 - } - if {!$is_d} { - set vtype [dict get $v type] - switch -- $vtype { - INT - FLOAT - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL - BOOL - LITERAL - STRING - MULTILITERAL - MULTISTRING {} - ARRAY { - if {$checkarrays} { - set arrdata [dict get $v value] - foreach el $arrdata { - if {![is_typeval_dict $el $checkarrays]} { - return 0 - } - } - } - } - default { - puts stderr "is_typeval_dict: Unexpected type '$vtype'" - return 0 - } - } - } - } - return 1 - } - - proc last_tomltype_posn {d} { - set last_simple -1 - set dictposn [expr {[dict size $d] -1}] - foreach k [lreverse [dict keys $d]] { - set dval [dict get $d $k] - if {[is_typeval $dval]} { - set last_simple $dictposn - break - } - incr dictposn -1 - } - return $last_simple - } - - - #review - proc name_from_tablestack {tablestack} { - set name "" - foreach tinfo [lrange $tablestack 1 end] { - lassign $tinfo type namepart - switch -- $type { - T { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - I { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - default { - #end at first break in the leading sequence of T & I tablenames - break - } - } - } - return $name - } - - - #tablenames_info is a flat dict with the key being an '@@' path - proc _show_tablenames {tablenames_info} { - #e.g {@l@a @@b} {ttype header_table tdefined closed} - append msg \n "tablenames_info:" \n - dict for {tkey tinfo} $tablenames_info { - append msg " " "table: $tkey" \n - dict for {field finfo} $tinfo { - append msg " " "$field $finfo" \n - } - } - return $msg - } - - #take a raw string and classify: result is a 2 element list comprised of KEY|SQKEY|DQKEY and the value being the appropriate inner string - proc classify_rawkey {rawval} { - if {![::tomlish::utils::is_barekey $rawval]} { - #requires quoting - # - #Any dot in the key would have been split by dict::from_tomlish - so if it's present here it's part of this key - not a level separator! - # - #we'll use a basic mechanisms for now to determine the type of quoting - # - whether it has any single quotes or not. - # (can't go in an SQKEY) - # - whether it has any chars that require quoting when in a Bstring - # (if so - then its visual representation might be unsuitable for a key in a toml text file, so escape and put in DQKEY instead of literal SQKEY) - #todo - more? - #REVIEW - the backslash might often be in things like a regex or windows path - which is often better expressed in a literal SQKEY - # from literal examples: - # 'c:\Users\nodejs\templates' - # '<\i\c*\s*>' - #If these are in *keys* our basic test will express these as: - # "c:\\Users\\nodejs\\templates" - # "<\\i\\c*\\s*>" - # This still works - but a smarter test might determine when SQKEY is the better form? - #when coming from external systems - can we even know if the value was already escaped? REVIEW - #Probably when coming from json - we know it's already escaped - and so we build our dict converting keys to unescaped - #TODO - clarify in documentation that keys resulting from dict::from_tomlish are in 'normalized' (unescaped) form - # - #For keys - we currently (2025) are only allowed barekeys,basic strings and literal strings. (no multiline forms) - set k_escaped [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $rawval] - if {[string length $k_escaped] != [string length $rawval]} { - #escaping made a difference - set has_escape_requirement 1 - } else { - set has_escape_requirement 0 - } - if {[string first ' $rawval] >=0 || $has_escape_requirement} { - #basic string - # (any ANSI SGR sequence will end up here in escaped form ) - return [list DQKEY $k_escaped] - } else { - #literal string - return [list SQKEY $rawval] - } - } else { - return [list KEY $rawval] - } - } - #the quoting implies the necessary escaping for DQKEYs - proc join_and_quote_rawkey_list {rawkeylist} { - set result "" - foreach rk $rawkeylist { - lassign [tomlish::dict::classify_rawkey $rk] type val - switch -- $type { - SQKEY { - append result "'$val'." - } - DQKEY { - append result "\"$val\"." - } - KEY { - append result "$val." - } - } - } - return [string range $result 0 end-1] - } - - proc _process_tomlish_dottedkey {element {context_refpath {}}} { - upvar tablenames_info tablenames_info - upvar datastructure datastructure - set dottedtables_defined [list] - set dkey_info [tomlish::get_dottedkey_info $element] - #e.g1 keys {x.y y} keys_raw {'x.y' "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - #e.g2 keys {x.y y} keys_raw {{"x.y"} "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - - #[a.b] - #t1.t2.dottedtable.leafkey = "val" - #we have already checked supertables a & {a b} - # - in basic case, passed in context_refpath as {@@a @@b} - # - our context_refpath could also include some combination of keys and array indices e.g {@@a @@b 3 @@subtablekey} - #We need to check {a b t1} & {a b t2} ('creation' only) - #and then 'dottedtable' is 'defined' while leafkey is an ordinary key in dottedtable - - #note we also get here as a 'dottedkey' with the following even though there is no dot in k - #[a.b] - #leafkey = "val" - - set all_dotted_keys [dict get $dkey_info keys] - set dottedkeyname [join $all_dotted_keys .] - - if {[llength $all_dotted_keys] > 1} { - #dottedtable.k=1 - #tX.dottedtable.k=1 - #etc - - #Wrap in a list so we can detect 'null' equivalent. - #We can't use empty string as that's a valid dotted key segment - set dottedtable_bag [list [lindex $all_dotted_keys end-1]] - set dotparents [lrange $all_dotted_keys 0 end-2] - } else { - #basic case - not really a 'dotted' key - #k = 1 - set dottedtable_bag [list] ;#empty bag - set dotparents [list] - } - #assert dottedtable_bag only ever holds 0 or 1 elements - set leaf_key [lindex $all_dotted_keys end] - - #see also: https://github.com/toml-lang/toml/issues/846 "Can dotted keys insert into already-defined [tables]?" - #This code was originally written with a misinterpretation of: - #"Dotted keys create and define a table for each key part before the last one, provided that such tables were not previously created." - # 'each key part before the last one' refers to each key in a single dotted key entry - # not each 2nd-to last key in a list of dotted keys. - - - #we've already tested the table/tablearray keys that got us here.. but not the dottedkey segments (if any) prior to dottedtable & leaf_key - set dottedsuper_refpath $context_refpath - foreach normkey $dotparents { - lappend dottedsuper_refpath @@$normkey - if {![dict exists $tablenames_info $dottedsuper_refpath ttype]} { - #supertable with this combined path (context_path plus parts of dottedkey) not yet 'created' - if {[tomlish::dict::path::exists $datastructure $dottedsuper_refpath]} { - #There is data so it must have been created as a keyval - set msg "Path $dottedsuper_refpath for dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - #dict set tablenames_info $dottedsuper_refpath ttype unknown_table ;#REVIEW - dict set tablenames_info $dottedsuper_refpath ttype unknown_dotted ;#REVIEW - - #see note above re dotted keys insert into already defined table - we need to 'define' all the dotted supers in this block - lappend dottedtables_defined $dottedsuper_refpath - - #ensure empty tables are still represented in the datastructure - tomlish::dict::path::setleaf datastructure $dottedsuper_refpath {} 0;#set to empty subdict - } else { - #added for fixed assumption - set ttype [dict get $tablenames_info $dottedsuper_refpath ttype] - set definedstate [dictn getdef $tablenames_info [list $dottedsuper_refpath tdefined] NULL] - switch -- $ttype { - dottedkey_table - unknown_dotted { - #'created' as dotted - but make sure it's from this header section - i.e defined not set - if {$definedstate ne "NULL"} { - #collision with some other dottedkey - set msg "Table at $dottedsuper_refpath represented by dottedkey $dottedkeyname has been 'defined' elsewhere (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - itable { - #itables are immediately defined - set msg "Table at $dottedsuper_refpath represented by dottedkey $dottedkeyname has been 'defined' as itable (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - default { - #header_table, header_tablearray or unknown_header - #is header_tablearray any different from header_table in this context? - #we don't set tdefined for tablearray anyway - so should be ok here. - if {$definedstate ne "NULL"} { - set msg "Table at $dottedsuper_refpath represented by dottedkey $dottedkeyname has been 'defined' in a header (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - } - } - } - - #dottedtable being 2nd last segment was for original assumption - todo - tidy up? we are duplicating the logic above - #review - any need/advantage to treat 2nd to last key any different from other supers? ie D in a.b.c.D.key=1 - #no need for 'unknown_dotted' vs 'dottedkey_table' ?? - if {[llength $dottedtable_bag] == 1} { - set dottedtable [lindex $dottedtable_bag 0] - set dottedkey_refpath [list {*}$dottedsuper_refpath "@@$dottedtable"] - #our dotted key is attempting to define a table - if {![dict exists $tablenames_info $dottedkey_refpath ttype]} { - #first one - but check datastructure for collisions - if {[tomlish::dict::path::exists $datastructure $dottedkey_refpath]} { - set msg "Path $dottedkey_refpath for dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #'create' the table - dict set tablenames_info $dottedkey_refpath ttype dottedkey_table - #don't actually set 'defined' here.. use the end of TABLE record to close them off by looking at this list - tomlish::dict::path::setleaf datastructure $dottedkey_refpath {} 0 - lappend dottedtables_defined $dottedkey_refpath - - # - } else { - #exists - but might be from another dottedkey within the current header section - #the table is open for adding keys until the next 'header' section ([tablename] / [[tablearray]]) - #check for 'defined' closed (or just existence) - set ttype [dict get $tablenames_info $dottedkey_refpath ttype] - set definedstate [dictn getdef $tablenames_info [list $dottedkey_refpath tdefined] NULL] - switch -- $ttype { - dottedkey_table - unknown_dotted { - #'created' as dotted - but make sure it's from this header section - i.e defined not set - if {$definedstate ne "NULL"} { - #collision with some other dottedkey - set msg "Table at $dottedkey_refpath represented by dottedkey $dottedkeyname has been 'defined' elsewhere (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - itable { - #itables are immediately defined - set msg "Table at $dottedkey_refpath represented by dottedkey $dottedkeyname has been 'defined' as itable (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - default { - #header_table, header_tablearray or unknown_header - #is header_tablearray any different from header_table in this context? - #we don't set tdefined for tablearray anyway - so should be ok here. - if {$definedstate ne "NULL"} { - set msg "Table at $dottedkey_refpath represented by dottedkey $dottedkeyname has been 'defined' in a header (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - } - } - } else { - set dottedkey_refpath $dottedsuper_refpath - } - #assert - dottedkey represents a key val pair that can be added - - - set fullkey_refpath [list {*}$dottedkey_refpath @@$leaf_key] - if {[tomlish::dict::path::exists $datastructure $fullkey_refpath]} { - set msg "Duplicate key. The key (path $fullkey_refpath) already exists at this level in the toml data. The toml data is not valid." - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - - #set keyval_dict [_get_keyval_value $element] - lassign [_get_keyval_value $element] _ keyval_dict _ sub_tablenames_info - - - #keyval_dict is either a {type value } - #or the result from parsing an arbitrary dict from an inline table - which could theoretically look the same at the topmost level - #punk::dict::is_typeval can distinguish - tomlish::log::debug "_process_tomlish_dottedkey>>> context:$context_refpath dottedkey $dottedkeyname kv: $keyval_dict" - tomlish::dict::path::setleaf datastructure $fullkey_refpath $keyval_dict 0 - - #remove ? - #if {![tomlish::dict::is_typeval $keyval_dict]} { - # #the value is either empty or or a dict structure with arbitrary (from-user-data) toplevel keys - # # inner structure will contain {type value } if all leaves are not empty ITABLES - # ##set tkey [list {*}$norm_segments {*}$all_dotted_keys] - - # #by not creating a tablenames_info record - we effectively make it closed anyway? - # #it should be detected as a key - # #is there any need to store tablenames_info for it?? - # #REVIEW - - # ##TODO - update? - # #dictn incr tablenames_info [list $tkey seencount] - # ##if the keyval_dict is not a simple type x value y - then it's an inline table ? - # ##if so - we should add the path to the leaf_key as a closed table too - as it's not allowed to have more entries added. - # #dictn set tablenames_info [list $tkey closed] 1 - #} - return [dict create dottedtables_defined $dottedtables_defined] - } - - - #tomlish::dict::from_tomlish is a *basic* programmatic datastructure for accessing the data. - # produce a dictionary of keys and values from a tomlish tagged list. - # ---------------------------------------------------------------- - # NOTE: - # can instead produce a list if passed an ARRAY at toplevel - # can produce a single value if passed a MULTISTRING or MULTILIST at toplevel - # These are fragments of tomlish used in recursive calls. - # Such fragments don't represent valid tomlish that can be converted to a toml doc. - # ---------------------------------------------------------------- - # dict::from_tomlish is primarily for read access to toml data. - #Extraneous (not within quoted sections) whitespace and comments are not preserved in this structure, - # so a roundtrip from toml to this datastructure and back to toml will lose whitespace formatting and comments. - # creating/changing toml values can be done directly on a tomlish list if preserving (or adding) formatting/comments is desired. - #A separate package 'tomlish::object' may be needed to allow easier programmatic creating/updating/deleting of data elements whilst preserving (or adding or selectively deleting/editing) such formatting. - # - - #within an ARRAY, we store a list of items such as plain dicts (possibly empty) and {type value } for simple types - #(ARRAYS can be mixed type) - #This means our dict structure should have only ARRAY and simple types which need to be in {type value } form - #A dict within an array encodeded as a type ITABLE value should also parse - but is the unpreferred form - REVIEW test? - - #Namespacing? - #ie note the difference: - #[Data] - #temp = { cpu = 79.5, case = 72.0} - # versus - #[Data] - #temps = [{cpu = 79.5, case = 72.0}] - proc from_tomlish {tomlish {returnextra 0}} { - package require dictn - - #keep track of which tablenames have already been directly defined, - # so we can raise an error to satisfy the toml rule: 'You cannot define any key or table more than once. Doing so is invalid' - #Note that [a] and then [a.b] is ok if there are no subkey conflicts - so we are only tracking complete tablenames here. - #we don't error out just because a previous tablename segment has already appeared. - - #Declaring, Creating, and Defining Tables - #https://github.com/toml-lang/toml/issues/795 - #(update - only Creating and Defining are relevant terminology) - - #review - #tablenames_info keys ttype created, tdefined, createdby, definedby, closedby ??? review keys - # [tname] = header_table [[tname]] = header_tablearray - - #consider the following 2 which are legal: - #[table] #'table' created, defined=open type header_table - #x.y = 3 - #[table.x.z] #'table' tdefined=closed closedby={header_table table.x.z}, 'table.x' created, 'table.x.z' created tdefined=open tdefinedby={header_table table.x.z} - #k= 22 - # #'table.x.z' tdefined=closed closedby={eof eof} - - #equivalent datastructure - - #[table] #'table' created, tdefined=open definedby={header_table table} - #[table.x] #'table' tdefined=closed closedby={header_table table.x}, 'table.x' created tdefined=open definedby={header_table table.x} - #y = 3 - #[table.x.z] #'table.x' tdefined=closed closedby={header_table table.x.z}, 'table.x.z' created tdefined=open definedby={header_table table.x.z} - #k=22 - - #illegal - #[table] #'table' created and tdefined=open - #x.y = 3 #'table.x' created first keyval pair tdefined=open definedby={keyval x.y = 3} - #[table.x.y.z] #'table' tdefined=closed, 'table.x' closed because parent 'table' closed?, 'table.x.y' cannot be created - #k = 22 - # - ## - we would fail on encountering table.x.y because only table and table.x are effectively tables - but that table.x is closed should be detected (?) - - #illegal - #[table] - #x.y = {p=3} - #[table.x.y.z] - #k = 22 - ## we should fail because y is an inline table which is closed to further entries - - #note: it is not safe to compare normalized tablenames using join! - # e.g a.'b.c'.d is not the same as a.b.c.d - # instead compare {a b.c d} with {a b c d} - # Here is an example where the number of keys is the same, but they must be compared as a list, not a joined string. - #'a.b'.'c.d.e' vs 'a.b.c'.'d.e' - #we need to normalize the tablenames seen so that {"x\ty"} matches {"xy"} - - - - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keyed on tablepath each of which is an @@path such as {@@config @@subgroup @@etc} (corresponding to config.subgroup.etc) - #also has non @@ indexes which are list indexes as taken by tcl list commands (int or end-1 etc) - #value is a dict with keys: ttype, tdefined - } - - if {![string is list $tomlish]} { - error "tomlish::dict::from_tomlish Supplied value for tomlish does not appear to be a tomlish list. Use tomlish::from_toml to get a tomlish list from toml." - } - - - log::info "---> dict::from_tomlish processing '$tomlish'<<<" - set items $tomlish - - foreach lst $items { - if {[lindex $lst 0] ni $::tomlish::tags} { - error "tomlish::dict::from_tomlish supplied list does not appear to be toml parsed into a tomlish tagged list. Run tomlish::decode::toml on the raw toml data to produce a tomlish list" - } - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - #ignore TOMLISH tag at beginning - set items [lrange $tomlish 1 end] - } - - set datastructure [dict create] - set dottedtables_defined [list] - foreach item $items { - set tag [lindex $item 0] - #puts "...> item:'$item' tag:'$tag'" - switch -exact -- $tag { - KEY - DQKEY - SQKEY - INT - FLOAT - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL - STRING - LITERAL { - #we don't require invalid tomlish fragments with these keys in our direct recursion - #(we do support ARRAY, MULTISTING, and MULTILITERAL tomlish fragments below) - error "tomlish::dict::from_tomlish error: invalid tag: $tag. At the toplevel, from_tomlish can only process WS NEWLINE COMMENT and compound elements DOTTEDKEY TABLE TABLEARRAY ITABLE MULTILITERAL MULTISTRING" - } - DOTTEDKEY { - #toplevel dotted key empty context_refpath - set dkinfo [_process_tomlish_dottedkey $item {}] - lappend dottedtables_defined {*}[dict get $dkinfo dottedtables_defined] - #at any level - we don't expect any more DOTTEDKEY records in a tomlish structure after TABLE or TABLEARRAY are encountered - #as those records should encapsulate their own dottedkeys - - } - TABLEARRAY { - #close off any dottedtables_defined created by dottedkeys at this level - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - set dottedtables_defined [list] ;#for closing off at end by setting 'defined' - - set tablearrayname [lindex $item 1] - tomlish::log::debug "---> tomlish::dict::from_tomlish processing item TABLENAME (name: $tablearrayname): $item" - set norm_segments [::tomlish::toml::tablename_split $tablearrayname true] ;#true to normalize - #we expect repeated tablearray entries - each adding a sub-object to the value, which is an array/list. - #tablearrayname is likely to appear multiple times - so unlike a TABLE we don't check for 'defined' for the full name as an indicator of a problem - set supertable [list] - ############## - # [[a.b.c.d]] - # norm_segments = {a b c d} - #check a {a b} {a b c} <---- supertables of a.b.c.d - ############## - set refpath [list] ;#e.g @@j1 @@j2 1 @@k1 end - foreach normseg [lrange $norm_segments 0 end-1] { - lappend supertable $normseg - lappend refpath @@$normseg - if {![dict exists $tablenames_info $refpath ttype]} { - #supertable with this path doesn't yet exist - if {[tomlish::dict::path::exists $datastructure $refpath]} { - #There is data though - so it must have been created as a keyval - set msg "Supertable [join $supertable .] of tablearray name $tablearrayname already has data but doesn't appear to be a table - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablearray_supertable_keycollision - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } else { - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - #review - we can't later specify as tablearray so should just set ttype to header_table even though it's being created - # because of a tablearray header? - #By setting ttype to something other than table_header we can provide more precise errorCode/msg ?? - dict set tablenames_info $refpath ttype unknown_header - #ensure empty tables are still represented in the datastructure - dict set datastructure {*}$supertable [list] - } - } else { - #REVIEW!! - # what happens with from_toml {[[a.b.c]]} {[[a.b]]} ??? - #presumed that a and a.b were 'created' as tables (supertables of tablearray at a.b.c) and can't now be redefined as tablearrays - - #supertable has already been created - and may be defined - but even if defined we can add subtables unless it is of type itable - #but if it's a tablearray - we need to point to the most 'recently defined table element of the array' - #(last member of that array - need to check type? allowed to have non-table elements ie nonhomogenous??) - set supertype [dict get $tablenames_info $refpath ttype] - if {$supertype eq "header_tablearray"} { - #exercised by toml-tests: - # valid/table/array-table-array - # valid/table/array-nest - - #puts stdout "todict!!! TABLEARRAY nesting required for supertable [join $supertable .]" - - #'refer' to the appropriate element in existing array - set arrdata [tomlish::dict::path::get $datastructure [list {*}$refpath @@value]] - set idx [expr {[llength $arrdata]-1}] - if {$idx < 0} { - #existing tablearray should have at least one entry even if empty (review) - set msg "reference to empty tablearray?" - return -code error -errorcode {TOMLISH STRUCTURE REFTOEMPTYTABLEARRAY} $msg - } - lappend refpath $idx - } - } - } - # - #puts "TABLE supertable refpath $refpath" - lappend refpath @@[lindex $norm_segments end] - tomlish::log::debug "TABLEARRAY refpath $refpath" - set tablearray_refpath $refpath - - - if {![dict exists $tablenames_info $tablearray_refpath ttype]} { - #first encounter of this tablearrayname - if {[tomlish::dict::path::exists $datastructure $tablearray_refpath]} { - #e.g from_toml {a=1} {[[a]]} - set msg "Cannot create tablearray name $tablearrayname. Key already has data but key doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablearray_direct_keycollision_error - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #no collision - we can create the tablearray and the array in the datastructure - dict set tablenames_info $tablearray_refpath ttype header_tablearray - #dict set datastructure {*}$norm_segments [list type ARRAY value {}] - #create array along with empty array-item at position zero - tomlish::dict::path::setleaf datastructure $tablearray_refpath [list type ARRAY value {{}}] 0 - set arrayitem_refpath [list {*}$tablearray_refpath 0] - #set ARRAY_ELEMENTS [list] - } else { - #we have an existing tablenames_info record for this path - but is it a tablearray? - set ttype [dict get $tablenames_info $tablearray_refpath ttype] - if {$ttype ne "header_tablearray"} { - #header_table or itable - switch -- $ttype { - itable {set ttypename itable} - header_table {set ttypename table} - dottedkey_table {set ttypename dottedkey_table} - unknown_header - unknown_dotted { - #table was created e.g as supertable - but not specifically a tablearray - #violates ordering - return specific test error - set msg "Table $tablearrayname referenced as supertable before tablearray defined (ordering)" - return -code error -errorcode {TOMLISH STRUCTURE TABLEARRAYORDERING} $msg - } - default {error "unrecognised type $ttype - expected header_table or itable"} - } - set msg "tablearray name $tablearrayname already appears to be already created as '$ttypename' not tablearray - invalid?" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #EXISTING tablearray - #add to array - #error "add_to_array not implemented" - #{type ARRAY value } - #set ARRAY_ELEMENTS [dict get $datastructure {*}$norm_segments value] - tomlish::log::debug ">>>>pre-extend-array dict::from_tomlish datastructure: $datastructure" - set existing_array [tomlish::dict::path::get $datastructure [list {*}$tablearray_refpath @@value]] - set arrayitem_refpath [list {*}$tablearray_refpath [llength $existing_array]] - tomlish::dict::path::lappend datastructure $tablearray_refpath {} - tomlish::log::debug ">>>>post-extend-array dict::from_tomlish datastructure: $datastructure" - } - - - #set object [dict create] ;#array context equivalent of 'datastructure' - - #add to ARRAY_ELEMENTS and write back in to datastructure. - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - tomlish::log::debug "----> todict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkinfo [_process_tomlish_dottedkey $element $arrayitem_refpath] - lappend dottedtables_defined {*}[dict get $dkinfo dottedtables_defined] - } - NEWLINE - COMMENT - WS { - #ignore - } - TABLE { - #we *perhaps* should be able to process tablearray subtables either as part of the tablearray record, or independently. - #(or even a mixture of both, although that is somewhat an edge case, and of limited utility) - #[[fruit]] - #x=1 - # [fruit.metadata] - # [fruit.otherdata] - - #when processing a dict destined for the above - the tomlish generator (e.g from_dict) - #should create as 1 or 3 records (but could create 2 records if there was an unrelated table in between the subtables) - #choices: all in tablearray record, tablearray + 1 or 2 table records. - # - #We are going the other way here - so we just need to realise that the list of tables 'belonging' to this tablearray might not be complete. - # - #the subtable names must be prefixed with the tablearray - we should validate that for any contained TABLE records - - #The default mechanism is for from_dict to produce tomlish with separate TABLE records - and use the ordering to determine membership - #If we were to support wrapping the TABLE records within a TABLEARRAY - we should also support TABLEARRAY within TABLEARRAY - # ----------------------------------------------------------------------- - #Implementing this is not critical for standard encoding/decoding of toml! - #It would be an alternative form for the tomlish intermediate form - and adds complexity. - # - #The upside would be to provide a function for sorting/rearranging in the tomlish form if all records were fully encapsulated. - #A possible downside is that unrelated tables placed before a tablearray is fully defined (within the tablearray definition area in toml) - # would have to be re-positioned before or after the encapsulated tablearray record. - # While unrelated tables in such a position aren't a recommended way to write toml, they appear to be valid - # and preserving the author's ordering is a goal of the basic encoding/decoding operations if no explicit sorting/reordering was requested. - # - #Consider an 'encapsulate' method to this (tomlish -> tomlish) - # ----------------------------------------------------------------------- - #todo - error "tomlish::dict::from_tomlish TABLE element within TABLEARRAY not handled - TABLE should be a separate tomlish record" - } - default { - error "tomlish::dict::from_tomlish Sub element of type '$type' not understood in tablearray context. Expected only DOTTEDKEY,NEWLINE,COMMENT,WS" - } - } - } - - #end of TABLEARRAY record - equivalent of EOF or next header - close off the dottedtables - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - } - TABLE { - #close off any dottedtables_defined created by dottedkeys at this level - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - set tablename [lindex $item 1] - set dottedtables_defined [list] ;#for closing off at end by setting 'defined' - #As our TABLE record contains all it's child DOTTEDKEY records - this should be equivalent to setting them as defined at EOF or next header. - - #----------------------------------------------------------------------------------- - #default assumption - our reference is to the main tablenames_info and datastructure - #Will need to append keys appropriately if we have recursed - #----------------------------------------------------------------------------------- - - log::debug "---> tomlish::dict::from_tomlish processing item TABLE (name: $tablename): $item" - set norm_segments [::tomlish::toml::tablename_split $tablename true] ;#true to normalize - - - - set name_segments [::tomlish::toml::tablename_split $tablename 0] ;#unnormalized e.g ['a'."b".c.d] -> 'a' "b" c d - #results of tablename_split 0 are 'raw' - ie some segments may be enclosed in single or double quotes. - - - set supertable [list] - ############## - # [a.b.c.d] - # norm_segments = {a b c d} - #check a {a b} {a b c} <---- supertables of a.b.c.d - ############## - - ############## - #[[a]] - #[a.b] #supertable a is tablearray - ############## - - #also consider - ############## - # [[a.b]] - # [a.b.c.d] #supertable a is a table, supertable a.b is tablearray, supertable a.b.c is elementtable - ############## - set refpath [list] ;#e.g @@j1 @@j2 1 @@k1 end - foreach normseg [lrange $norm_segments 0 end-1] { - lappend supertable $normseg - lappend refpath @@$normseg - if {![dict exists $tablenames_info $refpath ttype]} { - #supertable with this path doesn't yet exist - if {[tomlish::dict::path::exists $datastructure $refpath]} { - #There is data though - so it must have been created as a keyval - set msg "Supertable [join $supertable .] of table name $tablename (path $refpath) already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - #we also don't know whether it's a table or a dottedkey_table (not allowed to be tablearray - out of order?) - dict set tablenames_info $refpath ttype unknown_header - #ensure empty tables are still represented in the datastructure - #dict set datastructure {*}$supertable [list] - tomlish::dict::path::setleaf datastructure $refpath {} 0 - } else { - #supertable has already been created - and may be defined - but even if defined we can add subtables unless it is of type itable - if {[dict get $tablenames_info $refpath ttype] eq "header_tablearray"} { - #'refer' to the appropriate element in existing array - set arrdata [tomlish::dict::path::get $datastructure [list {*}$refpath @@value]] - set idx [expr {[llength $arrdata]-1}] - if {$idx < 0} { - #existing tablearray should have at least one entry even if empty (review) - set msg "reference to empty tablearray?" - return -code error -errorcode {TOMLISH STRUCTURE REFTOEMPTYTABLEARRAY} $msg - } - lappend refpath $idx - } else { - #?? - if {[dictn getdef $tablenames_info [list $refpath tdefined] NULL] eq "NULL"} { - } else { - } - } - } - } - #puts "TABLE supertable refpath $refpath" - lappend refpath @@[lindex $norm_segments end] - tomlish::log::info "TABLE refpath $refpath" - set table_refpath $refpath - - - - - #table [a.b.c.d] hasn't been defined - but may have been 'created' already by a longer tablename - # - or may have existing data from a keyval - if {![dict exists $tablenames_info $table_refpath ttype]} { - if {[tomlish::dict::path::exists $datastructure $table_refpath]} { - #e.g from_toml {a=1} {[a]} - set msg "Cannot create table name $tablename. Key already has data but key doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablename_keyval_collision_error - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #no data or previously created table - dict set tablenames_info $table_refpath ttype header_table - - #We are 'defining' this table's keys and values here (even if empty) - #dict set datastructure {*}$norm_segments [list] ;#ensure table still represented in datastructure even if we add no keyvals here - tomlish::dict::path::setleaf datastructure $table_refpath {} 0;#ensure table still represented in datastructure even if we add no keyvals here - } else { - if {[dict get $tablenames_info $table_refpath ttype] eq "header_tablearray"} { - #e.g tomltest invalid/table/duplicate-table-array2 - #[[tbl]] - #[tbl] - set msg "Table name $tablename has already been created as a tablearray. Invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } else { - #any other type tdefined is a problem - set T_DEFINED [dictn getdef $tablenames_info [list $table_refpath tdefined] NULL] - if {$T_DEFINED ne "NULL" } { - #our tablename e.g [a.b.c.d] declares a space to 'define' subkeys - but there has already been a definition space for this path - set msg "Table name $tablename has already been defined in the toml data. Invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - } - dict set tablenames_info $table_refpath tdefined open - - #now add the contained elements - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - log::debug "----> todict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkinfo [_process_tomlish_dottedkey $element $table_refpath] - lappend dottedtables_defined {*}[dict get $dkinfo dottedtables_defined] - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in table context. Expected only DOTTEDKEY,NEWLINE,COMMENT,WS" - } - } - } - - #end of TABLE record - equivalent of EOF or next header - close off the dottedtables - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - } - ITABLE { - #As there is no other mechanism to create tables within an ITABLE than dottedkeys - # and ITABLES are fully defined/enclosed - we can rely on key collision and don't need to track dottedtables_defined - REVIEW. - set dottedtables_defined [list] - #SEP??? - #ITABLE only ever on RHS of = or inside ARRAY - set datastructure [dict create] - set tablenames_info [dict create] - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkinfo [_process_tomlish_dottedkey $element] - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in ITABLE context. Expected only KEY,DQKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - } - ARRAY { - #invalid at toplevel of a 'complete' tomlish structure - but we support it here for recursive fragment processing - #arrays in toml are allowed to contain mixtures of types - set datastructure [list] - log::debug "--> processing array: $item" - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - STRING { - #JJJJ - #don't unescape string! - set value [lindex $element 1] - #lappend datastructure [list type $type value [::tomlish::utils::unescape_string $value]] - lappend datastructure [list type $type value $value] - } - LITERAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - ITABLE { - #anonymous table - #lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - lappend datastructure [::tomlish::dict::from_tomlish [list $element]] ;#store itables within arrays as raw dicts (possibly empty) - } - TABLE - TABLEARRAY { - #invalid? shouldn't be output from from_dict - but could manually be constructed as such? review - #doesn't make sense as table needs a name? - #take as synonym for ITABLE? - error "tomlish::dict::from_tomlish $type within array unexpected" - } - ARRAY - MULTISTRING - MULTILITERAL { - #set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::dict::from_tomlish [list $element]]] - } - WS - SEP - NEWLINE - COMMENT { - #ignore whitespace, commas, newlines and comments - } - default { - error "tomlish::dict::from_tomlish Unexpected value type '$type' found in array" - } - } - } - } - MULTILITERAL { - #Not for toplevel of complete tomlish - (recursive fragment processing) - - #triple squoted string - #first newline stripped only if it is the very first element - #(ie *immediately* following the opening delims) - #All whitespace other than newlines is within LITERALPARTS - # ------------------------------------------------------------------------- - #todo - consider extension to toml to allow indent-aware multiline literals - # how - propose as issue in toml github? Use different delim? e.g ^^^ ? - #e.g - # xxx=?'''abc - # def - # etc - # ''' - # - we would like to trimleft each line to the column following the opening delim - # ------------------------------------------------------------------------- - - log::debug "---> todict processing multiliteral: $item" - set parts [lrange $item 1 end] - if {[lindex $parts 0 0] eq "NEWLINE"} { - set parts [lrange $parts 1 end] ;#skip it - } - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - switch -exact -- $type { - LITERALPART { - append stringvalue [lindex $element 1] - } - NEWLINE { - set val [lindex $element 1] - if {$val eq "lf"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - MULTISTRING { - #Not for toplevel of complete tomlish - (recursive fragment processing) - #triple dquoted string - log::debug "---> tomlish::dict::from_tomlish processing multistring: $item" - set stringvalue "" - set idx 0 - set parts [lrange $item 1 end] - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - #We use STRINGPART in the tomlish representation as a distinct element to STRING - which would imply wrapping quotes to be reinserted - switch -exact -- $type { - STRING { - #todo - do away with STRING ? - #we don't build MULTISTRINGS containing STRING - but should we accept it? - tomlish::log::warn "double quoting a STRING found in MULTISTRING - should be STRINGPART?" - #append stringvalue "\"[::tomlish::utils::unescape_string [lindex $element 1]]\"" - append stringvalue "\"[lindex $element 1]\"" - } - STRINGPART { - #JJJ - #don't unescape string - #append stringvalue [::tomlish::utils::unescape_string [lindex $element 1]] - append stringvalue [lindex $element 1] - } - CONT { - #When the last non-whitespace character on a line is an unescaped backslash, - #it will be trimmed along with all whitespace (including newlines) up to the next non-whitespace character or closing delimiter - # review - we allow some whitespace in stringpart elements - can a stringpart ever be all whitespace? - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last (or first and only) line - return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} "Invalid whitespace escape - not a valid continuation position" - #set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - #if {$non_ws >= 0} { - # #append stringvalue "\\" - # return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} "Invalid whitespace escape - not a valid continuation position" - #} else { - # #skip over ws without emitting - # set idx [llength $parts] - #} - } else { - set parts_til_nl [lrange $parts 0 $next_nl-1] - set non_ws [lsearch -index 0 -start $idx+1 -not $parts_til_nl WS] - if {$non_ws >= 0} { - #This CONT is invalid. If there had been a non-whitespace char directly following it, - #it wouldn't have come through as a CONT token - #Now that we see it isn't the last non-whitespace backslash on the line we can reject - # as an invalid escape of space or tab - #append stringvalue "\\" - return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} "Invalid whitespace escape - not a valid continuation position" - } else { - #skip over ws on this line - set idx $next_nl - #then have to check each subsequent line until we get to first non-whitespace - set trimming 1 - while {$trimming && $idx < [llength $parts]} { - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - } else { - set idx [llength $parts] - } - set trimming 0 - } else { - set non_ws [lsearch -index 0 -start $idx+1 -not [lrange $parts 0 $next_nl-1] WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - set trimming 0 - } else { - set idx $next_nl - #keep trimming - } - } - } - } - } - } - NEWLINE { - #if newline is first element - it is not part of the data of a multistring - if {$idx > 0} { - set val [lindex $element 1] - if {$val eq "lf"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - } - WS { - append stringvalue [lindex $element 1] - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - WS - COMMENT - NEWLINE { - #ignore - } - BOM { - #this token is the unicode single char \uFFEF - #It doesn't tell us what encoding was originally used (though toml should only accept UTF-8 files) - #ignore at start - what about in other positions? - } - default { - error "Unexpected tag '$tag' in Tomlish list '$tomlish'" - } - } - } - if {!$returnextra} { - return $datastructure - } else { - return [dict create datastructure $datastructure tablenames_info $tablenames_info] - } - } -} -namespace eval tomlish::path { - namespace export {[a-z]*}; # Convention: export all lowercase - - set test_tomlish [tomlish::from_toml { } #comment {z=1} {x.y=2 #xy2} {[[shop.product]] #product1} {x=[ #array1} {11 #val1} {, 12 #val2} {]} {[unrelated.' etc ']} {a.b={c=666}} {a.x={}} {[[shop.product]]} {x="test"} {[shop]} {name="myshop"}] - - proc get {tomlish {path {}}} { - if {$path eq ""} { - return $tomlish - } - if {[string index $path 0] in [list . "\["]} { - set path [tomlish::utils::jq_to_path $path] - } - - #at the cost of some performance, sanity check that the tomlish is valid - if {[catch {tomlish::to_dict $tomlish} d]} { - error "tomlish::path::get error supplied tomlish is malformed\nerrmsg: $d" - } - #since we have the dict - test the path is valid - if {![tomlish::dict::path::exists $d $path]} { - error "tomlish::path::get - path \"$path\" not found in tomlish $tomlish" - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - set tomlish [lrange $tomlish 1 end] - } - ::set pathsofar [list] - ::set tomlitems [list] ;#reducing set. 2 element list {keypath itemlist} - foreach record $tomlish { - lappend tomlitems [list {} [list $record]] ;#root records - } - - ::set dictsubpath [list] ;#reset at every index encounter? - foreach p $path { - ::lappend pathsofar $p - set sublist [list] - if {[string range $p 0 1] eq "@@"} { - set realsearchkey [string range $p 2 end] - lappend dictsubpath $realsearchkey - foreach path_items $tomlitems { - lassign $path_items subpath tlist - lappend subpath $realsearchkey - foreach item $tlist { - set tp [lindex $item 0] - switch -- $tp { - WS - NEWLINE - COMMENT { - } - DOTTEDKEY { - #can occur at toplevel (before others) or within other elements - set keyinfo [tomlish::get_dottedkey_info $item] - set keys_raw [dict get $keyinfo keys_raw] - puts stderr "subpath:$subpath -->DOTTEDKEY keys_raw: $keys_raw" - #may not be enough keys_raw for subpath - but there could be further ITABLES to continue the dict further - set prefixparts [lrange $keys_raw 0 [llength $subpath]-1] - set is_kmatch 1 ;#default assumption only - foreach dsub $subpath kpart $prefixparts { - if {$dsub ne $kpart} { - set is_kmatch 0 - } - } - if {$is_kmatch} { - if {[llength $keys_raw] == [llength $subpath]} { - set subpath [list] - #e.g {DOTTEDKEY {{KEY xxx}} = {WS { }} {STRING blah}} - lappend sublist [list $subpath [lrange $item 3 end]] - } else { - lappend sublist [list $subpath [list $item]] - } - } - } - ITABLE { - #subelement only - set itablechildren [lrange $item 1 end] - puts stderr "subpath:$subpath -->ITABLE records: $itablechildren" - set nextpath [lmap v $subpath {string cat @@ $v}] - set results [tomlish::path::get $itablechildren $nextpath] - set subpath [list] - puts "--> lappending [list $subpath $results]" - lappend sublist [list $subpath $results] - } - TABLEARRAY { - #toplevel only - set fulltablename [lindex $item 1] - set normalise 1 - set tparts [tomlish::toml::tablename_split $fulltablename $normalise] - if {[llength $tparts] < [llength $subpath]} {continue} ;#not enough parts to satisfy current subpath query - set prefixparts [lrange $tparts 0 [llength $subpath]-1] - set is_tmatch 1 ;#default assumption only - foreach dsub $subpath tpart $prefixparts { - if {$dsub ne $tpart} { - set is_tmatch 0 - } - } - #TODO reference arrays - if {$is_tmatch} { - if {[llength $tparts] == [llength $subpath]} { - set subpath [list] - lappend sublist [list $subpath [lrange $item 2 end]] - } else { - #TODO - set subpath 0 - lappend sublist [list $subpath [list $item]] ;#add entire TABLE line - } - } - } - TABLE { - #toplevel only - set fulltablename [lindex $item 1] - set normalise 1 - set tparts [tomlish::toml::tablename_split $fulltablename $normalise] - if {[llength $tparts] < [llength $subpath]} {continue} ;#not enough parts to satisfy current subpath query - set prefixparts [lrange $tparts 0 [llength $subpath]-1] - set is_tmatch 1 ;#default assumption only - foreach dsub $subpath tpart $prefixparts { - if {$dsub ne $tpart} { - set is_tmatch 0 - } - } - if {$is_tmatch} { - if {[llength $tparts] == [llength $subpath]} { - set subpath [list] - lappend sublist [list $subpath [lrange $item 2 end]] - } else { - #leave subpath - lappend sublist [list $subpath [list $item]] ;#add entire TABLE line - } - } - } - ARRAY { - #subelement only - } - - } - } - } - } else { - #index - #will never occur at toplevel (dict::path::exists already ruled it out) - foreach path_items $toml_items { - lassign $path_items subpath $tlist - set tp [lindex $tlist 0] - switch -- $tp { - ARRAY { - } - } - } - } - #temp - puts stdout "pathsofar: $pathsofar" - puts stdout [punk::lib::showdict -roottype list $sublist] - set tomlitems $sublist - } - - #REVIEW - if {[llength $tomlitems] == 1} { - return [lindex $tomlitems 0 1] - } - set result [list] - foreach i $tomlitems { - lappend result [lindex $i 1] - } - return $result - #return [lindex $tomlitems 1] - } - -} - -namespace eval tomlish::dict::path { - - #access tomlish dict structure - namespace export {[a-z]*}; # Convention: export all lowercase - - #access with path such as: @@k @@k 0 @@k end where dict keys marked with @@ and plain values are list indices into in {type ARRAY value } - #leaf elements returned as structured {type value } - - proc get {dictval {path {} } } { - if {$path eq ""} { - return $dictval - } - if {[string index $path 0] in [list . "\[" ] } { - set path [tomlish::utils::jq_to_path $path] - } - - ::set data $dictval - ::set pathsofar [list] - ::set i 0 - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@" } { - #dict key - ::set data [dict get $data [string range $p 2 end]] - } else { - #ARRAY or raw list index - if {[llength $pathsofar] > 1 && [string trim [lindex $pathsofar $i-1]] eq ""} { - #previous path was query for entire list - result is a raw list, not a dict - if {[string trim $p] eq ""} { - #review - multiple {[]} in a row in the path is pretty suspicious - raise error - error "tomlish::dict::path::get error - multiple empty indices in a row not supported" - } - ::set data [lindex $data $p] - } else { - if {![tomlish::dict::is_typeval $data]} { - error "tomlish::dict::path::get error bad path $path. Attempt to access table or other value as array at subpath $pathsofar." - } - if {[dict get $data type] ne "ARRAY"} { - error "tomlish::dict::get error bad path $path. Subpath $pathsofar is not an array." - } - ::set arrdata [dict get $data value] - #when $p is empty string (or whitespace) - lindex returns entire list (or empty list) - # - this corresponds to jq: {[]} or path {""} - ::set data [lindex $arrdata $p] - } - } - incr i - } - return $data - } - - - proc exists {dictval path} { - #completely empty path considered to exist - review - if {[string index $path 0] in [list . "\[" ] } { - set path [tomlish::utils::jq_to_path $path] - } - ::set data $dictval - ::set pathsofar [list] - ::set exists 1 - ::set i 0 - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@"} { - #dict key - ::set k [string range $p 2 end] - if {![dict exists $data $k]} { - return 0 - } - ::set data [dict get $data $k] - } else { - #ARRAY or raw list index - if {[llength $pathsofar] > 1 && [string trim [lindex $pathsofar $i-1]] eq ""} { - #previous path was query for entire list - result is not a dict - if {[string trim $p] eq ""} { - #review - multiple {[]} in a row in the path is pretty suspicious - raise error - error "tomlish::dict::path::exists error - multiple empty indices in a row not supported" - #or just leave data as is? - } else { - ::set intp [tomlish::system::lindex_resolve_basic $data $p] - if {$intp == -1} { - return 0 - } - ::set data [lindex $data $p] - } - } else { - if {![tomlish::dict::is_typeval $data]} { - return 0 - } - if {[dict get $data type] ne "ARRAY"} { - return 0 - } - #special case for empty path syntax {jq: [] path: ""} meaning retrieve all elements in list - ::set arrdata [dict get $data value] - if {[string trim $p] eq ""} { - #we have confirmed above it is an ARRAY - we consider an empty list to exist. - #UUU - ::set data $arrdata - } else { - #for 'exists' we need to avoid lindex returning empty string for out of bounds - ::set intp [tomlish::system::lindex_resolve_basic $arrdata $p] ;#handle index math (end-1 etc) - if {$intp == -1} { - #out of bounds - return 0 - } - ::set data [lindex $arrdata $p] - } - } - } - incr i - } - return $exists - } - - - #raise error for invalid - proc validate_typeval {typeval} { - set valtype [dict get $typeval type] - set rawval [dict get $typeval value] - switch -- $valtype { - INT { - if {![tomlish::utils::is_int $rawval]} { - return -code error -errorcode {TOML TYPE NOT_INT} "validate_typeval value is not a valid toml int: '$rawval'" - } - } - BOOL { - #toml only accepts lower case true and false - #review - if {$rawval ni {true false} } { - return -code error -errorcode {TOML TYPE NOT_INT} "validate_typeval value is not a valid toml boolean (true|false): '$rawval'" - } - } - FLOAT { - if {![tomlish::utils::is_float $rawval]} { - return -code error -errorcode {TOML TYPE NOT_INT} "validate_typeval value is not a valid toml float: '$rawval'" - } - } - DATETIME { - #review - accept even when more specific types apply? - if {![tomlish::utils::is_datetime]} { - return -code error -errorcode {TOML TYPE NOT_DATETIME} "validate_typeval value is not a valid toml datetime: '$rawval'" - } - } - DATETIME-LOCAL { - if {![tomlish::utils::is_datetime-local]} { - return -code error -errorcode {TOML TYPE NOT_DATETIME-LOCAL} "validate_typeval value is not a valid toml datetime-local: '$rawval'" - } - } - DATE-LOCAL { - if {![tomlish::utils::is_date-local]} { - return -code error -errorcode {TOML TYPE NOT_DATE-LOCAL} "validate_typeval value is not a valid toml date-local: '$rawval'" - } - } - TIME-LOCAL { - if {![tomlish::utils::is_time-local]} { - return -code error -errorcode {TOML TYPE NOT_TIME-LOCAL} "validate_typeval value is not a valid toml time-local: '$rawval'" - } - } - ARRAY { - if {$rawval eq ""} { - return - } - foreach el $rawval { - validate_typeval $el - } - } - STRING { - if {![tomlish::utils::inner_Bstring_is_valid_toml $rawval]} { - return -code error -errorcode {TOML TYPE NOT_BSTRING} "validate_typeval value is not a valid toml basic string: '$rawval'" - } - } - MULTISTRING { - #multistring as a single value - #UUU - if {![tomlish::utils::inner_MultiBstring_is_valid_toml $rawval]} { - return -code error -errorcode {TOML TYPE NOT_MLBSTRING} "validate_typeval value is not a valid toml multistring: '$rawval'" - } - } - LITERAL { - #todo? - } - MULTILITERAL { - #? - } - default { - return -code error -errorcode {TOML TYPE UNRECOGNISED} "validate_typeval does not recognise type '$valtype'" - } - } - } - - #a restricted analogy of 'dictn set' - #set 'leaf' values only - don't create intermediate paths - # can replace an existing dict with another dict - # can create a key when key at tail end of path is a key (ie @@keyname, not index) - # can replace an existing {type value value } - # with added restriction that if is ARRAY the new must also be ARRAY - - - # vscode tcl syntax highlighter is unable to handle (in some cases!) some simple constructs like left square bracket in curly braces, - # yet it is ok in comments. i.e {[} is prolematic for the highlighter, so we use "\[" instead :/ - #e.g ------------------------------------------------ - # if {[string index $path 0] in [list . {[}] } { - # # ... - # } - # ------------------------------------------------ - #This may highlight ok - and even text immediately following can be ok - but - # the subsequent code block at global scope, perhaps *many* lines distant from where the syntax highlighting issue started, may then be completely miscoloured - # This is a big timewaster - a decent syntax highlighter is really needed for Tcl in vscode (2025-09) - - package require struct::list - proc setleaf {dictvariable path value {validate 1}} { - if {[string index $path 0] in [list . "\[" ] } { - set path [tomlish::utils::jq_to_path $path] - } - - upvar $dictvariable dict_being_edited - if {![info exists dict_being_edited]} { - error "tomlish::dict::path::setleaf error - supplied value for 'dictvariable' doesn't seem to be the name of an existing variable" - } - ::set data $dict_being_edited - ::set pathsofar [list] - if {!([tomlish::dict::is_typeval $value] || [tomlish::dict::is_typeval_dict $value 0])} { - #failed check of supplied value as basic type, or a sub-dict structure (not checking arrays) - error "tomlish::dict::path::setleaf error - value must already be in the tomlish form {type value } or be a dict with such forms as leaves" - } - if {$validate && [tomlish::dict::is_typeval $value]} { - #validate value element of $value is correct for type element - if {[catch {validate_typeval $value} errM]} { - return -code error -errorcode {TOMLISH VALIDATION TYPEFAIL} $errM - } - } - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@"} { - ::set k [string range $p 2 end] - - # if {![dict exists $data $k]} { - # error "tomlish::dict:path::set error bad path $path. Attempt to access nonexistent element at subpath $pathsofar." - # } - ::set varname v[incr v] - - if {[struct::list equal $pathsofar $path]} { - #see if leaf of the path given already exists - if {[dict exists $data $k]} { - ::set endpoint [dict get $data $k] - if {[tomlish::dict::is_typeval $endpoint]} { - set existing_tp [dict get $endpoint type] - if {![tomlish::dict::is_typeval $value]} { - error "tomlish::dict::path::setleaf error Unable to overwrite subpath '$pathsofar' which is of type $existing_tp with sub-dict. Supplied value not {type value val } with sub-dict: $value" - } - switch -- [dict get $endpoint type] { - ARRAY { - #disallow overwriting array - unless given value is an ARRAY? REVIEW - if {[dict get $value type] ne "ARRAY"} { - error "tomlish::dict::path::setleaf error bad path '$path'. Cannot overwrite array with non-array: $value" - } - } - default { - # - } - } - } else { - #leaf is a typeval dict not a plain typeval - only allow overwrite with a typeval dict - if {![tomlish::dict::is_typeval_dict $value 0]} { - error "tomlish::dict::path::setleaf error path '$path'. Cannot overwrite sub-dict (size: [dict size $endpoint]) with non sub-dict: $value" - } - } - ::set $varname $value - dict set vdict $pathsofar $varname - break - } else { - ::set arrdata [dict get $data value] - set idx [tomlish::system::lindex_resolve_basic $arrdata $p] - if {$idx == -1} { - error "tomlish::dict::path::setleaf error bad path '$path'. No existing element at $p" - } - ::set data [lindex $arrdata $p] - ::set $varname $data - dict set vdict $pathsofar $varname - } - } - } - #dict for {path varname} $vdict { - # puts "$path $varname\n" - # puts " '[::set $varname]'\n" - # puts "" - #} - - ::set i 0 - ::set reverse [lreverse $vdict] - foreach {varname path} $reverse { - set newval [::set $varname] - if {$i+2 == [llength $reverse]} { - ::set k [lindex $path end] - ::set k [string range $k 2 end] ;#first key is always @@something - dict set dict_being_edited $k $newval - #puts "--result $dict_being_edited" - break - } - ::set nextvarname [lindex $reverse $i+2] - ::set nextval [::set $nextvarname] - ::set k [lindex $path end] - if {[string match @@* $k]} { - #dict key - #dict set $nextvarname $k $newval - setleaf $nextvarname [list $k] $newval 0 - } else { - #list index - ::set nextarr [dict get $nextval value] - ::lset nextarr $k $newval - dict set $nextvarname value $nextarr - } - ::incr i 2 - } - - return $dict_being_edited - - } - #path must be to a {type ARRAY value } - #REVIEW - how to lappend to deep mixed dict/array structure without rewriting whole datastructure? - proc lappend {dictvariable path args} { - if {[string index $path 0] in [list . "\[" ]} { - set path [tomlish::utils::jq_to_path $path] - } - upvar $dictvariable dict_being_edited - ::set data $dict_being_edited - ::set pathsofar [list] - #::set newlist [list] - ::set v 0 - ::set vdict [dict create] - foreach a $args { - if {![::tomlish::utils::string_is_dict $a]} { - error "tomlish::dict::path::lappend error - lappended arguments must already be in the tomlish form {type value } or be a dict with such forms as leaves" - } - } - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@"} { - ::set k [string range $p 2 end] - if {![dict exists $data $k]} { - error "tomlish::dict::path::lappend error bad path $path. Attempt to access nonexistent element at subpath $pathsofar." - } - ::set varname v[incr v] - - if {[struct::list equal $pathsofar $path]} { - #see if endpoint of the path given is an ARRAY - ::set endpoint [dict get $data $k] - if {![tomlish::dict::is_typeval $endpoint]} { - error "tomlish::dict::path::lappend error bad path $path. Attempt to access table as array at subpath $pathsofar." - } - if {[dict get $endpoint type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Subpath $pathsofar is not an array." - } - ::set arrdata [dict get $endpoint value] - ::lappend arrdata {*}$args - dict set endpoint value $arrdata - ::set newlist $endpoint - ::set $varname $newlist - dict set vdict $pathsofar $varname - break - } - ::set data [dict get $data $k] - ::set $varname $data - dict set vdict $pathsofar $varname - } else { - if {![tomlish::dict::is_typeval $data]} { - error "tomlish::dict::path::lappend error bad path $path. Attempt to access table as array at subpath $pathsofar." - } - if {[dict get $data type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Subpath $pathsofar is not an array." - } - ::set varname v[incr v] - if {[struct::list equal $pathsofar $path]} { - if {[dict get $data type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Parent path is not an array." - } - ::set parentarray [dict get $data value] - ::set idx [tomlish::system::lindex_resolve_basic $parentarray $p] - if {$idx == -1} { - error "tomlish::dict::path::lappend error bad path $path. Index $p does not exist." - } - ::set endpoint [lindex $parentarray $p] - if {[dict get $endpoint type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Not an array." - } - - ::set arrdata [dict get $endpoint value] - ::lappend arrdata {*}$args - dict set endpoint value $arrdata - ::set newlist $endpoint - #::lset parentarray $p $newlist - #set parentarray $newlist - ::set $varname $newlist - dict set vdict $pathsofar $varname - break - } else { - ::set arrdata [dict get $data value] - set idx [tomlish::system::lindex_resolve_basic $arrdata $p] - if {$idx == -1} { - error "tomlish::dict::path::lappend error bad path $path. Subpath $pathsofar, index $p does not exist." - } - ::set data [lindex $arrdata $p] - ::set $varname $data - dict set vdict $pathsofar $varname - } - } - } - # todo tomlish::log::debug ? - # dict for {path varname} $vdict { - # puts "$path $varname\n" - # puts " [::set $varname]\n" - # puts "" - # } - ::set i 0 - ::set reverse [lreverse $vdict] - foreach {varname path} $reverse { - set newval [::set $varname] - if {$i+2 == [llength $reverse]} { - ::set k [lindex $path end] - ::set k [string range $k 2 end] ;#first key is always @@something - dict set dict_being_edited $k $newval - #puts "--result $dict_being_edited" - break - } - ::set nextvarname [lindex $reverse $i+2] - ::set nextval [::set $nextvarname] - ::set k [lindex $path end] - if {[string match @@* $k]} { - #dict key - set k [string range $k 2 end] - dict set $nextvarname $k $newval - } else { - #list index - ::set nextarr [dict get $nextval value] - ::lset nextarr $k $newval - dict set $nextvarname value $nextarr - } - ::incr i 2 - } - return $dict_being_edited - } -} - -tcl::namespace::eval tomlish::to_dict { - - proc @@path {dictkeys} { - lmap v $dictkeys {string cat @@ $v} - } - -} - -tcl::namespace::eval tomlish::app { - #*** !doctools - #[subsection {Namespace tomlish::app}] - #[para] - #[list_begin definitions] - - tcl::namespace::eval argdoc { - proc test_suites {} { - if {[package provide test::tomlish] eq ""} { - return [list] - } - return [test::tomlish::SUITES] - } - } - - package require punk::args - punk::args::define { - @id -id ::tomlish::app::decode_to_typedjson - @cmd -name tomlish::app::decode_to_typedjson -help\ - "Read toml on stdin until EOF - on error - returns non-zero exit code and writes error to - the errorchannel. - on success - returns zero exit code and writes typed JSON encoding - of the data to the outputchannel. - This decoder is intended to be compatble with toml-test. - toml-test defines the typed JSON format." - @leaders -min 0 -max 0 - @opts - -help -type none -help\ - "Display this usage message" - -inputchannel -default stdin - -inputencoding -default "iso8859-1" -choicerestricted 0 -choices {utf-8 utf-16 iso8859-1} -help\ - "configure encoding on input channel - iso8859-1 is equivalent to binary encoding" - -outputchannel -default stdout - -errorchannel -default stderr - @values -min 0 -max 0 - } - proc decode_to_typedjson {args} { - set argd [punk::args::parse $args withid ::tomlish::app::decode_to_typedjson] - set ch_input [dict get $argd opts -inputchannel] - set ch_input_enc [dict get $argd opts -inputencoding] - set ch_output [dict get $argd opts -outputchannel] - set ch_error [dict get $argd opts -errorchannel] - if {[dict exists $argd received -help]} { - return [punk::args::usage -scheme info ::tomlish::app::decode_to_typedjson] - } - - chan configure $ch_input -encoding $ch_input_enc - #translation? - chan configure $ch_input -translation lf ;# toml-test invalid/control tests we need to see raw CRs to reject them properly - auto translation won't do. - - #Just slurp it all - presumably we are not handling massive amounts of data on stdin. - # - even if the input is large, we probably don't gain much (aside from possible memory savings?) by attempting to process input as it arrives. - if {[catch { - set inputdata [read $ch_input] - if {$ch_input_enc eq "iso8859-1"} { - set toml [tomlish::toml::from_binary $inputdata] - } else { - set toml $inputdata - } - } errM]} { - puts stderr "read-input error: $errM" - #toml-tests expect exit code 1 - #e.g invalid/encoding/utf16-bom - exit 1 ;#read error - } - try { - set j [::tomlish::toml_to_typedjson $toml] - } on error {em} { - puts $ch_error "decoding failed: '$em'" - exit 1 - } - puts -nonewline $ch_output $j - exit 0 - } - - package require punk::args - punk::args::define { - @id -id ::tomlish::app::encode_from_typedjson - @cmd -name tomlish::app::encode_from_typedjson -help\ - "Read typed JSON on input until EOF - return non-zero exitcode if JSON data cannot be converted to - a valid TOML representation. - return zero exitcode and TOML data on output if JSON data can - be converted. - This encoder is intended to be compatible with toml-test. - toml-test defines the typed JSON format." - @leaders -min 0 -max 0 - @opts - -help -type none -help \ - "Display this usage message" - -restrict_barekeys -default 0 -help\ - "If true, keys containing unicode will be quoted. - If false, an extended range of barekeys will be used - in unquoted form." - -inputchannel -default stdin - -inputencoding -default "" -choicerestricted 0 -choices {utf-8 utf-16 iso8859-1} -help\ - "configure encoding on input channel - If not supplied, leave at Tcl default" - -outputchannel -default stdout - -errorchannel -default stderr - @values -min 0 -max 0 - } - proc encode_from_typedjson {args} { - set argd [punk::args::parse $args withid ::tomlish::app::encode_from_typedjson] - set restrict_barekeys [dict get $argd opts -restrict_barekeys] - set ch_input [dict get $argd opts -inputchannel] - set ch_input_enc [dict get $argd opts -inputencoding] - set ch_output [dict get $argd opts -outputchannel] - set ch_error [dict get $argd opts -errorchannel] - if {[dict exists $argd received -help]} { - return [punk::args::usage -scheme info ::tomlish::app::encode_from_typedjson] - } - #review - if {$ch_input_enc ne ""} { - chan configure $ch_input -encoding $ch_input_enc - } - #review - chan configure $ch_input -translation lf - - chan configure $ch_output -translation lf - - if {[catch { - set json [read $ch_input] - }]} { - exit 2 ;#read error - } - try { - #tomlish::typedjson_to_toml - set toml [::tomlish::toml::from_tomlish_from_dict_from_typedjson $json] - } trap {} {e eopts} { - puts $ch_error "encoding failed: '$e'" - puts $ch_error "$::errorInfo" - exit 1 - } - puts -nonewline $ch_output $toml - exit 0 - } - - punk::args::define { - @dynamic - @id -id ::tomlish::app::test - @cmd -name tomlish::app::test -help\ - "Run the internal tests on the tomlish library." - @leaders - @opts -any 1 - -help -type none -help\ - "Display this usage message - or further info if more args." - -suite -default tests -choices {${[::tomlish::app::argdoc::test_suites]}} - @values -min 0 -max -1 - } - proc test {args} { - package require test::tomlish - set argd [punk::args::parse $args withid ::tomlish::app::test] - set opts [dict get $argd opts] - set values [dict get $argd values] - set received [dict get $argd received] - set solos [dict get $argd solos] - set opt_suite [dict get $opts -suite] - if {[dict exists $received -help] && ![dict exists $received -suite]} { - return [punk::args::usage -scheme info ::tomlish::app::test] - } - - test::tomlish::SUITE $opt_suite - #if {[catch {test::tomlish::SUITE $opt_suite} errM]} { - # puts stderr "Unknown test suite '$opt_suite'. Available suites: [test::tomlish::SUITES]" - # exit 1 - #} - set run_opts [dict remove $opts -suite] - set run_opts [dict remove $run_opts {*}$solos] - set result [test::tomlish::RUN {*}$run_opts {*}$solos {*}$values] - return $result - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::app ---}] -} - -proc ::tomlish::appnames {} { - set applist [list] - foreach cmd [info commands ::tomlish::app::*] { - lappend applist [namespace tail $cmd] - } - return $applist -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::lib { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace tomlish::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 tomlish::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::system { - #*** !doctools - #[subsection {Namespace tomlish::system}] - #[para] - #[list_begin definitions] - - - - #taken from punk::lib - #todo - change list argument to integer length - proc lindex_resolve_basic {list index} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg list] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -1 for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - #'only' supports 2**32 max index on tcl < 9.0 - ok. - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= [llength $list])} { - #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. - return -1 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {[llength $list]} { - set indices [tomlish::system::range 0 [expr {[llength $list]-1}]] ;# uses lseq if available, has fallback. - #if lseq was available - $indices is an 'arithseries' - theoretically not taking up ram(?) - } else { - set indices [list] - } - set idx [lindex $indices $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -1 - } else { - return $idx - } - } - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - #required for tcl < 8.7 range command (lseq not available) - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {(.*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - if {[info commands ::lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::system ---}] -} - -if {[info exists ::argc] && $::argc > 0} { - #puts stderr "argc: $::argc args: $::argv" - set arglist $::argv - # -------------- - #make sure any dependant packages that are sourced don't get any commandline args - set ::argv {} - set ::argc 0 - # -------------- - package require punk::args - punk::args::define { - @dynamic - @id -id tomlish::cmdline - @cmd -name tomlish -help\ - "toml encoder/decoder written in Tcl" - @opts -any 1 - -help -type none -help\ - "Display this usage message or more specific - help if further arguments provided." - -app -choices {${[tomlish::appnames]}} - } - try { - set argd [punk::args::parse $arglist withid tomlish::cmdline] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - puts stderr $msg - exit 1 - } - - - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received -help] && ![dict exists $received -app]} { - #only emit cmdline help if -app not supplied as well - otherwise app function can act on -help for more specific help - #puts stdout "Usage: -app where appname one of:[tomlish::appnames]" - puts stdout [punk::args::usage -scheme info tomlish::cmdline] - exit 0 - } - if {![dict exists $received -app]} { - puts stderr [punk::args::usage -scheme error tomlish::cmdline] - exit 1 - } - - set app [dict get $opts -app] - set appnames [tomlish::appnames] - set app_opts [dict remove $opts -app {*}$solos] - try { - set result [tomlish::app::$app {*}$app_opts {*}$solos {*}$values] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - #The validation error should fully describe the issue - #no need for errortrace - keep the output cleaner - puts stderr $msg - exit 1 - } trap {} {msg erroropts} { - #unexpected error - uncaught throw will produce error trace - #todo - a support msg? Otherwise we may as well just leave off this trap. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - if {"-help" in $solos} { - puts stderr $result - exit 1 - } else { - if {$result ne ""} { - puts stdout $result - exit 0 - } - } -} - -## Ready -package provide tomlish [namespace eval tomlish { - variable pkg tomlish - variable version - set version 1.1.6 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/tomlish-1.1.7.tm b/src/bootsupport/modules/tomlish-1.1.7.tm deleted file mode 100644 index 973b8304..00000000 --- a/src/bootsupport/modules/tomlish-1.1.7.tm +++ /dev/null @@ -1,9470 +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 tomlish 1.1.7 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin tomlish_module_tomlish 0 1.1.7] -#[copyright "2024"] -#[titledesc {tomlish toml parser}] [comment {-- Name section and table of contents description --}] -#[moddesc {tomlish}] [comment {-- Description at end of page heading --}] -#[require tomlish] -#[keywords module parsing toml configuration] -#[description] -#[para] tomlish is an intermediate representation of toml data in a tree structure (tagged lists representing type information) -#[para] The design goals are for tomlish to be whitespace and comment preserving ie byte-for byte preservation during roundtrips from toml to tomlish and back to toml -#[para] The tomlish representation can then be converted to a Tcl dict structure or to other formats such as json, -#[para] although these other formats are generally unlikely to retain whitespace or comments -#[para] The other formats also won't preserve roundtripability e.g \t and a literal tab coming from a toml file will be indistinguishable. -#[para] A further goal is to allow at least a useful subset of in-place editing operations which also preserve whitespace and comments. -#[para] e.g leaf key value editing, and table reordering/sorting, key-renaming at any level, key insertions/deletions -#[para] The API for editing (tomldoc object?) may require explicit setting of type if accessing an existing key -#[para] e.g setting a key that already exists and is a different type (especially if nested structure such as a table or array) -#[para] will need a -type option (-force ?) to force overriding with another type such as an int. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of tomlish -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by tomlish -#[list_begin itemized] - -package require Tcl 8.6- -package require struct::stack -package require logger - -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {struct::stack}] - -#limit ourselves to clear, destroy, peek, pop, push, rotate, or size (e.g v 1.3 does not implement 'get') - - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish { - namespace export {[a-z]*}; # Convention: export all lowercase - variable types - - #*** !doctools - #[subsection {Namespace tomlish}] - #[para] Core API functions for tomlish - #[list_begin definitions] - - #default interp recursionlimit of 1000 is insufficient to pass 1000 deep nested structures as in certain toml tests. - #e.g https://github.com/iarna/toml-spec-tests/tree/latest/values - #1000 seems deep for a 'configuration' format - but toml sometimes used for other serialisation purposes. - #todo - review - set existing_recursionlimit [interp recursionlimit {}] - if {$existing_recursionlimit < 5000} { - interp recursionlimit {} 5000 - } - - #IDEAS: - # since get_toml produces tomlish with whitespace/comments intact: - # tomldoc object - allow (at least basic?) editing of toml whilst preserving comments & whitespace - # - setKey (set leaf only to value) how to specify type? -type option? - whole array vs index into arrays and further nested objects? - option for raw toml additions? - # - separate addKey?? - # - deleteKey (delete leaf) - # - deleteTable (delete table - if only has leaves? - option to delete with child tables?) - # - set/add Table? - position in doc based on existing tables/subtables? - - #The tomlish intermediate representation allows things such as sorting the toml document by table name or other re-ordering of tables - - # because the tables include subkeys, comments and newlines within their structure - those elements all come along with it nicely during reordering. - #The same goes for the first newline following a keyval e.g x=1 \ny=2\n\n - #The newline is part of the keyval structure so makes reordering easier - #example from_toml "a=1\nb=2\n\n\n" - # 0 = TOMLISH - # 1 = KEY a = {INT 1} {NEWLINE lf} - # 2 = NEWLINE lf - # 3 = KEY b = {INT 2} {NEWLINE lf} - # 4 = NEWLINE lf - # 5 = NEWLINE lf - - #This reordering idea is complicated by the nature of tablearrays - especially as a table header references last tablearrayname, - # and duplicate table headers are allowed in that context. - #e.g - #[[fruits]] - # name="apple" - # [fruits.metadata] - # id=1 - # - #[unrelated1] - # - #[[fruits]] - # name="pear" - # - #[unrelated2] - # silly="ordering" - # - #[fruits.metadata] - #id=2 - #The TABLEARRAY record can't be completely selfcontained on the default parsing mechanism - because it is legal (though not recommended) to have unrelated tables in between. - #If we were to 'insert' later related records (such as the 2nd [fruits.metadata] above) into the TABLEARRAY structure - then, even though it might produce 'nicer' toml, - # we would lose roundtripability toml->tomlish->toml - # ----------------------------------------------------- - #REVIEW - #todo - some sort of 'normalize'/'grouping' function on tomlish that at least makes records self-contained, and perhaps then (optionally) reorders resulting records sensibly. - #such a function on the tomlish may work - although it would be unwise to duplicate the validation aspects of dict::from_tomlish - #The most practical way might be to use dict::from_tomlish followed by from_dict - but that would lose comment info and formatting. - #In the above example - The decision by the toml author to put [unrelated1] between related tablearrays should be respected, - #but the positioning of [unrelated2] between a tablearray and one of its contained tables is suspect. - #Both [fruits.metadata] table records should theoretically be added as children to their corresponding [[fruits]] tablearray record in the tomlish. (just as their name keys are) - # ----------------------------------------------------- - - - - #ARRAY is analogous to a Tcl list - #TABLE is analogous to a Tcl dict - #WS = inline whitespace - #KEY = bare key and value - #DQKEY = double quoted key and value - #SQKEY = single quoted key and value - #ITABLE = inline table (*can* be anonymous table) - # inline table values immediately create a table with the opening brace - # inline tables are fully defined between their braces, as are dotted-key subtables defined within - # No additional subtables or arrays of tables may be defined within an inline table after the ending brace - they must be entirely self-contained - - set tags [list TOMLISH BOM ARRAY TABLE ITABLE TABLEARRAY WS NEWLINE COMMENT DOTTEDKEY KEY DQKEY SQKEY STRING STRINGPART MULTISTRING LITERAL LITERALPART MULTILITERAL INT FLOAT BOOL] - #DDDD - lappend tags {*}[list\ - DATETIME\ - DATETIME-LOCAL\ - DATE-LOCAL\ - TIME-LOCAL\ - ] - - #removed - ANONTABLE - #tomlish v1.0 should accept arbitrary 64-bit signed ints (from -2^63 to 2^63-1) - #we will restrict to this range for compatibility for now - although Tcl can handle larger (arbitrarily so?) - #todo - configurable - allow empty string for 'unlimited' - set min_int -9223372036854775808 ;#-2^63 - set max_int +9223372036854775807 ;#2^63-1 - - proc Dolog {lvl txt} { - #return "$lvl -- $txt" - set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] tomlish '$txt'" - puts stderr $msg - } - logger::initNamespace ::tomlish - foreach lvl [logger::levels] { - interp alias {} tomlish_log_$lvl {} ::tomlish::Dolog $lvl - log::logproc $lvl tomlish_log_$lvl - } - - - proc tags {} { - return $::tomlish::tags - } - - proc get_dottedkey_info {dottedkeyrecord} { - set key_hierarchy [list] - set key_hierarchy_raw [list] - if {[lindex $dottedkeyrecord 0] ne "DOTTEDKEY"} { - error "tomlish::get_dottedkey_info error. Supplied list doesn't appear to be a DOTTEDKEY (tag: [lindex $dottedkeyrecord 0])" - } - set compoundkeylist [lindex $dottedkeyrecord 1] - set expect_sep 0 - foreach part $compoundkeylist { - set parttag [lindex $part 0] - if {$parttag eq "WS"} { - continue - } - if {$expect_sep} { - if {$parttag ne "DOTSEP"} { - error "DOTTEDKEY missing dot separator between parts. '$dottedkeyrecord'" - } - set expect_sep 0 - } else { - set val [lindex $part 1] - switch -exact -- $parttag { - KEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw $val - } - DQKEY { - #REVIEW unescape or not? - #JJJJ - lappend key_hierarchy [::tomlish::utils::unescape_string $val] - lappend key_hierarchy_raw \"$val\" - } - SQKEY { - lappend key_hierarchy $val - lappend key_hierarchy_raw "'$val'" - } - default { - error "tomlish::get_dottedkey_info DOTTED key unexpected part '$parttag' - ensure dot separator is between key parts. '$compoundkeylist'" - } - } - set expect_sep 1 - } - } - return [dict create keys $key_hierarchy keys_raw $key_hierarchy_raw] - } - - #helper function for tomlish::dict::from_tomlish - proc _get_keyval_value {keyval_element} { - #e.g - #DOTTEDKEY {{KEY a} {WS { }}} = {WS { }} {ARRAY {INT 1} SEP {ITABLE {DOTTEDKEY {{KEY x}} = {INT 1} SEP} {DOTTEDKEY {{KEY y}} = {INT 2}}}} - - log::notice ">>> _get_keyval_value from '$keyval_element'<<<" - #find the value (or 2 values if space separated datetime - and stitch back into one) - # 3 is the earliest index at which the value could occur (depending on whitespace) - if {[lindex $keyval_element 2] ne "="} { - error "tomlish _get_keyval_value keyval_element doesn't seem to be a properly structured { = } list\n $keyval_element" - } - - #review - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keys are @@ paths {@@parenttable @@arrayable @@etc} corresponding to parenttable.arraytable[].etc - #value is a dict with keys such as ttype, tdefined - } - set sublist [lrange $keyval_element 3 end] ;# rhs of = - - set values [list] - set value_posns [list] - set posn 0 - foreach sub $sublist { - #note that a barekey/dquotedkey won't occur directly inside a barekey/dquotedkey - #DDDD - switch -exact -- [lindex $sub 0] { - STRING - LITERAL - MULTISTRING - MULTILITERAL - INT - FLOAT - BOOL - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL - TIME-TZ - TABLE - ARRAY - ITABLE { - lappend values $sub - lappend value_posns $posn - } - DOTTEDKEY { - #we should never see DOTTEDKEY as a toplevel element on RHS - #sanity check in case manually manipulated tomlish - or something went very wrong - set msg "tomlish::_get_keyval_value Unexpected toplevel value element DOTTEDKEY after =" - return -code error -errorcode {TOMLISH SYNTAX UNEXPECTEDDOTTEDKEYRHS} $msg - } - WS - NEWLINE - COMMENT {} - SEP {} - default { - set msg "tomlish::_get_keyval_value Unexpected toplevel value element [lindex $sub 0] after =" - return -code error -errorcode {TOMLISH SYNTAX UNEXPECTED} $msg - } - } - incr posn - } - switch -- [llength $values] { - 0 { - error "tomlish Failed to find value element in KEY. '$keyval_element'" - } - 1 { - lassign [lindex $values 0] type value - } - 2 { - #we generally expect a single 'value' item on RHS of = - #(ignoring WS,NEWLINE,SEP - #(either a simple type, or a container which has multiple values inside) - #exception for space separated datetime which is two toplevel values - - #validate than exactly single space was between the two values - lassign $value_posns p1 p2 - if {$p2 != $p1 +2} { - #sanity check - #can probably only get here through manual manipulation of the tomlish list to an unprocessable form - error "tomlish KEY appears to have more than one part - but not separated by whitespace - invalid '$keyval_element'" - } - set between_token [lindex $sublist $p1+1] - if {[lindex $between_token 1] ne " "} { - error "tomlish KEY in 2 parts is not separated by a single space - cannot consider for datetime '$keyval_element'" - } - lassign [lindex $values 0] type_d1 value_d1 - lassign [lindex $values 1] type_d2 value_d2 - #DDDD - if {$type_d1 ne "DATE-LOCAL" || $type_d2 ni {TIME-TZ TIME-LOCAL}} { - #we reuse DATETIME tag for standalone time with tz offset (or zZ) - error "tomlish KEY in 2 parts does not appear to be datetime '$keyval_element'" - } - if {$type_d2 eq "TIME-LOCAL"} { - set type DATETIME-LOCAL - } elseif {$type_d2 eq "TIME-TZ"} { - set type DATETIME - } else { - error "tomlish KEY in 2 parts does not appear to be datetime. (part 2 not a time value) '$keyval_element'" - } - set value "${value_d1}T${value_d2}" - } - default { - error "tomlish Found multiple value elements in KEY, expected one. (or 2 for space-separated datetime) '$keyval_element'" - } - } - set sub_tablenames_info [dict create] - switch -exact -- $type { - TIME-TZ { - #This is only valid in tomlish following a DATE-LOCAL - error "tomlish type TIME-TZ was not preceeded by DATE-LOCAL in keyval '$keyval_element'" - } - INT - FLOAT - BOOL - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - #DDDD - #simple (non-container, no-substitution) datatype - set result [list type $type value $value] - } - STRING - STRINGPART { - #JJJ - #!!! review - #set result [list type $type value [::tomlish::utils::unescape_string $value]] - set result [list type $type value $value] - } - LITERAL - LITERALPART { - #REVIEW - set result [list type $type value $value] - } - TABLE { - #invalid? - error "tomlish _get_keyval_value invalid to have type TABLE on rhs of =" - } - ITABLE { - #This one should not be returned as a type value structure! - # - set prev_tablenames_info $tablenames_info - set tablenames_info [dict create] - set result [::tomlish::dict::from_tomlish [ list [lindex $values 0] ]] - set sub_tablenames_info $tablenames_info - set tablenames_info $prev_tablenames_info - } - ARRAY { - #we need to recurse to get the corresponding dict for the contained item(s) - #pass in the whole [lindex $values 0] (type val) - not just the $value! - set prev_tablenames_info $tablenames_info - set tablenames_info [dict create] - set result [list type $type value [ ::tomlish::dict::from_tomlish [ list [lindex $values 0] ] ]] - set sub_tablenames_info $tablenames_info - set tablenames_info $prev_tablenames_info - } - MULTISTRING - MULTILITERAL { - #review - mapping these to STRING might make some conversions harder? - #if we keep the MULTI - we know we have to look for newlines for example when converting to json - #without specific types we'd have to check every STRING - and lose info about how best to map chars within it - set result [list type $type value [ ::tomlish::dict::from_tomlish [ list [lindex $values 0] ] ]] - } - default { - error "tomlish Unexpected value type '$type' found in keyval '$keyval_element'" - } - } - return [dict create result $result tablenames_info $sub_tablenames_info] - } - - - proc to_dict {tomlish {returnextra 0}} { - tomlish::dict::from_tomlish $tomlish $returnextra - } - - - - proc _from_dictval_tomltype {parents tablestack keys typeval} { - set type [dict get $typeval type] - set val [dict get $typeval value] - #These are the restricted sets of typed used in the tomlish::dict representation - #They are a subset of the types in tomlish: data types plus ARRAY, arranged in a dictionary form. - #The container types: ITABLE, TABLE, TABLEARRAY are not used as they are represented as dictionary keys and ARRAY items. - #The WS, COMMENT, and NEWLINE elements are also unrepresented in the dict structure. - switch -- $type { - ARRAY { - set subitems [list] - foreach item $val { - lappend subitems [_from_dictval [list {*}$parents ARRAY] $tablestack $keys $item] SEP - } - if {[lindex $subitems end] eq "SEP"} { - set subitems [lrange $subitems 0 end-1] - } - return [list ARRAY {*}$subitems] - } - ITABLE { - error "not applicable" - if {$val eq ""} { - return ITABLE - } else { - return [_from_dictval [list {*}$parents ITABLE] $tablestack $keys $val] - } - } - STRING { - #JSJS - #if our dict came from json - we have already decided what type of STRING/LITERAL etc to use when building the dict - - #do not validate like this - important that eg json val\\ue -> dict val\ue -> tomlish/toml val\\ue - #see toml-tests - #if {![tomlish::utils::rawstring_is_valid_tomlstring $val]} { - # #todo? - # return -code error -errorcode {TOML SYNTAX INVALIDSTRING} "Unescaped controls in string or invalid escapes" - #} - return [list STRING [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $val]] - } - MULTISTRING { - #value is a raw string that isn't encoded as tomlish - #create a valid toml snippet with the raw value and decode it to the proper tomlish MULTISTRING format - #We need to convert controls in $val to escape sequences - except for newlines - # - #consider an *option* to reformat for long lines? (perhaps overcomplex - byte equiv - but may fold in ugly places) - #we could use a line-length limit to decide when to put in a "line ending backslash" - #and even format it with a reasonable indent so that proper CONT and WS entries are made (?) REVIEW - # - #TODO - #set tomlpart "x=\"\"\"\\\n" ;#no need for continuation - set tomlpart "x=\"\"\"\n" - append tomlpart [tomlish::utils::rawstring_to_MultiBstring_with_escaped_controls $val] - append tomlpart "\"\"\"" - set tomlish [tomlish::from_toml $tomlpart] - #e.g if val = " etc\nblah" - #TOMLISH {DOTTEDKEY {{KEY x}} = {MULTISTRING CONT {NEWLINE LF} {WS { }} {STRINGPART etc} {NEWLINE lf} {STRINGPART blah} } } - #lindex 1 3 is the MULTISTRING tomlish list - return [lindex $tomlish 1 3] - } - MULTILITERAL { - #MLL string can contain newlines - but still no control chars - #todo - validate - e.g val can't contain more than 2 squotes in a row - if {[string first ''' $val] >=0} { - set msg "_from_dictval_tomltype error: more than 2 single quotes in a row found in MULTILITERAL - cannot encode dict to TOML-VALID TOMLISH" - return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} $msg - } - - #rawstring_is_valid_multiliteral - allow newlines as lf or crlf - but not bare cr - if {![tomlish::utils::rawstring_is_valid_multiliteral $val]} { - return -code error -errorcode {TOML SYNTAX INVALIDMULTILITERAL} "Controls other than tab or newlines found in multiliteral" - } - - set tomlpart "x='''\n" - append tomlpart $val ''' - set tomlish [tomlish::from_toml $tomlpart] - return [lindex $tomlish 1 3] - } - LITERAL { - #from v1.0 spec - "Control characters other than tab are not permitted in a literal string" - #(This rules out raw ANSI SGR - which is somewhat restrictive - but perhaps justified for a config format - # as copy-pasting ansi to a config value is probably not always wise, and it's not something that can be - # easily input via a text editor. ANSI can go in Basic strings using the \e escape if that's accepted v1.1?) - #we could choose to change the type to another format here when encountering invalid chars - but that seems - #like too much magic. We elect to error out and require the dict to have valid data for the types it specifies. - if {[string first ' $val] >=0} { - set msg "_from_dictval_tomltype error: single quote found in LITERAL - cannot encode dict to TOML-VALID TOMLISH" - return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} $msg - } - #JJJJ - if {![tomlish::utils::rawstring_is_valid_literal $val]} { - #has controls other than tab - #todo - squote? - return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} "Controls other than tab found in literal" - } - return [list LITERAL $val] - } - INT { - if {![::tomlish::utils::is_int $val]} { - error "_from_dictval_tomltype error: bad INT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list INT $val] - } - FLOAT { - if {![::tomlish::utils::is_float $val]} { - error "_from_dictval_tomltype error: bad FLOAT value '$val' - cannot encode dict to TOML-VALID TOMLISH" - } - return [list FLOAT $val] - } - default { - if {$type ni [::tomlish::tags]} { - error "_from_dictval_tomltype error: Unrecognised typename '$type' in {type value } - cannot encode dict to TOML-VALID TOMLISH" - } - return [list $type $val] - } - } - } - - proc _from_dictval {parents tablestack keys vinfo} { - set k [lindex $keys end] - set K_PART [tomlish::dict::classify_rawkey $k] ;#get [list SQKEY ] - #puts stderr "---parents:'$parents' keys:'$keys' vinfo: $vinfo---" - #puts stderr "---tablestack: $tablestack---" - set result [list] - set lastparent [lindex $parents end] - if {$lastparent in [list "" do_inline]} { - if {[tomlish::dict::is_typeval $vinfo]} { - set type [dict get $vinfo type] - #treat ITABLE differently? - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result DOTTEDKEY [list $K_PART {WS { }}] = {WS { }} $sublist {NEWLINE lf} - } else { - if {$vinfo ne ""} { - if {![tomlish::utils::string_is_dict $vinfo]} { - #e.g tomlish::dict::from_tomlish was called with return_extra 1 - return -code error -errorcode {TOMLISH SYNTAX INVALIDDICT} "tomlish::_from_dictval Supplied dict is not a valid format for converting to tomlish" ;#review - } - - #set result [list DOTTEDKEY [list [list KEY $k]] = ] - #set records [list ITABLE] - - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - - if {$lastparent eq "do_inline"} { - set result [list DOTTEDKEY [list $K_PART] =] - set records [list ITABLE] - } else { - set tname [tomlish::dict::join_and_quote_rawkey_list [list $k]] - set result [list TABLE $tname {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $k]] - set records [list] - } - - - - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - dict for {vk vv} $vinfo { - set VK_PART [tomlish::dict::classify_rawkey $vk] ;#get [list SQKEY ] - #(SQKEY & DQKEY do not have the enclosing quotes in their returned val) - #if {[regexp {\s} $vk] || [string first . $vk] >= 0} { - # set VK_PART [list SQKEY $vk] - #} else { - # set VK_PART [list KEY $vk] - #} - if {[tomlish::dict::is_typeval $vv]} { - #type x value y - #REVIEW - we could detect if value is an array of objects, - #and depending on parent context - emit a series of TABLEARRAY records instead of a DOTTEDKEY record containing an ARRAY of objects - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART {WS { }}] = {WS { }} $sublist] - } else { - if {$vv eq ""} { - #experimental - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - ::tomlish::log::notice "_from_dictval could uninline KEY $vk (tablestack:$tablestack)" - #set tname [tomlish::dict::name_from_tablestack [list {*}$tablestack [list T $vk]]] - - #we can't just join normalized keys - need keys with appropriate quotes and escapes - #set tname [join [list {*}$keys $vk] .] ;#WRONG - set tq [tomlish::dict::join_and_quote_rawkey_list [list {*}$keys $vk]] - - - ##wrong? results in TABLE within TABLE record?? todo pop? - #set record [list TABLE $tq {NEWLINE lf}] - #set tablestack [list {*}$tablestack [list T $vk]] - - #REVIEW!!! - - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - - } else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - set tablestack [list {*}$tablestack [list I $vk]] - } - } else { - if { 0 } { - #experiment.. sort of getting there. - if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - ::tomlish::log::notice "_from_dictval could uninline2 KEYS [list {*}$keys $vk] (tablestack:$tablestack)" - set tq [tomlish::dict::join_and_quote_rawkey_list [list {*}$keys $vk]] - set record [list TABLE $tq {NEWLINE lf}] - set tablestack [list {*}$tablestack [list T $vk]] - - #review - todo? - set dottedkey_value [_from_dictval [list {*}$parents TABLE] $tablestack [list {*}$keys $vk] $vv] - lappend record {*}$dottedkey_value - - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } else { - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - } - if {$dictidx != $lastidx} { - #lappend record SEP - if {$lastparent eq "do_inline"} { - lappend record SEP - } else { - lappend record {NEWLINE lf} - } - } - if {[llength $record]} { - lappend records $record - } - incr dictidx - } - if {$lastparent eq "do_inline"} { - lappend result $records {NEWLINE lf} - } else { - lappend result {*}$records {NEWLINE lf} - } - } else { - if {$lastparent eq "do_inline"} { - lappend result DOTTEDKEY [list $K_PART] = ITABLE {NEWLINE lf} - } else { - set tname [tomlish::dict::join_and_quote_rawkey_list [list $k]] - #REVIEW - lappend result TABLE $tname {NEWLINE lf} - } - } - } - } else { - #lastparent is not toplevel "" or "do_inline" - if {[tomlish::dict::is_typeval $vinfo]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vinfo] - lappend result {*}$sublist - } else { - if {$lastparent eq "TABLE"} { - #review - dict for {vk vv} $vinfo { - set VK_PART [tomlish::dict::classify_rawkey $vk] ;#get [list SQKEY ] - set dottedkey_value [_from_dictval [list {*}$parents DOTTEDKEY] $tablestack [list {*}$keys $vk] $vv] - lappend result [list DOTTEDKEY [list $VK_PART] = $dottedkey_value {NEWLINE lf}] - } - } else { - if {$vinfo ne ""} { - if {![tomlish::utils::string_is_dict $vinfo]} { - #e.g tomlish::dict::from_tomlish was called with return_extra 1 - return -code error -errorcode {TOMLISH SYNTAX INVALIDDICT} "tomlish::_from_dictval Supplied dict is not a valid format for converting to tomlish" ;#review - } - set lastidx [expr {[dict size $vinfo] -1}] - set dictidx 0 - set sub [list] - #REVIEW - #set result $lastparent ;#e.g sets ITABLE - set result ITABLE - set last_tomltype_posn [tomlish::dict::last_tomltype_posn $vinfo] - dict for {vk vv} $vinfo { - set VK_PART [tomlish::dict::classify_rawkey $vk] ;#get [list SQKEY ] - if {[tomlish::dict::is_typeval $vv]} { - #type x value y - set sublist [_from_dictval_tomltype $parents $tablestack $keys $vv] - set record [list DOTTEDKEY [list $VK_PART] = $sublist] - } else { - if {$vv eq ""} { - #can't just uninline at this level - #we need a better method to query main dict for uninlinability at each level - # (including what's been inlined already) - #if {[lindex $parents 0] eq "" && $dictidx > $last_tomltype_posn} { - # puts stderr "_from_dictval uninline2 KEY $keys" - # set tname [tomlish::dict::join_and_quote_rawkey_list [list {*}$keys $vk]] - # set record [list TABLE $tname {NEWLINE lf}] - # set tablestack [list {*}$tablestack [list T $vk]] - #} else { - set record [list DOTTEDKEY [list $VK_PART] = ITABLE] - #} - } else { - #set sub [_from_dictval ITABLE $vk $vv] - set dottedkey_value [_from_dictval [list {*}$parents ITABLE] $tablestack [list {*}$keys $vk] $vv] - #set record [list DOTTEDKEY [list $VK_PART] = ITABLE $dottedkey_value] - set record [list DOTTEDKEY [list $VK_PART] = $dottedkey_value] - } - } - if {$dictidx != $lastidx} { - lappend record SEP - } - lappend result $record - incr dictidx - } - } else { - #e.g x=[{}] - log::debug "---> _from_dictval empty ITABLE x-1" - #lappend result DOTTEDKEY [list $K_PART] = ITABLE ;#wrong - lappend result ITABLE - } - } - } - } - return $result - } - - - proc from_dict {d} { - #consider: - # t1={a=1,b=2} - # x = 1 - - # from_dict gives us: t1 {a {type INT value 1} b {type INT value 2}} x {type INT value 1} - - #If we represent t1 as an expanded table we get - # [t1] - # a=1 - # b=2 - # x=1 - # --- which is incorrect - as x was a toplevel key like t1! - #This issue doesn't occur if x is itself an inline table - # t1={a=1,b=2} - # x= {no="problem"} - # - # (or if we were to reorder x to come before t1) - - #ie the order of the dict elements influences how the toml can be represented. - - #As the dictionary form doesn't distinguish the structure used to create tables {[table1]\nk=v} vs inline {table1={k=v}} - #Without a solution, from_dict would have to always produce the inline form for toplevel tables unless we allowed re-ordering, - #which is unpreferred here. - - #A possible solution: - #scan the top level to see if all (trailing) elements are themselves dicts - # (ie not of form {type XXX value yyy}) - # - # A further point is that if all root level values are at the 'top' - we can treat lower table-like structures as {[table]} elements - #ie we don't need to force do_inline if all the 'simple' keys are before any compound keys - - #set root_has_values 0 - #approach 1) - the naive approach - forces inline when not always necessary - #dict for {k v} $d { - # if {[llength $v] == 4 && [lindex $v 0] eq "type"} { - # set root_has_values 1 - # break - # } - #} - - - #approach 2) - track the position of last {type x value y} in the dictionary built by dict::from_tomlish - # - still not perfect. Inlines dotted tables unnecessarily - #This means from_dict doesn't produce output optimal for human editing. - set last_simple [tomlish::dict::last_tomltype_posn $d] - - - ## set parent "do_inline" ;#a value used in _from_dictval to distinguish from "" or other context based parent values - #Any keys that are themselves tables - will need to be represented inline - #to avoid reordering, or incorrect assignment of plain values to the wrong table. - - ## set parent "" - #all toplevel keys in the dict structure can represent subtables. - #we are free to use {[tablename]\n} syntax for toplevel elements. - - - set tomlish [list TOMLISH] - set dictposn 0 - set tablestack [list [list T root]] ;#todo - dict for {t tinfo} $d { - if {$last_simple > $dictposn} { - set parents [list do_inline] - } else { - set parents [list ""] - } - set keys [list $t] - #review - where to make decision on - # DOTTEDKEY containing array of objs - #vs - # list of TABLEARRAY records - #At least for the top - set trecord [_from_dictval $parents $tablestack $keys $tinfo] - lappend tomlish $trecord - incr dictposn - } - return $tomlish - } - - proc typedjson_to_toml {json} { - #*** !doctools - #[call [fun typedjson_to_toml] [arg json]] - #[para] - - set tomlish [::tomlish::from_dict_from_typedjson $json] - lappend tomlish [list NEWLINE lf] - set toml [::tomlish::to_toml $tomlish] - } - - set json1 {{ "a": {"type": "integer", "value": "42"}}} - set json2 {{ - "a": {"type": "integer", "value": "42"}, - "b": {"type": "string", "value": "test"} - }} - set json3 { -{ - "best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}, - "numtheory": { - "boring": {"type": "bool", "value": "false"}, - "perfection": [ - {"type": "integer", "value": "6"}, - {"type": "integer", "value": "28"}, - {"type": "integer", "value": "496"} - ] - } -} - } - - set json4 { -{ - "best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}, - "numtheory": { - "boring": {"type": "bool", "value": "false"}, - "perfection": [ - {"type": "integer", "value": "6"}, - {"type": "integer", "value": "28"}, - {"type": "integer", "value": "496"} - ] - }, - "emptyobj": {}, - "emptyarray": [] -} - } - - set json5 { -{ - "a": { - " x ": {}, - "b.c": {}, - "d.e": {}, - "b": { - "c": {} - } - } -} - } - - #surrogate pair face emoji - set json6 { -{ - "surrogatepair": {"type": "string", "value": "\uD83D\uDE10"} -} - } - - - set json7 { -{ - "escapes": {"type": "string", "value": "val\\ue"} -} - } - - - proc from_dict_from_typedjson {json} { - set d [tomlish::dict::from_typedjson $json] - tomlish::from_dict $d ;#return tomlish - } - - - proc toml_to_typedjson {toml} { - set tomlish [::tomlish::from_toml $toml] - set d [tomlish::dict::from_tomlish $tomlish] - #full validation only occurs by re-encoding dict to tomlish - set test [tomlish::from_dict $d] - - set h [tomlish::typedhuddle::from_dict $d] - #huddle jsondump $h - tomlish::huddle::jsondumpraw $h - } - - #proc get_json {tomlish} { - # package require fish::json - # set d [::tomlish::dict::from_tomlish $tomlish] - - # #return [::tomlish::dict_to_json $d] - # return [fish::json::from "struct" $d] - #} - - #return a Tcl list of tomlish tokens - #i.e get a standard list of all the toml terms in string $s - #where each element of the list is a *tomlish* term.. i.e a specially 'tagged' Tcl list. - #(simliar to a tcl 'Huddle' - but also supporting whitespace preservation) - # ---------------------------------------------------------------------------------------------- - # NOTE: the production of tomlish from toml source doesn't indicate the toml source was valid!!! - # e.g we deliberately don't check certain things such as duplicate table declarations here. - # ---------------------------------------------------------------------------------------------- - #Part of the justification for this is that as long as the syntax is toml shaped - we can load files which violate certain rules and allow programmatic manipulation. - # (e.g perhaps a toml editor to highlight violations for fixing) - # A further stage is then necessary to load the tomlish tagged list into a data structure more suitable for efficient query/reading. - # e.g dicts or an object oriented structure - #Note also - *no* escapes in quoted strings are processed. This is up to the datastructure stage - #e.g dict::from_tomlish will substitute \r \n \uHHHH \UHHHHHHH etc - #This is important for tomlish to maintain the ability to perform competely lossless round-trips from toml to tomlish and back to toml. - # (which is handy for testing as well as editing some part of the structure with absolutely no effect on other parts of the document) - #If we were to unescape a tab character for example - # - we have no way of knowing if it was originally specified as \t \u0009 or \U00000009 or directly as a tab character. - # For this reason, we also do absolutely no line-ending transformations based on platform. - # All line-endings are maintained as is, and even a file with mixed lf crlf line-endings will be correctly interpreted and can be 'roundtripped' - - proc from_toml {args} { - - namespace upvar ::tomlish::parse s s - set s [join $args \n] - namespace upvar ::tomlish::parse i i - set i 0 ;#index into s - - namespace upvar ::tomlish::parse is_parsing is_parsing - set is_parsing 1 - - if {[info command ::tomlish::parse::spacestack] eq "::tomlish::parse::spacestack"} { - tomlish::parse::spacestack destroy - } - struct::stack ::tomlish::parse::spacestack - - namespace upvar ::tomlish::parse last_space_action last_space_action - namespace upvar ::tomlish::parse last_space_type last_space_type - - namespace upvar ::tomlish::parse tok tok - set tok "" - - namespace upvar ::tomlish::parse type type - namespace upvar ::tomlish::parse tokenType tokenType - ::tomlish::parse::set_tokenType "" - namespace upvar ::tomlish::parse tokenType_list tokenType_list - set tokenType [list] ;#Flat (un-nested) list of tokentypes found - - namespace upvar ::tomlish::parse lastChar lastChar - set lastChar "" - - - set result "" - namespace upvar ::tomlish::parse nest nest - set nest 0 - - namespace upvar ::tomlish::parse v v ;#array keyed on nest level - - - set v(0) {TOMLISH} - array set s0 [list] ;#whitespace data to go in {SPACE {}} element. - set parentlevel 0 - - - namespace upvar ::tomlish::parse state state - - namespace upvar ::tomlish::parse braceCount braceCount - set barceCount 0 - namespace upvar ::tomlish::parse bracketCount bracketCount - set bracketCount 0 - - set sep 0 - set r 1 - namespace upvar ::tomlish::parse token_waiting token_waiting - set token_waiting [dict create] ;#if ::tok finds a *complete* second token during a run, it will put the 2nd one here to be returned by the next call. - - - - set state "table-space" - ::tomlish::parse::spacestack push {type space state table-space} - namespace upvar ::tomlish::parse linenum linenum;#'line number' of input data. (incremented for each literal linefeed - but not escaped ones in data) - set linenum 1 - - set ::tomlish::parse::state_list [list] - try { - while {$r} { - set r [::tomlish::parse::tok] - #puts stdout "got tok: '$tok' while parsing string '$s' " - set next_tokenType_known 0 ;#whether we begin a new token here based on what terminated the token result of 'tok' - - - #puts "got token: '$tok' tokenType='$tokenType'. while v($nest) = [set v($nest)]" - #puts "-->tok: $tok tokenType='$tokenType'" - set prevstate $state - set transition_info [::tomlish::parse::goNextState $tokenType $tok $state] - #review goNextState could perform more than one space_action - set space_action [dict get $transition_info space_action] - set newstate [dict get $transition_info newstate] ;#use of 'newstate' vs 'state' makes code clearer below - - if {[tcl::string::match "err-*" $state]} { - ::tomlish::log::warn "---- State error in state $prevstate for tokenType: $tokenType token value: $tok. $state aborting parse. [tomlish::parse::report_line]" - lappend v(0) [list ERROR tokentype $tokenType state $prevstate to $state leveldata [set v($nest)]] - return $v(0) - } - # --------------------------------------------------------- - #NOTE there may already be a token_waiting at this point - #set_token_waiting can raise an error here, - # in which case the space_action branch needs to be rewritten to handle the existing token_waiting - # --------------------------------------------------------- - - if {$space_action eq "pop"} { - #pop_trigger_tokens: newline tablename endarray endinlinetable - #note a token is a pop trigger depending on context. e.g first newline during keyval is a pop trigger. - set parentlevel [expr {$nest -1}] - set do_append_to_parent 1 ;#most tokens will leave this alone - but some like tentative_accum_squote need to do their own append - switch -exact -- $tokenType { - tentative_accum_squote { - #should only apply within a multiliteral - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the xxx-squote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-squote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-squote-space { - } - default { - error "--- unexpected popped due to tentative_accum_squote but came from state '$prevstate' should have been trailing-squote-space" - } - } - switch -- $tok { - ' { - tomlish::parse::set_token_waiting type single_squote value $tok complete 1 startindex [expr {$i -1}] - } - '' { - #review - we should perhaps return double_squote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_squote value "" complete 1 startindex [expr {$i - 2}] - } - ''' { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 3}] - } - '''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left squote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]'" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "'"] - } - MULTILITERAL { - #empty - lappend v($parentlevel) [list LITERALPART "'"] - } - default { - error "--- don't know how to integrate extra trailing squote with data $v($parentlevel)" - } - } - } - ''''' { - tomlish::parse::set_token_waiting type triple_squote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 squotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - LITERALPART { - set newval "[lindex $lastpart 1]''" - set parentdata $v($parentlevel) - lset parentdata end [list LITERALPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE { - lappend v($parentlevel) [list LITERALPART "''"] - } - MULTILITERAL { - lappend v($parentlevel) [list LITERALPART "''"] - } - default { - error "--- don't know how to integrate extra trailing 2 squotes with data $v($parentlevel)" - } - } - } - } - } - triple_squote { - #presumably popping multiliteral-space - ::tomlish::log::debug "---- triple_squote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTILITERAL { - lappend merged $part - } - LITERALPART { - if {$lasttype eq "LITERALPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_squote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - tentative_accum_dquote { - #should only apply within a multistring - #### - set do_append_to_parent 0 ;#mark false to indicate we will do our own appends if needed - #Without this - we would get extraneous empty list entries in the parent - # - as the trailing-dquote-space isn't a space level from the toml perspective - # - the use of a space is to give us a hook here to (possibly) integrate extra quotes into the parent space when we pop - #assert prevstate always trailing-dquote-space - #dev guardrail - remove? assertion lib? - switch -exact -- $prevstate { - trailing-dquote-space { - } - default { - error "--- unexpected popped due to tentative_accum_dquote but came from state '$prevstate' should have been trailing-dquote-space" - } - } - switch -- $tok { - {"} { - tomlish::parse::set_token_waiting type single_dquote value $tok complete 1 startindex [expr {$i -1}] - } - {""} { - #review - we should perhaps return double_dquote instead? - #tomlish::parse::set_token_waiting type literal value "" complete 1 - tomlish::parse::set_token_waiting type double_dquote value "" complete 1 startindex [expr {$i - 2}] - } - {"""} { - #### - #if already an eof in token_waiting - set_token_waiting will insert before it - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 3}] - } - {""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i - 4}] - #todo integrate left dquote with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {"}] - } - MULTISTRING { - #empty - lappend v($parentlevel) [list STRINGPART {"}] - } - default { - error "--- don't know how to integrate extra trailing dquote with data $v($parentlevel)" - } - } - } - {"""""} { - tomlish::parse::set_token_waiting type triple_dquote value $tok complete 1 startindex [expr {$i-5}] - #todo integrate left 2 dquotes with nest data at this level - set lastpart [lindex $v($parentlevel) end] - switch -- [lindex $lastpart 0] { - STRINGPART { - set newval "[lindex $lastpart 1]\"\"" - set parentdata $v($parentlevel) - lset parentdata end [list STRINGPART $newval] - set v($parentlevel) $parentdata - } - NEWLINE - CONT - WS { - lappend v($parentlevel) [list STRINGPART {""}] - } - MULTISTRING { - lappend v($parentlevel) [list STRINGPART {""}] - } - default { - error "--- don't know how to integrate extra trailing 2 dquotes with data $v($parentlevel)" - } - } - } - } - } - triple_dquote { - #presumably popping multistring-space - ::tomlish::log::debug "---- triple_dquote for last_space_action pop leveldata: $v($nest)" - set merged [list] - set lasttype "" - foreach part $v($nest) { - switch -exact -- [lindex $part 0] { - MULTISTRING { - lappend merged $part - } - STRINGPART { - if {$lasttype eq "STRINGPART"} { - set prevpart [lindex $merged end] - lset prevpart 1 [lindex $prevpart 1][lindex $part 1] - lset merged end $prevpart - } else { - lappend merged $part - } - } - CONT - WS { - lappend merged $part - } - NEWLINE { - #note that even though first newline ultimately gets stripped from multiliterals - that isn't done here - #we still need the first one for roundtripping. The datastructure stage is where it gets stripped. - lappend merged $part - } - default { - error "---- triple_dquote unhandled part type [lindex $part 0] unable to merge leveldata: $v($nest)" - } - } - set lasttype [lindex $part 0] - } - set v($nest) $merged - } - equal { - #pop caused by = - switch -exact -- $prevstate { - dottedkey-space { - tomlish::log::debug "---- equal ending dottedkey-space for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - dottedkey-space-tail { - #experiment? - tomlish::log::debug "---- equal ending dottedkey-space-tail for last_space_action pop" - #re-emit for parent space - tomlish::parse::set_token_waiting type equal value = complete 1 startindex [expr {$i-1}] - } - } - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - tablename { - #note: a tablename only 'pops' if we are greater than zero - error "---- tablename pop should already have been handled as special case zeropoppushspace in goNextState" - } - tablearrayname { - #!review - tablearrayname different to tablename regarding push/pop? - #note: a tablename only 'pops' if we are greater than zero - error "---- tablearrayname pop should already have been handled as special case zeropoppushspace in goNextState" - } - endarray { - #nothing to do here. - } - comma { - #comma for inline table will pop the keyvalue space - lappend v($nest) "SEP" - } - endinlinetable { - ::tomlish::log::debug "---- endinlinetable for last_space_action pop" - } - default { - error "---- unexpected tokenType '$tokenType' for last_space_action 'pop'" - } - } - if {$do_append_to_parent} { - #e.g tentative_accum_squote does it's own appends as necessary - so won't get here - lappend v($parentlevel) [set v($nest)] - } - - incr nest -1 - - } elseif {$last_space_action eq "push"} { - set prevnest $nest - incr nest 1 - set v($nest) [list] - # push_trigger_tokens: barekey dquotedkey startinlinetable startarray tablename tablearrayname - - - switch -exact -- $tokenType { - tentative_trigger_squote - tentative_trigger_dquote { - #### this startok will always be tentative_accum_squote/tentative_accum_dquote starting with one accumulated squote/dquote - if {[dict exists $transition_info starttok] && [dict get $transition_info starttok] ne ""} { - lassign [dict get $transition_info starttok] starttok_type starttok_val - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType $starttok_type - set tok $starttok_val - } - } - single_squote { - #JMN - REVIEW - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - triple_squote { - ::tomlish::log::debug "---- push trigger tokenType triple_squote" - set v($nest) [list MULTILITERAL] ;#container for NEWLINE,LITERALPART - } - squotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - triple_dquote { - set v($nest) [list MULTISTRING] ;#container for NEWLINE,STRINGPART,CONT - } - dquotedkey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - barekey { - switch -exact -- $prevstate { - table-space - itable-space { - set v($nest) [list DOTTEDKEY] - } - } - #todo - check not something already waiting? - set waiting [tomlish::parse::get_token_waiting] - if {[llength $waiting]} { - set i [dict get $waiting startindex] - tomlish::parse::clear_token_waiting - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } else { - tomlish::parse::set_token_waiting type $tokenType value $tok complete 1 startindex [expr {$i -[tcl::string::length $tok]}] ;#re-submit token in the newly pushed space - } - } - tablename { - #note: we do not use the output of tablename_trim to produce a tablename for storage in the tomlish list! - #The tomlish list is intended to preserve all whitespace (and comments) - so a roundtrip from toml file to tomlish - # back to toml file will be identical. - #It is up to the datastructure stage to normalize and interpret tomlish for programmatic access. - # we call tablename_trim here only to to validate that the tablename data is well-formed at the outermost level, - # so we can raise an error at this point rather than create a tomlish list with obviously invalid table names from - # a structural perspective. - - #todo - review! It's arguable that we should not do any validation here, and just store even incorrect raw tablenames, - # so that the tomlish list is more useful for say a toml editor. Consider adding an 'err' tag to the appropriate place in the - # tomlish list? - - #set trimtable [tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablename: '$trimtable'" - set v($nest) [list TABLE $tok] ;#$tok is the *raw* table name - #note also that equivalent tablenames may have different toml representations even after being trimmed! - #e.g ["x\t\t"] & ["x "] (tab escapes vs literals) - #These will show as above in the tomlish list, but should normalize to the same tablename when used as keys by the datastructure stage. - } - tablearrayname { - #set trimtable [tablename_trim $tok] - #::tomlish::log::debug "---- trimmed (but not normalized) tablearrayname: '$trimtable'" - set v($nest) [list TABLEARRAY $tok] ;#$tok is the *raw* tablearray name - } - startarray { - set v($nest) [list ARRAY] ;#$tok is just the opening bracket - don't output. - } - startinlinetable { - set v($nest) [list ITABLE] ;#$tok is just the opening curly brace - don't output. - } - default { - error "---- push trigger tokenType '$tokenType' not yet implemented" - } - } - - } else { - #no space level change - switch -exact -- $tokenType { - squotedkey { - #puts "---- squotedkey in state $prevstate (no space level change)" - lappend v($nest) [list SQKEY $tok] - } - dquotedkey { - #puts "---- dquotedkey in state $prevstate (no space level change)" - lappend v($nest) [list DQKEY $tok] - } - barekey { - lappend v($nest) [list KEY $tok] - } - dotsep { - lappend v($nest) [list DOTSEP] - } - starttablename { - #$tok is triggered by the opening bracket and sends nothing to output - } - starttablearrayname { - #$tok is triggered by the double opening brackets and sends nothing to output - } - tablename - tablenamearray { - error "---- did not expect 'tablename/tablearrayname' without space level change (no space level change)" - #set v($nest) [list TABLE $tok] - } - endtablename - endtablearrayname { - #no output into the tomlish list for this token - } - startinlinetable { - puts stderr "---- decode::toml error. did not expect startinlinetable without space level change (no space level change)" - } - single_dquote { - switch -exact -- $newstate { - string-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "string" - set tok "" - } - dquoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "dquotedkey" - set tok "" - } - multistring-space { - lappend v($nest) [list STRINGPART {"}] - #may need to be joined on pop if there are neighbouring STRINGPARTS - } - default { - error "---- single_dquote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_dquote { - #leading extra quotes - test: toml_multistring_startquote2 - switch -exact -- $prevstate { - itable-keyval-value-expected - keyval-value-expected { - puts stderr "tomlish::decode::toml double_dquote TEST" - #empty string - lappend v($nest) [list STRINGPART ""] - } - multistring-space { - #multistring-space to multistring-space - lappend v($nest) [list STRINGPART {""}] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - - } - single_squote { - switch -exact -- $newstate { - literal-state { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "literal" - set tok "" - } - squoted-key { - set next_tokenType_known 1 - ::tomlish::parse::set_tokenType "squotedkey" - set tok "" - } - multiliteral-space { - #false alarm squote returned from tentative_accum_squote pop - ::tomlish::log::debug "---- adding lone squote to own LITERALPART nextstate: $newstate (no space level change)" - #(single squote - not terminating space) - lappend v($nest) [list LITERALPART '] - #may need to be joined on pop if there are neighbouring LITERALPARTs - } - default { - error "---- single_squote switch case not implemented for nextstate: $newstate (no space level change)" - } - } - } - double_squote { - switch -exact -- $prevstate { - keyval-value-expected { - lappend v($nest) [list LITERAL ""] - } - multiliteral-space { - #multiliteral-space to multiliteral-space - lappend v($nest) [list LITERALPART ''] - } - default { - error "--- unhandled tokenType '$tokenType' when transitioning from state $prevstate to $newstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - enddquote { - #nothing to do? - set tok "" - } - endsquote { - set tok "" - } - string { - #JJJJ - set tok [tomlish::from_Bstring $tok] - lappend v($nest) [list STRING $tok] ;#directly wrapped in dquotes - } - literal { - lappend v($nest) [list LITERAL $tok] ;#directly wrapped in squotes - } - multistring { - #review - #JJJJ ? - lappend v($nest) [list MULTISTRING $tok] - } - stringpart { - #JJJJ - set tok [tomlish::from_Bstring $tok] - lappend v($nest) [list STRINGPART $tok] ;#will not get wrapped in dquotes directly - } - multiliteral { - lappend v($nest) [LIST MULTILITERAL $tok] - } - literalpart { - lappend v($nest) [list LITERALPART $tok] ;#will not get wrapped in squotes directly - } - untyped_value { - #would be better termed unclassified_value - #we can't determine the type of unquoted values (int,float,datetime,bool) until the entire token was read. - unset -nocomplain tag - if {$tok in {true false}} { - set tag BOOL - } else { - if {[::tomlish::utils::is_int $tok]} { - set tag INT - } else { - if {[::tomlish::utils::string_is_integer -strict $tok]} { - #didn't qualify as a toml int - but still an int - #probably means is_int is limiting size and not accepting bigints (configurable?) - #or it didn't qualify due to more than 1 leading zero - #or other integer format issue such as repeated underscores - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. (looks close to being an int. Formatting or range issue?) [tomlish::parse::report_line] (no space level change)" - } else { - #DDDD - if {[::tomlish::utils::is_float $tok]} { - set tag FLOAT - } elseif {[::tomlish::utils::is_time-local $tok]} { - set tag TIME-LOCAL - } elseif {[::tomlish::utils::is_timepart $tok]} { - ###################################### - #Note we must allow lone timepart here (not just is_time-local which doesn't allow tz offsets) in case it followed a previous localdate - #set tag DATETIME ;#PLACEHOLDER tag - review standalone time with tz - no specific tag - only allowed as followup value from DATE-LOCAL - set tag TIME-TZ - #This will become a DATETIME or a DATETIME-LOCAL (or will error) - ###################################### - } elseif {[::tomlish::utils::is_date-local $tok]} { - set tag DATE-LOCAL - } elseif {[::tomlish::utils::is_date_or_time_or_datetime $tok]} { - #not just a date or just a time - #could be either local or have tz offset - #DDDD JJJ - set norm [string map {" " T} $tok];#prob unneeded - we won't get here if there was a space - would arrive as 2 separate tokens review. - lassign [split $norm T] dp tp - if {[::tomlish::utils::is_time-local $tp]} { - set tag DATETIME-LOCAL - } else { - set tag DATETIME - } - } else { - error "---- Unable to interpret '$tok' as Boolean, Integer, Float or Datetime as per the toml specs. [tomlish::parse::report_line] (no space level change)" - } - } - } - } - #assert either tag is set, or we errored out. - lappend v($nest) [list $tag $tok] - - } - comment { - #puts stdout "----- comment token returned '$tok'------" - #JJJJ - set tok [tomlish::from_comment $tok] - lappend v($nest) [list COMMENT "$tok"] - } - equal { - #we append '=' to the nest so that any surrounding whitespace is retained. - lappend v($nest) = - } - comma { - lappend v($nest) SEP - } - newline { - incr linenum - lappend v($nest) [list NEWLINE $tok] - } - whitespace { - lappend v($nest) [list WS $tok] - } - continuation { - lappend v($nest) CONT - } - bom { - lappend v($nest) BOM - } - eof { - #ok - nothing more to add to the tomlish list. - #!todo - check previous tokens are complete/valid? - } - default { - error "--- unknown tokenType '$tokenType' during state $prevstate [::tomlish::parse::report_line] (no space level change)" - } - } - } - - if {!$next_tokenType_known} { - ::tomlish::log::notice "---- tomlish::decode::toml - current tokenType:$tokenType Next token type not known" - ::tomlish::parse::set_tokenType "" - set tok "" - } - - if {$state eq "end-state"} { - break - } - - - } - - #while {$nest > 0} { - # lappend v([expr {$nest -1}]) [set v($nest)] - # incr nest -1 - #} - while {[::tomlish::parse::spacestack size] > 1} { - ::tomlish::parse::spacestack pop - lappend v([expr {$nest -1}]) [set v($nest)] - incr nest -1 - - #set parent [spacestack peek] ;#the level being appended to - #lassign $parent type state - #if {$type eq "space"} { - # - #} elseif {$type eq "buffer"} { - # lappend v([expr {$nest -1}]) {*}[set v($nest)] - #} else { - # error "invalid spacestack item: $parent" - #} - } - - } finally { - set is_parsing 0 - } - return $v(0) - } - - #toml dquoted string to tomlish STRING - # - only allow specified escape sequences - # - allow any unicode except those that must be escaped: dquote, bsl, and control chars(except tab) - proc from_Bstring {bstr} { - #JJJJ - if {[catch { - tomlish::utils::unescape_string $bstr - } errM]} { - return -code error -errorcode {TOML SYNTAX INVALIDESCAPE} "tomlish::from_Bstring toml Bstring contains invalid escape sequence\n$errM" ;#review - } - #assert: all escapes are now valid - - if {[regexp {[\u0000-\u0008\u000A-\u001F\u007f]} $bstr]} { - set msg "tomlish::from_Bstring toml Bstring contains controls that must be escaped" - return -code error -errorcode {TOML SYNTAX BSTRINGUNESCAPEDCONTROLS} $msg ;#review - } - return $bstr - } - #validate toml comment - # - disallow controls that must be escaped - #from spec: - # "Control characters other than tab (U+0000 to U+0008, U+000A to U+001F, U+007F) are not permitted in comments." - proc from_comment {comment} { - if {[regexp {[\u0000-\u0008\u000A-\u001F\u007f]} $comment]} { - set msg "tomlish::from_comment toml comment contains controls that must be escaped" - return -code error -errorcode {TOML SYNTAX COMMENTUNESCAPEDCONTROLS} $msg ;#review - } - return $comment - } - - - #return TOMLISH { value} from new and existing typeval dicts of form {type value value} but - # some such as MULTISTRING can be of form { ...} - # - #Don't validate here - validate in tomlish::dict::path::setleaf - proc _update_tomlish_typeval_convert_to_new_from_existing {new existing} { - #we deliberately don't support container types that can contain comments e.g ARRAY, ITABLE, DOTTEDKEY - #This is also not for higher level constructs such as TABLE, TABLEARRAY - if {!([tomlish::dict::is_typeval $target] && [tomlish::dict_is_typveval $source])} { - error "_update_tomlish_typeval_convert_to: target and source must be of form {type value are contained in the table - foreach tr $tablechildren { - set tr_type [lindex $tr 0] - switch -- $tr_type { - NEWLINE - WS - COMMENT { - lappend updated_tablechildren $tr - } - DOTTEDKEY { - #review - #UUU - set dktomlish [list TOMLISH $tr] - set dkdict [::tomlish::to_dict $dktomlish] - set newdktomlish [update_tomlish_from_dict $dktomlish $subd] - set newrecords [lrange $newdktomlish 1 end];#strip TOMLISH - lappend updated_tablechildren {*}$newrecords - } - default { - error "update_tomlish_from_dict: unexpected table record type $tr_type" - } - } - } - - #todo - add leaves from subd that weren't in the tablechildren list - #ordering? - - lappend output_tomlish [list {*}[lrange $tomlish_record 0 1] {*}$updated_tablechildren] - } - DOTTEDKEY { - #We don't have to check toml table rules regarding created/defined here as dict::from_tomlish has already ensured correctness - #UUU - set dkinfo [tomlish::get_dottedkey_info $tomlish_record] ;#e.g keys {j { k} l} keys_raw {j {' k'} l} - set keys [dict get $dkinfo keys] - set dk_refpath [lmap k $keys {string cat @@ $k}] - - set kvinfo [tomlish::_get_keyval_value $tomlish_record] - set existing_typeval [dict get $kvinfo result] - if {[tomlish::dict::is_typeval $existing_typeval] && [dict get $existing_typeval type] ne "ARRAY"} { - #leaf in supplied tomlish - source dict must also be leaf (invalid to rewrite a branch) - #e.g - #DOTTEDKEY {{KEY j} DOTSEP {SQKEY { k}} DOTSEP {KEY l}} = {INT 0} {WS { }} {COMMENT comment} {NEWLINE lf} - #existing_typeval: {type INT value 0} - #e.g - #DOTTEDKEY {{KEY j} DOTSEP {SQKEY { k}} DOTSEP {KEY l}} = {MULTISTRING {WS { }} {STRINGPART x} {WS { }}} {WS { }} {COMMENT comment} {NEWLINE lf} - #existing_typeval: {type MULTISTRING value { x }} - - #see if source dict has a simple typeval to set - set new_typeval [tomlish::dict::path::get $d $dk_refpath] - if {![tomlish::dict::is_typeval $new_typeval]} { - error "update_tomlish_from_dict - update dictionary has non-leaf data at path $dk_refpath - cannot set" - } - #update if type matches. Todo - flag -allowtypechange ? - set e_type [dict get $existing_typeval type] - set n_type [dict get $new_typeval type] - if {$e_type ne $n_type} { - error "update_tomlish_from_dict - cannot change type $e_type to $n_type at path $dk_refpath" - } - #-start 3 to begin search after = - set valindex [lsearch -start 3 -index 0 $tomlish_record $e_type] - if {$valindex == -1} { - error "update_tomlish_from_dict - unexpected error - failed to find $e_type in record $tomlish_record" - } - set rawval [dict get $new_typeval value] - switch -- $e_type { - MULTISTRING { - #UUU - set newval [tomlish::utils::rawstring_to_MultiBstring_with_escaped_controls $rawval] - set toml "" - append toml "x=\"\"\"" \n - append toml "$newval\"\"\"" \n - set tomlish [lrange [tomlish::from_toml $toml] 1 end] ;#remove TOMLISH keyword - #assert tomlish is a list with a single element - #e.g {DOTTEDKEY {{KEY x}} = {MULTISTRING {NEWLINE lf} {STRINGPART aaa}} {NEWLINE lf}} - set dklist [lindex $tomlish 0] - set msrecord [lindex $dklist 3] - #e.g - #MULTISTRING {NEWLINE lf} {STRINGPART aaa} - - #error "update_tomlish_from_dict MULTISTRING update unimplemented. Todo" - lset tomlish_record $valindex $msrecord - } - MULTILITERAL { - set toml "" - append toml "x='''" \n - append toml "$rawval'''" \n - set tomlish [lrange [tomlish::from_toml $toml] 1 end] ;#remove TOMLISH keyword - set dklist [lindex $tomlish 0] - set msrecord [lindex $dklist 3] - lset tomlish_record $valindex $msrecord - } - default { - switch -- $e_type { - STRING { - #review - set newval [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $rawval] - } - default { - set newval $rawval - } - } - lset tomlish_record $valindex [list $e_type $newval] - } - } - - } elseif {[tomlish::dict::is_typeval $existing_typeval] && [dict get $existing_typeval type] eq "ARRAY"} { - #e.g - #DOTTEDKEY {{KEY a}} = {ARRAY {INT 1} SEP {INT 2} SEP {INT 3}} - #DOTTEDKEY {{KEY a} {WS { }}} = {WS { }} {ARRAY {INT 1} {WS { }} SEP {INT 2} {WS { }} SEP {INT 3}} {WS { }} - #existing_typeval: {type ARRAY value {{type INT value 1} {type INT value 2} {type INT value 3}}} - - #= is always at index 2 (any preceding whitespace is attached to keylist) - set valindex [lsearch -start 3 -index 0 $tomlish_record ARRAY] - if {$valindex == -1} { - error "update_tomlish_from_dict - unexpected error - failed to find ARRAY in record $tomlish_record" - } - - set existing_arraytomlish [lindex $tomlish_record $valindex] - puts "update_tomlish_from_dict: existing_arraytomlish: $existing_arraytomlish" - set subd [tomlish::dict::path::get $d $dk_refpath] - #set existing_items [tomlish::dict::from_tomlish $tomlish_record] ;#utilise fragment processing of dict::from_tomlish - to produce a LIST - #we expect the subdict structure to be something like: - # {type ARRAY value {{type INT value 1} {type INT value 2}}} - # or with untagged subdicts (ITABLE in tomlish) - # {type ARRAY value {{x {type INT value 1}} {type INT value 2}}} - - - #we can only have one ARRAY record - so we can use lset - set newsubrecord_itable [update_tomlish_from_dict [list $existing_arraytomlish] $subd] - lset tomlish_record $valindex [lindex $newsubrecord_itable 0] ;#passed in a single element tomlish list - expect only one back - - } elseif {[tomlish::dict::is_typeval_dict $existing_typeval]} { - #Not actually a {type value } structure. - #sub dict (ITABLE) - #e.g - #DOTTEDKEY {{KEY j} DOTSEP {SQKEY { k}} DOTSEP {KEY l}} = {ITABLE {DOTTEDKEY {{KEY q}} = {INT 1}}} {WS { }} {COMMENT comment} {NEWLINE lf} - #DOTTEDKEY {{KEY x} {WS { }}} = {WS { }} {ITABLE {WS { }} {DOTTEDKEY {{KEY j}} = {INT 1} {WS { }} SEP} {WS { }} {DOTTEDKEY {{KEY k} {WS { }}} = {WS { }} {INT 333}}} {WS { }} {COMMENT {test }} - #existingvaldata: {q {type INT value 1}} - set subd [tomlish::dict::path::get $d $dk_refpath] - #= is always at index 2 (any preceding whitespace is attached to keylist) - set valindex [lsearch -start 3 -index 0 $tomlish_record ITABLE] - if {$valindex == -1} { - error "update_tomlish_from_dict - unexpected error - failed to find ITABLE in record $tomlish_record" - } - #we can only have one ITABLE record - so we can use lset - - set itablerecord [lindex $tomlish_record $valindex] - puts "update_tomlish_from_dict: existing_itabletomlish: $itablerecord" - set newsubrecord_itable [update_tomlish_from_dict [list $itablerecord] $subd] - lset tomlish_record $valindex [lindex $newsubrecord_itable 0] - } else { - #unreachable? - dict::from_tomlish didn't object. - error "update_tomlish_from_dict: Unexpected data in DOTTEDKEY record: $existing_typeval" - } - lappend output_tomlish $tomlish_record - } - ARRAY { - #UUU - #fragment recursion - puts "update_tomlish_from_dict: process ARRAY fragment" - puts "tomlish:\n$tomlish" - puts "updatedict:\n$d" - set source_d_elements [tomlish::dict::path::get $d {[]}] - - set updated_arraychildren [list] - set arrayrecord $tomlish_record - set arraychildren [lrange $arrayrecord 1 end] ;#includes WS, SEP, NEWLINE, COMMENT - set arridx 0 - set childidx 0 - foreach arrchild $arraychildren { - set arrchild_type [lindex $arrchild 0] - switch -- $arrchild_type { - SEP { - #we don't check for proper SEP interspersal here, presuming well-formed tomlish - review - lappend updated_arraychildren $arrchild - } - NEWLINE - WS - COMMENT { - lappend updated_arraychildren $arrchild - } - default { - #updatables - #review - type changes from existing value?? - set sourcedata [lindex $source_d_elements $arridx] - #todo - what happens when less source elements than in existing array? ie sourcedata is empty. - # - switch -- $arrchild_type { - STRING - LITERAL - FLOAT - INT - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - #basic types - no recursion needed - #REVIEW - change of type? flag to allow/disallow? - if {![tomlish::dict::is_typeval $sourcedata]} { - error "update_tomlish_from_dict - update dictionary has non-leaf data at path \[$arridx\] - cannot set" - } - set newval [dict get $sourcedata value] - set newtype [dict get $sourcedata type] - if {$newtype eq "STRING"} { - set newval [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $newval] - } - lappend updated_arraychildren [list $newtype $newval] - } - MULTISTRING { - #no need to recurse - puts stderr "multistring within array update - unimplemented" - } - MULTILITERAL { - #no need to recurse - puts stderr "multiliteral within array update - unimplemented" - } - ITABLE - ARRAY { - #recurse - puts stderr "update $arrchild_type within array" - set nextd [tomlish::dict::path::get $d $arridx] - set subrecord_tomlish [list $arrchild] - set newsubrecord_tomlish [update_tomlish_from_dict $subrecord_tomlish $nextd] - lappend updated_arraychildren {*}$newsubrecord_tomlish - } - default { - error "update_tomlish_from_dict: unexpected array child record type $arrchild_type" - } - } - incr arridx ;#only increment array index for updatables - } - } - } - - lappend output_tomlish [list ARRAY {*}$updated_arraychildren] - } - ITABLE { - #fragment recursion target - #ITABLE {DOTTEDKEY {{KEY j}} = {INT 1}} - #ITABLE {WS { }} {DOTTEDKEY {{KEY j}} = {INT 1} {WS { }} SEP} {WS { }} {DOTTEDKEY {{KEY k} {WS { }}} = {WS { }} {INT 333}} - #ITABLE {NEWLINE lf} {DOTTEDKEY {{KEY j} {WS { }}} = {WS { }} {INT 1} SEP} {WS { }} {COMMENT test} {NEWLINE lf} {WS { }} {DOTTEDKEY {{KEY k}} = {WS { }} {INT 2} {NEWLINE lf}} - puts "update_tomlish_from_dict: process ITABLE fragment" - puts "tomlish:\n$tomlish" - puts "updatedict:\n$d" - set updated_itablechildren [list] - set itablechildren [lrange $tomlish_record 1 end] ;#includes WS, NEWLINE, COMMENT (possibly SEP - though it may be attached to DOTTEDKEY record REVIEW) - #we only expect DOTTEDKEY records for data items within ITABLE - foreach itablechild $tomlish_record { - set itablechild_type [lindex $itablechild 0] - switch -- $itablechild_type { - SEP { - #REVIEW - #we don't necessarily expect a SEP *directly* within ITABLE records as currently when they're created by tomlish::from_toml - #it attaches them (along with intervening WS, COMMENTs) to each DOTTEDKEY record - #This feels somewhat misaligned with ARRAY - where we have no choice but to have SEP, and COMMENTs independent of the array elements. - #Attaching COMMENTs, SEP to the previous DOTTEDKEY has some merit - but perhaps consistency with ARRAY would be preferable. - #This may change - but in any case it should probably be valid/handled gracefully either way. - lappend updated_itablechildren $itablechild - } - COMMENT - WS - NEWLINE { - lappend updated_itablechildren $itablechild - } - DOTTEDKEY { - puts stderr "update dottedkey in itable: tomlish:[list $itablechild] d:$d" - set updatedtomlish [update_tomlish_from_dict [list $itablechild] $d] - set newrecord [lindex $updatedtomlish 0] - lappend updated_itablechildren $newrecord - } - } - } - - lappend output_tomlish [list ITABLE {*}$updated_itablechildren] - } - default { - error "update_tomlish_from_dict: Unexpected toplevel type $tomlish_type record: $tomlish_record" - } - } - } - return $output_tomlish - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -namespace eval tomlish::build { - #STRING,INT,FLOAT,BOOL, DATETIME - simple wrappers for completeness - # take a value of the appropriate type and wrap as a tomlish tagged item - proc STRING {s} { - return [list STRING [::tomlish::utils::rawstring_to_Bstring_with_escaped_controls $s]] - } - proc LITERAL {litstring} { - error todo - } - - proc INT {i} { - #whole numbers, may be prefixed with a + or - - #Leading zeros are not allowed - #Hex,octal binary forms are allowed (toml 1.0) - #We will error out on encountering commas, as commas are interpreted differently depending on locale (and don't seem to be supported in the toml spec anyway) - #!todo - Tcl can handle bignums - bigger than a 64bit signed long as specified in toml. - # - We should probably raise an error for number larger than this and suggest the user supply it as a string? - if {[tcl::string::last , $i] > -1} { - error "Unable to interpret '$i' as an integer. Use underscores if you need a thousands separator [::tomlish::parse::report_line]" - } - if {![::tomlish::utils::int_validchars $i]} { - error "Unable to interpret '$i' as an integer. Only 0-9 + 1 _ characters are acceptable. [::tomlish::parse::report_line]" - } - - if {[::tomlish::utils::is_int $i]} { - return [list INT $i] - } else { - error "'$i' is not a valid integer as per the Toml spec. [::tomlish::parse::report_line]" - } - - } - - proc FLOAT {f} { - #convert any non-lower case variants of special values to lowercase for Toml - if {[::tcl::string::tolower $f] in {nan +nan -nan inf +inf -inf}} { - return [list FLOAT [tcl::string::tolower $f]] - } - if {[::tomlish::utils::is_float $f]} { - return [list FLOAT $f] - } else { - error "Unable to interpret '$f' as Toml float. Check your input, or check that tomlish is able to handle all Toml floats properly [::tomlish::parse::report_line]" - } - } - - proc DATETIME {str} { - if {[::tomlish::utils::is_date_or_time_or_datetime $str]} { - return [list DATETIME $str] - } else { - error "Unable to interpret '$str' as Toml datetime. Check your input, or check that tomlish is able to handle all Toml datetimes properly [::tomlish::parse::report_line]" - } - } - proc DATETIME-LOCAL {str} { - error "build::DATETIME-LOCAL todo" - } - - proc BOOLEAN {b} { - #convert any Tcl-acceptable boolean to boolean as accepted by toml - lower case true/false - if {![tcl::string::is boolean -strict $b]} { - error "Unable to convert '$b' to Toml boolean true|false. [::tomlish::parse::report_line]" - } else { - if {$b && 1} { - return [::list BOOL true] - } else { - return [::list BOOL false] - } - } - } - - #REVIEW - #Take tablename followed by - # a) *tomlish* name-value pairs e.g table mydata [list KEY item11 = [list STRING "test"]] {KEY item2 = [list INT 1]} - # (accept also key value {STRING }) - # b) simple 2-element tcl lists being name & *simple* value pairs for which basic heuristics will be used to determine types - proc _table {name args} { - set pairs [list] - foreach t $args { - if {[llength $t] == 4} { - if {[tcl::string::tolower [lindex $t 0]] ne "key" || [tcl::string::tolower [lindex $t 2]] ni "= value"} { - error "Only items tagged as KEY = currently accepted as name-value pairs for table command" - } - lassign $t _k keystr _eq valuepart - if {[llength $valuepart] != 2} { - error "supplied value must be typed. e.g {INT 1} or {STRING test}" - } - lappend pairs [list KEY $keystr = $valuepart] - } elseif {[llength $t] == 2} { - #!todo - type heuristics - lassign $t n v - lappend pairs [list KEY $n = [list STRING $v]] - } else { - error "'KEY = { toml but - # the first newline is not part of the data. - # we elect instead to maintain a basic LITERALPART that must not contain newlines.. - # and to compose MULTILITERAL of multiple NEWLINE LITERALPART parts, - #with the datastructure representation dropping the first newline (if immediately following opening delim) when building the value. - set literal "" - foreach part [lrange $item 1 end] { - append literal [::tomlish::encode::tomlish [list $part] $nextcontext] - } - append toml '''$literal''' - } - INT - - BOOL - - FLOAT - - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - #DDDD - append toml [lindex $item 1] - } - INCOMPLETE { - error "cannot process tomlish term tagged as INCOMPLETE" - } - COMMENT { - append toml "#[lindex $item 1]" - } - BOM { - #Byte Order Mark may appear at beginning of a file. Needs to be preserved. - append toml "\uFEFF" - } - default { - error "Not a properly formed 'tomlish' taggedlist.\n '$list'\n Unknown tag '[lindex $item 0]'. See output of \[tomlish::tags\] command." - } - } - - } - return $toml - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::encode ---}] -} -#fish toml from tomlish - -#(encode tomlish as toml) -interp alias {} tomlish::to_toml {} tomlish::encode::tomlish - -# - - -namespace eval tomlish::decode { - #*** !doctools - #[subsection {Namespace tomlish::decode}] - #[para] - #[list_begin definitions] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::decode ---}] -} -#decode toml to tomlish -#interp alias {} tomlish::from_toml {} tomlish::decode::toml - -namespace eval tomlish::utils { - #*** !doctools - #[subsection {Namespace tomlish::utils}] - #[para] - #[list_begin definitions] - - #------------------------------------------------------------------------------ - # Tcl 8.6 support - #------------------------------------------------------------------------------ - if {[catch {tcl::string::is dict {}}]} { - proc string_is_dict {str} { - #we don't support -strict or -failindex for this fallback - expr {[::tcl::string::is list $str] && ([llength $str] % 2 == 0)} - } - } else { - proc string_is_dict {str} { - #we don't support -strict or -failindex for this fallback even though underlying supports it - ::tcl::string::is dict $str - } - } - if {![string is integer [expr {2**32}]]} { - proc string_is_integer {args} { - ::tcl::string::is entier {*}$args - } - } else { - proc string_is_integer {args} { - ::tcl::string::is integer {*}$args - } - } - #------------------------------------------------------------------------------ - - #subset of jq syntax for get/set operations on dicts - # no filters or multiple targets - # meant for 'leaf' queries - proc jq_to_path {jq} { - set jq [string trim $jq] ;#don't tokenize any leading/trailing whitespace - set path [list] - set in_arr 0 - set in_dq 0 - set tok "" - set bsl 0 - foreach c [split $jq ""] { - if {$c eq "\\"} { - if {$bsl} { - set bsl 0 - set c "\\" - } else { - set bsl 1 - continue - } - } else { - if {$bsl} { - set c "\\$c" - set bsl 0 - } - } - if {$in_arr} { - switch -- $c { - {]} { - set in_arr 0 - lappend path $tok - set tok "" - } - default { - append tok $c - } - } - } elseif {$in_dq} { - if {$c eq "\""} { - set in_dq 0 - #append tok "\"" - lappend path $tok - set tok "" - } else { - append tok $c - } - } else { - switch -- $c { - . { - if {$tok ne ""} { - lappend path $tok - } - set tok "@@" - } - {[} { - if {$tok ne ""} { - lappend path $tok - } - set in_arr 1 - set tok "" - } - {"} { - if {$tok eq "@@"} { - #set tok "@@\"" - set in_dq 1 - } else { - append tok "\"" - } - } - default { - append tok $c - } - } - } - } - if {$tok ne ""} { - lappend path $tok - } - return $path - } - proc path_to_jq {path} { - set jq "" - foreach p $path { - if {[string match @@* $p]} { - set key [string range $p 2 end] - if {![tomlish::utils::is_barekey $key]} { - set key [subst -nocommands -novariables $key] - set key "\"[tomlish::utils::rawstring_to_Bstring_with_escaped_controls $key]\"" - } - append jq ".$key" - } else { - append jq {[} $p {]} - } - } - return $jq - } - - - - #basic generic quote matching for single and double quotes - #note for example that {[o'malley]} will return sq - as the single quote is not closed or wrapped in double quotes - proc tok_in_quotedpart {tok} { - set sLen [tcl::string::length $tok] - set quote_type "" - set had_slash 0 - for {set i 0} {$i < $sLen} {incr i} { - set c [tcl::string::index $tok $i] - if {$quote_type eq ""} { - if {$had_slash} { - #don't enter quote mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - set quote_type dq - } - sq { - set quote_type sq - } - bsl { - set had_slash 1 - } - } - } - } else { - if {$had_slash} { - #don't leave quoted mode - #leave slash_mode because even if current char is slash - it is escaped - set had_slash 0 - } else { - set ctype [tcl::string::map [list {"} dq {'} sq \\ bsl] $c] - switch -- $ctype { - dq { - if {$quote_type eq "dq"} { - set quote_type "" - } - } - sq { - if {$quote_type eq "sq"} { - set quote_type "" - } - } - bsl { - set had_slash 1 - } - } - } - } - } - return $quote_type ;#dq | sq - } - - proc hex_escape_info {slashx} { - set exp {^\\x([0-9a-fA-F]{2}$)} - if {[regexp $exp $slashx match hex]} { - return [list ok [list char [subst -nocommand -novariable $slashx]]] - } else { - return [list err [list reason "Supplied string not of the form \\xHH where H in \[0-9a-fA-F\]"]] - } - } - proc unicode_escape_info {slashu} { - #!todo - # validate that slashu is either a \uxxxx or \Uxxxxxxxx value of the correct length and - # is a valid 'unicode scalar value' (any Unicode code point except high-surrogate and low-surrogate code points) - # ie integers in the range 0 to D7FF16 and E00016 to 10FFFF16 inclusive - #expr {(($x >= 0) && ($x <= 0xD7FF16)) || (($x >= 0xE00016) && ($x <= 0x10FFFF16))} - if {[tcl::string::match {\\u*} $slashu]} { - set exp {^\\u([0-9a-fA-F]{4}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %4x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } - } else { - return [list err [list reason "Supplied string not of the form \\uHHHH where H in \[0-9a-fA-F\]"]] - } - } elseif {[tcl::string::match {\\U*} $slashu]} { - set exp {^\\U([0-9a-fA-F]{8}$)} - if {[regexp $exp $slashu match hex]} { - if {[scan $hex %8x dec] != 1} { - #why would a scan ever fail after matching the regexp? !todo - review. unreachable branch? - return [list err [list reason "Failed to convert '$hex' to decimal"]] - } else { - if {(($dec >= 0) && ($dec <= 0xD7FF16)) || (($dec >= 0xE00016) && ($dec <= 0x10FFFF16))} { - return [list ok [list char [subst -nocommand -novariable $slashu]]] - } else { - return [list err [list reason "$slashu is not within the 'unicode scalar value' ranges 0 to 0xD7FF16 or 0xE00016 to 0x10FFFF16"]] - } - } - } else { - return [list err [list reason "Supplied string not of the form \\UHHHHHHHH where H in \[0-9a-fA-F\]"]] - } - } else { - return [list err [list reason "Supplied string did not start with \\u or \\U" ]] - } - - } - - #Note that unicode characters don't *have* to be escaped. - #So if we provide a function named 'escape_string', the name implies the inverse of unescape_string which unescapes unicode \u \U values. - #- an inverse of unescape_string would encode all unicode chars unnecessarily. - #- as toml accepts a compact escape sequence for common chars such as tab,backspace,linefeed etc but also allows the full form \u009 etc - #- escape_string and unescape_string would not be reliably roundtrippable inverses anyway. - #REVIEW - provide it anyway? When would it be desirable to use? - - variable Bstring_control_map [dict create] - dict set Bstring_control_map \b {\b} - dict set Bstring_control_map \n {\n} - dict set Bstring_control_map \r {\r} - dict set Bstring_control_map \" {\"} - dict set Bstring_control_map \x1b {\e} ;#In spec it's included in the list of 'must be escaped', as well as the 'convenience' escapes - so we make it go both ways. - dict set Bstring_control_map \\ "\\\\" - - #\e for \x1b seems like it might be included - v1.1?? hard to find current state of where toml is going :/ - #for a Bstring (Basic string) tab is explicitly mentioned as not being one that must be escaped. - #8 = \b - already in list. - #built the remainder whilst checking for entries already hardcoded above -in case more are added to the hardcoded list - for {set cdec 0} {$cdec <= 7} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - for {set cdec [expr {0x0A}]} {$cdec <= 0x1F} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Bstring_control_map $char]} { - dict set Bstring_control_map $char \\u$hhhh - } - } - # \u007F = 127 - dict set Bstring_control_map [format %c 127] \\u007F - - # ------------------------------------------------------------------ - variable Literal_control_map [dict create] - #controls other than tab - for {set cdec 0} {$cdec <= 8} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Literal_control_map $char]} { - dict set Literal_control_map $char \\u$hhhh - } - } - for {set cdec [expr {0x0A}]} {$cdec <= 0x1F} {incr cdec} { - set hhhh [format %.4X $cdec] - set char [format %c $cdec] - if {![dict exists $Literal_control_map $char]} { - dict set Literal_control_map $char \\u$hhhh - } - } - # \u007F = 127 - dict set Literal_control_map [format %c 127] \\u007F - # ------------------------------------------------------------------ - variable Multiliteral_control_map - set Multiliteral_control_map [dict remove $Literal_control_map \n] - - variable String_control_map - set String_control_map [dict remove $Literal_control_map \\] - - - variable MultiBstring_totoml_map - #'minimally' escaped sequences of double quotes. - #e.g {""\"""\"} vs {\"\"\"\"\"} - #This produces easier to read toml - and in many cases may be more likely to match original format when roundtripped from dict datastructure - # REVIEW - should this be configurable? - set MultiBstring_totoml_map [dict remove $Bstring_control_map {"} \r \n] - dict set MultiBstring_totoml_map {"""} {""\"} ;#" editor hack: commented quote for dumb syntax highlighers - - #Note the inclusion of backslash in the list of controls makes this non idempotent - subsequent runs would keep encoding the backslashes! - #escape only those chars that must be escaped in a Bstring (e.g not tab which can be literal or escaped) - #for example - can be used by from_dict to produce valid Bstring data for a tomlish record - proc rawstring_to_Bstring_with_escaped_controls {str} { - #for the well known chars that have compact escape sequences allowed by toml - we choose that form over the full \u form. - #we'll use a string map with an explicit list rather than algorithmic at runtime - # - the string map is probably more performant than splitting a string, especially if it's large - - upvar ::tomlish::utils::Bstring_control_map map - - return [string map $map $str] - } - proc rawstring_to_MultiBstring_with_escaped_controls {str} { - #for the well known chars that have compact escape sequences allowed by toml - we choose that form over the full \u form. - #we'll use a string map with an explicit list rather than algorithmic at runtime - # - the string map is probably more performant than splitting a string, especially if it's large - - upvar ::tomlish::utils::MultiBstring_totoml_map map - - return [string map $map $str] - } - - #anything is valid in this direction ?? review - #proc rawstring_is_valid_tomlstring {str} { - # #controls are allowed in this direction dict -> toml (they get quoted) - - # #check any existing escapes are valid - # if {[catch { - # unescape_string $str - # } errM]} { - # return 0 - # } - # return 1 - #} - - - #REVIEW - easier way to validate? regex? - #This is not used for the parsing of toml to tomlish, - # but can be used to validate for updating via dict e.g when setting with tomlish::dict::path::setleaf - proc inner_MultiBstring_is_valid_toml {str} { - set without_literal_backslashes [string map [list "\\\\" ""] $str] - #replace only escaped dquotes - use a placeholder - we don't want unescaped runs of dquotes merging. - set without_escaped_dquotes [string map [list "\\\"" ""] $without_literal_backslashes] - - if {[string first "\"\"\"" $without_escaped_dquotes] != -1} { - return 0 - } - #assert - all remaining backslashes are escapes - - #strip remaining dquotes - set dquoteless [string map [list "\"" ""] $without_escaped_dquotes] - #puts stderr "dquoteless: $dquoteless" - - #check any remaining escapes are valid - if {[catch { - #don't use the returned value - just check it - unescape_string $without_literal_backslashes - } errM]} { - return 0 - } - - - variable Bstring_control_map - #remove backslash from control map - we are happy with the remaining escapes (varying length) - set testmap [dict remove $Bstring_control_map "\\" \r \n] - set testval [string map $testmap $dquoteless] - #if they differ - there were raw controls - return [expr {$testval eq $dquoteless}] - } - proc inner_Bstring_is_valid_toml {str} { - set without_literal_backslashes [string map [list "\\\\" ""] $str] - #replace only escaped dquotes - use a placeholder - we don't want unescaped runs of dquotes merging. - set without_escaped_dquotes [string map [list "\\\"" ""] $without_literal_backslashes] - - #plain Bstring can't have unescaped dquotes at tall - if {[string first "\"" $without_escaped_dquotes] != -1} { - return 0 - } - #assert - all remaining backslashes are escapes - - #check any remaining escapes are valid - if {[catch { - #don't use the returned value - just check it - unescape_string $without_literal_backslashes - } errM]} { - return 0 - } - - variable Bstring_control_map - #remove backslash from control map - we are happy with the remaining escapes (varying length) - set testmap [dict remove $Bstring_control_map "\\"] - set testval [string map $testmap $without_escaped_dquotes] - #if they differ - there were raw controls - return [expr {$testval eq $without_escaped_dquotes}] - } - - proc rawstring_is_valid_literal {str} { - #detect control chars other than tab - variable Literal_control_map - set testval [string map $Literal_control_map $str] - return [expr {$testval eq $str}] - } - proc rawstring_is_valid_multiliteral {str} { - #detect control chars other than tab - variable Multiliteral_control_map - - set teststr [string map [list \r\n ok] $str] - - set testval [string map $Multiliteral_control_map $teststr] - return [expr {$testval eq $teststr}] - } - - #review - unescape what string? Bstring vs MLBstring? - #we should be specific in the function naming here - #used by dict::from_tomlish - so part of validation? - REVIEW - proc unescape_string {str} { - #note we can't just use Tcl subst because: - # it also transforms \a (audible bell) and \v (vertical tab) which are not in the toml spec. - # it would strip out backslashes inappropriately: e.g "\j" becomes just j - # it recognizes other escapes which aren't approprite e.g octal \nnn - # it replaces \ with a single whitespace (trailing backslash) - #This means we shouldn't use 'subst' on the whole string, but instead substitute only the toml-specified escapes (\r \n \b \t \f \\ \" \uhhhh & \Uhhhhhhhh - #plus \e for \x1b? - - set buffer "" - set buffer2 "" ;#buffer for 2 hex characters following a \x - set buffer4 "" ;#buffer for 4 hex characters following a \u - set buffer8 "" ;#buffer for 8 hex characters following a \u - - set sLen [tcl::string::length $str] - - #we need to handle arbitrarily long sequences of backslashes. \\\\\ etc - set slash_active 0 - set unicode2_active 0 - set unicode4_active 0 - set unicode8_active 0 - - ::tomlish::log::debug "unescape_string. got len [string length str] str $str" - - #!todo - check for invalid data in the form of a raw carriage return (decimal 13) without following linefeed? - set i 0 - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $str [expr {$i - 1}]] - } else { - set lastChar "" - } - - set c [tcl::string::index $str $i] - #::tomlish::log::debug "unescape_string. got char $c" ;#too much? - - ##---------------------- - ##as we are 'unescaping' - should we really be testing here for existing values that should have been escaped? - ##The answer is probably no - keep this function to a single purpose - test elsewhere for raw controls. - ##this test looks incomplete anyway REVIEW - #scan $c %c n - #if {($n <= 31) && ($n != 9) && ($n != 10) && ($n != 13)} { - # #we don't expect unescaped unicode characters from 0000 to 001F - - # #*except* for raw tab (which is whitespace) and newlines - # error "unescape_string. Invalid data for a toml string. Unescaped control character (decimal $n) [::tomlish::utils::string_to_slashu $c]" - #} - ##---------------------- - - incr i ;#must incr here because we do'returns'inside the loop - if {$c eq "\\"} { - if {$slash_active} { - append buffer "\\" - set slash_active 0 - } elseif {$unicode2_active} { - error "unescape_string. unexpected case slash during unicode2 not yet handled" - } elseif {$unicode4_active} { - error "unescape_string. unexpected case slash during unicode4 not yet handled" - } elseif {$unicode8_active} { - error "unescape_string. unexpected case slash during unicode8 not yet handled" - } else { - # don't output anything (yet) - set slash_active 1 - } - } else { - if {$unicode2_active} { - if {[tcl::string::length $buffer2] < 2} { - append buffer2 $c - } - if {[tcl::string::length $buffer2] == 2} { - #we have a \xHH to test - set unicode2_active 0 - set result [tomlish::utils::hex_escape_info "\\x$buffer2"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode4_active} { - if {[tcl::string::length $buffer4] < 4} { - append buffer4 $c - } - if {[tcl::string::length $buffer4] == 4} { - #we have a \uHHHH to test - set unicode4_active 0 - set result [tomlish::utils::unicode_escape_info "\\u$buffer4"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$unicode8_active} { - if {[tcl::string::length $buffer8] < 8} { - append buffer8 $c - } - if {[tcl::string::length $buffer8] == 8} { - #we have a \UHHHHHHHH to test - set unicode8_active 0 - set result [tomlish::utils::unicode_escape_info "\\U$buffer8"] - if {[lindex $result 0] eq "ok"} { - append buffer [dict get $result ok char] - } else { - error "unescape_string error: [lindex $result 1]" - } - } - } elseif {$slash_active} { - set slash_active 0 - set ctest [tcl::string::map {{"} dq} $c] - switch -exact -- $ctest { - dq { - append buffer {"} - } - b - t - n - f - r { - append buffer [subst -nocommand -novariable "\\$c"] - } - e { - append buffer \x1b - } - x { - #introduced in 1.1.0 \xHH - set unicode2_active 1 - set buffer2 "" - } - u { - set unicode4_active 1 - set buffer4 "" - } - U { - set unicode8_active 1 - set buffer8 "" - } - default { - set slash_active 0 - #review - toml spec says all other escapes are reserved - #and if they are used TOML should produce an error. - #append buffer "\\$c" - set msg "Invalid escape sequence \\ followed by '$c'" - return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} $msg - } - } - } else { - append buffer $c - } - } - } - #puts stdout "EOF 4:$unicode4_active 8:$unicode8_active slash:$slash_active" - if {$unicode2_active} { - error "End of string reached before complete hex escape sequence \xHH" - } - if {$unicode4_active} { - error "End of string reached before complete unicode escape sequence \uHHHH" - } - if {$unicode8_active} { - error "End of string reached before complete unicode escape sequence \UHHHHHHHH" - } - if {$slash_active} { - append buffer "\\" - } - try { - encoding convertto utf-8 $buffer - } trap {} {emsg eopts} { - return -code error -errorcode {TOMLISH SYNTAX ENCODINGERROR} $emsg - } - return $buffer - } - - #This does not have to do with unicode normal forms - which it seems toml has decided against regarding use in keys (review/references?) - #This is meant for internal use regarding ensuring we match equivalent keys which may have just been specified with different string mechanisms, - #e.g squoted vs dquoted vs barekey. - proc normalize_key {rawkey} { - set c1 [tcl::string::index $rawkey 0] - set c2 [tcl::string::index $rawkey end] - if {($c1 eq "'") && ($c2 eq "'")} { - #single quoted segment. No escapes allowed within it. - set key [tcl::string::range $rawkey 1 end-1] - } elseif {($c1 eq "\"") && ($c2 eq "\"")} { - #double quoted segment. Unapply escapes. - # - set keydata [tcl::string::range $rawkey 1 end-1] ;#strip outer quotes only - #e.g key could have mix of \UXXXXXXXX escapes and unicode chars - #or mix of \t and literal tabs. - #unescape to convert all to literal versions for comparison - set key [::tomlish::utils::unescape_string $keydata] - #set key [subst -nocommands -novariables $keydata] ;#wrong. Todo - create a string escape substitution function. - } else { - set key $rawkey - } - return $key - } - - proc string_to_slashu {string} { - set rv {} - foreach c [split $string {}] { - scan $c %c cdec - if {$cdec > 65535} { - append rv {\U} [format %.8X $cdec] - } else { - append rv {\u} [format %.4X $cdec] - } - } - return $rv - } - - #'nonprintable' is conservative here because some systems (e.g windows console) are very limited in what they can display. - #This is used for display purposes only (error msgs) - proc nonprintable_to_slashu {s} { - set res "" - foreach i [split $s ""] { - scan $i %c cdec - - set printable 0 - if {($cdec>31) && ($cdec<127)} { - set printable 1 - } - if {$printable} { - append res $i - } else { - if {$cdec > 65535} { - append res \\U[format %.8X $cdec] - } else { - append res \\u[format %.4X $cdec] - } - } - } - set res - } ;# initial version from tcl wiki RS - - proc rawstring_to_jsonstring {s} { - #like nonprintable_to_slashu - # - also escape every dquote - # - escape newlines - set res "" - foreach i [split $s ""] { - scan $i %c cdec - switch -- $cdec { - 34 { - #double quote - append res \\\" - } - 13 { - #carriage return - append res \\r - } - 8 { - append res \\b - } - 9 { - append res \\t - } - 10 { - #linefeed - append res \\n - } - 92 { - append res \\\\ - } - default { - set printable 0 - if {($cdec>31) && ($cdec<127)} { - set printable 1 - } - if {$printable} { - append res $i - } else { - if {$cdec > 65535} { - #append res $i - #append res \\U[format %.8X $cdec] ;#wrong - #append res "\\U{[format %.8x $cdec]}" ;#some variation of json? - package require punk::cesu - #e.g \U0001f610 emoticon face - #surrogate pair: \uD83D\uDE10 - set surrogatepair [punk::cesu::to_surrogatestring -format escape $i] - append res $surrogatepair - } else { - append res \\u[format %.4X $cdec] - } - } - } - } - } - set res - - } - - #check if str is valid for use as a toml bare key - #Early toml versions only allowed letters + underscore + dash - proc is_basic_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } else { - set matches [regexp -all {[a-zA-Z0-9\_\-]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters match the regexp - return 1 - } else { - return 0 - } - } - } - - #from toml.abnf in github.com/toml-lang/toml - #unquoted-key = 1*unquoted-key-char - #unquoted-key-char = ALPHA / DIGIT / %x2D / %x5F ; a-z A-Z 0-9 - _ - #unquoted-key-char =/ %xB2 / %xB3 / %xB9 / %xBC-BE ; superscript digits, fractions - #unquoted-key-char =/ %xC0-D6 / %xD8-F6 / %xF8-37D ; non-symbol chars in Latin block - #unquoted-key-char =/ %x37F-1FFF ; exclude GREEK QUESTION MARK, which is basically a semi-colon - #unquoted-key-char =/ %x200C-200D / %x203F-2040 ; from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - #unquoted-key-char =/ %x2070-218F / %x2460-24FF ; include super-/subscripts, letterlike/numberlike forms, enclosed alphanumerics - #unquoted-key-char =/ %x2C00-2FEF / %x3001-D7FF ; skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - #unquoted-key-char =/ %x2070-21FF / %x2300-24FF ; skip math operators - #unquoted-key-char =/ %x25A0-268B / %x2690-2757 ; skip box drawing, block elements, and some yin-yang symbols - #unquoted-key-char =/ %x2762-2767 / %x2776-27E5 ; skip some Dingbat punctuation - #unquoted-key-char =/ %x2801-297F ; skip some math brackets and arrows, and braille blank - #unquoted-key-char =/ %x2B00-2FFF / %x3001-D7FF ; skip various math operators and symbols, and ideographic space - #unquoted-key-char =/ %xF900-FDCF / %xFDF0-FFFD ; skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - #unquoted-key-char =/ %x10000-EFFFF ; all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - variable re_barekey - set ranges [list] - lappend ranges {a-zA-Z0-9\_\-} - lappend ranges {\u00B2} {\u00B3} {\u00B9} {\u00BC-\u00BE} ;# superscript digits, fractions - lappend ranges {\u00C0-\u00D6} {\u00D8-\u00F6} {\u00F8-\u037D} ;# non-symbol chars in Latin block - lappend ranges {\u037f-\u1FFF} ;# exclude GREEK QUESTION MARK, which is basically a semi-colon - lappend ranges {\u200C-\u200D} {\u203F-\u2040} ;# from General Punctuation Block, include the two tie symbols and ZWNJ, ZWJ - lappend ranges {\u2070-\u218f} {\u2460-\u24FF} ;# include super-subscripts, letterlike/numberlike forms, enclosed alphanumerics - lappend ranges {\u2C00-\u2FEF} {\u3001-\uD7FF} ;# skip arrows, math, box drawing etc, skip 2FF0-3000 ideographic up/down markers and spaces - lappend ranges {\u2070-\u21FF} {\u2300-\u24FF} ;# skip math operators - lappend ranges {\u25A0-\u268B} {\u2690-\u2757} ;# skip box drawing, block elements, and some yin-yang symbols - lappend ranges {\u2762-\u2767} {\u2776-\u27E5} ;# skip some Dingbat punctuation - lappend ranges {\u2801-\u297F} ;# skip some math brackets and arrows, and braille blank - lappend ranges {\u2B00-\u2FFF} {\u3001-\uD7FF} ;# skip various math operators and symbols, and ideographic space - lappend ranges {\uF900-\uFDCF} {\uFDF0-\uFFFD} ;# skip D800-DFFF surrogate block, E000-F8FF Private Use area, FDD0-FDEF intended for process-internal use (unicode) - lappend ranges {\U10000-\UEFFFF} ;# all chars outside BMP range, excluding Private Use planes (F0000-10FFFF) - set re_barekey {^[} - foreach r $ranges { - append re_barekey $r - } - append re_barekey {]+$} - - proc is_barekey {str} { - if {[tcl::string::length $str] == 0} { - return 0 - } - variable re_barekey - return [regexp $re_barekey $str] - } - - #test only that the characters in str are valid for the toml specified type 'integer'. - proc int_validchars1 {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_\-\+]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - #add support for hex,octal,binary 0x.. 0o.. 0b... - proc int_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - proc is_int {str} { - set matches [regexp -all {[0-9\_xo\-\+A-Fa-f]} $str] ;#0b101 etc covered by a-f - - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - - # --------------------------------------- - #check for leading zeroes in non 0x 0b 0o - #first strip any +, - or _ (just for this test) - #(but still allowing 0 -0 +0) - set check [tcl::string::map {+ "" - "" _ ""} $str] - if {([tcl::string::length $check] > 1) && ([tcl::string::index $check 0] eq "0") && ([tcl::string::index $check 1] ni {o x b})} { - return 0 - } - # --------------------------------------- - - #check +,- only occur in the first position. (excludes also +++1 etc) - if {[tcl::string::last - $str] > 0} { - return 0 - } - if {[tcl::string::last + $str] > 0} { - return 0 - } - - #------------------------------------------- - #unclear if a 'digit' includes the type specifiers x b o - #we assume the 0x 0b 0o are NOT counted as digits - as underscores here would seem - #to be likely to cause interop issues with other systems - #(e.g tcl allows 0b1_1 but not 0b_11) - #Most of this structure would be unnecessary if we could rely on string::is::integer understanding underscores (9+?) - #we still need to support earlier Tcl for now though. - - #first rule out any case with more than one underscore in a row - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_0xFF - if {[string index $utest 0] eq "_"} { - return 0 - } - if {[string range $utest 0 1] in {0x 0b 0o}} { - set testnum [string range $utest 2 end] - #spec says *non-negative* integers may *also* be expressed in hex, octal or binary - #and also explicitly states + not allowed - #presumed to mean negative not allowed. - if {[string index $str 0] in {- +}} { - return 0 - } - } else { - set testnum $utest - #exclude also things like 0_x 0___b that snuck past our prefix test - if {![string is digit -strict [string map {_ ""} $testnum]]} { - return 0 - } - #assert - only digits and underscores in testnum - #still may have underscores at each end - } - #assert testnum is now the 'digits' portion of a , 0x 0b 0o number - #(+ and - already stripped) - #It may still have chars unsuitable for its type - which will be caught by the string::is::integer test below - if {[string length $testnum] != [string length [string trim $testnum _]]} { - #had non-inner underscores in 'digit' part - return 0 - } - #assert str only has solo inner underscores (if any) between 'digits' - #------------------------------------------- - - set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores - #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![::tomlish::utils::string_is_integer -strict $numeric_value]} { - return 0 - } - - - - #!todo - check bounds only based on some config value - #even though Tcl can handle bignums, we won't accept anything outside of toml 1.0 minimum requirements by default (for now) - #presumably very large numbers would have to be supplied in a toml file as strings. - #Review - toml 1.0 only says that it must handle up to 2^63 - not that this is a max - #some question around implementations allowed to use lower values such as 2^31 on some systems? - if {$::tomlish::max_int ne "" && $numeric_value > $::tomlish::max_int} { - return 0 - } - if {$::tomlish::min_int ne "" && $numeric_value < $::tomlish::min_int} { - return 0 - } - } else { - return 0 - } - #Got this far - didn't find anything wrong with it. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'float'. - proc float_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[eE0-9\_\-\+\.]} $str] == $numchars} { - return 1 - } else { - #only allow lower case for these special values - as per Toml 1.0 spec - if {$str ni {inf +inf -inf nan +nan -nan}} { - return 0 - } else { - return 1 - } - } - } - - #note - Tcl's string is double will return true also for the subset of float values which are integers - #This function is to determine whether it matches the Toml float concept - so requires a . or e or E - proc is_float {str} { - #vip greenlight known literals, don't test for case variations - as Toml doesn't allow (whereas Tcl allows Inf NaN etc) - if {$str in {inf +inf -inf nan +nan -nan}} { - return 1 - } - #doorcheck the basics for floatiness vs members of that rival gang - ints - if {![regexp {[.eE]} $str]} { - #could be an integer - which isn't specifically a float for Toml purposes. - return 0 - } - - - #patdown for any contraband chars - set matches [regexp -all {[eE0-9\_\-\+\.]} $str] - if {[tcl::string::length $str] != $matches} { - return 0 - } - - #all characters in legal range - - #A leading zero is ok, but we should disallow multiple leading zeroes (same rules as toml ints) - - #Early Toml spec also disallowed leading zeros in the exponent part(?) - #... this seems less interoperable anyway (some libraries generate leading zeroes in exponents) - #we allow leading zeros in exponents here. - - #Check for leading zeros in main part - #first strip any +, - or _ (just for this test) - set check [tcl::string::map {+ "" - "" _ ""} $str] - set r {([0-9])*} - regexp $r $check intpart ;#intpart holds all numerals before the first .,e or E - #leading zero only if exactly one zero - if {$intpart ne "0" && [string match 0* $intpart]} { - return 0 - } - - #for floats, +,- may occur in multiple places - #e.g -2E-22 +3e34 - #!todo - check bounds ? - - #----------------------------------------- - if {[regexp {__} $str]} { - return 0 - } - if {[string index $str 0] eq "_" || [string index $str end] eq "_"} { - return 0 - } - set utest [string trimleft $str +-] - #test again for further trick like _+_ - if {[string index $utest 0] eq "_"} { - return 0 - } - #----------------------------------------- - - #decimal point, if used must be surrounded by at least one digit on each side - #e.g 3.e+20 also illegal - set dposn [string first . $str] - if {$dposn > -1 } { - set d3 [string range $str $dposn-1 $dposn+1] - if {![::tomlish::utils::string_is_integer -strict [string index $d3 0]] || ![::tomlish::utils::string_is_integer -strict [string index $d3 2]]} { - return 0 - } - } - #we've already eliminated leading/trailing underscores - #now ensure each inner underscore is surrounded by digits - if {[regexp {_[^0-9]|[^0-9]_} $str]} { - return 0 - } - - #strip underscores for tcl double check so we can support < tcl 9 versions which didn't allow underscores - set check [tcl::string::map {_ ""} $str] - #string is double accepts inf nan +NaN etc. - if {![tcl::string::is double $check]} { - return 0 - } - - #All good - seems to be a toml-approved float and not an int. - return 1 - } - - #test only that the characters in str are valid for the toml specified type 'datetime'. - proc datetime_validchars {str} { - set numchars [tcl::string::length $str] - if {[regexp -all {[zZtT0-9\-\+\.:]} $str] == $numchars} { - return 1 - } else { - return 0 - } - } - - - #allow only hh:mm:ss or hh:mm (no subseconds) - #return 2 when missing seconds - proc _is_hms_or_hm_time {val} { - set numchars [tcl::string::length $val] - if {[regexp -all {[0-9:]} $val] != $numchars} { - return 0 - } - #assert now digits and colons only - set hms_cparts [split $val :] - #2 or 3 parts only are valid - check contents of each part - if {[llength $hms_cparts] == 2} { - lassign $hms_cparts hr min - if {[string length $hr] != 2 || [string length $min] != 2} { - return 0 - } - if {$hr > 23 || $min > 59} { - return 0 - } - return 2 ;#missing seconds indicator (can still be used as boolean for true in tcl if we don't care whether hh::mm::ss or hh:mm - } elseif {[llength $hms_cparts] == 3} { - lassign $hms_cparts hr min sec - if {[string length $hr] != 2 || [string length $min] != 2 || [string length $sec] !=2} { - return 0 - } - #possible for sec to be 60 - leap second RFC 3339 - if {$hr > 23 || $min > 59 || $sec > 60} { - return 0 - } - return 1 - } else { - return 0 - } - } - proc is_timepart {str} { - #validate the part after the T (or space) - #we receive only that trailing part here. - - #odt1 = 1979-05-27T07:32:00Z - #odt2 = 1979-05-27T00:32:00-07:00 - #odt3 = 1979-05-27T00:32:00.5-07:00 - #odt4 = 1979-05-27T00:32:00.999999-07:00 - - set numchars [tcl::string::length $str] - #timepart can have negative or positive offsets so - and + must be accepted - if {[regexp -all {[zZt0-9\-\+\.:]} $str] == $numchars} { - #todo - #basic check that we have leading 2dig hr and 2dig min separated by colon - if {![regexp {^[0-9]{2}:[0-9]{2}$|^[0-9]{2}:[0-9]{2}[^0-9]{1}.*$} $str]} { - #nn:nn or nn:nnX.* where X is non digit - return 0 - } - set dotparts [split $str .] - if {[llength $dotparts] ni {1 2}} { - return 0 - } - if {[llength $dotparts] == 2} { - lassign $dotparts hms tail - if {[_is_hms_or_hm_time $hms] == 2} { - #If we have a dot - assume hh::mm::ss required - #toml spec is unclear on this but hh:mm. doesn't seem sensible - REVIEW - return 0 - } - #validate tail - which might have +- offset - if {[string index $tail end] ni {z Z}} { - #from hh:mm:??. - #check for +/- something - if {[regexp {(.*)[+-](.*)} $tail _match fraction offset]} { - if {![string is digit -strict $fraction]} { - return 0 - } - if {[_is_hms_or_hm_time $offset] != 2} { - #RFC3339 indicates offset can be specified as hh:mm or Z - not hh:mm:ss - return 0 - } - } else { - #tail has no +/-, only valid if fraction digits - #toml-test invalid/datetime/second-trailing-dot - if {![string is digit -strict $tail]} { - return 0 - } - } - } else { - set tail [string range $tail 0 end-1] - #expect tail nnnn (from hh:mm::ss.nnnnZ) - #had a dot and a zZ - if {![string is digit -strict $tail]} { - return 0 - } - } - - } else { - #no dot (fraction of second) - if {[regexp {(.*)[+-](.*)} $str _match hms offset]} { - #validate offset - #offset of +Z or -Z not valid - if {[_is_hms_or_hm_time $offset] != 2} { - #offset is not of required form hh:mm - return 0 - } - } else { - set hms $str - set offset "" - #trim a *single* z or Z off hms if present - multiple should error later - if {[string index $hms end] in {z Z}} { - set hms [string range $hms 0 end-1] - } - } - } - #hms is allowed in toml to be hh:mm:ss or hh:mm - #validate we have hh:mm:ss or hh:mm - exactly 2 digits each - if {![_is_hms_or_hm_time $hms]} { - return 0 - } - - return 1 - } else { - return 0 - } - } - - proc is_date-local {str} { - set matches [regexp -all {[0-9\-]} $str] - if {[tcl::string::length $str] != $matches} { - return 0 - } - #seems to require yyyy-mm-dd (e.g not allowing just yyyy-mm) - if {![regexp {^([0-9]{4})-([0-9]{2})-([0-9]{2})$} $str _match y m d]} { - return 0 - } - if {$m > 12 || $m == 0} { - return 0 - } - switch -- [expr {$m}] { - 1 - 3 - 5 - 7 - 8 - 10 - 12 { - if {$d > 31 || $d == 0} { - return 0 - } - } - 2 { - if {$d > 29 || $d == 0} { - return 0 - } - if {$d == 29} { - #leapyear check - if {[catch {clock scan $str -format %Y-%m-%d} errM]} { - return 0 - } - } - } - 4 - 6 - 9 - 11 { - if {$d > 30 || $d == 0} { - return 0 - } - } - } - return 1 - } - proc is_time-local {str} { - #time of day without any relation to a specific day or any offset or timezone - set numchars [tcl::string::length $str] - if {[regexp -all {[0-9\.:]} $str] == $numchars} { - #todo - if {![regexp {^[0-9]{2}:[0-9]{2}$|^[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]+){0,1}$} $str]} { - #hh:mm or hh:mm:ss or hh:mm::ss.nnn - return 0 - } - set dotparts [split $str .] - if {[llength $dotparts] ni {1 2}} { - return 0 - } - if {[llength $dotparts] == 2} { - lassign $dotparts hms _tail - #validate tail - just fractional seconds - regex has confirmed at least one digit and only digits - #nothing todo? max length? - } else { - #no fractional seconds - set hms $str - } - if {![_is_hms_or_hm_time $hms]} { - return 0 - } - return 1 - } else { - return 0 - } - } - proc is_datetime-local {str} { - set norm [string map {" " T} $str] - lassign [split $norm T] dp tp - if {$dp eq "" || $tp eq ""} {return 0} - if {![is_date-local $dp]} {return 0} - if {![is_timepart $tp]} {return 0} - if {![is_time-local $tp]} {return 0} - return 1 - } - proc is_datetime {str} { - set norm [string map {" " T} $str] - lassign [split $norm T] dp tp - if {$dp eq "" || $tp eq ""} {return 0} - if {![is_date-local $dp]} {return 0} - if {![is_timepart $tp]} {return 0} - if {[is_time-local $tp]} {return 0} - return 1 - } - #review - proc is_date_or_time_or_datetime {str} { - #Essentially RFC3339 formatted date-time - but: - #1) allowing seconds to be omitted (:00 assumed) - #2) T may be replaced with a single space character TODO - parser support for space in datetime! - # (RFC 3339 allows space instead of T also - but doesn't specify it *must* be a single space) - - #toml-lint @2025-04 doesn't accept t for T or z for Z - but RFC3339 does - #toml spec doesn't clarify - we will accept - - #e.g 1979-05-27 - #e.g 1979-05-27T00:32:00Z - #e.g 1979-05-27 00:32:00-07:00 - #e.g 1979-05-27 00:32:00+10:00 - #e.g 1979-05-27 00:32:00.999999-07:00 - - #review - #minimal datetimes? - # 2024 not ok - 2024T not accepted by tomlint why? - # 02:00 ok - # 02:00:00.5 ok - # 1:00 - not ok - RFC3339 requires 2-digit hr,min,sec - - #toml-lint.com accepts 2025-01 - - if {[string length $str] < 5} { - return 0 - } - - set matches [regexp -all {[zZtT0-9\ \-\+\.:]} $str] - if {[tcl::string::length $str] == $matches} { - #all characters in legal range - if {[regexp -all {\ } $str] > 1} { - #only a single space is allowed. - return 0 - } - #If we get a space - it is only valid as a convience to represent the T separator - #we can normalize by converting to T here before more tests - set str [string map {" " T t T} $str] - #a further sanity check on T - if {[regexp -all {T} $str] > 1} { - return 0 - } - - #!todo - use full RFC 3339 parser? - #!todo - what if the value is 'time only'? - - if {[string first T $str] > -1} { - lassign [split $str T] datepart timepart - if {![is_date-local $datepart]} { - return 0 - } - if {![is_timepart $timepart]} { - return 0 - } - } else { - #either a datepart or a localtime - #spec: "If you include only the time portion of an RFC 3339 formatted date-time, it will represent that time of day - # without any relation to a specific day or any offset or timezone." - if {!([is_date-local $str] || [is_time-local $str])} { - return 0 - } - } - - - #Tcl's free-form clock scan (no -format option) is deprecated - # - #if {[catch {clock scan $datepart} err]} { - # puts stderr "tcl clock scan failed err:'$err'" - # return 0 - #} - - } else { - return 0 - } - return 1 - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::utils ---}] -} - -namespace eval tomlish::parse { - #*** !doctools - #[subsection {Namespace tomlish::parse}] - #[para] - #[list_begin definitions] - - #This is a somewhat curly mix of a statemachine and toml-nesting-stack littered with special cases. - #The code is a pig's-nest - but it should be noted that for example trailing single double quotes in multiline strings are perhaps not so trivial to parse using more standard methods either: - # - e.g some kind of backtracking required if using an ABNF parser? - #I don't know the precise technical name for this sort of parser; probably something like "Dog's Breakfast" - #More seriously, we don't have distinct lex/parse steps - so it is basically a 'fused lexer' or 'scannerless parser' - - #It is also desirable for this system to be useful in 'interactive' use. review - would a separate lexer make this easier or harder? - - #A possible alternative more structured approach might be to use a PEG (Parsing Expression Grammar) - - - variable is_parsing 0 ;#whether we are in the middle of parsing tomlish text - - variable state - # states: - # table-space, itable-space, array-space - # array-value-expected,keyval-value-expected,itable-keyval-value-expected, keyval-syntax, - # dquoted-key, squoted-key - # string-state, literal-state, multistring... - # - # notes: - # only the -space states are also 'spaces' ie a container which is pushed/popped on the spacestack - - # - # xxx_value-expected - we also allow for leading whitespace in this state, but once a value is returned we jump to a state based on the containing space. e.g keyval-tail or array-syntax - # - #stateMatrix defines for each state, actions to take for each possible token. - #single-element actions are the name of the next state into which to transition, or a 'POPSPACE' instruction to pop a level off the spacestack and add the data to the parent container. - #dual-element actions are a push instruction and the name of the space to push on the stack. - # - PUSHSPACE is a simple push onto the spacestack, zeropoppushspace also pushes, but will first do a pop *if* the current space level is greater than zero (ie if only if not already in root table-space) - - # -- --- --- --- --- --- - #token/state naming guide - # -- --- --- --- --- --- - #tokens : underscore separated or bare name e.g newline, start_quote, start_squote - #private tokens: always have a leading underscore (These are private 'temporary state' tokens that are never returned as actual tokens e.g _start_squote_sequence - #states : always contain at least one dash e.g err-state, table-space - #instructions - # -- --- --- --- --- --- - - - #stateMatrix dict of elements mapping current state to next state based on returned tokens - # current-state {token-encountered next-state ... } - # where next-state can be a 1 or 2 element list. - #If 2 element - the first item is an instruction (ucase) - #If 1 element - it is either a lowercase dashed state name or an ucase instruction - #e.g {PUSHSPACE } or POPSPACE or SAMESPACE - - - #SAMESPACE - got to same space as parent without popping a level, but has it's own autotransition lookup - strange concept - review usecases - - variable stateMatrix - set stateMatrix [dict create] - #--------------------------------------------------------- - #WARNING - #The stateMatrix implementation here is currently messy. - #The code is a mixture of declarative via the stateMatrix and imperative via switch statements during PUSH/POP/SAMESPACE transitions. - #This means the state behaviour has to be reasoned about by looking at both in conjuction. - #--------------------------------------------------------- - - #xxx-space vs xxx-syntax inadequately documented - TODO - - #review - out of date? - # --------------------------------------------------------------------------------------------------------------# - # incomplete example of some state starting at table-space - # --------------------------------------------------------------------------------------------------------------# - # ( = -> keyval-value-expected) - # keyval-syntax (popped -> keyval-space -> keyval-tail) (autotransition on pop) - # keyval-space (autotransition on push ^) - # table-space (barekey^) (startdquote -> dquoted-key ^) - # --------------------------------------------------------------------------------------------------------------# - - dict set stateMatrix\ - table-space { - bom "table-space"\ - whitespace "table-space"\ - newline "table-space"\ - barekey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - squotedkey {PUSHSPACE "keyval-space" state "keyval-syntax" note ""}\ - dquotedkey {PUSHSPACE "keyval-space" state "keyval-syntax"}\ - XXXsingle_dquote "quoted-key"\ - XXXsingle_squote "squoted-key"\ - comment "table-space"\ - starttablename "tablename-state"\ - starttablearrayname "tablearrayname-state"\ - enddquote "err-state"\ - endsquote "err-state"\ - comma "err-state"\ - eof "end-state"\ - equal "err-state"\ - cr "err-lonecr"\ - } - - - - dict set stateMatrix\ - keyval-space {\ - whitespace "keyval-syntax"\ - equal "keyval-value-expected"\ - } - - # ' = ' portion of keyval - dict set stateMatrix\ - keyval-syntax {\ - whitespace "keyval-syntax"\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal "keyval-value-expected"\ - comma "err-state"\ - newline "err-state"\ - eof "err-state"\ - } - #### - dict set stateMatrix\ - keyval-value-expected {\ - whitespace "keyval-value-expected"\ - untyped_value {TOSTATE "keyval-untyped-sequence" note "possible datetime datepart"}\ - literal {TOSTATE "keyval-tail" note "required for empty literal at EOF"}\ - string {TOSTATE "keyval-tail" note "required for empty string at EOF"}\ - single_dquote {TOSTATE "string-state" returnstate keyval-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - single_squote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - } - #double_squote {TOSTATE "keyval-tail" note "empty literal received when double squote occurs"} - - #untyped_value sequences without intervening comma are allowed for datepart timepart - #we will produce tomlish with missing SEPS and to_dict must validate whether 2 adjacent barekeys are valid - dict set stateMatrix\ - keyval-untyped-sequence {\ - whitespace "keyval-untyped-sequence"\ - untyped_value {TOSTATE "keyval-tail"}\ - literal {TOSTATE "keyval-tail" note "required for empty literal at EOF"}\ - string {TOSTATE "keyval-tail" note "required for empty string at EOF"}\ - single_dquote {TOSTATE "string-state" returnstate keyval-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate keyval-tail}\ - single_squote {TOSTATE "literal-state" returnstate keyval-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate keyval-tail}\ - startinlinetable {PUSHSPACE itable-space returnstate keyval-tail}\ - startarray {PUSHSPACE array-space returnstate keyval-tail}\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - #2025 - no leading-squote-space - only trailing-squote-space. - - dict set stateMatrix\ - keyval-tail {\ - whitespace "keyval-tail"\ - newline "POPSPACE"\ - comment "keyval-tail"\ - eof "end-state"\ - } - - - #itable-space/ curly-syntax : itables - # x={y=1,} - dict set stateMatrix\ - itable-space {\ - whitespace "itable-space"\ - newline "itable-space"\ - barekey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - squotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - dquotedkey {PUSHSPACE "itable-keyval-space" state "itable-keyval-syntax"}\ - endinlinetable "POPSPACE"\ - comma "err-state"\ - comment "itable-space"\ - eof "err-state"\ - } - #we don't get single_squote etc here - instead we get the resulting squotedkey token - - - # ??? review - something like this - # - # x={y =1,} - dict set stateMatrix\ - itable-keyval-syntax {\ - whitespace {TOSTATE "itable-keyval-syntax"}\ - barekey {PUSHSPACE "dottedkey-space"}\ - squotedkey {PUSHSPACE "dottedkey-space"}\ - dquotedkey {PUSHSPACE "dottedkey-space"}\ - equal {TOSTATE "itable-keyval-value-expected"}\ - newline "err-state"\ - eof "err-state"\ - } - - # x={y=1} - dict set stateMatrix\ - itable-keyval-space {\ - whitespace "itable-keyval-syntax"\ - equal {TOSTATE "itable-keyval-value-expected" note "required"}\ - } - - dict set stateMatrix\ - itable-keyval-value-expected {\ - whitespace "itable-keyval-value-expected"\ - untyped_value {TOSTATE "itable-val-tail" note ""}\ - single_dquote {TOSTATE "string-state" returnstate itable-val-tail}\ - triple_dquote {PUSHSPACE "multistring-space" returnstate itable-val-tail}\ - single_squote {TOSTATE "literal-state" returnstate itable-val-tail note "usual way a literal is triggered"}\ - triple_squote {PUSHSPACE "multiliteral-space" returnstate itable-val-tail}\ - startinlinetable {PUSHSPACE "itable-space" returnstate itable-val-tail}\ - startarray {PUSHSPACE "array-space" returnstate itable-val-tail}\ - } - #double_squote not currently generated by _start_squote_sequence - '' processed as single_squote to literal-state just like 'xxx' - # review - # double_squote {TOSTATE "itable-val-tail" note "empty literal received when double squote occurs"} - - - - # x={y=1,z="x"} - #POPSPACE is transition from itable-keyval-space to parent itable-space - dict set stateMatrix\ - itable-val-tail {\ - whitespace "itable-val-tail"\ - endinlinetable "POPSPACE"\ - comma "POPSPACE"\ - newline {TOSTATE "itable-val-tail" note "itable-space ??"}\ - comment "itable-val-tail"\ - eof "err-state"\ - } - # XXXnewline "POPSPACE" - # We shouldn't popspace on newline - as if there was no comma we need to stay in itable-val-tail - # This means the newline and subsequent whitespace, comments etc become part of the preceeding dottedkey record - #e.g - # x = { - # j=1 - # #comment within dottedkey j record - # , # comment unattached - # #comment unattached - # k=2 , #comment unattached - # l=3 #comment within l record - # , m=4 - # #comment associated with m record - # - # #still associated with m record - # } - ## - This doesn't quite correspond to what a user might expect - but seems like a consistent mechanism. - #The awkwardness is because there is no way to put in a comment that doesn't consume a trailing comma - #so we cant do: j= 1 #comment for j1 , - # and have the trailing comma recognised. - # - # To associate: j= 1, #comment for j1 - # we would need some extra processing . (not popping until next key ? extra state itable-sep-tail?) REVIEW - worth doing? - # - # The same issue occurs with multiline arrays. The most natural assumption is that a comment on same line after a comma - # is 'associated' with the previous entry. - # - # These comment issues are independent of the data dictionary being generated for conversion to json etc - as the comments don't carry through anyway, - # but are a potential oddity for manipulating the intermediate tomlish structure whilst attempting to preserve 'associated' comments - # (e.g reordering records within an itable) - #The user's intention for 'associated' isn't always clear and the specs don't really guide on this. - - - #dottedkey-space is not (currently) used within [tablename] or [[tablearrayname]] - #it is for keyval ie x.y.z = value - - #this is the state after dot - #we are expecting a complete key token or whitespace - #(initial entry to the space is by one of the keys - which will immediately go to dottedkey-space-tail) - dict set stateMatrix\ - dottedkey-space {\ - whitespace "dottedkey-space"\ - dotsep "err-state"\ - barekey "dottedkey-space-tail"\ - squotedkey "dottedkey-space-tail"\ - dquotedkey "dottedkey-space-tail"\ - newline "err-state"\ - comma "err-state"\ - comment "err-state"\ - equal "err-state"\ - } - - #dottedkeyend "POPSPACE" - #equal "POPSPACE"\ - - - #jmn 2025 - #we have 1 or more dottedkeys so far - need dotsep to add more, whitespace to maintain, equal to pop - dict set stateMatrix\ - dottedkey-space-tail {\ - whitespace "dottedkey-space-tail" - dotsep "dottedkey-space" - equal "POPSPACE"\ - eof "err-state"\ - newline "err-state"\ - } - - #-------------------------------------------------------------------------- - #scratch area - #from_toml {x=1} - # barekey tok - # table-space PUSHSPACE keyval-space state keyval-syntax - # - - - #-------------------------------------------------------------------------- - - - #REVIEW - #toml spec looks like heading towards allowing newlines within inline tables - #https://github.com/toml-lang/toml/issues/781 - - #2025 - multiline itables appear to be valid for 1.1 - which we are targeting. - #https://github.com/toml-lang/toml/blob/main/toml.md#inline-table - - #JMN2025 - #review comment "err-state" vs comment "itable-space" - see if TOML 1.1 comes out and allows comments in multiline ITABLES - #We currently allow multiline ITABLES (also with comments) in the tokenizer. - #if we want to disallow as per TOML 1.0 - we should do so when attempting to get structure? - - - #JMN REVIEW - #dict set stateMatrix\ - # array-space {\ - # whitespace "array-space"\ - # newline "array-space"\ - # untyped_value "SAMESPACE"\ - # startarray {PUSHSPACE "array-space"}\ - # endarray "POPSPACE"\ - # startinlinetable {PUSHSPACE itable-space}\ - # single_dquote "string-state"\ - # single_squote "literal-state"\ - # triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax note "seems ok 2024"}\ - # comma "array-space"\ - # comment "array-space"\ - # eof "err-state-array-space-got-eof"\ - # } - - ## array-space ## - set aspace [dict create] - dict set aspace whitespace "array-space" - dict set aspace newline "array-space" - #dict set aspace untyped_value "SAMESPACE" - dict set aspace untyped_value "array-syntax" - dict set aspace startarray {PUSHSPACE "array-space"} - dict set aspace endarray "POPSPACE" - dict set aspace single_dquote {TOSTATE "string-state" returnstate array-syntax} - dict set aspace triple_dquote {PUSHSPACE "multistring-space" returnstate array-syntax} - dict set aspace single_squote {TOSTATE "literal-state" returnstate array-syntax} - dict set aspace triple_squote {PUSHSPACE "multiliteral-space" returnstate array-syntax} - dict set aspace startinlinetable {PUSHSPACE itable-space} - #dict set aspace comma "array-space" - dict set aspace comment "array-space" - dict set aspace eof "err-state-array-space-got-eof" - dict set stateMatrix array-space $aspace - - #when we pop from an inner array we get to array-syntax - #e.g {x=[[]] ??? - set tarntail [dict create] - dict set tarntail whitespace "err-state" ;#"tablearrayname-tail" ;#spec doesn't allow whitespace here - dict set tarntail newline "err-state" - dict set tarntail comment "err-state" - dict set tarntail eof "err-state" - dict set tarntail endtablename "tablearray-tail" - dict set stateMatrix tablearrayname-tail $tarntail - - #review - somewhat counterintuitive...? - # [(starttablearrayname) (endtablearrayname] - # [(starttablename) (endtablename)] - - # [[xxx]] ??? - set tartail [dict create] - dict set tartail whitespace "tablearray-tail" - dict set tartail newline "table-space" - dict set tartail comment "tablearray-tail" - dict set tartail eof "end-state" - dict set stateMatrix tablearray-tail $tartail - - - - - - - dict set stateMatrix\ - end-state {} - - set knowntokens [list] - set knownstates [list] - dict for {state transitions} $stateMatrix { - if {$state ni $knownstates} {lappend knownstates $state} - dict for {tok instructions} $transitions { - if {$tok ni $knowntokens} {lappend knowntokens $tok} - } - } - dict set stateMatrix nostate {} - foreach tok $knowntokens { - dict set stateMatrix nostate $tok "err-nostate-received-token-$tok" - } - - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #purpose - debugging? remove? - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #build a list of 'push triggers' from the stateMatrix - # ie tokens which can push a new space onto spacestack - set push_trigger_tokens [list] - tcl::dict::for {s transitions} $stateMatrix { - tcl::dict::for {token transition_to} $transitions { - set instruction [lindex $transition_to 0] - switch -exact -- $instruction { - PUSHSPACE - zeropoppushspace { - if {$token ni $push_trigger_tokens} { - lappend push_trigger_tokens $token - } - } - } - } - } - ::tomlish::log::debug "push_trigger_tokens: $push_trigger_tokens" - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - - - - #This seems hacky... (deprecate in favour of explicit arguments to the instructions in stateMatrix?) - #spacePopTransitions, spacePushTransitions, spaceSameTransitions below for auto state redirections on POPSPACE,PUSHSPACE,SAMESPACE - - #mainly for the -space states: - #redirect to another state $c based on a state transition from $whatever to $b - # e.g "string {array-space array-syntax}" means when transitioning from string to array-space, jump to array-syntax instead. - #this is useful as we often don't know state $b. e.g when it is decided by 'POPSPACE' - - #use dict set to add values so we can easily add/remove/comment lines - - #Push to, next - #default first states when we push to these spaces - variable spacePushTransitions [dict create] - dict set spacePushTransitions keyval-space keyval-syntax - dict set spacePushTransitions itable-keyval-space itable-keyval-syntax - dict set spacePushTransitions array-space array-space - dict set spacePushTransitions table-space tablename-state - #dict set spacePushTransitions #itable-space itable-space - - #Pop to, next - variable spacePopTransitions [dict create] - dict set spacePopTransitions array-space array-syntax - - - #itable-keyval-space itable-val-tail - #review - #we pop to keyval-space from dottedkey-space or from keyval-value-expected? we don't always want to go to keyval-tail - #leave it out and make the POPSPACE caller explicitly specify it - #keyval-space keyval-tail - - variable spaceSameTransitions [dict create] - #JMN test - #dict set spaceSameTransitions array-space array-syntax - - #itable-keyval-space itable-val-tail - - - variable state_list ;#reset every tomlish::decode::toml - - namespace export tomlish toml - namespace ensemble create - - #goNextState has various side-effects e.g pushes and pops spacestack - #REVIEW - setting nest and v elements here is ugly - #todo - make neater, more single-purpose? - proc goNextState {tokentype tok currentstate} { - variable state - variable nest - variable v - - set prevstate $currentstate - - - variable spacePopTransitions - variable spacePushTransitions - variable spaceSameTransitions - - variable last_space_action "none" - variable last_space_type "none" - variable state_list - - set result "" - set starttok "" - - if {[dict exists $::tomlish::parse::stateMatrix $currentstate $tokentype]} { - set transition_to [dict get $::tomlish::parse::stateMatrix $currentstate $tokentype] - ::tomlish::log::debug "--->> goNextState tokentype:$tokentype tok:$tok currentstate:$currentstate : transition_to = $transition_to" - switch -exact -- [lindex $transition_to 0] { - POPSPACE { - set popfromspace_info [spacestack peek] - set popfromspace_state [dict get $popfromspace_info state] - spacestack pop - set parent_info [spacestack peek] - set type [dict get $parent_info type] - set parentspace [dict get $parent_info state] - - set last_space_action "pop" - set last_space_type $type - - if {[dict exists $parent_info returnstate]} { - set next [dict get $parent_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected to stored returnstate $next <<---" - } else { - ### - #review - do away with spacePopTransitions - which although useful to provide a default.. - # - involve error-prone configurations distant to the main state transition configuration in stateMatrix - if {[dict exists $::tomlish::parse::spacePopTransitions $parentspace]} { - set next [dict get $::tomlish::parse::spacePopTransitions $parentspace] - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace redirected state to $next (spacePopTransitions)<<---" - } else { - set next $parentspace - ::tomlish::log::info "--->> POPSPACE transition from $popfromspace_state to parent space $parentspace<<---" - } - } - set result $next - } - SAMESPACE { - set currentspace_info [spacestack peek] - ::tomlish::log::debug "--->> SAMESPACE got current space entry: $currentspace_info <<<<<" - set type [dict get $currentspace_info type] - set currentspace [dict get $currentspace_info state] - - if {[dict exists $currentspace_info returnstate]} { - set next [dict get $currentspace_info returnstate] - #clear the returnstate on current level - set existing [spacestack pop] - dict unset existing returnstate - spacestack push $existing ;#re-push modification - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected to stored returnstate $next" - } else { - if {[dict exists $::tomlish::parse::spaceSameTransitions $currentspace]} { - set next [dict get $::tomlish::parse::spaceSameTransitions $currentspace] - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace redirected state to $next (spaceSameTransitions)" - } else { - set next $currentspace - ::tomlish::log::info "--->> SAMESPACE transition to space $currentspace" - } - } - set result $next - } - zeropoppushspace { - if {$nest > 0} { - #pop back down to the root level (table-space) - spacestack pop - set parentinfo [spacestack peek] - set type [dict get $parentinfo type] - set target [dict get $parentinfo state] - - set last_space_action "pop" - set last_space_type $type - - #----- - #standard pop - set parentlevel [expr {$nest -1}] - lappend v($parentlevel) [set v($nest)] - incr nest -1 - #----- - } - #re-entrancy - - #set next [list PUSHSPACE [lindex $transition_to 1]] - set nexttokentype ${tokentype}2 ;#fake token type e.g tablename2 or tablearrayname2 - ::tomlish::log::debug "--->> zeropoppushspace goNextState RECURSE. calling goNextState $nexttokentype $currentstate" - set transition_info [::tomlish::parse::goNextState $nexttokentype $tok $currentstate] - set result [dict get $transition_info newstate] - } - PUSHSPACE { - set original_target [dict get $transition_to PUSHSPACE] - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - if {[dict exists $transition_to starttok]} { - set starttok [dict get $transition_to starttok] - } - spacestack push [dict create type space state $original_target] - - set last_space_action "push" - set last_space_type "space" - - if {[dict exists $transition_to state]} { - #an explicit state in the pushed space was requested in the stateMatrix - override the spacePushTransition (spacePushTransitions can be deprecated if we require explicitness?) - set next [dict get $transition_to state] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next by explicit 'state' entry" - } else { - #puts $::tomlish::parse::spacePushTransitions - if {[dict exists $::tomlish::parse::spacePushTransitions $original_target]} { - set next [dict get $::tomlish::parse::spacePushTransitions $original_target] - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target redirected state to $next (spacePushTransitions) " - } else { - set next $original_target - ::tomlish::log::info "--->> PUSHSPACE transition to space $original_target" - } - } - set result $next - } - TOSTATE { - if {[dict exists $transition_to returnstate]} { - #adjust the existing space record on the stack. - #struct::stack doesn't really support that - so we have to pop and re-push - #todo - investigate a custom stack implementation where we can efficiently lset the top of the stack - set currentspace [spacestack pop] - dict set currentspace returnstate [dict get $transition_to returnstate] - spacestack push $currentspace ;#return modified info to stack so when we POPSPACE the returnstate is available. - } - set result [dict get $transition_to TOSTATE] - } - default { - #simplified version of TOSTATE - set result [lindex $transition_to 0] ;#ignore everything but first word - } - } - } else { - ::tomlish::log::error "--->> No state transition defined from state $currentstate when tokentype $tokentype received" - set result "nostate" - } - lappend state_list [list tokentype $tokentype from $currentstate to $result] - set state $result - ::tomlish::log::notice "--->> STATE TRANSITION tokenType: '$tokentype' tok:$tok triggering '$currentstate' -> '$result' last_space_action:$last_space_action " - return [dict create prevstate $prevstate newstate $result space_action $last_space_action starttok $starttok] - } - - proc report_line {{line ""}} { - variable linenum - variable is_parsing - if {$is_parsing} { - if {$line eq ""} { - set line $linenum - } - return "Line Number: $line" - } else { - #not in the middle of parsing tomlish text - return nothing. - return "" - } - } - - #produce a *slightly* more readable string rep of the nest for puts etc. - proc nest_pretty1 {list} { - set prettier "{" - - foreach el $list { - if { [lindex $el 0] eq "NEWLINE"} { - append prettier "[list $el]\n" - } elseif {([llength $el] > 1) && ([lindex $el 0] in {KEY DQKEY SQKEY TABLE ARRAY})} { - append prettier [nest_pretty1 $el] - } else { - append prettier "[list $el] " - } - } - append prettier "}" - return $prettier - } - - proc set_tokenType {t} { - variable tokenType - variable tokenType_list - if {![info exists tokenType]} { - set tokenType "" - } - lappend tokenType_list $t - set tokenType $t - } - - proc switch_tokenType {t} { - variable tokenType - variable tokenType_list - lset tokenType_list end $t - set tokenType $t - } - - proc get_tokenType {} { - variable tokenType - return $tokenType - } - - - proc get_token_waiting {} { - variable token_waiting - return [lindex $token_waiting 0] - } - proc clear_token_waiting {} { - variable token_waiting - set token_waiting [list] - } - - #token_waiting is a list - but our standard case is to have only one - #in certain circumstances such as near eof we may have 2 - #the set_token_waiting function only allows setting when there is not already one waiting. - #we want to catch cases of inadvertently trying to set multiple - # - the reason being that the state transition triggered by the previous token may have invalidated the assumptions made when a token was added as waiting. - proc set_token_waiting {args} { - if {[llength $args] %2 != 0} { - error "tomlish set_token_waiting must have args of form: type value complete 0|1" - } - variable token_waiting - - if {[llength $token_waiting] && [dict get [lindex $token_waiting end] type] ne "eof"} { - #tokloop already set a token_waiting - but something (post tokloop processing?) is trying to set another - #we may need to remove the existing token_waiting and reset the tokloop index to the previous char so it's reprocessed in the possibly new context - #rather than attempt to make the right decision here - we raise an error and require the caller to check/handle it - set err "tomlish set_token_waiting already has token_waiting: [lindex $token_waiting 0]" - append err \n " - cannot add token_waiting: $args" - error $err - #set tomlish::parse::i [expr {[dict get $token_waiting startindex] -1}] - #set token_waiting [list] - } - - set waiting [dict create] - dict for {k v} $args { - switch -exact -- $k { - type - complete { - dict set waiting $k $v - } - value { - dict set waiting tok $v - } - startindex { - dict set waiting startindex $v - } - default { - error "tomlish set_token_waiting error - unrecognised key $k. known keys: [dict keys $args]" - } - } - } - if {![tcl::string::is boolean -strict [dict get $waiting complete]]} { - error "tomlish set_token_waiting error - 'complete' must be a boolean. got [dict get $waiting complete]" - } - if {![llength $token_waiting]} { - set token_waiting [list $waiting] - } else { - #an extra sanity-check that we don't have more than just the eof.. - if {[llength $token_waiting] > 1} { - set err "tomlish Unexpected. Existing token_waiting count > 1.\n" - foreach tw $token_waiting { - append err " $tw" \n - } - append err " - cannot add token_waiting: $waiting" - error $err - } - #last entry must be a waiting eof - set token_waiting [list $waiting [lindex $token_waiting end]] - } - return - } - - #returns 0 or 1 - #tomlish::parse::tok - #we attempt to do this without lookahead (potential use in streaming toml? for what benefit?) todo -final flag - # - the possible benefit is being able to more easily process in arbitrarily split chunks (although we would still have to watch crlf splitting ?) - # - interactive use? - - proc tok {} { - variable nest - variable s - variable v - variable i - variable tok - variable type ;#character type - variable state ;#FSM - - - variable tokenType - variable tokenType_list - - - variable endToken - - variable lastChar - - variable braceCount - variable bracketCount - - - #------------------------------ - #Previous run found another (presumably single-char) token - #The normal case is for there to be only one dict in the list - #multiple is an exception - primarily for eof - variable token_waiting - if {[llength $token_waiting]} { - set waiting [lindex $token_waiting 0] - - set tokenType [dict get $waiting type] - set tok [dict get $waiting tok] - #todo: dict get $token_waiting complete - set token_waiting [lrange $token_waiting 1 end] - return 1 - } - #------------------------------ - - set resultlist [list] - set sLen [tcl::string::length $s] - - set slash_active 0 - set quote 0 - set c "" - for {} {$i < $sLen} {} { - if {$i > 0} { - set lastChar [tcl::string::index $s [expr {$i - 1}]] - set start_of_data h - } else { - set lastChar "" - set start_of_data 1 - #bom-handling - if {[tcl::string::index $s 0] eq "\uFEFF"} { - #bom (could be from various encodings - now decoded as single unicode char FEFF) - #incr i 1 ;#skip over initial bom? - } - } - - - set c [tcl::string::index $s $i] - set cindex $i - - set ctest [tcl::string::map {\{ lc \} rc \[ lb \] rb \" dq ' sq \\ bsl \r cr \n lf \t tab \uFEFF bom} $c] - tomlish::log::debug "- tokloop char <$ctest> index $i tokenType:$tokenType tok:<$tok>" - #puts "got char $c during tokenType '$tokenType'" - incr i ;#must incr here because we do returns inside the loop - - - - switch -exact -- $ctest { - # { - set had_slash $slash_active - set slash_active 0 - - if {$had_slash} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - #for multiliteral, multistring - data and/or end - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo token beginning with underscore - never returned to state machine - review - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed (see tomlish::utils::is_barekey). [tomlish::parse::report_line]" - } - whitespace { - # hash marks end of whitespace token - #do a return for the whitespace, set token_waiting - #set_token_waiting type comment value "" complete 1 - incr i -1 ;#leave comment for next run - return 1 - } - untyped_value { - #REVIEW! the spec isn't clear.. is whitespace after an int,bool etc required before comment? - #we will accept a comment marker as an immediate terminator of the untyped_value. - incr i -1 - return 1 - } - starttablename - starttablearrayname { - #fix? - error "tomlish Character '#' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare parts - but allowed in quoted parts - let tablename parser sort it out - append tok $c - } - default { - #dquotedkey, string,literal, multistring - append tok $c - } - } - } else { - switch -- $state { - multistring-space { - set_tokenType stringpart - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "#" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "#" - } - default { - #start of token if we're not in a token - set_tokenType comment - set tok "" ;#The hash is not part of the comment data - } - } - } - } - lc { - #left curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i [tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - #*bare* tablename can only contain letters,digits underscores - error "tomlish Invalid tablename first character \{ [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #valid in quoted parts - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\{" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - switch -exact -- $state { - itable-keyval-value-expected - keyval-value-expected { - #switch last key to tablename?? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - array-space - array-syntax { - #nested anonymous inline table - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "startinlinetable" - set tok "\{" - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\{" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\{" - } - default { - error "tomlish state: '$state'. left brace case not implemented [tomlish::parse::report_line]" - } - } - } - - } - rc { - #right curly brace - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - tablename { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endinlinetable value "" complete 1 startindex $cindex - return 1 - } - starttablearrayname - tablearrayname { - if {$had_slash} {append tok "\\"} - #invalid! - but leave for datastructure loading stage to catch - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } - default { - #end any other token - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-space { - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - tablename-state { - #e.g [] - empty tablename - allowed or not? - #empty tablename/tablearrayname ? - #error "unexpected tablename problem" - - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname-state problem" - set_tokenType "endinlinetable" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - array-syntax - array-space { - #invalid - set_tokenType "endinlinetable" - set tok "\}" - return 1 - } - itable-val-tail { - set_tokenType "endinlinetable" - set tok "" - #we need to pop the keyval - and then reprocess to pop the inlinetable - so we incr -1 - incr i -1 - return 1 - } - itable-keyval-syntax { - error "tomlish endinlinetable unexpected at this point. Expecting key=val syntax [tomlish::parse::report_line]" - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\}" - } - multiliteral-space { - set_tokenType "literalpart" ; #review - set tok "\}" - } - default { - #JMN2024b keyval-tail? - error "tomlish state '$state'. endinlinetable case not implemented [tomlish::parse::report_line]" - } - } - } - - } - lb { - #left square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename { - #change the tokenType - switch_tokenType "starttablearrayname" - set tok "" ;#no output into the tomlish list for this token - #any following whitespace is part of the tablearrayname, so return now - return 1 - } - tablename - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - #append tok "\\[" - append tok {\[} - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - #invalid at this point - state machine should disallow: - # table -> starttablearrayname - # tablearray -> starttablearrayname - set_token_waiting type starttablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "\[" - } - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok "\[" - } - default { - #end any other token. - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - keyval-value-expected - itable-keyval-value-expected { - set_tokenType "startarray" - set tok "\[" - return 1 - } - array-space - array-syntax { - #nested array? - set_tokenType "startarray" - set tok "\[" - return 1 - #error "state: array-space. startarray case not implemented [tomlish::parse::report_line]" - } - table-space { - #table name - #assume it's a single bracket - but we need to wait for non-bracket to confirm it's not a tablearray - #note that a starttablearrayname token may contain whitespace between the brackets - # e.g \[ \[ - set_tokenType "starttablename" - set tok "" ;#there is no output into the tomlish list for this token - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\[" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\[" - } - itable-space { - #handle state just to give specific error msg - error "tomlish state: '$state'. Left square bracket invalid. Cannot start array in inline table without key. Use key=\[\] syntax. [tomlish::parse::report_line]" - } - default { - error "tomlish state: '$state'. startarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - rb { - #right square bracket - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - #???? - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } else { - incr i -1 - if {$had_slash} {incr i -1} ;#reprocess - return 1 - } - } - starttablename { - #toml-test invalid/table/empty - - set_token_waiting type tablename value "" complete 1 startindex $cindex - incr i -1 - return 1 - } - tablename { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablename value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - tablearrayname { - #e.g a."x[0]".c is valid table name sequence - so we need to track quoting to know if rb is an end token - if {$had_slash} { - #resultant tablename may be invalid - but leave for datastructure loading stage to catch - append tok "\\]" - } else { - if {[tomlish::utils::tok_in_quotedpart $tok] eq ""} { - set_token_waiting type endtablearrayname value "" complete 1 startindex $cindex - return 1 - } else { - #we appear to still be in single or double quoted section - append tok "]" - } - } - } - default { - incr i -1 - return 1 - } - } - } else { - #$slash_active not relevant when no tokenType - switch -exact -- $state { - array-syntax - array-space { - #invalid - but allow parser statemachine to report it. - set_tokenType "endarray" - set tok "\]" - return 1 - } - table-space { - #invalid - but allow parser statemachine to report it. ? - set_tokenType "endarray" - set tok "\]" - return 1 - } - tablename-state { - #e.g [] - empty tablename - #tomltest 1.1.0 invalid/table/empty - #should be invalid - #we parse it and let dict::from_tomlish error when it tries to split table - - set_tokenType "endtablename" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-state { - error "tomlish unexpected tablearrayname problem" - set_tokenType "endtablearray" - set tok "" ;#no output into the tomlish list for this token - return 1 - } - tablearrayname-tail { - #[[xxx] - set_tokenType "endtablename" - #sequence: starttablename -> starttablearrayname -> endtablearrayname -> endtablename - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok "\]" - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\]" - } - default { - error "tomlish state '$state'. endarray case not implemented [tomlish::parse::report_line]" - } - } - } - } - bsl { - #backslash - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - #end whitespace token - incr i -1 ;#reprocess bsl in next run - return 1 - } else { - error "tomlish Unexpected backslash during whitespace. [tomlish::parse::report_line]" - } - } - literal - literalpart - squotedkey { - #never need to set slash_active true when in single quoted tokens - append tok "\\" - set slash_active 0 - } - string - dquotedkey - comment { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - stringpart { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - starttablename - starttablearrayname { - error "tomlish backslash is invalid as first character of $tokenType [tomlish::parse::report_line]" - } - tablename - tablearrayname { - if {$slash_active} { - set slash_active 0 - append tok "\\\\" - } else { - set slash_active 1 - } - } - barekey { - error "tomlish Unexpected backslash during barekey. [tomlish::parse::report_line]" - } - default { - error "tomlish Backslash unexpected during tokentype: '$tokenType'. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$slash_active} { - set_tokenType "stringpart" - set tok "\\\\" - set slash_active 0 - } else { - set slash_active 1 - } - } - multiliteral-space { - #nothing can be escaped in multiliteral-space - not even squotes (?) review - set_tokenType "literalpart" - set tok "\\" - } - default { - error "tomlish tok error: Unexpected backslash when no token is active. [tomlish::parse::report_line]" - } - } - } - } - sq { - #single quote - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote { - #for within multiliteral - #short tentative_accum_squote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_squote is multisquote + 2 = 5 - #return tok with value ''''' - return 1 - } - } - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multiliteral - #switch? - append tok $c - set_tokenType triple_squote - return 1 - } - default { - #if there are more than 3 leading squotes we also enter multiliteral space and the subsequent ones are handled - #by the tentative_accum_squote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 squotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_squote_sequence'" - } - } - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - #end whitespace - incr i -1 ;#reprocess sq - return 1 - } - literal { - #slash_active always false - #terminate the literal - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - literalpart { - #ended by ''' - but final could be '''' or ''''' (up to 2 squotes allowed directly before ending triple squote sequence) - #todo - # idea: end this literalpart (possibly 'temporarily') - # let the sq be reprocessed in the multiliteral-space to push an end-multiliteral-sequence to state stack - # upon popping end-multiliteral-sequence - stitch quotes back into this literalpart's token (if either too short - or a long ending sequence as shown above) - incr i -1 ;#throw the "'" back to loop - will be added to a tentative_accum_squote token for later processing - return 1 - } - XXXitablesquotedkey { - set_token_waiting type endsquote value "'" complete 1 startindex $cindex - return 1 - } - squotedkey { - ### - #set_token_waiting type endsquote value "'" complete 1 - return 1 - } - starttablename - starttablearrayname { - #!!! - incr i -1 - return 1 - } - tablename - tablearrayname { - append tok $c - } - barekey { - #barekeys now support all sorts of unicode letter/number chars for other cultures - #but not punctuation - not even for those of Irish heritage who don't object - #to the anglicised form of some names. - # o'shenanigan seems to not be a legal barekey - #The Irish will have to use an earlier form Ó - which apparently many may prefer anyway. - error "tomlish Unexpected single quote during barekey. [tomlish::parse::report_line]" - } - default { - append tok $c - } - } - } else { - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading squote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_squote token or triple_squote token - #It currently doesn't trigger double_squote token - #(handle '' same as 'x' ie produce a single_squote and go into processing literal) - #review - producing double_squote for empty literal may be slightly more efficient. - #This token is not used to handle squote sequences *within* a multiliteral - set_tokenType "_start_squote_sequence" - set tok "'" - } - multiliteral-space { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_squote" ;#trigger tentative_accum_squote - set tok "'" - return 1 - } - table-space - itable-space { - #tests: squotedkey.test squotedkey_itable.test - set_tokenType "squotedkey" - set tok "" - } - XXXtable-space - XXXitable-space { - #future - could there be multiline keys? MLLKEY, MLBKEY ? - #this would (almost) allow arbitrary tcl dicts to be stored in toml (aside from escaping issues) - #probably unlikely - as it's perhaps not very 'minimal' or ergonomic for config files - #@2025 ABNF for toml mentions key, simple-key, unquoted-key, quoted-key and dotted-key - #where key is simple-key or dotted-key - no MLL or MLB components - #the spec states solution for arbitrary binary data is application specific involving encodings - #such as hex, base64 - set_tokenType "_start_squote_sequence" - set tok "'" - return 1 - } - tablename-state { - #first char in tablename-state/tablearrayname-state - set_tokenType "tablename" - append tok "'" - } - tablearrayname-state { - set_tokenType "tablearrayname" - append tok "'" - } - literal-state { - #shouldn't get here? review - tomlish::log::debug "- tokloop sq during literal-state with no tokentype - empty literal?" - set_tokenType "literal" - incr -1 - return 1 - } - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - #error "tomlish unimplemented - squote during state '$state'. [tomlish::parse::report_line]" - } - dottedkey-space { - set_tokenType "squotedkey" - } - default { - error "tomlish unhandled squote during state '$state'. [tomlish::parse::report_line]" - } - } - } - - } - dq { - #double quote - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - tentative_accum_dquote { - #within multistring - #short tentative_accum_dquote tokens are returned if active upon receipt of any other character - #longest allowable for leading/trailing are returned here - #### - set existingtoklen [tcl::string::length $tok] ;#toklen prior to this squote - #assert state = trailing-squote-space - append tok $c - if {$existingtoklen == 4} { - #maxlen to be a tentative_accum_dquote is multidquote + 2 = 5 - #return tok with value """"" - return 1 - } - } - _start_dquote_sequence { - #pseudo/temp token creatable during keyval-value-expected itable-keyval-value-expected or array-space - switch -- [tcl::string::length $tok] { - 1 { - #no conclusion can yet be reached - append tok $c - } - 2 { - #enter multistring - #switch? - append tok $c - set_tokenType triple_dquote - return 1 - } - default { - #if there are more than 3 leading dquotes we also enter multistring space and the subsequent ones are handled - #by the tentative_accum_dquote check for ending sequence which can accept up to 5 and reintegrate the - #extra 1 or 2 dquotes as data. - error "tomlish unexpected token length [tcl::string::length $tok] in '_start_dquote_sequence'" - } - } - } - literal - literalpart { - append tok $c - } - string { - if {$had_slash} { - append tok "\\" $c - } else { - #unescaped quote always terminates a string - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - stringpart { - #sub element of multistring - if {$had_slash} { - append tok "\\" $c - } else { - incr i -1 ;#throw the {"} back to loop - will be added to a tentative_accum_dquote token for later processing - return 1 - } - } - whitespace { - #assert: had_slash will only ever be true in multistring-space - if {$had_slash} { - incr i -2 - return 1 - } else { - #end whitespace token - throw dq back for reprocessing - incr i -1 - return 1 - } - } - comment { - if {$had_slash} {append tok "\\"} - append tok $c - } - XXXdquotedkey { - if {$had_slash} { - append tok "\\" - append tok $c - } else { - set_token_waiting type enddquote value "\"" complete 1 startindex $cindex - return 1 - } - } - dquotedkey { - ### - if {$had_slash} { - append tok "\\" - append tok $c - } else { - #set_token_waiting type enddquote value {"} complete 1 - return 1 - } - } - squotedkey { - append tok $c - } - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok $c - } - starttablename - starttablearrayname { - incr i -1 ;## - return 1 - } - default { - error "tomlish got quote during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - #$slash_active not relevant when no tokenType - #token is string only if we're expecting a value at this point - switch -exact -- $state { - array-space - keyval-value-expected - itable-keyval-value-expected { - #leading dquote - #pseudo-token _start_squote_sequence ss not received by state machine - #This pseudotoken will trigger production of single_dquote token or triple_dquote token - #It currently doesn't trigger double_dquote token - #(handle "" same as "x" ie produce a single_dquote and go into processing string) - #review - producing double_dquote for empty string may be slightly more efficient. - #This token is not used to handle dquote sequences once *within* a multistring - set_tokenType "_start_dquote_sequence" - set tok {"} - } - multistring-space { - if {$had_slash} { - set_tokenType "stringpart" - set tok "\\\"" - } else { - #each literalpart is not necessarily started/ended with squotes - but may contain up to 2 in a row - #we are building up a tentative_accum_squote to determine if - #a) it is shorter than ''' so belongs in a literalpart (either previous, subsequent or it's own literalpart between newlines - #b) it is exactly ''' and we can terminate the whole multiliteral - #c) it is 4 or 5 squotes where the first 1 or 2 beling in a literalpart and the trailing 3 terminate the space - set_tokenType "tentative_trigger_dquote" ;#trigger tentative_accum_dquote - set tok {"} - return 1 - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok "\"" - } - table-space - itable-space { - set_tokenType "dquotedkey" - set tok "" - } - dottedkey-space { - set_tokenType dquotedkey - set tok "" - - #only if complex keys become a thing - #set_tokenType dquote_seq_begin - #set tok $c - } - tablename-state { - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - default { - error "tomlish Unexpected dquote during state '$state' [tomlish::parse::report_line]" - } - } - } - } - = { - set had_slash $slash_active - set slash_active 0 - - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart - squotedkey { - #assertion had_slash 0 - append tok $c - } - string - comment - dquotedkey { - #for these tokenTypes an = is just data. - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type equal value = complete 1 startindex $cindex - return 1 - } - } - barekey { - #set_token_waiting type equal value = complete 1 - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character '=' is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #invalid in bare name - but valid in quoted parts - leave for tablename parser to sort out - append tok $c - } - default { - error "tomlish unexpected = character during tokentype $tokenType. case not implemented. [tomlish::parse::report_line]" - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} { - append tok "\\" - } - append tok = - } - multiliteral-space { - set_tokenType "literalpart" - set tok "=" - } - dottedkey-space { - set_tokenType "equal" - set tok "=" - return 1 - } - default { - set_tokenType "equal" - set tok = - return 1 - } - } - } - } - cr { - #REVIEW! - # \r carriage return - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #we have received a double cr - ::tomlish::log::warn "double cr - will generate cr token. needs testing" - set_tokenType "cr" ;#lone cr token will generally raise an error - but let state machine handle it - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - append tok $c - } - literalpart { - #part of MLL string (multi-line literal string) - #we need to split out crlf as a separate NEWLINE to be consistent - ::tomlish::log::warn "literalpart ended by cr - needs testing" - #return literalpart temporarily - allow cr to be reprocessed from multiliteral-space - incr i -1 - return 1 - } - stringpart { - #stringpart is a part of MLB string (multi-line basic string) - #throw back the cr - if followed by lf it will become a {NEWLINE crlf} entry within the MULTISTRING list (e.g between STRINGPART entries) - incr i -1 - return 1 - } - starttablename - starttablearrayname { - error "tomlish Character is invalid first character for $tokenType. [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #could in theory be valid in quoted part of name - #review - might be better just to disallow here - append tok $c - } - whitespace { - #it should technically be part of whitespace if not followed by lf - #but outside of values we are also free to map it to be another NEWLINE instead? REVIEW - incr i -1 - return 1 - } - untyped_value { - incr i -1 - return 1 - } - comment { - #JJJJ - #review - incr i -1 - return 1 - } - default { - #!todo - error out if cr inappropriate for tokenType - append tok $c - } - } - } else { - #lf may be appended if next - #review - lone cr as newline? - this is uncommon - but so is lone cr in a string(?) - set_tokenType "newline" - set tok cr - } - } - lf { - # \n newline - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #review - #this lf is the trailing part of a crlf - append tok lf ;#assert we should now have tok "crlf" - as a previous cr is the only way to have an incomplete newline tok - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - #multiliteral or multistring - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal { - #nl is not allowed *within* a literal - require multiliteral syntax for any literal containing a newline ''' ''' - #even though we terminate the literal without the closing quote here - the token_waiting newline should trigger a state error - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - literalpart { - #we allow newlines - but store them within the multiliteral as their own element - #This is a legitimate end to the literalpart - but not the whole multiliteral - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - stringpart { - if {$had_slash} { - #emit the stringpart (return 1), queue the continuation, go back 1 to reprocess the lf (incr i -1) - set_token_waiting type continuation value \\ complete 1 startindex [expr {$cindex-1}] - incr i -1 - return 1 - } else { - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - starttablename - tablename - tablearrayname - starttablearrayname { - error "tomlish Character is invalid in $tokenType. [tomlish::parse::report_line]" - } - default { - #newline ends all other tokens. - #note for string: we don't add (raw unescaped) newline to simple string. (must use multi-string for this) - #note for whitespace: - # we will use the convention that \n terminates the current whitespace even if whitespace follows - # ie whitespace is split into separate whitespace tokens at each newline - - #puts "-------------- newline lf during tokenType $tokenType" - set_token_waiting type newline value lf complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "newline" - set tok lf - return 1 - } - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "newline" - set tok "lf" - return 1 - } - default { - #ignore slash? error? - set_tokenType "newline" - set tok lf - return 1 - } - } - #if {$had_slash} { - # #CONT directly before newline - allows strings_5_byteequivalent test to pass - # set_tokenType "continuation" - # set tok "\\" - # incr i -1 - # return 1 - #} else { - # set_tokenType newline - # set tok lf - # return 1 - #} - } - } - , { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - tablename - tablearrayname { - if {$had_slash} {append tok "\\"} - append tok , - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - #stringpart can have up to 2 quotes too - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - set_token_waiting type comma value "," complete 1 startindex $cindex - return 1 - } - } - default { - set_token_waiting type comma value "," complete 1 startindex $cindex - if {$had_slash} {append tok "\\"} - return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "," - } - multiliteral-space { - #assert had_slash 0 - set_tokenType "literalpart" - set tok "," - } - default { - set_tokenType "comma" - set tok "," - return 1 - } - } - } - } - . { - set had_slash $slash_active - set slash_active 0 - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - comment - untyped_value { - if {$had_slash} {append tok "\\"} - append tok $c - } - string - dquotedkey { - if {$had_slash} {append tok "\\"} - append tok $c - } - stringpart { - if {$had_slash} {append tok "\\"} - append tok $c - } - literal - literalpart - squotedkey { - #assert had_slash always 0 - append tok $c - } - whitespace { - switch -exact -- $state { - multistring-space { - #review - if {$had_slash} { - incr i -2 - } else { - incr i -1 - } - return 1 - } - xxxdottedkey-space { - incr i -1 - return 1 - } - dottedkey-space-tail { - incr i -1 - return 1 - } - default { - error "tomlish Received period during tokenType 'whitespace' [tomlish::parse::report_line]" - } - } - } - starttablename - starttablearrayname { - #This would correspond to an empty table name - error "tomlish Character '.' is not allowed as first character ($tokenType). [tomlish::parse::report_line]" - } - tablename - tablearrayname { - #subtable - split later - review - append tok $c - } - barekey { - #e.g x.y = 1 - #we need to transition the barekey to become a structured table name ??? review - #x is the tablename y is the key - set_token_waiting type dotsep value "." complete 1 startindex $cindex - return 1 - } - default { - error "tomlish Received period during tokenType '$tokenType' [tomlish::parse::report_line]" - #set_token_waiting type period value . complete 1 - #return 1 - } - } - } else { - switch -exact -- $state { - multistring-space { - set_tokenType "stringpart" - set tok "" - if {$had_slash} {append tok "\\"} - append tok "." - } - multiliteral-space { - set_tokenType "literalpart" - set tok "." - } - XXXdottedkey-space { - ### obs? - set_tokenType "dotsep" - set tok "." - return 1 - } - dottedkey-space-tail { - ### - set_tokenType "dotsep" - set tok "." - return 1 - } - default { - set_tokenType "untyped_value" - set tok "." - } - } - } - - } - " " - tab { - if {[tcl::string::length $tokenType]} { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #todo had_slash - emit token or error - #whitespace is a terminator for bare keys - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - comment { - if {$had_slash} { - append tok "\\" - } - append tok $c - } - string - dquotedkey { - if {$had_slash} { append tok "\\" } - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART xxx WS " " - incr i -1 - return 1 - } - } - literal - literalpart - squotedkey { - append tok $c - } - whitespace { - if {$state eq "multistring-space"} { - append tok $c - } else { - append tok $c - } - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace space during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok "" - if {$had_slash} {append tok "\\"} - append tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType "whitespace" - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - if {$had_slash} { - error "tomlish unexpected backslash [tomlish::parse::report_line]" - } - set_tokenType "whitespace" - append tok $c - } - } - } - } - tabX { - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out (?review) - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - barekey { - #whitespace is a terminator for bare keys - incr i -1 - #set_token_waiting type whitespace value $c complete 1 - return 1 - } - untyped_value { - #unquoted values (int,date,float etc) are terminated by whitespace - #set_token_waiting type whitespace value $c complete 1 - incr i -1 - return 1 - } - squotedkey { - append tok $c - } - dquotedkey - string - comment - whitespace { - #REVIEW - append tok $c - } - stringpart { - #for stringpart we store WS separately for ease of processing continuations (CONT stripping) - if {$had_slash} { - #REVIEW - #emit the stringpart - go back to the slash - incr i -2 - return 1 - } else { - #split into STRINGPART aaa WS " " - incr i -1 - return 1 - } - } - literal - literalpart { - append tok $c - } - starttablename - starttablearrayname { - incr i -1 - return 1 - } - tablename - tablearrayname { - #include whitespace in the tablename/tablearrayname - #Will need to be normalized upon interpreting the tomlish as a datastructure - append tok $c - } - default { - error "tomlish Received whitespace tab during tokenType '$tokenType' [tomlish::parse::report_line]" - } - } - } else { - set had_slash $slash_active - if {$slash_active} { - set slash_active 0 - } - switch -exact -- $state { - tablename-state { - #tablename can have leading,trailing and interspersed whitespace! - #These will not be treated as whitespace tokens, instead forming part of the name. - set_tokenType tablename - set tok $c - } - tablearrayname-state { - set_tokenType tablearrayname - set tok $c - } - multistring-space { - if {$had_slash} { - set_tokenType "continuation" - set tok "\\" - incr i -1 - return 1 - } else { - set_tokenType whitespace - append tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - default { - set_tokenType "whitespace" - append tok $c - } - } - } - } - bom { - #bom encoded as single unicode codepoint \uFFEF - #BOM (Byte Order Mark) - ignored by token consumer - if {[tcl::string::length $tokenType]} { - switch -exact -- $tokenType { - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - #assert - tok will be one or two squotes only - #A toml literal probably isn't allowed to contain this - #but we will parse and let the validator sort it out. - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - literal - literalpart { - append tok $c - } - string - stringpart { - append tok $c - } - default { - #state machine will generally not have entry to accept bom - let it crash - set_token_waiting type bom value "\uFEFF" complete 1 startindex $cindex - return 1 - } - } - } else { - switch -exact -- $state { - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - multistring-space { - set_tokenType "stringpart" - set tok $c - } - default { - set_tokenType "bom" - set tok "\uFEFF" - return 1 - } - } - } - } - default { - - if {[tcl::string::length $tokenType]} { - if {$slash_active} {append tok "\\"} ;#if tokentype not appropriate for \, we would already have errored out. - set slash_active 0 - switch -exact -- $tokenType { - newline { - #incomplete newline - set_tokenType "cr" - incr i -1 - return 1 - } - tentative_accum_squote - tentative_accum_dquote { - incr i -1 - return 1 - } - _start_squote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_squote" - return 1 - } - _start_dquote_sequence { - incr i -[tcl::string::length $tok] - set_tokenType "single_dquote" - return 1 - } - whitespace { - if {$state eq "multistring-space"} { - incr i -1 - return 1 - } else { - #review - incr i -1 ;#We don't have a full token to add to the token_waiting dict - so leave this char for next run. - return 1 - } - } - barekey { - if {[tomlish::utils::is_barekey $c]} { - append tok $c - } else { - error "tomlish Unexpected character '$c' during bare key. Only \[a-zA-Z0-9_-\] and a selection of letter-like chars allowed. (see tomlish::utils::is_barekey) [tomlish::parse::report_line]" - } - } - starttablename - starttablearrayname { - incr i -1 - #allow statemachine to set context for subsequent chars - return 1 - } - string - stringpart { - append tok $c - } - default { - #e.g comment/literal/literalpart/untyped_value/starttablename/starttablearrayname/tablename/tablearrayname - append tok $c - } - } - } else { - set had_slash $slash_active - set slash_active 0 - switch -exact -- $state { - table-space - itable-space { - #if no currently active token - assume another key value pair - if {[tomlish::utils::is_barekey $c]} { - set_tokenType "barekey" - append tok $c - } else { - error "tomlish Unexpected char $c ([tomlish::utils::nonprintable_to_slashu $c]) whilst no active tokenType. [tomlish::parse::report_line]" - } - } - multistring-space { - set_tokenType "stringpart" - if {$had_slash} { - set tok \\$c - } else { - set tok $c - } - } - multiliteral-space { - set_tokenType "literalpart" - set tok $c - } - tablename-state { - set_tokenType "tablename" - set tok $c - } - tablearrayname-state { - set_tokenType "tablearrayname" - set tok $c - } - dottedkey-space { - set_tokenType barekey - set tok $c - } - default { - #todo - something like ansistring VIEW to show control chars? - set cshow [string map [list \t tab \v vt] $c] - tomlish::log::debug "- tokloop char '$cshow' setting to untyped_value while state:$state [tomlish::parse::report_line]" - set_tokenType "untyped_value" - set tok $c - } - } - } - } - } - - } - - #run out of characters (eof) - if {[tcl::string::length $tokenType]} { - #check for invalid ending tokens - #if {$state eq "err-state"} { - # error "Reached end of data whilst tokenType = '$tokenType'. INVALID" - #} - switch -exact -- $tokenType { - _start_squote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open literal - error "tomlish eof reached without closing single quote for string literal. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "literal" - set tok "" - return 1 - - ##review - #set_token_waiting type endsquote value "'" complete 1 startindex [expr {$cindex -1}] - #set_tokenType "literal" - #set tok "" - #return 1 - } - } - } - _start_dquote_sequence { - set toklen [tcl::string::length $tok] - switch -- $toklen { - 1 { - #invalid eof with open string - error "tomlish eof reached without closing double quote for string. [tomlish::parse::report_line]" - } - 2 { - set_tokenType "string" - set tok "" - return 1 - } - } - } - newline { - #The only newline token that has still not been returned should have a tok value of "cr" - puts "tomlish eof reached - with incomplete newline token '$tok'" - if {$tok eq "cr"} { - #we convert lone cr to it's own "cr" token elsewhere in the document to allow statemachine to handle it. - #(which it should generally do by not handling it ie raising an error - or emitting an ERROR list in the tomlish) - #if trailing char is a lone cr - we should encode it the same way as elsewhere that is outside of values - # ie as it's own token. - switch_tokenType "cr" - return 1 - } else { - #should be unreachable - error "tomlish eof reached - with invalid newline token. value: $tok" - } - } - } - set_token_waiting type eof value eof complete 1 startindex $i ;#review - return 1 - } else { - ::tomlish::log::debug "- No current tokenType, ran out of characters, setting tokenType to 'eof' [tomlish::parse::report_line]" - set tokenType "eof" - set tok "eof" - } - return 0 - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::parse ---}] -} -namespace eval tomlish::huddle { - proc from_json {json} { - package require huddle - package require huddle::json - #note - huddle may now contain raw surrogate pair - which cannot be emitted to stdout - set h [huddle::json::json2huddle parse $json] - } - proc from_dict {d} { - error "tomlish::huddle::from_dict unimplemented" - } - - #raw - strings must already be processed into values suitable for json e.g surrogate pair escaping - proc jsondumpraw {huddle_object {offset " "} {newline "\n"} {begin ""}} { - upvar ::huddle::types types - set nextoff "$begin$offset" - set nlof "$newline$nextoff" - set sp " " - if {[string equal $offset ""]} {set sp ""} - - set type [huddle type $huddle_object] - - switch -- $type { - boolean - - number { - return [huddle get_stripped $huddle_object] - } - null { - return null - } - string { - set data [huddle get_stripped $huddle_object] - - # JSON permits only oneline string - #set data [string map { - # \n \\n - # \t \\t - # \r \\r - # \b \\b - # \f \\f - # \\ \\\\ - # \" \\\" - # / \\/ - # } $data - #] - return "\"$data\"" - } - list { - set inner {} - set len [huddle llength $huddle_object] - for {set i 0} {$i < $len} {incr i} { - set subobject [huddle get $huddle_object $i] - lappend inner [jsondumpraw $subobject $offset $newline $nextoff] - } - if {[llength $inner] == 1} { - return "\[[lindex $inner 0]\]" - } - return "\[$nlof[join $inner ,$nlof]$newline$begin\]" - } - dict { - set inner {} - foreach {key} [huddle keys $huddle_object] { - lappend inner [subst {"$key":$sp[jsondumpraw [huddle get $huddle_object $key] $offset $newline $nextoff]}] - } - #if {[llength $inner] == 1} { - # return $inner ;#wrong - breaks with quoted list representation - # #FAILS: toml-test valid/comment/tricky - #} - - return "\{$nlof[join $inner ,$nlof]$newline$begin\}" - } - default { - set node [unwrap $huddle_object] - #foreach {tag src} $node break - lassign $node tag src - return [$types(callback:$tag) jsondumpraw $huddle_object $offset $newline $nextoff] - } - } - } -} - -#typed as per toml-test types -namespace eval tomlish::typedhuddle { - proc from_json {json} { - set plainhuddle [tomlish::huddle::from_json $json] - - error "tomlish::typedhuddle::from_json unimplemented" - } - proc from_dict {d} { - package require huddle - set h [huddle create] - if {[tomlish::dict::is_typeval $d]} { - set dtype [dict get $d type] - switch -- $dtype { - ARRAY { - #error "typedhuddle::from_dict ARRAY not yet handled" - set h_list [huddle list] - set elements [dict get $d value] - foreach el $elements { - set sub [from_dict $el] - huddle append h_list $sub - } - return $h_list - } - default { - set tinfo [tomlish::dict::convert_typeval_to_tomltest $d] - #basic non-container types - set h_tdict [huddle create] - huddle set h_tdict type [huddle string [dict get $tinfo type]] - huddle set h_tdict value [huddle string [dict get $tinfo value]] - return $h_tdict - } - } - } else { - dict for {dictkey dictval} $d { - set jsonkey [tomlish::utils::rawstring_to_jsonstring $dictkey] - if {[tomlish::dict::is_typeval $dictval]} { - set dtype [dict get $dictval type] - switch -- $dtype { - ARRAY { - #error "typedhuddle::from_dict ARRAY not yet handled" - set h_next [huddle list] - set elements [dict get $dictval value] - foreach el $elements { - set sub [from_dict $el] - huddle append h_next $sub - } - } - default { - set tinfo [tomlish::dict::convert_typeval_to_tomltest $dictval] - set tp [dict get $tinfo type] - #basic non-container types - set h_next [huddle create] ;#dict - huddle set h_next type [huddle string [dict get $tinfo type]] - huddle set h_next value [huddle string [dict get $tinfo value]] - } - } - huddle set h $jsonkey $h_next - } else { - #dict - set sub [from_dict $dictval] - huddle set h $jsonkey $sub - } - } - } - return $h - } - proc is_typeval {huddled} { - set htype [huddle type $huddled] - if {$htype ne "dict"} { - return 0 - } - if {[huddle keys $huddled] ne {type value}} { - return 0 - } - set tp [huddle type $huddled type] - switch -- $tp { - string - integer - float - bool - datetime - datetime-local - date-local - time-local { - return 1 - } - } - return 0 - } - - #direction from typed json towards toml - proc convert_typeval_to_tomlish {huddled} { - set htype [huddle get_stripped $huddled type] - set hval [huddle get_stripped $huddled value] - switch -- $htype { - string { - #we need to decide here the type of string element to use in toml/tomlish - #STRING,MULTISTRING,LITERAL,MULTILITERAL - #set unesc [tomlish::utils::unescape_jsonstring $hval] ;#no need - json parser unescaped when creating the huddle - set unesc $hval - #(huddle::json::json2huddle parse $json) - #since it was unescaped any backslashes remaining represent themselves - reapply escape - REVIEW - #set hval [string map [list \\ \\\ ] $hval] - #JSJS - if {[string first \n $unesc] >= 0} { - #always use a MULTI - if {[string first ' $unesc] >=0} { - if {[string first ''' $unesc] >=0} { - set dtype MULTISTRING - } else { - set dtype MULTILITERAL - } - } else { - if {[string first \"\"\" $unesc] >=0} { - set dtype MULTILITERAL - } else { - set dtype MULTISTRING - } - } - } else { - #use multi if needed? - if {[string first '' $hval] >=0} { - if {[string first ''' $unesc] >=0} { - set dtype STRING - } else { - set dtype MULTILITERAL - } - } elseif {[string first ' $unesc] >= 0} { - set dtype STRING - } elseif {[string first \"\"\" $unesc] >= 0} { - set dtype LITERAL - } else { - #STRING or LITERAL? - set dtype STRING - } - } - - } - datetime - bool { - set dtype [string toupper $htype] - } - float { - set dtype FLOAT - if {[::tomlish::utils::string_is_integer -strict $hval]} { - #json FLOAT specified as integer - must have dot for toml - set hval [expr {double($hval)}] - } - } - integer { - set dtype INT - } - datetime - datetime-local - date-local - time-local { - #DDDD - #set dtype DATETIME - set dtype [string toupper $htype] - } - default { - error "tomlish::typedhuddle::convert_typeval_to_tomlish unrecognised type $htype" - } - } - return [list type $dtype value $hval] - } - -} -namespace eval tomlish::toml { - proc from_binary {bindata} { - set bom "" - set b12 [tcl::string::range $bindata 0 1] - set b12test [string map [list \xEF\xBB utf8_12 \xFE\xFF bom16be \xFF\xFE utf32le_12 \x00\x00 utf32be_12] $b12] - switch -- $b12test { - bom16be { - #FEFF - set bom utf-16be - } - utf32le_12 { - #FFFE - set b34 [tcl::string::range $bindata 2 3] - if {$b34 eq "\x00\x00"} { - set bom utf-32le - } else { - set bom utf-16le - } - } - utf32be_12 { - #0000 - set b34 [tcl::string::range $bindata 2 3] - if {$b34 eq "\xFE\xFF"} { - set bom utf-32be - } - } - utf8_12 { - set b3 [tcl::string::index $bindata 2] - if {$b3 eq "\xBF"} { - set bom utf-8 - } - } - } - if {$bom eq ""} { - #no bom - assume utf8 - but we read in as binary - #if data wasn't actually utf8 we may error here depending on content - or we may just get wrongly encoded chars - set tomldata [encoding convertfrom utf-8 $bindata] - } elseif {$bom eq "utf-8"} { - #utf-8 bom read in as binary - set tomldata [encoding convertfrom utf-8 $bindata] - #bom now encoded as single unicode char \uFFEF - } else { - return -code error -errorcode {TOML ENCODING NOTUTF8} "Input not UTF8 encoded according to BOM. Indicated encoding is '$bom' - invalid for toml" - } - return $tomldata - } - proc from_tomlish {tomlish} { - return [tomlish::encode::tomlish $tomlish] - } - - #todo - rename to taggedjson - proc from_tomlish_from_dict_from_typedjson {json} { - set d [tomlish::dict::from_typedjson $json] - from_tomlish [tomlish::from_dict $d] ;#return tomlish - } - - proc tablename_split {tablename {normalize false}} { - #we can't just split on . because we have to handle quoted segments which may contain a dot. - #eg {dog."tater.man"} - if {$tablename eq ""} { - error "tablename_split. No table name segments found. empty tablename" - } - set sLen [tcl::string::length $tablename] - set segments [list] - set mode "preval" ;#5 modes: preval, quoted,litquoted, unquoted, postval - #quoted is for double-quotes, litquoted is for single-quotes (string literal) - set seg "" - for {set i 0} {$i < $sLen} {incr i} { - - if {$i > 0} { - set lastChar [tcl::string::index $tablename [expr {$i - 1}]] - } else { - set lastChar "" - } - - #todo - track\count backslashes properly - - set c [tcl::string::index $tablename $i] - if {$c eq "\""} { - if {($lastChar eq "\\")} { - #not strictly correct - we could have had an even number prior-backslash sequence - #the toml spec would have us error out immediately on bsl in bad location - but we're - #trying to parse to unvalidated tomlish - set ctest escq - } else { - set ctest dq - } - } else { - set ctest [string map [list " " sp \t tab] $c] - } - - switch -- $ctest { - . { - switch -exact -- $mode { - preval { - error "tablename_split. dot not allowed - expecting a value" - } - unquoted { - #dot marks end of segment. - if {![tomlish::utils::is_barekey $seg]} { - error "tablename_split. unquoted key segment $seg is not a valid toml key" - } - lappend segments $seg - set seg "" - set mode "preval" - } - quoted { - append seg $c - } - litquoted { - append seg $c - } - postval { - #got dot in an expected location - set mode "preval" - } - } - } - dq { - #unescaped dquote - switch -- $mode { - preval { - set mode "quoted" - set seg "\"" - } - unquoted { - #invalid in barekey - but we are after structure only - append seg $c - } - quoted { - append seg $c - #JJJJ - if {$normalize} { - lappend segments [::tomlish::utils::unescape_string [tcl::string::range $seg 1 end-1]] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" ;#make sure we only accept a dot or end-of-data now. - } - litquoted { - append seg $c - } - postval { - error "tablename_split. expected whitespace or dot, got double quote. tablename: '$tablename'" - } - } - } - ' { - switch -- $mode { - preval { - append seg $c - set mode "litquoted" - } - unquoted { - #single quote inside e.g o'neill - ultimately invalid - but we pass through here. - append seg $c - } - quoted { - append seg $c - } - litquoted { - append seg $c - #no normalization to do aside from stripping squotes - if {$normalize} { - lappend segments [tcl::string::range $seg 1 end-1] - } else { - lappend segments $seg - } - set seg "" - set mode "postval" - } - postval { - error "tablename_split. expected whitespace or dot, got single quote. tablename: '$tablename'" - } - } - } - sp - tab { - switch -- $mode { - preval - postval { - #ignore - } - unquoted { - #terminates a barekey - lappend segments $seg - set seg "" - set mode "postval" - } - default { - #append to quoted or litquoted - append seg $c - } - } - } - default { - switch -- $mode { - preval { - set mode unquoted - append seg $c - } - postval { - error "tablename_split. Expected a dot separator. got '$c'. tablename: '$tablename'" - } - default { - append seg $c - } - } - } - } - - if {$i == $sLen-1} { - #end of data - ::tomlish::log::debug "End of data: mode='$mode'" - switch -exact -- $mode { - preval { - if {[llength $segments]} { - error "tablename_split. Expected a value after last dot separator. tablename: '$tablename'" - } else { - error "tablename_split. Whitespace only? No table name segments found. tablename: '$tablename'" - } - } - unquoted { - if {![tomlish::utils::is_barekey $seg]} { - #e.g toml-test invalid/table/with-pound required to fail for invalid barekey - error "tablename_split. unquoted key segment $seg is not a valid toml key" - } - lappend segments $seg - } - quoted { - error "tablename_split. Expected a trailing double quote. tablename: '$tablename'" - } - litquoted { - error "tablename_split. Expected a trailing single quote. tablename: '$tablename'" - } - postval { - #ok - segment already lappended - } - } - } - } - - #note - we must allow 'empty' quoted strings '' & "" - # (these are 'discouraged' but valid toml keys) - - return $segments - } - - #tablenames (& tablearraynames) may contain irrelevant leading, trailing and interspersed whitespace - # tablenames can be made up of segments delimited by dots. .eg [ a.b . c ] - #trimmed, the tablename becomes {a.b.c} - # A segment may contain whitespace if it is quoted e.g [a . b . "c etc " ] - #ie whitespace is only irrelevant if it's outside a quoted segment - #trimmed, the tablename becomes {a.b."c etc "} - proc tablename_trim {tablename} { - set segments [tomlish::toml::tablename_split $tablename false] - set trimmed_segments [list] - foreach seg $segments { - lappend trimmed_segments [::string trim $seg " \t"] - } - return [join $trimmed_segments .] - } -} - -namespace eval tomlish::dict { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - - #from_taggedjson - proc from_typedjson {json} { - package require huddle - package require huddle::json - set h [huddle::json::json2huddle parse $json] - #json2huddle parse unescapes the basic json escapes \n \\ etc - #$h could now contain raw form of surrogate pair (json2huddle parse as at 2025-014 doesn't convert the surrogates - just unescapes?) - if {[catch {encoding convertto utf-8 $h} errM]} { - #This test suggests we have raw surrogate pairs - REVIEW - package require punk::cesu - set h [punk::cesu::from_surrogatestring $h] - } - tomlish::dict::from_typedhuddle $h - } - proc from_typedhuddle {h} { - set resultd [dict create] - switch -- [huddle type $h] { - dict { - foreach k [huddle keys $h] { - switch -- [huddle type $h $k] { - dict { - set huddle_d [huddle get $h $k] - #puts stderr "huddle_d: $huddle_d" - #set v [huddle get_stripped $h $k] - if {[tomlish::typedhuddle::is_typeval $huddle_d]} { - dict set resultd $k [tomlish::typedhuddle::convert_typeval_to_tomlish $huddle_d] - } else { - dict set resultd $k [from_typedhuddle $huddle_d] - } - } - list { - set items [huddle get $h $k] - - set numitems [huddle llength $items] - if {$numitems == 0} { - dict set resultd $k [list type ARRAY value {}] - } else { - set arritems [list] - for {set i 0} {$i < $numitems} {incr i} { - set item [huddle get $items $i] - #puts stderr "item: $item" - #set v [huddle get $item] - if {[tomlish::typedhuddle::is_typeval $item]} { - lappend arritems [tomlish::typedhuddle::convert_typeval_to_tomlish $item] - } else { - lappend arritems [from_typedhuddle $item] - } - } - dict set resultd $k [list type ARRAY value $arritems] - } - } - default { - error "dict_from_json unexpected subtype [huddle type $h $k] in dict" - } - } - } - } - list { - set items [huddle get $h] - set numitems [huddle llength $items] - if {$numitems == 0} { - return [list type ARRAY value {}] - } else { - set arritems [list] - for {set i 0} {$i < $numitems} {incr i} { - set item [huddle get $items $i] - #puts stderr "item: $item" - #set v [huddle get $item] - if {[tomlish::typedhuddle::is_typeval $item]} { - lappend arritems [tomlish::typedhuddle::convert_typeval_to_tomlish $item] - } else { - lappend arritems [from_typedhuddle $item] - } - } - return [list type ARRAY value $arritems] - } - - } - } - return $resultd - } - - proc is_typeval {d} { - #designed to detect {type value } e.g {type INT value 3}, {type STRING value "blah etc"} - #as a sanity check we need to avoid mistaking user data that happens to match same form - #consider x.y={type="spud",value="blah"} - #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. - #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[::tomlish::utils::string_is_dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} - } - - #simple types only - not containers? - proc convert_typeval_to_tomltest {d} { - set dtype [dict get $d type] - set dval [dict get $d value] - switch -- $dtype { - INT { - set testtype integer - set dval [expr {$dval}] ;#convert e.g 0xDEADBEEF to 3735928559 - } - FLOAT - BOOL { - set testtype [string tolower $dtype] - } - DATE-LOCAL { - set testtype date-local - } - TIME-LOCAL { - if {[tomlish::utils::_is_hms_or_hm_time $dval] == 2} { - #add seconds for sending to json - set dval "${dval}:00" - } - set testtype time-local - } - DATETIME - DATETIME-LOCAL { - #we expect it to be basically well formed here - this is not validation - just adding possible missing seconds - if {![regexp {([tT\ ])} $dval _ dsep]} { - return -code error -errorcode {TOJSON SYNTAX INVALIDDATE} "Unable to process $dtype '$dval' - missing RFC3339 separator space or T" - } - lassign [split $dval $dsep] dp tail - - #toml allows HH:MM without seconds - but we need to add seconds 00 when passing to external systems - if {![tomlish::utils::is_time-local $tail]} { - #there is some offset component. We aren't checking its syntax here (presumed done when dict building) - regexp {([\+\-zZ])} $tail _ tsep ;#keep tsep for rebuilding - lassign [split $tail $tsep] tp offset ;#offset may be empty if z or Z - } else { - set tp $tail - set tsep "" - set offset "" - } - if {[tomlish::utils::_is_hms_or_hm_time $tp] == 2} { - #need to add seconds - set dval "${dp}${dsep}${tp}:00${tsep}${offset}" - } - set testtype [string tolower $dtype] - } - STRING - MULTISTRING { - set testtype string - #JJJJ - set dval [tomlish::utils::unescape_string $dval] - set dval [tomlish::utils::rawstring_to_jsonstring $dval] - } - LITERAL - MULTILITERAL { - set testtype string - #don't validate on way out to json here? - #decoder should validate by calling tomlish::from_dict - #if {![tomlish::utils::rawstring_is_valid_literal $dval]} { - # return -code error -errorcode {TOML SYNTAX INVALIDLITERAL} $msg - #} - set dval [tomlish::utils::rawstring_to_jsonstring $dval] - } - default { - error "convert_typeval_to_tomltest unhandled type $dtype" - } - } - return [list type $testtype value $dval] - } - - # Check that each leaf is a typeval or typeval dict - #importantly: must accept empty dict leaves e.g {x {}} - proc is_typeval_dict {d {checkarrays 0}} { - if {![::tomlish::utils::string_is_dict $d]} { - return 0 - } - dict for {k v} $d { - set is_d 0 - if {!([is_typeval $v] || [set is_d [is_typeval_dict $v $checkarrays]])} { - return 0 - } - if {!$is_d} { - set vtype [dict get $v type] - switch -- $vtype { - INT - FLOAT - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL - BOOL - LITERAL - STRING - MULTILITERAL - MULTISTRING {} - ARRAY { - if {$checkarrays} { - set arrdata [dict get $v value] - foreach el $arrdata { - if {![is_typeval_dict $el $checkarrays]} { - return 0 - } - } - } - } - default { - puts stderr "is_typeval_dict: Unexpected type '$vtype'" - return 0 - } - } - } - } - return 1 - } - - proc last_tomltype_posn {d} { - set last_simple -1 - set dictposn [expr {[dict size $d] -1}] - foreach k [lreverse [dict keys $d]] { - set dval [dict get $d $k] - if {[is_typeval $dval]} { - set last_simple $dictposn - break - } - incr dictposn -1 - } - return $last_simple - } - - - #review - proc name_from_tablestack {tablestack} { - set name "" - foreach tinfo [lrange $tablestack 1 end] { - lassign $tinfo type namepart - switch -- $type { - T { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - I { - if {$name eq ""} { - append name $namepart - } else { - append name .$namepart - } - } - default { - #end at first break in the leading sequence of T & I tablenames - break - } - } - } - return $name - } - - - #tablenames_info is a flat dict with the key being an '@@' path - proc _show_tablenames {tablenames_info} { - #e.g {@l@a @@b} {ttype header_table tdefined closed} - append msg \n "tablenames_info:" \n - dict for {tkey tinfo} $tablenames_info { - append msg " " "table: $tkey" \n - dict for {field finfo} $tinfo { - append msg " " "$field $finfo" \n - } - } - return $msg - } - - #take a raw string and classify: result is a 2 element list comprised of KEY|SQKEY|DQKEY and the value being the appropriate inner string - proc classify_rawkey {rawval} { - if {![::tomlish::utils::is_barekey $rawval]} { - #requires quoting - # - #Any dot in the key would have been split by dict::from_tomlish - so if it's present here it's part of this key - not a level separator! - # - #we'll use a basic mechanisms for now to determine the type of quoting - # - whether it has any single quotes or not. - # (can't go in an SQKEY) - # - whether it has any chars that require quoting when in a Bstring - # (if so - then its visual representation might be unsuitable for a key in a toml text file, so escape and put in DQKEY instead of literal SQKEY) - #todo - more? - #REVIEW - the backslash might often be in things like a regex or windows path - which is often better expressed in a literal SQKEY - # from literal examples: - # 'c:\Users\nodejs\templates' - # '<\i\c*\s*>' - #If these are in *keys* our basic test will express these as: - # "c:\\Users\\nodejs\\templates" - # "<\\i\\c*\\s*>" - # This still works - but a smarter test might determine when SQKEY is the better form? - #when coming from external systems - can we even know if the value was already escaped? REVIEW - #Probably when coming from json - we know it's already escaped - and so we build our dict converting keys to unescaped - #TODO - clarify in documentation that keys resulting from dict::from_tomlish are in 'normalized' (unescaped) form - # - #For keys - we currently (2025) are only allowed barekeys,basic strings and literal strings. (no multiline forms) - set k_escaped [tomlish::utils::rawstring_to_Bstring_with_escaped_controls $rawval] - if {[string length $k_escaped] != [string length $rawval]} { - #escaping made a difference - set has_escape_requirement 1 - } else { - set has_escape_requirement 0 - } - if {[string first ' $rawval] >=0 || $has_escape_requirement} { - #basic string - # (any ANSI SGR sequence will end up here in escaped form ) - return [list DQKEY $k_escaped] - } else { - #literal string - return [list SQKEY $rawval] - } - } else { - return [list KEY $rawval] - } - } - #the quoting implies the necessary escaping for DQKEYs - proc join_and_quote_rawkey_list {rawkeylist} { - set result "" - foreach rk $rawkeylist { - lassign [tomlish::dict::classify_rawkey $rk] type val - switch -- $type { - SQKEY { - append result "'$val'." - } - DQKEY { - append result "\"$val\"." - } - KEY { - append result "$val." - } - } - } - return [string range $result 0 end-1] - } - - proc _process_tomlish_dottedkey {element {context_refpath {}}} { - upvar tablenames_info tablenames_info - upvar datastructure datastructure - set dottedtables_defined [list] - set dkey_info [tomlish::get_dottedkey_info $element] - #e.g1 keys {x.y y} keys_raw {'x.y' "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - #e.g2 keys {x.y y} keys_raw {{"x.y"} "a\tb"} keys {x.y {a b}} (keys_raw has escape, keys has literal tab char) - - #[a.b] - #t1.t2.dottedtable.leafkey = "val" - #we have already checked supertables a & {a b} - # - in basic case, passed in context_refpath as {@@a @@b} - # - our context_refpath could also include some combination of keys and array indices e.g {@@a @@b 3 @@subtablekey} - #We need to check {a b t1} & {a b t2} ('creation' only) - #and then 'dottedtable' is 'defined' while leafkey is an ordinary key in dottedtable - - #note we also get here as a 'dottedkey' with the following even though there is no dot in k - #[a.b] - #leafkey = "val" - - set all_dotted_keys [dict get $dkey_info keys] - set dottedkeyname [join $all_dotted_keys .] - - if {[llength $all_dotted_keys] > 1} { - #dottedtable.k=1 - #tX.dottedtable.k=1 - #etc - - #Wrap in a list so we can detect 'null' equivalent. - #We can't use empty string as that's a valid dotted key segment - set dottedtable_bag [list [lindex $all_dotted_keys end-1]] - set dotparents [lrange $all_dotted_keys 0 end-2] - } else { - #basic case - not really a 'dotted' key - #k = 1 - set dottedtable_bag [list] ;#empty bag - set dotparents [list] - } - #assert dottedtable_bag only ever holds 0 or 1 elements - set leaf_key [lindex $all_dotted_keys end] - - #see also: https://github.com/toml-lang/toml/issues/846 "Can dotted keys insert into already-defined [tables]?" - #This code was originally written with a misinterpretation of: - #"Dotted keys create and define a table for each key part before the last one, provided that such tables were not previously created." - # 'each key part before the last one' refers to each key in a single dotted key entry - # not each 2nd-to last key in a list of dotted keys. - - - #we've already tested the table/tablearray keys that got us here.. but not the dottedkey segments (if any) prior to dottedtable & leaf_key - set dottedsuper_refpath $context_refpath - foreach normkey $dotparents { - lappend dottedsuper_refpath @@$normkey - if {![dict exists $tablenames_info $dottedsuper_refpath ttype]} { - #supertable with this combined path (context_path plus parts of dottedkey) not yet 'created' - if {[tomlish::dict::path::exists $datastructure $dottedsuper_refpath]} { - #There is data so it must have been created as a keyval - set msg "Path $dottedsuper_refpath for dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - #dict set tablenames_info $dottedsuper_refpath ttype unknown_table ;#REVIEW - dict set tablenames_info $dottedsuper_refpath ttype unknown_dotted ;#REVIEW - - #see note above re dotted keys insert into already defined table - we need to 'define' all the dotted supers in this block - lappend dottedtables_defined $dottedsuper_refpath - - #ensure empty tables are still represented in the datastructure - tomlish::dict::path::setleaf datastructure $dottedsuper_refpath {} 0;#set to empty subdict - } else { - #added for fixed assumption - set ttype [dict get $tablenames_info $dottedsuper_refpath ttype] - set definedstate [dictn getdef $tablenames_info [list $dottedsuper_refpath tdefined] NULL] - switch -- $ttype { - dottedkey_table - unknown_dotted { - #'created' as dotted - but make sure it's from this header section - i.e defined not set - if {$definedstate ne "NULL"} { - #collision with some other dottedkey - set msg "Table at $dottedsuper_refpath represented by dottedkey $dottedkeyname has been 'defined' elsewhere (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - itable { - #itables are immediately defined - set msg "Table at $dottedsuper_refpath represented by dottedkey $dottedkeyname has been 'defined' as itable (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - default { - #header_table, header_tablearray or unknown_header - #is header_tablearray any different from header_table in this context? - #we don't set tdefined for tablearray anyway - so should be ok here. - if {$definedstate ne "NULL"} { - set msg "Table at $dottedsuper_refpath represented by dottedkey $dottedkeyname has been 'defined' in a header (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - } - } - } - - #dottedtable being 2nd last segment was for original assumption - todo - tidy up? we are duplicating the logic above - #review - any need/advantage to treat 2nd to last key any different from other supers? ie D in a.b.c.D.key=1 - #no need for 'unknown_dotted' vs 'dottedkey_table' ?? - if {[llength $dottedtable_bag] == 1} { - set dottedtable [lindex $dottedtable_bag 0] - set dottedkey_refpath [list {*}$dottedsuper_refpath "@@$dottedtable"] - #our dotted key is attempting to define a table - if {![dict exists $tablenames_info $dottedkey_refpath ttype]} { - #first one - but check datastructure for collisions - if {[tomlish::dict::path::exists $datastructure $dottedkey_refpath]} { - set msg "Path $dottedkey_refpath for dotted key $dottedkeyname already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #'create' the table - dict set tablenames_info $dottedkey_refpath ttype dottedkey_table - #don't actually set 'defined' here.. use the end of TABLE record to close them off by looking at this list - tomlish::dict::path::setleaf datastructure $dottedkey_refpath {} 0 - lappend dottedtables_defined $dottedkey_refpath - - # - } else { - #exists - but might be from another dottedkey within the current header section - #the table is open for adding keys until the next 'header' section ([tablename] / [[tablearray]]) - #check for 'defined' closed (or just existence) - set ttype [dict get $tablenames_info $dottedkey_refpath ttype] - set definedstate [dictn getdef $tablenames_info [list $dottedkey_refpath tdefined] NULL] - switch -- $ttype { - dottedkey_table - unknown_dotted { - #'created' as dotted - but make sure it's from this header section - i.e defined not set - if {$definedstate ne "NULL"} { - #collision with some other dottedkey - set msg "Table at $dottedkey_refpath represented by dottedkey $dottedkeyname has been 'defined' elsewhere (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - itable { - #itables are immediately defined - set msg "Table at $dottedkey_refpath represented by dottedkey $dottedkeyname has been 'defined' as itable (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - default { - #header_table, header_tablearray or unknown_header - #is header_tablearray any different from header_table in this context? - #we don't set tdefined for tablearray anyway - so should be ok here. - if {$definedstate ne "NULL"} { - set msg "Table at $dottedkey_refpath represented by dottedkey $dottedkeyname has been 'defined' in a header (table redefinition) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - } - } - } else { - set dottedkey_refpath $dottedsuper_refpath - } - #assert - dottedkey represents a key val pair that can be added - - - set fullkey_refpath [list {*}$dottedkey_refpath @@$leaf_key] - if {[tomlish::dict::path::exists $datastructure $fullkey_refpath]} { - set msg "Duplicate key. The key (path $fullkey_refpath) already exists at this level in the toml data. The toml data is not valid." - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - - #set keyval_dict [_get_keyval_value $element] - lassign [_get_keyval_value $element] _ keyval_dict _ sub_tablenames_info - - - #keyval_dict is either a {type value } - #or the result from parsing an arbitrary dict from an inline table - which could theoretically look the same at the topmost level - #punk::dict::is_typeval can distinguish - tomlish::log::debug "_process_tomlish_dottedkey>>> context:$context_refpath dottedkey $dottedkeyname kv: $keyval_dict" - tomlish::dict::path::setleaf datastructure $fullkey_refpath $keyval_dict 0 - - #remove ? - #if {![tomlish::dict::is_typeval $keyval_dict]} { - # #the value is either empty or or a dict structure with arbitrary (from-user-data) toplevel keys - # # inner structure will contain {type value } if all leaves are not empty ITABLES - # ##set tkey [list {*}$norm_segments {*}$all_dotted_keys] - - # #by not creating a tablenames_info record - we effectively make it closed anyway? - # #it should be detected as a key - # #is there any need to store tablenames_info for it?? - # #REVIEW - - # ##TODO - update? - # #dictn incr tablenames_info [list $tkey seencount] - # ##if the keyval_dict is not a simple type x value y - then it's an inline table ? - # ##if so - we should add the path to the leaf_key as a closed table too - as it's not allowed to have more entries added. - # #dictn set tablenames_info [list $tkey closed] 1 - #} - return [dict create dottedtables_defined $dottedtables_defined] - } - - - #tomlish::dict::from_tomlish is a *basic* programmatic datastructure for accessing the data. - # produce a dictionary of keys and values from a tomlish tagged list. - # ---------------------------------------------------------------- - # NOTE: - # can instead produce a list if passed an ARRAY at toplevel - # can produce a single value if passed a MULTISTRING or MULTILIST at toplevel - # These are fragments of tomlish used in recursive calls. - # Such fragments don't represent valid tomlish that can be converted to a toml doc. - # ---------------------------------------------------------------- - # dict::from_tomlish is primarily for read access to toml data. - #Extraneous (not within quoted sections) whitespace and comments are not preserved in this structure, - # so a roundtrip from toml to this datastructure and back to toml will lose whitespace formatting and comments. - # creating/changing toml values can be done directly on a tomlish list if preserving (or adding) formatting/comments is desired. - #A separate package 'tomlish::object' may be needed to allow easier programmatic creating/updating/deleting of data elements whilst preserving (or adding or selectively deleting/editing) such formatting. - # - - #within an ARRAY, we store a list of items such as plain dicts (possibly empty) and {type value } for simple types - #(ARRAYS can be mixed type) - #This means our dict structure should have only ARRAY and simple types which need to be in {type value } form - #A dict within an array encodeded as a type ITABLE value should also parse - but is the unpreferred form - REVIEW test? - - #Namespacing? - #ie note the difference: - #[Data] - #temp = { cpu = 79.5, case = 72.0} - # versus - #[Data] - #temps = [{cpu = 79.5, case = 72.0}] - proc from_tomlish {tomlish {returnextra 0}} { - package require dictn - - #keep track of which tablenames have already been directly defined, - # so we can raise an error to satisfy the toml rule: 'You cannot define any key or table more than once. Doing so is invalid' - #Note that [a] and then [a.b] is ok if there are no subkey conflicts - so we are only tracking complete tablenames here. - #we don't error out just because a previous tablename segment has already appeared. - - #Declaring, Creating, and Defining Tables - #https://github.com/toml-lang/toml/issues/795 - #(update - only Creating and Defining are relevant terminology) - - #review - #tablenames_info keys ttype created, tdefined, createdby, definedby, closedby ??? review keys - # [tname] = header_table [[tname]] = header_tablearray - - #consider the following 2 which are legal: - #[table] #'table' created, defined=open type header_table - #x.y = 3 - #[table.x.z] #'table' tdefined=closed closedby={header_table table.x.z}, 'table.x' created, 'table.x.z' created tdefined=open tdefinedby={header_table table.x.z} - #k= 22 - # #'table.x.z' tdefined=closed closedby={eof eof} - - #equivalent datastructure - - #[table] #'table' created, tdefined=open definedby={header_table table} - #[table.x] #'table' tdefined=closed closedby={header_table table.x}, 'table.x' created tdefined=open definedby={header_table table.x} - #y = 3 - #[table.x.z] #'table.x' tdefined=closed closedby={header_table table.x.z}, 'table.x.z' created tdefined=open definedby={header_table table.x.z} - #k=22 - - #illegal - #[table] #'table' created and tdefined=open - #x.y = 3 #'table.x' created first keyval pair tdefined=open definedby={keyval x.y = 3} - #[table.x.y.z] #'table' tdefined=closed, 'table.x' closed because parent 'table' closed?, 'table.x.y' cannot be created - #k = 22 - # - ## - we would fail on encountering table.x.y because only table and table.x are effectively tables - but that table.x is closed should be detected (?) - - #illegal - #[table] - #x.y = {p=3} - #[table.x.y.z] - #k = 22 - ## we should fail because y is an inline table which is closed to further entries - - #note: it is not safe to compare normalized tablenames using join! - # e.g a.'b.c'.d is not the same as a.b.c.d - # instead compare {a b.c d} with {a b c d} - # Here is an example where the number of keys is the same, but they must be compared as a list, not a joined string. - #'a.b'.'c.d.e' vs 'a.b.c'.'d.e' - #we need to normalize the tablenames seen so that {"x\ty"} matches {"xy"} - - - - if {[uplevel 1 [list info exists tablenames_info]]} { - upvar tablenames_info tablenames_info - } else { - set tablenames_info [dict create] ;#keyed on tablepath each of which is an @@path such as {@@config @@subgroup @@etc} (corresponding to config.subgroup.etc) - #also has non @@ indexes which are list indexes as taken by tcl list commands (int or end-1 etc) - #value is a dict with keys: ttype, tdefined - } - - if {![string is list $tomlish]} { - error "tomlish::dict::from_tomlish Supplied value for tomlish does not appear to be a tomlish list. Use tomlish::from_toml to get a tomlish list from toml." - } - - - log::info "---> dict::from_tomlish processing '$tomlish'<<<" - set items $tomlish - - foreach lst $items { - if {[lindex $lst 0] ni $::tomlish::tags} { - error "tomlish::dict::from_tomlish supplied list does not appear to be toml parsed into a tomlish tagged list. Run tomlish::decode::toml on the raw toml data to produce a tomlish list" - } - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - #ignore TOMLISH tag at beginning - set items [lrange $tomlish 1 end] - } - - set datastructure [dict create] - set dottedtables_defined [list] - foreach item $items { - set tag [lindex $item 0] - #puts "...> item:'$item' tag:'$tag'" - switch -exact -- $tag { - KEY - DQKEY - SQKEY - INT - FLOAT - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL - STRING - LITERAL { - #we don't require invalid tomlish fragments with these keys in our direct recursion - #(we do support ARRAY, MULTISTING, and MULTILITERAL tomlish fragments below) - error "tomlish::dict::from_tomlish error: invalid tag: $tag. At the toplevel, from_tomlish can only process WS NEWLINE COMMENT and compound elements DOTTEDKEY TABLE TABLEARRAY ITABLE MULTILITERAL MULTISTRING" - } - DOTTEDKEY { - #toplevel dotted key empty context_refpath - set dkinfo [_process_tomlish_dottedkey $item {}] - lappend dottedtables_defined {*}[dict get $dkinfo dottedtables_defined] - #at any level - we don't expect any more DOTTEDKEY records in a tomlish structure after TABLE or TABLEARRAY are encountered - #as those records should encapsulate their own dottedkeys - - } - TABLEARRAY { - #close off any dottedtables_defined created by dottedkeys at this level - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - set dottedtables_defined [list] ;#for closing off at end by setting 'defined' - - set tablearrayname [lindex $item 1] - tomlish::log::debug "---> tomlish::dict::from_tomlish processing item TABLENAME (name: $tablearrayname): $item" - set norm_segments [::tomlish::toml::tablename_split $tablearrayname true] ;#true to normalize - #we expect repeated tablearray entries - each adding a sub-object to the value, which is an array/list. - #tablearrayname is likely to appear multiple times - so unlike a TABLE we don't check for 'defined' for the full name as an indicator of a problem - set supertable [list] - ############## - # [[a.b.c.d]] - # norm_segments = {a b c d} - #check a {a b} {a b c} <---- supertables of a.b.c.d - ############## - set refpath [list] ;#e.g @@j1 @@j2 1 @@k1 end - foreach normseg [lrange $norm_segments 0 end-1] { - lappend supertable $normseg - lappend refpath @@$normseg - if {![dict exists $tablenames_info $refpath ttype]} { - #supertable with this path doesn't yet exist - if {[tomlish::dict::path::exists $datastructure $refpath]} { - #There is data though - so it must have been created as a keyval - set msg "Supertable [join $supertable .] of tablearray name $tablearrayname already has data but doesn't appear to be a table - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablearray_supertable_keycollision - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } else { - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - #review - we can't later specify as tablearray so should just set ttype to header_table even though it's being created - # because of a tablearray header? - #By setting ttype to something other than table_header we can provide more precise errorCode/msg ?? - dict set tablenames_info $refpath ttype unknown_header - #ensure empty tables are still represented in the datastructure - dict set datastructure {*}$supertable [list] - } - } else { - #REVIEW!! - # what happens with from_toml {[[a.b.c]]} {[[a.b]]} ??? - #presumed that a and a.b were 'created' as tables (supertables of tablearray at a.b.c) and can't now be redefined as tablearrays - - #supertable has already been created - and may be defined - but even if defined we can add subtables unless it is of type itable - #but if it's a tablearray - we need to point to the most 'recently defined table element of the array' - #(last member of that array - need to check type? allowed to have non-table elements ie nonhomogenous??) - set supertype [dict get $tablenames_info $refpath ttype] - if {$supertype eq "header_tablearray"} { - #exercised by toml-tests: - # valid/table/array-table-array - # valid/table/array-nest - - #puts stdout "todict!!! TABLEARRAY nesting required for supertable [join $supertable .]" - - #'refer' to the appropriate element in existing array - set arrdata [tomlish::dict::path::get $datastructure [list {*}$refpath @@value]] - set idx [expr {[llength $arrdata]-1}] - if {$idx < 0} { - #existing tablearray should have at least one entry even if empty (review) - set msg "reference to empty tablearray?" - return -code error -errorcode {TOMLISH STRUCTURE REFTOEMPTYTABLEARRAY} $msg - } - lappend refpath $idx - } - } - } - # - #puts "TABLE supertable refpath $refpath" - lappend refpath @@[lindex $norm_segments end] - tomlish::log::debug "TABLEARRAY refpath $refpath" - set tablearray_refpath $refpath - - - if {![dict exists $tablenames_info $tablearray_refpath ttype]} { - #first encounter of this tablearrayname - if {[tomlish::dict::path::exists $datastructure $tablearray_refpath]} { - #e.g from_toml {a=1} {[[a]]} - set msg "Cannot create tablearray name $tablearrayname. Key already has data but key doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablearray_direct_keycollision_error - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #no collision - we can create the tablearray and the array in the datastructure - dict set tablenames_info $tablearray_refpath ttype header_tablearray - #dict set datastructure {*}$norm_segments [list type ARRAY value {}] - #create array along with empty array-item at position zero - tomlish::dict::path::setleaf datastructure $tablearray_refpath [list type ARRAY value {{}}] 0 - set arrayitem_refpath [list {*}$tablearray_refpath 0] - #set ARRAY_ELEMENTS [list] - } else { - #we have an existing tablenames_info record for this path - but is it a tablearray? - set ttype [dict get $tablenames_info $tablearray_refpath ttype] - if {$ttype ne "header_tablearray"} { - #header_table or itable - switch -- $ttype { - itable {set ttypename itable} - header_table {set ttypename table} - dottedkey_table {set ttypename dottedkey_table} - unknown_header - unknown_dotted { - #table was created e.g as supertable - but not specifically a tablearray - #violates ordering - return specific test error - set msg "Table $tablearrayname referenced as supertable before tablearray defined (ordering)" - return -code error -errorcode {TOMLISH STRUCTURE TABLEARRAYORDERING} $msg - } - default {error "unrecognised type $ttype - expected header_table or itable"} - } - set msg "tablearray name $tablearrayname already appears to be already created as '$ttypename' not tablearray - invalid?" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #EXISTING tablearray - #add to array - #error "add_to_array not implemented" - #{type ARRAY value } - #set ARRAY_ELEMENTS [dict get $datastructure {*}$norm_segments value] - tomlish::log::debug ">>>>pre-extend-array dict::from_tomlish datastructure: $datastructure" - set existing_array [tomlish::dict::path::get $datastructure [list {*}$tablearray_refpath @@value]] - set arrayitem_refpath [list {*}$tablearray_refpath [llength $existing_array]] - tomlish::dict::path::lappend datastructure $tablearray_refpath {} - tomlish::log::debug ">>>>post-extend-array dict::from_tomlish datastructure: $datastructure" - } - - - #set object [dict create] ;#array context equivalent of 'datastructure' - - #add to ARRAY_ELEMENTS and write back in to datastructure. - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - tomlish::log::debug "----> todict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkinfo [_process_tomlish_dottedkey $element $arrayitem_refpath] - lappend dottedtables_defined {*}[dict get $dkinfo dottedtables_defined] - } - NEWLINE - COMMENT - WS { - #ignore - } - TABLE { - #we *perhaps* should be able to process tablearray subtables either as part of the tablearray record, or independently. - #(or even a mixture of both, although that is somewhat an edge case, and of limited utility) - #[[fruit]] - #x=1 - # [fruit.metadata] - # [fruit.otherdata] - - #when processing a dict destined for the above - the tomlish generator (e.g from_dict) - #should create as 1 or 3 records (but could create 2 records if there was an unrelated table in between the subtables) - #choices: all in tablearray record, tablearray + 1 or 2 table records. - # - #We are going the other way here - so we just need to realise that the list of tables 'belonging' to this tablearray might not be complete. - # - #the subtable names must be prefixed with the tablearray - we should validate that for any contained TABLE records - - #The default mechanism is for from_dict to produce tomlish with separate TABLE records - and use the ordering to determine membership - #If we were to support wrapping the TABLE records within a TABLEARRAY - we should also support TABLEARRAY within TABLEARRAY - # ----------------------------------------------------------------------- - #Implementing this is not critical for standard encoding/decoding of toml! - #It would be an alternative form for the tomlish intermediate form - and adds complexity. - # - #The upside would be to provide a function for sorting/rearranging in the tomlish form if all records were fully encapsulated. - #A possible downside is that unrelated tables placed before a tablearray is fully defined (within the tablearray definition area in toml) - # would have to be re-positioned before or after the encapsulated tablearray record. - # While unrelated tables in such a position aren't a recommended way to write toml, they appear to be valid - # and preserving the author's ordering is a goal of the basic encoding/decoding operations if no explicit sorting/reordering was requested. - # - #Consider an 'encapsulate' method to this (tomlish -> tomlish) - # ----------------------------------------------------------------------- - #todo - error "tomlish::dict::from_tomlish TABLE element within TABLEARRAY not handled - TABLE should be a separate tomlish record" - } - default { - error "tomlish::dict::from_tomlish Sub element of type '$type' not understood in tablearray context. Expected only DOTTEDKEY,NEWLINE,COMMENT,WS" - } - } - } - - #end of TABLEARRAY record - equivalent of EOF or next header - close off the dottedtables - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - } - TABLE { - #close off any dottedtables_defined created by dottedkeys at this level - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - set tablename [lindex $item 1] - set dottedtables_defined [list] ;#for closing off at end by setting 'defined' - #As our TABLE record contains all it's child DOTTEDKEY records - this should be equivalent to setting them as defined at EOF or next header. - - #----------------------------------------------------------------------------------- - #default assumption - our reference is to the main tablenames_info and datastructure - #Will need to append keys appropriately if we have recursed - #----------------------------------------------------------------------------------- - - log::debug "---> tomlish::dict::from_tomlish processing item TABLE (name: $tablename): $item" - set norm_segments [::tomlish::toml::tablename_split $tablename true] ;#true to normalize - - - - set name_segments [::tomlish::toml::tablename_split $tablename 0] ;#unnormalized e.g ['a'."b".c.d] -> 'a' "b" c d - #results of tablename_split 0 are 'raw' - ie some segments may be enclosed in single or double quotes. - - - set supertable [list] - ############## - # [a.b.c.d] - # norm_segments = {a b c d} - #check a {a b} {a b c} <---- supertables of a.b.c.d - ############## - - ############## - #[[a]] - #[a.b] #supertable a is tablearray - ############## - - #also consider - ############## - # [[a.b]] - # [a.b.c.d] #supertable a is a table, supertable a.b is tablearray, supertable a.b.c is elementtable - ############## - set refpath [list] ;#e.g @@j1 @@j2 1 @@k1 end - foreach normseg [lrange $norm_segments 0 end-1] { - lappend supertable $normseg - lappend refpath @@$normseg - if {![dict exists $tablenames_info $refpath ttype]} { - #supertable with this path doesn't yet exist - if {[tomlish::dict::path::exists $datastructure $refpath]} { - #There is data though - so it must have been created as a keyval - set msg "Supertable [join $supertable .] of table name $tablename (path $refpath) already has data but doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #here we 'create' it, but it's not being 'defined' ie we're not setting keyvals for it here - #we also don't know whether it's a table or a dottedkey_table (not allowed to be tablearray - out of order?) - dict set tablenames_info $refpath ttype unknown_header - #ensure empty tables are still represented in the datastructure - #dict set datastructure {*}$supertable [list] - tomlish::dict::path::setleaf datastructure $refpath {} 0 - } else { - #supertable has already been created - and may be defined - but even if defined we can add subtables unless it is of type itable - if {[dict get $tablenames_info $refpath ttype] eq "header_tablearray"} { - #'refer' to the appropriate element in existing array - set arrdata [tomlish::dict::path::get $datastructure [list {*}$refpath @@value]] - set idx [expr {[llength $arrdata]-1}] - if {$idx < 0} { - #existing tablearray should have at least one entry even if empty (review) - set msg "reference to empty tablearray?" - return -code error -errorcode {TOMLISH STRUCTURE REFTOEMPTYTABLEARRAY} $msg - } - lappend refpath $idx - } else { - #?? - if {[dictn getdef $tablenames_info [list $refpath tdefined] NULL] eq "NULL"} { - } else { - } - } - } - } - #puts "TABLE supertable refpath $refpath" - lappend refpath @@[lindex $norm_segments end] - tomlish::log::info "TABLE refpath $refpath" - set table_refpath $refpath - - - - - #table [a.b.c.d] hasn't been defined - but may have been 'created' already by a longer tablename - # - or may have existing data from a keyval - if {![dict exists $tablenames_info $table_refpath ttype]} { - if {[tomlish::dict::path::exists $datastructure $table_refpath]} { - #e.g from_toml {a=1} {[a]} - set msg "Cannot create table name $tablename. Key already has data but key doesn't appear to be a table (keycollision) - invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - #test: datastructure_tablename_keyval_collision_error - return -code error -errorcode {TOMLISH STRUCTURE KEYCOLLISION} $msg - } - #no data or previously created table - dict set tablenames_info $table_refpath ttype header_table - - #We are 'defining' this table's keys and values here (even if empty) - #dict set datastructure {*}$norm_segments [list] ;#ensure table still represented in datastructure even if we add no keyvals here - tomlish::dict::path::setleaf datastructure $table_refpath {} 0;#ensure table still represented in datastructure even if we add no keyvals here - } else { - if {[dict get $tablenames_info $table_refpath ttype] eq "header_tablearray"} { - #e.g tomltest invalid/table/duplicate-table-array2 - #[[tbl]] - #[tbl] - set msg "Table name $tablename has already been created as a tablearray. Invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } else { - #any other type tdefined is a problem - set T_DEFINED [dictn getdef $tablenames_info [list $table_refpath tdefined] NULL] - if {$T_DEFINED ne "NULL" } { - #our tablename e.g [a.b.c.d] declares a space to 'define' subkeys - but there has already been a definition space for this path - set msg "Table name $tablename has already been defined in the toml data. Invalid" - append msg \n [tomlish::dict::_show_tablenames $tablenames_info] - #raise a specific type of error for tests to check - return -code error -errorcode {TOMLISH STRUCTURE TABLEREDEFINED} $msg - } - } - } - dict set tablenames_info $table_refpath tdefined open - - #now add the contained elements - foreach element [lrange $item 2 end] { - set type [lindex $element 0] - log::debug "----> todict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkinfo [_process_tomlish_dottedkey $element $table_refpath] - lappend dottedtables_defined {*}[dict get $dkinfo dottedtables_defined] - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in table context. Expected only DOTTEDKEY,NEWLINE,COMMENT,WS" - } - } - } - - #end of TABLE record - equivalent of EOF or next header - close off the dottedtables - foreach dtablepath $dottedtables_defined { - dict set tablenames_info $dtablepath tdefined closed - } - } - ITABLE { - #As there is no other mechanism to create tables within an ITABLE than dottedkeys - # and ITABLES are fully defined/enclosed - we can rely on key collision and don't need to track dottedtables_defined - REVIEW. - set dottedtables_defined [list] - #SEP??? - #ITABLE only ever on RHS of = or inside ARRAY - set datastructure [dict create] - set tablenames_info [dict create] - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - DOTTEDKEY { - set dkinfo [_process_tomlish_dottedkey $element] - } - NEWLINE - COMMENT - WS { - #ignore - } - default { - error "Sub element of type '$type' not understood in ITABLE context. Expected only KEY,DQKEY,SQKEY,NEWLINE,COMMENT,WS" - } - } - } - } - ARRAY { - #invalid at toplevel of a 'complete' tomlish structure - but we support it here for recursive fragment processing - #arrays in toml are allowed to contain mixtures of types - set datastructure [list] - log::debug "--> processing array: $item" - - foreach element [lrange $item 1 end] { - set type [lindex $element 0] - log::debug "----> tododict processing $tag subitem $type processing contained element $element" - switch -exact -- $type { - INT - FLOAT - BOOL - DATETIME - DATETIME-LOCAL - DATE-LOCAL - TIME-LOCAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - STRING { - #JJJJ - #don't unescape string! - set value [lindex $element 1] - #lappend datastructure [list type $type value [::tomlish::utils::unescape_string $value]] - lappend datastructure [list type $type value $value] - } - LITERAL { - set value [lindex $element 1] - lappend datastructure [list type $type value $value] - } - ITABLE { - #anonymous table - #lappend datastructure [list type $type value [::tomlish::to_dict [list $element]]] - lappend datastructure [::tomlish::dict::from_tomlish [list $element]] ;#store itables within arrays as raw dicts (possibly empty) - } - TABLE - TABLEARRAY { - #invalid? shouldn't be output from from_dict - but could manually be constructed as such? review - #doesn't make sense as table needs a name? - #take as synonym for ITABLE? - error "tomlish::dict::from_tomlish $type within array unexpected" - } - ARRAY - MULTISTRING - MULTILITERAL { - #set value [lindex $element 1] - lappend datastructure [list type $type value [::tomlish::dict::from_tomlish [list $element]]] - } - WS - SEP - NEWLINE - COMMENT { - #ignore whitespace, commas, newlines and comments - } - default { - error "tomlish::dict::from_tomlish Unexpected value type '$type' found in array" - } - } - } - } - MULTILITERAL { - #Not for toplevel of complete tomlish - (recursive fragment processing) - - #triple squoted string - #first newline stripped only if it is the very first element - #(ie *immediately* following the opening delims) - #All whitespace other than newlines is within LITERALPARTS - # ------------------------------------------------------------------------- - #todo - consider extension to toml to allow indent-aware multiline literals - # how - propose as issue in toml github? Use different delim? e.g ^^^ ? - #e.g - # xxx=?'''abc - # def - # etc - # ''' - # - we would like to trimleft each line to the column following the opening delim - # ------------------------------------------------------------------------- - - log::debug "---> todict processing multiliteral: $item" - set parts [lrange $item 1 end] - if {[lindex $parts 0 0] eq "NEWLINE"} { - set parts [lrange $parts 1 end] ;#skip it - } - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - switch -exact -- $type { - LITERALPART { - append stringvalue [lindex $element 1] - } - NEWLINE { - set val [lindex $element 1] - if {$val eq "lf"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - MULTISTRING { - #Not for toplevel of complete tomlish - (recursive fragment processing) - #triple dquoted string - log::debug "---> tomlish::dict::from_tomlish processing multistring: $item" - set stringvalue "" - set idx 0 - set parts [lrange $item 1 end] - for {set idx 0} {$idx < [llength $parts]} {incr idx} { - set element [lindex $parts $idx] - set type [lindex $element 0] - #We use STRINGPART in the tomlish representation as a distinct element to STRING - which would imply wrapping quotes to be reinserted - switch -exact -- $type { - STRING { - #todo - do away with STRING ? - #we don't build MULTISTRINGS containing STRING - but should we accept it? - tomlish::log::warn "double quoting a STRING found in MULTISTRING - should be STRINGPART?" - #append stringvalue "\"[::tomlish::utils::unescape_string [lindex $element 1]]\"" - append stringvalue "\"[lindex $element 1]\"" - } - STRINGPART { - #JJJ - #don't unescape string - #append stringvalue [::tomlish::utils::unescape_string [lindex $element 1]] - append stringvalue [lindex $element 1] - } - CONT { - #When the last non-whitespace character on a line is an unescaped backslash, - #it will be trimmed along with all whitespace (including newlines) up to the next non-whitespace character or closing delimiter - # review - we allow some whitespace in stringpart elements - can a stringpart ever be all whitespace? - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last (or first and only) line - return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} "Invalid whitespace escape - not a valid continuation position" - #set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - #if {$non_ws >= 0} { - # #append stringvalue "\\" - # return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} "Invalid whitespace escape - not a valid continuation position" - #} else { - # #skip over ws without emitting - # set idx [llength $parts] - #} - } else { - set parts_til_nl [lrange $parts 0 $next_nl-1] - set non_ws [lsearch -index 0 -start $idx+1 -not $parts_til_nl WS] - if {$non_ws >= 0} { - #This CONT is invalid. If there had been a non-whitespace char directly following it, - #it wouldn't have come through as a CONT token - #Now that we see it isn't the last non-whitespace backslash on the line we can reject - # as an invalid escape of space or tab - #append stringvalue "\\" - return -code error -errorcode {TOMLISH SYNTAX INVALIDESCAPE} "Invalid whitespace escape - not a valid continuation position" - } else { - #skip over ws on this line - set idx $next_nl - #then have to check each subsequent line until we get to first non-whitespace - set trimming 1 - while {$trimming && $idx < [llength $parts]} { - set next_nl [lsearch -index 0 -start $idx+1 $parts NEWLINE] - if {$next_nl == -1} { - #last line - set non_ws [lsearch -index 0 -start $idx+1 -not $parts WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - } else { - set idx [llength $parts] - } - set trimming 0 - } else { - set non_ws [lsearch -index 0 -start $idx+1 -not [lrange $parts 0 $next_nl-1] WS] - if {$non_ws >= 0} { - set idx [expr {$non_ws -1}] - set trimming 0 - } else { - set idx $next_nl - #keep trimming - } - } - } - } - } - } - NEWLINE { - #if newline is first element - it is not part of the data of a multistring - if {$idx > 0} { - set val [lindex $element 1] - if {$val eq "lf"} { - append stringvalue \n - } else { - append stringvalue \r\n - } - } - } - WS { - append stringvalue [lindex $element 1] - } - default { - error "Unexpected value type '$type' found in multistring" - } - } - } - set datastructure $stringvalue - } - WS - COMMENT - NEWLINE { - #ignore - } - BOM { - #this token is the unicode single char \uFFEF - #It doesn't tell us what encoding was originally used (though toml should only accept UTF-8 files) - #ignore at start - what about in other positions? - } - default { - error "Unexpected tag '$tag' in Tomlish list '$tomlish'" - } - } - } - if {!$returnextra} { - return $datastructure - } else { - return [dict create datastructure $datastructure tablenames_info $tablenames_info] - } - } -} -namespace eval tomlish::path { - namespace export {[a-z]*}; # Convention: export all lowercase - - set test_tomlish [tomlish::from_toml { } #comment {z=1} {x.y=2 #xy2} {[[shop.product]] #product1} {x=[ #array1} {11 #val1} {, 12 #val2} {]} {[unrelated.' etc ']} {a.b={c=666}} {a.x={}} {[[shop.product]]} {x="test"} {[shop]} {name="myshop"}] - - proc get {tomlish {path {}}} { - if {$path eq ""} { - return $tomlish - } - if {[string index $path 0] in [list . "\["]} { - set path [tomlish::utils::jq_to_path $path] - } - - #at the cost of some performance, sanity check that the tomlish is valid - if {[catch {tomlish::to_dict $tomlish} d]} { - error "tomlish::path::get error supplied tomlish is malformed\nerrmsg: $d" - } - #since we have the dict - test the path is valid - if {![tomlish::dict::path::exists $d $path]} { - error "tomlish::path::get - path \"$path\" not found in tomlish $tomlish" - } - - if {[lindex $tomlish 0] eq "TOMLISH"} { - set tomlish [lrange $tomlish 1 end] - } - ::set pathsofar [list] - ::set tomlitems [list] ;#reducing set. 2 element list {keypath itemlist} - foreach record $tomlish { - lappend tomlitems [list {} [list $record]] ;#root records - } - - ::set dictsubpath [list] ;#reset at every index encounter? - foreach p $path { - ::lappend pathsofar $p - set sublist [list] - if {[string range $p 0 1] eq "@@"} { - set realsearchkey [string range $p 2 end] - lappend dictsubpath $realsearchkey - foreach path_items $tomlitems { - lassign $path_items subpath tlist - lappend subpath $realsearchkey - foreach item $tlist { - set tp [lindex $item 0] - switch -- $tp { - WS - NEWLINE - COMMENT { - } - DOTTEDKEY { - #can occur at toplevel (before others) or within other elements - set keyinfo [tomlish::get_dottedkey_info $item] - set keys_raw [dict get $keyinfo keys_raw] - puts stderr "subpath:$subpath -->DOTTEDKEY keys_raw: $keys_raw" - #may not be enough keys_raw for subpath - but there could be further ITABLES to continue the dict further - set prefixparts [lrange $keys_raw 0 [llength $subpath]-1] - set is_kmatch 1 ;#default assumption only - foreach dsub $subpath kpart $prefixparts { - if {$dsub ne $kpart} { - set is_kmatch 0 - } - } - if {$is_kmatch} { - if {[llength $keys_raw] == [llength $subpath]} { - set subpath [list] - #e.g {DOTTEDKEY {{KEY xxx}} = {WS { }} {STRING blah}} - lappend sublist [list $subpath [lrange $item 3 end]] - } else { - lappend sublist [list $subpath [list $item]] - } - } - } - ITABLE { - #subelement only - set itablechildren [lrange $item 1 end] - puts stderr "subpath:$subpath -->ITABLE records: $itablechildren" - set nextpath [lmap v $subpath {string cat @@ $v}] - set results [tomlish::path::get $itablechildren $nextpath] - set subpath [list] - puts "--> lappending [list $subpath $results]" - lappend sublist [list $subpath $results] - } - TABLEARRAY { - #toplevel only - set fulltablename [lindex $item 1] - set normalise 1 - set tparts [tomlish::toml::tablename_split $fulltablename $normalise] - if {[llength $tparts] < [llength $subpath]} {continue} ;#not enough parts to satisfy current subpath query - set prefixparts [lrange $tparts 0 [llength $subpath]-1] - set is_tmatch 1 ;#default assumption only - foreach dsub $subpath tpart $prefixparts { - if {$dsub ne $tpart} { - set is_tmatch 0 - } - } - #TODO reference arrays - if {$is_tmatch} { - if {[llength $tparts] == [llength $subpath]} { - set subpath [list] - lappend sublist [list $subpath [lrange $item 2 end]] - } else { - #TODO - set subpath 0 - lappend sublist [list $subpath [list $item]] ;#add entire TABLE line - } - } - } - TABLE { - #toplevel only - set fulltablename [lindex $item 1] - set normalise 1 - set tparts [tomlish::toml::tablename_split $fulltablename $normalise] - if {[llength $tparts] < [llength $subpath]} {continue} ;#not enough parts to satisfy current subpath query - set prefixparts [lrange $tparts 0 [llength $subpath]-1] - set is_tmatch 1 ;#default assumption only - foreach dsub $subpath tpart $prefixparts { - if {$dsub ne $tpart} { - set is_tmatch 0 - } - } - if {$is_tmatch} { - if {[llength $tparts] == [llength $subpath]} { - set subpath [list] - lappend sublist [list $subpath [lrange $item 2 end]] - } else { - #leave subpath - lappend sublist [list $subpath [list $item]] ;#add entire TABLE line - } - } - } - ARRAY { - #subelement only - } - - } - } - } - } else { - #index - #will never occur at toplevel (dict::path::exists already ruled it out) - foreach path_items $toml_items { - lassign $path_items subpath $tlist - set tp [lindex $tlist 0] - switch -- $tp { - ARRAY { - } - } - } - } - #temp - puts stdout "pathsofar: $pathsofar" - puts stdout [punk::lib::showdict -roottype list $sublist] - set tomlitems $sublist - } - - #REVIEW - if {[llength $tomlitems] == 1} { - return [lindex $tomlitems 0 1] - } - set result [list] - foreach i $tomlitems { - lappend result [lindex $i 1] - } - return $result - #return [lindex $tomlitems 1] - } - -} - -namespace eval tomlish::dict::path { - - #access tomlish dict structure - namespace export {[a-z]*}; # Convention: export all lowercase - - #access with path such as: @@k @@k 0 @@k end where dict keys marked with @@ and plain values are list indices into in {type ARRAY value } - #leaf elements returned as structured {type value } - - proc get {dictval {path {} } } { - if {$path eq ""} { - return $dictval - } - if {[string index $path 0] in [list . "\[" ] } { - set path [tomlish::utils::jq_to_path $path] - } - - ::set data $dictval - ::set pathsofar [list] - ::set i 0 - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@" } { - #dict key - ::set data [dict get $data [string range $p 2 end]] - } else { - #ARRAY or raw list index - if {[llength $pathsofar] > 1 && [string trim [lindex $pathsofar $i-1]] eq ""} { - #previous path was query for entire list - result is a raw list, not a dict - if {[string trim $p] eq ""} { - #review - multiple {[]} in a row in the path is pretty suspicious - raise error - error "tomlish::dict::path::get error - multiple empty indices in a row not supported" - } - ::set data [lindex $data $p] - } else { - if {![tomlish::dict::is_typeval $data]} { - error "tomlish::dict::path::get error bad path $path. Attempt to access table or other value as array at subpath $pathsofar." - } - if {[dict get $data type] ne "ARRAY"} { - error "tomlish::dict::get error bad path $path. Subpath $pathsofar is not an array." - } - ::set arrdata [dict get $data value] - #when $p is empty string (or whitespace) - lindex returns entire list (or empty list) - # - this corresponds to jq: {[]} or path {""} - ::set data [lindex $arrdata $p] - } - } - incr i - } - return $data - } - - - proc exists {dictval path} { - #completely empty path considered to exist - review - if {[string index $path 0] in [list . "\[" ] } { - set path [tomlish::utils::jq_to_path $path] - } - ::set data $dictval - ::set pathsofar [list] - ::set exists 1 - ::set i 0 - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@"} { - #dict key - ::set k [string range $p 2 end] - if {![dict exists $data $k]} { - return 0 - } - ::set data [dict get $data $k] - } else { - #ARRAY or raw list index - if {[llength $pathsofar] > 1 && [string trim [lindex $pathsofar $i-1]] eq ""} { - #previous path was query for entire list - result is not a dict - if {[string trim $p] eq ""} { - #review - multiple {[]} in a row in the path is pretty suspicious - raise error - error "tomlish::dict::path::exists error - multiple empty indices in a row not supported" - #or just leave data as is? - } else { - ::set intp [tomlish::system::lindex_resolve_basic $data $p] - if {$intp == -1} { - return 0 - } - ::set data [lindex $data $p] - } - } else { - if {![tomlish::dict::is_typeval $data]} { - return 0 - } - if {[dict get $data type] ne "ARRAY"} { - return 0 - } - #special case for empty path syntax {jq: [] path: ""} meaning retrieve all elements in list - ::set arrdata [dict get $data value] - if {[string trim $p] eq ""} { - #we have confirmed above it is an ARRAY - we consider an empty list to exist. - #UUU - ::set data $arrdata - } else { - #for 'exists' we need to avoid lindex returning empty string for out of bounds - ::set intp [tomlish::system::lindex_resolve_basic $arrdata $p] ;#handle index math (end-1 etc) - if {$intp == -1} { - #out of bounds - return 0 - } - ::set data [lindex $arrdata $p] - } - } - } - incr i - } - return $exists - } - - - #raise error for invalid - proc validate_typeval {typeval} { - set valtype [dict get $typeval type] - set rawval [dict get $typeval value] - switch -- $valtype { - INT { - if {![tomlish::utils::is_int $rawval]} { - return -code error -errorcode {TOML TYPE NOT_INT} "validate_typeval value is not a valid toml int: '$rawval'" - } - } - BOOL { - #toml only accepts lower case true and false - #review - if {$rawval ni {true false} } { - return -code error -errorcode {TOML TYPE NOT_INT} "validate_typeval value is not a valid toml boolean (true|false): '$rawval'" - } - } - FLOAT { - if {![tomlish::utils::is_float $rawval]} { - return -code error -errorcode {TOML TYPE NOT_INT} "validate_typeval value is not a valid toml float: '$rawval'" - } - } - DATETIME { - #review - accept even when more specific types apply? - if {![tomlish::utils::is_datetime]} { - return -code error -errorcode {TOML TYPE NOT_DATETIME} "validate_typeval value is not a valid toml datetime: '$rawval'" - } - } - DATETIME-LOCAL { - if {![tomlish::utils::is_datetime-local]} { - return -code error -errorcode {TOML TYPE NOT_DATETIME-LOCAL} "validate_typeval value is not a valid toml datetime-local: '$rawval'" - } - } - DATE-LOCAL { - if {![tomlish::utils::is_date-local]} { - return -code error -errorcode {TOML TYPE NOT_DATE-LOCAL} "validate_typeval value is not a valid toml date-local: '$rawval'" - } - } - TIME-LOCAL { - if {![tomlish::utils::is_time-local]} { - return -code error -errorcode {TOML TYPE NOT_TIME-LOCAL} "validate_typeval value is not a valid toml time-local: '$rawval'" - } - } - ARRAY { - if {$rawval eq ""} { - return - } - foreach el $rawval { - validate_typeval $el - } - } - STRING { - if {![tomlish::utils::inner_Bstring_is_valid_toml $rawval]} { - return -code error -errorcode {TOML TYPE NOT_BSTRING} "validate_typeval value is not a valid toml basic string: '$rawval'" - } - } - MULTISTRING { - #multistring as a single value - #UUU - if {![tomlish::utils::inner_MultiBstring_is_valid_toml $rawval]} { - return -code error -errorcode {TOML TYPE NOT_MLBSTRING} "validate_typeval value is not a valid toml multistring: '$rawval'" - } - } - LITERAL { - #todo? - } - MULTILITERAL { - #? - } - default { - return -code error -errorcode {TOML TYPE UNRECOGNISED} "validate_typeval does not recognise type '$valtype'" - } - } - } - - #a restricted analogy of 'dictn set' - #set 'leaf' values only - don't create intermediate paths - # can replace an existing dict with another dict - # can create a key when key at tail end of path is a key (ie @@keyname, not index) - # can replace an existing {type value value } - # with added restriction that if is ARRAY the new must also be ARRAY - - - # vscode tcl syntax highlighter is unable to handle (in some cases!) some simple constructs like left square bracket in curly braces, - # yet it is ok in comments. i.e {[} is prolematic for the highlighter, so we use "\[" instead :/ - #e.g ------------------------------------------------ - # if {[string index $path 0] in [list . {[}] } { - # # ... - # } - # ------------------------------------------------ - #This may highlight ok - and even text immediately following can be ok - but - # the subsequent code block at global scope, perhaps *many* lines distant from where the syntax highlighting issue started, may then be completely miscoloured - # This is a big timewaster - a decent syntax highlighter is really needed for Tcl in vscode (2025-09) - - package require struct::list - proc setleaf {dictvariable path value {validate 1}} { - if {[string index $path 0] in [list . "\[" ] } { - set path [tomlish::utils::jq_to_path $path] - } - - upvar $dictvariable dict_being_edited - if {![info exists dict_being_edited]} { - error "tomlish::dict::path::setleaf error - supplied value for 'dictvariable' doesn't seem to be the name of an existing variable" - } - ::set data $dict_being_edited - ::set pathsofar [list] - if {!([tomlish::dict::is_typeval $value] || [tomlish::dict::is_typeval_dict $value 0])} { - #failed check of supplied value as basic type, or a sub-dict structure (not checking arrays) - error "tomlish::dict::path::setleaf error - value must already be in the tomlish form {type value } or be a dict with such forms as leaves" - } - if {$validate && [tomlish::dict::is_typeval $value]} { - #validate value element of $value is correct for type element - if {[catch {validate_typeval $value} errM]} { - return -code error -errorcode {TOMLISH VALIDATION TYPEFAIL} $errM - } - } - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@"} { - ::set k [string range $p 2 end] - - # if {![dict exists $data $k]} { - # error "tomlish::dict:path::set error bad path $path. Attempt to access nonexistent element at subpath $pathsofar." - # } - ::set varname v[incr v] - - if {[struct::list equal $pathsofar $path]} { - #see if leaf of the path given already exists - if {[dict exists $data $k]} { - ::set endpoint [dict get $data $k] - if {[tomlish::dict::is_typeval $endpoint]} { - set existing_tp [dict get $endpoint type] - if {![tomlish::dict::is_typeval $value]} { - error "tomlish::dict::path::setleaf error Unable to overwrite subpath '$pathsofar' which is of type $existing_tp with sub-dict. Supplied value not {type value val } with sub-dict: $value" - } - switch -- [dict get $endpoint type] { - ARRAY { - #disallow overwriting array - unless given value is an ARRAY? REVIEW - if {[dict get $value type] ne "ARRAY"} { - error "tomlish::dict::path::setleaf error bad path '$path'. Cannot overwrite array with non-array: $value" - } - } - default { - # - } - } - } else { - #leaf is a typeval dict not a plain typeval - only allow overwrite with a typeval dict - if {![tomlish::dict::is_typeval_dict $value 0]} { - error "tomlish::dict::path::setleaf error path '$path'. Cannot overwrite sub-dict (size: [dict size $endpoint]) with non sub-dict: $value" - } - } - ::set $varname $value - dict set vdict $pathsofar $varname - break - } else { - ::set arrdata [dict get $data value] - set idx [tomlish::system::lindex_resolve_basic $arrdata $p] - if {$idx == -1} { - error "tomlish::dict::path::setleaf error bad path '$path'. No existing element at $p" - } - ::set data [lindex $arrdata $p] - ::set $varname $data - dict set vdict $pathsofar $varname - } - } - } - #dict for {path varname} $vdict { - # puts "$path $varname\n" - # puts " '[::set $varname]'\n" - # puts "" - #} - - ::set i 0 - ::set reverse [lreverse $vdict] - foreach {varname path} $reverse { - set newval [::set $varname] - if {$i+2 == [llength $reverse]} { - ::set k [lindex $path end] - ::set k [string range $k 2 end] ;#first key is always @@something - dict set dict_being_edited $k $newval - #puts "--result $dict_being_edited" - break - } - ::set nextvarname [lindex $reverse $i+2] - ::set nextval [::set $nextvarname] - ::set k [lindex $path end] - if {[string match @@* $k]} { - #dict key - #dict set $nextvarname $k $newval - setleaf $nextvarname [list $k] $newval 0 - } else { - #list index - ::set nextarr [dict get $nextval value] - ::lset nextarr $k $newval - dict set $nextvarname value $nextarr - } - ::incr i 2 - } - - return $dict_being_edited - - } - #path must be to a {type ARRAY value } - #REVIEW - how to lappend to deep mixed dict/array structure without rewriting whole datastructure? - proc lappend {dictvariable path args} { - if {[string index $path 0] in [list . "\[" ]} { - set path [tomlish::utils::jq_to_path $path] - } - upvar $dictvariable dict_being_edited - ::set data $dict_being_edited - ::set pathsofar [list] - #::set newlist [list] - ::set v 0 - ::set vdict [dict create] - foreach a $args { - if {![::tomlish::utils::string_is_dict $a]} { - error "tomlish::dict::path::lappend error - lappended arguments must already be in the tomlish form {type value } or be a dict with such forms as leaves" - } - } - foreach p $path { - ::lappend pathsofar $p - if {[string range $p 0 1] eq "@@"} { - ::set k [string range $p 2 end] - if {![dict exists $data $k]} { - error "tomlish::dict::path::lappend error bad path $path. Attempt to access nonexistent element at subpath $pathsofar." - } - ::set varname v[incr v] - - if {[struct::list equal $pathsofar $path]} { - #see if endpoint of the path given is an ARRAY - ::set endpoint [dict get $data $k] - if {![tomlish::dict::is_typeval $endpoint]} { - error "tomlish::dict::path::lappend error bad path $path. Attempt to access table as array at subpath $pathsofar." - } - if {[dict get $endpoint type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Subpath $pathsofar is not an array." - } - ::set arrdata [dict get $endpoint value] - ::lappend arrdata {*}$args - dict set endpoint value $arrdata - ::set newlist $endpoint - ::set $varname $newlist - dict set vdict $pathsofar $varname - break - } - ::set data [dict get $data $k] - ::set $varname $data - dict set vdict $pathsofar $varname - } else { - if {![tomlish::dict::is_typeval $data]} { - error "tomlish::dict::path::lappend error bad path $path. Attempt to access table as array at subpath $pathsofar." - } - if {[dict get $data type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Subpath $pathsofar is not an array." - } - ::set varname v[incr v] - if {[struct::list equal $pathsofar $path]} { - if {[dict get $data type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Parent path is not an array." - } - ::set parentarray [dict get $data value] - ::set idx [tomlish::system::lindex_resolve_basic $parentarray $p] - if {$idx == -1} { - error "tomlish::dict::path::lappend error bad path $path. Index $p does not exist." - } - ::set endpoint [lindex $parentarray $p] - if {[dict get $endpoint type] ne "ARRAY"} { - error "tomlish::dict::path::lappend error bad path $path. Not an array." - } - - ::set arrdata [dict get $endpoint value] - ::lappend arrdata {*}$args - dict set endpoint value $arrdata - ::set newlist $endpoint - #::lset parentarray $p $newlist - #set parentarray $newlist - ::set $varname $newlist - dict set vdict $pathsofar $varname - break - } else { - ::set arrdata [dict get $data value] - set idx [tomlish::system::lindex_resolve_basic $arrdata $p] - if {$idx == -1} { - error "tomlish::dict::path::lappend error bad path $path. Subpath $pathsofar, index $p does not exist." - } - ::set data [lindex $arrdata $p] - ::set $varname $data - dict set vdict $pathsofar $varname - } - } - } - # todo tomlish::log::debug ? - # dict for {path varname} $vdict { - # puts "$path $varname\n" - # puts " [::set $varname]\n" - # puts "" - # } - ::set i 0 - ::set reverse [lreverse $vdict] - foreach {varname path} $reverse { - set newval [::set $varname] - if {$i+2 == [llength $reverse]} { - ::set k [lindex $path end] - ::set k [string range $k 2 end] ;#first key is always @@something - dict set dict_being_edited $k $newval - #puts "--result $dict_being_edited" - break - } - ::set nextvarname [lindex $reverse $i+2] - ::set nextval [::set $nextvarname] - ::set k [lindex $path end] - if {[string match @@* $k]} { - #dict key - set k [string range $k 2 end] - dict set $nextvarname $k $newval - } else { - #list index - ::set nextarr [dict get $nextval value] - ::lset nextarr $k $newval - dict set $nextvarname value $nextarr - } - ::incr i 2 - } - return $dict_being_edited - } -} - -tcl::namespace::eval tomlish::to_dict { - - proc @@path {dictkeys} { - lmap v $dictkeys {string cat @@ $v} - } - -} - -tcl::namespace::eval tomlish::app { - #*** !doctools - #[subsection {Namespace tomlish::app}] - #[para] - #[list_begin definitions] - - tcl::namespace::eval argdoc { - proc test_suites {} { - if {[package provide test::tomlish] eq ""} { - return [list] - } - return [test::tomlish::SUITES] - } - } - - package require punk::args - punk::args::define { - @id -id ::tomlish::app::decode_to_typedjson - @cmd -name tomlish::app::decode_to_typedjson -help\ - "Read toml on stdin until EOF - on error - returns non-zero exit code and writes error to - the errorchannel. - on success - returns zero exit code and writes typed JSON encoding - of the data to the outputchannel. - This decoder is intended to be compatble with toml-test. - toml-test defines the typed JSON format." - @leaders -min 0 -max 0 - @opts - -help -type none -help\ - "Display this usage message" - -inputchannel -default stdin - -inputencoding -default "iso8859-1" -choicerestricted 0 -choices {utf-8 utf-16 iso8859-1} -help\ - "configure encoding on input channel - iso8859-1 is equivalent to binary encoding" - -outputchannel -default stdout - -errorchannel -default stderr - @values -min 0 -max 0 - } - proc decode_to_typedjson {args} { - set argd [punk::args::parse $args withid ::tomlish::app::decode_to_typedjson] - set ch_input [dict get $argd opts -inputchannel] - set ch_input_enc [dict get $argd opts -inputencoding] - set ch_output [dict get $argd opts -outputchannel] - set ch_error [dict get $argd opts -errorchannel] - if {[dict exists $argd received -help]} { - return [punk::args::usage -scheme info ::tomlish::app::decode_to_typedjson] - } - - chan configure $ch_input -encoding $ch_input_enc - #translation? - chan configure $ch_input -translation lf ;# toml-test invalid/control tests we need to see raw CRs to reject them properly - auto translation won't do. - - #Just slurp it all - presumably we are not handling massive amounts of data on stdin. - # - even if the input is large, we probably don't gain much (aside from possible memory savings?) by attempting to process input as it arrives. - if {[catch { - set inputdata [read $ch_input] - if {$ch_input_enc eq "iso8859-1"} { - set toml [tomlish::toml::from_binary $inputdata] - } else { - set toml $inputdata - } - } errM]} { - puts stderr "read-input error: $errM" - #toml-tests expect exit code 1 - #e.g invalid/encoding/utf16-bom - exit 1 ;#read error - } - try { - set j [::tomlish::toml_to_typedjson $toml] - } on error {em} { - puts $ch_error "decoding failed: '$em'" - exit 1 - } - puts -nonewline $ch_output $j - exit 0 - } - - package require punk::args - punk::args::define { - @id -id ::tomlish::app::encode_from_typedjson - @cmd -name tomlish::app::encode_from_typedjson -help\ - "Read typed JSON on input until EOF - return non-zero exitcode if JSON data cannot be converted to - a valid TOML representation. - return zero exitcode and TOML data on output if JSON data can - be converted. - This encoder is intended to be compatible with toml-test. - toml-test defines the typed JSON format." - @leaders -min 0 -max 0 - @opts - -help -type none -help \ - "Display this usage message" - -restrict_barekeys -default 0 -help\ - "If true, keys containing unicode will be quoted. - If false, an extended range of barekeys will be used - in unquoted form." - -inputchannel -default stdin - -inputencoding -default "" -choicerestricted 0 -choices {utf-8 utf-16 iso8859-1} -help\ - "configure encoding on input channel - If not supplied, leave at Tcl default" - -outputchannel -default stdout - -errorchannel -default stderr - @values -min 0 -max 0 - } - proc encode_from_typedjson {args} { - set argd [punk::args::parse $args withid ::tomlish::app::encode_from_typedjson] - set restrict_barekeys [dict get $argd opts -restrict_barekeys] - set ch_input [dict get $argd opts -inputchannel] - set ch_input_enc [dict get $argd opts -inputencoding] - set ch_output [dict get $argd opts -outputchannel] - set ch_error [dict get $argd opts -errorchannel] - if {[dict exists $argd received -help]} { - return [punk::args::usage -scheme info ::tomlish::app::encode_from_typedjson] - } - #review - if {$ch_input_enc ne ""} { - chan configure $ch_input -encoding $ch_input_enc - } - #review - chan configure $ch_input -translation lf - - chan configure $ch_output -translation lf - - if {[catch { - set json [read $ch_input] - }]} { - exit 2 ;#read error - } - try { - #tomlish::typedjson_to_toml - set toml [::tomlish::toml::from_tomlish_from_dict_from_typedjson $json] - } trap {} {e eopts} { - puts $ch_error "encoding failed: '$e'" - puts $ch_error "$::errorInfo" - exit 1 - } - puts -nonewline $ch_output $toml - exit 0 - } - - punk::args::define { - @dynamic - @id -id ::tomlish::app::test - @cmd -name tomlish::app::test -help\ - "Run the internal tests on the tomlish library." - @leaders - @opts -any 1 - -help -type none -help\ - "Display this usage message - or further info if more args." - -suite -default tests -choices {${[::tomlish::app::argdoc::test_suites]}} - @values -min 0 -max -1 - } - proc test {args} { - package require test::tomlish - set argd [punk::args::parse $args withid ::tomlish::app::test] - set opts [dict get $argd opts] - set values [dict get $argd values] - set received [dict get $argd received] - set solos [dict get $argd solos] - set opt_suite [dict get $opts -suite] - if {[dict exists $received -help] && ![dict exists $received -suite]} { - return [punk::args::usage -scheme info ::tomlish::app::test] - } - - test::tomlish::SUITE $opt_suite - #if {[catch {test::tomlish::SUITE $opt_suite} errM]} { - # puts stderr "Unknown test suite '$opt_suite'. Available suites: [test::tomlish::SUITES]" - # exit 1 - #} - set run_opts [dict remove $opts -suite] - set run_opts [dict remove $run_opts {*}$solos] - set result [test::tomlish::RUN {*}$run_opts {*}$solos {*}$values] - return $result - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::app ---}] -} - -proc ::tomlish::appnames {} { - set applist [list] - foreach cmd [info commands ::tomlish::app::*] { - lappend applist [namespace tail $cmd] - } - return $applist -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::lib { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace tomlish::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 tomlish::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval tomlish::system { - #*** !doctools - #[subsection {Namespace tomlish::system}] - #[para] - #[list_begin definitions] - - - - #taken from punk::lib - #todo - change list argument to integer length - proc lindex_resolve_basic {list index} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg list] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -1 for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - #'only' supports 2**32 max index on tcl < 9.0 - ok. - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= [llength $list])} { - #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. - return -1 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {[llength $list]} { - set indices [tomlish::system::range 0 [expr {[llength $list]-1}]] ;# uses lseq if available, has fallback. - #if lseq was available - $indices is an 'arithseries' - theoretically not taking up ram(?) - } else { - set indices [list] - } - set idx [lindex $indices $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -1 - } else { - return $idx - } - } - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - #required for tcl < 8.7 range command (lseq not available) - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {(.*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - if {[info commands ::lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace tomlish::system ---}] -} - -if {[info exists ::argc] && $::argc > 0} { - #puts stderr "argc: $::argc args: $::argv" - set arglist $::argv - # -------------- - #make sure any dependant packages that are sourced don't get any commandline args - set ::argv {} - set ::argc 0 - # -------------- - package require punk::args - punk::args::define { - @dynamic - @id -id tomlish::cmdline - @cmd -name tomlish -help\ - "toml encoder/decoder written in Tcl" - @opts -any 1 - -help -type none -help\ - "Display this usage message or more specific - help if further arguments provided." - -app -choices {${[tomlish::appnames]}} - } - try { - set argd [punk::args::parse $arglist withid tomlish::cmdline] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - puts stderr $msg - exit 1 - } - - - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received -help] && ![dict exists $received -app]} { - #only emit cmdline help if -app not supplied as well - otherwise app function can act on -help for more specific help - #puts stdout "Usage: -app where appname one of:[tomlish::appnames]" - puts stdout [punk::args::usage -scheme info tomlish::cmdline] - exit 0 - } - if {![dict exists $received -app]} { - puts stderr [punk::args::usage -scheme error tomlish::cmdline] - exit 1 - } - - set app [dict get $opts -app] - set appnames [tomlish::appnames] - set app_opts [dict remove $opts -app {*}$solos] - try { - set result [tomlish::app::$app {*}$app_opts {*}$solos {*}$values] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - #The validation error should fully describe the issue - #no need for errortrace - keep the output cleaner - puts stderr $msg - exit 1 - } trap {} {msg erroropts} { - #unexpected error - uncaught throw will produce error trace - #todo - a support msg? Otherwise we may as well just leave off this trap. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - if {"-help" in $solos} { - puts stderr $result - exit 1 - } else { - if {$result ne ""} { - puts stdout $result - exit 0 - } - } -} - -## Ready -package provide tomlish [namespace eval tomlish { - variable pkg tomlish - variable version - set version 1.1.7 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/lib/app-punk/repl.tcl b/src/lib/app-punk/repl.tcl index c9fb0ed5..88af09d8 100644 --- a/src/lib/app-punk/repl.tcl +++ b/src/lib/app-punk/repl.tcl @@ -3,56 +3,18 @@ package provide app-punk 1.0 #By the time we get here, we don't expect other packages to have been loaded - but the lib/module paths have already been scanned to populate 'package names' - -#puts stdout "$::auto_path" -#puts stderr "-----------" -#puts stderr "tcl::tm::list" -#puts stderr "-----------" -#puts stderr "[join [tcl::tm::list] \n]" -#puts stderr "-----------" -#puts stderr "auto_path" -#puts stderr "-----------" -#puts stderr "[join $::auto_path \n]" -#puts stderr "-----------" -#puts stderr "thread? [package provide Thread]" set thread_version [package require Thread] #puts stderr "repl.tcl thread version:$thread_version" -#puts stderr "info loaded:" -#puts stderr [join [info loaded] \n] - -#set tpath [lindex [info loaded] 0 0] -#puts stdout "--$tpath--" -#puts stdout "--[file exists $tpath]--" -#set tid [thread::create -preserved] -#thread::send $tid {puts thread1} -#puts stdout "mythread: [thread::id]" #review #catch {package require tcllibc} #punk & shellrun should be in codethreads - but not required in the parent repl threads package require shellfilter - package require punk::repl -#set v [package provide punk::repl] -#puts stderr "punk::repl version:$v script: [package ifneeded punk::repl $v]" -#puts stderr "package names" -#set packages_present [list] -#foreach p [package names] { -# if {[package provide $p] ne ""} { -# lappend packages_present $p -# } -#} -#puts stderr [join $packages_present \n] -repl::init -safe 0 -#puts stderr "Launching repl::start stdin -title app-punk" -#flush stderr +repl::init -type 0 set replresult [repl::start stdin -title app-punk] -#catch { -# puts "app-punk ifneeded: [package ifneeded app-punk 1.0]" -#} - #review if {[string is integer -strict $replresult]} { #puts stdout "repl.tcl exiting with numeric code $replresult" diff --git a/src/lib/app-punkshell/punkshell.tcl b/src/lib/app-punkshell/punkshell.tcl index 98798f45..807be1f3 100644 --- a/src/lib/app-punkshell/punkshell.tcl +++ b/src/lib/app-punkshell/punkshell.tcl @@ -43,8 +43,7 @@ namespace eval punkshell { set stdout_log "" set stderr_log "" - #set stdout_log [file normalize ~]/punkshell-stdout.txt - #set stderr_log [file normalize ~]/punkshell-stderr.txt + set stdout_log "[pwd]/punkshell_out.log" set stderr_log "[pwd]/punkshell_err.log" @@ -58,7 +57,7 @@ namespace eval punkshell { #ideally we don't want to launch an external process to run the script #variable punkshell_status_log #shellfilter::log::write $punkshell_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" - set exepath [file dirname [file join [info nameofexecutable] __dummy__]] + set exepath [file dirname [file join [info nameofexecutable] __dummy__]] set exedir [file dirname $exepath] set scriptpath [file normalize $scriptname] if {![file exists $scriptpath]} { @@ -87,6 +86,14 @@ info script $prevscript dict with prevglobal {} }] + #---------------------------------------------------- + append repl_lines {package require punk::repl} \n + append repl_lines {repl::init -type 0} \n + append repl_lines [list repl::submit stdin "eval \{ $script \};\n"] \n + append repl_lines {repl::start stdin} \n + set script $repl_lines + #---------------------------------------------------- + dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle punkshell #dict set params -teehandle punksh @@ -177,7 +184,7 @@ dict with prevglobal {} set repl_lines "" append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines {repl::start stdin} \n #test @@ -351,16 +358,24 @@ dict with prevglobal {} } flush stderr flush stdout - catch { - shellfilter::stack::remove stderr $chanstack_stderr_redir - shellfilter::stack::remove stdout $chanstack_stdout_redir - } + #puts stderr "\n - punkshell.tcl 1" + #flush stderr + + + #catch { + # shellfilter::stack::remove stderr $chanstack_stderr_redir + # shellfilter::stack::remove stdout $chanstack_stdout_redir + #} + #puts stderr "punkshell.tcl 2" shellfilter::stack::delete punkshellout shellfilter::stack::delete punkshellerr + #puts stderr "punkshell.tcl 3" set free_info [shellthread::manager::shutdown_free_threads] + #puts stderr "punkshell.tcl 4" foreach tid [thread::names] { thread::release $tid } + #puts stderr "punkshell.tcl 5" if {[dict size $exitinfo] == 0} { puts stderr "No result" @@ -380,7 +395,8 @@ dict with prevglobal {} exit 1 } else { puts -nonewline stdout [dict get $exitinfo result] - exit 0 + #don't call exit on success - allows tk apps to stay alive. + #exit 0 } } diff --git a/src/lib/app-shellspy/shellspy.tcl b/src/lib/app-shellspy/shellspy.tcl index 863aec34..42d2748a 100644 --- a/src/lib/app-shellspy/shellspy.tcl +++ b/src/lib/app-shellspy/shellspy.tcl @@ -77,6 +77,7 @@ package require shellfilter package require punk::ansi package require punk::packagepreference punk::packagepreference::install +package require logger #The whole punk infrastructure is overkill for calling arbitrary scripts #package require punk @@ -98,32 +99,69 @@ namespace eval shellspy { #todo - default to no logging not even to local syslog #load a .toml config which can configure logging as desired - set do_log 0 + set do_log 1 if {$do_log} { set debug_syslog_server 127.0.0.1:514 #set debug_syslog_server 172.16.6.42:51500 set error_syslog_server 127.0.0.1:514 set data_syslog_server 127.0.0.1:514 } else { - set debug_syslog_server "" - set error_syslog_server "" + set debug_syslog_server "" + set error_syslog_server "" set data_syslog_server "" } + #------------------------------------------------------------------------------------------------- + #tcllib logger infrastructure. + #------------------------------------------------------------------------------------------------- + namespace eval ::shellspy::loggerprocs { + #container for procs/aliases to be pointed to by tcllib logger using log::logproc + proc Dolog {lvl txt} { + #logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted + set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'" + puts stderr $msg + } + proc Runlog {lvl script} { + uplevel 1 $script + } + } + if {![catch { + package require logger + }]} { + logger::initNamespace ::shellspy + foreach lvl [logger::levels] { + interp alias {} ::shellspy::loggerprocs::Log_$lvl {} ::shellspy::loggerprocs::Runlog $lvl + log::logproc $lvl ::shellspy::loggerprocs::Log_$lvl + } + logger::setlevel warn + #namespace path ::shellspy::log + } else { + #e.g tcllib not available, safe interp? + #fake out the logger calls + namespace eval ::shellspy::log { + foreach lvl {debug info notice warn error critical alert emergency} { + proc $lvl {args} {} + } + } + } + #------------------------------------------------------------------------------------------------- + + + log::notice {puts stderr "shellspy starting with args '$::argv'"} shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog $debug_syslog_server -file ""] - shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'" - shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]" + log::notice {shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'"} + log::info {shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]"} #------------------------------------------------------------------------- ##don't write to stdout/stderr before you've redirected them to a log using shellfilter functions ## puts to stdout/stderr will comingle with command's output if performed before the channel stacks are configured. - - chan configure stdin -buffering line - chan configure stdout -buffering none - chan configure stderr -buffering none + + chan configure stdin -buffering line + chan configure stdout -buffering none + chan configure stderr -buffering none #set id_ansistrip [shellfilter::stack::add stderr ansistrip -settings {}] #set id_ansistrip [shellfilter::stack::add stdout ansistrip -settings {}] @@ -131,12 +169,12 @@ namespace eval shellspy { #redir on the shellfilter stack with no log or syslog specified acts to suppress output of stdout & stderr. #todo - fix shellfilter code to make this noop more efficient (avoid creating corresponding logging thread and filter?) #JMN - #set redirconfig {-settings {-syslog 127.0.0.1:514 -file ""}} - set redirconfig {} + set redirconfig {-settings {-syslog 127.0.0.1:514 -file ""}} + #set redirconfig {} lassign [shellfilter::redir_output_to_log "SUPPRESS" {*}$redirconfig] chanstack_stdout_redir chanstack_stderr_redir - shellfilter::log::write $shellspy_status_log "shellfilter::redir_output_to_log SUPPRESS DONE [clock_sec]" - - + log::notice {shellfilter::log::write $shellspy_status_log "shellfilter::redir_output_to_log SUPPRESS DONE [clock_sec]"} + + ### #we can now safely write to stderr/stdout and it will not interfere with stderr/stdout from the dispatched command. #This is because when shellfilter::run is called it temporarily adds it's own filter in place of the redirection we just added. @@ -179,24 +217,32 @@ namespace eval shellspy { set stdout_log "" set stderr_log "" - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr ABOUTTO [clock_sec]" + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr ABOUTTO [clock_sec]"} set errdeviceinfo [shellfilter::stack::new shellspyerr -settings [list -tag "shellspyerr" -buffering none -raw 1 -syslog $data_syslog_server -file $stderr_log]] - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr DONE [clock_sec]" + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr DONE [clock_sec]"} - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout ABOUTTO [clock_sec]" + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout ABOUTTO [clock_sec]"} set outdeviceinfo [shellfilter::stack::new shellspyout -settings [list -tag "shellspyout" -buffering none -raw 1 -syslog $data_syslog_server -file $stdout_log]] set commandlog [dict get $outdeviceinfo localchan] #puts $commandlog "HELLO $commandlog" #flush $commandlog - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout DONE [clock_sec]" - + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout DONE [clock_sec]"} - #note that this filter is inline with the data teed off to the shellspyout log. - #To filter the main stdout an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data. - set id_ansistrip [shellfilter::stack::add shellspyout ansistrip -settings {}] - shellfilter::log::write $shellspy_status_log "shellfilter::stack::add shellspyout ansistrip DONE [clock_sec]" + #------------------------------------------------------------------------------ + #note that this filter is inline with the data teed off to the shellspyout log. (ie it is a transform on one of the fifo2 ends) + #To filter the main stdout, an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data. + #TODO - review + set id_ansistrip_out [shellfilter::stack::add shellspyout ansistrip -settings {}] + set id_ansistrip_err [shellfilter::stack::add shellspyerr ansistrip -settings {}] + #This can cause problems e.g unknown channel rc2 (tcl::chan::fifo2) in the repl - review + #something wrong with adding and removing transforms on the fifo2 channels in shellfilter::run? + #only seems to happen when in mode raw where we need to emit to stdout after each keypress, + #and only when we have a queued tcl script that will run before or after the repl. + #e.g punk902z dev shellspy lib:hello.tcL + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::add shellspyout ansistrip DONE [clock_sec]"} + #------------------------------------------------------------------------------ #set id_out [shellfilter::stack::add stdout rebuffer -settings {}] @@ -378,7 +424,7 @@ namespace eval shellspy { package require shellrun package require punk::repl puts stdout "quit to exit" - repl::init -safe 0 + repl::init -type 0 repl::start stdin -defaultresult %r% }]] } @@ -709,7 +755,7 @@ dict with prevglobal {} set repl_lines "" append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines {repl::start stdin} \n #test @@ -728,12 +774,11 @@ dict with prevglobal {} dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle shellspy - - + set params [dict merge $params [get_channel_config $::testconfig]] set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] - + set exitinfo [shellfilter::run $script {*}$params] shellfilter::stack::remove stderr $id_err @@ -822,17 +867,21 @@ dict with prevglobal {} if {$replwhen eq "repl_first"} { + #this concept is probably overly complex and unnecessary. If the user is in a repl then they can just source scripts from the prompt. + # #we need to cooperate with the repl to get the script to run on exit namespace eval ::repl {} set ::repl::post_script $script append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n + append repl_lines [list repl::submit stdin "eval \{ puts \"Queued script will run after 'quit'. 'exit' to abort.\" \};\n"] \n append repl_lines {repl::start stdin} \n set script "$repl_lines" } elseif {$replwhen eq "repl_last"} { + #run a script first e.g could be a tk app - but maintain control via the repl. append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines [list repl::submit stdin "eval \{ $script \};\n"] \n append repl_lines {repl::start stdin} \n @@ -848,27 +897,31 @@ dict with prevglobal {} dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle shellspy + dict set params -debug 1 ;#JMN + #dict set params -syslog "127.0.0.1:514" + dict set params -syslog {} + #dict set params -teehandle punksh - - + + set params [dict merge $params [get_channel_config $::testconfig]] set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] - + #set id_out [shellfilter::stack::add stdout ansiwrap -action sink-locked -settings {-colour {green bold}}] + set exitinfo [shellfilter::run $script {*}$params] + #jmn 2026-05-19 + shellfilter::log::write $shellspy_status_log "do_script exitinfo $exitinfo" + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stdout $id_out - #if {[lindex $exitinfo 0] eq "exitcode"} { - # shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" - #} - #jjj - #shellfilter::log::write $shellspy_status_log "do_script raw exitinfo: $exitinfo" if {[dict exists $exitinfo errorInfo]} { #strip out the irrelevant info from the errorInfo - we don't want info beyond 'invoked from within' as this is just plumbing related to the script sourcing set stacktrace [string map [list \r\n \n] [dict get $exitinfo errorInfo]] - set output "" + set output "" set tracelines [split $stacktrace \n] foreach ln $tracelines { if {[string match "*invoked from within*" $ln]} { @@ -891,7 +944,8 @@ dict with prevglobal {} lappend out $a } return $out - } + } + proc do_shell {shell args} { variable shellspy_status_log shellfilter::log::write $shellspy_status_log "do_shell $shell got '$args' [llength $args]" @@ -899,37 +953,38 @@ dict with prevglobal {} shellfilter::log::write $shellspy_status_log "do_shell $shell xgot '$args'" set params [do_callback_parameters $shell] dict set params -teehandle shellspy - - + + set params [dict merge $params [get_channel_config $::testconfig]] - + set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] #shells that take -c and need all args passed together as a string - + set exitinfo [shellfilter::run [concat $shell -c [shellescape $args]] {*}$params] shellfilter::stack::remove stdout $id_out - + if {[lindex $exitinfo 0] eq "exitcode"} { shellfilter::log::write $shellspy_status_log "do_shell $shell returning $exitinfo" } return $exitinfo } + proc do_wsl {distdefault args} { variable shellspy_status_log shellfilter::log::write $shellspy_status_log "do_wsl $distdefault got '$args' [llength $args]" set args [do_callback wsl {*}$args] ;#use dist? shellfilter::log::write $shellspy_status_log "do_wsl $distdefault xgot '$args'" set params [do_callback_parameters wsl] - + dict set params -debug 0 - + set params [dict merge $params [get_channel_config $::testconfig]] - + set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] - + dict set params -teehandle shellspy ;#shellspyout shellspyerr must exist set exitinfo [shellfilter::run [concat wsl -d $distdefault -e [shellescape $args]] {*}$params] @@ -949,11 +1004,11 @@ dict with prevglobal {} lappend commands [list punkd [list sub punkdict singleopts {any}]] - #'shout' extension (all uppercase) to force use of tclsh as a separate process + #'shout' extension (all uppercase) to force use of tclsh as a separate process #todo - detect various extensions - and use all script-preceding unmatched args as interpreter+options - #e.g perl,php,python etc. + #e.g perl,php,python etc. #For tcl will make it easy to test different interpreter output from tclkitsh, tclsh8.6, tclsh8.7 etc - #for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config + #for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config #(or just attempt launch in case there is shebang line in script) #we may get ambiguity with existing shell match-specs such as -c /c -r. todo - only match those in first slot? lappend commands [list tclscriptprocess [list match [list {.*\.TCL$} {.*\.TM$} {.*\.TK$}] dispatch [list shellspy::do_script_process [info nameofexecutable] %matched%] dispatchtype raw dispatchglobal 1 singleopts {any}]] @@ -997,7 +1052,8 @@ dict with prevglobal {} lappend commands [list runcmdfile [list sub word$i singleopts {any}]] } - lappend commands [list libscript [list match [list {lib:.*$} ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] + #Only match lib:scriptname at start, with a single colon (avoid matching on -tcl lib::blah::func -tcl punk::lib::func etc) + lappend commands [list libscript [list match [list {^lib:[^:].*$} ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] for {set i 0} {$i < 25} {incr i} { lappend commands [list libscript [list sub word$i singleopts {any}]] } @@ -1043,8 +1099,7 @@ dict with prevglobal {} for {set i 0} {$i < 25} {incr i} { lappend commands [list runpwsht [list sub word$i singleopts {any}]] } - - + lappend commands {runcmd {match {^/c$} dispatch shellspy::do_in_cmdshell dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}}} for {set i 0} {$i < 25} {incr i} { lappend commands [list runcmd [list sub word$i singleopts {any}]] @@ -1058,16 +1113,16 @@ dict with prevglobal {} for {set i 0} {$i < 25} {incr i} { lappend commands [list runcmdb [list sub word$i singleopts {any} longopts {any} pairopts {any}]] } - + lappend commands [list wslraw [list match {^wsl$} dispatch [list shellspy::do_wsl AlmaLinux9] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]] for {set i 0} {$i < 25} {incr i} { lappend commands [list wslraw [list sub word$i singleopts {any}]] } #e.g - # punk -tcl info patch + # punk -tcl info patch # punk -tcl eval "package require punk::char;punk::char::charset_page dingbats" - + lappend commands [list tclline [list match [list {^-tcl$}] dispatch [list shellspy::do_tclline tcl] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]] for {set i 0} {$i < 25} {incr i} { lappend commands [list tclline [list sub word$i singleopts {any}]] @@ -1126,38 +1181,50 @@ dict with prevglobal {} set is_call_error 0 set arglist [list] ;#processed args result - contains dispatch info etc. if {[catch { + #----------------------------------------------------------------- + #check_flags does the actual dispatch ie calling of the command above such as do_script, do_shell etc. + #in turn these use shellfilter::run which does the actual running of the command and capturing of output, exitcode etc. set arglist [check_flags {*}$argdefinitions] + #----------------------------------------------------------------- } callError]} { + log::error {shellfilter::log::write $shellspy_status_log "check_flags error: $callError"} + puts -nonewline stderr "|shellspy-stderr> ERROR during command dispatch\n" puts -nonewline stderr "|shellspy-stderr> $callError\n" puts -nonewline stderr "|shellspy-stderr> [set ::errorInfo]\n" - shellfilter::log::write $shellspy_status_log "check_flags error: $callError" set is_call_error 1 } else { - shellfilter::log::write $shellspy_status_log "check_flags result: $arglist" + log::notice {shellfilter::log::write $shellspy_status_log "check_flags result: $arglist"} } - shellfilter::log::write $shellspy_status_log "check_flags dispatch -done- [clock_sec]" - + log::debug {shellfilter::log::write $shellspy_status_log "check_flags dispatch -done- [clock_sec]"} + #puts stdout "sp2. $::argv" - - if {[catch { - set tidyinfo [shellfilter::logtidyup] - } errMsg]} { - - shellfilter::log::open shellspy-error {-tag shellspy-error -syslog $error_syslog_server} - shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]" - after 200 - } - #don't open more logs.. + + #---------------------------------------------------------------------------------- + # Calling shellfilter::logtidyup (with no tags) when using tcl::chan::fifo2 will cause a deadlock. + # review - why exactly? + #---------------------------------------------------------------------------------- + #if {[catch { + # set tidyinfo [shellfilter::logtidyup] + # } errMsg]} { + # + # shellfilter::log::open shellspy-error {-tag shellspy-error -syslog $error_syslog_server} + # shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]" + # after 200 + #} + ##don't open more logs.. #puts stdout ">$tidyinfo" + #---------------------------------------------------------------------------------- #lassign [shellfilter::redir_output_to_log "SUPPRESS"] id_stdout_redir id_stderr_redir - catch { + #catch { + # shellfilter::stack::remove stderr $chanstack_stderr_redir + # shellfilter::stack::remove stdout $chanstack_stdout_redir + #} shellfilter::stack::remove stderr $chanstack_stderr_redir shellfilter::stack::remove stdout $chanstack_stdout_redir - } #shellfilter::log::write $shellspy_status_log "logtidyup -done- $tidyinfo" @@ -1165,7 +1232,7 @@ dict with prevglobal {} set errorlist [dict get $tidyinfo errors] if {[llength $errorlist]} { foreach err $errorlist { - puts -nonewline stderr "|shellspy-final> worker-error-set $err\n" + puts -nonewline stderr "|shellspy-final> worker-error-set $err\n" } } } @@ -1182,8 +1249,8 @@ dict with prevglobal {} } errMsg]} { puts stdout "shellspy logtidyup error $errMsg" flush stdout - shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server} - shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]" + log::error {shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server}} + log::error {shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]"} after 100 } #puts [shellfilter::stack::status shellspyout] @@ -1215,19 +1282,29 @@ dict with prevglobal {} #} - shellfilter::stack::delete shellspyout - shellfilter::stack::delete shellspyerr + if {[catch { + shellfilter::stack::delete shellspyout + shellfilter::stack::delete shellspyerr + } errMsg]} { + #shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server} + #shellfilter::log::write shellspy-final "FINAL stack delete error $errMsg\n [set ::errorInfo]" + puts stderr "FINAL stack delete error $errMsg\n [set ::errorInfo]" + after 100 + } set free_info [shellthread::manager::shutdown_free_threads] #puts stdout $free_info #flush stdout - if {[package provide zzzload] ne ""} { - #if zzzload used and not shutdown - we can get deadlock - #puts "zzzload::loader_tid [set ::zzzload::loader_tid]" - #zzzload::shutdown - } + + # if {[package provide zzzload] ne ""} { + # #if zzzload used and not shutdown - we can get deadlock + # #puts "zzzload::loader_tid [set ::zzzload::loader_tid]" + # #zzzload::shutdown + # } + #puts stdout "threads: [thread::names]" #flush stdout #puts stdout "calling release on remaining threads" + #flush stdout foreach tid [thread::names] { thread::release $tid } @@ -1254,7 +1331,7 @@ dict with prevglobal {} flush stderr exit 1 } - } + } foreach tclscript_flavour [list tclline tclkit punkline punkshellline tkline tkshellline libscript help] { if {[dict exists $arglist dispatch $tclscript_flavour result error]} { @@ -1278,12 +1355,12 @@ dict with prevglobal {} foreach tclscript_flavour [list tclline tclkit punkline punkshellline tkline tkshellline libscript help] { if {[dict exists $arglist dispatch $tclscript_flavour result result]} { puts -nonewline stdout [dict get $arglist dispatch $tclscript_flavour result result] + flush stdout exit 0 } } - #if we call exit - package require Tk script files will exit prematurely - #review + #if we call exit - package require Tk script files will exit prematurely #exit 0 } diff --git a/src/lib/app_shell/app_shell.tcl b/src/lib/app_shell/app_shell.tcl index 03dd1975..2b0b6e32 100644 --- a/src/lib/app_shell/app_shell.tcl +++ b/src/lib/app_shell/app_shell.tcl @@ -3,7 +3,7 @@ package provide app_shell 1.0 package require Thread package require shellfilter package require punk::repl -repl::init -safe 0 +repl::init -type 0 set replresult [repl::start stdin -title app_shell] if {[string is integer -strict $replresult]} { exit $replresult diff --git a/src/lib/app_shellrun/app_shellrun.tcl b/src/lib/app_shellrun/app_shellrun.tcl index ced47a22..99280ec2 100644 --- a/src/lib/app_shellrun/app_shellrun.tcl +++ b/src/lib/app_shellrun/app_shellrun.tcl @@ -178,7 +178,7 @@ dict with prevglobal {} set repl_lines "" append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines {repl::start stdin} \n #test diff --git a/src/modules/overtype-999999.0a1.0.tm b/src/modules/overtype-999999.0a1.0.tm index b89d6c40..a298da2b 100644 --- a/src/modules/overtype-999999.0a1.0.tm +++ b/src/modules/overtype-999999.0a1.0.tm @@ -401,13 +401,20 @@ tcl::namespace::eval overtype { set opt_console [tcl::dict::get $opts -console] + #-------------------------------------------------------------------------- + #TODO + #REVIEW - punk::console package may not be loaded set cursor_style_overtype {3 underline-blink} set cursor_style_insert {5 beam-blink} if {$opt_insert_mode} { - punk::console::cursor_style -console $opt_console $cursor_style_insert + set initial_cursor_style $cursor_style_insert } else { + set initial_cursor_style $cursor_style_overtype + } + catch { punk::console::cursor_style -console $opt_console $cursor_style_overtype } + #-------------------------------------------------------------------------- # ---------------------------- # -experimental dev flag to set flags etc @@ -695,22 +702,23 @@ tcl::namespace::eval overtype { #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\ - -overtext_type $overtext_type\ + 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 {*}{ + } -overtext_type $overtext_type {*}{ + } ] set rinfo [renderline {*}$renderopts $undertext $overtext] @@ -940,14 +948,15 @@ tcl::namespace::eval overtype { puts stdout ">>>renderspace<<<[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 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..? @@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype { } #JMN - if {[tcl::dict::get $vtstate insert_mode]} { - puts "setting cursor to insert style" - punk::console::cursor_style -console $opt_console $cursor_style_insert - } else { - punk::console::cursor_style -console $opt_console $cursor_style_overtype - } + #REVIEW - we don't want to emit cursor_style ANSI unless it changes. + #if {[tcl::dict::get $vtstate insert_mode]} { + # puts "setting cursor to insert style" + # punk::console::cursor_style -console $opt_console $cursor_style_insert + #} else { + # punk::console::cursor_style -console $opt_console $cursor_style_overtype + #} #puts "renderedrow_max: $renderedrow_max" #check for null lines below renderedrow_max (and at tail) and trim. @@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype { #broken: #todo - renderline -overflow is invalid. # we need renderline to support -expand_left ?? - 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 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} { @@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype { -crm_mode -default 0 -type boolean -autowrap_mode -default 1 -type boolean -reverse_mode -default 0 -type boolean - -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\ + -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\ + -choicelabels\ { 1 "return a dict with raw fields" 2 "return a dict using ansistring VIEW" diff --git a/src/modules/poshinfo-999999.0a1.0.tm b/src/modules/poshinfo-999999.0a1.0.tm index 56fa4bf1..23fd29dd 100644 --- a/src/modules/poshinfo-999999.0a1.0.tm +++ b/src/modules/poshinfo-999999.0a1.0.tm @@ -207,7 +207,7 @@ tcl::namespace::eval poshinfo { -help "e.g omp" -as -default "table" -choices {list showlist dict showdict table tableobject plaintext}\ -help "return type of result" - @values -min 0 + @values -min 0 globs -multiple 1 -default * -help "" } proc themes {args} { diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index 8994be71..91ead8b0 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -26,13 +26,11 @@ namespace eval punk { #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 + list apply {{ap tmlist} { + set ::auto_path $ap tcl::tm::list - set ::tcl::tm::paths \$tmlist - }} {$::auto_path} {[tcl::tm::list]} - " + set ::tcl::tm::paths $tmlist + }} $::auto_path [tcl::tm::list] } @@ -283,14 +281,6 @@ namespace eval punk { #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;" - } # ------------------------ #Note that unlike an ordinary Tcl array - the linked ::env behaves differently. @@ -307,6 +297,15 @@ namespace eval punk { } # ------------------------ + 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;" + } + #change2 if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { set lookfor [list $name] @@ -5492,9 +5491,10 @@ namespace eval punk { #ctrl-c propagation also needs to be considered set teehandle punksh - uplevel 1 [list ::catch \ - [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ - ::tcl::UnknownResult ::tcl::UnknownOptions] + uplevel 1 [list ::catch {*}{ + } [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{ + } ::tcl::UnknownResult ::tcl::UnknownOptions + ] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { dict set ::tcl::UnknownOptions -code error diff --git a/src/modules/punk/aliascore-999999.0a1.0.tm b/src/modules/punk/aliascore-999999.0a1.0.tm index 66d6a13b..e7e7e2df 100644 --- a/src/modules/punk/aliascore-999999.0a1.0.tm +++ b/src/modules/punk/aliascore-999999.0a1.0.tm @@ -109,6 +109,7 @@ tcl::namespace::eval punk::aliascore { set aliases [tcl::dict::create {*}{ val ::punk::pipe::val tstr ::punk::args::lib::tstr + FOR ::punk::lib::FOR list_as_lines ::punk::lib::list_as_lines lines_as_list ::punk::lib::lines_as_list linelist ::punk::lib::linelist diff --git a/src/modules/punk/ansi-999999.0a1.0.tm b/src/modules/punk/ansi-999999.0a1.0.tm index 331f76f6..693d863c 100644 --- a/src/modules/punk/ansi-999999.0a1.0.tm +++ b/src/modules/punk/ansi-999999.0a1.0.tm @@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi { @cmd -name punk::ansi::sauce -summary\ "SAUCE info from file"\ -help\ - "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data." + "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data. + + Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format + that was commonly used in old ANSI art files to store information about the file, such as + title, author, group, date, and comments. + It may also have fields to specify the number of columns and rows in the ANSI art, + as well as flags for specific display attributes. + + It may also be used on other types of files such as bitmap, vector, audio, binarytext, + xbin, archive and executable files. + + It is a 128-byte block of data that is typically appended to the end of a file. + https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm" -encoding -default iso8859-1 -type string -help\ "The default iso8859-1 is equivalent to binary ans should work in the usual case. diff --git a/src/modules/punk/ansi/sauce-999999.0a1.0.tm b/src/modules/punk/ansi/sauce-999999.0a1.0.tm index b8519fee..c6cd9d79 100644 --- a/src/modules/punk/ansi/sauce-999999.0a1.0.tm +++ b/src/modules/punk/ansi/sauce-999999.0a1.0.tm @@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce { variable PUNKARGS variable PUNKARGS_aliases + #https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm lappend PUNKARGS [list { @id -id "(package)punk::ansi::sauce" @package -name "punk::ansi::sauce" -help\ diff --git a/src/modules/punk/args/moduledoc/iocp-999999.0a1.0.tm b/src/modules/punk/args/moduledoc/iocp-999999.0a1.0.tm new file mode 100644 index 00000000..1fd7e319 --- /dev/null +++ b/src/modules/punk/args/moduledoc/iocp-999999.0a1.0.tm @@ -0,0 +1,144 @@ +# -*- 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: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.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) 2026 +# +# @@ Meta Begin +# Application punk::args::moduledoc::iocp 999999.0a1.0 +# Meta platform tcl +# Meta license BSD +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +package require Tcl 8.6- + + + +tcl::namespace::eval punk::args::moduledoc::iocp { + variable PUNKARGS + + namespace eval argdoc { + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + variable PUNKARGS + variable PUNKARGS_aliases + + lappend PUNKARGS_aliases {::iocp::inet::socket ::socket} + + lappend PUNKARGS [list { + @id -id "(package)punk::args::moduledoc::iocp" + @package -name "punk::args::moduledoc::iocp" -help\ + "punk::args documentation for magicsplat iocp package" + }] + + } + +} + + + +# == === === === === === === === === === === === === === === +# Sample 'about' function with punk::args documentation +# == === === === === === === === === === === === === === === +tcl::namespace::eval punk::args::moduledoc::iocp { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + + namespace eval argdoc { + #namespace for custom argument documentation + proc package_name {} { + return punk::args::moduledoc::iocp + } + proc about_topics {} { + #info commands results are returned in an arbitrary order (like array keys) + set topic_funs [info commands [namespace current]::get_topic_*] + set about_topics [list] + foreach f $topic_funs { + set tail [namespace tail $f] + lappend about_topics [string range $tail [string length get_topic_] end] + } + #Adjust this function or 'default_topics' if a different order is required + return [lsort $about_topics] + } + proc default_topics {} {return [list Description *]} + + # ------------------------------------------------------------- + # get_topic_ functions add more to auto-include in about topics + # ------------------------------------------------------------- + proc get_topic_Description {} { + punk::args::lib::tstr [string trim { + package punk::args::moduledoc::iocp + } \n] + } + proc get_topic_License {} { + return "BSD" + } + proc get_topic_Version {} { + return "$::punk::args::moduledoc::iocp::version" + } + proc get_topic_Contributors {} { + set authors {} + set contributors "" + foreach a $authors { + append contributors $a \n + } + if {[string index $contributors end] eq "\n"} { + set contributors [string range $contributors 0 end-1] + } + return $contributors + } + # ------------------------------------------------------------- + } + + # we re-use the argument definition from punk::args::standard_about and override some items + set overrides [dict create] + dict set overrides @id -id "::punk::args::moduledoc::iocp::about" + dict set overrides @cmd -name "punk::args::moduledoc::iocp::about" + dict set overrides @cmd -help [string trim [punk::args::lib::tstr { + About punk::args::moduledoc::iocp + }] \n] + dict set overrides topic -choices [list {*}[punk::args::moduledoc::iocp::argdoc::about_topics] *] + dict set overrides topic -choicerestricted 1 + dict set overrides topic -default [punk::args::moduledoc::iocp::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict + set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *] + lappend PUNKARGS [list $newdef] + proc about {args} { + package require punk::args + #standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on + set argd [punk::args::parse $args withid ::punk::args::moduledoc::iocp::about] + lassign [dict values $argd] _leaders opts values _received + punk::args::package::standard_about -package_about_namespace ::punk::args::moduledoc::iocp::argdoc {*}$opts {*}[dict get $values topic] + } +} +# end of sample 'about' function +# == === === === === === === === === === === === === === === + + +# ----------------------------------------------------------------------------- +# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked +# ----------------------------------------------------------------------------- +# variable PUNKARGS +# variable PUNKARGS_aliases +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + lappend ::punk::args::register::NAMESPACES ::punk::args::moduledoc::iocp ::punk::args::moduledoc::iocp::argdoc +} +# ----------------------------------------------------------------------------- + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::args::moduledoc::iocp [tcl::namespace::eval punk::args::moduledoc::iocp { + variable pkg punk::args::moduledoc::iocp + variable version + set version 999999.0a1.0 +}] +return + diff --git a/src/modules/punk/args/moduledoc/iocp-buildversion.txt b/src/modules/punk/args/moduledoc/iocp-buildversion.txt new file mode 100644 index 00000000..f9b49a39 --- /dev/null +++ b/src/modules/punk/args/moduledoc/iocp-buildversion.txt @@ -0,0 +1,3 @@ +2.0.2 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/punk/args/moduledoc/tkcore-999999.0a1.0.tm b/src/modules/punk/args/moduledoc/tkcore-999999.0a1.0.tm index 7532019d..a2188e2d 100644 --- a/src/modules/punk/args/moduledoc/tkcore-999999.0a1.0.tm +++ b/src/modules/punk/args/moduledoc/tkcore-999999.0a1.0.tm @@ -368,7 +368,6 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- lappend PUNKARGS_aliases {::button ::tk::button} punk::args::define { - @dynamic @id -id ::tk::button @cmd -name "Tk Builtin: tk::button"\ -summary\ @@ -392,8 +391,7 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { @opts -type string -parsekey "" -group "STANDARD OPTIONS" -grouphelp\ "" - }\ - { + } { ${[punk::args::resolved_def -types opts (default)::punk::args::moduledoc::tkcore::tk_standardoptions\ -activebackground\ -activeforeground\ @@ -419,8 +417,8 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { -textvariable\ -underline\ -wraplength\ - ]}}\ - { + ]} + } { @opts -type string -parsekey "" -group "WIDGET-SPECIFIC OPTIONS" -grouphelp\ "" -command -type script -help\ @@ -508,7 +506,8 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { dict set CLASS_BUTTON_CHOICEINFO $sub {{doctype native} {doctype punkargs}} #override manual synopsis entry #puts stderr "override manual synopsis entry with [punk::ns::synopsis "::package $sub"]" - dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::ns::synopsis "(widgetcommand)Class_Button $sub"] + #dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::ns::synopsis "(widgetcommand)Class_Button $sub"] + dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::args::synopsis "(widgetcommand)Class_Button $sub"] } } diff --git a/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm b/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm index 13b77697..992c2923 100644 --- a/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm +++ b/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm @@ -628,7 +628,7 @@ namespace eval punk::cap::handlers::templates { #we will ignore any .tm files that don't have versions that tcl understands - but warn #this reduces the cases we have to test later set fname [file tail $tf] - lassign [split [punk::mix::cli::lib::split_modulename_version $fname]] mname ver + lassign [split [punk::mix::util::split_modulename_version $fname]] mname ver if {[catch {punk::mix::cli::lib::validate_modulename $mname} errM]} { puts stderr "Invalid module name/version $tf - please rename with standard Tcl .tm module name and version (or leave out version)" if {[string match *-* $mname]} { diff --git a/src/modules/punk/cesu-999999.0a1.0.tm b/src/modules/punk/cesu-999999.0a1.0.tm index fa6e9bb1..b4020860 100644 --- a/src/modules/punk/cesu-999999.0a1.0.tm +++ b/src/modules/punk/cesu-999999.0a1.0.tm @@ -128,11 +128,12 @@ tcl::namespace::eval punk::cesu { #binary scan $4 c 4 incr 1 ;#// Effectively adds 0x10000 to the codepoint ? - return [binary format ccca \ - [expr {0xF0 | (($1 & 0xC) >> 2)}] \ - [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] \ - [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] \ - $4] + return [binary format ccca {*}{ + } [expr {0xF0 | (($1 & 0xC) >> 2)}] {*}{ + } [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] {*}{ + } [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] {*}{ + } $4 + ] } # @@ -147,14 +148,15 @@ tcl::namespace::eval punk::cesu { #binary scan $4 c 4 incr 1 - return [binary format ccca \ - [expr {0xF0 | (($1 & 0xC) >> 2)}] \ - [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] \ - [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] \ - $4] + return [binary format ccca {*}{ + } [expr {0xF0 | (($1 & 0xC) >> 2)}] {*}{ + } [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] {*}{ + } [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] {*}{ + } $4 + ] } else { - puts "Invalid sequence: $char" + puts "Invalid sequence: $char" return $char } } diff --git a/src/modules/punk/char-999999.0a1.0.tm b/src/modules/punk/char-999999.0a1.0.tm index 9a5cd7f0..d4091473 100644 --- a/src/modules/punk/char-999999.0a1.0.tm +++ b/src/modules/punk/char-999999.0a1.0.tm @@ -2468,10 +2468,10 @@ tcl::namespace::eval punk::char { #short-circuit basic cases #support tcl pre 2023-11 - see regexp bug below - #if {![regexp {[\uFF-\U10FFFF]} $text]} { + #if {![regexp {[\u100-\U10FFFF]} $text]} { # return [tcl::string::length $text] #} - if {![regexp "\[\uFF-\U10FFFF\]" $text]} { + if {![regexp "\[\u100-\U10FFFF\]" $text]} { return [tcl::string::length $text] #punk::char::wcswidth has to split and examine dec value of each code #By stripping controls + 7F (leaving tab) we've already eliminated the non-printable ascii - REVIEW diff --git a/src/modules/punk/config-0.1.tm b/src/modules/punk/config-0.1.tm index 43edd6a3..61885dcc 100644 --- a/src/modules/punk/config-0.1.tm +++ b/src/modules/punk/config-0.1.tm @@ -563,7 +563,7 @@ tcl::namespace::eval punk::config { lappend PUNKARGS [list { @dynamic @id -id ::punk::config::show - @cmd -name punk::config::get -help\ + @cmd -name punk::config::show -help\ "Display configuration values from a config. Accepts globs eg XDG*" @leaders -min 1 -max 1 diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index 9f72652d..9e0a3cb9 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/src/modules/punk/console-999999.0a1.0.tm @@ -44,7 +44,14 @@ #[list_begin itemized] package require Tcl 8.6- + +#---------------------------------------------------- +#Although we need to be in an environment with Thread available to use punk::console, +# we don't want to require Thread as a hard dependency in the interp we're running in. +# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp. +#TODO. package require Thread ;#tsv required to sync is_raw +#---------------------------------------------------- package require punk::ansi package require punk::args #*** !doctools @@ -282,7 +289,7 @@ namespace eval punk::console { ignore the regex match 'ok' response and keep going." -return -type string -default payload -choices {payload dict} -choicelabels { - dict\ + dict "dict with keys prefix,response,payload,all" } -help\ "Return format" @@ -290,12 +297,12 @@ namespace eval punk::console { -console -default {stdin stdout} -type list -help\ "console/terminal (currently list of in/out channels) (todo - object?)" -passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels { - none\ + none { ANSI sent without any passthrough wrapping. A terminal multiplexer such as tmux,screen,zellij may not pass the request through to the underlying terminal(s) This is the recommended/normal value for the option.} - tmux\ + tmux { Wrap ANSI sequence with tmux passthrough sequence. \x1bPtmux\;\x1b\\ Note that a tmux session could be connected to multiple @@ -304,7 +311,7 @@ namespace eval punk::console { Passthrough should generally be avoided except for debug/test purposes. } - auto\ + auto { Use existence of ::env(TMUX) to detect tmux and send tmux passthrough sequence. Not recommended except for debug/test purposes. @@ -2779,6 +2786,7 @@ namespace eval punk::console { This allows querying the current style and then re-setting it after temporarily changing it." }] } + proc cursor_style {args} { set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style] lassign [dict values $argd] leaders opts values diff --git a/src/modules/punk/du-999999.0a1.0.tm b/src/modules/punk/du-999999.0a1.0.tm index 1db0198f..6f69c4db 100644 --- a/src/modules/punk/du-999999.0a1.0.tm +++ b/src/modules/punk/du-999999.0a1.0.tm @@ -1947,15 +1947,15 @@ namespace eval punk::du { #e.g we can populate compsizes for files (compressed size) proc du_dirlisting_zipfs {folderpath args} { puts stderr "zipfs: $folderpath" - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] @@ -2114,15 +2114,15 @@ namespace eval punk::du { } proc du_dirlisting_tclvfs {folderpath args} { - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] diff --git a/src/modules/punk/lib-999999.0a1.0.tm b/src/modules/punk/lib-999999.0a1.0.tm index a8dcd9aa..e0a84a89 100644 --- a/src/modules/punk/lib-999999.0a1.0.tm +++ b/src/modules/punk/lib-999999.0a1.0.tm @@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check { if {"windows" ne $::tcl_platform(platform)} { set bug 0 } else { - set tmpdir [file tempdir] ;#tcl 9+ - set testfile [file join $tmpdir "bugtest"] + if {![catch {file tempdir} tmpdir]} { + #tcl 9+ has 'file tempdir' + set testfile [file join $tmpdir "bugtest"] + } else { + #fallback for older tcl versions - use env TEMP/TMP or current directory + set tmpdir "" + foreach e {TEMP TMP} { + if {[info exists ::env($e)] && [file isdirectory ::env($e)]} { + set tmpdir ::env($e) + break + } + } + if {$tmpdir eq ""} { + #no env vars - fallback to current directory + set tmpdir [pwd] + } + set testfile [file join $tmpdir "bugtest"] + } + set fd [open $testfile w] puts $fd test close $fd set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*] + if {[file exists $testfile]} { + file delete $testfile + } foreach r $globresult { if {$r ne "bugtest"} { set bug 1 @@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat { #*** !doctools #[call [fun lpop] [arg listvar] [opt {index}]] #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l + #upvar $lvar l + upvar 1 $lvar l if {![llength $args]} { set args [list end] } @@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat { #set newlist [lremove $newlist $tailidx] #set newlist [lreplace $newlist $tailidx $tailidx] set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx] - #don't use ledit here! + #we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin. } else { set sublist [lindex $newlist {*}$sublist_path] #set sublist [lremove $sublist $tailidx] @@ -1990,6 +2011,7 @@ namespace eval punk::lib { #for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution. #--------------------------------- proc tclscript_info {script {nscontext ""}} { + package require parser #if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing. if {[punk::ansi::ta::detect $script]} { #we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts. @@ -3393,16 +3415,51 @@ namespace eval punk::lib { return [list $stdout $stderr 0] } - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " + namespace eval argdoc { + variable PUNKARGS + upvar ::punk::lib::has_punk_ansi has_punk_ansi + + #if {!$has_punk_ansi} { + # set RST "" + # set sep " = " + # set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + #} else { + # set RST [punk::ansi::a] + # #set sep " [a+ Web-seagreen]=[a] " + # set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support + # #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + # #NOTE that \u2260 not suitable for non utf-8 terminals. + # set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + #} + #todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8. + # (safe detection methods for utf-8 support?) + #if colour is disabled we want to refresh this. + #therefore we use @dynamic + proc get_sep {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep " = " + } else { + #set sep " [a+ Web-seagreen]=[a] " + set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support + } + return $sep + } + proc get_sep_mismatch {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + } else { + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #NOTE that \u2260 not suitable for non utf-8 terminals. + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] " + } + return $sep_mismatch } - set argspec [string map [list %sep% $sep] { + set DYN_SEP {${[get_sep]}} + set DYN_SEP_MISMATCH {${[get_sep_mismatch]}} + lappend PUNKARGS [list { + @dynamic @id -id ::punk::lib::pdict @cmd -name pdict -help\ "Print dict keys,values to channel @@ -3412,7 +3469,7 @@ namespace eval punk::lib { @opts -any 1 #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" + -separator -default "${$DYN_SEP}" -roottype -default "dict" -substructure -default {} -channel -default stdout -help\ @@ -3423,33 +3480,36 @@ namespace eval punk::lib { dictvar -type string -help "name of variable. Can be a dict, list or array" patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - (todo - change to indexset syntax @1..3 @1..end-1 etc) - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. + Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) + The system uses similar patterns to the punk pipeline pattern-matching system. + The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. + Segments are classified into list,dict and string operations. + Leading % indicates a string operation - e.g %# gives string length + A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 + (todo - change to indexset syntax @1..3 @1..end-1 etc) + A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' + The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. + e.g1 pdict env */%# + the pattern starts with default type dict, so * retrieves all keys & values, + the next hierarchy switches to a string operation to get the length of each value. + e.g2 pdict env W* S* + Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns + e.g3 pdict punk_testd */* + This displays 2 levels of the dict hierarchy. + Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) + - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. + e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 + Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent + The second level segment in each pattern switches to a dict operation to retrieve the value by key. + When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. } }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] + } + proc pdict {args} { + package require punk::args + variable has_punk_ansi + set argd [punk::args::parse $args withid ::punk::lib::pdict] set opts [dict get $argd opts] set dvar [dict get $argd values dictvar] set patterns [dict get $argd values patterns] @@ -3467,32 +3527,11 @@ namespace eval punk::lib { showdict {*}$opts $dvalue {*}$patterns } - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock + namespace eval argdoc { + variable PUNKARGS - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { + lappend PUNKARGS [list { + @dynamic @id -id ::punk::lib::showdict @cmd -name punk::lib::showdict -help "display dictionary keys and values" #todo - table tableobject @@ -3502,10 +3541,8 @@ namespace eval punk::lib { "Trim whitespace off rhs of each line. This can help prevent a single long line that wraps in terminal from making every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" + -separator -default "${$DYN_SEP}" -help "Separator column between keys and values" + -separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch" -roottype -default "dict" -help\ "list,dict,string" -ansibase_keys -default "" -help\ @@ -3524,7 +3561,23 @@ namespace eval punk::lib { "dict or list value" patterns -default "*" -type string -multiple 1 -help\ "key or key glob pattern" - }]] + }] + } + #TODO - much. + #showdict needs to be able to show different branches which share a root path + #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) + # - specify ansi colour per pattern so different branches can be highlighted? + # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc + # - The current version is incomplete but passably usable. + # - Copy proc and attempt rework so we can get back to this as a baseline for functionality + proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) + package require punk::pipe + #package require punk ;#we need pipeline pattern matching features + package require textblock + + set RST [punk::ansi::a] + + set argd [punk::args::parse $args withid ::punk::lib::showdict] #for punk::lib - we want to reduce pkg dependencies. # - so we won't even use the tcllib debug pkg here @@ -4879,6 +4932,23 @@ namespace eval punk::lib { the range will come out the same, so the result needs to be treated as a 1-based set of indices when performing further operations. " + -return -type string -default indices -choices {indices pairs} -choicecolumns 1 -choicelabels { + indices + " return a list of all indices in the order specified by the indexset, + with duplicates if specified by the indexset. + So for example + indexset_resolve 6 3..0,2,4,end + would return 3 2 1 0 2 4 5" + pairs + " return a list of index pairs representing the start and end of each range, + which may be increasing or decreasing, or just a single index + (where start and end are the same). + So for example + indexset_resolve -return pairs 6 3..0,2,4,end + would return {3 0} {2 2} {4 5} + indexset_resolve -return pairs 7 3..0,2,4,end + would return {3 0} {2 2} {4 4} {6 6}" + } @values -min 2 -max 3 numitems -type integer indexset -type indexset -help "comma delimited specification for indices to return" @@ -4892,32 +4962,66 @@ namespace eval punk::lib { # for the unhappy path - the punk::args::parse is fine to generate the usage/error information. # -------------------------------------------------- if {[llength $args] < 2} { + #too few args - use parser to generate error message punk::args::resolve $args withid ::punk::lib::indexset_resolve } - set indexset [lindex $args end] set numitems [lindex $args end-1] + set indexset [lindex $args end] if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { #use parser on unhappy path only set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] } #assert we have 2 or more args + set optlist [lrange $args 0 end-2] + if {[llength $optlist] % 2 != 0} { + #options should come in pairs + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype "indices" ;#default set base 0 ;#default - if {[llength $args] > 2} { - #if more than just numitems and indexset - we expect only -base ie 4 args in total - if {[llength $args] != 4} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set optname [lindex $args 0] - set optval [lindex $args 1] - set fulloptname [tcl::prefix::match -error "" -base $optname] - if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + dict for {opt val} $optlist { + set fulloptname [tcl::prefix::match -error "" {-base -return} $opt] + switch -exact -- $fulloptname { + -return { + set fullval [tcl::prefix::match -error "" {indices pairs} $val] + if {$fullval ni {indices pairs}} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype $fullval + } + -base { + if {![string is integer -strict $val]} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set base $val + } + default { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } } - set base $optval } + + #set base 0 ;#default + #if {[llength $args] > 2} { + # #if more than just numitems and indexset - we expect only -base ie 4 args in total + # if {[llength $args] != 4} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set optname [lindex $args 0] + # set optval [lindex $args 1] + # set fulloptname [tcl::prefix::match -error "" -base $optname] + # if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set base $optval + #} # -------------------------------------------------- @@ -5104,8 +5208,84 @@ namespace eval punk::lib { } } } + if {$returntype eq "pairs"} { + return [indices_to_pairs $index_list] + } return $index_list } + proc indices_to_pairs {indices} { + #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing. + #if the direction of the run changes - we end the previous run and start a new one + if {[llength $indices] == 0} { + return [list] + } + set pairs [list] + set start [lindex $indices 0] + set prev $start + set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing + for {set i 1} {$i < [llength $indices]} {incr i} { + set idx [lindex $indices $i] + if {![string is integer -strict $idx]} { + error "non-integer index '$idx' in indices list" + } + if {$idx == $prev + 1} { + #increase since prev + if {$direction == 0} { + set direction 1 + } elseif {$direction == -1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } else { + #still increasing + } + set prev $idx + } elseif {$idx == $prev - 1} { + #decrease since prev + if {$direction == 0} { + set direction -1 + } elseif {$direction == 1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } + set prev $idx + } else { + #run ended - add pair to list + lappend pairs [list $start $prev] + set start $idx + set prev $idx + set direction 0 + } + } + # add final run + lappend pairs [list $start $prev] + return $pairs + } + + #proc indices_to_pairs {indices} { + # #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices + # set pairs [list] + # set start [lindex $indices 0] + # set prev $start + # for {set i 1} {$i < [llength $indices]} {} { + # set idx [lindex $indices $i] + # if {$idx == $prev + 1} { + # #still in a run + # set prev $idx + # } else { + # #run ended - add pair to list + # lappend pairs [list $start $prev] + # set start $idx + # set prev $idx + # } + # } + # # add final run + # lappend pairs [list $start $prev] + # return $pairs + #} # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side #This doesn't need the list itself - just the length suffices. punk::args::define { @@ -7779,6 +7959,77 @@ namespace eval punk::lib { } } + #sugar + #exclusive end - more intuitive for some cases and more consistent with other languages + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::lib::FOR + @cmd -name punk::lib::FOR\ + -summary\ + "BASIC style integer 'for loop'"\ + -help\ + "Sugar syntax for a common looping pattern. + The loop variable takes on values from start to end (exclusive) in increments of step. + If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end. + This is a common looping pattern that isn't directly supported by Tcl's built in control structures, + and this syntax is more concise for convenient interactive usage. For example: + + FOR i 0 10 {puts $i} + + will print the numbers 0 to 9. + + This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops, + so may not be suitable for performance critical inner loops. + + See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop + " + @values -min 3 -max 4 + varname -type string -help "loop variable name" + start -type integer -help "initial value for loop variable" + end -type integer -help "end value for loop variable (exclusive)" + step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)" + script -type script -help "script to execute for each loop iteration" + }] + } + proc FOR { var args } { + switch -- [llength $args] { + 3 { + # FOR x start end {} + lassign $args start end script + set step [ expr {$start > $end ? - 1 : 1} ] + } + 4 { + # FOR x start end step {} + lassign $args start end step script + } + default { + error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script" + } + } + if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} { + error "FOR: start,end and step values must be integers" + } + upvar $var loopVar + set loopVar [expr {$start - $step}] + #to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition + if {$start < $end} { + if {$step <= 0} { + error "FOR: step value must be positive when start < end" + } + while {[incr loopVar $step] < $end} { + uplevel $script + } + } else { + if {$step >= 0} { + error "FOR: step value must be negative when start > end" + } + while {[incr loopVar $step] > $end} { + uplevel $script + } + } + } + #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 diff --git a/src/modules/punk/mix/cli-999999.0a1.0.tm b/src/modules/punk/mix/cli-999999.0a1.0.tm index be8506db..e6ef2a90 100644 --- a/src/modules/punk/mix/cli-999999.0a1.0.tm +++ b/src/modules/punk/mix/cli-999999.0a1.0.tm @@ -333,11 +333,11 @@ namespace eval punk::mix::cli { set defaults [list {*}{ -errorprefix projectname }] - if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"} + if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"} set known_opts [dict keys $defaults] foreach k [dict keys $args] { if {$k ni $known_opts} { - error "validate_modulename error: unknown option $k. known options: $known_opts" + error "validate_projectname error: unknown option $k. known options: $known_opts" } } set opts [dict merge $defaults $args] @@ -381,34 +381,6 @@ namespace eval punk::mix::cli { return $name } - #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path - #ignore trailing .tm .TM if present - #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error - #Up to caller to validate. - proc split_modulename_version {fullmodulename} { - set lastpart [namespace tail $fullmodulename] - set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components - if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { - set fileparts [split [file rootname $lastpart] -] - } else { - set fileparts [split $lastpart -] - } - if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} { - set versionsegment [lindex $fileparts end] - set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch - } else { - # - set namesegment [join $fileparts -] - set versionsegment "" - } - set base [namespace qualifiers $fullmodulename] - if {$base ne ""} { - set modulename "${base}::$namesegment" - } else { - set modulename $namesegment - } - return [list $modulename $versionsegment] - } proc get_status {{workingdir ""} args} { set result "" diff --git a/src/modules/punk/mix/commandset/module-999999.0a1.0.tm b/src/modules/punk/mix/commandset/module-999999.0a1.0.tm index 61d085dd..aa4b2344 100644 --- a/src/modules/punk/mix/commandset/module-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/module-999999.0a1.0.tm @@ -9,7 +9,7 @@ # @@ Meta Begin # Application punk::mix::commandset::module 999999.0a1.0 # Meta platform tcl -# Meta license BSD +# Meta license BSD # @@ Meta End @@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_version_supplied [dict get $opts -version] set opt_version $opt_version_supplied - if {![util::is_valid_tm_version $opt_version]} { + if {![punk::mix::util::is_valid_tm_version $opt_version]} { error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version" } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module { set mversion_supplied "" ;#version supplied directly in module argument if {[string first - $module]> 0} { #if it has a dash then version is required to be valid - lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion - if {![util::is_valid_tm_version $mversion]} { + lassign [punk::mix::util::split_modulename_version $module] modulename mversion + if {![punk::mix::util::is_valid_tm_version $mversion]} { error "deck module.new error - unable to determine modulename-version from supplied value '$module'" } set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name @@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module { } else { set module [string range [string length $vendor.] end] } - lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion + lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion lappend key_version_list [list $m $mversion] } if {[llength $matches]} { @@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module { } else { error "module.new error: Unable to interpret filename components of template file '$templatefile'" } - lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version + lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version #t_version may be empty string if template is unversioned e.g template_whatever.tm set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd @@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module { } else { # - if {[util::is_valid_tm_version $t_version]} { + if {[punk::mix::util::is_valid_tm_version $t_version]} { if {$mversion_supplied eq ""} { set build_version $t_version } else { @@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module { set name_version_pairs [list] lappend name_version_pairs [list $moduletail $infile_version] foreach existing $existing_versions { - lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored + lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored if {[string match #modpod-* $namepart]} { set namepart [string range $namepart 8 end] } diff --git a/src/modules/punk/mix/util-999999.0a1.0.tm b/src/modules/punk/mix/util-999999.0a1.0.tm index e78b6df6..be0ef25d 100644 --- a/src/modules/punk/mix/util-999999.0a1.0.tm +++ b/src/modules/punk/mix/util-999999.0a1.0.tm @@ -330,6 +330,35 @@ namespace eval punk::mix::util { return 0 } } + + #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path + #ignore trailing .tm .TM if present + #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error + #Up to caller to validate. + proc split_modulename_version {fullmodulename} { + set lastpart [namespace tail $fullmodulename] + set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components + if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { + set fileparts [split [file rootname $lastpart] -] + } else { + set fileparts [split $lastpart -] + } + if {[is_valid_tm_version [lindex $fileparts end]]} { + set versionsegment [lindex $fileparts end] + set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch + } else { + set namesegment [join $fileparts -] + set versionsegment "" + } + set base [namespace qualifiers $fullmodulename] + if {$base ne ""} { + set modulename "${base}::$namesegment" + } else { + set modulename $namesegment + } + return [list $modulename $versionsegment] + } + #Note that semver only has a small overlap with tcl tm versions. #todo - work out what overlap and whether it's even useful #see also TIP #439: Semantic Versioning (tcl 9??) @@ -337,6 +366,7 @@ namespace eval punk::mix::util { set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$} } #todo - semver conversion/validation for other systems? + proc magic_tm_version {} { set magicbase 999999 ;#deliberately large so given load-preference when testing! #we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version diff --git a/src/modules/punk/nav/fs-999999.0a1.0.tm b/src/modules/punk/nav/fs-999999.0a1.0.tm index f14fcb13..b65f7277 100644 --- a/src/modules/punk/nav/fs-999999.0a1.0.tm +++ b/src/modules/punk/nav/fs-999999.0a1.0.tm @@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) if {$linktarget_file_type eq "directory"} { - set linktarget [file readlink $cdtarget] - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { @@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs { link { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) - set linktarget [file readlink $cdtarget] if {$linktarget_file_type eq "directory"} { - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { diff --git a/src/modules/punk/ns-999999.0a1.0.tm b/src/modules/punk/ns-999999.0a1.0.tm index 82ba6d75..d35287f9 100644 --- a/src/modules/punk/ns-999999.0a1.0.tm +++ b/src/modules/punk/ns-999999.0a1.0.tm @@ -2167,8 +2167,13 @@ y" {return quirkykeyscript} puts stdout "leaving $target" puts stdout "call $commandstring\x1b\[m" - puts stdout "result:" - puts stdout $result + if {$code == 0} { + puts stdout "result:" + puts stdout $result + } else { + puts stdout "error message:" + puts stdout $::errorInfo + } puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset set cmdtype [dict get $linedict $target cmdtype] @@ -2647,6 +2652,8 @@ y" {return quirkykeyscript} upvar ::punk::ns::linedict linedict set ::punk::ns::linedict [::tcl::dict::create] + set body_cache [tcl::dict::create] + set resolved_targets [list] foreach tgt $targets { set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]] @@ -4179,7 +4186,7 @@ y" {return quirkykeyscript} #this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces) #we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace. nseval $targetns [list apply [list {procname argname} { - set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]] + set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]] if {$has_default} { set answer [dict create exists 1 default $::punk::ns::corp_defvar] } else { @@ -5018,6 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs + #puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -6618,18 +6626,19 @@ y" {return quirkykeyscript} separately calling 'info args ' 'info body ' etc. The body may display with an additional - comment inserted to display information such as the + comment inserted above the proc line to display information such as the namespace origin. Such a comment begins with #corp#. - Returns a list: proc - (as long as any syntax highlighter is written to - avoid breaking the structure. e.g by avoiding the - insertion of ANSI between an escaping backslash and - its target character) + Returns a string: proc If the output is to be used as a script to regenerate a procedure, '-syntax none' should be used to avoid ANSI colours, or the resulting arglist and body should be run through 'ansistrip'. + + (any syntax highlighter should be written to + avoid breaking the structure. e.g by avoiding the + insertion of ANSI between an escaping backslash and + its target character) " @opts #todo - make definition @dynamic - load highlighters as functions? @@ -6647,7 +6656,13 @@ y" {return quirkykeyscript} "Whether to replace tabs in the body with spaces or a visible Unicode symbol." -ranges -type indexset -default "0..end" -help\ "comma delimited set of line ranges. - Restrict output to the specified line ranges of the body. Lines are numbered starting at 1." + Restrict output to the specified line ranges of the body. Lines are numbered starting at 1. + For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body. + The special index 0 is used to specify the line before the first line of the body, + which is where the #corp# info comment is placed if it exists. + So the default range 0..end includes the info comment and all lines of the body. + Specifying -ranges 1..5,0 would include the info comment at the end of the output. + " -syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\ -choicelabels { none @@ -6690,11 +6705,6 @@ y" {return quirkykeyscript} set indent [string repeat " " $tw] ;#match #set indent [string repeat " " $tw] ;#A more sensible default for code - review - if {[info exists ::auto_index($path)]} { - set infoheader "\n${indent}#corp# auto_index $::auto_index($path)" - } else { - set infoheader "" - } #we want to handle edge cases of commands such as "" or :x #various builtins such as 'namespace which' won't work @@ -6753,13 +6763,29 @@ y" {return quirkykeyscript} return [list alias {*}$alias] } } - if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} { - append infoheader \n "${indent}#corp# namespace origin $origin" - } - if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { - append infoheader \n + #-------------------------------------------------------------------------- + if {[info exists ::auto_index($path)]} { + #set infoheader "${indent}#corp# auto_index $::auto_index($path)" + set infoheader "#corp# auto_index $::auto_index($path)" + } else { + set infoheader "" } + #puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'" + #if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {} + if {$origin ne $targetcmd} { + #append infoheader "${indent}#corp# namespace origin $origin" + append infoheader "#corp# namespace origin $origin" + } + if {$infoheader ne "" && $syntax eq "basic"} { + set infoheader [ansiwrap green $infoheader] + } + #if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { + # append infoheader \n + #} + #-------------------------------------------------------------------------- + + set body "" #set bodytext [info body $origin] #relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname @@ -6851,47 +6877,126 @@ y" {return quirkykeyscript} } } - if {$ranges ni {"0..end" ".." "0.."}} { - set lines [split $body \n] - set linecount [llength $lines] + set lines [split $body \n] + set linecount [llength $lines] + + + #------------------------------------------------------------------------------------------------ + #When we resolve our 1-based indexset - the zero index used to specify the info comment is lost. + #we need to search for it manually and add it back in if it's in the specified ranges. + set rangelist [split $ranges ,] + set info_positions [list] + set lnum 0 + foreach range $rangelist { + lassign [split $range ..] start _ end + set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range] + if {$start eq "0"} { + lappend info_positions $lnum + } + incr lnum [llength $r_indices] + #if {$end eq "0"} { + # #ignore + #} + } + #puts "info_positions: $info_positions" + #------------------------------------------------------------------------------------------------ + + + set body "" + if {[lindex $info_positions 0] == 0 && $infoheader ne ""} { + append body "$infoheader" \n + } + + if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} { set w [string length $linecount] set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges] - set body "" + set outputlines [llength $indices] + if {$do_ln} { - set n 0 foreach idx $indices { - append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + if {$idx == 1} { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}" + } else { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } else { foreach idx $indices { - append body [lindex $lines $idx-1] \n + if {$idx == 1} { + set ln1 "proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body [lindex $lines end] "\}" + } else { + append body [lindex $lines $idx-1] \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } + #no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match + if {[string index $body end] eq "\n"} { + set body [string range $body 0 end-1] + } } else { + #range was specified in a standard way to mean 'all lines' + set outputlines $linecount if {$do_ln} { - set linebody "" - set n 0 - set lines [split $body \n] - set linecount [llength $lines] set w [string length $linecount] - foreach ln $lines { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + set n 2 + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$lnc[format %${w}s $n]$lnr $ln" incr n - append linebody "$lnc[format %${w}s $n]$lnr $ln" \n } - set body [string range $linebody 0 end-1] - #set body $linebody + if {$linecount > 1} { + append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody + } else { + set ln1 "proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$ln" + } + if {$linecount > 1} { + append linebody \n "[lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody } } - - - if {$is_highlighted} { - #ansi colourised items in list format may not always have desired string representation (list escaping can occur) - #return as a string - which may not be a proper Tcl list! - return "proc $resolved {$argl} {\n$infoheader$body\n}" - } else { - list proc $resolved $argl $infoheader$body + #ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end? + if {[lindex $info_positions end] > 0} { + if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} { + append body "\n$infoheader" + } } + return $body } @@ -7086,6 +7191,7 @@ y" {return quirkykeyscript} nstest eval {package require punk::ns} set ns "" if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + #review set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] set ns [nstest eval $script] } else { diff --git a/src/modules/punk/packagepreference-999999.0a1.0.tm b/src/modules/punk/packagepreference-999999.0a1.0.tm index 2e1cf6f2..fa13511d 100644 --- a/src/modules/punk/packagepreference-999999.0a1.0.tm +++ b/src/modules/punk/packagepreference-999999.0a1.0.tm @@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference { #[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names #[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall + if {![catch {commandstack::get_stack} cstack]} { + if {[dict exists $cstack ::package]} { + set pstack [dict get $cstack ::package] + foreach record $pstack { + if {[dict get $record rename] eq "punk::packagepreference"} { + #already installed - silently ignore. + return 0 + } + } + } + } + + #todo - review/update commandstack package #modern module/lib names should preferably be lower case #see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9) @@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference { if {[llength $pkgloadedinfo]} { if {[llength $available_versions] > 1} { - puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + #puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid" } lassign $pkgloadedinfo loaded_path name set lc_loadedpath [string tolower $loaded_path] @@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference { puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg" } } else { - puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid" } } #--------------------------------------------------------------- diff --git a/src/modules/punk/repl-999999.0a1.0.tm b/src/modules/punk/repl-999999.0a1.0.tm index aa4a43e2..21905f43 100644 --- a/src/modules/punk/repl-999999.0a1.0.tm +++ b/src/modules/punk/repl-999999.0a1.0.tm @@ -165,11 +165,11 @@ namespace eval punk::repl { variable frametype set frametype ascii; #conservative default - if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { - if {$testcharwidth == 1} { - set frametype light - } - } + #if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + # if {$testcharwidth == 1} { + # set frametype light + # } + #} variable debug_repl 0 variable signal_control_c 0 @@ -432,7 +432,7 @@ proc repl::start {inchan args} { variable codethread #review if {$codethread eq ""} { - error "start - no codethread. call init first. (options -safe 0|1)" + error "start - no codethread. call init first. (options -type 0|1)" } variable commandstr @@ -478,7 +478,7 @@ proc repl::start {inchan args} { #set ::punk::repl::codethread::running 1 #the interp in which commands such as d/ run - #we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values + #we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values #punk::repl::codethread::running is required whether safe or not. interp eval code { namespace eval ::punk::repl::codethread {} @@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} { } -#add to sliding buffer of last x chars emmitted to screen by repl +# add to sliding buffer of last x chars emmitted to screen by repl #(we could maintain only one char - more kept merely for debug assistance) #will not detect emissions from exec with stdout redirected and presumably some extensions etc proc repl::screen_last_char_add {c what {why ""}} { @@ -1961,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config } else { set is_vt52 0 } + variable codethread variable loopinstance incr loopinstance @@ -2091,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config set chunk "\b\x7f\b\x7f" } elseif {$chunk eq "\x1c"} { #ctrl-bslash + #This is commonly used in terminals as a 'harder' ctrl-c. #try to brutally terminate process #attempt to leave terminal in a reasonable state - mode line ;#may be aliased to ::repl::interphelpers::mode - after 250 {exit 42} + punk::console::mode line + #for now - exit with small delay for tidyup + after 1000 {exit 43} return } elseif {$chunk eq "\x1a"} { - #for now - exit with small delay for tidyup #ctrl-z #::punk::repl::handler_console_control "ctrl-z_via_rawloop" - if {[catch {punk::console::mode line}]} { - #REVIEW - interp eval code {punk::console::mode line} + #JMN + #set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}] + set iname $::punk::repl::codethread::replthread_interp + #only the highest level subshell has an interp name of empty string (lower levels are named 'code') + if {$iname eq ""} { + punk::console::mode line } - after 1000 {exit 43} + after 250 [list thread::send $codethread [list interp eval code {quit 42}]] return } @@ -2118,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #if {$chunk eq "\x1b\[C"} { #} + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_off flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- $editbuf add_chunk $chunk @@ -2230,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config lappend input_chunks_waiting($inputchan) $waiting } } + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_on flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- if {$editbuf_linenum_submitted == 0} { #(there is no line 0 - lines start at 1) @@ -2259,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap1 POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error1 in repl_handler: $repl_error" + rputs stderr "error1 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "erroropts: $erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2397,6 +2406,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #set commandstr "set ::punk::repl::debug_repl" set commandstr "" } + + + if {$::punk::repl::debug_repl > 100} { proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { set p %p% @@ -2416,12 +2428,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs debugreport $clearance$p[string map [list \n \n$p] $msg] }] set info "" - append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" - append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" + append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" + append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n" set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]] append info "lastrunchunks\n" - append info "chunks: [llength $lastrunchunks]\n" - append info "namespace: $::punk::nav::ns::ns_current" + append info "chunks : [llength $lastrunchunks]\n" + #JMN + set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]] + append info "codethread namespace: $codethread_ns\n" + append info "stdinlines : [llength $stdinlines] lines\n" + foreach ln $stdinlines { + append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n" + } + append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n" + #append info "namespace: $::punk::nav::ns::ns_current" debug_repl_emit $info } else { proc debug_repl_emit {msg} {return} @@ -2466,11 +2486,16 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config # lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] #} - variable codethread variable codethread_cond variable codethread_mutex - lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + #JJJJ JMN test + #REVIEW + #lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + + #thread::transfer $codethread stderr #chan configure stdout -buffering none @@ -2895,8 +2920,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config if {[llength $waiting]} { set c [lindex $waiting end] } else { - #set c " " - set c \u240a + set c \u240a ;#unicode linefeed symbol. } doprompt ">$c " } @@ -2925,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error in repl_handler: $repl_error" + rputs stderr "error2 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "$erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2972,10 +2996,10 @@ namespace eval repl { variable codethread_cond variable codethread_mutex - set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] + set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] foreach {k v} $args { switch -- $k { - -force - -safe - -safelog - -paths - -callback_interp { + -force - -type - -safelog - -paths - -callback_interp { dict set opts $k $v } default { @@ -2984,7 +3008,7 @@ namespace eval repl { } } set opt_force [dict get $opts -force] - set opt_safe [dict get $opts -safe] + set opt_repltype [dict get $opts -type] set opt_safelog [dict get $opts -safelog] if {$opt_safelog eq "0"} { set opt_safelog "" @@ -3008,16 +3032,17 @@ namespace eval repl { set codethread_mutex [thread::mutex create] - set scriptmap [list %args% [list $opts] \ - %argv0% [list $::argv0] \ - %argv% [list $::argv] \ - %argc% [list $::argc] \ - %replthread% [thread::id] \ - %replthread_cond% $codethread_cond \ - %replthread_interp% [list $opt_callback_interp] \ - %tmlist% [list [tcl::tm::list]] \ - %autopath% [list $::auto_path] \ - %lib_epoch% [list $::punk::libunknown::epoch]\ + set scriptmap [list %args% [list $opts] {*}{ + } %argv0% [list $::argv0] {*}{ + } %argv% [list $::argv] {*}{ + } %argc% [list $::argc] {*}{ + } %replthread% [thread::id] {*}{ + } %replthread_cond% $codethread_cond {*}{ + } %replthread_interp% [list $opt_callback_interp] {*}{ + } %tmlist% [list [tcl::tm::list]] {*}{ + } %autopath% [list $::auto_path] {*}{ + } %lib_epoch% [list $::punk::libunknown::epoch] {*}{ + } ] #scriptmap applied at end to satisfy silly editor highlighting. set init_script { @@ -3098,46 +3123,50 @@ namespace eval repl { package require punk::args #package require Thread - if {[catch {package require thread} errM]} { - puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" + #if {[catch {package require thread} errM]} { + # puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" if {[catch {package require Thread} errM2]} { puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2" puts stdout ">>repl::init auto_path : $::auto_path" puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]" } - } + #} + namespace eval ::punk::repl::codethread {} + + # ------------------------------------------------------------------------------------------------------------------------------------------------------- #----- #review - icomm as a possible way to talk to thread outside of the code interp. #thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow #thread::send to caller defined interp targets (reference?) #snit required for icomm - if {[catch {package require snit} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" - } - if {[catch {package require punk::icomm} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" - } + #if {[catch {package require snit} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" + #} + #if {[catch {package require punk::icomm} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" + #} #----- - - namespace eval ::punk::repl::codethread {} - #todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) - catch {package require tcl::chan::fifo2} - if {[catch { - #first use can raise error being a version number e.g 0.1.0 - why? - lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside - } errMsg]} { - puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" - } else { - #experimental? - #puts stdout "transferring chan $replside to thread %replthread%" - #flush stdout - #if {[catch { - # #after 0 [list thread::transfer %replthread% $replside] - #} errMsg]} { - # #puts stdout "---thread::transfer error: $errMsg" - #} - } + #---------------------------------------------------------------------------------------------------------------------- + # todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) + # catch {package require tcl::chan::fifo2} + # if {[catch { + # #first use can raise error being a version number e.g 0.1.0 - why? + # lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside + # } errMsg]} { + # puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" + # } else { + # #experimental? + # #puts stdout "transferring chan $replside to thread %replthread%" + # #flush stdout + # # if {[catch { + # # #after 0 [list thread::transfer %replthread% $replside] + # # } errMsg]} { + # # #puts stdout "---thread::transfer error: $errMsg" + # # } + # } + #---------------------------------------------------------------------------------------------------------------------- + # ------------------------------------------------------------------------------------------------------------------------------------------------------- package require punk::console package require punk::repl::codethread @@ -3318,7 +3347,7 @@ namespace eval repl { set ts_start [clock seconds] set replresult [interp eval code { package require punk::repl - repl::init -safe punk + repl::init -type punk repl::start stdin }] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3328,7 +3357,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe safe {*}$args] + interp eval code [list repl::init -type safe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3338,7 +3367,7 @@ namespace eval repl { interp eval code { package require punk::repl } - set codethread [interp eval code [list repl::init -safe safebase {*}$args]] + set codethread [interp eval code [list repl::init -type safebase {*}$args]] puts stdout "safebase codethread:$codethread" set replresult [interp eval code [list repl::start stdin]] @@ -3349,7 +3378,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe punksafe {*}$args] + interp eval code [list repl::init -type punksafe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3361,14 +3390,14 @@ namespace eval repl { #flush stdout set args %args% - set safe [dict get $args -safe] + set repltype [dict get $args -type] set safelog [dict get $args -safelog] set paths [list] if {[dict exists $args -paths]} { set paths [dict get $args -paths] } - switch -- $safe { + switch -- $repltype { safe { interp create -safe -- code code eval [list namespace eval ::punk::libunknown {}] @@ -3429,7 +3458,7 @@ namespace eval repl { #todo a specific punk::libunknown 'package unknown' handler for safe interps #pull in code via calls to source cached code? - switch -- $safe { + switch -- $repltype { safe { if {[llength $paths]} { package require punk::island @@ -3782,8 +3811,27 @@ namespace eval repl { } punk - 0 { + #------------------------------------------------------------------------ + #Test - experimental 2026 + if {"stdout" in [chan names]} { + interp share {} stdout code + } else { + interp share {} [shellfilter::stack::item_tophandle stdout] code + } + if {"stderr" in [chan names]} { + interp share {} stderr code + } else { + interp share {} [shellfilter::stack::item_tophandle stderr] code + } + foreach nm {shellspyout shellspyerr} { + set thandle [shellfilter::stack::item_tophandle $nm] + if {$thandle ne ""} { + interp share {} $thandle code + } + } + #------------------------------------------------------------------------ interp eval code { - #safe !=1 and safe !=2, tmlist: %tmlist% + #repltype !=1 and repltype !=2, tmlist: %tmlist% set ::argv0 %argv0% set ::argv %argv% set ::argc %argc% @@ -3963,8 +4011,8 @@ namespace eval repl { thread::id } set init_script [string map $scriptmap $init_script] - #REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args% - #we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that? + #REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args% + #we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that? #thread::send $codethread $init_script if {![catch { @@ -3979,7 +4027,7 @@ namespace eval repl { error $errMsg } } - #init - don't auto init - require init with possible options e.g -safe + #init - don't auto init - require init with possible options e.g -type } package provide punk::repl [namespace eval punk::repl { variable version diff --git a/src/modules/punkcheck-0.1.0.tm b/src/modules/punkcheck-0.1.0.tm index eb439c6e..86823a45 100644 --- a/src/modules/punkcheck-0.1.0.tm +++ b/src/modules/punkcheck-0.1.0.tm @@ -121,14 +121,6 @@ namespace eval punkcheck { } method as_record {} { - #set fields [list\ - # -targets $o_targets\ - # -keep_installrecords $o_keep_installrecords\ - # -keep_skipped $o_keep_skipped\ - # -keep_inprogress $o_keep_inprogress\ - # body $o_records\ - #] - dict create {*}{ } tag FILEINFO {*}{ } -targets $o_targets {*}{ @@ -216,18 +208,6 @@ namespace eval punkcheck { } else { set tsiso_end "" } - #set fields [list\ - # -tsiso_begin $tsiso_begin\ - # -ts_begin $o_ts_begin\ - # -tsiso_end $tsiso_end\ - # -ts_end $o_ts_end\ - # -id $o_id\ - # -source $o_rel_sourceroot\ - # -targets $o_rel_targetroot\ - # -types $o_types\ - # -config $o_configdict\ - #] - #set record [dict create tag EVENT {*}$fields] dict create {*}{ } tag EVENT {*}{ diff --git a/src/modules/shellfilter-999999.0a1.0.tm b/src/modules/shellfilter-999999.0a1.0.tm index fbe0b4dc..2174e99c 100644 --- a/src/modules/shellfilter-999999.0a1.0.tm +++ b/src/modules/shellfilter-999999.0a1.0.tm @@ -67,6 +67,8 @@ tcl::namespace::eval shellfilter::log { } proc ::shellfilter::log::close {tag} { #shellthread::manager::close_worker $tag + + #unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag. shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed } @@ -106,6 +108,13 @@ namespace eval shellfilter::pipe { package require shellthread #we are only using the fifo in a single direction to pipe to another thread # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + #---------------------------------------------------------------------------------------- + # Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations: + # (not necessarily a comprehensive list) + # - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms + # tcl::chan::fifo2 is a pure Tcl implementation. + # - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2. + #---------------------------------------------------------------------------------------- if {![catch {package require Memchan}]} { lassign [fifo2] wchan rchan } else { @@ -289,7 +298,8 @@ namespace eval shellfilter::chan { set o_enc [tcl::dict::get $tf -encoding] set o_encbuf "" set settingsdict [tcl::dict::get $tf -settings] - set varname [tcl::dict::get $settingsdict -varname] + set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them? + #these are not upvared - must be fully qualified variable names. set o_datavars $varname if {[tcl::dict::exists $tf -junction]} { set o_is_junction [tcl::dict::get $tf -junction] @@ -298,18 +308,18 @@ namespace eval shellfilter::chan { } } method initialize {ch mode} { - return [list initialize finalize write flush clear] + if {"read" in $mode} { + #this should raise an error in 'chan push' + error "shellfilter::chan::tee_to_var transform does not support read mode" + } + return [list initialize finalize write flush] } method finalize {ch} { my destroy } - method clear {ch} { - return - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } + #method clear {ch} { + # return + #} #method read {ch count} { # return ? #} @@ -320,9 +330,14 @@ namespace eval shellfilter::chan { #puts stdout "" #review - just clear o_encbuf and emit nothing? #we wouldn't have a value there if it was convertable from the channel encoding? - set clear $o_encbuf + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {ch bytes} { #test with set x [string repeat " \U1f6c8" 2043] @@ -387,10 +402,12 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read drain write flush clear finalize] + #return [list initialize read drain write flush clear finalize] + return [list initialize write flush clear finalize] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #Note that an error in the finalize can stop 'chan pop' from running properly. + #::shellfilter::log::close $o_logsource my destroy } method watch {transform_handle events} { @@ -398,26 +415,53 @@ namespace eval shellfilter::chan { # post any events } method clear {transform_handle} { + set o_encbuf "" return } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $logdata - return $bytes - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set logdata [tcl::encoding::convertfrom $o_enc $bytes] + # #::shellfilter::log::write $o_logsource $logdata + # puts -nonewline $o_localchan $logdata + # return $bytes + #} + #method flush {transform_handle} { + # #return "" + # set clear $o_encbuf[set o_encbuf ""] + # if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + # #if we can't convert the buffer contents to a string - put it back and try again with more data later + # #REVIEW? + # set o_encbuf $clear + # puts -nonewline $o_localchan "" + # return "" + # } + # #jjj + # puts -nonewline $o_localchan $stringdata + # flush $o_localchan + # return $clear + #} method flush {transform_handle} { - #return "" - set clear $o_encbuf + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {transform_handle bytes} { #set logdata [tcl::encoding::convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes + if {$inputbytes eq ""} { + #review - do we even get empty writes? + puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write." + return "" + } + set o_encbuf "" set tail_offset 0 while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { @@ -426,18 +470,23 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { + #nothing convertable in the buffer - put it back and try again with more data later set stringdata "" set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } #::shellfilter::log::write $o_logsource $logdata puts -nonewline $o_localchan $stringdata - #return $bytes - return [::tcl::string::range $inputbytes 0 end-$tail_offset] + return $converted ;#same data as $stringdata but in bytes form. } #a tee is not a redirection - because data still flows along the main path method meta_is_redirection {} { @@ -501,6 +550,7 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { @@ -508,10 +558,15 @@ namespace eval shellfilter::chan { set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } - set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] + #set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] ::shellfilter::log::write $o_logsource $stringdata - return $bytes + #return $bytes + return $converted ;#same data as $stringdata but in bytes form. } method meta_is_redirection {} { return $o_is_junction @@ -519,6 +574,10 @@ namespace eval shellfilter::chan { } + #see TIP 230 Tcl Channel Transformation Reflection API + + #the logonly transform does not emit any data downwards towards the base channel - it only writes to the log. + # - ie we are redirecting all data to the log. oo::class create logonly { variable o_tid variable o_logsource @@ -537,19 +596,35 @@ namespace eval shellfilter::chan { set o_tid [::shellfilter::log::open $o_logsource $settingsdict] } method initialize {transform_handle mode} { - return [list initialize finalize write] + #mode is a list containing any of the strings 'read' or 'write'. + if {"read" in $mode} { + #Note that raising an error prevents creation of the transformation. + #The thrown error will appear as a error thrown by 'chan push'. + error "logonly transform does not support read mode" + } + #return all methods supported by the handler + #(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter) + return [list initialize finalize write flush] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #review - tip 230 states that any return value or error raised by finalize is ignored + #we wrap in catch to ensure the 'my destroy' is always called. + catch {::shellfilter::log::close $o_logsource} my destroy } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events + #clear? + method flush {transform_handle} { + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log? + #REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string. + #This may be useful for debugging issues, but it may also result in garbage data in the log. + ::shellfilter::log::write $o_logsource $o_encbuf + set o_encbuf "" + } + return } - #method read {transform_handle count} { - # return ? - #} method write {transform_handle bytes} { #set logdata [encoding convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes @@ -600,26 +675,49 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read write clear flush drain finalize] + #return [list initialize read write clear flush drain finalize] + + #REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work. + #this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel. + #---------------------- + #don't do this + #---------------------- + #if {"read" in $mode} { + # #Note that raising an error prevents creation of the transformation. + # #The thrown error will appear as a error thrown by 'chan push'. + # error "shellfilter::chan::ansistrip transform does not support read mode" + #} + #---------------------- + return [list initialize read write flush finalize] } method finalize {transform_handle} { my destroy } - method clear {transform_handle} { - return - } - method watch {transform_handle events} { - } - method drain {transform_handle} { - return "" - } + #method clear {transform_handle} { + # return + #} + #method drain {transform_handle} { + # return "" + #} method read {transform_handle bytes} { set instring [encoding convertfrom $o_enc $bytes] set outstring [punk::ansi::ansistrip $instring] return [encoding convertto $o_enc $outstring] } + #method flush {transform_handle} { + # return "" + #} method flush {transform_handle} { - return "" + #return "" + set clear $o_encbuf[set o_encbuf ""] + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - put it back and try again with more data later + #REVIEW? + set o_encbuf $clear + return "" + } + #review + return $stringdata } #method write {transform_handle bytes} { # #broken due to occasional unexpected byte sequence @@ -989,29 +1087,44 @@ namespace eval shellfilter::chan { } method initialize {transform_handle mode} { #clear undesirable in terminal output channels (review) - return [list initialize write flush read drain finalize] + #return [list initialize write flush read drain finalize] + if {$mode eq "read"} { + error "shellfilter::chan::ansiwrap channel transform does not support read mode" + } + return [list initialize write flush finalize] } method finalize {transform_handle} { my destroy } - method watch {transform_handle events} { - } method clear {transform_handle} { #In the context of stderr/stdout - we probably don't want clear to run. #Terminals might call it in the middle of a split ansi code - resulting in broken output. - #Leave clear of it the init call + #Leave clear out of the initialize call for now puts stdout "" set emit [tcl::encoding::convertto $o_enc $o_buffered] set o_buffered "" return $emit } + #method flush {transform_handle} { + # #puts stdout "" + # set inputbytes $o_buffered$o_encbuf + # set emit [tcl::encoding::convertto $o_enc $inputbytes] + # set o_buffered "" + # set o_encbuf "" + # return $emit + #} method flush {transform_handle} { - #puts stdout "" - set inputbytes $o_buffered$o_encbuf - set emit [tcl::encoding::convertto $o_enc $inputbytes] + #return "" + set clear $o_buffered$o_encbuf + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes? + # - probably not. + #REVIEW? + return "" + } set o_buffered "" set o_encbuf "" - return $emit + return $stringdata } method write {transform_handle bytes} { #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally @@ -1076,14 +1189,14 @@ namespace eval shellfilter::chan { #set outstring ">>>$instring" return [tcl::encoding::convertto $o_enc $outstring] } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - return [tcl::encoding::convertto $o_enc $outstring] - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set instring [tcl::encoding::convertfrom $o_enc $bytes] + # set outstring "$o_do_colour$instring$o_do_normal" + # return [tcl::encoding::convertto $o_enc $outstring] + #} method meta_is_redirection {} { return $o_is_junction } @@ -1326,9 +1439,11 @@ namespace eval shellfilter::stack { } proc status {{pipename *} args} { variable pipelines + package require textblock set pipecount [dict size $pipelines] set tabletitle "$pipecount pipelines active" set t [textblock::class::table new $tabletitle] + $t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts. $t add_column -headers [list channel-ident] $t add_column -headers [list device-info localchan] $t configure_column 1 -header_colspans {3} @@ -1337,6 +1452,9 @@ namespace eval shellfilter::stack { $t add_column -headers [list stack-info] foreach k [dict keys $pipelines $pipename] { set lc [dict get $pipelines $k device localchan] + if {[catch {chan configure $lc -encoding} lc_enc]} { + set lc_enc "" + } set rc [dict get $pipelines $k device remotechan] if {[dict exists $k device workertid]} { set tid [dict get $pipelines $k device workertid] @@ -1348,18 +1466,35 @@ namespace eval shellfilter::stack { set stackinfo "" } else { set tbl_inner [textblock::class::table new] + $tbl_inner configure -frametype ascii $tbl_inner configure -show_edge 0 + $tbl_inner add_column -headers id + $tbl_inner add_column -headers transform + $tbl_inner add_column -headers handle + $tbl_inner add_column -headers settings + $tbl_inner add_column -headers aside foreach rec $stack { - set handle [punk::lib::dict_getdef $rec -handle ""] set id [punk::lib::dict_getdef $rec -id ""] set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set handle [punk::lib::dict_getdef $rec -handle ""] + if {$handle ne ""} { + if {[catch {chan configure $handle -encoding} handle_enc]} { + set handle_enc "" + } + } else { + set handle_enc "" + } set settings [punk::lib::dict_getdef $rec -settings ""] - $tbl_inner add_row [list $id $transform $handle $settings] + set aside [punk::lib::dict_getdef $rec -aside ""] + if {$aside ne ""} { + set aside [punk::lib::showdict $aside] + } + $tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside] } set stackinfo [$tbl_inner print] $tbl_inner destroy } - $t add_row [list $k $lc $rc $tid $stackinfo] + $t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo] } set result [$t print] $t destroy @@ -1511,15 +1646,24 @@ namespace eval shellfilter::stack { proc unwind {pipename} { variable pipelines - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] foreach tf [lreverse $stack] { - chan pop $localchan + if {[catch {chan eof $localchan} _eof]} { + #We don't actually care about eof state - but we use this to test if the channel exists. + #('chan names' doesn't reliably show all channels in some cases) + #do nothing. + } else { + #if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel + # but here we should only be calling it when there are transforms on the channel as indicated by the stack variable. + chan pop $localchan + } } dict set pipelines $pipename [list] } #todo proc delete {pipename {wait 0}} { + #::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"] variable pipelines set pipeinfo [dict get $pipelines $pipename] set deviceinfo [dict get $pipeinfo device] @@ -1534,18 +1678,29 @@ namespace eval shellfilter::stack { thread::release $tid } - #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' + #- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed. catch {chan close $localchan} + #if {[catch {chan close $localchan} errMsg]} { + # ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg" + #} } #review - proc name clarity is questionable. remove_stackitem? proc remove {pipename remove_id} { + #::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"] variable pipelines if {![dict exists $pipelines $pipename]} { - puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + #puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + ::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" return } set stack [dict get $pipelines $pipename stack] set localchan [dict get $pipelines $pipename device localchan] + set previous_blockingstate [chan configure $localchan -blocking] + if {$previous_blockingstate} { + chan configure $localchan -blocking 0 + } + set posn 0 set idposn -1 set asideposn -1 @@ -1572,46 +1727,76 @@ namespace eval shellfilter::stack { dict set container -aside {} lset stack $asideposn $container dict set pipelines $pipename stack $stack + #::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'" } else { if {$idposn < 0} { - ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" - puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + #::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + #puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" return 0 } set removed_item [lindex $stack $idposn] #include idposn in poplist set poplist [lrange $stack $idposn end] - #set stack [lreplace $stack $idposn end] - set stack [lreplace $stack[set stack {}] $idposn end] + #set stack [lreplace $stack[set stack {}] $idposn end] + # 2026-05-19 + ledit stack $idposn end + + #pop all chans before adding anything back in! foreach p $poplist { + #review + #update idletasks + #puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]" + #::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---" + #after 0 [list chan pop $localchan] + chan pop $localchan + #::shellfilter::log::write shellfilter-remove "POPPED" + #update idletasks } + #after 5 + #::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" + #puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" - if {[llength [dict get $removed_item -aside]]} { - set restore [dict get $removed_item -aside] - set t [dict get $restore -transform] - set tsettings [dict get $restore -settings] + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] set obj [$t new $restore] set h [chan push $localchan $obj] dict set restore -handle $h dict set restore -obj $obj lappend stack $restore + #puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" } + #after 5 #put popped back except for the first one, which we want to remove foreach p [lrange $poplist 1 end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] + set t [dict get $p -transform] + set tsettings [dict get $p -settings] set obj [$t new $p] set h [chan push $localchan $obj] + dict set p -handle $h dict set p -obj $obj lappend stack $p + #update idletasks + #puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" } + #after 5 dict set pipelines $pipename stack $stack } + + #puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE" + #::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE" + + if {$previous_blockingstate} { + chan configure $localchan -blocking 1 + } #JMNJMN 2025 review! #show_pipeline $pipename -note "after_remove $remove_id" return 1 @@ -1622,8 +1807,9 @@ namespace eval shellfilter::stack { variable pipelines set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] set poplist [lrange $stack $bottom_pop_posn end] - #set stack [lreplace $stack $bottom_pop_posn end] - set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] + #set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] + # 2026-05-19 + ledit stack $bottom_pop_posn end set localchan [dict get $pipelines $pipename device localchan] foreach p [lreverse $poplist] { @@ -1849,6 +2035,41 @@ namespace eval shellfilter::stack { namespace eval shellfilter { variable sources [list] variable stacks [dict create] + #------------------------------------------------------------------------------------------------- + #tcllib logger infrastructure. + #------------------------------------------------------------------------------------------------- + namespace eval ::shellfilter::loggerprocs { + #container for procs/aliases to be pointed to by tcllib logger using log::logproc + + proc Dolog {lvl txt} { + #logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted + set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'" + puts stderr $msg + } + proc Runlog {lvl script} { + uplevel 1 $script + } + } + if {![catch { + package require logger + }]} { + logger::initNamespace ::shellfilter + foreach lvl [logger::levels] { + interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl + log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl + } + logger::setlevel warn + #namespace path ::shellfilter::log + } else { + #e.g tcllib not available, safe interp? + #fake out the logger calls + namespace eval ::shellfilter::log { + foreach lvl {debug info notice warn error critical alert emergency} { + proc $lvl {args} {} + } + } + } + #------------------------------------------------------------------------------------------------- proc ::shellfilter::redir_channel_to_log {chan args} { variable sources @@ -2392,13 +2613,9 @@ namespace eval shellfilter { #must be a list. If it was a shell commandline string. convert it elsewhere first. variable sources - set runtag "shellfilter-run" - #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] if {[catch {llength $commandlist} listlen]} { set listlen "" } - ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" #flush stdout #flush stderr @@ -2412,6 +2629,7 @@ namespace eval shellfilter { -errchan stderr -inchan stdin -tclscript 0 + -syslog "" }] set opts [dict merge $defaults $args] @@ -2428,6 +2646,13 @@ namespace eval shellfilter { set teehandle_err ${teehandle}err set teehandle_in ${teehandle}in + set syslog [dict get $opts -syslog] + dict unset opts -syslog + set runtag "shellfilter-run" + set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + #set tid [::shellfilter::log::open $runtag [list -syslog $syslog]] + log::info {::shellfilter::log::write $runtag " opts: $opts"} + log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"} #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" @@ -2437,19 +2662,32 @@ namespace eval shellfilter { lappend sources $source } } - set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] - set outpipechan [dict get $outdeviceinfo localchan] - set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] - set errpipechan [dict get $errdeviceinfo localchan] + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] #set inpipechan [dict get $indeviceinfo localchan] + #--------------------- + # #TEST + # chan configure $outpipechan -blocking 0 + # chan configure $errpipechan -blocking 0 + log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"} + log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"} + #--------------------- + + + log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"} #NOTE:These transforms are not necessarily at the top of each stack! #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + log::critical { + ::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]" + } # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this # If non os-level channel - the command can't be run with the redirection # stderr/stdout can be run with non-os handles in the call - @@ -2489,6 +2727,7 @@ namespace eval shellfilter { set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] } } + log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"} } else { if {[catch { #script result @@ -2496,29 +2735,45 @@ namespace eval shellfilter { } errMsg]} { set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] } + log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"} } #puts "shellfilter::run finished call" #------------------------- #warning - without flush stdout - we can get hang, but only on some terminals # - mechanism for this problem not understood! + #todo - test/document. flush stdout flush stderr + #update idletasks #------------------------- - #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal - #Remove execution-time Tees from stack - shellfilter::stack::remove stdout $id_out - shellfilter::stack::remove stderr $id_err - #shellfilter::stack::remove stderr $id_in - #puts stderr "shellfilter::run complete..." + #---------------------------------------------------------------------------------------------- + # wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug. + # Logger allows the contents to be evaluated only if logging is switched on. + #---------------------------------------------------------------------------------------------- + #todo - change to log::debug + log::critical { + if {![catch {package require punk::ansi}]} { + set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]] + } else { + set stackstatus [shellfilter::stack status] + } + ::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]" + } + #---------------------------------------------------------------------------------------------- + - #chan configure stderr -buffering line - #flush stdout + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"} + shellfilter::stack::remove $outchan $id_out + log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"} + shellfilter::stack::remove $errchan $id_err - ::shellfilter::log::write $runtag " return '$exitinfo'" + log::info {::shellfilter::log::write $runtag " return '$exitinfo'"} ::shellfilter::log::close $runtag return $exitinfo } @@ -2548,6 +2803,7 @@ namespace eval shellfilter { } if {$close} { lappend tidied_sources $s + #unsubscribe from source tag s. shellfilter::log::close $s lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] } @@ -2728,13 +2984,13 @@ namespace eval shellfilter { ::shellfilter::log::write $runtag "checking for redirections in $commandlist" #sometimes we see a redirection without a following space e.g >C:/somewhere #normalize - switch -regexp -- $lastitem\ - {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - }\ - {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} diff --git a/src/modules/shellfilter-buildversion.txt b/src/modules/shellfilter-buildversion.txt index 5e50bcd0..bc664671 100644 --- a/src/modules/shellfilter-buildversion.txt +++ b/src/modules/shellfilter-buildversion.txt @@ -1,3 +1,3 @@ -0.2.1 +0.2.2 #First line must be a semantic version number #all other lines are ignored. \ No newline at end of file diff --git a/src/modules/shellthread-999999.0a1.0.tm b/src/modules/shellthread-999999.0a1.0.tm index 13bf117d..bd1ff041 100644 --- a/src/modules/shellthread-999999.0a1.0.tm +++ b/src/modules/shellthread-999999.0a1.0.tm @@ -119,29 +119,58 @@ namespace eval shellthread::worker { set waitvar ::shellthread::worker::wait($inpipe,[clock micros]) #tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan - chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] - vwait $waitvar - } - proc pipe_read {chan source waitfor readbuffering writebuffering} { + #chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering - } else { - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunksize [chan gets $chan chunk] + if {$chunksize >= 0} { + if {![chan eof $chan]} { + ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + } else { + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + } } - } + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] + } else { - set chunk [chan read $chan] - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $chan + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunk [chan read $chan] + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] } + + vwait $waitvar } + #proc pipe_read {chan source waitfor readbuffering writebuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + # } else { + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # } + # } else { + # set chunk [chan read $chan] + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $chan + # } + #} proc start_pipe_write {source writechan args} { variable outpipe @@ -181,8 +210,8 @@ namespace eval shellthread::worker { chan configure $writechan -blocking 0 set waitvar ::shellthread::worker::wait($outpipe,[clock micros]) - chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { - if {$readbuffering eq "line"} { + if {$readbuffering eq "line"} { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunksize [chan gets $chan chunk] if {$chunksize >= 0} { if {![chan eof $chan]} { @@ -191,19 +220,55 @@ namespace eval shellthread::worker { puts -nonewline $writechan $chunk } } - } else { + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + flush $writechan ;#2026-05-19 - ensure all data is sent before closing + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } + } + }} $readchan $writechan $source $waitvar] + } else { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunk [chan read $chan] puts -nonewline $writechan $chunk - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $writechan - if {$chan ne "stdin"} { - chan close $chan + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } } - } - }} $readchan $writechan $source $waitvar $readbuffering] + }} $readchan $writechan $source $waitvar] + + } + + # chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # puts $writechan $chunk + # } else { + # puts -nonewline $writechan $chunk + # } + # } + # } else { + # set chunk [chan read $chan] + # puts -nonewline $writechan $chunk + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $writechan + # if {$chan ne "stdin"} { + # chan close $chan + # } + # } + # }} $readchan $writechan $source $waitvar $readbuffering] vwait $waitvar } @@ -479,6 +544,10 @@ namespace eval shellthread::manager { set sourcetag [lindex $sourcetaglist 0] ;#todo - use all set defaults [dict create {*}{ + -raw 0 + -file {} + -syslog {} + -direction out -workertype message }] set settingsdict [dict merge $defaults $settingsdict] @@ -501,6 +570,10 @@ namespace eval shellthread::manager { return [dict get $winfo tid] } elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} { #review - magic dict seems brittle - shouldn't hard code here.??? + + #existing worker has default settings - so we'll assume it's a placeholder and update it with our settings + #review - where/when do we override the default settings? + dict lappend winfo list_client_tids $tidclient dict set workers $sourcetag $winfo ;#writeback return [dict get $winfo tid] @@ -575,11 +648,11 @@ namespace eval shellthread::manager { package require Thread package require shellthread if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} { - unset ::settingsinfo - set ::shellthread_init "ok" + unset ::settingsinfo + set ::shellthread_init "ok" } else { - unset ::settingsinfo - set ::shellthread_init "err $errmsg" + unset ::settingsinfo + set ::shellthread_init "err $errmsg" } }] @@ -622,15 +695,23 @@ namespace eval shellthread::manager { proc write_log {source msg args} { variable workers - set ts_micros_sent [clock micros] - set defaults [list -async 1 -level info] - set opts [dict merge $defaults $args] - - if {[dict exists $workers $source]} { + if {[dict exists $workers $source tid]} { set tidworker [dict get $workers $source tid] if {$tidworker eq "noop"} { return } + } else { + set tidworker "" + } + + set ts_micros_sent [clock micros] + set defaults [list {*}{ + -async 1 + -level info + }] + set opts [dict merge $defaults $args] + + if {$tidworker ne ""} { if {![thread::exists $tidworker]} { # -syslog -file ? set tidworker [new_worker $source] @@ -674,10 +755,7 @@ namespace eval shellthread::manager { if {[dict exists $workers $source]} { set list_client_tids [dict get $workers $source list_client_tids] if {[set posn [lsearch $list_client_tids $mytid]] >= 0} { - #set list_client_tids [lreplace $list_client_tids $posn $posn] - #set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn] ledit list_client_tids $posn $posn - dict set workers $source list_client_tids $list_client_tids } if {![llength $list_client_tids]} { @@ -685,7 +763,6 @@ namespace eval shellthread::manager { } } } - #we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all. set subscriberless_workers [list] @@ -696,8 +773,8 @@ namespace eval shellthread::manager { set subscriber_count 0 set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed foreach taginfo $worker_tags { - incr subscriber_count [llength [dict get $taginfo list_client_tids]] - incr kill_count [llength [dict get $taginfo ts_end_list]] + incr subscriber_count [llength [dict get $taginfo list_client_tids]] + incr kill_count [llength [dict get $taginfo ts_end_list]] } if {$subscriber_count == 0} { lappend subscriberless_workers $workertid @@ -760,7 +837,7 @@ namespace eval shellthread::manager { set ::shellthread::waitfor waiting #after $timeout [list set ::shellthread::waitfor] #2025-07 timed-out untested review - set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]] + set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}] set waiting_for [list] set ended [list] @@ -769,7 +846,9 @@ namespace eval shellthread::manager { if {[thread::exists $tid]} { lappend waiting_for $tid #thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads) - thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor + set tid_client [thread::id] + #shellthread::worker::terminate will return thread id of terminating thread (or empty string) + thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor } } if {[llength $waiting_for]} { @@ -779,13 +858,13 @@ namespace eval shellthread::manager { set timedout 1 break } else { - after cancel $cancelid + after cancel $timeout_timer lappend ended $::shellthread::waitfor } } } set free_threads [list] - return [dict create existed $waiting_for ended $ended timedout $timedout] + return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]] } #TODO - important. diff --git a/src/modules/test/punk/#modpod-ansi-999999.0a1.0/ansi-0.1.1_testsuites/ansi/ansimerge.test b/src/modules/test/punk/#modpod-ansi-999999.0a1.0/ansi-0.1.1_testsuites/ansi/ansimerge.test index caa2e54c..e2a951e8 100644 --- a/src/modules/test/punk/#modpod-ansi-999999.0a1.0/ansi-0.1.1_testsuites/ansi/ansimerge.test +++ b/src/modules/test/punk/#modpod-ansi-999999.0a1.0/ansi-0.1.1_testsuites/ansi/ansimerge.test @@ -1,4 +1,3 @@ - package require tcltest namespace eval ::testspace { diff --git a/src/modules/test/punk/#modpod-ns-999999.0a1.0/ns-0.1.0_testsuites/ns/corp.test b/src/modules/test/punk/#modpod-ns-999999.0a1.0/ns-0.1.0_testsuites/ns/corp.test index 90e7c445..310dc98f 100644 --- a/src/modules/test/punk/#modpod-ns-999999.0a1.0/ns-0.1.0_testsuites/ns/corp.test +++ b/src/modules/test/punk/#modpod-ns-999999.0a1.0/ns-0.1.0_testsuites/ns/corp.test @@ -87,4 +87,46 @@ namespace eval ::testspace { -result [list\ 1 ] + + test corp_single_line_function {Test that punk::ns::corp returns only a single line for a single line proc} {*}{ + } -setup $common -body { + proc spud3 {} {return single-line function} + set body [punk::ns::corp -syntax none spud3] + lappend result [llength [split $body \n]] + } {*}{ + } -cleanup { + rename spud3 "" + } {*}{ + } -result [list {*}{ + 1 + }] + + test corp_linecount_match {Test that punk::ns::corp returns same number of lines} {*}{ + } -setup $common -body { + + #7 lines including proc line and closing brace, with 2 empty lines 2 comment lines + proc spud4 {a {b default}} { + + #multiline function with some comments and empty lines + #test etc. + + return spud4 + } + + set body [punk::ns::corp -syntax none spud4] + lappend result [llength [split $body \n]] + + #now test that when restricted to 3 lines it returns 3 lines + #(no extra trailing newline allowed) + set body3 [punk::ns::corp -syntax none -ranges 1..3 spud4] + lappend result [llength [split $body3 \n]] + + } {*}{ + } -cleanup { + rename spud4 "" + } {*}{ + } -result [list {*}{ + 7 + 3 + }] } \ No newline at end of file diff --git a/src/modules/textblock-999999.0a1.0.tm b/src/modules/textblock-999999.0a1.0.tm index 9f8f546c..e5d6500f 100644 --- a/src/modules/textblock-999999.0a1.0.tm +++ b/src/modules/textblock-999999.0a1.0.tm @@ -5107,9 +5107,25 @@ tcl::namespace::eval textblock { tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::string::length $v}] } - if {[catch {package require parser}]} { - #tclparser c extension not available - use tcl string functions to count line-endings + + #try not to load parser (and associated punk::args::moduledoc::parser) immediately. + if {[package provide parser] ne ""} { + #parser already loaded. proc height {textblock} { + if {[string first \v $textblock] >= 0} { + #use standard (slower) mechanism for counting lines + #vertical tab on a proper terminal should move directly down. + #Whether or not the terminal in use actually does this - we need to calculate as if it does. (there might not even be a terminal) + set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list + return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le + } else { + return [expr {[parse countnewline $textblock {}] + 1}] + } + } + } else { + #parser not loaded - but might be loadable. + #install a 'height' function that will load parser + proc _height_tcl {textblock} { #This is the height as it will/would-be rendered - not the number of input lines purely in terms of le #empty string still has height 1 (at least for left-right/right-left languages) @@ -5119,8 +5135,7 @@ tcl::namespace::eval textblock { set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le } - } else { - proc height {textblock} { + proc _height_c {textblock} { if {[string first \v $textblock] >= 0} { #use standard (slower) mechanism for counting lines #vertical tab on a proper terminal should move directly down. @@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock { return [expr {[parse countnewline $textblock {}] + 1}] } } + + #oneshot height function - renames itself on first call to the appropriate implementation. + proc height {textblock} { + if {[catch {package require parser}]} { + #parser not available - use tcl implementation + rename ::textblock::height "" + rename ::textblock::_height_tcl ::textblock::height + } else { + #parser available - use c implementation + rename ::textblock::height "" + rename ::textblock::_height_c ::textblock::height + } + tailcall ::textblock::height $textblock + } } + #MAINTENANCE - same as overtype::blocksize? proc size {textblock} { if {$textblock eq ""} { @@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock { -etabs -default 0\ -help "expanding tabs - experimental/unimplemented." #review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels - -type -default light\ - -type dict\ - -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ - -choices {${$DYN_FRAMETYPES}}\ - -choicerestricted 0 -choicecolumns 8\ - -unindentedfields {-choicelabels}\ - -choicelabels { - ${$DYN_FRAMESAMPLES} - }\ - -help "Type of border for frame." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${$DYN_FRAMETYPES}}\ + -choicerestricted 0\ + -choicecolumns 8\ + -unindentedfields {-choicelabels}\ + -choicelabels { + ${$DYN_FRAMESAMPLES} + }\ + -help "Type of border for frame." -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. passing an empty string will result in no box, but title/subtitle will still appear if supplied. ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.7.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.7.4.tm index 403e971c..6c427f1d 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.7.4.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.7.4.tm @@ -401,13 +401,20 @@ tcl::namespace::eval overtype { set opt_console [tcl::dict::get $opts -console] + #-------------------------------------------------------------------------- + #TODO + #REVIEW - punk::console package may not be loaded set cursor_style_overtype {3 underline-blink} set cursor_style_insert {5 beam-blink} if {$opt_insert_mode} { - punk::console::cursor_style -console $opt_console $cursor_style_insert + set initial_cursor_style $cursor_style_insert } else { + set initial_cursor_style $cursor_style_overtype + } + catch { punk::console::cursor_style -console $opt_console $cursor_style_overtype } + #-------------------------------------------------------------------------- # ---------------------------- # -experimental dev flag to set flags etc @@ -695,22 +702,23 @@ tcl::namespace::eval overtype { #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\ - -overtext_type $overtext_type\ + 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 {*}{ + } -overtext_type $overtext_type {*}{ + } ] set rinfo [renderline {*}$renderopts $undertext $overtext] @@ -940,14 +948,15 @@ tcl::namespace::eval overtype { puts stdout ">>>renderspace<<<[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 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..? @@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype { } #JMN - if {[tcl::dict::get $vtstate insert_mode]} { - puts "setting cursor to insert style" - punk::console::cursor_style -console $opt_console $cursor_style_insert - } else { - punk::console::cursor_style -console $opt_console $cursor_style_overtype - } + #REVIEW - we don't want to emit cursor_style ANSI unless it changes. + #if {[tcl::dict::get $vtstate insert_mode]} { + # puts "setting cursor to insert style" + # punk::console::cursor_style -console $opt_console $cursor_style_insert + #} else { + # punk::console::cursor_style -console $opt_console $cursor_style_overtype + #} #puts "renderedrow_max: $renderedrow_max" #check for null lines below renderedrow_max (and at tail) and trim. @@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype { #broken: #todo - renderline -overflow is invalid. # we need renderline to support -expand_left ?? - 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 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} { @@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype { -crm_mode -default 0 -type boolean -autowrap_mode -default 1 -type boolean -reverse_mode -default 0 -type boolean - -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\ + -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\ + -choicelabels\ { 1 "return a dict with raw fields" 2 "return a dict using ansistring VIEW" 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 index 4473c1d1..a0ae202c 100644 --- 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 @@ -283,14 +283,6 @@ namespace eval punk { #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;" - } # ------------------------ #Note that unlike an ordinary Tcl array - the linked ::env behaves differently. @@ -307,6 +299,15 @@ namespace eval punk { } # ------------------------ + 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;" + } + #change2 if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { set lookfor [list $name] @@ -5492,9 +5493,10 @@ namespace eval punk { #ctrl-c propagation also needs to be considered set teehandle punksh - uplevel 1 [list ::catch \ - [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ - ::tcl::UnknownResult ::tcl::UnknownOptions] + uplevel 1 [list ::catch {*}{ + } [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{ + } ::tcl::UnknownResult ::tcl::UnknownOptions + ] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { dict set ::tcl::UnknownOptions -code error @@ -8428,19 +8430,44 @@ namespace eval punk { set I [punk::ansi::a+ italic] set NI [punk::ansi::a+ noitalic] + set sizedict [punk::console::get_size] + set cols [dict get $sizedict columns] + set rows [dict get $sizedict rows] + + + + #todo - provide a mechanism to configure the default frametype everywhere and describe it in this help. + + set frametype ascii ;#conservative default. + #if the test char width fails - it's likely we're on a very old terminal that doesn't support unicode at all. + if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + if {$cols <= 80} { + # Be conservative with frame types on narrow terminals for help. + # an 80x30 terminal is more likely to be an older style terminal and may not have unicode support. + # unicode on a non-unicode terminal is a bad experience - with the frame chars showing as garbage (e.g 3 chars per grapheme). + set frametype ascii + } else { + if {$testcharwidth == 1} { + set frametype light ;#unicode box-drawing chars. + } + } + } + + # ------------------------------------------------------- set logoblock "" if {[catch { package require patternpunk #lappend chunks [list stderr [>punk . rhs]] - append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]] + append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]] }]} { - append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""] + append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""] } set title "[a+ brightgreen] Help System: " set cmdinfo [list] lappend cmdinfo [list help "?${I}topic${NI}?" "This help.\nTo see available subitems type:\nhelp topics\n\nFor an unrecognised ${I}topic${NI}\nhelp will look for basic\ninfo for it as a command.\n"] set t [textblock::class::table new -minwidth 51 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8468,6 +8495,7 @@ namespace eval punk { lappend cmdinfo [list newdir "${I}subdir${NI}..." "make new dir or dirs and show status"] lappend cmdinfo [list fcat "${I}file ?file?...${NI}" "cat file(s)"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8491,6 +8519,7 @@ namespace eval punk { lappend cmdinfo [list "nn/" "" "go up one namespace"] lappend cmdinfo [list "newns" "${I}ns${NI}" "make child namespace and switch to it"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8513,6 +8542,7 @@ namespace eval punk { lappend cmdinfo [list eg "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show example from manpage"] lappend cmdinfo [list corp "${I}proc${NI}" "View proc body and arguments with basic highlighting"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8537,6 +8567,7 @@ namespace eval punk { lappend cmdinfo [list a "?${I}colourcode${NI}...?" "Return ANSI codes (with leading reset)\n e.g puts \"\[a+ purple\]purple\[a Green\]normal on green\[a\]\"\n [a+ purple]purple[a Green]normal on green[a] "] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8606,6 +8637,7 @@ namespace eval punk { set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + $t configure -frametype $frametype if {"windows" eq $::tcl_platform(platform)} { #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. #The Tcl ::env array is linked to the underlying process view of the environment @@ -8634,6 +8666,7 @@ namespace eval punk { $t destroy set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + $t configure -frametype $frametype foreach {v vinfo} $otherenv_config { if {[info exists ::env($v)]} { set env_val [set ::env($v)] @@ -8851,6 +8884,7 @@ namespace eval punk { }] set t [textblock::class::table new -show_seps 0] + $t configure -frametype $frametype $t add_column -headers [list "Topic"] $t add_column foreach {k v} $topics { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm index f9997649..e8518d0f 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm @@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi { @cmd -name punk::ansi::sauce -summary\ "SAUCE info from file"\ -help\ - "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data." + "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data. + + Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format + that was commonly used in old ANSI art files to store information about the file, such as + title, author, group, date, and comments. + It may also have fields to specify the number of columns and rows in the ANSI art, + as well as flags for specific display attributes. + + It may also be used on other types of files such as bitmap, vector, audio, binarytext, + xbin, archive and executable files. + + It is a 128-byte block of data that is typically appended to the end of a file. + https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm" -encoding -default iso8859-1 -type string -help\ "The default iso8859-1 is equivalent to binary ans should work in the usual case. @@ -6044,6 +6056,7 @@ be as if this was off - ie lone CR. #[para]These functions will emit the code - but read it in from stdin so that it doesn't display, and then return the row and column as a colon-delimited string or list respectively. #[para]The punk::ansi::cursor_pos function is used by punk::console::get_cursor_pos and punk::console::get_cursor_pos_list return \033\[6n + #same as 'tput u7' which just emits CSI 6n to stdout } proc cursor_pos_extended {} { @@ -6053,6 +6066,7 @@ be as if this was off - ie lone CR. } + #DECFRA - Fill rectangular area #REVIEW - vt100 accepts decimal values 132-126 and 160-255 ("in the current GL or GR in-use table") #some modern terminals accept and display characters outside this range - but this needs investigation. diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm index 79ea5901..e7428d84 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm @@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce { variable PUNKARGS variable PUNKARGS_aliases + #https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm lappend PUNKARGS [list { @id -id "(package)punk::ansi::sauce" @package -name "punk::ansi::sauce" -help\ diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm index 5271bbfb..b7c4cd7a 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm @@ -44,7 +44,14 @@ #[list_begin itemized] package require Tcl 8.6- + +#---------------------------------------------------- +#Although we need to be in an environment with Thread available to use punk::console, +# we don't want to require Thread as a hard dependency in the interp we're running in. +# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp. +#TODO. package require Thread ;#tsv required to sync is_raw +#---------------------------------------------------- package require punk::ansi package require punk::args #*** !doctools @@ -282,7 +289,7 @@ namespace eval punk::console { ignore the regex match 'ok' response and keep going." -return -type string -default payload -choices {payload dict} -choicelabels { - dict\ + dict "dict with keys prefix,response,payload,all" } -help\ "Return format" @@ -290,12 +297,12 @@ namespace eval punk::console { -console -default {stdin stdout} -type list -help\ "console/terminal (currently list of in/out channels) (todo - object?)" -passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels { - none\ + none { ANSI sent without any passthrough wrapping. A terminal multiplexer such as tmux,screen,zellij may not pass the request through to the underlying terminal(s) This is the recommended/normal value for the option.} - tmux\ + tmux { Wrap ANSI sequence with tmux passthrough sequence. \x1bPtmux\;\x1b\\ Note that a tmux session could be connected to multiple @@ -304,7 +311,7 @@ namespace eval punk::console { Passthrough should generally be avoided except for debug/test purposes. } - auto\ + auto { Use existence of ::env(TMUX) to detect tmux and send tmux passthrough sequence. Not recommended except for debug/test purposes. @@ -1012,6 +1019,7 @@ namespace eval punk::console { #e.g \033\[46;1R set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$} ;#must capture prefix,entire-response,response-payload + #This is all 'tput u7' does (emits CSI 6n on stdout) set request "\033\[6n" set payload [punk::console::internal::get_ansi_response_payload -console $inoutchannels $request $capturingregex] #some terminals fail to respond properly to \x1b\[6n but do respond to \x1b\[?6n and vice-versa :/ @@ -1021,6 +1029,9 @@ namespace eval punk::console { return $payload } + + + proc get_checksum_rect {id page t l b r {inoutchannels {stdin stdout}}} { #e.g \x1b\[P44!~E797\x1b\\ #re e.g {(.*)(\x1b\[P44!~([[:alnum:]])\x1b\[\\)$} @@ -1295,7 +1306,7 @@ namespace eval punk::console { default {set keyboard_name "unknown"} } - return [dict create {*} { + return [dict create {*}{ } class $class_name {*}{ } version $version {*}{ } keyboard $keyboard_name {*}{ @@ -1522,17 +1533,114 @@ namespace eval punk::console { } #todo - determine cursor on/off state before the call to restore properly. + variable get_size_mechanism + set get_size_mechanism [dict create] + proc get_size {{inoutchannels {stdin stdout}}} { lassign $inoutchannels in out - #we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810 - #chan eof is faster whether chan exists or not than - if {[catch {chan eof $out} is_eof]} { - error "punk::console::get_size output channel $out seems to be closed ([info level 1])" - } else { - if {$is_eof} { - error "punk::console::get_size eof on output channel $out ([info level 1])" + set tried_mechlist [list] + + #fastest mechanism if available - use Tcl's inbuilt -winsize key from chan configure if available - this is much faster than any ANSI mechanism + #unknown which platforms support this. + if {![catch {get_size_using_chanconfigure $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist "chanconfigure" + + + variable is_vt52 + if {$is_vt52} { + #vt52 doesn't support cursor save/restore or cursor position reports. + if {![catch {get_size_using_tput $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist "tput" + error "can't get console size. Tried mechanisms: $mechlist" + } + + variable get_size_mechanism ;#dict keyed on terminal ident. (currently just list of inoutchannels but may be something else in future such as terminal object or ident string) + if {![dict exists $get_size_mechanism $inoutchannels]} { + set try_order [list] + #call each mechanism once to see if it works - and to ensure we don't include initial run in our timings. + #we will also use the results for our initial return of the size. + set successful_mechs [list] + set sizedict [dict create] + if {![catch {get_size_using_cursorrestore $inoutchannels} result]} { + lappend successful_mechs "cursorrestore" + if {![dict size $sizedict]} { + set sizedict $result + } + } + if {![catch {get_size_using_cursormove $inoutchannels} result]} { + lappend successful_mechs "cursormove" + if {![dict size $sizedict]} { + set sizedict $result + } } + if {![catch {get_size_using_tput $inoutchannels} result] } { + lappend successful_mechs "tput" + if {![dict size $sizedict]} { + set sizedict $result + } + } + set timings [list] + set t_ms 250 ;#default timerate is 1000ms - we are trying at least 3 mechanisms so 1000ms is a bit long for this test as it can slow down the first call to get_size significantly. + foreach sm $successful_mechs { + catch { + set timing_result [timerate {get_size_using_$sm $inoutchannels} $t_ms] + set micros [expr {int([lindex $timing_result 0])}] + lappend timings [list $micros $sm] + } + } + set sorted [lsort -integer -index 0 $timings] + if {[llength $sorted] > 0} { + set try_order [lmap t $sorted {lindex $t 1}] + } else { + set try_order [list] + } + dict set get_size_mechanism $inoutchannels $try_order + return $sizedict + } + + foreach mech [dict get $get_size_mechanism $inoutchannels] { + if {![catch {get_size_using_$mech $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist $mech } + + #if {![catch {get_size_using_cursorrestore $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "cursorrestore" + + #if {![catch {get_size_using_cursormove $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "cursormove" + + #if {![catch {get_size_using_tput $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "tput" + + error "can't get console size. Tried mechanisms: $tried_mechlist" + } + + proc get_size_using_chanconfigure {{inoutchannels {stdin stdout}}} { + set out [lindex $inoutchannels 1] + set outconf [chan configure $out] + if {[dict exists $outconf -winsize]} { + #this mechanism is much faster than ansi cursor movements + #REVIEW check if any x-platform anomalies with this method? + #can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least + lassign [dict get $outconf -winsize] cols lines + if {[string is integer -strict $cols] && [string is integer -strict $lines]} { + return [dict create columns $cols rows $lines] + } + } + error "chan configure method of getting console size not supported or failed to get valid size info" + #we don't need to care about the input channel if chan configure on the output can give us the info. #short circuit ansi cursor movement method if chan configure supports the -winsize value set outconf [chan configure $out] @@ -1542,43 +1650,45 @@ namespace eval punk::console { #can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least lassign [dict get $outconf -winsize] cols lines if {[string is integer -strict $cols] && [string is integer -strict $lines]} { - return [list columns $cols rows $lines] + return [dict create columns $cols rows $lines] } #continue on to ansi mechanism if we didn't get 2 ints } - if {[catch {chan eof $in} is_eof]} { - error "punk::console::get_size input channel $in seems to be closed ([info level 1])" + } + + proc get_size_using_tput {{inoutchannels {stdin stdout}}} { + set tputcmd [auto_execok tput] + if {$tputcmd eq ""} { + error "tput command not found - cannot use tput method to get console size" + } + lassign [exec {*}$tputcmd lines cols] lines cols + return [dict create columns $cols rows $lines] + } + + proc get_size_using_cursormove {{inoutchannels {stdin stdout}}} { + set out [lindex $inoutchannels 1] + #we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810 + #chan eof is faster whether chan exists or not than + if {[catch {chan eof $out} is_eof]} { + error "punk::console::get_size_using_cursormove output channel $out seems to be closed ([info level 1])" } else { if {$is_eof} { - error "punk::console::get_size eof on input channel $in ([info level 1])" + error "punk::console::get_size_using_cursormove eof on output channel $out ([info level 1])" } } - #keep out of catch - no point in even trying a restore move if we can't get start position - just fail here. - #no vt52 equiv? may as well strip all vt52 from here? lassign [get_cursor_pos_list $inoutchannels] start_row start_col - variable is_vt52 - if {!$is_vt52} { - set movefunc "punk::ansi::move" - set func_coff "punk::ansi::cursor_off" - set func_con "punk::ansi::cursor_on" - } else { - set movefunc "punk::ansi::vt52move" - set func_coff "punk::ansi::vt52cursor_off" - set func_con "punk::ansi::vt52cursor_on" - } if {[catch { #some terminals (conemu on windows) scroll the viewport when we make a big move down like this - a move to 1 1 immediately after cursor_save doesn't seem to fix that. #This issue also occurs when switching back from the alternate screen buffer - so perhaps that needs to be addressed elsewhere. - - puts -nonewline $out [$func_coff][$movefunc 2000 2000] + puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::move 2000 2000] lassign [get_cursor_pos_list $inoutchannels] lines cols - puts -nonewline $out [$movefunc $start_row $start_col][$func_con];flush stdout - set result [list columns $cols rows $lines] + puts -nonewline $out [punk::ansi::move $start_row $start_col][punk::ansi::cursor_on];flush stdout + set result [dict create columns $cols rows $lines] } errM]} { - puts -nonewline $out [$movefunc $start_row $start_col] - puts -nonewline $out [$func_con] + puts -nonewline $out [punk::ansi::move $start_row $start_col] + puts -nonewline $out [punk::ansi::cursor_on] error "$errM" } else { return $result @@ -1586,14 +1696,16 @@ namespace eval punk::console { } #faster than get_size when it is using ansi mechanism - but uses cursor_save - which we may want to avoid if calling during another operation which uses cursor save/restore - proc get_size_cursorrestore {{inoutchannels {stdin stdout}}} { + proc get_size_using_cursorrestore {{inoutchannels {stdin stdout}}} { lassign $inoutchannels in out - #we use the same shortcircuit mechanism as get_size to avoid ansi at all if the output channel will give us the info directly set outconf [chan configure $out] - if {[dict exists $outconf -winsize]} { - lassign [dict get $outconf -winsize] cols lines - if {[string is integer -strict $cols] && [string is integer -strict $lines]} { - return [list columns $cols rows $lines] + #don't use shortcut mechanisms - this function is intended to specificall use the cursor_save/restore method + + if {[catch {chan eof $out} is_eof]} { + error "punk::console::get_size_using_cursorrestore output channel $out seems to be closed ([info level 1])" + } else { + if {$is_eof} { + error "punk::console::get_size_using_cursorrestore eof on output channel $out ([info level 1])" } } @@ -1603,7 +1715,7 @@ namespace eval punk::console { puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::cursor_save_dec][punk::ansi::move 2000 2000] lassign [get_cursor_pos_list $inoutchannels] lines cols puts -nonewline $out [punk::ansi::cursor_restore][punk::console::cursor_on];flush $out - set result [list columns $cols rows $lines] + set result [dict create columns $cols rows $lines] } errM]} { puts -nonewline $out [punk::ansi::cursor_restore_dec] puts -nonewline $out [punk::ansi::cursor_on] @@ -1612,10 +1724,15 @@ namespace eval punk::console { return $result } } + + proc get_dimensions {{inoutchannels {stdin stdout}}} { lassign [get_size $inoutchannels] _c cols _l lines return "${cols}x${lines}" } + + + #the (xterm?) CSI 18t query is supported by *some* terminals proc get_xterm_size {{inoutchannels {stdin stdout}}} { set capturingregex {(.*)(\x1b\[8;([0-9]+;[0-9]+)t)$} ;#must capture prefix,entire-response,response-payload @@ -2669,6 +2786,7 @@ namespace eval punk::console { This allows querying the current style and then re-setting it after temporarily changing it." }] } + proc cursor_style {args} { set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style] lassign [dict values $argd] leaders opts values diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm index e67acfda..5ee354a6 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm @@ -1641,7 +1641,7 @@ namespace eval punk::du { #convert time from windows (100ns units since jan 1, 1601) to Tcl time (seconds since Jan 1, 1970) #We lose some precision by not passing the boolean to the large_system_time_to_secs_since_1970 function which returns fractional seconds #but we need to maintain compatibility with other platforms and other tcl functions so if we want to return more precise times we will need another flag and/or result dict - dict set alltimes $fullname [dict create {*} { + dict set alltimes $fullname [dict create {*}{ } c [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo ctime]] {*}{ } a [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo atime]] {*}{ } m [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo mtime]] {*}{ @@ -2114,15 +2114,15 @@ namespace eval punk::du { } proc du_dirlisting_tclvfs {folderpath args} { - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm index aee51015..8f25047c 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm @@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check { if {"windows" ne $::tcl_platform(platform)} { set bug 0 } else { - set tmpdir [file tempdir] ;#tcl 9+ - set testfile [file join $tmpdir "bugtest"] + if {![catch {file tempdir} tmpdir]} { + #tcl 9+ has 'file tempdir' + set testfile [file join $tmpdir "bugtest"] + } else { + #fallback for older tcl versions - use env TEMP/TMP or current directory + set tmpdir "" + foreach e {TEMP TMP} { + if {[info exists ::env($e)] && [file isdirectory ::env($e)]} { + set tmpdir ::env($e) + break + } + } + if {$tmpdir eq ""} { + #no env vars - fallback to current directory + set tmpdir [pwd] + } + set testfile [file join $tmpdir "bugtest"] + } + set fd [open $testfile w] puts $fd test close $fd set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*] + if {[file exists $testfile]} { + file delete $testfile + } foreach r $globresult { if {$r ne "bugtest"} { set bug 1 @@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat { #*** !doctools #[call [fun lpop] [arg listvar] [opt {index}]] #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l + #upvar $lvar l + upvar 1 $lvar l if {![llength $args]} { set args [list end] } @@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat { #set newlist [lremove $newlist $tailidx] #set newlist [lreplace $newlist $tailidx $tailidx] set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx] - #don't use ledit here! + #we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin. } else { set sublist [lindex $newlist {*}$sublist_path] #set sublist [lremove $sublist $tailidx] @@ -459,26 +480,46 @@ tcl::namespace::eval punk::lib::compat { } } set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -Inf { - #index below lower bound - set post [lrange $l 0 end] - } - Inf { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + #set post [lrange $l $last+1 end] + switch -exact -- $lidx { + -Inf { + #index below lower bound + set post [lrange $l 0 end] + } + Inf { + #index above upper bound + set post [list] + } + default { set post [lrange $l $lidx+1 end] } } } + #switch -exact -- $lidx { + # -Inf { + # #index below lower bound + # set post [lrange $l 0 end] + # } + # Inf { + # #index above upper bound + # set post [list] + # } + # default { + # if {$lidx < $fidx} { + # #from ledit man page: + # #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + # set post [lrange $l $fidx end] + # } else { + # #set post [lrange $l $last+1 end] + # set post [lrange $l $lidx+1 end] + # } + # } + #} set l [list {*}$pre {*}$args {*}$post] } @@ -2481,7 +2522,7 @@ namespace eval punk::lib { #no parse tree - This is likely for an empty argument with expansion e.g {*}{ } #This construct occurs when using {*} in place of line continuation for long lists or dicts, e.g #dict create {*}{ - # } key1 $dynamic {*} { + # } key1 $dynamic {*}{ # key2 value2 #} #review - the 'empty' argument will still have an entry in cmdlineranges - as although 'empty' in terms of how it expands it may be whitespace across multiple lines. @@ -3447,32 +3488,53 @@ namespace eval punk::lib { showdict {*}$opts $dvalue {*}$patterns } - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + namespace eval argdoc { + variable PUNKARGS + upvar ::punk::lib::has_punk_ansi has_punk_ansi + + + #if {!$has_punk_ansi} { + # set RST "" + # set sep " = " + # set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + #} else { + # set RST [punk::ansi::a] + # #set sep " [a+ Web-seagreen]=[a] " + # set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support + # #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + # #NOTE that \u2260 not suitable for non utf-8 terminals. + # set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + #} + #todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8. + # (safe detection methods for utf-8 support?) + #if colour is disabled we want to refresh this. + #therefore we use @dynamic + proc get_sep {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep " = " + } else { + #set sep " [a+ Web-seagreen]=[a] " + set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support + } + return $sep } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock + proc get_sep_mismatch {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + } else { + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #NOTE that \u2260 not suitable for non utf-8 terminals. + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] " + } + return $sep_mismatch + } + set DYN_SEP {${[get_sep]}} + set DYN_SEP_MISMATCH {${[get_sep_mismatch]}} - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { + lappend PUNKARGS [list { + @dynamic @id -id ::punk::lib::showdict @cmd -name punk::lib::showdict -help "display dictionary keys and values" #todo - table tableobject @@ -3482,10 +3544,8 @@ namespace eval punk::lib { "Trim whitespace off rhs of each line. This can help prevent a single long line that wraps in terminal from making every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" + -separator -default "${$DYN_SEP}" -help "Separator column between keys and values" + -separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch" -roottype -default "dict" -help\ "list,dict,string" -ansibase_keys -default "" -help\ @@ -3504,7 +3564,23 @@ namespace eval punk::lib { "dict or list value" patterns -default "*" -type string -multiple 1 -help\ "key or key glob pattern" - }]] + }] + } + #TODO - much. + #showdict needs to be able to show different branches which share a root path + #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) + # - specify ansi colour per pattern so different branches can be highlighted? + # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc + # - The current version is incomplete but passably usable. + # - Copy proc and attempt rework so we can get back to this as a baseline for functionality + proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) + package require punk::pipe + #package require punk ;#we need pipeline pattern matching features + package require textblock + + set RST [punk::ansi::a] + + set argd [punk::args::parse $args withid ::punk::lib::showdict] #for punk::lib - we want to reduce pkg dependencies. # - so we won't even use the tcllib debug pkg here diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm index 05e8724f..87449fb3 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm @@ -566,6 +566,7 @@ namespace eval punk::mix::cli { #lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list # -- --- set installer [punkcheck::installtrack new $installername $punkcheck_file] + #set installer [punkcheck::installtrack new $installername $punkcheck_file stderr] ;#with debugchannel $installer set_source_target $srcdir $basedir set event [$installer start_event $config] # -- --- @@ -601,9 +602,9 @@ namespace eval punk::mix::cli { } #debug for issues on non-windows platforms. - if {$::tcl_platform(platform) ne "windows"} { - set is_interesting 1 - } + #if {$::tcl_platform(platform) ne "windows"} { + # set is_interesting 1 + #} if {$is_interesting} { puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath" @@ -662,6 +663,7 @@ namespace eval punk::mix::cli { # -max_depth -1 for no limit set build_installername pods_in_$current_source_dir set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck] + #set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel $build_installer set_source_target $current_source_dir/$modpath $buildfolder set build_event [$build_installer start_event $config] # -- --- diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm index 5635cd09..7d85e311 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) if {$linktarget_file_type eq "directory"} { - set linktarget [file readlink $cdtarget] - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { @@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs { link { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) - set linktarget [file readlink $cdtarget] if {$linktarget_file_type eq "directory"} { - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index dc8c765c..e89997c5 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -2167,8 +2167,13 @@ y" {return quirkykeyscript} puts stdout "leaving $target" puts stdout "call $commandstring\x1b\[m" - puts stdout "result:" - puts stdout $result + if {$code == 0} { + puts stdout "result:" + puts stdout $result + } else { + puts stdout "error message:" + puts stdout $::errorInfo + } puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset set cmdtype [dict get $linedict $target cmdtype] @@ -2647,6 +2652,8 @@ y" {return quirkykeyscript} upvar ::punk::ns::linedict linedict set ::punk::ns::linedict [::tcl::dict::create] + set body_cache [tcl::dict::create] + set resolved_targets [list] foreach tgt $targets { set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]] @@ -5018,6 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs + puts "punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -6618,18 +6626,19 @@ y" {return quirkykeyscript} separately calling 'info args ' 'info body ' etc. The body may display with an additional - comment inserted to display information such as the + comment inserted above the proc line to display information such as the namespace origin. Such a comment begins with #corp#. - Returns a list: proc - (as long as any syntax highlighter is written to - avoid breaking the structure. e.g by avoiding the - insertion of ANSI between an escaping backslash and - its target character) + Returns a string: proc If the output is to be used as a script to regenerate a procedure, '-syntax none' should be used to avoid ANSI colours, or the resulting arglist and body should be run through 'ansistrip'. + + (any syntax highlighter should be written to + avoid breaking the structure. e.g by avoiding the + insertion of ANSI between an escaping backslash and + its target character) " @opts #todo - make definition @dynamic - load highlighters as functions? @@ -6647,7 +6656,13 @@ y" {return quirkykeyscript} "Whether to replace tabs in the body with spaces or a visible Unicode symbol." -ranges -type indexset -default "0..end" -help\ "comma delimited set of line ranges. - Restrict output to the specified line ranges of the body. Lines are numbered starting at 1." + Restrict output to the specified line ranges of the body. Lines are numbered starting at 1. + For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body. + The special index 0 is used to specify the line before the first line of the body, + which is where the #corp# info comment is placed if it exists. + So the default range 0..end includes the info comment and all lines of the body. + Specifying -ranges 1..5,0 would include the info comment at the end of the output. + " -syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\ -choicelabels { none @@ -6690,11 +6705,6 @@ y" {return quirkykeyscript} set indent [string repeat " " $tw] ;#match #set indent [string repeat " " $tw] ;#A more sensible default for code - review - if {[info exists ::auto_index($path)]} { - set infoheader "\n${indent}#corp# auto_index $::auto_index($path)" - } else { - set infoheader "" - } #we want to handle edge cases of commands such as "" or :x #various builtins such as 'namespace which' won't work @@ -6753,13 +6763,29 @@ y" {return quirkykeyscript} return [list alias {*}$alias] } } - if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} { - append infoheader \n "${indent}#corp# namespace origin $origin" - } - if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { - append infoheader \n + #-------------------------------------------------------------------------- + if {[info exists ::auto_index($path)]} { + #set infoheader "${indent}#corp# auto_index $::auto_index($path)" + set infoheader "#corp# auto_index $::auto_index($path)" + } else { + set infoheader "" } + #puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'" + #if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {} + if {$origin ne $targetcmd} { + #append infoheader "${indent}#corp# namespace origin $origin" + append infoheader "#corp# namespace origin $origin" + } + if {$infoheader ne "" && $syntax eq "basic"} { + set infoheader [ansiwrap green $infoheader] + } + #if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { + # append infoheader \n + #} + #-------------------------------------------------------------------------- + + set body "" #set bodytext [info body $origin] #relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname @@ -6851,47 +6877,126 @@ y" {return quirkykeyscript} } } - if {$ranges ni {"0..end" ".." "0.."}} { - set lines [split $body \n] - set linecount [llength $lines] + set lines [split $body \n] + set linecount [llength $lines] + + + #------------------------------------------------------------------------------------------------ + #When we resolve our 1-based indexset - the zero index used to specify the info comment is lost. + #we need to search for it manually and add it back in if it's in the specified ranges. + set rangelist [split $ranges ,] + set info_positions [list] + set lnum 0 + foreach range $rangelist { + lassign [split $range ..] start _ end + set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range] + if {$start eq "0"} { + lappend info_positions $lnum + } + incr lnum [llength $r_indices] + #if {$end eq "0"} { + # #ignore + #} + } + #puts "info_positions: $info_positions" + #------------------------------------------------------------------------------------------------ + + + set body "" + if {[lindex $info_positions 0] == 0 && $infoheader ne ""} { + append body "$infoheader" \n + } + + if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} { set w [string length $linecount] set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges] - set body "" + set outputlines [llength $indices] + if {$do_ln} { - set n 0 foreach idx $indices { - append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + if {$idx == 1} { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}" + } else { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } else { foreach idx $indices { - append body [lindex $lines $idx-1] \n + if {$idx == 1} { + set ln1 "proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body [lindex $lines end] "\}" + } else { + append body [lindex $lines $idx-1] \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } + #no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match + if {[string index $body end] eq "\n"} { + set body [string range $body 0 end-1] + } } else { + #range was specified in a standard way to mean 'all lines' + set outputlines $linecount if {$do_ln} { - set linebody "" - set n 0 - set lines [split $body \n] - set linecount [llength $lines] set w [string length $linecount] - foreach ln $lines { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + set n 2 + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$lnc[format %${w}s $n]$lnr $ln" incr n - append linebody "$lnc[format %${w}s $n]$lnr $ln" \n } - set body [string range $linebody 0 end-1] - #set body $linebody + if {$linecount > 1} { + append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody + } else { + set ln1 "proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$ln" + } + if {$linecount > 1} { + append linebody \n "[lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody } } - - - if {$is_highlighted} { - #ansi colourised items in list format may not always have desired string representation (list escaping can occur) - #return as a string - which may not be a proper Tcl list! - return "proc $resolved {$argl} {\n$infoheader$body\n}" - } else { - list proc $resolved $argl $infoheader$body + #ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end? + if {[lindex $info_positions end] > 0} { + if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} { + append body "\n$infoheader" + } } + return $body } diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm index c8f7fc3a..a7aff5a6 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -21,6 +21,7 @@ unset stdin_info # ----------------------------------- + #------------------------------------------------------------------------------------- if {[package provide punk::libunknown] eq ""} { #maintenance - also in src/vfs/_config/punk_main.tcl @@ -162,6 +163,13 @@ namespace eval punk::repl { #todo - key on shell/subshell tsv::set repl runchunks-0 [list] ;#last_run_display + variable frametype + set frametype ascii; #conservative default + #if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + # if {$testcharwidth == 1} { + # set frametype light + # } + #} variable debug_repl 0 variable signal_control_c 0 @@ -991,6 +999,8 @@ namespace eval punk::repl::class { dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered] } method add_chunk {chunk} { + upvar ::punk::repl::frametype frametype + #we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw] @@ -1065,7 +1075,7 @@ namespace eval punk::repl::class { append debug \n [showdict $dinfo] append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col" package require textblock - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] + set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug] if {![punk::console::vt52]} { catch {punk::console::move_emitblock_return $debug_first_row 1 $debug} } else { @@ -1148,7 +1158,7 @@ namespace eval punk::repl::class { set debug "add_chunk$i" append debug \n $mergedinfo append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]" - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] + set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug] #catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug} set result [dict get $mergedinfo result] @@ -1816,6 +1826,8 @@ proc punk::repl::console_debugview {editbuf consolewidth args} { } package require textblock variable debug_repl + variable frametype + if {$debug_repl <= 0} { return [dict create width 0 height 0 topleft {}] } @@ -1850,11 +1862,11 @@ proc punk::repl::console_debugview {editbuf consolewidth args} { } set debug_height [expr {[llength $lines]+2}] ;#framed height } errM]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] set debug_height [textblock::height $info] } else { #treat as ephemeral (unreusable) frames due to varying width & height - therefore set -buildcache 0 - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] } set debug_width [textblock::widthtopline $info] @@ -1881,6 +1893,8 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { package require textblock upvar ::repl::editbuf_list editbuf_list + variable frametype + set defaults {-row 10 -rightmargin 0} set opts [dict merge $defaults $args] set opt_row [dict get $opts -row] @@ -1894,14 +1908,14 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { set info [punk::lib::list_as_lines $lines] } } editbuf_error]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] } else { set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]" append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]" set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]" set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]" set info [a+ green bold]$row1\n$row2[a]\n$info - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] } set editbuf_width [textblock::widthtopline $info] set spacepatch [textblock::block $editbuf_width 2 " "] @@ -1915,8 +1929,10 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { return [dict create width $editbuf_width] } + proc punk::repl::console_controlnotification {message consolewidth consoleheight args} { package require textblock + variable frametype set defaults {-bottommargin 0 -rightmargin 0} set opts [dict merge $defaults $args] set opt_bottommargin [dict get $opts -bottommargin] @@ -1924,8 +1940,14 @@ proc punk::repl::console_controlnotification {message consolewidth consoleheight set messagelines [split $message \n] set message [lindex $messagelines 0] ;#only allow single line set info "[a+ bold red]$message[a]" - set hlt [dict get [textblock::framedef light] hlt] - set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] + + if {$frametype eq "ascii"} { + set box [textblock::frame -type ascii -checkargs 0 -boxmap [list tlc + trc +] -title $message -height 1] + } else { + set hlt [dict get [textblock::framedef light] hlt] + set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] + } + set notification_width [textblock::widthtopline $info] set box_offset [expr {$consolewidth - $notification_width - $opt_rightmargin}] set row [expr {$consoleheight - $opt_bottommargin}] @@ -1939,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config } else { set is_vt52 0 } + variable codethread variable loopinstance incr loopinstance @@ -2069,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config set chunk "\b\x7f\b\x7f" } elseif {$chunk eq "\x1c"} { #ctrl-bslash + #This is commonly used in terminals as a 'harder' ctrl-c. #try to brutally terminate process #attempt to leave terminal in a reasonable state - mode line ;#may be aliased to ::repl::interphelpers::mode - after 250 {exit 42} + punk::console::mode line + #for now - exit with small delay for tidyup + after 1000 {exit 43} return } elseif {$chunk eq "\x1a"} { - #for now - exit with small delay for tidyup #ctrl-z #::punk::repl::handler_console_control "ctrl-z_via_rawloop" - if {[catch {punk::console::mode line}]} { - #REVIEW - interp eval code {punk::console::mode line} + #JMN + #set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}] + set iname $::punk::repl::codethread::replthread_interp + #only the highest level subshell has an interp name of empty string (lower levels are named 'code') + if {$iname eq ""} { + punk::console::mode line } - after 1000 {exit 43} + after 250 [list thread::send $codethread [list interp eval code {quit 42}]] return } @@ -2375,6 +2402,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #set commandstr "set ::punk::repl::debug_repl" set commandstr "" } + + + if {$::punk::repl::debug_repl > 100} { proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { set p %p% @@ -2394,12 +2424,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs debugreport $clearance$p[string map [list \n \n$p] $msg] }] set info "" - append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" - append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" + append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" + append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n" set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]] append info "lastrunchunks\n" - append info "chunks: [llength $lastrunchunks]\n" - append info "namespace: $::punk::nav::ns::ns_current" + append info "chunks : [llength $lastrunchunks]\n" + #JMN + set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]] + append info "codethread namespace: $codethread_ns\n" + append info "stdinlines : [llength $stdinlines] lines\n" + foreach ln $stdinlines { + append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n" + } + append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n" + #append info "namespace: $::punk::nav::ns::ns_current" debug_repl_emit $info } else { proc debug_repl_emit {msg} {return} @@ -2444,7 +2482,6 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config # lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] #} - variable codethread variable codethread_cond variable codethread_mutex @@ -2873,8 +2910,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config if {[llength $waiting]} { set c [lindex $waiting end] } else { - #set c " " - set c \u240a + set c \u240a ;#unicode linefeed symbol. } doprompt ">$c " } @@ -2986,16 +3022,17 @@ namespace eval repl { set codethread_mutex [thread::mutex create] - set scriptmap [list %args% [list $opts] \ - %argv0% [list $::argv0] \ - %argv% [list $::argv] \ - %argc% [list $::argc] \ - %replthread% [thread::id] \ - %replthread_cond% $codethread_cond \ - %replthread_interp% [list $opt_callback_interp] \ - %tmlist% [list [tcl::tm::list]] \ - %autopath% [list $::auto_path] \ - %lib_epoch% [list $::punk::libunknown::epoch]\ + set scriptmap [list %args% [list $opts] {*}{ + } %argv0% [list $::argv0] {*}{ + } %argv% [list $::argv] {*}{ + } %argc% [list $::argc] {*}{ + } %replthread% [thread::id] {*}{ + } %replthread_cond% $codethread_cond {*}{ + } %replthread_interp% [list $opt_callback_interp] {*}{ + } %tmlist% [list [tcl::tm::list]] {*}{ + } %autopath% [list $::auto_path] {*}{ + } %lib_epoch% [list $::punk::libunknown::epoch] {*}{ + } ] #scriptmap applied at end to satisfy silly editor highlighting. set init_script { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm index e147b4d1..eb439c6e 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -81,16 +81,18 @@ namespace eval punkcheck { } return $record_list } - proc save_records_to_file {recordlist punkcheck_file {trigger {}}} { - if {$trigger ne ""} { - puts stderr "\x1b\[36mSaving [llength $recordlist] records to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m" - } + proc save_records_to_file {recordlist punkcheck_file {trigger {}} {debugchannel ""}} { set newtdl [punk::tdl::prettyprint $recordlist] set linecount [llength [split $newtdl \n]] + + if {$debugchannel ne "" && $trigger ne ""} { + puts $debugchannel "\x1b\[36mSaving [llength $recordlist] records as $linecount lines to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m" + } #puts stdout $newtdl set fd [open $punkcheck_file w] chan configure $fd -translation binary puts -nonewline $fd $newtdl + flush $fd close $fd return [list recordcount [llength $recordlist] linecount $linecount] } @@ -170,8 +172,10 @@ namespace eval punkcheck { variable o_path_cksum_cache variable o_fileset_record variable o_installer ;#parent object + variable o_debugchannel constructor {installer rel_sourceroot rel_targetroot args} { set o_installer $installer + set o_debugchannel [$installer get_debugchannel] set o_operation_start_ts "" set o_path_cksum_cache [dict create] set o_operation "" @@ -321,7 +325,7 @@ namespace eval punkcheck { set extractioninfo [punkcheck::recordlist::extract_or_create_fileset_record $o_targets $record_list] set o_fileset_record [dict get $extractioninfo record] - set record_list [dict get $extractioninfo recordset] + set record_list [dict get $extractioninfo recordset] ;#if fileset wasn't present, same as original record_list, otherwise full recordset with the fileset record removed, ready for reinsertion. set isnew [dict get $extractioninfo isnew] set oldposition [dict get $extractioninfo oldposition] unset extractioninfo @@ -530,7 +534,9 @@ namespace eval punkcheck { variable o_record_list variable o_active_event variable o_events - constructor {installername punkcheck_file} { + variable o_debugchannel + constructor {installername punkcheck_file {debugchannel ""}} { + set o_debugchannel $debugchannel set o_active_event "" set o_name $installername @@ -539,6 +545,8 @@ namespace eval punkcheck { set o_targetroot "" set o_rel_sourceroot "" set o_rel_targetroot "" + set o_record_list [list] + #todo - validate punkcheck file location further?? set punkcheck_folder [file dirname $o_checkfile] if {![file isdirectory $punkcheck_folder]} { @@ -546,6 +554,55 @@ namespace eval punkcheck { } my load_all_records + if {![llength $o_record_list] && $o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[32mNo existing records found in punkcheck file '$o_checkfile' for installer '$installername'. Starting with empty record list.\x1b\[m" + } else { + #verify no duplicate installer records for this installer. + #JMN + set sanity_dict [dict create] + set insane "" + foreach rec $o_record_list { + if {[dict get $rec tag] eq "INSTALLER"} { + set name [dict get $rec -name] + if {[dict exists $sanity_dict $name]} { + #todo - warn - duplicate record for same targetlist - shouldn't happen as we should be using get_file_record to find existing records + if {$o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[31mpunkcheck installtrack - multiple INSTALLER records with same name '$name'\x1b\[m" + } + set insane "$name" + break + } + dict set sanity_dict $name {} + } + } + if {$insane ne ""} { + set msg "Sanity check: punkcheck file '$o_checkfile' contains multiple records for INSTALLER -name '$insane'." + append msg \n "This may indicate a problem such as multiple concurrent installtrack instances using the same punkcheck file," + append msg \n " or a previous installtrack instance that did not complete properly." + append msg \n " Do you want to DELETE the .punkcheck file?" + append msg \n " It is safe to delete .punkcheck files, at the cost of loss of history and checksums used to optimize installs." + append msg \n " They are a record of installation events and checksums used to avoid unnecessary reinstalls." + append msg \n " If not confirmed, an error will be raised - likely aborting the current operation." + append msg \n "confirm deletion and continue by regenerating the file, by typing the 3 letters: 'yes'." + set answer [punk::lib::askuser $msg] + if {[string tolower $answer] ne "yes"} { + error "Failing due to sanity check failure. User did not confirm with 'yes'." + } + if {[file exists $o_checkfile] && [file isfile $o_checkfile]} { + file delete $o_checkfile + } + if {[file exists $o_checkfile]} { + error "Failed to delete punkcheck file '$o_checkfile' after sanity check failure. Please investigate and resolve the issue before proceeding." + } + set o_record_list [list] + } else { + if {$o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[32mSanity check passed: no duplicate INSTALLER records found for installer '$installername' in punkcheck file '$o_checkfile'.\x1b\[m" + } + } + unset sanity_dict + } + set resultinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list] set existing_header_posn [dict get $resultinfo position] if {$existing_header_posn == -1} { @@ -580,6 +637,9 @@ namespace eval punkcheck { method get_checkfile {} { return $o_checkfile } + method get_debugchannel {} { + return $o_debugchannel + } #call set_source_target before calling start_event/end_event #each event can have different source->target pairs - but may often have same, so set on installtrack as defaults. Only persisted in event records. @@ -2164,10 +2224,10 @@ namespace eval punkcheck { return [dict create changed $changed unchanged $unchanged] } - #assume only one for name - use first encountered + #assume only one for name - use first encountered? proc get_installer_record {name record_list} { set posn 0 - set found_posn -1 + set found_posns [list] set record "" #puts ">>>> checking [llength $record_list] punkcheck records" foreach rec $record_list { @@ -2175,12 +2235,20 @@ namespace eval punkcheck { if {[dict get $rec -name] eq $name} { set found_posn $posn set record $rec - break + lappend found_posns $posn } } incr posn } - return [list position $found_posn record $record] + if {[llength $found_posns] > 1} { + error "punkcheck::recordlist::get_installer_record - multiple installer records with name '$name' found at positions $found_posns" + } elseif {[llength $found_posns] == 0} { + return [list position -1 record ""] + } else { + #single record found + return [list position [lindex $found_posn 0] record $record] + } + } proc new_installer_record {name args} { @@ -2374,7 +2442,8 @@ namespace eval punkcheck { set fileset_record [dict create tag FILEINFO -targets $relative_target_paths body {}] } else { #set recordset [lreplace $recordset[unset recordset] $existing_posn $existing_posn] - set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn] + #set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn] + ledit recordset $existing_posn $existing_posn set isnew 0 set fileset_record [dict get $fetch_record_result record] } diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm index 6d357bd7..42ae92cd 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm @@ -2728,13 +2728,13 @@ namespace eval shellfilter { ::shellfilter::log::write $runtag "checking for redirections in $commandlist" #sometimes we see a redirection without a following space e.g >C:/somewhere #normalize - switch -regexp -- $lastitem\ - {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - }\ - {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} diff --git a/src/bootsupport/modules/shellfilter-0.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm similarity index 86% rename from src/bootsupport/modules/shellfilter-0.2.tm rename to src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm index 8017d3f5..a0fa060a 100644 --- a/src/bootsupport/modules/shellfilter-0.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm @@ -4,7 +4,7 @@ #Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. #To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. #e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. -#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway # - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work # (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) # @@ -18,16 +18,19 @@ tcl::namespace::eval shellfilter::log { proc disable {} { variable is_enabled set is_enabled 0 - proc ::shellfilter::log::open {tag settingsdict} {} - proc ::shellfilter::log::write {tag msg} {} - proc ::shellfilter::log::write_sync {tag msg} {} - proc ::shellfilter::log::close {tag} {} + proc ::shellfilter::log::open {tag settingsdict} {} + proc ::shellfilter::log::write {tag msg} {} + proc ::shellfilter::log::write_sync {tag msg} {} + proc ::shellfilter::log::close {tag} {} } proc enable {} { variable is_enabled set is_enabled 1 - #'tag' is an identifier for the log source. + #'tag' is an identifier for the log source. + #(well.. really it's a common *target* of file and/or syslog host:port which can be written to from any thread that uses the tag) + #the terminology here is kinda ratshit. + # each tag will use it's own thread to write to the configured log target proc ::shellfilter::log::open {tag {settingsdict {}}} { upvar ::shellfilter::sources sourcelist @@ -43,7 +46,7 @@ tcl::namespace::eval shellfilter::log { lappend sourcelist $tag } - #note new_worker + #review new_worker/assign_worker? set worker_tid [shellthread::manager::new_worker $tag $settingsdict] #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" return $worker_tid @@ -76,6 +79,7 @@ tcl::namespace::eval shellfilter::log { if {![string length $is_open_required]} { return $allow_adhoc_tags } else { + #why not use string is boolean? set truevalues [list y yes true 1] set falsevalues [list n no false 0] if {[string tolower $is_open_required] in $truevalues} { @@ -115,7 +119,7 @@ namespace eval shellfilter::pipe { #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. - chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] #puts stderr "worker_tid: $worker_tid" @@ -137,6 +141,7 @@ namespace eval shellfilter::pipe { chan configure $worker_chan -buffering [dict get $settingsdict -buffering] chan configure $program_chan -buffering [dict get $settingsdict -buffering] + #review chan configure $program_chan -blocking 0 chan configure $worker_chan -blocking 0 set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] @@ -158,14 +163,14 @@ namespace eval shellfilter::chan { oo::class create var { variable o_datavar - variable o_trecord + variable o_trecord variable o_enc variable o_is_junction constructor {tf} { set o_trecord $tf set o_enc [dict get $tf -encoding] set settingsdict [dict get $tf -settings] - set varname [dict get $settingsdict -varname] + set varname [dict get $settingsdict -varname] set o_datavar $varname if {[dict exists $tf -junction]} { set o_is_junction [dict get $tf -junction] @@ -192,7 +197,7 @@ namespace eval shellfilter::chan { return "" } method meta_is_redirection {} { - return $o_is_junction + return $o_is_junction } method meta_buffering_supported {} { return [list line full none] @@ -202,12 +207,12 @@ namespace eval shellfilter::chan { #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? oo::class create tee_grep_to_var { variable o_datavar - variable o_lastxlines - variable o_trecord + variable o_lastxlines + variable o_trecord variable o_grepfor variable o_prelines variable o_postlines - variable o_postcountdown + variable o_postcountdown variable o_enc variable o_is_junction constructor {tf} { @@ -240,8 +245,8 @@ namespace eval shellfilter::chan { # return ? #} method write {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - set lastx $o_lastxlines + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines lappend o_lastxlines $logdata if {$o_postcountdown > 0} { @@ -399,7 +404,7 @@ namespace eval shellfilter::chan { return "" } method read {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set logdata [tcl::encoding::convertfrom $o_enc $bytes] #::shellfilter::log::write $o_logsource $logdata puts -nonewline $o_localchan $logdata return $bytes @@ -415,14 +420,14 @@ namespace eval shellfilter::chan { set inputbytes $o_encbuf$bytes set o_encbuf "" set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { incr tail_offset } if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { set stringdata "" set o_encbuf $inputbytes @@ -432,7 +437,7 @@ namespace eval shellfilter::chan { #::shellfilter::log::write $o_logsource $logdata puts -nonewline $o_localchan $stringdata #return $bytes - return [string range $inputbytes 0 end-$tail_offset] + return [::tcl::string::range $inputbytes 0 end-$tail_offset] } #a tee is not a redirection - because data still flows along the main path method meta_is_redirection {} { @@ -441,9 +446,9 @@ namespace eval shellfilter::chan { } oo::class create tee_to_log { - variable o_tid + variable o_tid variable o_logsource - variable o_trecord + variable o_trecord variable o_enc variable o_encbuf variable o_is_junction @@ -475,7 +480,7 @@ namespace eval shellfilter::chan { # post any events } method read {ch bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set logdata [tcl::encoding::convertfrom $o_enc $bytes] ::shellfilter::log::write $o_logsource $logdata return $bytes } @@ -486,27 +491,27 @@ namespace eval shellfilter::chan { return $o_encbuf } method write {ch bytes} { - #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes set o_encbuf "" set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { incr tail_offset } if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { set stringdata "" set o_encbuf $inputbytes return "" } } - ::shellfilter::log::write $o_logsource $logdata - #return $bytes - return [string range $inputbytes 0 end-$tail_offset] + set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] + ::shellfilter::log::write $o_logsource $stringdata + return $bytes } method meta_is_redirection {} { return $o_is_junction @@ -546,18 +551,18 @@ namespace eval shellfilter::chan { # return ? #} method write {transform_handle bytes} { - #set logdata [encoding convertfrom $o_enc $bytes] + #set logdata [encoding convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes set o_encbuf "" set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { incr tail_offset } if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { set stringdata "" set o_encbuf $inputbytes @@ -574,7 +579,7 @@ namespace eval shellfilter::chan { } } - #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! @@ -586,10 +591,10 @@ namespace eval shellfilter::chan { constructor {tf} { package require punk::ansi set o_trecord $tf - set o_enc [dict get $tf -encoding] + set o_enc [::tcl::dict::get $tf -encoding] set o_encbuf "" - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] + if {[::tcl::dict::exists $tf -junction]} { + set o_is_junction [::tcl::dict::get $tf -junction] } else { set o_is_junction 0 } @@ -609,7 +614,7 @@ namespace eval shellfilter::chan { return "" } method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] set outstring [punk::ansi::ansistrip $instring] return [encoding convertto $o_enc $outstring] } @@ -618,7 +623,7 @@ namespace eval shellfilter::chan { } #method write {transform_handle bytes} { # #broken due to occasional unexpected byte sequence - # set instring [encoding convertfrom $o_enc $bytes] + # set instring [encoding convertfrom $o_enc $bytes] # set outstring [punk::ansi::ansistrip $instring] # return [encoding convertto $o_enc $outstring] #} @@ -629,14 +634,14 @@ namespace eval shellfilter::chan { set inputbytes $o_encbuf$bytes set o_encbuf "" set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { incr tail_offset } if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { set stringdata "" set o_encbuf $inputbytes @@ -654,7 +659,7 @@ namespace eval shellfilter::chan { #a test oo::class create reconvert { - variable o_trecord + variable o_trecord variable o_enc constructor {tf} { set o_trecord $tf @@ -669,14 +674,14 @@ namespace eval shellfilter::chan { method watch {transform_handle events} { } method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] set outstring $instring return [encoding convertto $o_enc $outstring] } method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] set outstring $instring @@ -689,7 +694,7 @@ namespace eval shellfilter::chan { } } - + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. #It can be useful for test/debugging #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi @@ -750,6 +755,8 @@ namespace eval shellfilter::chan { #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" set buf $o_buffered$chunk set emit "" + #Note 8-bit csi \x9b has already been mapped in the chunk to 7-bit form \x1b\[ by the caller - so we only need to check for \x1b here + #(under review - ideally we might not want to normalize 8-bit to 7-bit in a channel transform)) if {[string last \x1b $buf] >= 0} { #detect will detect ansi SGR and gron groff and other codes #REVIEW - ta::detect won't detect SOS without paired ST for things like PM @@ -786,33 +793,36 @@ namespace eval shellfilter::chan { #} set c1c2 [tcl::string::range $code 0 1] - set leadernorm [tcl::string::range [tcl::string::map [list\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\( 7GFX\ - ] $c1c2] 0 3] + set leadernorm [tcl::string::range [tcl::string::map [list {*}{ + \x1b\[ 7CSI + \x9b 8CSI + \x1b\( 7GFX + }] $c1c2] 0 3] switch -- $leadernorm { 7CSI - 8CSI { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #todo - make caching is_sgr method - set dup_posns [lsearch -all -exact $o_codestack $code] - set o_codestack [lremove $o_codestack {*}$dup_posns] - lappend o_codestack $code - } else { - + set code_endswith_m [expr {[tcl::string::index $code end] eq "m"}] + if {$code_endswith_m} { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + #review this normalizing of reset to a single form. + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } } + } 7GFX { switch -- [tcl::string::index $code 2] { "0" { - set o_gx_state on + set o_gx_state on } "B" { - set o_gx_state off + set o_gx_state off } } } @@ -862,7 +872,7 @@ namespace eval shellfilter::chan { } else { #REVIEW - this holding a buffer without emitting as we go is ugly. # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. - # - we'd then need to detect the appropriate close to restart splitting and codestacking + # - we'd then need to detect the appropriate close to restart splitting and codestacking # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. @@ -892,7 +902,7 @@ namespace eval shellfilter::chan { set o_buffered "" } } else { - set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code + set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code #most opening sequences are 1,2 or 3 chars - review? set open_sequence_detected [punk::ansi::ta::detect_open $buf] if {$possible_code_len > 10 && !$open_sequence_detected} { @@ -993,7 +1003,7 @@ namespace eval shellfilter::chan { puts stdout "" set emit [tcl::encoding::convertto $o_enc $o_buffered] set o_buffered "" - return $emit + return $emit } method flush {transform_handle} { #puts stdout "" @@ -1024,6 +1034,21 @@ namespace eval shellfilter::chan { return "" } } + + #------------------------------------------------------ + # REVIEW + #Trackcodes logic is primarily designed for 7-bit codes + #It would be complex for it to support 8-bit as well + #- we can do a simple pre-map to convert 8-bit CSI to 7-bit CSI before processing + #we already normalize things like resets to a single 7-bit form anyway. + #review - is there a need for an ansiwrap channel that preserves 8-bit codes? + #8-bit are rarely used these days - and many terminals don't support them. + #We could take the view here that we should understand them but not emit them in general. + #Nonetheless - converting them on a channel transform like this is potentially suprising in some circumstances, + #and we don't necessarily know the intent of both the producer and consumer of the stream. + set stringdata [string map [list \x9b \x1b\[ ] $stringdata] + #------------------------------------------------------ + set streaminfo [my Trackcodes $stringdata] set emit [dict get $streaminfo emit] @@ -1063,7 +1088,7 @@ namespace eval shellfilter::chan { return $o_is_junction } } - #todo - something + #todo - something oo::class create rebuffer { variable o_trecord variable o_enc @@ -1080,14 +1105,14 @@ namespace eval shellfilter::chan { method watch {transform_handle events} { } method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] set outstring $instring return [encoding convertto $o_enc $outstring] } method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] #set outstring [string map [list \n ] $instring] set outstring $instring @@ -1102,9 +1127,9 @@ namespace eval shellfilter::chan { } } - #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence oo::class create tounix { - variable o_trecord + variable o_trecord variable o_enc variable o_last_char_was_cr variable o_is_junction @@ -1129,14 +1154,14 @@ namespace eval shellfilter::chan { } #don't use read method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] set outstring $instring return [encoding convertto $o_enc $outstring] } method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] #set outstring [string map [list \n ] $instring] if {$o_last_char_was_cr} { @@ -1144,7 +1169,7 @@ namespace eval shellfilter::chan { } set outstring [string map {\r\n \n} $instring] - set lastchar [string range $outstring end end] + set lastchar [string range $outstring end end] if {$lastchar eq "\r"} { set o_last_char_was_cr 1 set outstring [string range $outstring 0 end-1] @@ -1165,7 +1190,7 @@ namespace eval shellfilter::chan { } #write to handle case where line-endings already \r\n too oo::class create towindows { - variable o_trecord + variable o_trecord variable o_enc variable o_last_char_was_cr variable o_is_junction @@ -1190,14 +1215,14 @@ namespace eval shellfilter::chan { } #don't use read method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] set outstring $instring return [encoding convertto $o_enc $outstring] } method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] + set instring [encoding convertfrom $o_enc $bytes] #set outstring [string map [list \n ] $instring] if {$o_last_char_was_cr} { @@ -1208,7 +1233,7 @@ namespace eval shellfilter::chan { set outstring [string map {\n \r\n} $outstring] set outstring [string map {\uFFFF \r\n} $outstring] - set lastchar [string range $outstring end end] + set lastchar [string range $outstring end end] if {$lastchar eq "\r"} { set o_last_char_was_cr 1 set outstring [string range $outstring 0 end-1] @@ -1224,7 +1249,7 @@ namespace eval shellfilter::chan { } oo::define towindows { method meta_is_redirection {} { - return $o_is_junction + return $o_is_junction } } @@ -1234,17 +1259,17 @@ namespace eval shellfilter::chan { # ---------------------------------------------------------------------------- #review float/sink metaphor. #perhaps something with the concept of upstream and downstream? -#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. ## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. #upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) #This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. -#The idea would be that whether input or output +#The idea would be that whether input or output # upstream additions go to the side closest to the datasource -# downstream additions go furthest from the datasource +# downstream additions go furthest from the datasource # - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. -# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. -# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) -# neutral-upstream goes to the datasource side of the neutral-upstream list. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. # No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. # No 'neutral-downstream' to reduce complexity. # downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. @@ -1268,7 +1293,7 @@ namespace eval shellfilter::chan { # # ## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. -## +## namespace eval shellfilter::stack { namespace export {[a-z]*} namespace ensemble create @@ -1287,7 +1312,7 @@ namespace eval shellfilter::stack { } proc item_tophandle {pipename} { variable pipelines - set handle "" + set handle "" if {[dict exists $pipelines $pipename stack]} { set stack [dict get $pipelines $pipename stack] set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? @@ -1320,7 +1345,7 @@ namespace eval shellfilter::stack { } set stack [dict get $pipelines $k stack] if {![llength $stack]} { - set stackinfo "" + set stackinfo "" } else { set tbl_inner [textblock::class::table new] $tbl_inner configure -show_edge 0 @@ -1409,7 +1434,7 @@ namespace eval shellfilter::stack { } return $tableprefix$table } - #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir proc _get_stack_floaters {stack} { set floaters [list] foreach t [lreverse $stack] { @@ -1460,9 +1485,9 @@ namespace eval shellfilter::stack { } - #for input-channels ? + #for input-channels ? proc _get_stack_bottom_redirection {stack} { - set i 0 + set i 0 foreach t $stack { set obj [dict get $t -obj] if {[$obj meta_is_redirection]} { @@ -1497,7 +1522,7 @@ namespace eval shellfilter::stack { proc delete {pipename {wait 0}} { variable pipelines set pipeinfo [dict get $pipelines $pipename] - set deviceinfo [dict get $pipeinfo device] + set deviceinfo [dict get $pipeinfo device] set localchan [dict get $deviceinfo localchan] unwind $pipename @@ -1512,7 +1537,7 @@ namespace eval shellfilter::stack { #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? catch {chan close $localchan} } - #review - proc name clarity is questionable. remove_stackitem? + #review - proc name clarity is questionable. remove_stackitem? proc remove {pipename remove_id} { variable pipelines if {![dict exists $pipelines $pipename]} { @@ -1534,7 +1559,7 @@ namespace eval shellfilter::stack { if {[llength [dict get $t -aside]]} { set a [dict get $t -aside] if {[dict get $a -id] eq $remove_id} { - set asideposn $posn + set asideposn $posn break } } @@ -1545,7 +1570,7 @@ namespace eval shellfilter::stack { #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record set container [lindex $stack $asideposn] dict set container -aside {} - lset stack $asideposn $container + lset stack $asideposn $container dict set pipelines $pipename stack $stack } else { if {$idposn < 0} { @@ -1553,11 +1578,15 @@ namespace eval shellfilter::stack { puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" return 0 } - set removed_item [lindex $stack $idposn] + set removed_item [lindex $stack $idposn] #include idposn in poplist set poplist [lrange $stack $idposn end] - set stack [lreplace $stack $idposn end] + #set stack [lreplace $stack[set stack {}] $idposn end] + # 2026-05-19 + ledit stack $idposn end + + #pop all chans before adding anything back in! foreach p $poplist { chan pop $localchan @@ -1596,7 +1625,9 @@ namespace eval shellfilter::stack { variable pipelines set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] set poplist [lrange $stack $bottom_pop_posn end] - set stack [lreplace $stack $bottom_pop_posn end] + #set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] + # 2026-05-19 + ledit stack $bottom_pop_posn end set localchan [dict get $pipelines $pipename device localchan] foreach p [lreverse $poplist] { @@ -1605,7 +1636,7 @@ namespace eval shellfilter::stack { set transformname [dict get $transformrecord -transform] set transformsettings [dict get $transformrecord -settings] set obj [$transformname new $transformrecord] - set h [chan push $localchan $obj] + set h [chan push $localchan $obj] dict set transformrecord -handle $h dict set transformrecord -obj $obj dict set transformrecord -note "insert_transform" @@ -1625,14 +1656,14 @@ namespace eval shellfilter::stack { return $stack } - #fifo2 + #fifo2 proc new {pipename args} { variable pipelines if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { error "shellfilter::stack::new error: pipename '$pipename' already exists" } - set opts [dict merge {-settings {}} $args] + set opts [dict merge {-settings {}} $args] set defaultsettings [dict create -raw 1 -buffering line -direction out] set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] @@ -1687,7 +1718,7 @@ namespace eval shellfilter::stack { set deviceinfo [dict get $pipelines $pipename device] } - set id [get_next_counter $pipename] + set id [get_next_counter $pipename] set stack [dict get $pipelines $pipename stack] set localchan [dict get $deviceinfo localchan] @@ -1752,12 +1783,12 @@ namespace eval shellfilter::stack { set transformname [dict get $transform_record -transform] set transform_settings [dict get $transform_record -settings] set obj [$transformname new $transform_record] - set h [chan push $localchan $obj] + set h [chan push $localchan $obj] dict set transform_record -handle $h dict set transform_record -obj $obj dict set transform_record -note "insert_transform-with-aside" lappend stack $transform_record - #add back poplist *except* the one we transferred into -aside (if we were able) + # add back poplist *except* the one we transferred into -aside (if we were able) foreach p [lrange $poplist $put_aside end] { set t [dict get $p -transform] set tsettings [dict get $p -settings] @@ -1825,15 +1856,17 @@ namespace eval shellfilter { proc ::shellfilter::redir_channel_to_log {chan args} { variable sources - set default_logsettings [dict create \ - -tag redirected_$chan -syslog "" -file ""\ - ] + set default_logsettings [dict create {*}{ + -tag redirected_$chan + -syslog "" + -file "" + }] if {[dict exists $args -action]} { set action [dict get $args -action] } else { # action "sink" is a somewhat reasonable default for an output redirection transform - # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack - # also.. for stdin transform sink makes less sense.. + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. #todo - default "stack" instead of empty string set action "" } @@ -1865,9 +1898,9 @@ namespace eval shellfilter { set tag [dict get $settings -tag] if {$tag ne $tagprefix} { - error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" } - lappend sources ${tagprefix}stdout ${tagprefix}stderr + lappend sources ${tagprefix}stdout ${tagprefix}stderr set stdoutsettings $settings dict set stdoutsettings -tag ${tagprefix}stdout @@ -1878,7 +1911,7 @@ namespace eval shellfilter { set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] return [list $idout $iderr] - } + } #eg try: set v [list #a b c] #vs set v {#a b c} @@ -1888,8 +1921,8 @@ namespace eval shellfilter { string equal $l [list {*}$l] } - #return a dict keyed on numerical list index showing info about each element - # - particularly + #return a dict keyed on numerical list index showing info about each element + # - particularly # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) proc list_element_info {inputlist} { @@ -1977,24 +2010,24 @@ namespace eval shellfilter { dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 dict set iteminfo head_tail_chars [list $char_a $char_z] - set namemap [list \ - \r cr\ - \n lf\ - {"} doublequote\ - {'} singlequote\ - "`" backquote\ - "^" caret\ - \t tab\ - " " sp\ - "\[" lsquare\ - "\]" rsquare\ - "(" lbracket\ - ")" rbracket\ - "\{" lbrace\ - "\}" rbrace\ - \\ backslash\ - / forwardslash\ - ] + set namemap [list {*}{ + \r cr + \n lf + {"} doublequote + {'} singlequote + "`" backquote + "^" caret + \t tab + " " sp + "\[" lsquare + "\]" rsquare + "(" lbracket + ")" rbracket + "\{" lbrace + "\}" rbrace + \\ backslash + / forwardslash + }] if {[string length $char_a]} { set char_a_name [string map $namemap $char_a] } else { @@ -2017,30 +2050,31 @@ namespace eval shellfilter { #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list - #e.g {(^c:/my spacey/path^ >^somewhere^)} - #e.g {(blah (etc))}" - #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc - # Note that + # Note that #maintenance warning - duplication in branches for bracketed vs unbracketed! proc parse_cmd_brackets {str} { #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. - set wordwrappers [list \ - "\"" [list "\"" "\"" "\""]\ - {^} [list "\"" "\"" "^"]\ - "'" [list "'" "'" "'"]\ - "\{" [list "\{" "\}" "\}"]\ - {[} [list {[} {]} {]}]\ - ] ;#dict mapping start_character to {replacehead replacetail expectedtail} - set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + set wordwrappers [list {*}{ + } "\"" [list "\"" "\"" "\""] {*}{ + } {^} [list "\"" "\"" "^"] {*}{ + } "'" [list "'" "'" "'"] {*}{ + } "\{" [list "\{" "\}" "\}"] {*}{ + } {[} [list {[} {]} {]}] {*}{ + } + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. #puts "pb:$str" set in_bracket 0 set in_word 0 set word "" set result {} - set word_bdepth 0 + set word_bdepth 0 set word_bstack [list] set wordwrap "" ;#only one active at a time set bracketed_elements [dict create] @@ -2066,7 +2100,7 @@ namespace eval shellfilter { #can potentially close off a word - or start a new one if word-so-far is a shell-special if {$word in $shell_specials} { if {$char eq ")"} { - dict lappend bracketed_elements $in_bracket $word + dict lappend bracketed_elements $in_bracket $word set subresult [dict get $bracketed_elements $in_bracket] dict set bracketed_elements $in_bracket [list] incr in_bracket -1 @@ -2078,11 +2112,11 @@ namespace eval shellfilter { set word "" set in_word 0 } elseif {[regexp {[\s]} $char]} { - dict lappend bracketed_elements $in_bracket $word + dict lappend bracketed_elements $in_bracket $word set word "" set in_word 0 - } elseif {$char eq "("} { - dict lappend bracketed_elements $in_bracket $word + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word set word "" set in_word 0 incr in_bracket @@ -2090,7 +2124,7 @@ namespace eval shellfilter { #at end of shell-specials is another point to look for word started by a wordwrapper char #- expect common case of things like >^/my/path^ if {$char in [dict keys $wordwrappers]} { - dict lappend bracketed_elements $in_bracket $word + dict lappend bracketed_elements $in_bracket $word set word "" set in_word 1 ;#just for explicitness.. we're straight into the next word. set wordwrap $char @@ -2101,7 +2135,7 @@ namespace eval shellfilter { } } } else { - + if {$char eq ")"} { dict lappend bracketed_elements $in_bracket $word set subresult [dict get $bracketed_elements $in_bracket] @@ -2120,7 +2154,7 @@ namespace eval shellfilter { set in_word 0 } elseif {$char eq "("} { #ordinary word up-against and opening bracket - brackets are part of word. - incr word_bdepth + incr word_bdepth append word "(" } else { append word $char @@ -2132,12 +2166,12 @@ namespace eval shellfilter { "(" { incr word_bdepth lappend word_bstack $char - append word $char + append word $char } ")" { incr word_bdepth -1 set word_bstack [lrange $word_bstack 0 end-1] - append word $char + append word $char } default { #spaces and chars added to word as it's still in a bracketed section @@ -2147,10 +2181,10 @@ namespace eval shellfilter { } } } else { - + if {$char eq "("} { incr in_bracket - + } elseif {$char eq ")"} { set subresult [dict get $bracketed_elements $in_bracket] dict set bracketed_elements $in_bracket [list] @@ -2187,15 +2221,15 @@ namespace eval shellfilter { append word $char } } else { - + if {$word_bdepth == 0} { if {$word in $shell_specials} { if {[regexp {[\s]} $char]} { - lappend result $word + lappend result $word set word "" set in_word 0 - } elseif {$char eq "("} { - lappend result $word + } elseif {$char eq "("} { + lappend result $word set word "" set in_word 0 incr in_bracket @@ -2203,7 +2237,7 @@ namespace eval shellfilter { #at end of shell-specials is another point to look for word started by a wordwrapper char #- expect common case of things like >^/my/path^ if {$char in [dict keys $wordwrappers]} { - lappend result $word + lappend result $word set word "" set in_word 1 ;#just for explicitness.. we're straight into the next word. set wordwrap $char @@ -2213,7 +2247,7 @@ namespace eval shellfilter { append word $char } } - + } else { if {[regexp {[\s)]} $char]} { lappend result $word @@ -2291,7 +2325,7 @@ namespace eval shellfilter { } } - #proc dquote_if_not_bracketed/braced? + #proc dquote_if_not_bracketed/braced? #wrap in double quotes if not double-quoted proc dquote_if_not_dquoted {a} { @@ -2354,7 +2388,7 @@ namespace eval shellfilter { } - # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) # By the point run is called - any transforms should already be in place on the channels if they're needed. # The tees will be inline with none,some or all of those transforms depending on how the stack was configured # (upstream,downstream configured via -float,-sink etc) @@ -2371,18 +2405,18 @@ namespace eval shellfilter { ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" #flush stdout - #flush stderr + #flush stderr #adding filters with sink-aside will temporarily disable the existing redirection #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog - set defaults [dict create \ - -teehandle command \ - -outchan stdout \ - -errchan stderr \ - -inchan stdin \ - -tclscript 0 \ - ] + set defaults [dict create {*}{ + -teehandle command + -outchan stdout + -errchan stderr + -inchan stdin + -tclscript 0 + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -2415,14 +2449,14 @@ namespace eval shellfilter { #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] #set inpipechan [dict get $indeviceinfo localchan] - #NOTE:These transforms are not necessarily at the top of each stack! + #NOTE:These transforms are not necessarily at the top of each stack! #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this # If non os-level channel - the command can't be run with the redirection - # stderr/stdout can be run with non-os handles in the call - + # stderr/stdout can be run with non-os handles in the call - # but then it does introduce issues with terminal-detection and behaviour for stdout at least # # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. @@ -2436,7 +2470,7 @@ namespace eval shellfilter { #we need to catch errors - and ensure stack::remove calls occur. #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. # - if {!$is_script} { + if {!$is_script} { set experiment 0 if {$experiment} { try { @@ -2447,7 +2481,7 @@ namespace eval shellfilter { set exitinfo [list exitcode $exitcode] } } else { - if {[catch { + if {[catch { #run process with stdout/stderr/stdin or with configured channels #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] @@ -2467,7 +2501,14 @@ namespace eval shellfilter { set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] } } + #puts "shellfilter::run finished call" + #------------------------- + #warning - without flush stdout - we can get hang, but only on some terminals + # - mechanism for this problem not understood! + flush stdout + flush stderr + #------------------------- #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal #Remove execution-time Tees from stack @@ -2475,6 +2516,7 @@ namespace eval shellfilter { shellfilter::stack::remove stderr $id_err #shellfilter::stack::remove stderr $id_in + #puts stderr "shellfilter::run complete..." #chan configure stderr -buffering line #flush stdout @@ -2493,7 +2535,7 @@ namespace eval shellfilter { # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. # we should ensure the thread already exists early on if we really need logging here. - # + # #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" @@ -2519,7 +2561,7 @@ namespace eval shellfilter { if {$s ni $tidied_sources} { lappend remaining_sources $s } - } + } #set sources [concat $remaining_sources $tidytag] set sources $remaining_sources @@ -2533,23 +2575,23 @@ namespace eval shellfilter { # e.g set errchan [tcl::chan::null] # e.g chan push stdout [shellfilter::chan::var new ::some_var] proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { - set valid_flags [list \ - -timeout \ - -outprefix \ - -errprefix \ - -debug \ - -copytempfile \ - -outbuffering \ - -errbuffering \ - -inbuffering \ - -readprocesstranslation \ - -outtranslation \ - -stdinhandler \ - -outchan \ - -errchan \ - -inchan \ - -teehandle\ - ] + set valid_flags [list {*}{ + -timeout + -outprefix + -errprefix + -debug + -copytempfile + -outbuffering + -errbuffering + -inbuffering + -readprocesstranslation + -outtranslation + -stdinhandler + -outchan + -errchan + -inchan + -teehandle + }] set runtag shellfilter-run2 #JMN - load from config @@ -2587,24 +2629,24 @@ namespace eval shellfilter { error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" } #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order - #there may be data where line buffering is inappropriate, so it's configurable per std channel + #there may be data where line buffering is inappropriate, so it's configurable per std channel #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. - set defaults [dict create \ - -outchan stdout \ - -errchan stderr \ - -inchan stdin \ - -outbuffering none \ - -errbuffering none \ - -readprocesstranslation auto \ - -outtranslation lf \ - -inbuffering none \ - -timeout 900000\ - -outprefix ""\ - -errprefix ""\ - -debug 0\ - -copytempfile 0\ - -stdinhandler ""\ - ] + set defaults [dict create {*}{ + -outchan stdout + -errchan stderr + -inchan stdin + -outbuffering none + -errbuffering none + -readprocesstranslation auto + -outtranslation lf + -inbuffering none + -timeout 900000 + -outprefix "" + -errprefix "" + -debug 0 + -copytempfile 0 + -stdinhandler "" + }] @@ -2638,7 +2680,12 @@ namespace eval shellfilter { ::shellfilter::log::write $debugname " waitvar '$waitvar'" } lassign [chan pipe] rderr wrerr - chan configure $wrerr -blocking 0 + + #--------------- + #JMN 2025 + # e.g cannot run ansible cmdline tools if non-blocking + #chan configure $wrerr -blocking 0 + #------------------ set custom_stderr "" set lastitem [lindex $commandlist end] @@ -2653,7 +2700,7 @@ namespace eval shellfilter { switch -- [string trim $lastitem] { {&} { set name [lindex $commandlist 0] - #background execution - stdout and stderr from child still comes here - but process is backgrounded + #background execution - stdout and stderr from child still comes here - but process is backgrounded #FIX! - this is broken for paths with backslashes for example #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] set pidlist [exec {*}$commandlist] @@ -2664,7 +2711,7 @@ namespace eval shellfilter { set commandlist [lrange $commandlist 0 end-1] } default { - # 2> filename + # 2> filename # 2>> filename # 2>@ openfileid set redir2test [string range $lastitem 0 1] @@ -2683,16 +2730,16 @@ namespace eval shellfilter { #The problem here - is that we can't always know what was intended on the commandline regarding quoting ::shellfilter::log::write $runtag "checking for redirections in $commandlist" - #sometimes we see a redirection without a following space e.g >C:/somewhere + #sometimes we see a redirection without a following space e.g >C:/somewhere #normalize - switch -regexp -- $lastitem\ - {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - }\ - {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } - + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} @@ -2705,7 +2752,7 @@ namespace eval shellfilter { #don't use lassign or lrange on the element itself without checking first #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. - #lassign $lastitem redir redirtarget + #lassign $lastitem redir redirtarget #set commandlist [lrange $commandlist 0 end-1] # set itemchars [split $lastitem ""] @@ -2713,9 +2760,9 @@ namespace eval shellfilter { set lastchar [lindex $itemchars end] #NAIVE test for double quoted only! - #consider for example {"a" x="b"} + #consider for example {"a" x="b"} #testing first and last is not decisive - #We need to decide what level of drilling down is even appropriate here.. + #We need to decide what level of drilling down is even appropriate here.. #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) set head_tail_chars [list $firstchar $lastchar] set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] @@ -2734,11 +2781,11 @@ namespace eval shellfilter { set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below set redirtarget [lrange $innerwords 1 end] ;#all the rest } elseif {$doublequoted} { - ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" set inner [string range $lastitem 1 end-1] set innerwords [regexp -inline -all {\S+} $inner] set redir [lindex $innerwords 0] - set redirtarget [lrange $innerwords 1 end] + set redirtarget [lrange $innerwords 1 end] } else { set itemwords [regexp -inline -all {\S+} $lastitem] # e.g > c:\test becomes > {c:\test} @@ -2747,7 +2794,7 @@ namespace eval shellfilter { set redirtarget [lrange $itemwords 1 end] } set commandlist [lrange $commandlist 0 end-1] - + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { #unwrapped redirection #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list @@ -2755,7 +2802,7 @@ namespace eval shellfilter { set redirtarget [lindex $commandlist end] set commandlist [lrange $commandlist 0 end-2] } else { - #no redirection + #no redirection set redir "" set redirtarget "" #no change to command list @@ -2791,169 +2838,172 @@ namespace eval shellfilter { } } default { - ::shellfilter::log::write $runtag "No redir found!!" + ::shellfilter::log::write $runtag "No redir found!!" } } - #often first element of command list is wrapped and cannot be run directly - #e.g {{ls -l} {> {temp.tmp}}} - #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. - # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. - #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) - set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] - #todo? - #child process environment. - # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. - #to restore buffering states after run - set remember_in_out_err_buffering [list \ - [chan configure $inchan -buffering] \ - [chan configure $outchan -buffering] \ - [chan configure $errchan -buffering] \ - ] + #to restore buffering states after run + set remember_in_out_err_buffering [list {*}{ + } [chan configure $inchan -buffering] {*}{ + } [chan configure $outchan -buffering] {*}{ + } [chan configure $errchan -buffering] {*}{ + } + ] - set remember_in_out_err_translation [list \ - [chan configure $inchan -translation] \ - [chan configure $outchan -translation] \ - [chan configure $errchan -translation] \ - ] + set remember_in_out_err_translation [list {*}{ + } [chan configure $inchan -translation] {*}{ + } [chan configure $outchan -translation] {*}{ + } [chan configure $errchan -translation] {*}{ + } + ] - #chan configure $inchan -buffering none -blocking 1 ;#test - chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + #chan configure $inchan -buffering none -blocking 1 ;#test + #chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $inchan -buffering $inbuffering - chan configure $errchan -buffering $errbuffering - #chan configure $outchan -blocking 0 - chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. - # + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # - #-------------------------------------------- - #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto - #cmd, pwsh, tcl - #chan configure $outchan -translation lf - #chan configure $errchan -translation lf - #-------------------------------------------- - chan configure $outchan -translation $outtranslation - chan configure $errchan -translation $outtranslation + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation - #puts stderr "chan configure $wrerr [chan configure $wrerr]" - if {$debug} { - ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" - } - #todo - handle custom redirection of stderr to a file? - if {[string length $custom_stderr]} { - #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" - #set rdout [open |[concat $commandlist $custom_stderr] a+] - ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" - set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] - set rderr "bogus" ;#so we don't wait for it - } else { - ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" - #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] - #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] - - # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. - # This is the whole reason we need these file-event loops. - # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination - # - and that at least appears like a terminal to the called command. - #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] - - #REVIEW! - #if the child process takes a while to begin reading stdin - the data on stdin between when we stopped the parent chan event handler and when the child gets data, - #seems to stay buffered somewhere. It is then read by the parent, after the child returns. (ie not lost, but out-of-order) - #This can be apparent sometimes even with fast typing upon calling an executable. (e.g occasionally even vim - but seems to be timing based so might only happen first time if at all) - # see scriptlib/stdin_race.tcl etc test files. - #similar problem with python & perl - issue seems to be in libc or OS buffering behaviour for standard channels. - #note that zig (repo/jn/zig/stdin_race) seems to avoid this issue - todo - make zig based binary extension for open/exec? - - set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] - - chan configure $rderr -buffering $errbuffering -blocking 0 - chan configure $rderr -translation $readprocesstranslation - } + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + #REVIEW! + #if the child process takes a while to begin reading stdin - the data on stdin between when we stopped the parent chan event handler and when the child gets data, + #seems to stay buffered somewhere. It is then read by the parent, after the child returns. (ie not lost, but out-of-order) + #This can be apparent sometimes even with fast typing upon calling an executable. (e.g occasionally even vim - but seems to be timing based so might only happen first time if at all) + # see scriptlib/stdin_race.tcl etc test files. + #similar problem with python & perl - issue seems to be in libc or OS buffering behaviour for standard channels. + #note that zig (repo/jn/zig/stdin_race) seems to avoid this issue - todo - make zig based binary extension for open/exec? - set command_pids [pid $rdout] - #puts stderr "command_pids: $command_pids" - #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway - # the child process generally won't shut down until channels are closed. - # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. - # worked around in punk/repl using 'script' command as a fake tty. - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $command_pids 0] ni $subprocesses} { - # puts stderr "pid [lindex $command_pids 0] not running $errMsg" - #} else { - # puts stderr "pid [lindex $command_pids 0] is running" - #} + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] - - if {$debug} { - ::shellfilter::log::write $debugname "pipeline pids: $command_pids" - } + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } - #jjj - chan configure $rdout -buffering $outbuffering -blocking 0 - chan configure $rdout -translation $readprocesstranslation + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} - if {![string length $custom_stderr]} { - chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { - if {$errbuffering eq "line"} { - set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved - #errprefix only applicable to line buffered output - if {$countchunk >= 0} { - if {[chan eof $chan]} { - puts -nonewline $errchan ${errprefix}$chunk - } else { - puts $errchan "${errprefix}$chunk" + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk } } - } else { - set chunk [chan read $chan] - if {[string length $chunk]} { - puts -nonewline $errchan $chunk - } - } - if {[chan eof $chan]} { - flush $errchan ;#jmn - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $pids 0] ni $subprocesses} { - # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" - #} else { - # puts stderr "stderr reader: pid [lindex $pids 0] still running" - #} - chan close $chan - #catch {chan close $wrerr} - #if {$other ni [chan names]} { - # set $waitfor stderr - #} - if {[catch {chan configure $other}]} { - set $waitfor stderr + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + #if {$other ni [chan names]} { + # set $waitfor stderr + #} + if {[catch {chan configure $other}]} { + set $waitfor stderr + } } - } - }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] - } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } #todo - handle case where large amount of stdin coming in faster than rdout can handle #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable - # - we're just pumping it in to the non-blocking rdout buffers + # - we're just pumping it in to the non-blocking rdout buffers # ie there is no backpressure and stdin will suck in as fast as possible. # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc # # - + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. # Not known if that is significant - ## with inchan configured -buffering line + ## with inchan configured -buffering line #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof #instate b:0 eof:0 pend:-1 count:4 @@ -2986,48 +3036,48 @@ namespace eval shellfilter { set chunk [chan read $chan] if {[string length $chunk]} { puts -nonewline $wrchan $chunk - } + } } if {[chan eof $chan]} { puts stderr "|stdin_reader>eof [chan configure stdin]" chan event $chan readable {} - #chan close $chan + #chan close $chan chan close $wrchan write ;#half close #set $waitfor "stdin" } }} $inchan $rdout $inbuffering $waitvar] - + if {[string length $stdinhandler]} { chan configure stdin -buffering line -blocking 0 chan event stdin readable $stdinhandler } - } + } set actual_proc_out_buffering [chan configure $rdout -buffering] set actual_outchan_buffering [chan configure $outchan -buffering] #despite whatever is configured - we match our reading to how we need to output set read_proc_out_buffering $actual_outchan_buffering - + if {[string length $teefile]} { set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] if {$teefile eq "write"} { - ::shellfilter::log::write $logname "opening '$winfile' for write" + ::shellfilter::log::write $logname "opening '$winfile' for write" set fd [open $winfile w] } else { - ::shellfilter::log::write $logname "opening '$winfile' for appending" + ::shellfilter::log::write $logname "opening '$winfile' for appending" set fd [open $winfile a] } - #chan configure $fd -translation lf + #chan configure $fd -translation lf chan configure $fd -translation $outtranslation - chan configure $fd -encoding utf-8 + chan configure $fd -encoding utf-8 - set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] set $tempvar_bytetotal 0 chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { - #review - if we write outprefix to normal stdout.. why not to redirected file? + #review - if we write outprefix to normal stdout.. why not to redirected file? #usefulness of outprefix is dubious upvar $bytevar totalbytes if {$read_proc_out_buffering eq "line"} { @@ -3057,7 +3107,7 @@ namespace eval shellfilter { if {[chan eof $chan]} { flush $writefilefd ;#jmn #set blocking so we can get exit code - chan configure $chan -blocking 1 + chan configure $chan -blocking 1 catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" catch {close $writefilefd} @@ -3086,7 +3136,7 @@ namespace eval shellfilter { } } }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] - + } else { # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' @@ -3136,19 +3186,19 @@ namespace eval shellfilter { #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is - + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important - #this detection is disabled for now - but left for debugging in case it means something.. or changes + #this detection is disabled for now - but left for debugging in case it means something.. or changes chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { #set outchunk [chan read $chan] - + if {$read_proc_out_buffering eq "line"} { set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved #countchunk can be -1 before eof e.g when blocked #debugging output inline with data - don't leave enabled #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" if {$countchunk >= 0} { - if {![chan eof $chan]} { + if {![chan eof $chan]} { puts $outchan ${outprefix}$outchunk } else { puts -nonewline $outchan ${outprefix}$outchunk @@ -3171,10 +3221,12 @@ namespace eval shellfilter { set outchunk [chan read $chan] #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" if {[string length $outchunk]} { - #set stringrep [encoding convertfrom utf-8 $outchunk] + #set stringrep [encoding convertfrom utf-8 $outchunk] #set newbytes [encoding convertto utf-16 $stringrep] #puts -nonewline $outchan $newbytes puts -nonewline $outchan $outchunk + #jmn test 2025 + flush $outchan } } @@ -3219,7 +3271,7 @@ namespace eval shellfilter { catch {chan close $wrerr} #if {$other ni [chan names]} { # set $waitfor stdout - #} + #} if {[catch {chan configure $other}]} { set $waitfor stdout } @@ -3343,5 +3395,5 @@ namespace eval shellfilter { package provide shellfilter [namespace eval shellfilter { variable version - set version 0.2 + set version 0.2.2 }] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/overtype-1.7.4.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/overtype-1.7.4.tm index 403e971c..6c427f1d 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/overtype-1.7.4.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/overtype-1.7.4.tm @@ -401,13 +401,20 @@ tcl::namespace::eval overtype { set opt_console [tcl::dict::get $opts -console] + #-------------------------------------------------------------------------- + #TODO + #REVIEW - punk::console package may not be loaded set cursor_style_overtype {3 underline-blink} set cursor_style_insert {5 beam-blink} if {$opt_insert_mode} { - punk::console::cursor_style -console $opt_console $cursor_style_insert + set initial_cursor_style $cursor_style_insert } else { + set initial_cursor_style $cursor_style_overtype + } + catch { punk::console::cursor_style -console $opt_console $cursor_style_overtype } + #-------------------------------------------------------------------------- # ---------------------------- # -experimental dev flag to set flags etc @@ -695,22 +702,23 @@ tcl::namespace::eval overtype { #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\ - -overtext_type $overtext_type\ + 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 {*}{ + } -overtext_type $overtext_type {*}{ + } ] set rinfo [renderline {*}$renderopts $undertext $overtext] @@ -940,14 +948,15 @@ tcl::namespace::eval overtype { puts stdout ">>>renderspace<<<[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 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..? @@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype { } #JMN - if {[tcl::dict::get $vtstate insert_mode]} { - puts "setting cursor to insert style" - punk::console::cursor_style -console $opt_console $cursor_style_insert - } else { - punk::console::cursor_style -console $opt_console $cursor_style_overtype - } + #REVIEW - we don't want to emit cursor_style ANSI unless it changes. + #if {[tcl::dict::get $vtstate insert_mode]} { + # puts "setting cursor to insert style" + # punk::console::cursor_style -console $opt_console $cursor_style_insert + #} else { + # punk::console::cursor_style -console $opt_console $cursor_style_overtype + #} #puts "renderedrow_max: $renderedrow_max" #check for null lines below renderedrow_max (and at tail) and trim. @@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype { #broken: #todo - renderline -overflow is invalid. # we need renderline to support -expand_left ?? - 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 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} { @@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype { -crm_mode -default 0 -type boolean -autowrap_mode -default 1 -type boolean -reverse_mode -default 0 -type boolean - -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\ + -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\ + -choicelabels\ { 1 "return a dict with raw fields" 2 "return a dict using ansistring VIEW" diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm index 4473c1d1..a0ae202c 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm @@ -283,14 +283,6 @@ namespace eval punk { #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;" - } # ------------------------ #Note that unlike an ordinary Tcl array - the linked ::env behaves differently. @@ -307,6 +299,15 @@ namespace eval punk { } # ------------------------ + 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;" + } + #change2 if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { set lookfor [list $name] @@ -5492,9 +5493,10 @@ namespace eval punk { #ctrl-c propagation also needs to be considered set teehandle punksh - uplevel 1 [list ::catch \ - [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ - ::tcl::UnknownResult ::tcl::UnknownOptions] + uplevel 1 [list ::catch {*}{ + } [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{ + } ::tcl::UnknownResult ::tcl::UnknownOptions + ] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { dict set ::tcl::UnknownOptions -code error @@ -8428,19 +8430,44 @@ namespace eval punk { set I [punk::ansi::a+ italic] set NI [punk::ansi::a+ noitalic] + set sizedict [punk::console::get_size] + set cols [dict get $sizedict columns] + set rows [dict get $sizedict rows] + + + + #todo - provide a mechanism to configure the default frametype everywhere and describe it in this help. + + set frametype ascii ;#conservative default. + #if the test char width fails - it's likely we're on a very old terminal that doesn't support unicode at all. + if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + if {$cols <= 80} { + # Be conservative with frame types on narrow terminals for help. + # an 80x30 terminal is more likely to be an older style terminal and may not have unicode support. + # unicode on a non-unicode terminal is a bad experience - with the frame chars showing as garbage (e.g 3 chars per grapheme). + set frametype ascii + } else { + if {$testcharwidth == 1} { + set frametype light ;#unicode box-drawing chars. + } + } + } + + # ------------------------------------------------------- set logoblock "" if {[catch { package require patternpunk #lappend chunks [list stderr [>punk . rhs]] - append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]] + append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]] }]} { - append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""] + append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""] } set title "[a+ brightgreen] Help System: " set cmdinfo [list] lappend cmdinfo [list help "?${I}topic${NI}?" "This help.\nTo see available subitems type:\nhelp topics\n\nFor an unrecognised ${I}topic${NI}\nhelp will look for basic\ninfo for it as a command.\n"] set t [textblock::class::table new -minwidth 51 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8468,6 +8495,7 @@ namespace eval punk { lappend cmdinfo [list newdir "${I}subdir${NI}..." "make new dir or dirs and show status"] lappend cmdinfo [list fcat "${I}file ?file?...${NI}" "cat file(s)"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8491,6 +8519,7 @@ namespace eval punk { lappend cmdinfo [list "nn/" "" "go up one namespace"] lappend cmdinfo [list "newns" "${I}ns${NI}" "make child namespace and switch to it"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8513,6 +8542,7 @@ namespace eval punk { lappend cmdinfo [list eg "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show example from manpage"] lappend cmdinfo [list corp "${I}proc${NI}" "View proc body and arguments with basic highlighting"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8537,6 +8567,7 @@ namespace eval punk { lappend cmdinfo [list a "?${I}colourcode${NI}...?" "Return ANSI codes (with leading reset)\n e.g puts \"\[a+ purple\]purple\[a Green\]normal on green\[a\]\"\n [a+ purple]purple[a Green]normal on green[a] "] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8606,6 +8637,7 @@ namespace eval punk { set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + $t configure -frametype $frametype if {"windows" eq $::tcl_platform(platform)} { #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. #The Tcl ::env array is linked to the underlying process view of the environment @@ -8634,6 +8666,7 @@ namespace eval punk { $t destroy set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + $t configure -frametype $frametype foreach {v vinfo} $otherenv_config { if {[info exists ::env($v)]} { set env_val [set ::env($v)] @@ -8851,6 +8884,7 @@ namespace eval punk { }] set t [textblock::class::table new -show_seps 0] + $t configure -frametype $frametype $t add_column -headers [list "Topic"] $t add_column foreach {k v} $topics { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm index f9997649..e8518d0f 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm @@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi { @cmd -name punk::ansi::sauce -summary\ "SAUCE info from file"\ -help\ - "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data." + "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data. + + Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format + that was commonly used in old ANSI art files to store information about the file, such as + title, author, group, date, and comments. + It may also have fields to specify the number of columns and rows in the ANSI art, + as well as flags for specific display attributes. + + It may also be used on other types of files such as bitmap, vector, audio, binarytext, + xbin, archive and executable files. + + It is a 128-byte block of data that is typically appended to the end of a file. + https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm" -encoding -default iso8859-1 -type string -help\ "The default iso8859-1 is equivalent to binary ans should work in the usual case. @@ -6044,6 +6056,7 @@ be as if this was off - ie lone CR. #[para]These functions will emit the code - but read it in from stdin so that it doesn't display, and then return the row and column as a colon-delimited string or list respectively. #[para]The punk::ansi::cursor_pos function is used by punk::console::get_cursor_pos and punk::console::get_cursor_pos_list return \033\[6n + #same as 'tput u7' which just emits CSI 6n to stdout } proc cursor_pos_extended {} { @@ -6053,6 +6066,7 @@ be as if this was off - ie lone CR. } + #DECFRA - Fill rectangular area #REVIEW - vt100 accepts decimal values 132-126 and 160-255 ("in the current GL or GR in-use table") #some modern terminals accept and display characters outside this range - but this needs investigation. diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm index 79ea5901..e7428d84 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm @@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce { variable PUNKARGS variable PUNKARGS_aliases + #https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm lappend PUNKARGS [list { @id -id "(package)punk::ansi::sauce" @package -name "punk::ansi::sauce" -help\ diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm index 5271bbfb..b7c4cd7a 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm @@ -44,7 +44,14 @@ #[list_begin itemized] package require Tcl 8.6- + +#---------------------------------------------------- +#Although we need to be in an environment with Thread available to use punk::console, +# we don't want to require Thread as a hard dependency in the interp we're running in. +# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp. +#TODO. package require Thread ;#tsv required to sync is_raw +#---------------------------------------------------- package require punk::ansi package require punk::args #*** !doctools @@ -282,7 +289,7 @@ namespace eval punk::console { ignore the regex match 'ok' response and keep going." -return -type string -default payload -choices {payload dict} -choicelabels { - dict\ + dict "dict with keys prefix,response,payload,all" } -help\ "Return format" @@ -290,12 +297,12 @@ namespace eval punk::console { -console -default {stdin stdout} -type list -help\ "console/terminal (currently list of in/out channels) (todo - object?)" -passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels { - none\ + none { ANSI sent without any passthrough wrapping. A terminal multiplexer such as tmux,screen,zellij may not pass the request through to the underlying terminal(s) This is the recommended/normal value for the option.} - tmux\ + tmux { Wrap ANSI sequence with tmux passthrough sequence. \x1bPtmux\;\x1b\\ Note that a tmux session could be connected to multiple @@ -304,7 +311,7 @@ namespace eval punk::console { Passthrough should generally be avoided except for debug/test purposes. } - auto\ + auto { Use existence of ::env(TMUX) to detect tmux and send tmux passthrough sequence. Not recommended except for debug/test purposes. @@ -1012,6 +1019,7 @@ namespace eval punk::console { #e.g \033\[46;1R set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$} ;#must capture prefix,entire-response,response-payload + #This is all 'tput u7' does (emits CSI 6n on stdout) set request "\033\[6n" set payload [punk::console::internal::get_ansi_response_payload -console $inoutchannels $request $capturingregex] #some terminals fail to respond properly to \x1b\[6n but do respond to \x1b\[?6n and vice-versa :/ @@ -1021,6 +1029,9 @@ namespace eval punk::console { return $payload } + + + proc get_checksum_rect {id page t l b r {inoutchannels {stdin stdout}}} { #e.g \x1b\[P44!~E797\x1b\\ #re e.g {(.*)(\x1b\[P44!~([[:alnum:]])\x1b\[\\)$} @@ -1295,7 +1306,7 @@ namespace eval punk::console { default {set keyboard_name "unknown"} } - return [dict create {*} { + return [dict create {*}{ } class $class_name {*}{ } version $version {*}{ } keyboard $keyboard_name {*}{ @@ -1522,17 +1533,114 @@ namespace eval punk::console { } #todo - determine cursor on/off state before the call to restore properly. + variable get_size_mechanism + set get_size_mechanism [dict create] + proc get_size {{inoutchannels {stdin stdout}}} { lassign $inoutchannels in out - #we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810 - #chan eof is faster whether chan exists or not than - if {[catch {chan eof $out} is_eof]} { - error "punk::console::get_size output channel $out seems to be closed ([info level 1])" - } else { - if {$is_eof} { - error "punk::console::get_size eof on output channel $out ([info level 1])" + set tried_mechlist [list] + + #fastest mechanism if available - use Tcl's inbuilt -winsize key from chan configure if available - this is much faster than any ANSI mechanism + #unknown which platforms support this. + if {![catch {get_size_using_chanconfigure $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist "chanconfigure" + + + variable is_vt52 + if {$is_vt52} { + #vt52 doesn't support cursor save/restore or cursor position reports. + if {![catch {get_size_using_tput $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist "tput" + error "can't get console size. Tried mechanisms: $mechlist" + } + + variable get_size_mechanism ;#dict keyed on terminal ident. (currently just list of inoutchannels but may be something else in future such as terminal object or ident string) + if {![dict exists $get_size_mechanism $inoutchannels]} { + set try_order [list] + #call each mechanism once to see if it works - and to ensure we don't include initial run in our timings. + #we will also use the results for our initial return of the size. + set successful_mechs [list] + set sizedict [dict create] + if {![catch {get_size_using_cursorrestore $inoutchannels} result]} { + lappend successful_mechs "cursorrestore" + if {![dict size $sizedict]} { + set sizedict $result + } + } + if {![catch {get_size_using_cursormove $inoutchannels} result]} { + lappend successful_mechs "cursormove" + if {![dict size $sizedict]} { + set sizedict $result + } } + if {![catch {get_size_using_tput $inoutchannels} result] } { + lappend successful_mechs "tput" + if {![dict size $sizedict]} { + set sizedict $result + } + } + set timings [list] + set t_ms 250 ;#default timerate is 1000ms - we are trying at least 3 mechanisms so 1000ms is a bit long for this test as it can slow down the first call to get_size significantly. + foreach sm $successful_mechs { + catch { + set timing_result [timerate {get_size_using_$sm $inoutchannels} $t_ms] + set micros [expr {int([lindex $timing_result 0])}] + lappend timings [list $micros $sm] + } + } + set sorted [lsort -integer -index 0 $timings] + if {[llength $sorted] > 0} { + set try_order [lmap t $sorted {lindex $t 1}] + } else { + set try_order [list] + } + dict set get_size_mechanism $inoutchannels $try_order + return $sizedict + } + + foreach mech [dict get $get_size_mechanism $inoutchannels] { + if {![catch {get_size_using_$mech $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist $mech } + + #if {![catch {get_size_using_cursorrestore $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "cursorrestore" + + #if {![catch {get_size_using_cursormove $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "cursormove" + + #if {![catch {get_size_using_tput $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "tput" + + error "can't get console size. Tried mechanisms: $tried_mechlist" + } + + proc get_size_using_chanconfigure {{inoutchannels {stdin stdout}}} { + set out [lindex $inoutchannels 1] + set outconf [chan configure $out] + if {[dict exists $outconf -winsize]} { + #this mechanism is much faster than ansi cursor movements + #REVIEW check if any x-platform anomalies with this method? + #can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least + lassign [dict get $outconf -winsize] cols lines + if {[string is integer -strict $cols] && [string is integer -strict $lines]} { + return [dict create columns $cols rows $lines] + } + } + error "chan configure method of getting console size not supported or failed to get valid size info" + #we don't need to care about the input channel if chan configure on the output can give us the info. #short circuit ansi cursor movement method if chan configure supports the -winsize value set outconf [chan configure $out] @@ -1542,43 +1650,45 @@ namespace eval punk::console { #can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least lassign [dict get $outconf -winsize] cols lines if {[string is integer -strict $cols] && [string is integer -strict $lines]} { - return [list columns $cols rows $lines] + return [dict create columns $cols rows $lines] } #continue on to ansi mechanism if we didn't get 2 ints } - if {[catch {chan eof $in} is_eof]} { - error "punk::console::get_size input channel $in seems to be closed ([info level 1])" + } + + proc get_size_using_tput {{inoutchannels {stdin stdout}}} { + set tputcmd [auto_execok tput] + if {$tputcmd eq ""} { + error "tput command not found - cannot use tput method to get console size" + } + lassign [exec {*}$tputcmd lines cols] lines cols + return [dict create columns $cols rows $lines] + } + + proc get_size_using_cursormove {{inoutchannels {stdin stdout}}} { + set out [lindex $inoutchannels 1] + #we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810 + #chan eof is faster whether chan exists or not than + if {[catch {chan eof $out} is_eof]} { + error "punk::console::get_size_using_cursormove output channel $out seems to be closed ([info level 1])" } else { if {$is_eof} { - error "punk::console::get_size eof on input channel $in ([info level 1])" + error "punk::console::get_size_using_cursormove eof on output channel $out ([info level 1])" } } - #keep out of catch - no point in even trying a restore move if we can't get start position - just fail here. - #no vt52 equiv? may as well strip all vt52 from here? lassign [get_cursor_pos_list $inoutchannels] start_row start_col - variable is_vt52 - if {!$is_vt52} { - set movefunc "punk::ansi::move" - set func_coff "punk::ansi::cursor_off" - set func_con "punk::ansi::cursor_on" - } else { - set movefunc "punk::ansi::vt52move" - set func_coff "punk::ansi::vt52cursor_off" - set func_con "punk::ansi::vt52cursor_on" - } if {[catch { #some terminals (conemu on windows) scroll the viewport when we make a big move down like this - a move to 1 1 immediately after cursor_save doesn't seem to fix that. #This issue also occurs when switching back from the alternate screen buffer - so perhaps that needs to be addressed elsewhere. - - puts -nonewline $out [$func_coff][$movefunc 2000 2000] + puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::move 2000 2000] lassign [get_cursor_pos_list $inoutchannels] lines cols - puts -nonewline $out [$movefunc $start_row $start_col][$func_con];flush stdout - set result [list columns $cols rows $lines] + puts -nonewline $out [punk::ansi::move $start_row $start_col][punk::ansi::cursor_on];flush stdout + set result [dict create columns $cols rows $lines] } errM]} { - puts -nonewline $out [$movefunc $start_row $start_col] - puts -nonewline $out [$func_con] + puts -nonewline $out [punk::ansi::move $start_row $start_col] + puts -nonewline $out [punk::ansi::cursor_on] error "$errM" } else { return $result @@ -1586,14 +1696,16 @@ namespace eval punk::console { } #faster than get_size when it is using ansi mechanism - but uses cursor_save - which we may want to avoid if calling during another operation which uses cursor save/restore - proc get_size_cursorrestore {{inoutchannels {stdin stdout}}} { + proc get_size_using_cursorrestore {{inoutchannels {stdin stdout}}} { lassign $inoutchannels in out - #we use the same shortcircuit mechanism as get_size to avoid ansi at all if the output channel will give us the info directly set outconf [chan configure $out] - if {[dict exists $outconf -winsize]} { - lassign [dict get $outconf -winsize] cols lines - if {[string is integer -strict $cols] && [string is integer -strict $lines]} { - return [list columns $cols rows $lines] + #don't use shortcut mechanisms - this function is intended to specificall use the cursor_save/restore method + + if {[catch {chan eof $out} is_eof]} { + error "punk::console::get_size_using_cursorrestore output channel $out seems to be closed ([info level 1])" + } else { + if {$is_eof} { + error "punk::console::get_size_using_cursorrestore eof on output channel $out ([info level 1])" } } @@ -1603,7 +1715,7 @@ namespace eval punk::console { puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::cursor_save_dec][punk::ansi::move 2000 2000] lassign [get_cursor_pos_list $inoutchannels] lines cols puts -nonewline $out [punk::ansi::cursor_restore][punk::console::cursor_on];flush $out - set result [list columns $cols rows $lines] + set result [dict create columns $cols rows $lines] } errM]} { puts -nonewline $out [punk::ansi::cursor_restore_dec] puts -nonewline $out [punk::ansi::cursor_on] @@ -1612,10 +1724,15 @@ namespace eval punk::console { return $result } } + + proc get_dimensions {{inoutchannels {stdin stdout}}} { lassign [get_size $inoutchannels] _c cols _l lines return "${cols}x${lines}" } + + + #the (xterm?) CSI 18t query is supported by *some* terminals proc get_xterm_size {{inoutchannels {stdin stdout}}} { set capturingregex {(.*)(\x1b\[8;([0-9]+;[0-9]+)t)$} ;#must capture prefix,entire-response,response-payload @@ -2669,6 +2786,7 @@ namespace eval punk::console { This allows querying the current style and then re-setting it after temporarily changing it." }] } + proc cursor_style {args} { set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style] lassign [dict values $argd] leaders opts values diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm index e67acfda..5ee354a6 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm @@ -1641,7 +1641,7 @@ namespace eval punk::du { #convert time from windows (100ns units since jan 1, 1601) to Tcl time (seconds since Jan 1, 1970) #We lose some precision by not passing the boolean to the large_system_time_to_secs_since_1970 function which returns fractional seconds #but we need to maintain compatibility with other platforms and other tcl functions so if we want to return more precise times we will need another flag and/or result dict - dict set alltimes $fullname [dict create {*} { + dict set alltimes $fullname [dict create {*}{ } c [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo ctime]] {*}{ } a [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo atime]] {*}{ } m [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo mtime]] {*}{ @@ -2114,15 +2114,15 @@ namespace eval punk::du { } proc du_dirlisting_tclvfs {folderpath args} { - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm index aee51015..8f25047c 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm @@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check { if {"windows" ne $::tcl_platform(platform)} { set bug 0 } else { - set tmpdir [file tempdir] ;#tcl 9+ - set testfile [file join $tmpdir "bugtest"] + if {![catch {file tempdir} tmpdir]} { + #tcl 9+ has 'file tempdir' + set testfile [file join $tmpdir "bugtest"] + } else { + #fallback for older tcl versions - use env TEMP/TMP or current directory + set tmpdir "" + foreach e {TEMP TMP} { + if {[info exists ::env($e)] && [file isdirectory ::env($e)]} { + set tmpdir ::env($e) + break + } + } + if {$tmpdir eq ""} { + #no env vars - fallback to current directory + set tmpdir [pwd] + } + set testfile [file join $tmpdir "bugtest"] + } + set fd [open $testfile w] puts $fd test close $fd set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*] + if {[file exists $testfile]} { + file delete $testfile + } foreach r $globresult { if {$r ne "bugtest"} { set bug 1 @@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat { #*** !doctools #[call [fun lpop] [arg listvar] [opt {index}]] #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l + #upvar $lvar l + upvar 1 $lvar l if {![llength $args]} { set args [list end] } @@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat { #set newlist [lremove $newlist $tailidx] #set newlist [lreplace $newlist $tailidx $tailidx] set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx] - #don't use ledit here! + #we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin. } else { set sublist [lindex $newlist {*}$sublist_path] #set sublist [lremove $sublist $tailidx] @@ -459,26 +480,46 @@ tcl::namespace::eval punk::lib::compat { } } set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -Inf { - #index below lower bound - set post [lrange $l 0 end] - } - Inf { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + #set post [lrange $l $last+1 end] + switch -exact -- $lidx { + -Inf { + #index below lower bound + set post [lrange $l 0 end] + } + Inf { + #index above upper bound + set post [list] + } + default { set post [lrange $l $lidx+1 end] } } } + #switch -exact -- $lidx { + # -Inf { + # #index below lower bound + # set post [lrange $l 0 end] + # } + # Inf { + # #index above upper bound + # set post [list] + # } + # default { + # if {$lidx < $fidx} { + # #from ledit man page: + # #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + # set post [lrange $l $fidx end] + # } else { + # #set post [lrange $l $last+1 end] + # set post [lrange $l $lidx+1 end] + # } + # } + #} set l [list {*}$pre {*}$args {*}$post] } @@ -2481,7 +2522,7 @@ namespace eval punk::lib { #no parse tree - This is likely for an empty argument with expansion e.g {*}{ } #This construct occurs when using {*} in place of line continuation for long lists or dicts, e.g #dict create {*}{ - # } key1 $dynamic {*} { + # } key1 $dynamic {*}{ # key2 value2 #} #review - the 'empty' argument will still have an entry in cmdlineranges - as although 'empty' in terms of how it expands it may be whitespace across multiple lines. @@ -3447,32 +3488,53 @@ namespace eval punk::lib { showdict {*}$opts $dvalue {*}$patterns } - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + namespace eval argdoc { + variable PUNKARGS + upvar ::punk::lib::has_punk_ansi has_punk_ansi + + + #if {!$has_punk_ansi} { + # set RST "" + # set sep " = " + # set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + #} else { + # set RST [punk::ansi::a] + # #set sep " [a+ Web-seagreen]=[a] " + # set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support + # #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + # #NOTE that \u2260 not suitable for non utf-8 terminals. + # set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + #} + #todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8. + # (safe detection methods for utf-8 support?) + #if colour is disabled we want to refresh this. + #therefore we use @dynamic + proc get_sep {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep " = " + } else { + #set sep " [a+ Web-seagreen]=[a] " + set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support + } + return $sep } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock + proc get_sep_mismatch {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + } else { + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #NOTE that \u2260 not suitable for non utf-8 terminals. + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] " + } + return $sep_mismatch + } + set DYN_SEP {${[get_sep]}} + set DYN_SEP_MISMATCH {${[get_sep_mismatch]}} - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { + lappend PUNKARGS [list { + @dynamic @id -id ::punk::lib::showdict @cmd -name punk::lib::showdict -help "display dictionary keys and values" #todo - table tableobject @@ -3482,10 +3544,8 @@ namespace eval punk::lib { "Trim whitespace off rhs of each line. This can help prevent a single long line that wraps in terminal from making every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" + -separator -default "${$DYN_SEP}" -help "Separator column between keys and values" + -separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch" -roottype -default "dict" -help\ "list,dict,string" -ansibase_keys -default "" -help\ @@ -3504,7 +3564,23 @@ namespace eval punk::lib { "dict or list value" patterns -default "*" -type string -multiple 1 -help\ "key or key glob pattern" - }]] + }] + } + #TODO - much. + #showdict needs to be able to show different branches which share a root path + #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) + # - specify ansi colour per pattern so different branches can be highlighted? + # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc + # - The current version is incomplete but passably usable. + # - Copy proc and attempt rework so we can get back to this as a baseline for functionality + proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) + package require punk::pipe + #package require punk ;#we need pipeline pattern matching features + package require textblock + + set RST [punk::ansi::a] + + set argd [punk::args::parse $args withid ::punk::lib::showdict] #for punk::lib - we want to reduce pkg dependencies. # - so we won't even use the tcllib debug pkg here diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm index 05e8724f..87449fb3 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm @@ -566,6 +566,7 @@ namespace eval punk::mix::cli { #lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list # -- --- set installer [punkcheck::installtrack new $installername $punkcheck_file] + #set installer [punkcheck::installtrack new $installername $punkcheck_file stderr] ;#with debugchannel $installer set_source_target $srcdir $basedir set event [$installer start_event $config] # -- --- @@ -601,9 +602,9 @@ namespace eval punk::mix::cli { } #debug for issues on non-windows platforms. - if {$::tcl_platform(platform) ne "windows"} { - set is_interesting 1 - } + #if {$::tcl_platform(platform) ne "windows"} { + # set is_interesting 1 + #} if {$is_interesting} { puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath" @@ -662,6 +663,7 @@ namespace eval punk::mix::cli { # -max_depth -1 for no limit set build_installername pods_in_$current_source_dir set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck] + #set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel $build_installer set_source_target $current_source_dir/$modpath $buildfolder set build_event [$build_installer start_event $config] # -- --- diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm index 5635cd09..7d85e311 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) if {$linktarget_file_type eq "directory"} { - set linktarget [file readlink $cdtarget] - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { @@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs { link { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) - set linktarget [file readlink $cdtarget] if {$linktarget_file_type eq "directory"} { - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index dc8c765c..e89997c5 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -2167,8 +2167,13 @@ y" {return quirkykeyscript} puts stdout "leaving $target" puts stdout "call $commandstring\x1b\[m" - puts stdout "result:" - puts stdout $result + if {$code == 0} { + puts stdout "result:" + puts stdout $result + } else { + puts stdout "error message:" + puts stdout $::errorInfo + } puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset set cmdtype [dict get $linedict $target cmdtype] @@ -2647,6 +2652,8 @@ y" {return quirkykeyscript} upvar ::punk::ns::linedict linedict set ::punk::ns::linedict [::tcl::dict::create] + set body_cache [tcl::dict::create] + set resolved_targets [list] foreach tgt $targets { set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]] @@ -5018,6 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs + puts "punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -6618,18 +6626,19 @@ y" {return quirkykeyscript} separately calling 'info args ' 'info body ' etc. The body may display with an additional - comment inserted to display information such as the + comment inserted above the proc line to display information such as the namespace origin. Such a comment begins with #corp#. - Returns a list: proc - (as long as any syntax highlighter is written to - avoid breaking the structure. e.g by avoiding the - insertion of ANSI between an escaping backslash and - its target character) + Returns a string: proc If the output is to be used as a script to regenerate a procedure, '-syntax none' should be used to avoid ANSI colours, or the resulting arglist and body should be run through 'ansistrip'. + + (any syntax highlighter should be written to + avoid breaking the structure. e.g by avoiding the + insertion of ANSI between an escaping backslash and + its target character) " @opts #todo - make definition @dynamic - load highlighters as functions? @@ -6647,7 +6656,13 @@ y" {return quirkykeyscript} "Whether to replace tabs in the body with spaces or a visible Unicode symbol." -ranges -type indexset -default "0..end" -help\ "comma delimited set of line ranges. - Restrict output to the specified line ranges of the body. Lines are numbered starting at 1." + Restrict output to the specified line ranges of the body. Lines are numbered starting at 1. + For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body. + The special index 0 is used to specify the line before the first line of the body, + which is where the #corp# info comment is placed if it exists. + So the default range 0..end includes the info comment and all lines of the body. + Specifying -ranges 1..5,0 would include the info comment at the end of the output. + " -syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\ -choicelabels { none @@ -6690,11 +6705,6 @@ y" {return quirkykeyscript} set indent [string repeat " " $tw] ;#match #set indent [string repeat " " $tw] ;#A more sensible default for code - review - if {[info exists ::auto_index($path)]} { - set infoheader "\n${indent}#corp# auto_index $::auto_index($path)" - } else { - set infoheader "" - } #we want to handle edge cases of commands such as "" or :x #various builtins such as 'namespace which' won't work @@ -6753,13 +6763,29 @@ y" {return quirkykeyscript} return [list alias {*}$alias] } } - if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} { - append infoheader \n "${indent}#corp# namespace origin $origin" - } - if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { - append infoheader \n + #-------------------------------------------------------------------------- + if {[info exists ::auto_index($path)]} { + #set infoheader "${indent}#corp# auto_index $::auto_index($path)" + set infoheader "#corp# auto_index $::auto_index($path)" + } else { + set infoheader "" } + #puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'" + #if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {} + if {$origin ne $targetcmd} { + #append infoheader "${indent}#corp# namespace origin $origin" + append infoheader "#corp# namespace origin $origin" + } + if {$infoheader ne "" && $syntax eq "basic"} { + set infoheader [ansiwrap green $infoheader] + } + #if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { + # append infoheader \n + #} + #-------------------------------------------------------------------------- + + set body "" #set bodytext [info body $origin] #relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname @@ -6851,47 +6877,126 @@ y" {return quirkykeyscript} } } - if {$ranges ni {"0..end" ".." "0.."}} { - set lines [split $body \n] - set linecount [llength $lines] + set lines [split $body \n] + set linecount [llength $lines] + + + #------------------------------------------------------------------------------------------------ + #When we resolve our 1-based indexset - the zero index used to specify the info comment is lost. + #we need to search for it manually and add it back in if it's in the specified ranges. + set rangelist [split $ranges ,] + set info_positions [list] + set lnum 0 + foreach range $rangelist { + lassign [split $range ..] start _ end + set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range] + if {$start eq "0"} { + lappend info_positions $lnum + } + incr lnum [llength $r_indices] + #if {$end eq "0"} { + # #ignore + #} + } + #puts "info_positions: $info_positions" + #------------------------------------------------------------------------------------------------ + + + set body "" + if {[lindex $info_positions 0] == 0 && $infoheader ne ""} { + append body "$infoheader" \n + } + + if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} { set w [string length $linecount] set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges] - set body "" + set outputlines [llength $indices] + if {$do_ln} { - set n 0 foreach idx $indices { - append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + if {$idx == 1} { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}" + } else { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } else { foreach idx $indices { - append body [lindex $lines $idx-1] \n + if {$idx == 1} { + set ln1 "proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body [lindex $lines end] "\}" + } else { + append body [lindex $lines $idx-1] \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } + #no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match + if {[string index $body end] eq "\n"} { + set body [string range $body 0 end-1] + } } else { + #range was specified in a standard way to mean 'all lines' + set outputlines $linecount if {$do_ln} { - set linebody "" - set n 0 - set lines [split $body \n] - set linecount [llength $lines] set w [string length $linecount] - foreach ln $lines { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + set n 2 + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$lnc[format %${w}s $n]$lnr $ln" incr n - append linebody "$lnc[format %${w}s $n]$lnr $ln" \n } - set body [string range $linebody 0 end-1] - #set body $linebody + if {$linecount > 1} { + append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody + } else { + set ln1 "proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$ln" + } + if {$linecount > 1} { + append linebody \n "[lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody } } - - - if {$is_highlighted} { - #ansi colourised items in list format may not always have desired string representation (list escaping can occur) - #return as a string - which may not be a proper Tcl list! - return "proc $resolved {$argl} {\n$infoheader$body\n}" - } else { - list proc $resolved $argl $infoheader$body + #ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end? + if {[lindex $info_positions end] > 0} { + if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} { + append body "\n$infoheader" + } } + return $body } diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm index c8f7fc3a..a7aff5a6 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -21,6 +21,7 @@ unset stdin_info # ----------------------------------- + #------------------------------------------------------------------------------------- if {[package provide punk::libunknown] eq ""} { #maintenance - also in src/vfs/_config/punk_main.tcl @@ -162,6 +163,13 @@ namespace eval punk::repl { #todo - key on shell/subshell tsv::set repl runchunks-0 [list] ;#last_run_display + variable frametype + set frametype ascii; #conservative default + #if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + # if {$testcharwidth == 1} { + # set frametype light + # } + #} variable debug_repl 0 variable signal_control_c 0 @@ -991,6 +999,8 @@ namespace eval punk::repl::class { dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered] } method add_chunk {chunk} { + upvar ::punk::repl::frametype frametype + #we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw] @@ -1065,7 +1075,7 @@ namespace eval punk::repl::class { append debug \n [showdict $dinfo] append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col" package require textblock - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] + set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug] if {![punk::console::vt52]} { catch {punk::console::move_emitblock_return $debug_first_row 1 $debug} } else { @@ -1148,7 +1158,7 @@ namespace eval punk::repl::class { set debug "add_chunk$i" append debug \n $mergedinfo append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]" - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] + set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug] #catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug} set result [dict get $mergedinfo result] @@ -1816,6 +1826,8 @@ proc punk::repl::console_debugview {editbuf consolewidth args} { } package require textblock variable debug_repl + variable frametype + if {$debug_repl <= 0} { return [dict create width 0 height 0 topleft {}] } @@ -1850,11 +1862,11 @@ proc punk::repl::console_debugview {editbuf consolewidth args} { } set debug_height [expr {[llength $lines]+2}] ;#framed height } errM]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] set debug_height [textblock::height $info] } else { #treat as ephemeral (unreusable) frames due to varying width & height - therefore set -buildcache 0 - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] } set debug_width [textblock::widthtopline $info] @@ -1881,6 +1893,8 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { package require textblock upvar ::repl::editbuf_list editbuf_list + variable frametype + set defaults {-row 10 -rightmargin 0} set opts [dict merge $defaults $args] set opt_row [dict get $opts -row] @@ -1894,14 +1908,14 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { set info [punk::lib::list_as_lines $lines] } } editbuf_error]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] } else { set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]" append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]" set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]" set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]" set info [a+ green bold]$row1\n$row2[a]\n$info - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] } set editbuf_width [textblock::widthtopline $info] set spacepatch [textblock::block $editbuf_width 2 " "] @@ -1915,8 +1929,10 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { return [dict create width $editbuf_width] } + proc punk::repl::console_controlnotification {message consolewidth consoleheight args} { package require textblock + variable frametype set defaults {-bottommargin 0 -rightmargin 0} set opts [dict merge $defaults $args] set opt_bottommargin [dict get $opts -bottommargin] @@ -1924,8 +1940,14 @@ proc punk::repl::console_controlnotification {message consolewidth consoleheight set messagelines [split $message \n] set message [lindex $messagelines 0] ;#only allow single line set info "[a+ bold red]$message[a]" - set hlt [dict get [textblock::framedef light] hlt] - set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] + + if {$frametype eq "ascii"} { + set box [textblock::frame -type ascii -checkargs 0 -boxmap [list tlc + trc +] -title $message -height 1] + } else { + set hlt [dict get [textblock::framedef light] hlt] + set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] + } + set notification_width [textblock::widthtopline $info] set box_offset [expr {$consolewidth - $notification_width - $opt_rightmargin}] set row [expr {$consoleheight - $opt_bottommargin}] @@ -1939,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config } else { set is_vt52 0 } + variable codethread variable loopinstance incr loopinstance @@ -2069,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config set chunk "\b\x7f\b\x7f" } elseif {$chunk eq "\x1c"} { #ctrl-bslash + #This is commonly used in terminals as a 'harder' ctrl-c. #try to brutally terminate process #attempt to leave terminal in a reasonable state - mode line ;#may be aliased to ::repl::interphelpers::mode - after 250 {exit 42} + punk::console::mode line + #for now - exit with small delay for tidyup + after 1000 {exit 43} return } elseif {$chunk eq "\x1a"} { - #for now - exit with small delay for tidyup #ctrl-z #::punk::repl::handler_console_control "ctrl-z_via_rawloop" - if {[catch {punk::console::mode line}]} { - #REVIEW - interp eval code {punk::console::mode line} + #JMN + #set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}] + set iname $::punk::repl::codethread::replthread_interp + #only the highest level subshell has an interp name of empty string (lower levels are named 'code') + if {$iname eq ""} { + punk::console::mode line } - after 1000 {exit 43} + after 250 [list thread::send $codethread [list interp eval code {quit 42}]] return } @@ -2375,6 +2402,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #set commandstr "set ::punk::repl::debug_repl" set commandstr "" } + + + if {$::punk::repl::debug_repl > 100} { proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { set p %p% @@ -2394,12 +2424,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs debugreport $clearance$p[string map [list \n \n$p] $msg] }] set info "" - append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" - append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" + append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" + append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n" set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]] append info "lastrunchunks\n" - append info "chunks: [llength $lastrunchunks]\n" - append info "namespace: $::punk::nav::ns::ns_current" + append info "chunks : [llength $lastrunchunks]\n" + #JMN + set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]] + append info "codethread namespace: $codethread_ns\n" + append info "stdinlines : [llength $stdinlines] lines\n" + foreach ln $stdinlines { + append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n" + } + append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n" + #append info "namespace: $::punk::nav::ns::ns_current" debug_repl_emit $info } else { proc debug_repl_emit {msg} {return} @@ -2444,7 +2482,6 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config # lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] #} - variable codethread variable codethread_cond variable codethread_mutex @@ -2873,8 +2910,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config if {[llength $waiting]} { set c [lindex $waiting end] } else { - #set c " " - set c \u240a + set c \u240a ;#unicode linefeed symbol. } doprompt ">$c " } @@ -2986,16 +3022,17 @@ namespace eval repl { set codethread_mutex [thread::mutex create] - set scriptmap [list %args% [list $opts] \ - %argv0% [list $::argv0] \ - %argv% [list $::argv] \ - %argc% [list $::argc] \ - %replthread% [thread::id] \ - %replthread_cond% $codethread_cond \ - %replthread_interp% [list $opt_callback_interp] \ - %tmlist% [list [tcl::tm::list]] \ - %autopath% [list $::auto_path] \ - %lib_epoch% [list $::punk::libunknown::epoch]\ + set scriptmap [list %args% [list $opts] {*}{ + } %argv0% [list $::argv0] {*}{ + } %argv% [list $::argv] {*}{ + } %argc% [list $::argc] {*}{ + } %replthread% [thread::id] {*}{ + } %replthread_cond% $codethread_cond {*}{ + } %replthread_interp% [list $opt_callback_interp] {*}{ + } %tmlist% [list [tcl::tm::list]] {*}{ + } %autopath% [list $::auto_path] {*}{ + } %lib_epoch% [list $::punk::libunknown::epoch] {*}{ + } ] #scriptmap applied at end to satisfy silly editor highlighting. set init_script { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm index e147b4d1..eb439c6e 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -81,16 +81,18 @@ namespace eval punkcheck { } return $record_list } - proc save_records_to_file {recordlist punkcheck_file {trigger {}}} { - if {$trigger ne ""} { - puts stderr "\x1b\[36mSaving [llength $recordlist] records to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m" - } + proc save_records_to_file {recordlist punkcheck_file {trigger {}} {debugchannel ""}} { set newtdl [punk::tdl::prettyprint $recordlist] set linecount [llength [split $newtdl \n]] + + if {$debugchannel ne "" && $trigger ne ""} { + puts $debugchannel "\x1b\[36mSaving [llength $recordlist] records as $linecount lines to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m" + } #puts stdout $newtdl set fd [open $punkcheck_file w] chan configure $fd -translation binary puts -nonewline $fd $newtdl + flush $fd close $fd return [list recordcount [llength $recordlist] linecount $linecount] } @@ -170,8 +172,10 @@ namespace eval punkcheck { variable o_path_cksum_cache variable o_fileset_record variable o_installer ;#parent object + variable o_debugchannel constructor {installer rel_sourceroot rel_targetroot args} { set o_installer $installer + set o_debugchannel [$installer get_debugchannel] set o_operation_start_ts "" set o_path_cksum_cache [dict create] set o_operation "" @@ -321,7 +325,7 @@ namespace eval punkcheck { set extractioninfo [punkcheck::recordlist::extract_or_create_fileset_record $o_targets $record_list] set o_fileset_record [dict get $extractioninfo record] - set record_list [dict get $extractioninfo recordset] + set record_list [dict get $extractioninfo recordset] ;#if fileset wasn't present, same as original record_list, otherwise full recordset with the fileset record removed, ready for reinsertion. set isnew [dict get $extractioninfo isnew] set oldposition [dict get $extractioninfo oldposition] unset extractioninfo @@ -530,7 +534,9 @@ namespace eval punkcheck { variable o_record_list variable o_active_event variable o_events - constructor {installername punkcheck_file} { + variable o_debugchannel + constructor {installername punkcheck_file {debugchannel ""}} { + set o_debugchannel $debugchannel set o_active_event "" set o_name $installername @@ -539,6 +545,8 @@ namespace eval punkcheck { set o_targetroot "" set o_rel_sourceroot "" set o_rel_targetroot "" + set o_record_list [list] + #todo - validate punkcheck file location further?? set punkcheck_folder [file dirname $o_checkfile] if {![file isdirectory $punkcheck_folder]} { @@ -546,6 +554,55 @@ namespace eval punkcheck { } my load_all_records + if {![llength $o_record_list] && $o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[32mNo existing records found in punkcheck file '$o_checkfile' for installer '$installername'. Starting with empty record list.\x1b\[m" + } else { + #verify no duplicate installer records for this installer. + #JMN + set sanity_dict [dict create] + set insane "" + foreach rec $o_record_list { + if {[dict get $rec tag] eq "INSTALLER"} { + set name [dict get $rec -name] + if {[dict exists $sanity_dict $name]} { + #todo - warn - duplicate record for same targetlist - shouldn't happen as we should be using get_file_record to find existing records + if {$o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[31mpunkcheck installtrack - multiple INSTALLER records with same name '$name'\x1b\[m" + } + set insane "$name" + break + } + dict set sanity_dict $name {} + } + } + if {$insane ne ""} { + set msg "Sanity check: punkcheck file '$o_checkfile' contains multiple records for INSTALLER -name '$insane'." + append msg \n "This may indicate a problem such as multiple concurrent installtrack instances using the same punkcheck file," + append msg \n " or a previous installtrack instance that did not complete properly." + append msg \n " Do you want to DELETE the .punkcheck file?" + append msg \n " It is safe to delete .punkcheck files, at the cost of loss of history and checksums used to optimize installs." + append msg \n " They are a record of installation events and checksums used to avoid unnecessary reinstalls." + append msg \n " If not confirmed, an error will be raised - likely aborting the current operation." + append msg \n "confirm deletion and continue by regenerating the file, by typing the 3 letters: 'yes'." + set answer [punk::lib::askuser $msg] + if {[string tolower $answer] ne "yes"} { + error "Failing due to sanity check failure. User did not confirm with 'yes'." + } + if {[file exists $o_checkfile] && [file isfile $o_checkfile]} { + file delete $o_checkfile + } + if {[file exists $o_checkfile]} { + error "Failed to delete punkcheck file '$o_checkfile' after sanity check failure. Please investigate and resolve the issue before proceeding." + } + set o_record_list [list] + } else { + if {$o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[32mSanity check passed: no duplicate INSTALLER records found for installer '$installername' in punkcheck file '$o_checkfile'.\x1b\[m" + } + } + unset sanity_dict + } + set resultinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list] set existing_header_posn [dict get $resultinfo position] if {$existing_header_posn == -1} { @@ -580,6 +637,9 @@ namespace eval punkcheck { method get_checkfile {} { return $o_checkfile } + method get_debugchannel {} { + return $o_debugchannel + } #call set_source_target before calling start_event/end_event #each event can have different source->target pairs - but may often have same, so set on installtrack as defaults. Only persisted in event records. @@ -2164,10 +2224,10 @@ namespace eval punkcheck { return [dict create changed $changed unchanged $unchanged] } - #assume only one for name - use first encountered + #assume only one for name - use first encountered? proc get_installer_record {name record_list} { set posn 0 - set found_posn -1 + set found_posns [list] set record "" #puts ">>>> checking [llength $record_list] punkcheck records" foreach rec $record_list { @@ -2175,12 +2235,20 @@ namespace eval punkcheck { if {[dict get $rec -name] eq $name} { set found_posn $posn set record $rec - break + lappend found_posns $posn } } incr posn } - return [list position $found_posn record $record] + if {[llength $found_posns] > 1} { + error "punkcheck::recordlist::get_installer_record - multiple installer records with name '$name' found at positions $found_posns" + } elseif {[llength $found_posns] == 0} { + return [list position -1 record ""] + } else { + #single record found + return [list position [lindex $found_posn 0] record $record] + } + } proc new_installer_record {name args} { @@ -2374,7 +2442,8 @@ namespace eval punkcheck { set fileset_record [dict create tag FILEINFO -targets $relative_target_paths body {}] } else { #set recordset [lreplace $recordset[unset recordset] $existing_posn $existing_posn] - set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn] + #set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn] + ledit recordset $existing_posn $existing_posn set isnew 0 set fileset_record [dict get $fetch_record_result record] } diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm index 6d357bd7..42ae92cd 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm @@ -2728,13 +2728,13 @@ namespace eval shellfilter { ::shellfilter::log::write $runtag "checking for redirections in $commandlist" #sometimes we see a redirection without a following space e.g >C:/somewhere #normalize - switch -regexp -- $lastitem\ - {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - }\ - {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm new file mode 100644 index 00000000..a0fa060a --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm @@ -0,0 +1,3399 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + variable is_enabled 0 + + proc disable {} { + variable is_enabled + set is_enabled 0 + proc ::shellfilter::log::open {tag settingsdict} {} + proc ::shellfilter::log::write {tag msg} {} + proc ::shellfilter::log::write_sync {tag msg} {} + proc ::shellfilter::log::close {tag} {} + } + + proc enable {} { + variable is_enabled + set is_enabled 1 + #'tag' is an identifier for the log source. + #(well.. really it's a common *target* of file and/or syslog host:port which can be written to from any thread that uses the tag) + #the terminology here is kinda ratshit. + + # each tag will use it's own thread to write to the configured log target + proc ::shellfilter::log::open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #review new_worker/assign_worker? + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc ::shellfilter::log::write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc ::shellfilter::log::write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc ::shellfilter::log::close {tag} { + #shellthread::manager::close_worker $tag + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + #why not use string is boolean? + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } + if {[catch {package require shellthread}]} { + shellfilter::log::disable + } else { + shellfilter::log::enable + } + +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + #review + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize finalize write flush clear] + } + method finalize {ch} { + my destroy + } + method clear {ch} { + return + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + #method flush {ch} { + # return "" + #} + method flush {transform_handle} { + #puts stdout "" + #review - just clear o_encbuf and emit nothing? + #we wouldn't have a value there if it was convertable from the channel encoding? + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #test with set x [string repeat " \U1f6c8" 2043] + #or + #test with set x [string repeat " \U1f6c8" 683] + #most windows terminals (at least) may emit two unrecognised chars "??" at the end + + #Our goal with the while loop here is to avoid encoding conversion errors + #the source of the bogus chars in terminals is unclear. + #Alacritty on windows doesn't seem to have the problem, but wezterm,cmd,windows terminal do. + + #set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + foreach v $o_datavars { + append $v $stringdata + } + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_encbuf + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read drain write flush clear finalize] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + return + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {transform_handle bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $stringdata + #return $bytes + return [::tcl::string::range $inputbytes 0 end-$tail_offset] + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write flush finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] + ::shellfilter::log::write $o_logsource $stringdata + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set o_encbuf "" + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + #set logdata [encoding convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return + } + } + + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $stringdata + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [::tcl::dict::get $tf -encoding] + set o_encbuf "" + if {[::tcl::dict::exists $tf -junction]} { + set o_is_junction [::tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read write clear flush drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + return + } + method watch {transform_handle events} { + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method flush {transform_handle} { + return "" + } + #method write {transform_handle bytes} { + # #broken due to occasional unexpected byte sequence + # set instring [encoding convertfrom $o_enc $bytes] + # set outstring [punk::ansi::ansistrip $instring] + # return [encoding convertto $o_enc $outstring] + #} + method write {transform_handle bytes} { + #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally + #bytes can break at arbitrary points making encoding conversions invalid. + + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + set outstring [punk::ansi::ansistrip $stringdata] + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + set sixelstart_re {\x1bP([;0-9]*)q} ;#7-bit - todo 8bit + #todo kitty graphics \x1b_G... + #todo iterm graphics + + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_encbuf ;#buffering for partial encoding bytes + variable o_colour + variable o_do_colour + variable o_do_colourlist + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered ;#buffering for partial ansi codes + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + #warning - we can't merge certain extended attributes such as undercurly into single SGR escape sequence + #while some terminals may handle these extended attributes even when merged - we need to cater for those that + #don't. Keeping them as a separate escape allows terminals that don't handle them to ignore just that code without + #affecting the interpretation of the other codes. + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_colourlist [punk::ansi::ta::get_codes_single $o_do_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_colourlist {} + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_encbuf "" + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + + + #todo - track when in sixel,iterm,kitty graphics data - can be very large + method Trackcodes {chunk} { + #note - caller can use 2 resets in a single unit to temporarily reset to no sgr (override ansiwrap filter) + #e.g [a+ reset reset] (0;0m vs 0;m) + + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + #Note 8-bit csi \x9b has already been mapped in the chunk to 7-bit form \x1b\[ by the caller - so we only need to check for \x1b here + #(under review - ideally we might not want to normalize 8-bit to 7-bit in a channel transform)) + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + #REVIEW - ta::detect won't detect SOS without paired ST for things like PM + # ta::detectcode will - but then split_codes_single will treat unpaired SOS as text? + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # append emit $o_do_colour$pt$o_do_normal + # #append emit $pt + #} else { + # append emit $pt + #} + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list {*}{ + \x1b\[ 7CSI + \x9b 8CSI + \x1b\( 7GFX + }] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + set code_endswith_m [expr {[tcl::string::index $code end] eq "m"}] + if {$code_endswith_m} { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + #review this normalizing of reset to a single form. + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } + } + + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$trailing_pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$trailing_pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$trailing_pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + #if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + # append emit $o_do_colour$trailing_pt$o_do_normal + #} else { + # append emit $trailing_pt + #} + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #REVIEW - this holding a buffer without emitting as we go is ugly. + # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. + # - we'd then need to detect the appropriate close to restart splitting and codestacking + # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. + + + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string index $buf end] eq "\x1b" && [string first \x1b $buf] == [string length $buf]-1} { + #string index in first part of && clause to avoid some unneeded scans of whole string for this test + #we can't use 'string last' - as we need to know only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit $o_do_colour[string range $buf 0 end-1]$o_do_normal + #set emit [string range $buf 0 end-1] + set buf "" + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + set buf "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } else { + set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + set o_buffered "" + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } + } + } + if {$emit_anyway} { + #assert: any time emit_anyway == 1 buf already contains all of previous o_buffered and o_buffered has been cleared. + + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # set emit $o_do_colour$buf$o_do_normal + #} else { + # set emit $buf + #} + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + return [list initialize write flush read drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear of it the init call + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + method flush {transform_handle} { + #puts stdout "" + set inputbytes $o_buffered$o_encbuf + set emit [tcl::encoding::convertto $o_enc $inputbytes] + set o_buffered "" + set o_encbuf "" + return $emit + } + method write {transform_handle bytes} { + #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally + #bytes can break at arbitrary points making encoding conversions invalid. + + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + #------------------------------------------------------ + # REVIEW + #Trackcodes logic is primarily designed for 7-bit codes + #It would be complex for it to support 8-bit as well + #- we can do a simple pre-map to convert 8-bit CSI to 7-bit CSI before processing + #we already normalize things like resets to a single 7-bit form anyway. + #review - is there a need for an ansiwrap channel that preserves 8-bit codes? + #8-bit are rarely used these days - and many terminals don't support them. + #We could take the view here that we should understand them but not emit them in general. + #Nonetheless - converting them on a channel transform like this is potentially suprising in some circumstances, + #and we don't necessarily know the intent of both the producer and consumer of the stream. + set stringdata [string map [list \x9b \x1b\[ ] $stringdata] + #------------------------------------------------------ + + set streaminfo [my Trackcodes $stringdata] + set emit [dict get $streaminfo emit] + + #review - wrapping already done in Trackcodes + #if {[dict get $streaminfo stacksize] == 0} { + # #no ansi on the stack - we can wrap + # #review + # set outstring "$o_do_colour$emit$o_do_normal" + #} else { + #} + #if {[llength $o_codestack]} { + # set outstring [punk::ansi::codetype::sgr_merge_singles $o_codestack]$emit + #} else { + # set outstring $emit + #} + #set outstring $emit + + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $emit] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + namespace export {[a-z]*} + namespace ensemble create + #todo - implement as oo ? + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + set pipecount [dict size $pipelines] + set tabletitle "$pipecount pipelines active" + set t [textblock::class::table new $tabletitle] + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -show_edge 0 + foreach rec $stack { + set handle [punk::lib::dict_getdef $rec -handle ""] + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set settings [punk::lib::dict_getdef $rec -settings ""] + $tbl_inner add_row [list $id $transform $handle $settings] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k $lc $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + chan pop $localchan + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + catch {chan close $localchan} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + variable pipelines + if {![dict exists $pipelines $pipename]} { + puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + } else { + if {$idposn < 0} { + ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + #set stack [lreplace $stack[set stack {}] $idposn end] + # 2026-05-19 + ledit stack $idposn end + + + #pop all chans before adding anything back in! + foreach p $poplist { + chan pop $localchan + } + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + } + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + } + dict set pipelines $pipename stack $stack + } + #JMNJMN 2025 review! + #show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + #set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] + # 2026-05-19 + ledit stack $bottom_pop_posn end + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + # add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + + #JMNJMN + #show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + if {[catch { + ::shellfilter::log::open $tag {-syslog ""} + } err]} { + #e.g safebase interp can't load required modules such as shellthread (or Thread) + puts stderr "shellfilter::show_pipeline cannot open log" + return + } + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create {*}{ + -tag redirected_$chan + -syslog "" + -file "" + }] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list {*}{ + \r cr + \n lf + {"} doublequote + {'} singlequote + "`" backquote + "^" caret + \t tab + " " sp + "\[" lsquare + "\]" rsquare + "(" lbracket + ")" rbracket + "\{" lbrace + "\}" rbrace + \\ backslash + / forwardslash + }] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list {*}{ + } "\"" [list "\"" "\"" "\""] {*}{ + } {^} [list "\"" "\"" "^"] {*}{ + } "'" [list "'" "'" "'"] {*}{ + } "\{" [list "\{" "\}" "\}"] {*}{ + } {[} [list {[} {]} {]}] {*}{ + } + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + set runtag "shellfilter-run" + #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create {*}{ + -teehandle command + -outchan stdout + -errchan stderr + -inchan stdin + -tclscript 0 + }] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if {$experiment} { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + } + #puts "shellfilter::run finished call" + + #------------------------- + #warning - without flush stdout - we can get hang, but only on some terminals + # - mechanism for this problem not understood! + flush stdout + flush stderr + #------------------------- + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + shellfilter::stack::remove stdout $id_out + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stderr $id_in + + #puts stderr "shellfilter::run complete..." + + #chan configure stderr -buffering line + #flush stdout + + + ::shellfilter::log::write $runtag " return '$exitinfo'" + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list {*}{ + -timeout + -outprefix + -errprefix + -debug + -copytempfile + -outbuffering + -errbuffering + -inbuffering + -readprocesstranslation + -outtranslation + -stdinhandler + -outchan + -errchan + -inchan + -teehandle + }] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {[llength $args] % 2} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create {*}{ + -outchan stdout + -errchan stderr + -inchan stdin + -outbuffering none + -errbuffering none + -readprocesstranslation auto + -outtranslation lf + -inbuffering none + -timeout 900000 + -outprefix "" + -errprefix "" + -debug 0 + -copytempfile 0 + -stdinhandler "" + }] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set ::shellfilter::shellcommandvars($call_id,timeoutid) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + + #--------------- + #JMN 2025 + # e.g cannot run ansible cmdline tools if non-blocking + #chan configure $wrerr -blocking 0 + #------------------ + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list {*}{ + } [chan configure $inchan -buffering] {*}{ + } [chan configure $outchan -buffering] {*}{ + } [chan configure $errchan -buffering] {*}{ + } + ] + + set remember_in_out_err_translation [list {*}{ + } [chan configure $inchan -translation] {*}{ + } [chan configure $outchan -translation] {*}{ + } [chan configure $errchan -translation] {*}{ + } + ] + + + + + #chan configure $inchan -buffering none -blocking 1 ;#test + #chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $inchan -buffering $inbuffering + + + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + #REVIEW! + #if the child process takes a while to begin reading stdin - the data on stdin between when we stopped the parent chan event handler and when the child gets data, + #seems to stay buffered somewhere. It is then read by the parent, after the child returns. (ie not lost, but out-of-order) + #This can be apparent sometimes even with fast typing upon calling an executable. (e.g occasionally even vim - but seems to be timing based so might only happen first time if at all) + # see scriptlib/stdin_race.tcl etc test files. + #similar problem with python & perl - issue seems to be in libc or OS buffering behaviour for standard channels. + #note that zig (repo/jn/zig/stdin_race) seems to avoid this issue - todo - make zig based binary extension for open/exec? + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + #if {$other ni [chan names]} { + # set $waitfor stderr + #} + if {[catch {chan configure $other}]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + #if {$other ni [chan names]} { + # set $waitfor stdout + #} + if {[catch {chan configure $other}]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + #jmn test 2025 + flush $outchan + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + #if {$other ni [chan names]} { + # set $waitfor stdout + #} + if {[catch {chan configure $other}]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + #review - stdin??? + set ::shellfilter::shellcommandvars($call_id,timeoutid) [after $timeout [string map [list %cpids% $command_pids %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + #killing the task (on windows) doesn't seem to work if done after we close the output channels + catch {puts stderr "timeout - closing.";flush stderr} + set command_pids "{%cpids%}" + if {[llength $command_pids]} { + set pid [lindex $command_pids 0] + if {$::tcl_platform(platform) eq "windows"} { + set killcmd [list [auto_execok taskkill] /F /PID $pid] + } else { + #set killcmd [list kill -9 $pid] + set killcmd [list kill -TERM $pid] + } + if {[catch { + exec {*}$killcmd + } errM]} { + puts stderr "Failed to kill '$pid': errMsg $errM" + flush stderr + } + } + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }]] + + + vwait $waitvar + after cancel $::shellfilter::shellcommandvars($call_id,timeoutid) + + #puts stderr "waitvar:[set $waitvar]" + #flush stderr + #if {[set $waitvar] eq "timeout"} { + # #note: attempting to kill a process here (after channels closed) doesn't work (on windows at least) + # puts stderr "command_pids: $command_pids" + # flush stderr + #} + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode closed_by:[set $waitvar]" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.2.2 +}] diff --git a/src/runtime/mapvfs.config b/src/runtime/mapvfs.config index 067e8a76..8243abd5 100644 --- a/src/runtime/mapvfs.config +++ b/src/runtime/mapvfs.config @@ -17,7 +17,13 @@ tclkit86bi.exe {punk8win.vfs punkbi kit} #tclkit-win64-dyn.exe {punk86bawt.vfs punkbawt kit} -tclkit-win64-dyn.exe {punk86bawt.vfs punksys kit} + +#------------------------------------------------------------------------ +#broken 'lreplace' - but runtime beforehand is ok - thread library issue? +#tclkit-win64-dyn.exe {punk86bawt.vfs punksys kit} +#------------------------------------------------------------------------ +#same kit with different .vfs is ok +tclkit-win64-dyn.exe {punk8win.vfs punksys kit} #tclkit87a5.exe {punk86.vfs punk87} {punk.vfs punkmain} diff --git a/src/vendorlib_tcl8/win32-x86_64/Memchan2.3/Memchan23.dll b/src/vendorlib_tcl8/win32-x86_64/Memchan2.3/Memchan23.dll new file mode 100644 index 0000000000000000000000000000000000000000..cfc4603b6bb065686dd25ee5ef14223929bde840 GIT binary patch literal 37376 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 literal 0 HcmV?d00001 diff --git a/src/vendorlib_tcl8/win32-x86_64/Memchan2.3/pkgIndex.tcl b/src/vendorlib_tcl8/win32-x86_64/Memchan2.3/pkgIndex.tcl new file mode 100644 index 00000000..ac83c2b5 --- /dev/null +++ b/src/vendorlib_tcl8/win32-x86_64/Memchan2.3/pkgIndex.tcl @@ -0,0 +1,2 @@ +package ifneeded Memchan 2.3 \ + [list load [file join $dir Memchan23.dll]] diff --git a/src/vendormodules/include_modules.config b/src/vendormodules/include_modules.config index b2b0e247..a8ecc53b 100644 --- a/src/vendormodules/include_modules.config +++ b/src/vendormodules/include_modules.config @@ -1,41 +1,42 @@ #todo - change to include_modules.toml #aim is to be programatically editable whilst retaining comments -set local_modules [list\ - c:/repo/jn/tclmodules/fauxlink/modules fauxlink\ - c:/repo/jn/tclmodules/gridplus/modules gridplus\ - c:/repo/jn/tclmodules/modpod/modules modpod\ - c:/repo/jn/tclmodules/packageTest/modules packagetest\ - c:/repo/jn/tclmodules/tablelist/modules tablelist\ - c:/repo/jn/tclmodules/tablelist/modules tablelist_tile\ - c:/repo/jn/tclmodules/tomlish/modules tomlish\ - c:/repo/jn/tclmodules/tomlish/modules test::tomlish\ - c:/repo/jn/tclmodules/dictn/modules dictn\ - c:/repo/jn/tclmodules/dollarcent/modules dollarcent\ - c:/repo/jn/tclmodules/pattern/modules pattern\ - c:/repo/jn/tclmodules/pattern/modules pattern2\ - c:/repo/jn/tclmodules/pattern/modules patterncmd\ - c:/repo/jn/tclmodules/pattern/modules patternlib\ - c:/repo/jn/tclmodules/pattern/modules patterncipher\ - c:/repo/jn/tclmodules/pattern/modules metaface\ - c:/repo/jn/tclmodules/pattern/modules patternpredator1\ - c:/repo/jn/tclmodules/pattern/modules patternpredator2\ - c:/repo/jn/tclmodules/pattern/modules patterndispatcher\ - c:/repo/jn/tclmodules/pattern/modules treeobj\ - c:/repo/jn/tclmodules/pattern/modules pattern::ms\ - c:/repo/jn/tclmodules/pattern/modules pattern::IPatternBuilder\ - c:/repo/jn/tclmodules/pattern/modules pattern::IPatternInterface\ - c:/repo/jn/tclmodules/pattern/modules pattern::IPatternSystem\ - c:/repo/jn/tclmodules/pattern/modules test::pattern\ - c:/repo/jn/tclmodules/voo/modules voo\ - c:/repo/jn/tarjar/modules tarjar\ -] +set local_modules [list {*}{ + c:/repo/jn/tclmodules/fauxlink/modules fauxlink + c:/repo/jn/tclmodules/gridplus/modules gridplus + c:/repo/jn/tclmodules/modpod/modules modpod + c:/repo/jn/tclmodules/packageTest/modules packagetest + c:/repo/jn/tclmodules/tablelist/modules tablelist + c:/repo/jn/tclmodules/tablelist/modules tablelist_tile + c:/repo/jn/tclmodules/tomlish/modules tomlish + c:/repo/jn/tclmodules/tomlish/modules test::tomlish + c:/repo/jn/tclmodules/dictn/modules dictn + c:/repo/jn/tclmodules/dollarcent/modules dollarcent + c:/repo/jn/tclmodules/pattern/modules pattern + c:/repo/jn/tclmodules/pattern/modules pattern2 + c:/repo/jn/tclmodules/pattern/modules patterncmd + c:/repo/jn/tclmodules/pattern/modules patternlib + c:/repo/jn/tclmodules/pattern/modules patterncipher + c:/repo/jn/tclmodules/pattern/modules metaface + c:/repo/jn/tclmodules/pattern/modules patternpredator1 + c:/repo/jn/tclmodules/pattern/modules patternpredator2 + c:/repo/jn/tclmodules/pattern/modules patterndispatcher + c:/repo/jn/tclmodules/pattern/modules treeobj + c:/repo/jn/tclmodules/pattern/modules pattern::ms + c:/repo/jn/tclmodules/pattern/modules pattern::IPatternBuilder + c:/repo/jn/tclmodules/pattern/modules pattern::IPatternInterface + c:/repo/jn/tclmodules/pattern/modules pattern::IPatternSystem + c:/repo/jn/tclmodules/pattern/modules test::pattern + c:/repo/jn/tclmodules/voo/modules voo + c:/repo/jn/tarjar/modules tarjar + c:/repo/jn/tclmodules/sqids-tcl/modules sqids +}] #moved overtype into punkshell project # c:/repo/jn/tclmodules/overtype/modules overtype -set fossil_modules [dict create\ -] +set fossil_modules [dict create {*}{ +}] -set git_modules [dict create\ -] \ No newline at end of file +set git_modules [dict create {*}{ +}] \ No newline at end of file diff --git a/src/vendormodules/sqids-0.3.1.tm b/src/vendormodules/sqids-0.3.1.tm new file mode 100644 index 00000000..b7fddc82 --- /dev/null +++ b/src/vendormodules/sqids-0.3.1.tm @@ -0,0 +1,931 @@ +#Do not update version of this file. Update sqids-buildversion.txt and run make.tcl to update the version in this file and copy to modules folder. +package require Tcl 8.6- + +#MIT license +#Julian Noble 2026 + +#example: +# % package require sqids +# % set s1 [sqids::idscope new] +# ::oo::Obj275 +# % $s1 encode {1 2 3} +# 86Rf07 +# % $s1 decode 86Rf07 +# 1 2 3 + + +namespace eval sqids { + oo::class create idscope { + variable o_alphabet + variable o_alphabet_configured + variable o_alpha_re + variable o_minlength + variable o_blocklist + variable o_maxsafeinteger + #note that methods beginning with uppercase letters are private. + constructor {args} { + set defaults [dict create {*}{ + -alphabet "" + -minlength "" + -blocklist "" + -maxsafeinteger "" + }] + if {[llength $args] %2 !=0} { + error "sqids::idscope constructor: Require option value pairs. Known options:[dict keys $defaults]." + } + set useropts [dict create] + set explicit_empty_blocklist 0 ;#as opposed to default due to being unspecified. + dict for {k v} $args { + set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist -maxsafeinteger} $k] + switch -exact -- $fullmatch { + -alphabet - -minlength - -maxsafeinteger { + dict set useropts $fullmatch $v + } + -blocklist { + if {[llength $v] == 0} { + set explicit_empty_blocklist 1 + } + dict set useropts -blocklist $v + } + default { + error "sqids::idscope constructor: unknown option '$k'. Known options:[dict keys $defaults]." + } + } + } + set opts [dict merge $defaults $useropts] + + set opt_alphabet [dict get $opts -alphabet] + if {$opt_alphabet eq ""} { + set o_alphabet $::sqids::data::default_alphabet + } else { + if {[string length $opt_alphabet] < 3} { + error "sqids::idscope constructor: -alphabet length must be at least 3." + } + #review - deny multibyte + if {[regexp {[^\u00-\u7F]} $opt_alphabet]} { + error "sqids::idscope constructor: -alphabet must not contain multibyte characters." + } + if {[regexp {(.).*\1} $opt_alphabet]} { + error "sqids::idscope constructor: -alphabet must contain unique characters." + } + set o_alphabet $opt_alphabet + } + set o_alphabet_configured $o_alphabet ;#for use in public method alphabet, which returns the configured alphabet in the order it was configured, not the shuffled order used for encoding. + set alphamatch [string map [list . \\. \[ \\\[ \] \\\] \{ \\\{ \} \\\}] $o_alphabet] ;#review + set o_alpha_re "^\[$alphamatch\]+\$" ;#independent of shuffled order. + + set o_alphabet [my shuffle $o_alphabet[set o_alphabet {}]] + + set opt_minlength [dict get $opts -minlength] + if {$opt_minlength eq ""} { + set o_minlength $::sqids::data::default_minlength + } else { + set maxval 255 + if {![string is integer -strict $opt_minlength] || $opt_minlength < 0 || $opt_minlength > $maxval} { + error "sqids constructor: -minlength must be an integer from 0 to $maxval inclusive." + } + set o_minlength $opt_minlength + } + set opt_blocklist [dict get $opts -blocklist] + if {!$explicit_empty_blocklist && $opt_blocklist eq ""} { + set o_blocklist $::sqids::data::default_blocklist + #default blocklist is already in lowercase. + } else { + set o_blocklist $opt_blocklist + set o_blocklist [string tolower $o_blocklist] + } + #Considered pruning blocklist entries that are 3 chars or less, + #or that contain characters not in the alphabet, as they will never match any id and just add overhead + #to the is_blocked method. + #This however adds some object instantiation overhead. + #counterpoint - caller should provide an appropriate blocklist for the supplied alphabet. + + set opt_maxsafeinteger [dict get $opts -maxsafeinteger] + if {$opt_maxsafeinteger eq ""} { + set o_maxsafeinteger $::sqids::data::MAX_SAFE_INTEGER + } else { + #accept arbitrarily large values as long as they're valid bignum integers. + if {[package vsatisfies [info tclversion] 8.7-]} { + if {![string is integer -strict $opt_maxsafeinteger] || $opt_maxsafeinteger < 0} { + error "sqids constructor: -maxsafeinteger must be a non-negative integer." + } + } else { + if {![string is entier -strict $opt_maxsafeinteger] || $opt_maxsafeinteger < 0} { + error "sqids constructor: -maxsafeinteger must be a non-negative integer." + } + } + set o_maxsafeinteger $opt_maxsafeinteger + } + + } + method config {{option {}}} { + #introspection method. + #return a dict of the configured options if no option specified, otherwise return the value of the specified option. + + #no facility is provided to change options after construction as a new idscope object should be used for different configurations (different scope of sqid ids). + #note that -alphabet refers to the configured alphabet in the order it was configured, not the shuffled order used for encoding. + if {$option eq ""} { + # + return [dict create {*}{ + } -blocklist $o_blocklist {*}{ + } -maxsafeinteger $o_maxsafeinteger {*}{ + } -minlength $o_minlength {*}{ + } -alphabet $o_alphabet_configured {*}{ + } + ] + } + set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist -maxsafeinteger} $option] + switch -exact -- $fullmatch { + -alphabet {return $o_alphabet_configured} + -minlength {return $o_minlength} + -blocklist {return $o_blocklist} + -maxsafeinteger {return $o_maxsafeinteger} + default { + error "sqids::idscope config: unknown option '$option'. Known options:-alphabet -minlength -blocklist -maxsafeinteger." + } + } + } + #review tcl8.7 behaves like tcl 9 + #tcl 8.7 wasn't ever officially released (and won't be) - but it was available for a while and may exist in the wild. + if {[package vsatisfies [info tclversion] 8.7-]} { + #'string is integer' for tcl versions 8.7 and above supports bignums, which can be arbitrarily large. + method encode {numlist} { + if {[llength $numlist] == 0} {return} + #cannot encode negative numbers, or non-integers. + foreach num $numlist { + if {![string is integer -strict $num] || $num < 0 || $num > $o_maxsafeinteger} { + error "sqids encode: can only encode integers from 0 to $o_maxsafeinteger. Invalid value: '$num'" + } + } + return [my EncodeNumbers $numlist] + } + } else { + #In tcl 8.6, 'string is integer' is limited to 2**32-1, use the now deprecated 'string is entier'. + #Otherwise - integer operations still support bignums. + #(versions below 8.6 not supported by this modules) + method encode {numlist} { + if {[llength $numlist] == 0} {return} + #cannot encode negative numbers, or non-integers. + foreach num $numlist { + if {![string is entier -strict $num] || $num < 0 || $num > $o_maxsafeinteger} { + error "sqids encode: can only encode integers from 0 to $o_maxsafeinteger. Invalid value: '$num'" + } + } + return [my EncodeNumbers $numlist] + } + } + method EncodeNumbers {numlist {increment 0}} { + #assert number of letters in o_alphabet and number of letters in local alpha are the same and don't effectively change during this function. + #('set alpha {}' in calls to my shuffle is an optimization to avoid shared string and copy-on-write overhead. As alpha is set to the result, it doesn't violate the previous assertion.) + set alpha_len [string length $o_alphabet] + + if {$increment > $alpha_len} { + error "sqids EncodeNumbers: Reached max attempts to re-generate the ID" + } + + set offset [llength $numlist] + set i -1 + foreach v $numlist { + incr i + set x [scan [string index $o_alphabet [expr {$v % $alpha_len}]] %c] + set offset [expr {$offset + $x + $i}] + } + set offset [expr {$offset % $alpha_len}] + set offset [expr {($offset + $increment) % $alpha_len}] + set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] + set prefix [string index $alpha 0] + set alpha [string reverse $alpha] + set id $prefix + set i -1 + foreach num $numlist { + incr i + append id [my ToId $num [string range $alpha 1 end]] + if {$i < [llength $numlist]-1} { + append id [string index $alpha 0] + set alpha [my shuffle $alpha[set alpha {}]] + } + } + if {$o_minlength > [string length $id]} { + append id [string index $alpha 0] + while {$o_minlength - [string length $id] > 0} { + set alpha [my shuffle $alpha[set alpha {}]] + set numchars [expr {min($o_minlength - [string length $id],$alpha_len)}] + append id [string range $alpha 0 $numchars-1] + } + } + if {[my is_blocked $id]} { + set id [my EncodeNumbers $numlist [expr {$increment+1}]] + } + return $id + } + method is_blocked {id} { + #deliberately public method. + if {![llength $o_blocklist]} { + return 0 + } + #o_blocklist is stored in lowercase, so compare against lowercase id. + set idtest [string tolower $id] + set idlen [string length $idtest] + if {$idlen < 3} { + #sqids rule: short ids less than 3 chars will not be blocked. + #(this is from the FAQ - but spec (code in isblocked) seems to contradict - saying <= 3 must match exactly) + #however - most implementations filter out blocklist entries shorter than 3 at construction time. + #- so effectively the FAQ seems right but the reference code implements it in a very roundabout and unintuitive way. + #REVIEW. Why are there no tests regarding such short ids? + return 0 + } + if {$idlen == 3} { + if {$idtest in $o_blocklist} { + return 1 + } + } else { + foreach blocked $o_blocklist { + if {[string length $blocked] <= 3} { + #sqids rule: blocklist entries of 3 chars will only be blocked if they match the entire id exactly, + #so skip them in this loop as we've already checked for exact matches of the whole id when idlen == 3. + #note blocklist entries of 0 1 or 2 chars will never match any id - but in this implementation we leave + #it to the caller to provide a sensible blocklist. Nevertheless if we encounter them we will just skip them here. + continue + } + set posn [string first $blocked $idtest] + if {$posn == -1} { + continue + } + if {$posn == 0} { + #whether leetspeak or not, blocklist entries that match at the beginning of the id will be blocked. + return 1 + } + if {[regexp {[0-9]} $blocked]} { + #sqids rule: blocklist entries with digits (leetspeak) will only be blocked if the match is at the beginning or end of the id. + #we've already checked the beginning, so check the end now. + set endpos [expr {$idlen - [string length $blocked]}] + if {$posn == $endpos} { + return 1 + } + } else { + #sqids rule: blocklist entries without digits will be blocked if they match anywhere in the id. + return 1 + } + } + } + return 0 + } + method ToId {num alpha} { + set id "" + set alpha_len [string length $alpha] + while 1 { + set id [string index $alpha [expr {$num % $alpha_len}]]$id + set num [expr {$num / $alpha_len}] + if {$num == 0} break + } + return $id + } + method ToNumber {id alpha} { + set number 0 + set alpha_len [string length $alpha] + for {set i 0} {$i < [string length $id]} {incr i} { + set posn [string first [string index $id $i] $alpha] + set number [expr {($number * $alpha_len) + $posn}] + } + return $number + } + method shuffle {alpha} { + #public method. Primarily for internal use but can be used externally to examine the shuffled alphabet being used for encoding. + #e.g myscopeobject shuffle [myscopeobject config -alphabet] would show the shuffled alphabet being used for encoding. + #consistent shuffle (always produce the same result for same input) + set alpha_len [string length $alpha] + if {$alpha_len < 2} { + return $alpha + } + set chars [split $alpha ""] + + for {set i 0; set j [expr {$alpha_len-1}]} {$j > 0} {incr i; incr j -1} { + set iv [scan [lindex $chars $i] %c] + set jv [scan [lindex $chars $j] %c] + set r [expr {($i * $j + $iv + $jv) % $alpha_len}] + set item2 [lindex $chars $r] + lset chars $r [lindex $chars $i] + lset chars $i $item2 + } + return [join $chars ""] + } + method decode {id} { + if {$id eq ""} {return} + set result [list] + + if {![regexp $o_alpha_re $id]} { + puts stderr "sqids decode: ID contains characters not in the alphabet. re: $o_alpha_re id: $id" + return [list] + } + set prefix [string index $id 0] + set offset [string first $prefix $o_alphabet] + set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] + set alpha [string reverse $alpha] + set id [string range $id 1 end] + while {[string length $id] > 0} { + set separator [string index $alpha 0] + #split on first occurrence of separator only. + set sep_posn [string first $separator $id] + if {$sep_posn == -1} { + set parts [list $id] + } else { + set parts [list [string range $id 0 $sep_posn-1] [string range $id $sep_posn+1 end]] + } + #assert parts has 1 or 2 elements + + if {[lindex $parts 0] eq ""} { + #separator was at start of the id - done. + return $result + } + lappend result [my ToNumber [lindex $parts 0] [string range $alpha 1 end]] + if {[llength $parts] == 2} { + set alpha [my shuffle $alpha[set alpha {}]] + set id [lindex $parts 1] + } else { + set id "" + } + } + return $result + } + } +} +namespace eval sqids::data { + variable default_alphabet {abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789} + + variable default_minlength 0 + + #arbitrary 1 googol limit (approx 2**332). We could go much higher e.g [string repeat 9 1000] + #Tcl bignums are limited by available memory and max string length (e.g approx 2**30 bytes?) + #- but speed of encoding and decoding will degrade as the number increases. + #Can be overridden by providing a -maxsafeinteger option to the idscope constructor. + variable MAX_SAFE_INTEGER [expr {"1[string repeat 0 100]"}] + + variable default_blocklist { + 0rgasm + 1d10t + 1d1ot + 1di0t + 1diot + 1eccacu10 + 1eccacu1o + 1eccacul0 + 1eccaculo + 1mbec11e + 1mbec1le + 1mbeci1e + 1mbecile + a11upat0 + a11upato + a1lupat0 + a1lupato + aand + ah01e + ah0le + aho1e + ahole + al1upat0 + al1upato + allupat0 + allupato + ana1 + ana1e + anal + anale + anus + arrapat0 + arrapato + arsch + arse + ass + b00b + b00be + b01ata + b0ceta + b0iata + b0ob + b0obe + b0sta + b1tch + b1te + b1tte + ba1atkar + balatkar + bastard0 + bastardo + batt0na + battona + bitch + bite + bitte + bo0b + bo0be + bo1ata + boceta + boiata + boob + boobe + bosta + bran1age + bran1er + bran1ette + bran1eur + bran1euse + branlage + branler + branlette + branleur + branleuse + c0ck + c0g110ne + c0g11one + c0g1i0ne + c0g1ione + c0gl10ne + c0gl1one + c0gli0ne + c0glione + c0na + c0nnard + c0nnasse + c0nne + c0u111es + c0u11les + c0u1l1es + c0u1lles + c0ui11es + c0ui1les + c0uil1es + c0uilles + c11t + c11t0 + c11to + c1it + c1it0 + c1ito + cabr0n + cabra0 + cabrao + cabron + caca + cacca + cacete + cagante + cagar + cagare + cagna + cara1h0 + cara1ho + caracu10 + caracu1o + caracul0 + caraculo + caralh0 + caralho + cazz0 + cazz1mma + cazzata + cazzimma + cazzo + ch00t1a + ch00t1ya + ch00tia + ch00tiya + ch0d + ch0ot1a + ch0ot1ya + ch0otia + ch0otiya + ch1asse + ch1avata + ch1er + ch1ng0 + ch1ngadaz0s + ch1ngadazos + ch1ngader1ta + ch1ngaderita + ch1ngar + ch1ngo + ch1ngues + ch1nk + chatte + chiasse + chiavata + chier + ching0 + chingadaz0s + chingadazos + chingader1ta + chingaderita + chingar + chingo + chingues + chink + cho0t1a + cho0t1ya + cho0tia + cho0tiya + chod + choot1a + choot1ya + chootia + chootiya + cl1t + cl1t0 + cl1to + clit + clit0 + clito + cock + cog110ne + cog11one + cog1i0ne + cog1ione + cogl10ne + cogl1one + cogli0ne + coglione + cona + connard + connasse + conne + cou111es + cou11les + cou1l1es + cou1lles + coui11es + coui1les + couil1es + couilles + cracker + crap + cu10 + cu1att0ne + cu1attone + cu1er0 + cu1ero + cu1o + cul0 + culatt0ne + culattone + culer0 + culero + culo + cum + cunt + d11d0 + d11do + d1ck + d1ld0 + d1ldo + damn + de1ch + deich + depp + di1d0 + di1do + dick + dild0 + dildo + dyke + encu1e + encule + enema + enf01re + enf0ire + enfo1re + enfoire + estup1d0 + estup1do + estupid0 + estupido + etr0n + etron + f0da + f0der + f0ttere + f0tters1 + f0ttersi + f0tze + f0utre + f1ca + f1cker + f1ga + fag + fica + ficker + figa + foda + foder + fottere + fotters1 + fottersi + fotze + foutre + fr0c10 + fr0c1o + fr0ci0 + fr0cio + fr0sc10 + fr0sc1o + fr0sci0 + fr0scio + froc10 + froc1o + froci0 + frocio + frosc10 + frosc1o + frosci0 + froscio + fuck + g00 + g0o + g0u1ne + g0uine + gandu + go0 + goo + gou1ne + gouine + gr0gnasse + grognasse + haram1 + harami + haramzade + hund1n + hundin + id10t + id1ot + idi0t + idiot + imbec11e + imbec1le + imbeci1e + imbecile + j1zz + jerk + jizz + k1ke + kam1ne + kamine + kike + leccacu10 + leccacu1o + leccacul0 + leccaculo + m1erda + m1gn0tta + m1gnotta + m1nch1a + m1nchia + m1st + mam0n + mamahuev0 + mamahuevo + mamon + masturbat10n + masturbat1on + masturbate + masturbati0n + masturbation + merd0s0 + merd0so + merda + merde + merdos0 + merdoso + mierda + mign0tta + mignotta + minch1a + minchia + mist + musch1 + muschi + n1gger + neger + negr0 + negre + negro + nerch1a + nerchia + nigger + orgasm + p00p + p011a + p01la + p0l1a + p0lla + p0mp1n0 + p0mp1no + p0mpin0 + p0mpino + p0op + p0rca + p0rn + p0rra + p0uff1asse + p0uffiasse + p1p1 + p1pi + p1r1a + p1rla + p1sc10 + p1sc1o + p1sci0 + p1scio + p1sser + pa11e + pa1le + pal1e + palle + pane1e1r0 + pane1e1ro + pane1eir0 + pane1eiro + panele1r0 + panele1ro + paneleir0 + paneleiro + patakha + pec0r1na + pec0rina + pecor1na + pecorina + pen1s + pendej0 + pendejo + penis + pip1 + pipi + pir1a + pirla + pisc10 + pisc1o + pisci0 + piscio + pisser + po0p + po11a + po1la + pol1a + polla + pomp1n0 + pomp1no + pompin0 + pompino + poop + porca + porn + porra + pouff1asse + pouffiasse + pr1ck + prick + pussy + put1za + puta + puta1n + putain + pute + putiza + puttana + queca + r0mp1ba11e + r0mp1ba1le + r0mp1bal1e + r0mp1balle + r0mpiba11e + r0mpiba1le + r0mpibal1e + r0mpiballe + rand1 + randi + rape + recch10ne + recch1one + recchi0ne + recchione + retard + romp1ba11e + romp1ba1le + romp1bal1e + romp1balle + rompiba11e + rompiba1le + rompibal1e + rompiballe + ruff1an0 + ruff1ano + ruffian0 + ruffiano + s1ut + sa10pe + sa1aud + sa1ope + sacanagem + sal0pe + salaud + salope + saugnapf + sb0rr0ne + sb0rra + sb0rrone + sbattere + sbatters1 + sbattersi + sborr0ne + sborra + sborrone + sc0pare + sc0pata + sch1ampe + sche1se + sche1sse + scheise + scheisse + schlampe + schwachs1nn1g + schwachs1nnig + schwachsinn1g + schwachsinnig + schwanz + scopare + scopata + sexy + sh1t + shit + slut + sp0mp1nare + sp0mpinare + spomp1nare + spompinare + str0nz0 + str0nza + str0nzo + stronz0 + stronza + stronzo + stup1d + stupid + succh1am1 + succh1ami + succhiam1 + succhiami + sucker + t0pa + tapette + test1c1e + test1cle + testic1e + testicle + tette + topa + tr01a + tr0ia + tr0mbare + tr1ng1er + tr1ngler + tring1er + tringler + tro1a + troia + trombare + turd + twat + vaffancu10 + vaffancu1o + vaffancul0 + vaffanculo + vag1na + vagina + verdammt + verga + w1chsen + wank + wichsen + x0ch0ta + x0chota + xana + xoch0ta + xochota + z0cc01a + z0cc0la + z0cco1a + z0ccola + z1z1 + z1zi + ziz1 + zizi + zocc01a + zocc0la + zocco1a + zoccola + } +} + +package provide sqids [namespace eval sqids { + variable version + set version 0.3.1 +}] + diff --git a/src/vendormodules_tcl9/Thread-3.0b3.tm b/src/vendormodules_tcl9/Thread-3.0b3.tm deleted file mode 100644 index 0c1e89af1a5fef6e6768c11ea40d9e7b0b077db0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14273 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 rlY#c8z(<7Po1d<0XEsP2z;Z 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 diff --git a/src/vfs/_config/punk_main.tcl b/src/vfs/_config/punk_main.tcl index 14ad0e1c..003e33d5 100644 --- a/src/vfs/_config/punk_main.tcl +++ b/src/vfs/_config/punk_main.tcl @@ -23,6 +23,7 @@ # - and restrict package paths to those coming from a vfs (if not launched with 'dev' or 'os' first arg which allows external paths to remain) apply { args { + set ::punkargv $args set tclmajorv [lindex [split [info tclversion] .] 0] namespace eval ::punkboot { #This is somewhat ugly - but we don't want to do any 'package require' operations at this stage @@ -336,6 +337,7 @@ apply { args { #for a total of 11 possible final orderings. #(16 possible values for package_mode argument when you include the short-forms "",os,dev,os-dev,dev-os which always have 'internal' appended) set test_package_mode [lindex $args 0] + #puts stderr "main.tcl test_package_mode: '$test_package_mode'" switch -exact -- $test_package_mode { internal - @@ -859,49 +861,80 @@ apply { args { #-------------------------------------------------------- #Now that new 'package unknown' mechanism is in place - we can use package require - #assert arglist has had 'dev|os|os-dev etc' first arg removed if it was present. - if {[llength $arglist] == 1 && [lindex $arglist 0] eq "tclsh"} { - #called as dev tclsh or tclsh - #we would like to drop through to standard tclsh repl without launching another process - #tclMain.c doesn't allow it unless patched. - if {![info exists ::env(TCLSH_PIPEREPL)]} { - set is_tclsh_piperepl_env_true 0 - } else { - if {[string is boolean -strict $::env(TCLSH_PIPEREPL)]} { - set is_tclsh_piperepl_env_true $::env(TCLSH_PIPEREPL) - } else { + + + #tclsh,shell,shellspy + set subcommand [lindex $arglist 0] + switch -- $subcommand { + tclsh - shellspy - punk { + set subcommand_arglist [lrange $arglist 1 end] + } + default { + set subcommand punk + set subcommand_arglist $arglist + } + } + set ::argv $subcommand_arglist + set ::argc [llength $subcommand_arglist] + + switch -- $subcommand { + tclsh { + #called as dev tclsh or tclsh + #we would like to drop through to standard tclsh repl without launching another process + #tclMain.c doesn't allow it unless patched. + if {![info exists ::env(TCLSH_PIPEREPL)]} { set is_tclsh_piperepl_env_true 0 + } else { + if {[string is boolean -strict $::env(TCLSH_PIPEREPL)]} { + set is_tclsh_piperepl_env_true $::env(TCLSH_PIPEREPL) + } else { + set is_tclsh_piperepl_env_true 0 + } + } + if {!$is_tclsh_piperepl_env_true} { + puts stderr "tcl_interactive: $::tcl_interactive" + puts stderr "stdin: [chan configure stdin]" + puts stderr "Environment variable TCLSH_PIPEREPL is not set or is false or is not a boolean" + } else { + #according to env TCLSH_PIPEREPL and our commandline argument - tclsh repl is desired + #check if tclsh/punk has had the piperepl patch applied - in which case tclsh(istty) should exist + if {![info exists ::tclsh(istty)]} { + puts stderr "error: the runtime doesn't appear to have been compiled with the piperepl patch" + } + } + set ::tcl_interactive 1 + set ::tclsh(dorepl) 1 + if {[llength $subcommand_arglist]} { + set normscript [file normalize [lindex $subcommand_arglist 0]] + info script $normscript + set ::argv0 $normscript + set ::argv [lrange $subcommand_arglist 1 end] + set ::argc [llength $::argv] + #we are in an apply context here - so we need to uplevel to get the source to work as expected + uplevel 1 [list source [lindex $subcommand_arglist 0]] } } - if {!$is_tclsh_piperepl_env_true} { - puts stderr "tcl_interactive: $::tcl_interactive" - puts stderr "stdin: [chan configure stdin]" - puts stderr "Environment variable TCLSH_PIPEREPL is not set or is false or is not a boolean" - } else { - #according to env TCLSH_PIPEREPL and our commandline argument - tclsh repl is desired - #check if tclsh/punk has had the piperepl patch applied - in which case tclsh(istty) should exist - if {![info exists ::tclsh(istty)]} { - puts stderr "error: the runtime doesn't appear to have been compiled with the piperepl patch" - } - } - set ::tcl_interactive 1 - set ::tclsh(dorepl) 1 - } elseif {[lindex $arglist 0] eq "shellspy"} { - #pass through to shellspy commandline processor - #puts stdout "main.tcl launching app-shellspy" - package require app-shellspy - } elseif {[llength $arglist]} { - package require app-punkshell - } else { - #punk shell - #todo logger ? - #puts stdout "main.tcl launching app-punk. pkg names count:[llength [package names]]" - #puts ">> $::auto_path" - #puts ">>> [tcl::tm::list]" - #puts ">>>> [package unknown]" - package require app-punk - #app-punk starts repl - #repl::start stdin -title "main.tcl" + shellspy { + #pass through to shellspy commandline processor + package require app-shellspy + } + punk { + if {[llength $subcommand_arglist]} { + #puts stdout "main.tcl launching app-punkshell with args: $subcommand_arglist" + package require app-punkshell + } else { + #punk interactive shell + package require app-punk + #app-punk starts repl + } + } } + + + #if {[llength $arglist] == 1 && [lindex $arglist 0] eq "tclsh"} { + #} elseif {[lindex $arglist 0] eq "shellspy"} { + #} elseif {[llength $arglist]} { + #} else { + #} }} {*}$::argv diff --git a/src/vfs/_vfscommon.vfs/lib/app-punk/repl.tcl b/src/vfs/_vfscommon.vfs/lib/app-punk/repl.tcl index c9fb0ed5..88af09d8 100644 --- a/src/vfs/_vfscommon.vfs/lib/app-punk/repl.tcl +++ b/src/vfs/_vfscommon.vfs/lib/app-punk/repl.tcl @@ -3,56 +3,18 @@ package provide app-punk 1.0 #By the time we get here, we don't expect other packages to have been loaded - but the lib/module paths have already been scanned to populate 'package names' - -#puts stdout "$::auto_path" -#puts stderr "-----------" -#puts stderr "tcl::tm::list" -#puts stderr "-----------" -#puts stderr "[join [tcl::tm::list] \n]" -#puts stderr "-----------" -#puts stderr "auto_path" -#puts stderr "-----------" -#puts stderr "[join $::auto_path \n]" -#puts stderr "-----------" -#puts stderr "thread? [package provide Thread]" set thread_version [package require Thread] #puts stderr "repl.tcl thread version:$thread_version" -#puts stderr "info loaded:" -#puts stderr [join [info loaded] \n] - -#set tpath [lindex [info loaded] 0 0] -#puts stdout "--$tpath--" -#puts stdout "--[file exists $tpath]--" -#set tid [thread::create -preserved] -#thread::send $tid {puts thread1} -#puts stdout "mythread: [thread::id]" #review #catch {package require tcllibc} #punk & shellrun should be in codethreads - but not required in the parent repl threads package require shellfilter - package require punk::repl -#set v [package provide punk::repl] -#puts stderr "punk::repl version:$v script: [package ifneeded punk::repl $v]" -#puts stderr "package names" -#set packages_present [list] -#foreach p [package names] { -# if {[package provide $p] ne ""} { -# lappend packages_present $p -# } -#} -#puts stderr [join $packages_present \n] -repl::init -safe 0 -#puts stderr "Launching repl::start stdin -title app-punk" -#flush stderr +repl::init -type 0 set replresult [repl::start stdin -title app-punk] -#catch { -# puts "app-punk ifneeded: [package ifneeded app-punk 1.0]" -#} - #review if {[string is integer -strict $replresult]} { #puts stdout "repl.tcl exiting with numeric code $replresult" diff --git a/src/vfs/_vfscommon.vfs/lib/app-punkshell/punkshell.tcl b/src/vfs/_vfscommon.vfs/lib/app-punkshell/punkshell.tcl index 98798f45..807be1f3 100644 --- a/src/vfs/_vfscommon.vfs/lib/app-punkshell/punkshell.tcl +++ b/src/vfs/_vfscommon.vfs/lib/app-punkshell/punkshell.tcl @@ -43,8 +43,7 @@ namespace eval punkshell { set stdout_log "" set stderr_log "" - #set stdout_log [file normalize ~]/punkshell-stdout.txt - #set stderr_log [file normalize ~]/punkshell-stderr.txt + set stdout_log "[pwd]/punkshell_out.log" set stderr_log "[pwd]/punkshell_err.log" @@ -58,7 +57,7 @@ namespace eval punkshell { #ideally we don't want to launch an external process to run the script #variable punkshell_status_log #shellfilter::log::write $punkshell_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" - set exepath [file dirname [file join [info nameofexecutable] __dummy__]] + set exepath [file dirname [file join [info nameofexecutable] __dummy__]] set exedir [file dirname $exepath] set scriptpath [file normalize $scriptname] if {![file exists $scriptpath]} { @@ -87,6 +86,14 @@ info script $prevscript dict with prevglobal {} }] + #---------------------------------------------------- + append repl_lines {package require punk::repl} \n + append repl_lines {repl::init -type 0} \n + append repl_lines [list repl::submit stdin "eval \{ $script \};\n"] \n + append repl_lines {repl::start stdin} \n + set script $repl_lines + #---------------------------------------------------- + dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle punkshell #dict set params -teehandle punksh @@ -177,7 +184,7 @@ dict with prevglobal {} set repl_lines "" append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines {repl::start stdin} \n #test @@ -351,16 +358,24 @@ dict with prevglobal {} } flush stderr flush stdout - catch { - shellfilter::stack::remove stderr $chanstack_stderr_redir - shellfilter::stack::remove stdout $chanstack_stdout_redir - } + #puts stderr "\n - punkshell.tcl 1" + #flush stderr + + + #catch { + # shellfilter::stack::remove stderr $chanstack_stderr_redir + # shellfilter::stack::remove stdout $chanstack_stdout_redir + #} + #puts stderr "punkshell.tcl 2" shellfilter::stack::delete punkshellout shellfilter::stack::delete punkshellerr + #puts stderr "punkshell.tcl 3" set free_info [shellthread::manager::shutdown_free_threads] + #puts stderr "punkshell.tcl 4" foreach tid [thread::names] { thread::release $tid } + #puts stderr "punkshell.tcl 5" if {[dict size $exitinfo] == 0} { puts stderr "No result" @@ -380,7 +395,8 @@ dict with prevglobal {} exit 1 } else { puts -nonewline stdout [dict get $exitinfo result] - exit 0 + #don't call exit on success - allows tk apps to stay alive. + #exit 0 } } diff --git a/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl b/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl index 863aec34..42d2748a 100644 --- a/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl +++ b/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl @@ -77,6 +77,7 @@ package require shellfilter package require punk::ansi package require punk::packagepreference punk::packagepreference::install +package require logger #The whole punk infrastructure is overkill for calling arbitrary scripts #package require punk @@ -98,32 +99,69 @@ namespace eval shellspy { #todo - default to no logging not even to local syslog #load a .toml config which can configure logging as desired - set do_log 0 + set do_log 1 if {$do_log} { set debug_syslog_server 127.0.0.1:514 #set debug_syslog_server 172.16.6.42:51500 set error_syslog_server 127.0.0.1:514 set data_syslog_server 127.0.0.1:514 } else { - set debug_syslog_server "" - set error_syslog_server "" + set debug_syslog_server "" + set error_syslog_server "" set data_syslog_server "" } + #------------------------------------------------------------------------------------------------- + #tcllib logger infrastructure. + #------------------------------------------------------------------------------------------------- + namespace eval ::shellspy::loggerprocs { + #container for procs/aliases to be pointed to by tcllib logger using log::logproc + proc Dolog {lvl txt} { + #logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted + set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'" + puts stderr $msg + } + proc Runlog {lvl script} { + uplevel 1 $script + } + } + if {![catch { + package require logger + }]} { + logger::initNamespace ::shellspy + foreach lvl [logger::levels] { + interp alias {} ::shellspy::loggerprocs::Log_$lvl {} ::shellspy::loggerprocs::Runlog $lvl + log::logproc $lvl ::shellspy::loggerprocs::Log_$lvl + } + logger::setlevel warn + #namespace path ::shellspy::log + } else { + #e.g tcllib not available, safe interp? + #fake out the logger calls + namespace eval ::shellspy::log { + foreach lvl {debug info notice warn error critical alert emergency} { + proc $lvl {args} {} + } + } + } + #------------------------------------------------------------------------------------------------- + + + log::notice {puts stderr "shellspy starting with args '$::argv'"} shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog $debug_syslog_server -file ""] - shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'" - shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]" + log::notice {shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'"} + log::info {shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]"} #------------------------------------------------------------------------- ##don't write to stdout/stderr before you've redirected them to a log using shellfilter functions ## puts to stdout/stderr will comingle with command's output if performed before the channel stacks are configured. - - chan configure stdin -buffering line - chan configure stdout -buffering none - chan configure stderr -buffering none + + chan configure stdin -buffering line + chan configure stdout -buffering none + chan configure stderr -buffering none #set id_ansistrip [shellfilter::stack::add stderr ansistrip -settings {}] #set id_ansistrip [shellfilter::stack::add stdout ansistrip -settings {}] @@ -131,12 +169,12 @@ namespace eval shellspy { #redir on the shellfilter stack with no log or syslog specified acts to suppress output of stdout & stderr. #todo - fix shellfilter code to make this noop more efficient (avoid creating corresponding logging thread and filter?) #JMN - #set redirconfig {-settings {-syslog 127.0.0.1:514 -file ""}} - set redirconfig {} + set redirconfig {-settings {-syslog 127.0.0.1:514 -file ""}} + #set redirconfig {} lassign [shellfilter::redir_output_to_log "SUPPRESS" {*}$redirconfig] chanstack_stdout_redir chanstack_stderr_redir - shellfilter::log::write $shellspy_status_log "shellfilter::redir_output_to_log SUPPRESS DONE [clock_sec]" - - + log::notice {shellfilter::log::write $shellspy_status_log "shellfilter::redir_output_to_log SUPPRESS DONE [clock_sec]"} + + ### #we can now safely write to stderr/stdout and it will not interfere with stderr/stdout from the dispatched command. #This is because when shellfilter::run is called it temporarily adds it's own filter in place of the redirection we just added. @@ -179,24 +217,32 @@ namespace eval shellspy { set stdout_log "" set stderr_log "" - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr ABOUTTO [clock_sec]" + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr ABOUTTO [clock_sec]"} set errdeviceinfo [shellfilter::stack::new shellspyerr -settings [list -tag "shellspyerr" -buffering none -raw 1 -syslog $data_syslog_server -file $stderr_log]] - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr DONE [clock_sec]" + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr DONE [clock_sec]"} - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout ABOUTTO [clock_sec]" + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout ABOUTTO [clock_sec]"} set outdeviceinfo [shellfilter::stack::new shellspyout -settings [list -tag "shellspyout" -buffering none -raw 1 -syslog $data_syslog_server -file $stdout_log]] set commandlog [dict get $outdeviceinfo localchan] #puts $commandlog "HELLO $commandlog" #flush $commandlog - shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout DONE [clock_sec]" - + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout DONE [clock_sec]"} - #note that this filter is inline with the data teed off to the shellspyout log. - #To filter the main stdout an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data. - set id_ansistrip [shellfilter::stack::add shellspyout ansistrip -settings {}] - shellfilter::log::write $shellspy_status_log "shellfilter::stack::add shellspyout ansistrip DONE [clock_sec]" + #------------------------------------------------------------------------------ + #note that this filter is inline with the data teed off to the shellspyout log. (ie it is a transform on one of the fifo2 ends) + #To filter the main stdout, an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data. + #TODO - review + set id_ansistrip_out [shellfilter::stack::add shellspyout ansistrip -settings {}] + set id_ansistrip_err [shellfilter::stack::add shellspyerr ansistrip -settings {}] + #This can cause problems e.g unknown channel rc2 (tcl::chan::fifo2) in the repl - review + #something wrong with adding and removing transforms on the fifo2 channels in shellfilter::run? + #only seems to happen when in mode raw where we need to emit to stdout after each keypress, + #and only when we have a queued tcl script that will run before or after the repl. + #e.g punk902z dev shellspy lib:hello.tcL + log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::add shellspyout ansistrip DONE [clock_sec]"} + #------------------------------------------------------------------------------ #set id_out [shellfilter::stack::add stdout rebuffer -settings {}] @@ -378,7 +424,7 @@ namespace eval shellspy { package require shellrun package require punk::repl puts stdout "quit to exit" - repl::init -safe 0 + repl::init -type 0 repl::start stdin -defaultresult %r% }]] } @@ -709,7 +755,7 @@ dict with prevglobal {} set repl_lines "" append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines {repl::start stdin} \n #test @@ -728,12 +774,11 @@ dict with prevglobal {} dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle shellspy - - + set params [dict merge $params [get_channel_config $::testconfig]] set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] - + set exitinfo [shellfilter::run $script {*}$params] shellfilter::stack::remove stderr $id_err @@ -822,17 +867,21 @@ dict with prevglobal {} if {$replwhen eq "repl_first"} { + #this concept is probably overly complex and unnecessary. If the user is in a repl then they can just source scripts from the prompt. + # #we need to cooperate with the repl to get the script to run on exit namespace eval ::repl {} set ::repl::post_script $script append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n + append repl_lines [list repl::submit stdin "eval \{ puts \"Queued script will run after 'quit'. 'exit' to abort.\" \};\n"] \n append repl_lines {repl::start stdin} \n set script "$repl_lines" } elseif {$replwhen eq "repl_last"} { + #run a script first e.g could be a tk app - but maintain control via the repl. append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines [list repl::submit stdin "eval \{ $script \};\n"] \n append repl_lines {repl::start stdin} \n @@ -848,27 +897,31 @@ dict with prevglobal {} dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle shellspy + dict set params -debug 1 ;#JMN + #dict set params -syslog "127.0.0.1:514" + dict set params -syslog {} + #dict set params -teehandle punksh - - + + set params [dict merge $params [get_channel_config $::testconfig]] set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] - + #set id_out [shellfilter::stack::add stdout ansiwrap -action sink-locked -settings {-colour {green bold}}] + set exitinfo [shellfilter::run $script {*}$params] + #jmn 2026-05-19 + shellfilter::log::write $shellspy_status_log "do_script exitinfo $exitinfo" + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stdout $id_out - #if {[lindex $exitinfo 0] eq "exitcode"} { - # shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" - #} - #jjj - #shellfilter::log::write $shellspy_status_log "do_script raw exitinfo: $exitinfo" if {[dict exists $exitinfo errorInfo]} { #strip out the irrelevant info from the errorInfo - we don't want info beyond 'invoked from within' as this is just plumbing related to the script sourcing set stacktrace [string map [list \r\n \n] [dict get $exitinfo errorInfo]] - set output "" + set output "" set tracelines [split $stacktrace \n] foreach ln $tracelines { if {[string match "*invoked from within*" $ln]} { @@ -891,7 +944,8 @@ dict with prevglobal {} lappend out $a } return $out - } + } + proc do_shell {shell args} { variable shellspy_status_log shellfilter::log::write $shellspy_status_log "do_shell $shell got '$args' [llength $args]" @@ -899,37 +953,38 @@ dict with prevglobal {} shellfilter::log::write $shellspy_status_log "do_shell $shell xgot '$args'" set params [do_callback_parameters $shell] dict set params -teehandle shellspy - - + + set params [dict merge $params [get_channel_config $::testconfig]] - + set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] #shells that take -c and need all args passed together as a string - + set exitinfo [shellfilter::run [concat $shell -c [shellescape $args]] {*}$params] shellfilter::stack::remove stdout $id_out - + if {[lindex $exitinfo 0] eq "exitcode"} { shellfilter::log::write $shellspy_status_log "do_shell $shell returning $exitinfo" } return $exitinfo } + proc do_wsl {distdefault args} { variable shellspy_status_log shellfilter::log::write $shellspy_status_log "do_wsl $distdefault got '$args' [llength $args]" set args [do_callback wsl {*}$args] ;#use dist? shellfilter::log::write $shellspy_status_log "do_wsl $distdefault xgot '$args'" set params [do_callback_parameters wsl] - + dict set params -debug 0 - + set params [dict merge $params [get_channel_config $::testconfig]] - + set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] - + dict set params -teehandle shellspy ;#shellspyout shellspyerr must exist set exitinfo [shellfilter::run [concat wsl -d $distdefault -e [shellescape $args]] {*}$params] @@ -949,11 +1004,11 @@ dict with prevglobal {} lappend commands [list punkd [list sub punkdict singleopts {any}]] - #'shout' extension (all uppercase) to force use of tclsh as a separate process + #'shout' extension (all uppercase) to force use of tclsh as a separate process #todo - detect various extensions - and use all script-preceding unmatched args as interpreter+options - #e.g perl,php,python etc. + #e.g perl,php,python etc. #For tcl will make it easy to test different interpreter output from tclkitsh, tclsh8.6, tclsh8.7 etc - #for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config + #for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config #(or just attempt launch in case there is shebang line in script) #we may get ambiguity with existing shell match-specs such as -c /c -r. todo - only match those in first slot? lappend commands [list tclscriptprocess [list match [list {.*\.TCL$} {.*\.TM$} {.*\.TK$}] dispatch [list shellspy::do_script_process [info nameofexecutable] %matched%] dispatchtype raw dispatchglobal 1 singleopts {any}]] @@ -997,7 +1052,8 @@ dict with prevglobal {} lappend commands [list runcmdfile [list sub word$i singleopts {any}]] } - lappend commands [list libscript [list match [list {lib:.*$} ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] + #Only match lib:scriptname at start, with a single colon (avoid matching on -tcl lib::blah::func -tcl punk::lib::func etc) + lappend commands [list libscript [list match [list {^lib:[^:].*$} ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] for {set i 0} {$i < 25} {incr i} { lappend commands [list libscript [list sub word$i singleopts {any}]] } @@ -1043,8 +1099,7 @@ dict with prevglobal {} for {set i 0} {$i < 25} {incr i} { lappend commands [list runpwsht [list sub word$i singleopts {any}]] } - - + lappend commands {runcmd {match {^/c$} dispatch shellspy::do_in_cmdshell dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}}} for {set i 0} {$i < 25} {incr i} { lappend commands [list runcmd [list sub word$i singleopts {any}]] @@ -1058,16 +1113,16 @@ dict with prevglobal {} for {set i 0} {$i < 25} {incr i} { lappend commands [list runcmdb [list sub word$i singleopts {any} longopts {any} pairopts {any}]] } - + lappend commands [list wslraw [list match {^wsl$} dispatch [list shellspy::do_wsl AlmaLinux9] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]] for {set i 0} {$i < 25} {incr i} { lappend commands [list wslraw [list sub word$i singleopts {any}]] } #e.g - # punk -tcl info patch + # punk -tcl info patch # punk -tcl eval "package require punk::char;punk::char::charset_page dingbats" - + lappend commands [list tclline [list match [list {^-tcl$}] dispatch [list shellspy::do_tclline tcl] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]] for {set i 0} {$i < 25} {incr i} { lappend commands [list tclline [list sub word$i singleopts {any}]] @@ -1126,38 +1181,50 @@ dict with prevglobal {} set is_call_error 0 set arglist [list] ;#processed args result - contains dispatch info etc. if {[catch { + #----------------------------------------------------------------- + #check_flags does the actual dispatch ie calling of the command above such as do_script, do_shell etc. + #in turn these use shellfilter::run which does the actual running of the command and capturing of output, exitcode etc. set arglist [check_flags {*}$argdefinitions] + #----------------------------------------------------------------- } callError]} { + log::error {shellfilter::log::write $shellspy_status_log "check_flags error: $callError"} + puts -nonewline stderr "|shellspy-stderr> ERROR during command dispatch\n" puts -nonewline stderr "|shellspy-stderr> $callError\n" puts -nonewline stderr "|shellspy-stderr> [set ::errorInfo]\n" - shellfilter::log::write $shellspy_status_log "check_flags error: $callError" set is_call_error 1 } else { - shellfilter::log::write $shellspy_status_log "check_flags result: $arglist" + log::notice {shellfilter::log::write $shellspy_status_log "check_flags result: $arglist"} } - shellfilter::log::write $shellspy_status_log "check_flags dispatch -done- [clock_sec]" - + log::debug {shellfilter::log::write $shellspy_status_log "check_flags dispatch -done- [clock_sec]"} + #puts stdout "sp2. $::argv" - - if {[catch { - set tidyinfo [shellfilter::logtidyup] - } errMsg]} { - - shellfilter::log::open shellspy-error {-tag shellspy-error -syslog $error_syslog_server} - shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]" - after 200 - } - #don't open more logs.. + + #---------------------------------------------------------------------------------- + # Calling shellfilter::logtidyup (with no tags) when using tcl::chan::fifo2 will cause a deadlock. + # review - why exactly? + #---------------------------------------------------------------------------------- + #if {[catch { + # set tidyinfo [shellfilter::logtidyup] + # } errMsg]} { + # + # shellfilter::log::open shellspy-error {-tag shellspy-error -syslog $error_syslog_server} + # shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]" + # after 200 + #} + ##don't open more logs.. #puts stdout ">$tidyinfo" + #---------------------------------------------------------------------------------- #lassign [shellfilter::redir_output_to_log "SUPPRESS"] id_stdout_redir id_stderr_redir - catch { + #catch { + # shellfilter::stack::remove stderr $chanstack_stderr_redir + # shellfilter::stack::remove stdout $chanstack_stdout_redir + #} shellfilter::stack::remove stderr $chanstack_stderr_redir shellfilter::stack::remove stdout $chanstack_stdout_redir - } #shellfilter::log::write $shellspy_status_log "logtidyup -done- $tidyinfo" @@ -1165,7 +1232,7 @@ dict with prevglobal {} set errorlist [dict get $tidyinfo errors] if {[llength $errorlist]} { foreach err $errorlist { - puts -nonewline stderr "|shellspy-final> worker-error-set $err\n" + puts -nonewline stderr "|shellspy-final> worker-error-set $err\n" } } } @@ -1182,8 +1249,8 @@ dict with prevglobal {} } errMsg]} { puts stdout "shellspy logtidyup error $errMsg" flush stdout - shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server} - shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]" + log::error {shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server}} + log::error {shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]"} after 100 } #puts [shellfilter::stack::status shellspyout] @@ -1215,19 +1282,29 @@ dict with prevglobal {} #} - shellfilter::stack::delete shellspyout - shellfilter::stack::delete shellspyerr + if {[catch { + shellfilter::stack::delete shellspyout + shellfilter::stack::delete shellspyerr + } errMsg]} { + #shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server} + #shellfilter::log::write shellspy-final "FINAL stack delete error $errMsg\n [set ::errorInfo]" + puts stderr "FINAL stack delete error $errMsg\n [set ::errorInfo]" + after 100 + } set free_info [shellthread::manager::shutdown_free_threads] #puts stdout $free_info #flush stdout - if {[package provide zzzload] ne ""} { - #if zzzload used and not shutdown - we can get deadlock - #puts "zzzload::loader_tid [set ::zzzload::loader_tid]" - #zzzload::shutdown - } + + # if {[package provide zzzload] ne ""} { + # #if zzzload used and not shutdown - we can get deadlock + # #puts "zzzload::loader_tid [set ::zzzload::loader_tid]" + # #zzzload::shutdown + # } + #puts stdout "threads: [thread::names]" #flush stdout #puts stdout "calling release on remaining threads" + #flush stdout foreach tid [thread::names] { thread::release $tid } @@ -1254,7 +1331,7 @@ dict with prevglobal {} flush stderr exit 1 } - } + } foreach tclscript_flavour [list tclline tclkit punkline punkshellline tkline tkshellline libscript help] { if {[dict exists $arglist dispatch $tclscript_flavour result error]} { @@ -1278,12 +1355,12 @@ dict with prevglobal {} foreach tclscript_flavour [list tclline tclkit punkline punkshellline tkline tkshellline libscript help] { if {[dict exists $arglist dispatch $tclscript_flavour result result]} { puts -nonewline stdout [dict get $arglist dispatch $tclscript_flavour result result] + flush stdout exit 0 } } - #if we call exit - package require Tk script files will exit prematurely - #review + #if we call exit - package require Tk script files will exit prematurely #exit 0 } diff --git a/src/vfs/_vfscommon.vfs/lib/app_shell/app_shell.tcl b/src/vfs/_vfscommon.vfs/lib/app_shell/app_shell.tcl index 03dd1975..2b0b6e32 100644 --- a/src/vfs/_vfscommon.vfs/lib/app_shell/app_shell.tcl +++ b/src/vfs/_vfscommon.vfs/lib/app_shell/app_shell.tcl @@ -3,7 +3,7 @@ package provide app_shell 1.0 package require Thread package require shellfilter package require punk::repl -repl::init -safe 0 +repl::init -type 0 set replresult [repl::start stdin -title app_shell] if {[string is integer -strict $replresult]} { exit $replresult diff --git a/src/vfs/_vfscommon.vfs/lib/app_shellrun/app_shellrun.tcl b/src/vfs/_vfscommon.vfs/lib/app_shellrun/app_shellrun.tcl index ced47a22..99280ec2 100644 --- a/src/vfs/_vfscommon.vfs/lib/app_shellrun/app_shellrun.tcl +++ b/src/vfs/_vfscommon.vfs/lib/app_shellrun/app_shellrun.tcl @@ -178,7 +178,7 @@ dict with prevglobal {} set repl_lines "" append repl_lines {package require punk::repl} \n - append repl_lines {repl::init -safe 0} \n + append repl_lines {repl::init -type 0} \n append repl_lines {repl::start stdin} \n #test diff --git a/src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm b/src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm index 1eb475a144b5b4a5b207752f0ae0a8699e4b0fc4..8dfa947483ca725fac32f69f502fad8353d935ad 100644 GIT binary patch delta 26 gcmdlnfqBOS<_+uQm~V0H*}PFsbtQ-~`R4I>0G?nBHvj+t delta 26 gcmdlnfqBOS<_+uQnDghWZ{8@Ux)Q{geDiob0G1aEWdHyG diff --git a/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.0.tm deleted file mode 100644 index a2966397b32eb48991f4f2bf39c0cb4b29cc1fa1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9348 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 diff --git a/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm index be64f8636784b318c482acf3fe743e6f4c4042be..827b97bfe36801dd6cde778f5426bfd17eba5af3 100644 GIT binary patch delta 26 fcmX@)dB}6aU0Ie}9D8CmKa{Oy1G6S4sU!dZrY8$( delta 26 ecmX@)dB}6aU0Ie{Iq}h(AIjFUfmxH2R1yH7^$PX? diff --git a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.1.tm b/src/vfs/_vfscommon.vfs/modules/overtype-1.7.1.tm deleted file mode 100644 index 18fa78ea..00000000 --- a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.1.tm +++ /dev/null @@ -1,4772 +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.7.1 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.7.1] -#[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 [expr {$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} { - 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 && $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.7.1 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.2.tm b/src/vfs/_vfscommon.vfs/modules/overtype-1.7.2.tm deleted file mode 100644 index aa7405e2..00000000 --- a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.2.tm +++ /dev/null @@ -1,4892 +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.7.2 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.7.2] -#[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 "ANSI capable 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 [expr {$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 - - namespace eval argdoc { - variable PUNKARGS - - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - interp alias "" ::overtype::example "" ::punk::args::helpers::example - } - - - namespace eval argdoc { - lappend PUNKARGS [list { - @id -id ::overtype::renderline - @cmd -name overtype::renderline\ - -summary\ - {Render a line of text/ANSI input over a line of text.}\ - -help\ - {renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode - commandline repl for the Tcl Punk Shell. - It is also a central part of an ansi (micro) virtual terminal-emulator of sorts. - 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. - Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't - affect another. - Calling on the punk::ansi library - it can coalesce codes to keep the size down. - - It is a giant mess of doing exactly what common wisdom says not to do... lots at once. - Renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a - static underlay. - 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. - The overlay couuld be similar - in which case it may often be used to overwrite a column or section of - the underlay. - The overlay could however be a sequence of ANSI-laden text that jumps all over the place. - - 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. - By suppyling the ${$B}-info${$N} 1 option - it can return various fields indicating the state of the render. - The main 3 are: result, overflow_right, and unapplied. - 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. - } - @opts - -etabs -default 0 -type boolean - -width -default \uFFEF -type integer - -expand_right -default 0 -type boolean - -transparent -default 0 -type {literal(0)|literal(1)|regexp} -help\ - "0 to disable transparency processing - 1 to enable space characters in the - overlay to be transparent, or a regex - to match the character(s) required to be - transparent in the overlay." - -startcolumn -default 1 -type integer - -cursor_column -default 1 -type integer -help\ - {First column is 1. Cursor column can be zero or negative} - -cursor_row -default "" -type integer - -insert_mode -default 1 -type boolean - -crm_mode -default 0 -type boolean - -autowrap_mode -default 1 -type boolean - -reverse_mode -default 0 -type boolean - -info -default 0 -type boolean -help\ - "When set to 1, return a dictionary of settings useful for - processing ANSI input in a loop. When zero, the resulting - string will contain the updated line, but not all the - overtext may have been applied." - -exposed1 -default \uFFFD -help\ - {A character of single terminal column width to use - as replacement when first-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the second-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - -exposed2 -default \uFFFD -help\ - {A character of single terminal column width to use - as replacement when second-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the first-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - -cursor_restore_attributes -default "" - -cp437 -default 0 -type boolean - -experimental -default {} - - @values -min 2 -max 2 - undertext -type string -help\ - "A single line of text which may contain pre-rendered ANSI. - 'pre-rendered' in this context means that it may contain - various static ANSI codes such as SGR colours and attributes - but not motion-control or more complex ANSI sequences. - It is an error to supply a newline (lf) character in the - undertext." - overtext -type string -help\ - "ANSI (or plain text) to overlay onto the undertext. - May contain ANSI movement codes even if they would move the - cursor to another line. If -info is zero, the output will - only display up to the point where the cursor moved off-line. - If -info is 1, the line moved to may be reflected in the - cursor_row element of the result. Overtext may contain an lf - which is effectively a form of 'movement control' to increment - the row. - Other ANSI codes may perform operations such as changing the - insert_mode or reverse_mode - and these are reflected in the - result dictionary when '-info 1' is used, so that the state - can then be applied to subsequent lines." - }] - } - - proc renderline {args} { - # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### - # 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? - - - - #*** !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} { - 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 && $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::renderline_transparent {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 -} - - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::overtype ::overtype::argdoc -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [tcl::namespace::eval overtype { - variable version - set version 1.7.2 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.3.tm b/src/vfs/_vfscommon.vfs/modules/overtype-1.7.3.tm deleted file mode 100644 index e6828473..00000000 --- a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.3.tm +++ /dev/null @@ -1,5019 +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.7.3 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.7.3] -#[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 "ANSI capable 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 [expr {$renderwidth + 1}] - } else { - return $endpos - } - } - } -} - -tcl::namespace::eval overtype { - #*** !doctools - #[subsection {Namespace overtype}] - #[para] Core API functions for overtype - #[list_begin definitions] - - namespace eval argdoc { - variable PUNKARGS - #non-colour SGR codes - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - interp alias "" ::overtype::example "" ::punk::args::helpers::example - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::overtype::renderspace - @cmd -name overtype::renderspace\ - -summary\ - {}\ - -help\ - {} - @opts - #because underblocks value is optional - restrict opts to flag pairs (no solos) - #We don't use punk::args::parse in the actual function to parse args - so keep it simpler. - -bias -default left -type string -choices {left right} -help ignored - -width -default \uFFEF -type integer - -height -default \uFFEF -type integer - -startcolumn -default 1 -type integer - -startrow -default 1 -type integer - -ellipsis -default 0 -type boolean - -ellipsistext -default ${$::overtype::default_ellipsis_horizontal} -type char - -ellipsiswhitespace -default 0 -type boolean - -expand_right -default 0 -type boolean - -appendlines -default 1 -type boolean - -transparent -default 0 -type {literal(0)|literal(1)|regexp} -help\ - "0 to disable transparency processing - 1 to enable space characters in the - overlay to be transparent, or a regex - to match the character(s) required to be - transparent in the overlay." - -exposed1 -default \uFFFD -type char -help\ - {A character of single terminal column width to use - as replacement when first-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the second-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - -exposed2 -default \uFFFD -type char -help\ - {A character of single terminal column width to use - as replacement when second-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the first-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - - -experimental -default 0 - -cp437 -default 0 -type boolean - -looplimit -default \uFFEF\ -type integer -help\ - "internal failsafe - experimental" - -crm_mode -default 0 -type boolean - -reverse_mode -default 0 -type boolean - -insert_mode -default 1 -type boolean - -wrap -default 0 -type boolean - -info -default 0 -type boolean -help\ - "When set to 1, return a dictionary (experimental)" - -console -default {stdin stdout stderr} -type list - - @values -min 1 -max 2 - underblock -type string -optional 1 - overblock -type string -optional 0 - }] - } - - - #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] < 1} { - 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 { - #no solo flags - so we assume only an overblock was supplied - set overblock [lindex $args end] - set underblock "" - set argsflags [lrange $args 0 end-1] - - #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\ - -startrow 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 - -startrow - -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_startrow [tcl::dict::get $opts -startrow] - 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] - if {$opt_startrow > 1} { - set down [expr {$opt_startrow -1}] - set overblock [punk::ansi::move_down $down]$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 [lpop inputchunks 0] ;#could be a list 'ansisplit' or text 'plain|mixed' - - #use eq test with emptystring instead of 'string length' - test for emptiness shouldn't cause shimmering if popped inputchunks member if an 'ansisplit' list - if {$overtext eq ""} { - 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 $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.. - - #todo!!! - # 2025 fix - this does nothing - so what uses it?? create a test! - 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] - #JMN - ledit inputchunks -1 -1 $overflow_right$unapplied - - set pt_ansi_pt [punk::ansi::ta::split_codes_single $overflow_right] - #join the trailing and leading pt parts of the 2 lists - ledit pt_ansi_pt end end "[lindex $pt_ansi_pt end][lindex $unapplied_list 0]" - lappend pt_ansi_pt [lrange $unapplied_list 1 end] - - ledit inputchunks -1 -1 $pt_ansi_pt ;#combined overflow_right and unapplied - in ansisplit form - #JMN 2025 - set overtext_type "ansisplit" - - set overflow_right "" - set unapplied "" - set unapplied_list [list] - - 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] - #JMN - assume backwards compat ledit available from punk::lib (for tcl <9) - ledit inputchunks -1 -1 $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 - - - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::overtype::renderline - @cmd -name overtype::renderline\ - -summary\ - {Render a line of text/ANSI input over a line of text.}\ - -help\ - {renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode - commandline repl for the Tcl Punk Shell. - It is also a central part of an ansi (micro) virtual terminal-emulator of sorts. - 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. - Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't - affect another. - Calling on the punk::ansi library - it can coalesce codes to keep the size down. - - It is a giant mess of doing exactly what common wisdom says not to do... lots at once. - Renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a - static underlay. - 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. - The overlay couuld be similar - in which case it may often be used to overwrite a column or section of - the underlay. - The overlay could however be a sequence of ANSI-laden text that jumps all over the place. - - 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. - By suppyling the ${$B}-info${$N} 1 option - it can return various fields indicating the state of the render. - The main 3 are: result, overflow_right, and unapplied. - 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. - } - @opts - -etabs -default 0 -type boolean - -width -default \uFFEF -type integer - -expand_right -default 0 -type boolean - -transparent -default 0 -type {literal(0)|literal(1)|regexp} -help\ - "0 to disable transparency processing - 1 to enable space characters in the - overlay to be transparent, or a regex - to match the character(s) required to be - transparent in the overlay." - -startcolumn -default 1 -type integer - -cursor_column -default 1 -type integer -help\ - {First column is 1. Cursor column can be zero or negative} - -cursor_row -default "" -type integer - -insert_mode -default 1 -type boolean - -crm_mode -default 0 -type boolean - -autowrap_mode -default 1 -type boolean - -reverse_mode -default 0 -type boolean - -info -default 0 -type boolean -help\ - "When set to 1, return a dictionary of settings useful for - processing ANSI input in a loop. When zero, the resulting - string will contain the updated line, but not all the - overtext may have been applied." - -exposed1 -default \uFFFD -help\ - {A character of single terminal column width to use - as replacement when first-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the second-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - -exposed2 -default \uFFFD -help\ - {A character of single terminal column width to use - as replacement when second-half of an underlying char - is exposed due to overlay positioning/transparency - which obscures the first-half of the char. May be ANSI - coloured as this doesn't affect the display width. - Default is \uFFFD - the unicode replacement char.} - -cursor_restore_attributes -default "" - -cp437 -default 0 -type boolean - -experimental -default {} - - @values -min 2 -max 2 - undertext -type string -help\ - "A single line of text which may contain pre-rendered ANSI. - 'pre-rendered' in this context means that it may contain - various static ANSI codes such as SGR colours and attributes - but not motion-control or more complex ANSI sequences. - It is an error to supply a newline (lf) character in the - undertext." - overtext -type string -help\ - "ANSI (or plain text) to overlay onto the undertext. - May contain ANSI movement codes even if they would move the - cursor to another line. If -info is zero, the output will - only display up to the point where the cursor moved off-line. - If -info is 1, the line moved to may be reflected in the - cursor_row element of the result. Overtext may contain an lf - which is effectively a form of 'movement control' to increment - the row. - Other ANSI codes may perform operations such as changing the - insert_mode or reverse_mode - and these are reflected in the - result dictionary when '-info 1' is used, so that the state - can then be applied to subsequent lines." - }] - } - - proc renderline {args} { - # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### - # 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? - - - - #*** !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 {}\ - -overtext_type mixed\ - ] - #-overtext_type plain|mixed|ansisplit - - #-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 - -overtext_type { - 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] - set opt_overtext_type [tcl::dict::get $opts -overtext_type] - 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] - } - - #review - is untabifying sensible at this point?? - if {$opt_overtext_type eq "ansisplit"} { - #todo - something for each pt part? - } else { - #plain|mixed - 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 {$opt_overtext_type eq "ansisplit"} { - set overmap $overdata - lset overmap 0 "$startpadding[lindex $overmap 0]" - } else { - if {[punk::ansi::ta::detect $overdata]} { - #TODO!! rework this. - #e.g 200K+ input file with no newlines - we are wastefully calling split_codes_single repeatedly on mostly the same data. - #set overmap [punk::ansi::ta::split_codes_single $startpadding$overdata] - set overmap [punk::ansi::ta::split_codes_single $overdata] - lset overmap 0 "$startpadding[lindex $overmap 0]" - - } 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 - - - set overlay_grapheme_control_stacks [list] - #REVIEW - even if we pass in a pre-split overtext (-overtext_type ansisplit) - #we are re-generating the overlay_grapheme_control_stacks list each time - #this is a big issue when overtext is not broken into lines, but is just a big long ansi and/or plain text string. - #todo - return also the unapplied portion of the 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 ""]] - #JMN - backwards compat ledit from punk::lib for tcl <9 - ledit unapplied_list -1 -1 {*}[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 ""]] - #JMN - ledit unapplied_list -1 -1 {*}[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} { - 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 && $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::renderline_transparent {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] - #JMN insert via ledit - ledit o $i $i-1 $c - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - #set ustacks [linsert $ustacks $i $sgrstack] - #set gxstacks [linsert $gxstacks $i $gx0stack] - #insert via ledit - ledit ustacks $i $i-1 $sgrstack - ledit gxstacks $i $i-1 $gx0stack - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -tcl::namespace::eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::overtype ::overtype::argdoc -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [tcl::namespace::eval overtype { - variable version - set version 1.7.3 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.4.tm b/src/vfs/_vfscommon.vfs/modules/overtype-1.7.4.tm index 403e971c..6c427f1d 100644 --- a/src/vfs/_vfscommon.vfs/modules/overtype-1.7.4.tm +++ b/src/vfs/_vfscommon.vfs/modules/overtype-1.7.4.tm @@ -401,13 +401,20 @@ tcl::namespace::eval overtype { set opt_console [tcl::dict::get $opts -console] + #-------------------------------------------------------------------------- + #TODO + #REVIEW - punk::console package may not be loaded set cursor_style_overtype {3 underline-blink} set cursor_style_insert {5 beam-blink} if {$opt_insert_mode} { - punk::console::cursor_style -console $opt_console $cursor_style_insert + set initial_cursor_style $cursor_style_insert } else { + set initial_cursor_style $cursor_style_overtype + } + catch { punk::console::cursor_style -console $opt_console $cursor_style_overtype } + #-------------------------------------------------------------------------- # ---------------------------- # -experimental dev flag to set flags etc @@ -695,22 +702,23 @@ tcl::namespace::eval overtype { #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\ - -overtext_type $overtext_type\ + 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 {*}{ + } -overtext_type $overtext_type {*}{ + } ] set rinfo [renderline {*}$renderopts $undertext $overtext] @@ -940,14 +948,15 @@ tcl::namespace::eval overtype { puts stdout ">>>renderspace<<<[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 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..? @@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype { } #JMN - if {[tcl::dict::get $vtstate insert_mode]} { - puts "setting cursor to insert style" - punk::console::cursor_style -console $opt_console $cursor_style_insert - } else { - punk::console::cursor_style -console $opt_console $cursor_style_overtype - } + #REVIEW - we don't want to emit cursor_style ANSI unless it changes. + #if {[tcl::dict::get $vtstate insert_mode]} { + # puts "setting cursor to insert style" + # punk::console::cursor_style -console $opt_console $cursor_style_insert + #} else { + # punk::console::cursor_style -console $opt_console $cursor_style_overtype + #} #puts "renderedrow_max: $renderedrow_max" #check for null lines below renderedrow_max (and at tail) and trim. @@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype { #broken: #todo - renderline -overflow is invalid. # we need renderline to support -expand_left ?? - 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 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} { @@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype { -crm_mode -default 0 -type boolean -autowrap_mode -default 1 -type boolean -reverse_mode -default 0 -type boolean - -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\ + -info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\ + -choicelabels\ { 1 "return a dict with raw fields" 2 "return a dict using ansistring VIEW" diff --git a/src/vfs/_vfscommon.vfs/modules/packagetrace-0.8.tm b/src/vfs/_vfscommon.vfs/modules/packagetrace-0.8.tm deleted file mode 100644 index 2025cdc2..00000000 --- a/src/vfs/_vfscommon.vfs/modules/packagetrace-0.8.tm +++ /dev/null @@ -1,643 +0,0 @@ - - -#JMN 2005 - Public Domain -# -#REVIEW: This package may not robustly output xml. More testing & development required. -# - -#NOTE: the 'x' attribute on the 'info' tag may have its value truncated. -#It is a human-readable indicator only and should not be used to cross-reference to the corresponding 'require' tag using the 'p' attribute. -#Use the fact that the corresponding 'info' tag directly follows its 'require' tag. - - -#changes -#2021-09-17 -# - added variable ::packagetrace::showpresent with default 1 -# setting this to 0 will hide the tags which sometimes make the output too verbose. -# - changed t from an integer number of milliseconds to show fractional millis by using ([clock microseconds]-$t0)/1000.0 in the expr. - -namespace eval packagetrace::class { - if {[info commands [namespace current]::tracer] eq ""} { - oo::class create tracer { - method get {} { - } - method test {} { - return tracertest - } - } - } -} - - -namespace eval packagetrace { - variable tracerlist [list] - variable chan stderr - variable showpresent 1 - variable output "" - - - proc help {} { - return { - REVIEW - documentation inaccurate -Enable package tracing using 'package require packagetrace' -Disable package tracing using 'package forget packagetrace; package require packagetrace' - (This 2nd 'package require packagetrace' will raise an error. This is deliberate.) -use packagetrace::channel to desired output channel or none. (default stderr) - -set packagetrace::showpresent 0 to skip output -} - } - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - tm_version... functions - primary source is punk::lib module - # - these should be synced with code from latest punk::lib - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - #convenience function in case the sequence 'package forget packagetrace;package require packagetrace' is too unintuitive or weird. - #REVIEW - proc unload {} { - package forget packagetrace - if {[catch {package require packagetrace}]} { - return 1 ;#yes - we get an error if we unloaded successfully - } else { - error "packagetrace was not unloaded" - } - } - proc emit {str} { - variable chan - variable output - append output $str - if {$chan ne "none"} { - puts -nonewline $chan $str - } - return - } - proc get {{as raw}} { - variable output - switch -- [string tolower $as] { - asxml { - if {[package provide tdom] eq ""} { - set previous_output $output - package require tdom - set output $previous_output - } - set d [dom parse $output] - return [$d asXML] - } - aslist { - if {[package provide tdom] eq ""} { - set previous_output $output - package require tdom - set output $previous_output - } - set d [dom parse $output] - return [$d asList] - } - default { - return $output - } - } - } - proc channel {{ch ""}} { - variable chan - switch -exact -- $ch { - "" { - return $chan - } - none { - set chan none - return none - } - stderr - stdout { - #note stderr stdout not necessarily in [chan names] (due to transforms etc?) but can still work - set chan $ch - return $ch - } - default { - if {$ch in [chan names]} { - set chan $ch - return $ch - } else { - error "chan '$ch' not in \[chan names\]: [chan names]" - } - } - } - } - proc init {} { - uplevel 1 { - set ::packagetrace::level -1 - if {![llength [info commands tcl_findLibrary]]} { - tcl::namespace::eval :: $::auto_index(tcl_findLibrary) - } - package require commandstack - - - set targetcommand [namespace which tcl_findLibrary] ;# normalize to presumably ::tcl_findLibrary - set stackrecord [commandstack::rename_command -renamer packagetrace $targetcommand [info args $targetcommand] { - set marg [string repeat { } $::packagetrace::level] - packagetrace::emit "${marg} tcl_findLibrary $basename $version $patch $initScript $enVarName $varName \n" - uplevel 1 [list $COMMANDSTACKNEXT $basename $version $patch $initScript $enVarName $varName] - }] - if {[dict get $stackrecord implementation] ne ""} { - set old_tcl_findLibrary [dict get $stackrecord implementation] - puts stderr "packagetrace::init renamed $targetcommand to $old_tcl_findLibrary and is providing an override" - } else { - puts stderr "packagetrace::init failed to rename $targetcommand" - } - - - - set package_command [namespace which package] - set stackrecord [commandstack::rename_command -renamer packagetrace $package_command {subcommand args} { - set tracerlist $::packagetrace::tracerlist - set tracer [lindex $tracerlist end] - if {$tracer eq ""} { - - } - set ch $::packagetrace::chan - set next $COMMANDSTACKNEXT - if {$next ne "$COMMANDSTACKNEXT_ORIGINAL"} { - puts stderr "(packagetrace package) DEBUG - command changed since start: $COMMANDSTACKNEXT_ORIGINAL is now $next" - } - - #cache $ch instead of using upvar, - #because namespace may be deleted during call. - - #!todo - optionally silence Tcl & Tk requires to reduce output? - #if {[lindex $args 0] eq "Tcl"} { - # return [$next $subcommand {*}$args] - #} - switch -exact -- [tcl::prefix::match {files forget ifneeded names prefer present provide require unknown vcompare versions vsatisfies} $subcommand] { - require { - #columns - set c1 [string repeat { } 30] ;#tree col - set c1a " " - set c2 [string repeat { } 20] ;#package name col - set c2a " " ;# close require/present tags - set c3 [string repeat { } 18] ;#version col - must handle v= "999999.0a1.0" without truncation - set c4 [string repeat { } 12] ;#timing col 5 chars of punct leaving remainder for value. - set c5 [string repeat { } 10] ;#module col - set c5a [string repeat { } 3] ;#close result tag col - - #we assume 'package require' API sticks to solo option flags like -exact and is relatively stable. - set argidx 0 - set is_exact 0 - foreach a $args { - if {[string range $a 0 0] ne "-"} { - #assume 1st non-dashed argument is package name - set pkg $a - set v_requirements [lrange $args $argidx+1 end] - #normalize - if {$is_exact} { - set req [lindex $v_requirements 0] ;#only one is allowed for -exact - set v_requirements $req-$req ;#translate to v-v normalised equiv of -exact - } else { - set reqs [list] - foreach req $v_requirements { - lappend reqs [::packagetrace::tm_version_required_canonical $v_requirement] ;#empty remains empty, v -> v-, leading zeros stripped from all segments. - } - set v_requirements $reqs ;#each normalised - } - set pkg_ [lrange $args $argidx end] ;# raw e.g "Tcl 8.6" or "Tcl 8.5 9" - break - } else { - if {$a eq "-exact"} { - set is_exact 1 - } - } - incr argidx - } - - - incr ::packagetrace::level - if {$::packagetrace::level == 0} { - set packagetrace::output "" - } - - - set marg [string repeat { } $::packagetrace::level] - set margnext [string repeat { } [expr {$::packagetrace::level + 1}]] - - if {![catch {set ver [$next present {*}$args]}]} { - if {$::packagetrace::showpresent} { - #already loaded.. - set f1 [packagetrace::overtype::left $c1 "${marg} " - #puts -nonewline $ch $f1$c1a$f2$f2a$f3$f4$f5\n - packagetrace::emit $f1$c1a$f2$f2a$f3$f4$f5$f5a\n - } - } else { - set f1 [packagetrace::overtype::left $c1 "${marg} " - #puts -nonewline $ch $f1$c1a$f2$f2a$f3$f4$f5\n - packagetrace::emit $f1$c1a$f2$f2a$f3$f4$f5$f5a\n - - set errMsg "" - #set t0 [clock clicks -milliseconds] - set t0 [clock microseconds] - - if {[catch {set ver [$next require {*}$args]} errMsg]} { - set ver "" - # - #NOTE error must be raised at some point - see below - } - #set t [expr {[clock clicks -millisec]-$t0}] - set t [expr {([clock microseconds]-$t0)/1000.0}] - - - - - #jmn - set f1 [packagetrace::overtype::left $c1 "${margnext} [expr {[string length $c4]}]} { - set f4 "[packagetrace::overtype::left [string range $c4 0 end-1] [string trimright $f4]]\"" - } - - if {[string length $ver]} { - set num "" - foreach c [split $ver ""] { - if {[string is digit $c] || $c eq "."} { - append num $c - } else { - break - } - } - set ver $num - - #review - scr not guaranteed to be valid tcl list - should parse properly? - set scr [$next ifneeded $pkg $ver] - if {[string range $scr end-2 end] ne ".tm"} { - set f5 $c5 - } else { - #!todo - optionally output module path instead of boolean? - #set f5 [packagetrace::overtype::left -ellipsis 1 -ellipsistext ... [string range $c5 0 end-1] "tm= \"[lindex $scr end]"] - set f5 [packagetrace::overtype::left -ellipsis 1 -ellipsistext ... [string range $c5 0 end-1] "tm= \"1"] - if {[string length [string trimright $f5]] <= [expr [string length $c5]-1]} { - set f5 [packagetrace::overtype::left $c5 [string trimright $f5]\"] - } - } - } else { - set f5 $c5 - } - - set f5a [packagetrace::overtype::left $c5a "/>"] ;#end of "] - set f1a "" - set f2 "" - set c2a "" - set f3 "" - set f4 "" - set f5 "" - set f5a "" - #puts -nonewline $ch $f1$f1a$f2$c2a$f3$f4$f5$f5a\n - packagetrace::emit $f1$f1a$f2$c2a$f3$f4$f5$f5a\n - - - if {![string length $ver]} { - if {[lindex $args 0] eq "packagetrace"} { - #REVIEW - what is going on here? - namespace delete ::packagetrace::overtype - } - - #we must raise an error if original 'package require' would have - incr ::packagetrace::level -1 - error $errMsg - } - - } - incr ::packagetrace::level -1 - return $ver - } - vcompare - vsatisifies - provide - ifneeded { - set result [$next $subcommand {*}$args] - #puts -nonewline $ch " -- package $subcommand $args\n" - return $result - } - default { - set result [$next $subcommand {*}$args] - #puts $ch "*** here $subcommand $args" - return $result - } - } - - }] - if {[set stored_target [dict get $stackrecord implementation]] ne ""} { - puts stderr "packagetrace::init renamed $package_command to $stored_target and is providing an override" - set f1 [string repeat { } 30] - #set f1a " " - set f1a "" - set f2 [packagetrace::overtype::left [string repeat { } 20] "PACKAGE"] - set f2a " " - set f3 [packagetrace::overtype::left [string repeat { } 15] "VERSION"] - set f4 [packagetrace::overtype::left [string repeat { } 12] "LOAD-ms"] - set f5 [packagetrace::overtype::left [string repeat { } 10] "MODULE"] - - #puts -nonewline $::packagetrace::chan "-$f1$f1a$f2$f2a$f3$f4$f5\n" - #packagetrace::emit "-$f1$f1a$f2$f2a$f3$f4$f5\n" - puts -nonewline stderr "-$f1$f1a$f2$f2a$f3$f4$f5\n" - unset f1 f1a f2 f2a f3 f4 f5 - } else { - puts stderr "packagetrace::init failed to rename $package_command" - } - } - } -} - - - -#The preferred source of the ::overtype:: functions is the 'overtype' package -# - pasted here because packagetrace should have no extra dependencies. -# - overtype package has better support for ansi and supports wide chars -namespace eval packagetrace::overtype {set version INLINE} - -namespace eval packagetrace::overtype { - 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}] - if {$diff > 0} { - if {$opt(-overflow)} { - return $overtext - } else { - if {$opt(-ellipsis)} { - return [right [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]" - } - } - - proc centre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - foreach {undertext overtext} [lrange $args end-1 end] break - - set opt(-bias) left - set opt(-overflow) 0 - array set opt [lrange $args 0 end-2] - - - set olen [string length $overtext] - set ulen [string length $undertext] - set diff [expr {$ulen - $olen}] - if {$diff > 0} { - set half [expr {round(int($diff / 2))}] - if {[string match right $opt(-bias)]} { - if {[expr {2 * $half}] < $diff} { - incr half - } - } - - set rhs [expr {$diff - $half - 1}] - set lhs [expr {$half - 1}] - - set a [string range $undertext 0 $lhs] - set b $overtext - set c [string range $undertext end-$rhs end] - return $a$b$c - } else { - if {$diff < 0} { - if {$opt(-overflow)} { - return $overtext - } else { - return [string range $overtext 0 [expr {$ulen - 1}]] - } - } else { - return $overtext - } - } - } - - proc right {args} { - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] undertext overtext - - 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 packagetrace::deinit {} { - packagetrace::disable - #namespace delete packagetrace - #package forget packagetrace -} -proc packagetrace::disable {} { - ::commandstack::remove_rename {::tcl_findLibrary packagetrace} - ::commandstack::remove_rename {::package packagetrace} -} -proc packagetrace::enable {} { - #init doesn't clear state - so this is effectively an alias - tailcall packagetrace::init -} - -#clear state - reset to defaults -proc packagetrace::clear {} { - variable chan - set chan stderr - variable showpresent - set showpresent 1 -} - -package provide packagetrace [namespace eval packagetrace { - set version 0.8 -}] - - diff --git a/src/vfs/_vfscommon.vfs/modules/poshinfo-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/poshinfo-0.1.0.tm index 60069618..7fa8f99b 100644 --- a/src/vfs/_vfscommon.vfs/modules/poshinfo-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/poshinfo-0.1.0.tm @@ -207,7 +207,7 @@ tcl::namespace::eval poshinfo { -help "e.g omp" -as -default "table" -choices {list showlist dict showdict table tableobject plaintext}\ -help "return type of result" - @values -min 0 + @values -min 0 globs -multiple 1 -default * -help "" } proc themes {args} { diff --git a/src/vfs/_vfscommon.vfs/modules/punk-0.1.tm b/src/vfs/_vfscommon.vfs/modules/punk-0.1.tm index 4473c1d1..91ead8b0 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk-0.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk-0.1.tm @@ -26,13 +26,11 @@ namespace eval punk { #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 + list apply {{ap tmlist} { + set ::auto_path $ap tcl::tm::list - set ::tcl::tm::paths \$tmlist - }} {$::auto_path} {[tcl::tm::list]} - " + set ::tcl::tm::paths $tmlist + }} $::auto_path [tcl::tm::list] } @@ -283,14 +281,6 @@ namespace eval punk { #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;" - } # ------------------------ #Note that unlike an ordinary Tcl array - the linked ::env behaves differently. @@ -307,6 +297,15 @@ namespace eval punk { } # ------------------------ + 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;" + } + #change2 if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { set lookfor [list $name] @@ -5492,9 +5491,10 @@ namespace eval punk { #ctrl-c propagation also needs to be considered set teehandle punksh - uplevel 1 [list ::catch \ - [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ - ::tcl::UnknownResult ::tcl::UnknownOptions] + uplevel 1 [list ::catch {*}{ + } [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{ + } ::tcl::UnknownResult ::tcl::UnknownOptions + ] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { dict set ::tcl::UnknownOptions -code error @@ -8428,19 +8428,44 @@ namespace eval punk { set I [punk::ansi::a+ italic] set NI [punk::ansi::a+ noitalic] + set sizedict [punk::console::get_size] + set cols [dict get $sizedict columns] + set rows [dict get $sizedict rows] + + + + #todo - provide a mechanism to configure the default frametype everywhere and describe it in this help. + + set frametype ascii ;#conservative default. + #if the test char width fails - it's likely we're on a very old terminal that doesn't support unicode at all. + if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + if {$cols <= 80} { + # Be conservative with frame types on narrow terminals for help. + # an 80x30 terminal is more likely to be an older style terminal and may not have unicode support. + # unicode on a non-unicode terminal is a bad experience - with the frame chars showing as garbage (e.g 3 chars per grapheme). + set frametype ascii + } else { + if {$testcharwidth == 1} { + set frametype light ;#unicode box-drawing chars. + } + } + } + + # ------------------------------------------------------- set logoblock "" if {[catch { package require patternpunk #lappend chunks [list stderr [>punk . rhs]] - append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]] + append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]] }]} { - append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""] + append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""] } set title "[a+ brightgreen] Help System: " set cmdinfo [list] lappend cmdinfo [list help "?${I}topic${NI}?" "This help.\nTo see available subitems type:\nhelp topics\n\nFor an unrecognised ${I}topic${NI}\nhelp will look for basic\ninfo for it as a command.\n"] set t [textblock::class::table new -minwidth 51 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8468,6 +8493,7 @@ namespace eval punk { lappend cmdinfo [list newdir "${I}subdir${NI}..." "make new dir or dirs and show status"] lappend cmdinfo [list fcat "${I}file ?file?...${NI}" "cat file(s)"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8491,6 +8517,7 @@ namespace eval punk { lappend cmdinfo [list "nn/" "" "go up one namespace"] lappend cmdinfo [list "newns" "${I}ns${NI}" "make child namespace and switch to it"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8513,6 +8540,7 @@ namespace eval punk { lappend cmdinfo [list eg "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show example from manpage"] lappend cmdinfo [list corp "${I}proc${NI}" "View proc body and arguments with basic highlighting"] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8537,6 +8565,7 @@ namespace eval punk { lappend cmdinfo [list a "?${I}colourcode${NI}...?" "Return ANSI codes (with leading reset)\n e.g puts \"\[a+ purple\]purple\[a Green\]normal on green\[a\]\"\n [a+ purple]purple[a Green]normal on green[a] "] set t [textblock::class::table new -minwidth 80 -show_seps 0] + $t configure -frametype $frametype foreach row $cmdinfo { $t add_row $row } @@ -8606,6 +8635,7 @@ namespace eval punk { set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + $t configure -frametype $frametype if {"windows" eq $::tcl_platform(platform)} { #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. #The Tcl ::env array is linked to the underlying process view of the environment @@ -8634,6 +8664,7 @@ namespace eval punk { $t destroy set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + $t configure -frametype $frametype foreach {v vinfo} $otherenv_config { if {[info exists ::env($v)]} { set env_val [set ::env($v)] @@ -8851,6 +8882,7 @@ namespace eval punk { }] set t [textblock::class::table new -show_seps 0] + $t configure -frametype $frametype $t add_column -headers [list "Topic"] $t add_column foreach {k v} $topics { diff --git a/src/vfs/_vfscommon.vfs/modules/punk/aliascore-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/aliascore-0.1.0.tm index 22c27388..296fa148 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/aliascore-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/aliascore-0.1.0.tm @@ -109,6 +109,7 @@ tcl::namespace::eval punk::aliascore { set aliases [tcl::dict::create {*}{ val ::punk::pipe::val tstr ::punk::args::lib::tstr + FOR ::punk::lib::FOR list_as_lines ::punk::lib::list_as_lines lines_as_list ::punk::lib::lines_as_list linelist ::punk::lib::linelist diff --git a/src/vfs/_vfscommon.vfs/modules/punk/ansi-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/ansi-0.1.1.tm index f9997649..e8518d0f 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/ansi-0.1.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/ansi-0.1.1.tm @@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi { @cmd -name punk::ansi::sauce -summary\ "SAUCE info from file"\ -help\ - "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data." + "Wrapper for punk::ansi::sauce::from_file to display SAUCE block data. + + Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format + that was commonly used in old ANSI art files to store information about the file, such as + title, author, group, date, and comments. + It may also have fields to specify the number of columns and rows in the ANSI art, + as well as flags for specific display attributes. + + It may also be used on other types of files such as bitmap, vector, audio, binarytext, + xbin, archive and executable files. + + It is a 128-byte block of data that is typically appended to the end of a file. + https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm" -encoding -default iso8859-1 -type string -help\ "The default iso8859-1 is equivalent to binary ans should work in the usual case. @@ -6044,6 +6056,7 @@ be as if this was off - ie lone CR. #[para]These functions will emit the code - but read it in from stdin so that it doesn't display, and then return the row and column as a colon-delimited string or list respectively. #[para]The punk::ansi::cursor_pos function is used by punk::console::get_cursor_pos and punk::console::get_cursor_pos_list return \033\[6n + #same as 'tput u7' which just emits CSI 6n to stdout } proc cursor_pos_extended {} { @@ -6053,6 +6066,7 @@ be as if this was off - ie lone CR. } + #DECFRA - Fill rectangular area #REVIEW - vt100 accepts decimal values 132-126 and 160-255 ("in the current GL or GR in-use table") #some modern terminals accept and display characters outside this range - but this needs investigation. diff --git a/src/vfs/_vfscommon.vfs/modules/punk/ansi/sauce-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/ansi/sauce-0.1.0.tm index 79ea5901..e7428d84 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/ansi/sauce-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/ansi/sauce-0.1.0.tm @@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce { variable PUNKARGS variable PUNKARGS_aliases + #https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm lappend PUNKARGS [list { @id -id "(package)punk::ansi::sauce" @package -name "punk::ansi::sauce" -help\ diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.2.tm deleted file mode 100644 index fc54929b..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.2.tm +++ /dev/null @@ -1,10705 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::args 0.2 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.2] -#[copyright "2024"] -#[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] -#[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -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 -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -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 "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args::register}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::moduledoc::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - -tcl::namespace::eval ::punk::args {} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %G% \x1b\[32m %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name - -summary - -help - %B%@leaders%N% ?opt val...? - (used for leading args that come before switches/opts) - directive-options: - -min -max (min and max number of leaders) - -unnamed (allow unnamed positional leaders) - -takewhenargsmodulo (assign args to leaders based on modulo - of total number of args. If value is not supplied (or < 2) then - leaders are assigned based on whether configured opts are - encountered, and whether the min number of leaders and values - can be satisfied. In this case optional leaders are assigned if - the type of the argument can be matched.) - (also accepts options as defaults for subsequent leader definitions) - %B%@opts%N% ?opt val...? - directive-options: -any|-arbitrary - (also accepts options as defaults for subsequent flag definitions) - %B%@values%N% ?opt val...? - (used for trailing args that come after switches/opts) - directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent value definitions) - %B%@form%N% ?opt val...? - (used for commands with multiple forms) - directive-options: -form -synopsis - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@formdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (override autogenerated arg info for form) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@examples%N% ?opt val...? - directive-options: -help - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - A typenamelist represents a multi-value clause where each - value must match the specified type in order. This is not - valid for flags - which can only take a single value. - - typename and entries in typenamelist can take 2 forms: - 1) basic form: elements of llength 1 such as a simple type, - or a pipe-delimited set of type-alternates. - e.g for a single typename: - -type int, -type int|char, -type int|literal(abc) - e.g for a typenamelist - -type {int double}, -type {int|char double} - 2) special form: elements of variable length - e.g for a single typename: - -type {{literal |}} - -type {{literal | | literal (}} - e.g for a typenamelist - -type {{literal |} {stringstartswith abc | int}} - The 2 forms can be mixed: - -type {{literal |} {stringstartswith a|c | int} literal(xyz)|int} - - Defaults to string. If no other restrictions - are required, choosing -type any does the least validation. - recognised types: - any - (unvalidated - accepts anything) - unknown - (unvalidated - accepts anything) - none - (used for flags/switches only. Indicates this is - a 'solo' flag ie accepts no value) - Not valid as a member of a clause's typenamelist. - int - integer - number - list - indexexpression - indexset - (as accepted by punk::lib::is_indexset) - dict - double - float - bool - boolean - char - file - directory - ansistring - globstring - (any of the types accepted by 'string is') - - The above all perform some validation checks - - string - (also any of the 'string is' types such as - xdigit, graph, punct, lower etc) - -type string on its own does not need validation, - but still checks for string-related restrictions - such as regexprefail, & minsize - - literal() - (exact match for string) - literalprefix() - (prefix match for string, other literal and literalprefix - entries specified as alternates using | are used in the - calculation) - stringstartswith() - (value must match glob *) - The value of string must not contain pipe char '|' - - Note that types can be combined with | to indicate an 'or' - operation - e.g char|int - e.g literal(xxx)|literal(yyy) - e.g literalprefix(text)|literalprefix(binary) - (when all in the pipe-delimited type-alternates set are - literal or literalprefix - this is similar to the -choices - option) - - - and more.. (todo - document here) - If a typenamelist is supplied and has length > 1 - then -typeranges must be used instead of -range - The number of elements in -typeranges must match - the number of elements specified in -type. - - -typesynopsis - Must be same length as value in -type - This provides and override for synopsis display of types. - Any desired italicization must be applied manually to the - value. - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choiceprefixreservelist {} - These choices are additional values used in prefix calculation. - The values will not be added to the list of available choices. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant - only valid if -type is a single item) - -typeranges (list with same number of elements as -type) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - #text should be a well-formed Tcl list - text -type list -multiple 1 -help\ - {Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - ${[punk::args::moduledoc::tclcore::argdoc::example { - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\ - "Description of command" - - %G%#The following option defines an option-value pair%R% - %G%#It may have aliases by separating them with a pipe |%R% - -fg|-foreground -default blah -type string -help\ - "In the result dict returned by punk::args::parse - the value used in the opts key will always be the last - entry, in this case -foreground" - %G%#The following option defines a flag style option (solo)%R% - -flag1 -default 0 -type none -help\ - "Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars" - - @values -min 1 -max -1 - %G%#Items that don't begin with * or - are value definitions%R% - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } "@doc -name Manpage: -url [myfunc_manpage_geturl myns::myfunc]"}]} - } - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderdirective_defaults [tcl::dict::create\ - -type any\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optdirective_defaults [tcl::dict::create\ - -type any\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - -parsekey ""\ - -group ""\ - ] - #parsekey is name of argument to use as a key in punk::args::parse result dicts - - set valdirective_defaults [tcl::dict::create\ - -type any\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED [list]\ - LEADER_NAMES [list]\ - LEADER_MIN ""\ - LEADER_MAX ""\ - LEADER_TAKEWHENARGSMODULO 0\ - LEADER_UNNAMED false\ - LEADERSPEC_DEFAULTS $leaderdirective_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - OPT_DEFAULTS [tcl::dict::create]\ - OPT_REQUIRED [list]\ - OPT_NAMES [list]\ - OPT_ANY 0\ - OPT_MIN ""\ - OPT_MAX ""\ - OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optdirective_defaults\ - OPT_CHECKS_DEFAULTS {}\ - OPT_GROUPS {}\ - VAL_DEFAULTS [tcl::dict::create]\ - VAL_REQUIRED [list]\ - VAL_NAMES [list]\ - VAL_MIN ""\ - VAL_MAX ""\ - VAL_UNNAMED false\ - VALSPEC_DEFAULTS $valdirective_defaults\ - VAL_CHECKS_DEFAULTS {}\ - FORMDISPLAY [tcl::dict::create]\ - ] - - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set lvl 2 - set id [rawdef_id $args $lvl] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - undefine $id 0 - - ##dict unset argdata_cache $prevraw ;#silently does nothing if key not present - #dict for {k v} $argdata_cache { - # if {[dict get $v id] eq $id} { - # dict unset argdata_cache $k - # } - #} - #dict for {k v} $rawdef_cache { - # if {[dict get $v -id] eq $id} { - # dict unset rawdef_cache $k - # } - #} - #dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - proc undefine {id {quiet 0}} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[id_exists $id]} { - if {!$quiet} { - puts stderr "punk::args::undefine clearing existing data for id:$id" - } - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } else { - if {!$quiet} { - puts stderr "punk::args::undefine unable to find id: '$id'" - } - } - } - #'punk::args::parse $args withdef $deflist' can raise parsing error after an autoid was generated - # In this case we don't see the autoid in order to delete it - #proc undefine_deflist {deflist} { - #} - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus but terribly slow without an accelerator) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - #punk::args::get_by_id ::punk::args::define {} - punk::args::parse {} -errorstyle minimal withid ::punk::args::define - return - } - #if {[lindex $args 0] eq "-dynamic"} { - # set is_dynamic [lindex $args 1] - # set textargs [lrange $args 2 end] - #} - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - #JJJ - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - #set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -paramindents none -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - #set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - #JJJ - review - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands -paramindents none $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache [list $optionspecs]]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache [list $optionspecs]] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - - #set opt_defaults [tcl::dict::create] - #set val_defaults [tcl::dict::create] - - #set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - set examples_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - #set opt_any 0 - #set val_min 0 - #set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::resolve @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::moduledoc::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #NOTE - this is switch arm for the literal "default" (@default) - not the default arm of the switch block! - - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - - #foreach fid $record_form_ids { - # #only use elements with matching form id? - # #probably this feature mainly useful for _default anyway so that should be ok - # #cooperative doc sets specified in same file could share via known form ids too - # FORMDISPLAY has keys -header -body - # if {![dict size $F $fid $FORMDISPLAY]} { - # if {[dict exists $copyfrom FORMS $fid FORMDISPLAY]} { - # dict set F $fid FORMDISPLAY [dict get $copyfrom FORMS $fid FORMDISPLAY] - # } - # } - # #TODO - # #create leaders opts vals depending on position of @default line? - # #options on @default line to exclude/include sets??? - #} - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - - # @form "-synopsis" is optional - and only exists in case the user really wants - # to display something different. The system should generate consistent synopses - # with appropriate italics/bracketing etc. - # For manual -synopsis - features such as italics must be manually added. - - #spitballing.. - #The punk::args parser should generally be able to determine the appropriate form based - #on supplied arguments, e.g automatically using argument counts and matching literals. - #We may need to support some hints for forcing more efficient -form discriminators - # - # e.g compare with -takewhenargsmodulo that is available on @leaders - - #the -arities idea below is a rough one; potentially something to consider.. but - #we want to be able to support command completion.. and things like literals should probably - #take preference for partially typed commands.. as flipping to other forms based on argcount - #could be confusing. Need to match partial command to closest form automatically but allow - #user to lock in a form interactively and see mismatches (?) - #Probably the arity-ranges of a form are best calculated automatically rather than explicitly, - #otherwise we have a strong potential for misdefinition.. (conflict with defined leaders,opts,values) - #The way forward might be to calculate some 'arity' structure from the forms to aid in form-discrimination at arg parse time. - #(this is currently covered in some ways by the LEADER_MIN,LEADER_MAX,OPT_MIN,OPT_MAX,VAL_MIN,VAL_MAX members of the FORMS dict.) - - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - #e.g -name - # -summary - # -help - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - formdisplay { - #override the displayed argument table for the form. - #(formdisplay keys -header -body) - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - foreach fid $record_form_ids { - tcl::dict::set F $fid FORMDISPLAY [dict merge [tcl::dict::get $F $fid FORMDISPLAY] $at_specs] - } - } - opts { - foreach fid $record_form_ids { - if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::resolve - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - tcl::dict::set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -any - -arbitrary - - -anyopts { - #set opt_any $v - tcl::dict::set F $fid OPT_ANY $v - } - -min { - dict set F $fid OPT_MIN $v - } - -max { - #if no -max explicitly specified, and llength OPT_NAMES == 0 and OPT_ANY == 0 - -max will be set to 0 below. - dict set F $fid OPT_MAX $v - } - -minsize - -maxsize - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - #v is a typelist - #foreach t $v { - # #validate? - #} - tcl::dict::set tmp_optspec_defaults -type $v - } - -defaultdisplaytype { - #how the -default is displayed - #-default doesn't have to be the same type as -type which validates user input that is not defaulted. - tcl::dict::set tmp_optspec_defaults -defaultdisplaytype $v - } - -parsekey { - tcl::dict::set tmp_optspec_defaults -parsekey $v - - } - -group { - tcl::dict::set tmp_optspec_defaults -group $v - if {$v ne "" && ![tcl::dict::exists $F $fid OPT_GROUPS $v]} { - tcl::dict::set F $fid OPT_GROUPS $v {-parsekey {} -help {}} - } - if {$v ne ""} { - if {[tcl::dict::exists $at_specs -parsekey]} { - tcl::dict::set F $fid OPT_GROUPS $v -parsekey [tcl::dict::get $at_specs -parsekey] - } - } - } - -grouphelp { - if {![tcl::dict::exists $at_specs -group]} { - error "punk::args::resolve Bad @opt line. -group entry is required if -grouphelp is being configured. @id:$DEF_definition_id" - } - set g [tcl::dict::get $at_specs -group] - if {$g eq ""} { - error "punk::args::resolve Bad @opt line. -group non-empty value is required if -grouphelp is being configured. @id:$DEF_definition_id" - } - set groupdict [tcl::dict::get $F $fid OPT_GROUPS] - #set helprecords [tcl::dict::get $F $fid OPT_GROUPS_HELP] - if {![tcl::dict::exists $groupdict $g]} { - tcl::dict::set F $fid OPT_GROUPS $g [dict create -parsekey {} -help $v] - } else { - tcl::dict::set F $fid OPT_GROUPS $g -help $v - } - } - -range { - if {[dict exists $at_specs -type]} { - set tp [dict get $at_specs -type] - } else { - set tp [dict get $tmp_optspec_defaults -type] - } - if {[llength $tp] == 1} { - tcl::dict::set tmp_optspec_defaults -typeranges [list $v] - } else { - error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -range only applies to single-item type. Use -typeranges instead. @id:$DEF_definition_id" - } - } - -typeranges { - if {[dict exists $at_specs -type]} { - set tp [dict get $at_specs -type] - } else { - set tp [dict get $tmp_optspec_defaults -type] - } - if {[llength $tp] != [llength $v]} { - error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -typeranges has length [llength $v]. Lengths must match. @id:$DEF_definition_id" - } - tcl::dict::set tmp_optspec_defaults -typeranges $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -validationtransform { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple - - -prefix { - #check is bool - if {![string is boolean -strict $v]} { - error "punk::args::resolve - Option '$k' has value '$v'of wrong type in @opts line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -parsekey -group -grouphelp\ - -any -anyopts -arbitrary -form -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -type -range -typeranges -default -defaultdisplaytype -typedefaults - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::resolve - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - tcl::dict::set F $fid OPTSPEC_DEFAULTS $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::resolve - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -takewhenargsmodulo { - dict set F $fid LEADER_TAKEWHENARGSMODULO $v - } - -choiceprefix - - -choicerestricted { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo - -choicelabels { - if {[llength $v] %2 != 0} { - error "punk::args::resolve - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - #$v is a list of types - #foreach t $v { - #validate? - #} - #switch -- $v { - # int - integer { - # set v int - # } - # char - character { - # set v char - # } - # bool - boolean { - # set v bool - # } - # dict - dictionary { - # set v dict - # } - # list { - - # } - # index { - # set v indexexpression - # } - # default { - # #todo - disallow unknown types unless prefixed with custom- - # } - #} - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -range { - tcl::dict::set tmp_leaderspec_defaults -range $v - } - -typeranges { - tcl::dict::set tmp_leaderspec_defaults -range $v - } - -minsize - -maxsize - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefixdenylist - -choiceprefixreservelist - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -unnamed { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - dict set F $fid LEADER_UNNAMED $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::resolve - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -unnamed\ - } - error "punk::args::resolve - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid LEADERSPEC_DEFAULTS $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid VALSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is 0. got $v @id:$DEF_definition_id" - } - #set val_min $v - dict set F $fid VAL_MIN $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - #set val_max $v - dict set F $fid VAL_MAX $v - } - -minsize - -maxsize - -choices - -choicemultiple - -choicecolumns - - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::resolve - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -range { - tcl::dict::set tmp_valspec_defaults -range $v - } - -typeranges { - tcl::dict::set tmp_valspec_defaults -typeranges $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform { - tcl::dict::set tmp_valspec_defaults $k $v - } - -unnamed { - if {![string is boolean -strict $v]} { - error "punk::args::resolve - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" - } - dict set F $fid VAL_UNNAMED $v - } - default { - set known { -type -range -typeranges\ - -min -form -minvalues -max -maxvalues\ - -minsize -maxsize\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -unnamed\ - } - error "punk::args::resolve - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid VALSPEC_DEFAULTS $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - examples { - set examples_info [dict merge $examples_info $at_specs] - } - default { - error "punk::args::resolve - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @examples @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argdef_values $record_values - #Note that we can get options defined with aliases e.g "-x|-suppress" - #Here we store the full string as the argname - but in the resulting dict upon parsing it will have the final - # entry as the key for retrieval e.g {leaders {} opts {-suppress true} values {} ...} - - #we can also have longopts within the list e.g "-f|--filename=" - #This accepts -f or --filename= - # (but not --filename ) - #if the clausemember is optional - then the flag can act as a solo, but a parameter can only be specified on the commandline with an = - #e.g "-x|--something= -type ?string? - #accepts all of: - # -x - # --something - # --something=blah - - - #while most longopts require the = some utilities (e.g fossil) - #accept --longname - #(fossil accepts either --longopt or --longopt=) - #For this reason, "-f|--filename" is different to gnu-style longopt "-f|--filename=" - - #for "--filename=" we can specify an 'optional' clausemember using for example -type ?string? - - #4? cases - #1) - #--longopt - # (not really a longopt - can only parse with --longopt - [optional member not supported, but could be solo if -type none]) - #2) - #--longopt= - # (gnu style longopt - parse with --longopt= - solo allowed if optional member - does not support solo via -type none) - #3) - #--longopt|--longopt= -types int - # (mixed - as fossil does - parse with --longopt= or --longopt [optional member not supported?]) - #4) - # --xxx|--longopt= -types {?int?} - #(repeating such as --longopt --longopt= not valid?) - #redundant? - #ie --longopt|--longopt= -types {?int?} - # equivalent to - # --longopt= -types {?int?} - #allow parsing -xxx only as solo and --longopt as solo or --longopt=n ? - - #the above set would not cover the edge-case where we have an optional member but we don't want --longopt to be allowed solo - #e.g - #-soloname|--longopt= -types ?int? - #allows parsing "-soloname" or "--longopt" or "--longopt=n" - #but what if we want it to mean only accept: - # "-soloname" or "--longopt=n" ?? - - #we deliberately don't support - #--longopt -type ?type? - #or -opt -type ?type? - #as this results in ambiguities and more complexity in parsing depending on where flag occurs in args compared to positionals - - #for these reasons - we can't only look for leading -- here to determine 'longopt' - - - set argname $firstword - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - #do some basic validation here - #1 "-type none" would not be valid for "--filename=" - #2 a -type can only be optional (specified as -type ?xxx?) if at least one entry in the argname has a trailing = - #3 require --longopt if has a trailing =. ie disallow -opt= ? - - set has_equal 0 - set optaliases [split $firstword |] - if {[lsearch $optaliases *=] >=0} { - set has_equal 1 - } - #todo - if no -type specified in this flag record, we still need to check the default -type from the @opts record - which could have been - #overridden from just 'string' - if {[tcl::dict::exists $argdef_values -type]} { - set tp [tcl::dict::get $argdef_values -type] - if {[llength $tp] != 1} { - #clauselength > 1 not currently supported for flags - #e.g -myflag -type {list int} - # e.g called on commandline with cmd -myflag {a b c} 3 - #review - seems an unlikely and complicating feature to allow - evidence of tools using/supporting this in the wild not known of. - error "punk::args::resolve - Multiple space-separated arguments (as indicated by -type having multiple entries) for a flag are not supported. flag $argname -type '$tp' @id:$DEF_definition_id" - } - if {$argname eq "--"} { - if {$tp ne "none"} { - #error to explicitly attempt to configure -- as a value-taking option - error "punk::args::resolve - special flag named -- cannot be configured as a value-accepting flag. set -type none or omit -type from definition. @id:$DEF_definition_id" - } - } - if {$tp eq "none"} { - if {$has_equal} { - error "punk::args::resolve - flag type 'none' (indicating non-parameter-taking flag) is not supported when any flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" - } - } elseif {[string match {\?*\?} $tp]} { - #optional flag value - if {!$has_equal} { - error "punk::args::resolve - Optional flag parameter (as indicated by leading & trailing ?) is not supported when no flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" - } - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - ##set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - - tcl::dict::set argdef_values -ARGTYPE option - #set all_choices [_resolve_get_record_choices] - - foreach fid $record_form_ids { - switch -exact -- [dict get $F $fid argspace] { - leaders { - dict set F $fid argspace "options" - } - values { - error "punk::args::resolve - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - } - set record_type option - - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - #This can happen if the definition has repeated values - error "punk::args::resolve - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - if {[dict get $F $fid LEADER_MAX] < [llength $temp_leadernames]} { - puts stderr "punk::args::resolve warning arg $argname LEADER_MAX == [dict get $F $fid LEADER_MAX] but [llength $temp_leadernames] leader names found @id:$DEF_definition_id" - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - if {$argname ni $temp_valnames} { - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - } else { - error "punk::args::resolve - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - #lappend val_names $argname - if {[dict get $F $fid VAL_MAX] >= 0} { - if {[dict get $F $fid VAL_MAX] < [llength $temp_valnames]} { - puts stderr "punk::args::resolve warning arg $argname VAL_MAX == [dict get $F $fid VAL_MAX] but [llength $temp_valnames] value names found @id:$DEF_definition_id" - dict set F $fid VAL_MAX [llength $temp_valnames] - } - } - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a leader, value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid VALSPEC_DEFAULTS] - } else { - set spec_merged [dict get $F $fid LEADERSPEC_DEFAULTS] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form {} - -type { - #todo - could be a list e.g {any int literal(Test)} - #case must be preserved in literal bracketed part - set typelist [list] - foreach typespec $specval { - if {[string match {\?*\?} $typespec]} { - set tspec [string range $typespec 1 end-1] - set optional_clausemember true - } else { - set tspec $typespec - set optional_clausemember false - } - set type_alternatives [_split_type_expression $tspec] - set normlist [list] - foreach alt $type_alternatives { - set firstword [lindex $alt 0] - set lc_firstword [tcl::string::tolower $firstword] - #normalize here so we don't have to test during actual args parsing in main function - set normtype "" ;#assert - should be overridden in all branches of switch - switch -- $lc_firstword { - int - integer {set normtype int} - double - float { - #review - user may wish to preserve 'float' in help display - consider how best to implement - set normtype double - } - bool - boolean {set normtype bool} - char - character {set normtype char} - dict - dictionary {set normtype dict} - index - indexexpression {set normtype indexexpression} - indexset {set normtype indexset} - "" - none - solo { - if {$is_opt} { - #review - are we allowing clauses for flags? - #e.g {-flag -type {int int}} - #this isn't very tcl like, where we'd normally mark the flag with -multiple true and - # instead require calling as: -flag -flag - #It seems this is a reasonably rare/unlikely requirement in most commandline tools. - - if {[llength $specval] > 1} { - #makes no sense to have 'none' in a clause - error "punk::args::resolve - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" - } - #tcl::dict::set spec_merged -type none - set normtype none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - } else { - #solo only valid for flags - error "punk::args::resolve - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" - } - } - any - anything {set normtype any} - unknown { - #'unspecified' ?? - set normtype unknown - } - ansi - ansistring {set normtype ansistring} - string - globstring {set normtype $lc_firstword} - packageversion {set normtype packageversion} - packagerequirement {set normtype packagerequirement} - literal { - #value was split out by _split_type_expression - set normtype literal([lindex $alt 1]) - } - literalprefix { - set normtype literalprefix([lindex $alt 1]) - } - stringstartswith { - set normtype stringstartswith([lindex $alt 1]) - } - stringendswith { - set normtype stringendswith([lindex $alt 1]) - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - #tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - #todo - set normtype $alt - } - } - lappend normlist $normtype - } - set norms [join $normlist |] - if {$optional_clausemember} { - lappend typelist ?$norms? - } else { - lappend typelist $norms - } - } - tcl::dict::set spec_merged -type $typelist - } - -typesynopsis { - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount != [llength $specval]} { - error "punk::args::resolve - invalid -typesynopsis specification for argument '$argname'. -typesynopsis has [llength $specval] entries, but requires $typecount entries (one for each entry in -types. Use empty string list members for default) @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -typesynopsis $specval - } - -parsekey - -group { - tcl::dict::set spec_merged -typesynopsis $specval - } - -solo - - -choices - -choicegroups - -choicemultiple - -choicecolumns - - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -range { - #allow simple case to be specified without additional list wrapping - #only multi-types require full list specification - #arg1 -type int -range {0 4} - #arg2 -type {int string} -range {{0 4} {"" ""}} - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount == 1} { - tcl::dict::set spec_merged -typeranges [list $specval] - } else { - error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -range only applies to single-item type. Use -typeranges instead. @id:$DEF_definition_id" - } - } - -typeranges { - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount != [llength $specval]} { - error "punk::args::resolve - invalid -typeranges specification for argument '$argname'. -typeranges has [llength $specval] entries, but requires $typecount entries (one for each entry in -types) @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -typeranges $specval - } - -default { - #The -default is for when the entire clause is missing - #It doesn't necessarily have to have the same number of elements as the clause {llength $typelist} - #review - tcl::dict::set spec_merged -default $specval - if {![dict exists $argdef_values -optional]} { - tcl::dict::set spec_merged -optional 1 - } - } - -defaultdisplaytype { - tcl::dict::set spec_merged -defaultdisplaytype $specval - } - -typedefaults { - set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount != [llength $specval]} { - error "punk::args::resolve - invalid -typedefaults specification for argument '$argname'. -typedefaults has [llength $specval] entries, but requires $typecount entries (one for each entry in -types) @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -typedefaults $specval - } - -optional { - #applies to whole arg - not each -type - tcl::dict::set spec_merged -optional $specval - } - -ensembleparameter { - #applies to whole arg - not each -type - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #applies to whole arg - not each -type - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::resolve - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -command - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::resolve - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - #TODO! - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::resolve argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::resolve argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list\ - -form -type\ - -parsekey -group\ - -range -typeranges\ - -default -defaultdisplaytype -typedefaults\ - -minsize -maxsize -choices -choicegroups\ - -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::resolve - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize - if {$argname eq "--"} { - #force -type none - in case no -type was specified and @opts -type is some other default such as string - tcl::dict::set spec_merged -type none - } - if {[tcl::dict::get $spec_merged -type] eq "none"} { - dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] - } - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - #if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} {} - if {![tcl::dict::get $spec_merged -optional]} { - if {$is_opt} { - set temp_opt_required [dict get $F $fid OPT_REQUIRED] - lappend temp_opt_required $argname - dict set F $fid OPT_REQUIRED $temp_opt_required - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - } else { - set temp_val_required [dict get $F $fid VAL_REQUIRED] - lappend temp_val_required $argname - dict set F $fid VAL_REQUIRED $temp_val_required - } - } - } - - - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - #JJJ - set parsekey [dict get $F $fid ARG_INFO $argname -default] - if {$parsekey eq ""} { - set parsekey $argname - } - tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #now cycle through ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - if {[tcl::dict::get $F $fid OPT_MAX] eq ""} { - if {[llength [tcl::dict::get $F $fid OPT_NAMES]] == 0 && ![tcl::dict::get $F $fid OPT_ANY]} { - tcl::dict::set F $fid OPT_MAX 0 ;#aid in parsing to avoid scanning for opts unnecessarily - #review - when using resolved_def to create a definiation based on another - OPT_MAX may need to be overridden - a bit ugly? - } - } - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - disallow any -multiple == true entries if any leaders have -multiple == true? - #(creates parsing ambiguity) - #ambiguity could be resolved if at least one required option/flag eg -- - #ambiguities could theoretically also be resolved with required literals or choices - or even based on argument type - #(overcomplex? todo see if any core/tcllib commands work like that) - - #only allow a single entry within VAL_NAMES to have -multiple == true - #example of command with non-trailing -multiple == true is core command: 'file copy ?-force? ?--? source ?source?... targetDir - set val_multiples 0 - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - if {$val_multiples > 0} { - error "bad setting -multiple true on argument spec for value '$valname' in command form:'$fid'. Only a single value argument specification can be marked with -multiple true @id:$DEF_definition_id" - } - incr val_multiples - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata LEADERSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata OPTSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata VALSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - seealso_info $seealso_info\ - keywords_info $keywords_info\ - examples_info $examples_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - form_info $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache [list $optionspecs] $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @examples @formdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @examples @formdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - - Each argument can have members of its spec overridden using the - -override dictionary The members of each override sub dictionary are - usually options beginning with a dash. The key 'name' can be used to - override the name of the leader/option/value itself. - e.g - punk::args::resolved_def -types values -override {version {name version1 -optional 0}} (shared)::package version - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - switch -exact -- $a { - -type - -types { - incr i - dict set opts -types [lindex $args $i] - } - default { - if {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - } - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - #set fullk [tcl::prefix::match -error "" {-return -form -types -antiglobs -override} $k] - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @examples @seealso} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - - #todo @formdisplay - #todo @ref ? - - - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $directive [dict get $specdict FORMS $formname $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - set overdict [dict get $opt_override $m] - append result \n "\"$m\" [dict merge $argspec $overdict]" - dict set resultdict $m [dict merge $argspec $overdict] - } else { - append result \n "\"$m\" $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @examples - @seealso { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - #todo @formdisplay - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $type [dict get $specdict FORMS $formname $defaults_key] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - set overdict [dict get $opt_override $m] - if {[dict exists $opt_override $m name]} { - #special override for name of argument itself - #e.g - #punk::args::resolved_def -types values -override {version {name version1 -optional 1}} (shared)::package version - set newname [dict get $opt_override $m name] - dict unset overdict name - append result \n "\"$newname\" [dict merge $argspec $overdict]" - dict set resultdict $newname [dict merge $argspec $overdict] - } else { - append result \n "\"$m\" [dict merge $argspec $overdict]" - dict set resultdict $m [dict merge $argspec $overdict] - } - } else { - append result \n "\"$m\" $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$deflist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set deflist [raw_def $id] - if {$deflist eq ""} { - return - } - return [resolve {*}$deflist] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of first 3 blocks - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef {lvl 1}} { - set id "" - set found_id_line 0 - foreach d [lrange $rawdef 0 2] { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - set found_id_line 1 - #review - uplevel 2 would be a call from punk::args::define ?? - set rest [uplevel $lvl [list punk::args::lib::tstr -allowcommands $rest]] - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - } - break - } - } - } - if {$found_id_line} { - break - } - } - if {$id eq "" && $found_id_line} { - #Looked like an @id - but presumable the rest of the line was malformed. - #we won't produce an autoid for such a definition. - set first3blocks "" - foreach b [lrange $rawdef 0 2] { - append first3blocks $b\n - } - error "punk::args::rawdef_id found an @id line in the first 4 lines of one of the 1st 3 blocks - but failed to retrieve a value for it.\nraw_def 1st 3 blocks:\n$first3blocks" - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc aliases {} { - variable aliases - punk::lib::showdict $aliases - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - lappend PUNKARGS [list { - @id -id ::punk::args::update_definitions - @cmd -name punk::args::update_definitions\ - -summary\ - ""\ - -help\ - "" - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - #set id [rawdef_id $definitionlist] - set lvl 1 ;#level at which tstr substitution occurs in @id line - set id [namespace eval $pkgns [list punk::args::rawdef_id $definitionlist $lvl]] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {[string match (autodef)* $pkgns]} { - set pkgns [string range $pkgns 9 end] - } - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - set depth [info level] - set maxd [expr {min($depth,4)}] - set call_level [expr {-1 * $maxd}] - #set call_level -3 ;#for get_dict call - #set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - set maxloop 10 ;#failsafe - while {[string last \n $cmdinfo] >= 1 && $maxloop > -1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - incr maxloop -1 - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -parsedargs -type dict -help\ - "Result of successful punk::pargs::parse - (currently only looks at 'received')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - -form -default 0 -help\ - "Ordinal index or name of command form" - }] ] - - - variable arg_error_CLR - array set arg_error_CLR {} - variable arg_error_CLR_nocolour - array set arg_error_CLR_nocolour {} - variable arg_error_CLR_info - array set arg_error_CLR_info {} - variable arg_error_CLR_error - array set arg_error_CLR_error {} - - proc _argerror_load_colours {{forcereload 0}} { - variable arg_error_CLR - #todo - option for reload/retry? - if {!$forcereload && [array size arg_error_CLR] > 0} { - return - } - - if {[catch {package require punk::ansi} errMsg]} { - puts stderr "punk::args FAILED to load punk::ansi\n$errMsg" - proc ::punk::args::a {args} {} - proc ::punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #array set arg_error_CLR {} - set arg_error_CLR(testsinglecolour) [a+ yellow] ;#A single SGR colour to test current colour on|off state (empty string vs some result - used to determine if forcereload required) - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(linebase) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] - variable arg_error_CLR_nocolour - #array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" - variable arg_error_CLR_info - #array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error - #array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] - } - - - #bas ic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::parse {} withdef {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - set arg_error_isrunning 0 - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - #set arg_error_CLR(testsinglecolour) [a+ brightred] - upvar ::punk::args::arg_error_CLR CLR - set forcereload 0 ;#no need for forcereload to be true for initial run - empty array will trigger initial load - if {[info exists CLR(testsinglecolour)]} { - set terminal_colour_is_on [expr {[string length [a+ yellow]]}] - set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] - if {$terminal_colour_is_on ^ $error_colour_is_on} { - #results differ - set forcereload 1 - } - } - _argerror_load_colours $forcereload - - if {[llength $args] %2 != 0} { - set arg_error_isrunning 0 - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - #----------------------- - #todo!! make changeable from config file - #JJJ 2025-07-16 - set returntype table ;#table as string - #set returntype string - #---------------------- - set as_error 1 ;#usual case is to raise an error - set scheme error - set form 0 - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme -form} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -parsedargs { - #todo? - dict for {set setinfo} $v { - switch -- $set { - received { - foreach {r rpos} $setinfo { - if {$r ni $goodargs} { - lappend goodargs $r - } - } - } - } - } - set parsedargs $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - -form { - set form $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - set formnames [dict get $spec_dict form_names] - if {[string is integer -strict $form]} { - if {$form < 0 || $form > [llength $formnames]-1} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - set selected_forms [list [lindex $formnames $form]] - } else { - if {$form eq "*"} { - set selected_forms $formnames - } else { - if {$form in $formnames} { - set selected_forms [list $form] - } else { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - } - } - - - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - - switch -- $scheme { - nocolour { - variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour] - } - info { - variable arg_error_CLR_info - array set CLR [array get arg_error_CLR_info] - } - error { - variable arg_error_CLR_error - array set CLR [array get arg_error_CLR_error] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[0m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdsummary [Dict_getdef $spec_dict cmd_info -summary ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - #set example [Dict_getdef $spec_dict examples_info -help ""] - set has_example [dict exists $spec_dict examples_info -help] - - #review - when can there be more than one selected form? - set argdisplay_header "" - set argdisplay_body "" - if {[llength $selected_forms] == 1} { - set fid [lindex $selected_forms 0] - set FRM [dict get $spec_dict FORMS $fid] - if {[dict size [dict get $FRM FORMDISPLAY]]} { - set argdisplay_header [Dict_getdef $FRM FORMDISPLAY -header ""] - set argdisplay_body [Dict_getdef $FRM FORMDISPLAY -body ""] - } - } - - - # if {![dict size $F $fid $FORMDISPLAY]} {} - #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - #set is_custom_argdisplay 0 - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - #set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - set cmdhelp_display [punk::ansi::ansiwrap_raw $CLR(linebase_header) "" "" $cmdhelp] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - if {$has_example} { - lappend blank_header_col "" - set example_display "[a+ white]eg [dict get $spec_dict id]$RST" - } else { - set example_display "" - } - #synopsis - set synopsis "# [Dict_getdef $spec_dict cmd_info -summary {}]\n" - set form_info [dict get $spec_dict form_info] - dict for {fid finfo} $form_info { - set form_synopsis [Dict_getdef $finfo -synopsis ""] - if {$form_synopsis eq ""} { - #todo - set form_synopsis [punk::args::synopsis -noheader -form $fid [dict get $spec_dict id]] - set ansifree_synopsis [punk::ansi::ansistripraw $form_synopsis] - if {[string length $ansifree_synopsis] > 90} { - # - set form_synopsis [punk::args::synopsis -noheader -return summary -form $fid [dict get $spec_dict id]] - } - #review - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - } - if {$fid in $selected_forms} { - set form_synopsis [punk::ansi::a+ underline]$form_synopsis[punk::ansi::a+ nounderline] - } - append synopsis $form_synopsis \n - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$has_example} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Example: $example_display] - } else { - lappend errlines "Example: $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - #set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space (zwsp) - set A_PREFIXEND [a+ nounderline] - #review - zwsp problematic on older terminals that print it visibly - #- especially if they also lie about cursor position after it's emitted. - #so although the zwsp fixes the issue where the underline extends to rhs padding if all text was underlined, - #It's probably best fixed in the padding functionality. - } else { - set A_PREFIXEND $RST - } - - #TODO - foreach fid - set fid [lindex $selected_forms 0] - set form_dict [dict get $spec_dict FORMS $fid] - - set opt_names [list] - set opt_names_display [list] - set opt_names_hints [list] ;#comments in first column below name display. - set lookup_optset [dict create] - if {[llength [dict get $form_dict OPT_NAMES]]} { - set all_opts [list] - foreach optionset [dict get $form_dict OPT_NAMES] { - #e.g1 "-alias1|-realname" - #e.g2 "-f|--filename" (fossil longopt style) - #e.g3 "-f|--filename=" (gnu longopt style) - set optmembers [split $optionset |] - lappend all_opts {*}$optmembers - foreach o $optmembers { - dict set lookup_optset $o $optionset - #goodargs - } - } - set full_goodargs [list] - #goodargs may have simplified entries for received opts of form -alias1|-alias2|-realname - #map -realname to full argname - foreach g $goodargs { - if {[string match -* $g] && [dict exists $lookup_optset $g]} { - lappend full_goodargs [dict get $lookup_optset $g] - } else { - lappend full_goodargs $g - } - } - set goodargs $full_goodargs - if {![catch {package require punk::trie}]} { - #todo - reservelist for future options - or just to affect the prefix calculation - # (similar to -choiceprefixreservelist) - - set trie [punk::trie::trieclass new {*}$all_opts --] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach optset [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $optset] - set parsekey [dict get $arginfo -parsekey] - set storageinfo "" - if {$parsekey ne "" && $parsekey ne $optset} { - set storageinfo "(stored as: $parsekey)" - } - if {[dict get $arginfo -prefix]} { - set opt_members [split $optset |] - set odisplay [list] - foreach opt $opt_members { - set id [dict get $idents $opt] - #REVIEW - if {$id eq $opt} { - set prefix $opt - set tail "" - } else { - set idlen [string length $id] - lassign [punk::lib::string_splitbefore $opt $idlen] prefix tail - } - lappend odisplay $A_PREFIX$prefix$A_PREFIXEND$tail - } - #lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - lappend opt_names_display [join $odisplay |] - } else { - lappend opt_names_display $optset - } - lappend opt_names_hints $storageinfo - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $optset - } - } else { - set opt_names [dict get $form_dict OPT_NAMES] - foreach optset [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $optset] - set parsekey [dict get $arginfo -parsekey] - set storageinfo "" - if {$parsekey ne "" && $parsekey ne $optset} { - set storageinfo "(stored as: $parsekey)" - } - lappend opt_names_display $optset - lappend opt_names_hints $storageinfo - } - #set opt_names_display $opt_names - } - } - set leading_val_names [dict get $form_dict LEADER_NAMES] - set trailing_val_names [dict get $form_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $form_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set leading_val_names_hints {} - set trailing_val_names_display $trailing_val_names - set trailing_val_names_hints {} - #puts "--> parsedargs: $parsedargs" - set parsed_leaders [Dict_getdef $parsedargs leaders {}] - set parsed_opts [Dict_getdef $parsedargs opts {}] - set parsed_values [Dict_getdef $parsedargs values {}] - - #display options first then values - foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names $parsed_leaders] [list opts $opt_names_display $opt_names_hints $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames parsedvalues - set lastgroup "" - set lastgroup_parsekey "" - foreach argshow $argnames_display hint $argnames_hints arg $argnames { - set arginfo [dict get $form_dict ARG_INFO $arg] - - if {$argumentclass eq "opts"} { - set thisgroup [dict get $arginfo -group] - if {$thisgroup ne $lastgroup} { - if {[dict exists $form_dict OPT_GROUPS $thisgroup -parsekey]} { - set thisgroup_parsekey [dict get $form_dict OPT_GROUPS $thisgroup -parsekey] - } else { - set thisgroup_parsekey "" - } - - #footer/line? - if {$use_table} { - $t add_row [list " " "" "" "" ""] - } else { - lappend errlines " " - } - - if {$thisgroup eq ""} { - } else { - #SHOW group 'header' (not really a table header - just another row) - set help "" - if {[dict exists $form_dict OPT_GROUPS $thisgroup -help]} { - set help [dict get $form_dict OPT_GROUPS $thisgroup -help] - } - if {$thisgroup_parsekey eq ""} { - set groupinfo "(documentation group)" - } else { - set groupinfo "(common flag group)\nkey:$thisgroup_parsekey" - } - if {$use_table} { - $t add_row [list " $thisgroup" $groupinfo "" "" $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" - set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] - append arghelp \n - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - set lastgroup $thisgroup - set lastgroup_parsekey $thisgroup_parsekey - } - if {[dict exists $arginfo -parsekey]} { - set mypkey [dict get $arginfo -parsekey] - if {$mypkey eq "$lastgroup_parsekey" || $mypkey eq [string trimright [lindex [split $arg |] end] =]} { - set hint "" - } - } - } - - if {[dict exists $arginfo -default]} { - #default isn't necessarily of same type as -type required for validation - #Guessing at the type from the data is likely to be unsatisfactory. - - set defaultdisplaytype [Dict_getdef $arginfo -defaultdisplaytype string] - switch -- $defaultdisplaytype { - dict { - #single level - set rawdefault [dict get $arginfo -default] - set default "{\n" - dict for {k v} $rawdefault { - append default " \"$k\" \"$v\"\n" - } - append default "}" - } - list { - set default "{\n" - foreach v $rawdefault { - append default " \"$v\"\n" - } - append default "}" - } - default { - #set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - set default "'[dict get $arginfo -default]'" - } - } - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - set choiceprefixreservelist [Dict_getdef $arginfo -choiceprefixreservelist {}] ;#names used to calc prefix - but not available as actual choice. - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - #review - does choiceprefixdenylist need to be added? - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_prefixcalc [list {*}[string tolower $allchoices_originalcase] {*}$choiceprefixreservelist] - } else { - set casemsg " (case sensitive)" - set allchoices_prefixcalc [list {*}$allchoices_originalcase {*}$choiceprefixreservelist] - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help "Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_prefixcalc] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - lassign [punk::lib::string_splitbefore $c [string length $shortestid]] prefix tail - #if {$shortestid eq $c} { - # set prefix $c - # set tail "" - #} else { - # set idlen [string length $shortestid] - # set prefix [string range $c 0 $idlen-1] - # set tail [string range $c $idlen end] - #} - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - append help \n[textblock::join -- " " [$obj print]] - #------------- - #todo - tests - #see special case double reset at end of content in textblock class table get_column_by_index - #bug fixed - needed to ensure last two resets were actually concurrent and at end. - #append help "\nbase[a+ green]ab\nc[a]base" ;#ok - #vs - #append help "\nbase[a+ green]a[a]b\nc[a]base" ;#not ok - #------------- - - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -typeranges]} { - set ranges [dict get $arginfo -typeranges] - if {[llength $ranges] == 1} { - append typeshow \n "-range [lindex [dict get $arginfo -typeranges] 0]" - } else { - append typeshow \n "-ranges" - foreach r $ranges { - append typeshow " {$r}" - } - } - } - - if {$use_table} { - if {$hint ne ""} { - set col1 $argshow\n$hint - } else { - set col1 $argshow - } - $t add_row [list $col1 $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set linetail " TYPE:$typeshow DEFAULT:$default MULTI:$multiple" - if {$hint ne ""} { - set arghelp [textblock::join -- "[a+ bold]$argshow\n$hint$RST" $linetail] - } else { - set arghelp "[a+ bold]$argshow$RST $linetail" - } - append arghelp \n - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - - # ------------------------------------------------------------------------------------------------------- - # if the argument class can accept unnamed arguments (or if opts accepts unspecified flags) - display an indication - # ------------------------------------------------------------------------------------------------------- - switch -- $argumentclass { - leaders - values { - if {$argumentclass eq "leaders"} { - set class_unnamed LEADER_UNNAMED - set class_max LEADER_MAX - set class_required LEADER_REQUIRED - set class_directive_defaults LEADERSPEC_DEFAULTS - } else { - set class_unnamed VAL_UNNAMED - set class_max VAL_MAX - set class_required VAL_REQUIRED - set class_directive_defaults VALSPEC_DEFAULTS - } - if {[dict get $form_dict $class_unnamed]} { - set valmax [dict get $form_dict $class_max] - #set valmin [dict get $form_dict VAL_MIN] - if {$valmax eq ""} { - set valmax -1 - } - if {$valmax == -1} { - set possible_unnamed -1 - } else { - set possible_unnamed [expr {$valmax - [llength [dict get $form_dict $class_required]]}] - if {$possible_unnamed < 0} { - set possible_unnamed 0 - } - } - if {$possible_unnamed == -1 || $possible_unnamed > 0} { - #Note 'multiple' is always empty here as each unnamed is assigned to its own positional index - if {$possible_unnamed == 1} { - set argshow ?? - } else { - set argshow ?...? - } - set tp [dict get $form_dict $class_directive_defaults -type] - if {[dict exists $form_dict $class_directive_defaults -default]} { - set default [dict get $form_dict $class_directive_defaults -default] - } else { - set default "" - } - if {$use_table} { - $t add_row [list "$argshow" $tp $default "" ""] - } else { - set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n" - lappend errlines $arghelp - } - } - } - } - opts { - #display row to indicate if -any|-arbitrary true - - #review OPTSPEC_DEFAULTS -multiple ? - if {[dict get $form_dict OPT_ANY]} { - set argshow "?...?" - set tp [dict get $form_dict OPTSPEC_DEFAULTS -type] - if {[dict exists $form_dict OPTSPEC_DEFAULTS -default]} { - set default [dict get $form_dict OPTSPEC_DEFAULTS -default] - } else { - set default "" - } - if {$use_table} { - $t add_row [list "$argshow" $tp $default "" ""] - } else { - set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n" - lappend errlines $arghelp - } - } - } - } - - } ;#end foreach argumentclass - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - catch {$t destroy} - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } else { - #put original error at bottom of table too - append result \n $msg - } - } else { - set result $errmsg - append result \n $msg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::cmdhelp (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - }\ - {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}}\ - {${[punk::args::resolved_def -types opts ::punk::args::resolved_def -form]}}\ - { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict $definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance - #todo - configurable per interp/namespace - -errorstyle -type string -default enhanced -choices {enhanced standard basic minimal} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - @values -max 2 - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - #puts "punk::args::parse --> '$args'" - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - #error "punk::args::parse - invalid call. < 3 args" - punk::args::parse $args withid ::punk::args::parse - } - set opts_and_vals $args - set parseargs [lpop opts_and_vals 0] - - set opts [list] - set values [list] - for {set i 0} {$i < [llength $opts_and_vals]} {incr i} { - if {[string match -* [lindex $opts_and_vals $i]]} { - if {[catch { - lappend opts [lpop opts_and_vals 0] [lpop opts_and_vals 0] - }]} { - #unhappy path - not enough options - #review - which form of punk::args::parse? - #we expect this to always raise error - review - set result [punk::args::parse $args withid ::punk::args::parse] - puts stderr "punk::args::parse unexpected result $result" - return ;#failsafe - } - incr i -1 - #lappend opts $a [lindex $opts_and_vals $i] - } else { - break - } - } - #set values [lrange $opts_and_vals $i end] - set values $opts_and_vals - #puts "---values: $values" - set tailtype [lindex $values 0] - set tailargs [lrange $values 1 end] - - - #set split [lsearch -exact $tailargs withid] - #if {$split < 0} { - # set split [lsearch -exact $tailargs withdef] - # if {$split < 0} { - # #punk::args::usage arg_error? - # #error "punk::args::parse - invalid call. keyword withid|withdef required" - # punk::args::parse $args withid ::punk::args::parse - # } else { - # set tailtype withdef - #} - #} else { - # set tailtype withid - #} - #set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - - - #if {[llength $opts] % 2} { - #error "punk::args::parse Even number of -flag val pairs required after arglist" - #} - - #Default the -errorstyle to standard - # (slow on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle standard\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - #puts "def: $defaultopts opts: $opts" - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - #error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - punk::args::parse $args withid ::punk::args::parse - } - } - } - switch -- $tailtype { - withid { - if {[llength $tailargs] != 1} { - #error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - punk::args::parse $args withid ::punk::args::parse - } - set id [lindex $tailargs 0] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - #puts stdout "punk::args::parse '$parseargs' withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist $tailargs - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - #puts stdout "punk::args::parse '$parseargs' with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist was '$tailtype'. Must be 'withid' or 'withdef'" - } - } - try { - #puts stdout "parse --> get_dict $parseargs -form [dict get $opts -form]" - set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - set ecode [dict get $erroropts -errorcode] - #punk ecode is of form PUNKARGS VALIDATION {description..} -key val ... - set msg [string map [list %caller% [Get_caller]] $msg] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - - #return number of values we can assign to cater for variable length clauses such as {"elseif" expr "?then?" body} - #review - efficiency? each time we call this - we are looking ahead at the same info - proc _get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { - set ARG_INFO [dict get $formdict ARG_INFO] - set all_remaining [lrange $values $idx end] - set thisname [lindex $names $nameidx] - set thistype [dict get $ARG_INFO $thisname -type] - set tailnames [lrange $names $nameidx+1 end] - - #todo - work backwards with any (optional or not) literals at tail that match our values - and remove from assignability. - set ridx 0 - foreach clausename [lreverse $tailnames] { - #puts "=============== clausename:$clausename all_remaining: $all_remaining" - set typelist [dict get $ARG_INFO $clausename -type] - if {[lsearch $typelist literal*] == -1} { - break - } - set max_clause_length [llength $typelist] - if {$max_clause_length == 1} { - #basic case - set alloc_ok 0 - #set v [lindex $values end-$ridx] - set v [lindex $all_remaining end] - set tp [lindex $typelist 0] - # ----------------- - set tp [string trim $tp ?] ;#shouldn't be necessary - #review - ignore ?literal? and ?literal(xxx)? when clause-length == 1? (should raise error during definition instead? - #we shouldn't have an optional clause member if there is only one member - the whole argument should be marked -optional true instead. - # ----------------- - - #todo - support complex type members such as -type {{literal a|b} int OR} - #for now - require llength 1 - simple type such as -type {literal(ab)|int} - if {[llength $tp] !=1} { - error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" - } - - #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { - switch -exact -- [lindex $tp_alternative 0] { - literal { - set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) - set match [lindex $tp_alternative 1] - if {$v eq $match} { - set alloc_ok 1 - ledit all_remaining end end - if {![dict get $ARG_INFO $clausename -multiple]} { - ledit tailnames end end - } - #the type (or one of the possible type alternates) matched a literal - break - } - } - stringstartswith { - set pfx [lindex $tp_alternative 1] - if {[string match "$pfx*" $v} { - set alloc_ok 1 - set alloc_ok 1 - ledit all_remaining end end - if {![dict get $ARG_INFO $clausename -multiple]} { - ledit tailnames end end - } - break - } - - } - stringendswith { - set sfx [lindex $tp_alternative 1] - if {[string match "*$sfx" $v} { - set alloc_ok 1 - set alloc_ok 1 - ledit all_remaining end end - if {![dict get $ARG_INFO $clausename -multiple]} { - ledit tailnames end end - } - break - } - - } - default {} - } - } - if {!$alloc_ok} { - if {![dict get $ARG_INFO $clausename -optional]} { - break - } - } - - } else { - #todo - use _split_type_expression - - - #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) - #This is better caught during definition. - #e.g rn = {elseif expr (?then?) body} typelist = {literal expr ?literal? script} - #set cvals [lrange $values end-$ridx end-[expr {$ridx + $max_clause_length-1}]] - set cvals [lrange $values end-[expr {$ridx + $max_clause_length-1}] end-$ridx] - set rcvals [lreverse $cvals] - set alloc_count 0 - #clause name may have more entries than types - extras at beginning are ignored - set rtypelist [lreverse $typelist] - set alloc_ok 0 - set reverse_type_index 0 - #todo handle type-alternates - # for example: -type {string literal(x)|literal(y)} - foreach tp $rtypelist { - #set rv [lindex $rcvals end-$alloc_count] - set rv [lindex $all_remaining end-$alloc_count] - if {[string match {\?*\?} $tp]} { - set clause_member_optional 1 - } else { - set clause_member_optional 0 - } - set tp [string trim $tp ?] - switch -glob -- $tp { - literal* { - set litinfo [string range $tp 7 end] - set match [string range $litinfo 1 end-1] - #todo -literalprefix - if {$rv eq $match} { - set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok - incr alloc_count - } else { - if {$clause_member_optional} { - # - } else { - set alloc_ok 0 - break - } - } - } - "stringstartswith(*" { - set pfx [string range $tp 17 end-1] - if {[string match "$pfx*" $tp]} { - set alloc_ok 1 - incr alloc_count - } else { - if {!$clause_member_optional} { - set alloc_ok 0 - break - } - } - } - default { - if {$clause_member_optional} { - #review - optional non-literal makes things harder.. - #we don't want to do full type checking here - but we now risk allocating an item that should actually - #be allocated to the previous value - # todo - lsearch to next literal or non-optional? - set prev_type [lindex $rtypelist $reverse_type_index+1] - if {[string match literal* $prev_type]} { - set litinfo [string range $prev_type 7 end] - #todo -literalprefix - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - set match [lindex $rclausename $reverse_type_index+1] - } - if {$rv ne $match} { - #current val doesn't match previous type - allocate here - incr alloc_count - } - } else { - #no literal to anchor against.. - incr alloc_count - } - } else { - #allocate regardless of type - we're only matching on arity and literal positioning here. - #leave final type-checking for later. - incr alloc_count - } - } - } - incr reverse_type_index - } - if {$alloc_ok && $alloc_count > 0} { - #set n [expr {$alloc_count -1}] - #set all_remaining [lrange $all_remaining end-$n end] - set all_remaining [lrange $all_remaining 0 end-$alloc_count] - #don't lpop if -multiple true - if {![dict get $ARG_INFO $clausename -multiple]} { - #lpop tailnames - ledit tailnames end end - } - } else { - break - } - } - incr ridx - } - set num_remaining [llength $all_remaining] - - if {[dict get $ARG_INFO $thisname -optional] || ([dict get $ARG_INFO $thisname -multiple] && $thisname in $namesreceived)} { - #todo - check -multiple for required min/max (not implemented: make -multiple accept ?) - #thisname already satisfied, or not required - set tail_needs 0 - foreach t $tailnames { - if {![dict get $ARG_INFO $t -optional]} { - set min_clause_length [llength [lsearch -all -not [dict get $ARG_INFO $t -type] {\?*\?}]] - incr tail_needs $min_clause_length - } - } - set all_remaining [lrange $all_remaining 0 end-$tail_needs] - } - - #thistype - set alloc_ok 1 ;#default assumption only - set alloc_count 0 - set resultlist [list] - set n [expr {[llength $thistype]-1}] - set tpidx 0 - set newtypelist $thistype - set has_choices [expr {[tcl::dict::exists $ARG_INFO $thisname -choices] || [tcl::dict::exists $ARG_INFO $thisname -choicegroups]}] - foreach tp $thistype { - #usual case is a single tp (basic length-1 clause) - but tp may commonly have alternates eg int|literal(xxx) - set v [lindex $all_remaining $alloc_count] - if {[string match {\?*\?} $tp]} { - set clause_member_optional 1 - } else { - set clause_member_optional 0 - } - set tp [string trim $tp ?] - - set member_satisfied 0 - if {$has_choices} { - #each tp in the clause is just for validating a value outside the choice-list when -choicerestricted 0 - set member_satisfied 1 - } - - - if {!$member_satisfied} { - #----------------------------------------------------------------------------------- - #first build category lists of any literal,literalprefix,stringstartwith,other - # - set ctg_literals [list] - set ctg_literalprefixes [list] - set ctg_stringstartswith [list] - set ctg_stringendswith [list] - set ctg_other [list] - #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { - #JJJJ - lassign $tp_alternative t textra - switch -exact -- $t { - literal { - lappend ctg_literals $textra - } - literalprefix { - lappend ctg_literalprefixes $textra - } - stringstartswith { - lappend ctg_stringstartswith $textra - } - stringendswith { - lappend ctg_stringendswith $textra - } - default { - lappend ctg_other $tp_alternative - } - } - } - #----------------------------------------------------------------------------------- - if {[llength $ctg_other] > 0} { - #presence of any ordinary type as one of the alternates - means we consider it a match if certain basic types align - #we don't do full validation here -leave main validation for later (review) - foreach tp_alternative $ctg_other { - switch -exact -- $tp_alternative { - int { - if {[string is integer -strict $v]} { - set member_satisfied 1 - break - } - } - double { - if {[string is double -strict $v]} { - set member_satisfied 1 - break - } - } - bool { - if {[string is boolean -strict $v]} { - set member_satisfied 1 - break - } - } - number { - if {[string is integer -strict $v] || [string is double -strict $v]} { - set member_satisfied 1 - break - } - } - dict { - if {[punk::args::lib::string_is_dict $v]} { - set member_satisfied 1 - break - } - } - default { - #REVIEW!!! - #can get infinite loop in get_dict if not satisfied - unstoppable until memory exhausted. - #todo - catch/detect in caller - set member_satisfied 1 - break - } - } - } - } - } - - if {!$member_satisfied && ([llength $ctg_literals] || [llength $ctg_literalprefixes])} { - if {$v in $ctg_literals} { - set member_satisfied 1 - lset newtypelist $tpidx validated-$tp - } else { - #ctg_literals is included in the prefix-calc - but a shortened version of an entry in literals is not allowed - #(exact match would have been caught in other branch of this if) - #review - how does ctg_stringstartswith affect prefix calc for literals? - set full_v [tcl::prefix::match -error "" [list {*}$ctg_literals {*}$ctg_literalprefixes] $v] - if {$full_v ne "" && $full_v ni $ctg_literals} { - #matched prefix must be for one of the entries in ctg_literalprefixes - valid - set member_satisfied 1 - set v $full_v ;#map prefix given as arg to the full literalprefix value - lset newtypelist $tpidx validated-$tp - } - } - } - if {!$member_satisfied && [llength $ctg_stringstartswith]} { - foreach pfx $ctg_stringstartswith { - if {[string match "$pfx*" $v]} { - set member_satisfied 1 - lset newtypelist $tpidx validated-$tp - #review. consider multi-word typespec with RPN? - # {*}$tp_alternative validated - break - } - } - } - if {!$member_satisfied && [llength $ctg_stringendswith]} { - foreach pfx $ctg_stringendswith { - if {[string match "*$pfx" $v]} { - set member_satisfied 1 - lset newtypelist $tpidx validated-$tp - break - } - } - } - - - - if {$member_satisfied} { - if {$clause_member_optional && $alloc_count >= [llength $all_remaining]} { - if {[dict exists $ARG_INFO $thisname -typedefaults]} { - set d [lindex [dict get $ARG_INFO $thisname -typedefaults] $tpidx] - lappend resultlist $d - lset newtypelist $tpidx ?defaulted-$tp? - } else { - lset newtypelist $tpidx ?omitted-$tp? - lappend resultlist "" - } - } else { - #may have satisfied one of the basic type tests above - lappend resultlist $v - incr alloc_count - } - } else { - if {$clause_member_optional} { - if {[dict exists $ARG_INFO $thisname -typedefaults]} { - set d [lindex [dict get $ARG_INFO $thisname -typedefaults] $tpidx] - lappend resultlist $d - lset newtypelist $tpidx ?defaulted-$tp? - } else { - lappend resultlist "" - lset newtypelist $tpidx ?omitted-$tp? - } - } else { - set alloc_ok 0 - } - } - - if {$alloc_count > [llength $all_remaining]} { - set alloc_ok 0 - break - } - incr tpidx - } - - #?omitted-*? and ?defaulted-*? in typelist are a way to know which elements in the clause were missing/defaulted - #so that they are not subject to type validation - #such elements shouldn't be subject to validation - if {$alloc_ok} { - #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" - set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] - } else { - puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" - set d [dict create consumed 0 resultlist {} typelist $thistype] - } - #puts ">>>> _get_dict_can_assign_value $d" - return $d - } - - #_split_type_expression - #only handles toplevel 'or' for type_expression e.g int|char - #we have no mechanism for & - (although it would be useful) - #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) - #or perhaps more performant, RPN to avoid bracket parsing - #if literal(..), literalprefix(..), stringstartswith(..) etc can have pipe symbols and brackets etc - we can't just use split - #if we require -type to always be treated as a list - and if an element is length 1 - require it to - #have properly balanced brackets that don't contain | ( ) etc we can simplify - REVIEW - - #consider: - #1 basic syntax - only OR supported - limits on what chars can be put in 'textn' elements. - #mode -type literalprefix(text1)|literalprefix(text2) -optional 1 - #2 expanded syntax - supports arbitrary chars in 'textn' elements - but still doesn't support more complex OR/AND logic - #mode -type {{literalprefix text1 | literalprefix text2}} - #3 RPN (reverse polish notation) - somewhat unintuitive, but allows arbitrary textn, and complex OR/AND logic without brackets. - #(forth like - stack based definition of types) - #mode -type {literalprefix text1 literalprefix text2 OR} - #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - - proc _split_type_expression {type_expression} { - if {[llength $type_expression] == 1} { - #simple expressions of length one must be splittable on | - #disallowed: things such as literal(|) or literal(x|etc)|int - #these would have to be expressed as {literal |} and {literal x|etc | int} - set or_type_parts [split $type_expression |] - set type_alternatives [list] - foreach t $or_type_parts { - if {[regexp {([^\(^\)]*)\((.*)\)$} $t _ name val]} { - lappend type_alternatives [list $name $val] - } else { - lappend type_alternatives $t - } - } - return $type_alternatives - } else { - error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" - #todo - #RPN reverse polish notation - #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - #equivalent logic: ((stringstartswith(x)|stringstartswith(y))&stringendswith(z))|int - # {int ; stringstartswith x stringstartswith y OR } - - #experimental.. seems like a pointless syntax. - #may as well just use list of lists with |(or) as the intrinsic operator instead of parsing this - #e.g {stringstartswith x | literal | | int} - set type_alternatives [list] - set expect_separator 0 - for {set w 0} {$w < [llength $type_expression]} {incr w} { - set word [lindex $type_expression $w] - if {$expect_separator} { - if {$word eq "|"} { - #pipe could be last entry - not strictly correct, but can ignore - set expect_separator 0 - continue - } else { - error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" - } - } - switch -exact -- $word { - literal - literalprefix - stringstartswith - stringendswith - stringcontains { - if {$w+1 > [llength $type_expression]} { - #premature end - no arg available for type which requires one - error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" - } - lappend type_alternatives [list $word [lindex $type_expression $w+1]] - incr w ;#consume arg - set expect_separator 1 - } - default { - #simple types such as int,double,string - lappend type_alternatives $word - set expect_separator 1 - } - } - } - return $type_alternatives - } - } - - #old version - ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { - ### #set type $type_expression ;#todo - 'split' on | - ### set vlist $clausevalues_raw - ### set vlist_check $clausevalues_check - - ### set type_alternatives [_split_type_expression $type_expression] - ### #each type_alternative is a list of varying length depending on arguments supported by first word. - ### #TODO? - ### #single element types: int double string etc - ### #two element types literal literalprefix stringstartswith stringendswith - ### #TODO - ### set stype [lindex $type_alternatives 0] - ### #e.g int - ### #e.g {literal blah)etc} - ### set type [lindex $stype 0] - ### #switch on first word of each stype - ### # - - ### #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? - ### switch -- $type { - ### any {} - ### literal { - ### foreach clauseval $vlist { - ### set e [lindex $clauseval $clausecolumn] - ### set testval [lindex $stype 1] - ### if {$e ne $testval} { - ### set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### stringstartwith { - ### foreach clauseval $vlist { - ### set e [lindex $clauseval $clausecolumn] - ### set testval [lindex $stype 1] - ### if {![string match $testval* $e]} { - ### set msg "$argclass '$argname' for %caller% requires stringstartswith value '$argname'. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### list { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is list -strict $e_check]} { - ### set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - ### } - ### if {[tcl::dict::size $thisarg_checks]} { - ### tcl::dict::for {checkopt checkval} $thisarg_checks { - ### switch -- $checkopt { - ### -minsize { - ### # -1 for disable is as good as zero - ### if {[llength $e_check] < $checkval} { - ### set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### -maxsize { - ### if {$checkval ne "-1"} { - ### if {[llength $e_check] > $checkval} { - ### set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### } - ### } - ### indexexpression { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[catch {lindex {} $e_check}]} { - ### set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### string - ansistring - globstring { - ### #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - ### #we possibly don't want to always have to regex on things that don't pass the other more basic checks - ### # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - ### # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - ### # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - ### # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - ### # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - ### # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - ### #todo? - way to validate both unstripped and stripped? - ### set pass_quick_list_e [list] - ### set pass_quick_list_e_check [list] - ### set remaining_e $vlist - ### set remaining_e_check $vlist_check - ### #review - order of -regexprepass and -regexprefail in original rawargs significant? - ### #for now -regexprepass always takes precedence - ### set regexprepass [tcl::dict::get $thisarg -regexprepass] - ### set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - ### if {$regexprepass ne ""} { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[regexp [lindex $regexprepass $clausecolumn] $e]} { - ### lappend pass_quick_list_e $clauseval - ### lappend pass_quick_list_e_check $clauseval_check - ### } - ### } - ### set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - ### set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - ### } - ### if {$regexprefail ne ""} { - ### foreach clauseval $remaining_e clauseval_check $remaining_e_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### #puts "----> checking $e vs regex $regexprefail" - ### if {[regexp $regexprefail $e]} { - ### if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - ### #review - %caller% ?? - ### set msg [tcl::dict::get $thisarg -regexprefailmsg] - ### } else { - ### set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - ### } - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### switch -- $type { - ### ansistring { - ### #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - ### #.. so we need to look at the original values in $vlist not $vlist_check - - ### #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - ### #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - ### package require punk::ansi - ### foreach clauseval $remaining_e { - ### set e [lindex $clauseval $clausecolumn] - ### if {![punk::ansi::ta::detect $e]} { - ### set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### globstring { - ### foreach clauseval $remaining_e { - ### set e [lindex $clauseval $clausecolumn] - ### if {![regexp {[*?\[\]]} $e]} { - ### set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - - ### if {[tcl::dict::size $thisarg_checks]} { - ### foreach clauseval $remaining_e_check { - ### set e_check [lindex $clauseval $clausecolumn] - ### if {[dict exists $thisarg_checks -minsize]} { - ### set minsize [dict get $thisarg_checks -minsize] - ### # -1 for disable is as good as zero - ### if {[tcl::string::length $e_check] < $minsize} { - ### set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[dict exists $thisarg_checks -maxsize]} { - ### set maxsize [dict get $thisarg_checks -maxsize] - ### if {$checkval ne "-1"} { - ### if {[tcl::string::length $e_check] > $maxsize} { - ### set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### number { - ### #review - consider effects of Nan and Inf - ### #NaN can be considered as 'technically' a number (or at least a special numeric value) - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::exists $thisarg -typeranges]} { - ### set ranges [tcl::dict::get $thisarg -typeranges] - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set range [lindex $ranges $clausecolumn] - ### lassign {} low high ;#set both empty - ### lassign $range low high - - ### if {"$low$high" ne ""} { - ### if {[::tcl::mathfunc::isnan $e]} { - ### set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### if {$low eq ""} { - ### if {$e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } elseif {$high eq ""} { - ### if {$e_check < $low} { - ### set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } else { - ### if {$e_check < $low || $e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### int { - ### #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is integer -strict $e_check]} { - ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::exists $thisarg -typeranges]} { - ### set ranges [tcl::dict::get $thisarg -typeranges] - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set range [lindex $ranges $clausecolumn] - ### lassign $range low high - ### if {"$low$high" ne ""} { - ### if {$low eq ""} { - ### #lowside unspecified - check only high - ### if {$e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } elseif {$high eq ""} { - ### #highside unspecified - check only low - ### if {$e_check < $low} { - ### set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } else { - ### #high and low specified - ### if {$e_check < $low || $e_check > $high} { - ### set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### double { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is double -strict $e_check]} { - ### set e [lindex $clauseval $clausecolumn] - ### set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::size $thisarg_checks]} { - ### if {[dict exists $thisarg_checks -typeranges]} { - ### set ranges [dict get $thisarg_checks -typeranges] - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set range [lindex $ranges $clausecolumn] - ### #todo - small-value double comparisons with error-margin? review - ### #todo - empty string for low or high - ### lassign $range low high - ### if {$e_check < $low || $e_check > $high} { - ### set e [lindex $clauseval $clausecolumn] - ### set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### bool { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is boolean -strict $e_check]} { - ### set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### dict { - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[llength $e_check] %2 != 0} { - ### set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### if {[tcl::dict::size $thisarg_checks]} { - ### if {[dict exists $thisarg_checks -minsize]} { - ### set minsizes [dict get $thisarg_checks -minsize] - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set minsize [lindex $minsizes $clausecolumn] - ### # -1 for disable is as good as zero - ### if {[tcl::dict::size $e_check] < $minsize} { - ### set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### if {[dict exists $thisarg_checks -maxsize]} { - ### set maxsizes [dict get $thisarg_checks -maxsize] - ### foreach clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### set maxsize [lindex $maxsizes $clausecolumn] - ### if {$maxsize ne "-1"} { - ### if {[tcl::dict::size $e_check] > $maxsize} { - ### set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### } - ### } - ### alnum - - ### alpha - - ### ascii - - ### control - - ### digit - - ### graph - - ### lower - - ### print - - ### punct - - ### space - - ### upper - - ### wordchar - - ### xdigit { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {![tcl::string::is $type -strict $e_check]} { - ### set e [lindex $clauseval $t] - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### file - - ### directory - - ### existingfile - - ### existingdirectory { - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e [lindex $clauseval $clausecolumn] - ### set e_check [lindex $clauseval_check $clausecolumn] - ### #//review - we may need '?' char on windows - ### if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - ### #what about special file names e.g on windows NUL ? - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - ### } - ### if {$type eq "existingfile"} { - ### if {![file exists $e_check]} { - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - ### } - ### } elseif {$type eq "existingdirectory"} { - ### if {![file isdirectory $e_check]} { - ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### } - ### char { - ### #review - char vs unicode codepoint vs grapheme? - ### foreach clauseval $vlist clauseval_check $vlist_check { - ### set e_check [lindex $clauseval_check $clausecolumn] - ### if {[tcl::string::length $e_check] != 1} { - ### set e [lindex $clauseval $clausecolumn] - ### set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - ### } - ### } - ### } - ### default { - ### } - ### } - - ###} - - #new version - #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname - #common basic case: list of single item being a single value clause. - #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} - proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { - #default_type_expression is for the chosen clausecolumn - #if {$argname eq "frametype"} { - #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" - #puts "--->list_of_clauses_raw : $list_of_clauses_raw" - #puts "--->list_of_clauses_check: $list_of_clauses_check" - #puts "--->$argname -type: [dict get $thisarg -type]" - #} - - set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn - - set default_type_alternatives [_split_type_expression $default_type_expression] - #--------------------- - #pre-calc prefix sets based on the default. - set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] - set literals [lmap v $alt_literals {lindex $v 1}] - set alt_literalprefixes [lsearch -all -inline -index 0 $default_type_alternatives literalprefix] - set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] - #--------------------- - - #each type_alternative is a list of varying length depending on arguments supported by first word. - #TODO? - #single element types: int double string etc - #two element types literal literalprefix stringstartswith stringendswith - #TODO - - #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) - set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $default_type_alternatives] _]] - #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 - #-types {int|char|literal(ok) char double} - #we are checking a and 1 against the defaulttype_expression e.g int|char|literal(ok) (type_alternatives = {int char literal(ok)} - #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} - #review: for a particular clause the active type_expression might be overridden with 'any' if the column has already passed a -choices test - # - - set e_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_raw *] - set check_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] - set typelist_vals_raw [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_types *] - set typelist_vals [lmap v $typelist_vals_raw {string trim $v ?}] - - set c_idx -1 - foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { - incr c_idx - set col_type_alternatives [_split_type_expression $clause_column_type_expression] - set firstany [lsearch -exact $col_type_alternatives any] - if {$firstany > -1} { - lset clause_results $c_idx $firstany 1 - continue - } - set a_idx -1 - foreach typealt $col_type_alternatives { - incr a_idx - lassign $typealt type testval ;#testval will be empty for basic types, but applies to literal, literalprefix, stringstartswith etc. - switch -exact -- $type { - literal { - if {$e ne $testval} { - set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } else { - lset clause_results $c_idx $a_idx 1 - #this clause is satisfied - no need to process it for other typealt - break - } - } - literalprefix { - #this specific literalprefix testval value not relevant - we're testing against all in the set of typealternates - set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] - if {$match ne "" && $match ni $literals} { - lset clause_results $c_idx $a_idx 1 - #this clause is satisfied - no need to process it for other typealt - break - } else { - set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } - } - stringstartswith { - if {[string match $testval* $e]} { - lset clause_results $c_idx $a_idx 1 - break - } else { - set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } - } - stringendswith { - if {[string match *$testval $e]} { - lset clause_results $c_idx $a_idx 1 - break - } else { - set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - } - } - list { - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] - continue - } else { - if {[dict exists $thisarg_checks -minsize]} { - # -1 for disable is as good as zero - set minsize [dict get $thisarg_checks -minsize] - if {[llength $e_check] < $minsize} { - set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - continue - } - } - if {[dict exist $thisarg_checks -maxsize]} { - set maxsize [dict get $thisarg_checks -maxsize] - if {$maxsize ne "-1"} { - if {[llength $e_check] > $maxsize} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - indexexpression { - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - indexset { - if {![punk::lib::is_indexset $e_check]} { - set msg "$argclass $argname for %caller% requires type indexset. A comma-delimited set of indexes or index-ranges separated by '..' Received: '$e_check'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - packageversion { - if {[catch {::package vsatisfies $e_check $e_check}]} { - set msg "$argclass $argname for %caller% requires type packageversion. A package version number as understood by 'package vsatifies'. Received: '$e_check'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - packagerequirement { - set parts [split $e_check -] - if {[llength $parts] > 2} { - set msg "$argclass $argname for %caller% requires type packagerequirement. (form min min- or min-max) Received: '$e_check'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - set vchecklist [list] - if {[llength $parts] == 1} { - lappend vchecklist [lindex $parts 0] - } else { - lassign $parts vmin vmax - if {$vmax eq ""} { - #empty vmax allowed - ignore - lappend vchecklist $vmin - } else { - lappend vchecklist $vmin $vmax - } - } - #we have either just the min, or min and max - set v_ok 1 ;#default assumption - foreach vcheck $vchecklist { - if {[catch {::package vsatisfies $vcheck $vcheck}]} { - set msg "$argclass $argname for %caller% requires type packagerequirement. (from min min- or min-max) . Received: '$e_check'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - set v_ok 0 - break ;#inner loop - } - } - if {$v_ok} { - lset clause_results $c_idx $a_idx 1 - break - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - if {$regexprepass ne ""} { - if {[regexp [lindex $regexprepass $clausecolumn] $e]} { - lset clause_results $c_idx $a_idx 1 - break - } - } - if {$regexprefail ne ""} { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] - #review - tests? - continue - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - globstring { - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -minsize]} { - set minsize [dict get $thisarg_checks -minsize] - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - set range [lindex $ranges $clausecolumn] - lassign {} low high ;#set both empty - lassign $range low high - if {"$low$high" ne ""} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - int { - #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - set range [lindex $ranges $clausecolumn] - lassign $range low high - if {"$low$high" ne ""} { - if {$low eq ""} { - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$high eq ""} { - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } else { - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - double { - if {![tcl::string::is double -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - if {[tcl::dict::exists $thisarg_checks -typeranges]} { - set ranges [dict get $thisarg_checks -typeranges] - set range [lindex $ranges $clausecolumn] - #todo - small-value double comparisons with error-margin? review - lassign $range low high - if {$low$high ne ""} { - if {$low eq ""} { - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass $argname for %caller% must be double less than or equal to $high. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$high eq ""} { - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass $argname for %caller% must be double greater than or equal to $low. Received: '$e'" - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } elseif {$e_check < $low || $e_check > $high} { - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - bool { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - continue - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - dict { - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -minsize]} { - set minsizes [dict get $thisarg_checks -minsize] - set minsize [lindex $minsizes $clausecolumn] - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - lset clause_results $c_idx $a_idx [list err [list sizeviolation $type minsize $minsize] msg $msg] - continue - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsize [lindex $maxsizes $clausecolumn] - if {$maxsize ne "-1"} { - if {[tcl::dict::size $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - lset clause_results $c_idx $a_idx [list err [list sizeviolation $type maxsize $maxsize] msg $msg] - continue - } - } - } - } - lset clause_results $c_idx $a_idx 1 - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - if {![tcl::string::is $type -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - file - - directory - - existingfile - - existingdirectory { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - if {$type eq "existingfile"} { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } elseif {$type eq "existingdirectory"} { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } - lset clause_results $c_idx $a_idx 1 - } - char { - #review - char vs unicode codepoint vs grapheme? - if {[tcl::string::length $e_check] != 1} { - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } else { - lset clause_results $c_idx $a_idx 1 - break - } - } - tk_screen_units { - switch -exact -- [string index $e_check end] { - c - i - m - p { - set numpart [string range $e_check 0 end-1] - if {![tcl::string::is double $numpart]} { - set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } - default { - if {![tcl::string::is double $e_check]} { - set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] - continue - } - } - } - lset clause_results $c_idx $a_idx 1 - break - } - default { - #default pass for unrecognised types - review. - lset clause_results $c_idx $a_idx 1 - break - } - } - } - } - - foreach clauseresult $clause_results { - if {[lsearch $clauseresult 1] == -1} { - #no pass for this clause - fetch first? error and raise - #todo - return error containing clause_indices so we can report more than one failing element at once? - foreach e $clauseresult { - switch -exact -- [lindex $e 0] { - errorcode { - #errorcode msg checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against type_expression $type_expression" - # puts "--->list_of_clauses_raw : $list_of_clauses_raw" - # puts "--->list_of_clauses_check: $list_of_clauses_check" - # puts "--->$argname -type: [dict get $thisarg -type]" - # } - - # set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just passed type_expression - - # #set vlist [list] - # set clauses_dict [dict create] ;#key is ordinal position, remove entries as they are satsified - # set cidx -1 - # foreach cv $list_of_clauses_raw { - # incr cidx - # #REVIEW - # #if {$clause_size ==1} { - # # lappend vlist [list $cidx [list $cv]] - # #} else { - # #lappend vlist [list $cidx $cv] ;#store the index so we can reduce vlist as we go - # dict set clauses_dict $cidx $cv - # #} - # } - # #set vlist_check [list] - # set clauses_dict_check [dict create] - # set cidx -1 - # foreach cv $list_of_clauses_check { - # incr cidx - # #if {$clause_size == 1} { - # # lappend vlist_check [list $cidx [list $cv]] - # #} else { - # #lappend vlist_check [list $cidx $cv] - # dict set clauses_dict_check $cidx $cv - # #} - # } - - # set type_alternatives [_split_type_expression $type_expression] - # #each type_alternative is a list of varying length depending on arguments supported by first word. - # #TODO? - # #single element types: int double string etc - # #two element types literal literalprefix stringstartswith stringendswith - # #TODO - - # #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) - # set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $type_alternatives] _]] - # #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 - # #-types {int|char|literal(ok) char double} - # #we are checking a and 1 against the type_expression int|char|literal(ok) (type_alternatives = {int char literal(ok)} - # #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} - # # - - - # set a_idx -1 - # foreach typealt $type_alternatives { - # incr a_idx - - # set type [lindex $typealt 0] - # #e.g int - # #e.g {literal blah} - # #e.g {literalprefix abc} - - # #switch on first word of each typealt - # # - - # #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? - # switch -- $type { - # any {} - # literal { - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # set testval [lindex $typealt 1] - # if {$e ne $testval} { - # set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # #this clause is satisfied - no need to process it for other typealt - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # literalprefix { - # set alt_literals [lsearch -all -inline -index 0 $type_alternatives literal] - # set literals [lmap v $alt_literals {lindex $v 1}] - # set alt_literalprefixes [lsearch -all -inline -index 0 $type_alternatives literalprefix] - # set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] - - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # #this specific literalprefix value not relevant - we're testing against all in the set of typealternates - # #set testval [lindex $typealt 1] - # set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] - # if {$match ne "" && $match ni $literals} { - # lset clause_results $c_idx $a_idx 1 - # #this clause is satisfied - no need to process it for other typealt - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } else { - # set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } - # } - # } - # stringstartswith { - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # set testval [lindex $typealt 1] - # if {[string match $testval* $e]} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } else { - # set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } - # } - # } - # stringendswith { - # dict for {c_idx clauseval} $clauses_dict { - # set e [lindex $clauseval $clausecolumn] - # set testval [lindex $typealt 1] - # if {[string match *$testval $e]} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } else { - # set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] - # } - # } - # } - # list { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # set passed_checks 1 - # if {![tcl::string::is list -strict $e_check]} { - # set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } else { - # if {[dict exists $thisarg_checks -minsize]} { - # # -1 for disable is as good as zero - # set minsize [dict get $thisarg_checks -minsize] - # if {[llength $e_check] < $minsize} { - # set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks && [dict exist $thisarg_checks -maxsize]} { - # set maxsize [dict get $thisarg_checks -maxsize] - # if {$maxsize ne "-1"} { - # if {[llength $e_check] > $maxsize} { - # set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # indexexpression { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[catch {lindex {} $e_check}]} { - # set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # string - ansistring - globstring { - # #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - # #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - # #todo? - way to validate both unstripped and stripped? - # #review - order of -regexprepass and -regexprefail in original rawargs significant? - # #for now -regexprepass always takes precedence - # #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? - # set regexprepass [tcl::dict::get $thisarg -regexprepass] - # set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - # if {$regexprepass ne ""} { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[regexp [lindex $regexprepass $clausecolumn] $e]} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # if {$regexprefail ne ""} { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # #puts "----> checking $e vs regex $regexprefail" - # if {[regexp $regexprefail $e]} { - # if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - # #review - %caller% ?? - # set msg [tcl::dict::get $thisarg -regexprefailmsg] - # } else { - # set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - # } - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] - # #review - tests? - # } - # } - # } - # switch -- $type { - # ansistring { - # #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - # #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check - - # #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - # #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - # package require punk::ansi - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # if {![punk::ansi::ta::detect $e]} { - # set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # } - # globstring { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # if {![regexp {[*?\[\]]} $e]} { - # set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # } - # } - - # dict for {c_idx clauseval_check} $clauses_dict_check { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # if {[tcl::dict::size $thisarg_checks]} { - # set passed_checks 1 - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[dict exists $thisarg_checks -minsize]} { - # set minsize [dict get $thisarg_checks -minsize] - # # -1 for disable is as good as zero - # if {[tcl::string::length $e_check] < $minsize} { - # set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { - # set maxsize [dict get $thisarg_checks -maxsize] - # if {$checkval ne "-1"} { - # if {[tcl::string::length $e_check] > $maxsize} { - # set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } else { - # if {[lindex $clause_results $c_idx $a_idx] eq "_"} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # } - # number { - # #review - consider effects of Nan and Inf - # #NaN can be considered as 'technically' a number (or at least a special numeric value) - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # if {[tcl::dict::exists $thisarg -typeranges]} { - # set ranges [tcl::dict::get $thisarg -typeranges] - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # set range [lindex $ranges $clausecolumn] - # lassign {} low high ;#set both empty - # lassign $range low high - # set passed_checks 1 - # if {"$low$high" ne ""} { - # if {[::tcl::mathfunc::isnan $e]} { - # set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # if {$passed_checks} { - # if {$low eq ""} { - # if {$e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } elseif {$high eq ""} { - # if {$e_check < $low} { - # set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } else { - # if {$e_check < $low || $e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict usnet clauses_dict_check $c_idx - # } - # } - # } else { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict usnet clauses_dict_check $c_idx - # } - # } - - # } - # int { - # #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is integer -strict $e_check]} { - # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # if {[tcl::dict::exists $thisarg -typeranges]} { - # set ranges [tcl::dict::get $thisarg -typeranges] - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - # set range [lindex $ranges $clausecolumn] - # lassign $range low high - # set passed_checks 1 - # if {"$low$high" ne ""} { - # if {$low eq ""} { - # #lowside unspecified - check only high - # if {$e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } elseif {$high eq ""} { - # #highside unspecified - check only low - # if {$e_check < $low} { - # set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } else { - # #high and low specified - # if {$e_check < $low || $e_check > $high} { - # set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } else { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # double { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is double -strict $e_check]} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } - # } - # if {[tcl::dict::exists $thisarg_checks -typeranges]} { - # set ranges [dict get $thisarg_checks -typeranges] - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set e_check [lindex $clauseval_check $clausecolumn] - # set range [lindex $ranges $clausecolumn] - # #todo - small-value double comparisons with error-margin? review - # #todo - empty string for low or high - # set passed_checks 1 - # lassign $range low high - # if {$low$high ne ""} { - # if {$e_check < $low || $e_check > $high} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } else { - # dict for {c_idx clauseval} $clauses_dict { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # bool { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is boolean -strict $e_check]} { - # set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # dict { - # dict for {c_idx clauseval_check} $clauses_dict_check { - # puts "check_clausecolumn2 dict handler: c_idx:$c_idx clausecolumn:$clausecolumn clauseval_check:$clauseval_check" - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[llength $e_check] %2 != 0} { - # set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] - # } - # } - # dict for {c_idx clauseval_check} $clauses_dict_check { - # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { - # continue - # } - # set passed_checks 1 - # if {[tcl::dict::size $thisarg_checks]} { - # if {[dict exists $thisarg_checks -minsize]} { - # set minsizes [dict get $thisarg_checks -minsize] - # set e_check [lindex $clauseval_check $clausecolumn] - # set minsize [lindex $minsizes $clausecolumn] - # # -1 for disable is as good as zero - # if {[tcl::dict::size $e_check] < $minsize} { - # set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { - # set e_check [lindex $clauseval_check $clausecolumn] - # set maxsize [lindex $maxsizes $clausecolumn] - # if {$maxsize ne "-1"} { - # if {[tcl::dict::size $e_check] > $maxsize} { - # set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # } - - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # alnum - - # alpha - - # ascii - - # control - - # digit - - # graph - - # lower - - # print - - # punct - - # space - - # upper - - # wordchar - - # xdigit { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - # set e_check [lindex $clauseval_check $clausecolumn] - # if {![tcl::string::is $type -strict $e_check]} { - # set e [lindex $clauseval $t] - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # file - - # directory - - # existingfile - - # existingdirectory { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e [lindex $clauseval $clausecolumn] - # set e_check [lindex $clauseval_check $clausecolumn] - - # #//review - we may need '?' char on windows - # set passed_checks 1 - # if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - # #what about special file names e.g on windows NUL ? - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # if {$passed_checks} { - # if {$type eq "existingfile"} { - # if {![file exists $e_check]} { - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } elseif {$type eq "existingdirectory"} { - # if {![file isdirectory $e_check]} { - # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # char { - # #review - char vs unicode codepoint vs grapheme? - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e_check [lindex $clauseval_check $clausecolumn] - # if {[tcl::string::length $e_check] != 1} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # } else { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # tk_screen_units { - # dict for {c_idx clauseval} $clauses_dict { - # set clauseval_check [dict get $clauses_dict_check $c_idx] - - # set e_check [lindex $clauseval_check $clausecolumn] - # set passed_checks 1 - # switch -exact -- [string index $e_check end] { - # c - i - m - p { - # set numpart [string range $e_check 0 end-1] - # if {![tcl::string::is double $numpart]} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # default { - # if {![tcl::string::is double $e_check]} { - # set e [lindex $clauseval $clausecolumn] - # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." - # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] - # set passed_checks 0 - # } - # } - # } - # if {$passed_checks} { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # default { - # #default pass for unrecognised types - review. - # dict for {c_idx clauseval} $clauses_dict { - # lset clause_results $c_idx $a_idx 1 - # dict unset clauses_dict $c_idx - # dict unset clauses_dict_check $c_idx - # } - # } - # } - # } - # foreach clauseresult $clause_results { - # if {[lsearch $clauseresult 1] == -1} { - # #no pass for this clause - fetch first? error and raise - # #todo - return error containing clause_indices so we can report more than one failing element at once? - # foreach e $clauseresult { - # if {[lindex $e 0] eq "errorcode"} { - # #errorcode msg values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional etc - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $proc_opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] - } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list $opt_form] - } - - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - #todo - handle multiple fids? - set fid [lindex $selected_forms 0] - set formdict [dict get $FORMS $fid] - tcl::dict::with formdict {} - #populate vars ARG_INFO,LEADER_MAX,LEADER_NAMES etc - if {$VAL_MIN eq ""} { - set valmin 0 - #set VAL_MIN 0 - foreach v $VAL_REQUIRED { - # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) - # e.g -types {a ?xxx?} - #this has one required and one optional - set typelist [dict get $ARG_INFO $v -type] - set clause_length 0 - foreach t $typelist { - if {![string match {\?*\?} $t]} { - incr clause_length - } - } - incr valmin $clause_length - } - } else { - set valmin $VAL_MIN - } - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - #set optnames [lsearch -all -inline $argnames -*] - #JJJ - set lookup_optset [dict create] - foreach optset $OPT_NAMES { - #optset e.g {-x|--longopt|--longopt=|--otherlongopt} - foreach optdef [split $optset |] { - set opt [string trimright $optdef =] - if {![dict exists $lookup_optset $opt]} { - dict set lookup_optset $opt $optset - } - } - } - set all_opts [dict keys $lookup_optset] - - set ridx 0 - set rawargs_copy $rawargs - set remaining_rawargs $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - - - #consider for example: LEADER_NAMES {"k v" leader2 leader3} with -type {int number} & -type {int int int} & -type string - #(i.e clause-length of 2 3 and 1) - #This will take 6 raw leaders to fill in the basic case that all are -optional 0 and -multiple 0 - #REVIEW - what about optional members in leaders e.g -type {int ?double?} - set named_leader_args_max 0 - foreach ln $LEADER_NAMES { - set typelist [dict get $ARG_INFO $ln -type] - incr named_leader_args_max [llength $typelist] - } - - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" - #} - - set can_have_leaders 1 ;#default assumption - if {$LEADER_MAX == 0 || ([llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED)} { - set can_have_leaders 0 - } - - #REVIEW - this attempt to classify leaders vs opts vs values doesn't account for leaders with clauses containing optional elements - #e.g @leaders {x -type {int ?int?}} - set nameidx 0 - if {$can_have_leaders} { - if {$LEADER_TAKEWHENARGSMODULO} { - #assign set of leaders purely based on number of total args - set take [expr {[llength $remaining_rawargs] % $LEADER_TAKEWHENARGSMODULO}] - set pre_values [lrange $remaining_rawargs 0 $take-1] - set remaining_rawargs [lrange $remaining_rawargs $take end] - } else { - #greedy taking of leaders based on type-matching - - set leadernames_seen [list] - for {set ridx 0} {$ridx < [llength $rawargs]} {incr ridx} { - set raw [lindex $rawargs $ridx] ;#received raw arg - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {[llength $LEADER_NAMES] && $nameidx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $nameidx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > $named_leader_args_max-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string - } - if {$OPT_MAX ne "0" && [string match -* $raw]} { - #all_opts includes end_of_opts marker -- if configured - no need to explicitly check for it separately - set possible_flagname $raw - if {[string match --* $raw]} { - set eposn [string first = $raw] - # --flag=xxx - if {$eposn >=3} { - set possible_flagname [string range $raw 0 $eposn-1] - } - } - set matchopt [::tcl::prefix::match -error {} $all_opts $possible_flagname] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - set leader_type [dict get $ARG_INFO $leader_posn_name -type] - #todo - variable clauselengths e.g 'if' command which has optional 'then' and 'else' "noise words" - set clauselength [llength $leader_type] - set min_clauselength 0 - foreach t $leader_type { - if {![string match {\?*\?} $t]} { - incr min_clauselength - } - } - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] $raw] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop remaining_rawargs 0] - # incr ridx - # continue - # } - #} - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break - } - - #check if enough remaining_rawargs to fill any required values - if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { - break - } - - #leadername may be a 'clause' of arbitrary length (e.g -type {int double} or {int string number}) - set end_leaders 0 - set tentative_pre_values [list] - set tentative_idx $ridx - if {$OPT_MAX ne "0"} { - foreach t $leader_type { - set raw [lindex $rawargs $tentative_idx] - if {[string match {\?*\?} $t] && [string match -* $raw]} { - #review - limitation of optional leaders is they can't be same value as any defined flags/opts - set flagname $raw - if {[string match --* $raw]} { - set eposn [string first = $raw] - # --flag=xxx - if {$eposn >=3} { - set flagname [string range $raw 0 $eposn-1] - } - } - set matchopt [::tcl::prefix::match -error {} $all_opts $flagname] - if {$matchopt ne ""} { - #don't consume if flaglike (and actually matches an opt) - set end_leaders 1 - break ;#break out of looking at -type members in the clause - } else { - #unrecognised flag - treat as value for optional member of the clause - #lappend pre_values [lpop remaining_rawargs 0] - lappend tentative_pre_values $raw - incr tentative_idx - } - } else { - #lappend pre_values [lpop remaining_rawargs 0] - lappend tentative_pre_values $raw - incr tentative_idx - } - } - if {$end_leaders} { - break - } - } else { - foreach t $leader_type { - #JJJ - set raw [lindex $rawargs $tentative_idx] - #lappend pre_values [lpop remaining_rawargs 0] - lappend tentative_pre_values $raw - incr tentative_idx - } - } - set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] - set consumed [dict get $assign_d consumed] - set resultlist [dict get $assign_d resultlist] - set newtypelist [dict get $assign_d typelist] - if {$consumed != 0} { - if {$leader_posn_name ni $leadernames_seen} { - lappend leadernames_seen $leader_posn_name - } - dict incr leader_posn_names_assigned $leader_posn_name - #for {set c 0} {$c < $consumed} {incr c} { - # lappend pre_values [lpop remaining_rawargs 0] - #} - lappend pre_values {*}[lrange $remaining_rawargs 0 $consumed-1] - ledit remaining_rawargs 0 $consumed-1 - - incr ridx $consumed - incr ridx -1 ;#leave ridx at index of last r that we set - } else { - - } - if {!$is_multiple} { - incr nameidx - } - } else { - #clause is required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one complete clause for this name - requirement satisfied - now equivalent to optional - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break - } - - if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but still enough remaining_rawargs for required values - #we still need to check if enough values for the leader itself - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill *required* leader - break - } - - set end_leaders 0 - - #review - are we allowing multivalue leader clauses where the optional members are not at the tail? - #eg @leaders {double -type {?int? char}} - #as we don't type-check here while determining leaders vs opts vs values - this seems impractical. - #for consistency and simplification - we should only allow optional clause members at the tail - # and only for the last defined leader. This should be done in the definition parsing - not here. - foreach t $leader_type { - set raw [lindex $rawargs $ridx] - if {[string match {\?*\?} $t] && [string match -* $raw]} { - #review - limitation of optional leaders is they can't be same value as any defined flags/opts - - set matchopt [::tcl::prefix::match -error {} $all_opts $raw] - if {$matchopt ne ""} { - #don't consume if flaglike (and actually matches an opt) - set end_leaders 1 - break ;#break out of looking at -type members in the clause - } else { - #unrecognised flag - treat as value for optional member of the clause - #ridx must be valid if we matched -* - so lpop will succeed - lappend pre_values [lpop remaining_rawargs 0] - incr ridx - } - } else { - if {[string match {\?*\?} $t]} { - if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { - set end_leaders 1 - break - } - if {[catch { - lappend pre_values [lpop remaining_rawargs 0] - }]} { - set end_leaders 1 - break - } - } else { - if {[catch { - lappend pre_values [lpop remaining_rawargs 0] - }]} { - set end_leaders 1 - break ;#let validation of required leaders report the error? - } - } - incr ridx - } - } - incr ridx -1 - if {$end_leaders} { - break - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break - } else { - if {$valmin > 0} { - if {[llength $remaining_rawargs] > $valmin} { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } else { - break - } - } else { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #review - if is_multiple, keep going if enough remaining_rawargs for values? - break - } - } - - #incr ridx - } ;# end foreach r $rawargs_copy - } - } - #puts "get_dict ================> pre: $pre_values" - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - if {[llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED} { - set leadermax 0 - } else { - set leadermax -1 - } - } else { - set leadermax $LEADER_MAX - } - - if {$VAL_MAX eq ""} { - if {[llength $VAL_NAMES] == 0 && !$VAL_UNNAMED} { - set valmax 0 - } else { - set valmax -1 - } - } else { - set valmax $VAL_MAX - } - - #assert leadermax leadermin are numeric - #assert - remaining_rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to OPT_DEFAULTS here - - set leaders [list] - set arglist {} - set post_values {} - #valmin, valmax - #puts stderr "remaining_rawargs: $remaining_rawargs" - #puts stderr "argstate: $argstate" - if {$OPT_MAX ne "0" && [lsearch $remaining_rawargs -*] >= 0} { - #contains at least one possible flag - set maxidx [expr {[llength $remaining_rawargs] -1}] - if {$valmax == -1} { - set vals_total_possible [llength $remaining_rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $valmax - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set remaining_args_including_this [expr {[llength $remaining_rawargs] - $i}] - #lowest valmin is 0 - if {$remaining_args_including_this <= $valmin} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - set a [lindex $remaining_rawargs $i] - switch -glob -- $a { - -- { - if {$a in $OPT_NAMES} { - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $remaining_rawargs 0 $i] - set post_values [lrange $remaining_rawargs $i+1 end] - } else { - #assume it's a value. - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - } - break - } - --* { - set eposn [string first = $a] - if {$eposn > 2} { - #only allow longopt-style = for double leading dash longopts - #--*= usage - if {$flagname ni $raw_optionset_members} { - # - set msg "Bad options for %caller%. Option $optionset at index [expr {$i-1}] requires a value, but '$flagname' not specified in definition to allow space-separated value." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list badoptionformat $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg - } - } - if {$solo_only} { - #same logic as 'solo' branch below for -type none - if {[tcl::dict::exists $argstate $optionset -typedefaults]} { - set tdflt [tcl::dict::get $argstate $optionset -typedefaults] - } else { - #normal default for a solo is 1 unless overridden by -typedefaults - set tdflt 1 - } - if {[tcl::dict::get $argstate $optionset -multiple]} { - if {$api_opt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $flag_ident $tdflt - } else { - tcl::dict::lappend opts $flag_ident $tdflt - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - if {$flag_ident_is_parsekey} { - lappend opts $flag_ident $tdflt - } else { - tcl::dict::set opts $flag_ident $tdflt - } - } - incr vals_remaining_possible -1 - lappend solosreceived $api_opt ;#dups ok - } else { - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - #review - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - #flagval comes from next remaining rawarg - set flagval [lindex $remaining_rawargs $i+1] - if {[tcl::dict::get $argstate $optionset -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$api_opt ni $flagsreceived} { - tcl::dict::set opts $flag_ident [list $flagval] - } else { - tcl::dict::lappend opts $flag_ident $flagval - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - #tcl::dict::set opts $flag_ident $flagval - if {$flag_ident_is_parsekey} { - #necessary shimmer - lappend opts $flag_ident $flagval - } else { - tcl::dict::set opts $flag_ident $flagval - } - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $optionset at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg - } - } - } - } else { - #none / solo - if {[tcl::dict::exists $argstate $optionset -typedefaults]} { - set tdflt [tcl::dict::get $argstate $optionset -typedefaults] - } else { - #normal default for a solo is 1 unless overridden by -typedefaults - set tdflt 1 - } - if {[tcl::dict::get $argstate $optionset -multiple]} { - if {$api_opt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $flag_ident $tdflt - } else { - tcl::dict::lappend opts $flag_ident $tdflt - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - #test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence} - #tcl::dict::set opts $flag_ident $tdflt - if {$flag_ident_is_parsekey} { - #(shimmer - but required for ordering correctness during override) - lappend opts $flag_ident $tdflt - } else { - tcl::dict::set opts $flag_ident $tdflt - } - } - incr vals_remaining_possible -1 - lappend solosreceived $api_opt ;#dups ok - } - lappend flagsreceived $api_opt ;#dups ok - } else { - #starts with - but unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even if optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - if {!([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$OPT_ANY} { - #exlude argument with whitespace from being a possible option e.g dict - #todo - passthrough of unrecognised --longopt=xxx without looking for following flag-value - set eposn [string first = $a] - if {[string match --* $a] && $eposn > 2} { - #only allow longopt-style = for double leading dash longopts - #--*= $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::exists $argstate $a -typedefaults]} { - set tdflt [tcl::dict::get $argstate $a -typedefaults] - } else { - set tdflt 1 - } - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a $tdflt - } else { - tcl::dict::lappend opts $a $tdflt - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $tdflt - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - } - - lappend flagsreceived $undefined_flagsupplied ;#adhoc flag name (if --x=1 -> --x) - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES (3)" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any|-arbitrary false" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $optionset - } - } else { - #not a flag/option - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - } - - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $remaining_rawargs - #set values [list {*}$pre_values {*}$remaining_rawargs] ;#no -flags detected - set arglist [list] - } - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> arglist: $arglist" - #puts stderr "get_dict--> leaders: $leaders" - #puts stderr "get_dict--> values: $values" - #} - - #--------------------------------------- - #Order the received options by the order in which they are *defined* - #EXCEPT that grouped options using same parsekey must be processed in received order - set ordered_opts [dict create] - - #set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] - ##unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) - ## e.g -fg|-foreground - ## e.g -x|--fullname= - ##Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} - #foreach o $unaliased_opts optset $OPT_NAMES { - # if {[dict exists $opts $o]} { - # dict set ordered_opts $o [dict get $opts $o] - # } elseif {[dict exists $OPT_DEFAULTS $optset]} { - # #JJJ - # set parsekey "" - # if {[tcl::dict::exists $argstate $o -parsekey]} { - # set parsekey [tcl::dict::get $argstate $o -parsekey] - # } - # if {$parsekey eq ""} { - # set parsekey $o - # } - # dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] - # } - #} - - #puts ">>>>==== $opts" - set seen_pks [list] - #treating opts as list for this loop. - foreach optset $OPT_NAMES { - set parsekey "" - set has_parsekey_override 0 - if {[tcl::dict::exists $argstate $optset -parsekey]} { - set parsekey [tcl::dict::get $argstate $optset -parsekey] - } - if {$parsekey eq ""} { - set has_parsekey_override 0 - #fall back to last element of aliased option e.g -fg|-foreground -> "-foreground" - set parsekey [string trimright [lindex [split $optset |] end] =] - } else { - set has_parsekey_override 1 - } - lappend seen_pks $parsekey - set is_found 0 - set foundkey "" - set foundval "" - #no lsearch -stride avail in 8.6 - foreach {k v} $opts { - if {$k eq $parsekey} { - set foundkey $k - set is_found 1 - set foundval $v - #can be multiple - last match wins - don't 'break' out of foreach - } - } ;#avoiding further dict/list shimmering - #if {[dict exists $opts $parsekey]} { - # set found $parsekey - # set foundval [dict get $opts $parsekey] - #} - if {!$is_found && $parsekey ne $optset} { - #.g we may have in opts things like: -decreasing|-SORTDIRECTION -increasing|-SORTDIRECTION - #(where -SORTDIRECTION was configured as -parsekey) - #last entry must win - #NOTE - do not use dict for here. opts is not strictly a dict - dupe keys will cause wrong ordering - foreach {o v} $opts { - if {[string match *|$parsekey $o]} { - set foundkey $o - set is_found 1 - set foundval $v - #last match wins - don't 'break' out of foreach - } - } - } - if {$is_found} { - dict set ordered_opts $foundkey $foundval - } elseif {[tcl::dict::exists $OPT_DEFAULTS $optset]} { - if {$parsekey ne $optset} { - set tailopt [string trimright [lindex [split $optset |] end] =] - if {$tailopt ne $parsekey} { - #defaults for multiple options sharing a -parsekey value ? review - dict set ordered_opts $tailopt|$parsekey [dict get $OPT_DEFAULTS $optset] - } else { - dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] - } - } else { - dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] - } - } - } - - #add in possible arbitrary opts after the defined opts, due to @opts directive flag '-any|-arbitrary true' - #But make sure not to add any repeated parsekey e.g -increasing|-SORT -decreasing|-SORT - #use the seen_pks from the ordered_opts loop above - #keep working with opts only as list here.. - if {[llength $opts] > 2*[dict size $ordered_opts]} { - foreach {o o_val} $opts { - lassign [split $o |] _ parsekey ;#single pipe - 2 elements only | - if {$parsekey ne "" && $parsekey in $seen_pks} { - continue - } - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $o_val - } - } - } - set opts $ordered_opts - #opts is a proper dict now - - #NOTE opts still may contain some entries in non-final form such as -flag|-PARSEKEY - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set leadername_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - - #---------------------------------------- - #Establish firm leaders ordering - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value - - - set start_position $positionalidx - set nameidx 0 - #MAINTENANCE - (*nearly*?) same loop logic as for value - for {set ldridx 0} {$ldridx < [llength $leaders]} {incr ldridx} { - set leadername [lindex $LEADER_NAMES $nameidx] - set ldr [lindex $leaders $ldridx] - if {$leadername ne ""} { - set leadertypelist [tcl::dict::get $argstate $leadername -type] - set leader_clause_size [llength $leadertypelist] - - set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] - set consumed [dict get $assign_d consumed] - set resultlist [dict get $assign_d resultlist] - set newtypelist [dict get $assign_d typelist] - if {[tcl::dict::get $argstate $leadername -optional]} { - if {$consumed == 0} { - puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" - incr ldridx -1 - set leadername_multiple "" - incr nameidx - continue - } - } else { - #required named arg - if {$consumed == 0} { - if {$leadername ni $leadernames_received} { - #puts stderr "_get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" - set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg - } else { - puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" - incr ldridx -1 - set leadername_multiple "" - incr nameidx - continue - } - } - } - - if {$leader_clause_size == 1} { - #set clauseval $ldr - set clauseval [lindex $resultlist 0] - } else { - set clauseval $resultlist - incr ldridx [expr {$consumed - 1}] - - #not quite right.. this sets the -type for all clauses - but they should run independently - #e.g if expr {} elseif 2 {script2} elseif 3 then {script3} (where elseif clause defined as "literal(elseif) expr ?literal(then)? script") - #the elseif 2 {script2} will raise an error because the newtypelist from elseif 3 then {script3} overwrote the newtypelist where then was given the type ?omitted-...? - - tcl::dict::set argstate $leadername -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries - } - - if {[tcl::dict::get $argstate $leadername -multiple]} { - #if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - # #current stored ldr equals defined default - don't include default in the list we build up - # tcl::dict::set leaders_dict $leadername [list $clauseval] ;#important to treat first element as a list - #} else { - # tcl::dict::lappend leaders_dict $leadername $clauseval - #} - if {$leadername in $leadernames_received} { - tcl::dict::lappend leaders_dict $leadername $clauseval - tcl::dict::lappend argument_clause_typestate $leadername $newtypelist - } else { - tcl::dict::set leaders_dict $leadername [list $clauseval] - tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] - } - set leadername_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $clauseval - tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] - set leadername_multiple "" - incr nameidx - } - lappend leadernames_received $leadername - } else { - if {$leadername_multiple ne ""} { - set leadertypelist [tcl::dict::get $argstate $leadername_multiple -type] - if {[llength $leadertypelist] == 1} { - set clauseval $ldr - } else { - set clauseval [list] - incr ldridx -1 - foreach t $leadertypelist { - incr ldridx - if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername_multiple', but requires up to [llength $leadertypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $leadertypelist] ] -argspecs $argspecs]] $msg - } - lappend clauseval [lindex $leaders $ldridx] - } - } - tcl::dict::lappend leaders_dict $leadername_multiple $clauseval - #name already seen - but must add to leadernames_received anyway (as with opts and values) - lappend leadernames_received $leadername_multiple - } else { - if {$LEADER_UNNAMED} { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } else { - set msg "Bad number of leaders for %caller%. Received more leaders than can be assigned to argument names. (set '@leaders -unnamed true' to allow unnamed leaders)" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list toomanyarguments [llength $values] index $positionalidx] -argspecs $argspecs]] $msg - } - } - } - set positionalidx [expr {$start_position + $ldridx + 1}] - } - #----------------------------------------------------- - #satisfy test parse_withdef_leaders_no_phantom_default - #foreach leadername [dict keys $leaders_dict] { - # if {[string is integer -strict $leadername]} { - # #ignore leadername that is a positionalidx - # #review - always trailing - could use break? - # continue - # } - # if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { - # #remove the name with empty-string default we used to establish fixed order of names - # #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - # dict unset leaders_dict $leadername - # } - #} - dict for {leadername _v} $leaders_dict { - if {[string is integer -strict $leadername]} { - #ignore leadername that is a positionalidx - #review - always trailing - could use break? - continue - } - if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { - #remove the name with empty-string default we used to establish fixed order of names - #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - dict unset leaders_dict $leadername - } - } - #----------------------------------------------------- - - - set validx 0 - set valname_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #Establish firm values ordering - ## Don't set values_dict to VAL_DEFAULTS - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - #set ALL valnames to lock in positioning - #note - later we need to unset any optional that had no default and was not received (no phantom default) - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $VAL_DEFAULTS] - #------------------------------------------ - set nameidx 0 - set start_position $positionalidx - #MAINTENANCE - (*nearly*?) same loop logic as for leaders - for {set validx 0} {$validx < [llength $values]} {incr validx} { - set valname [lindex $VAL_NAMES $nameidx] - set val [lindex $values $validx] - if {$valname ne ""} { - set valtypelist [tcl::dict::get $argstate $valname -type] - set clause_size [llength $valtypelist] ;#common case is clause_size == 1 - - set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] - set consumed [dict get $assign_d consumed] - set resultlist [dict get $assign_d resultlist] - set newtypelist [dict get $assign_d typelist] - if {[tcl::dict::get $argstate $valname -optional]} { - if {$consumed == 0} { - #error 333 - puts stderr "get_dict cannot assign val:$val to valname:$valname (333)" - incr validx -1 - set valname_multiple "" - incr nameidx - continue - } - } else { - #required named arg - if {$consumed == 0} { - if {$valname ni $valnames_received} { - #puts stderr "_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" - set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg - } else { - #error 444 - puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" - incr validx -1 - set valname_multiple "" - incr nameidx - continue - } - } - } - #assert can_assign != 0, we have at least one value to assign to clause - - if {$clause_size == 1} { - #set clauseval $val - set clauseval [lindex $resultlist 0] - } else { - #clauseval must contain as many elements as the max length of -types! - #(empty-string/default for optional (?xxx?) clause members) - set clauseval $resultlist - #_get_dict_can_assign has only validated clause-length and literals match - #we assign and leave further validation for main validation loop. - incr validx [expr {$consumed -1}] - if {$validx > [llength $values]-1} { - error "get_dict unreachable" - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to $clause_size values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength $clause_size ] -argspecs $argspecs]] $msg - } - - #incorrect - we shouldn't update the default. see argument_clause_typestate dict of lists of -type - tcl::dict::set argstate $valname -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries - } - - if {[tcl::dict::get $argstate $valname -multiple]} { - #if {[tcl::dict::exists $VAL_DEFAULTS $valname]} { - # #current stored val equals defined default - don't include default in the list we build up - # tcl::dict::set values_dict $valname [list $clauseval] ;#important to treat first element as a list - #} else { - # tcl::dict::lappend values_dict $valname $clauseval - #} - if {$valname in $valnames_received} { - tcl::dict::lappend values_dict $valname $clauseval - tcl::dict::lappend argument_clause_typestate $valname $newtypelist - } else { - tcl::dict::set values_dict $valname [list $clauseval] - tcl::dict::set argument_clause_typestate $valname [list $newtypelist] - } - set valname_multiple $valname - } else { - tcl::dict::set values_dict $valname $clauseval - tcl::dict::set argument_clause_typestate $valname [list $newtypelist] ;#list protect - set valname_multiple "" - incr nameidx - } - lappend valnames_received $valname - } else { - #unnamed - if {$valname_multiple ne ""} { - set valtypelist [tcl::dict::get $argstate $valname_multiple -type] - if {[llength $valname_multiple] == 1} { - set clauseval $val - } else { - set clauseval [list] - incr validx -1 - for {set i 0} {$i < [llength $valtypelist]} {incr i} { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname_multiple', but requires [llength $valtypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valtypelist] ] -argspecs $argspecs]] $msg - } - lappend clauseval [lindex $values $validx] - } - } - tcl::dict::lappend values_dict $valname_multiple $clauseval - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $valname_multiple - } else { - if {$VAL_UNNAMED} { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $VALSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } else { - set msg "Bad number of values for %caller%. Received more values than can be assigned to argument names. (set '@values -unnamed true' to allow unnamed values)" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list toomanyarguments [llength $values] index $positionalidx] -argspecs $argspecs]] $msg - } - } - } - set positionalidx [expr {$start_position + $validx + 1}] - } - #----------------------------------------------------- - #satisfy test parse_withdef_values_no_phantom_default - foreach vname [dict keys $values_dict] { - if {[string is integer -strict $vname]} { - #ignore vname that is a positionalidx - #review - always trailing - could break? - continue - } - if {$vname ni $valnames_received && ![dict exists $VAL_DEFAULTS $vname]} { - #remove the name with empty-string default we used to establish fixed order of names - #The 'values' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - dict unset values_dict $vname - } - } - #----------------------------------------------------- - #JJJJJJ - #if {[dict size $argument_clause_typestate]} { - # puts ">>>>>[dict get $argspecs id] typestate $argument_clause_typestate" - #} - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } - } - - if {$valmax == -1} { - #only check min - if {$num_values < $valmin} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } else { - if {$num_values < $valmin || $num_values > $valmax} { - if {$valmin == $valmax} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $valmin and $valmax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } - } - - #assertion - opts keys are full-length option names if -any|-arbitrary was false or if the supplied option as a shortname matched one of our defined options - #(and may still contain non-final flag_ident entries such as -increasing|-SORTDIRECTION) - - - #opts explicitly marked as -optional 0 must be present - regardless of -any|-arbitrary (which allows us to ignore additional opts to pass on to next call) - #however - if -any|-arbitrary is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $OPT_REQUIRED $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $VAL_REQUIRED $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength $LEADER_REQUIRED]} { - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - } - if {[llength $OPT_REQUIRED]} { - set api_opt_required [lmap v $OPT_REQUIRED {lindex [split $v |] end}] - if {[llength [set missing [punklib_ldiff $api_opt_required $flagsreceived]]]} { - set full_missing [dict get $lookup_optset $missing] - set msg "Required option missing for %caller%. missing flags: '$full_missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $full_missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - } - if {[llength $VAL_REQUIRED]} { - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - } - - #--------------------------------------------------------------------------------------------- - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - #--------------------------------------------------------------------------------------------- - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "get_dict>>>>>>>> ---opts_and_values:$opts_and_values" - #puts " >>>>>>> ---lookup_optset :$lookup_optset" - #puts "---argstate:$argstate" - #JJJ argname_or_ident; ident example: -increasing|-SORTOPTION - tcl::dict::for {argname_or_ident value_group} $opts_and_values { - # - #parsekey: key used in resulting leaders opts values dictionaries - # often distinct from the full argname in the ARG_INFO structure - # - if {[string match -* $argname_or_ident]} { - #ident format only applies to options/flags - if {[string first | $argname_or_ident] > -1} { - #flag_ident style (grouped fullname of option with -parsekey) - lassign [split $argname_or_ident |] fullflag parsekey ;#we expect only a single pipe in ident form | - if {[dict exists $lookup_optset $fullflag]} { - set argname [dict get $lookup_optset $fullflag] - #idents should already have correct parsekey - } else { - puts stderr "punk::args::get_dict unable to find $fullflag in $lookup_optset (parsekey:$parsekey) (value_group: $value_group)" - } - } else { - if {[dict exists $lookup_optset $argname_or_ident]} { - #get full option name such as -fg|-foreground from non-alias name such as -foreground - #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined - set argname [dict get $lookup_optset $argname_or_ident] - set pkoverride [Dict_getdef $argstate -parsekey ""] - if {$pkoverride ne ""} { - set parsekey $pkoverride - } else { - #default parsekey: last element in argname without trailing = - set parsekey [string trimright [lindex [split $argname |] end] =] - } - } else { - puts stderr "punk::args::get_dict unable to find $argname_or_ident in $lookup_optset (value_group: $value_group)" - } - } - } else { - set argname $argname_or_ident - set pkoverride [Dict_getdef $argstate -parsekey ""] - if {$pkoverride ne ""} { - set parsekey $pkoverride - } else { - #leader or value of form x|y has no special meaning and forms the parsekey in entirety by default. - set parsekey $argname - } - } - #assert: argname is the key for the relevant argument info in the FORMS//ARG_INFO dict. (here each member available as $argstate) - #argname is usually the full name as specified in the definition: - #e.g -f|-path|--filename= - # (where the parsekey will be by default --filename, possibly overridden by -parsekey value) - #an example argname_or_compound for the above might be: -path|--filename - # where -path is the expanded form of the actual flag used (could have been for example just -p) and --filename is the parsekey - - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set typelist [tcl::dict::get $thisarg -type] - set clause_size [llength $typelist] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - #JJJJ - #if {$is_multiple} { - # set vlist $value_group - #} else { - # set vlist [list $value_group] - #} - ##JJJJ - #if {$clause_size == 1} { - # set vlist [list $vlist] - #} - - - #JJ 2025-07-25 - set vlist [list] - #vlist is a list of clauses. Each clause is a list of values of length $clause_size. - #The common case is clause_size 1 - but as we need to treat each clause as a list during validation - we need to list protect the clause when clause_size == 1. - if {$is_multiple} { - if {$clause_size == 1} { - foreach c $value_group { - lappend vlist [list $c] - } - } else { - set vlist $value_group - } - } else { - if {$clause_size ==1} { - set vlist [list [list $value_group]] - } else { - set vlist [list $value_group] - } - } - set vlist_typelist [list] - if {[dict exists $argument_clause_typestate $argname]} { - #lookup saved newtypelist (argument_clause_typelist) from can_assign_value result where some optionals were given type ?omitted-? or ?defaulted-? - # args.test: parse_withdef_value_clause_missing_optional_multiple - set vlist_typelist [dict get $argument_clause_typestate $argname] - } else { - foreach v $vlist { - lappend vlist_typelist $typelist - } - } - - - - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach clause_value $vlist { - #lappend vlist_check [punk::ansi::ansistrip $clause_value] - set stripped [list] - foreach element $clause_value { - lappend stripped [punk::ansi::ansistrip $element] - } - lappend vlist_check $stripped - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - switch -- [Dict_getdef $thisarg -ARGTYPE unknown] { - leader { - set dname leaders_dict - set argclass "Leading argument" - } - option { - set dname opts - set argclass Option - } - value { - set dname values_dict - set argclass "Trailing argument" - } - default { - set dname "_unknown_" ;#NA - set argclass "Unknown argument" - } - } - set vlist_validate [list] - set vlist_check_validate [list] - set vlist_typelist_validate [list] - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestricted 0 where some selections match and others don't) - if {$parsekey in $receivednames && $has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choiceprefixreservelist [Dict_getdef $thisarg -choiceprefixreservelist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - set clause_index -1 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - #J2 - #set vlist_validate [list] - #set vlist_check_validate [list] - foreach clause $vlist clause_check $vlist_check clause_typelist $vlist_typelist { - incr clause_index - set element_index -1 ;#element within clause - usually clause size is only 1 - foreach e $clause e_check $clause_check { - incr element_index - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set choices_test $allchoices - set v_test $c_check - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$chosen eq "" || $chosen in $choiceprefixreservelist} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - #our existing values in $dname are not list-protected - so we need to check clause_size - if {$choice_in_list && !$choice_exact_match} { - set existing [tcl::dict::get [set $dname] $argname_or_ident] - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - #single choice allowed per clause-member - if {$is_multiple} { - if {$clause_size == 1} { - #no list wrapping of single element in $dname dict - so don't index into it with element_index - lset existing $element_index $chosen - } else { - lset existing $clause_index $element_index $chosen - } - tcl::dict::set $dname $argname_or_ident $existing - } else { - #test: choice_multielement_clause - lset existing $element_index $chosen - tcl::dict::set $dname $argname_or_ident $existing - } - } else { - if {$is_multiple} { - #puts ">>> existing $existing $choice_idx" - if {$clause_size == 1} { - #no list wrapping of single element in $dname dict - so don't index into it with element_index - lset existing $clause_index $choice_idx $chosen - } else { - lset existing $clause_index $element_index $choice_idx $chosen - } - tcl::dict::set $dname $argname_or_ident $existing - } else { - lset existing $element_index $choice_idx $chosen - tcl::dict::set $dname $argname_or_ident $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $clause_index $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - #JJJ - #lappend vlist_validate $c - #lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } else { - #choice is in list or matches default - no validation for this specific element in the clause - lset clause_typelist $element_index any - } - incr choice_idx - } - - } ;#end foreach e in clause - #jjj 2025-07-16 - #if not all clause_typelist are 'any' - if {[lsearch -not $clause_typelist any] > -1} { - #at least one element still needs validation - lappend vlist_validate $clause - lappend vlist_check_validate $clause_check - lappend vlist_typelist_validate $clause_typelist - } - - - } ;#end foreach clause in vlist - - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - set vlist_typelist $vlist_typelist_validate - } - - #todo - don't add to validation lists if not in receivednames - #if we have an optionset such as "-f|-x|-etc"; the parsekey is -etc (unless it was overridden by -parsekey in definition) - if {$parsekey ni $receivednames} { - set vlist [list] - set vlist_check_validate [list] - } else { - if {[llength $vlist] && $has_default} { - #defaultval here is a value for the entire clause. (clause usually length 1) - #J2 - #set vlist_validate [list] - #set vlist_check_validate [list] - set tp [dict get $thisarg -type] - set clause_size [llength $tp] - foreach clause_value $vlist clause_check $vlist_check clause_typelist $vlist_typelist { - #JJJJ - #REVIEW!!! we're inadvertently adding back in things that may have already been decided in choicelist loop as not requiring validation? - if {$clause_value ni $vlist_validate} { - if {$clause_size ==1} { - #for -choicemultiple with default that could be a list use 'ni' - #?? review! - if {[lindex $clause_check 0] ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check - lappend vlist_typelist_validate $clause_typelist - } - } else { - if {$clause_check ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check - lappend vlist_typelist_validate $clause_typelist - } - } - } - #if {[llength $tp] == 1} { - # if {$clause_value ni $vlist_validate} { - # #for -choicemultiple with default that could be a list use 'ni' - # #?? review! - # if {[lindex $clause_check 0] ne $defaultval} { - # lappend vlist_validate $clause_value - # lappend vlist_check_validate $clause_check - # } - # } - #} else { - # if {$clause_value ni $vlist_validate} { - # if {$clause_check ne $defaultval} { - # lappend vlist_validate $clause_value - # lappend vlist_check_validate $clause_check - # } - # } - #} - #Todo? - #else ??? - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - set vlist_typelist $vlist_typelist_validate - } - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach clause_value $vlist { - foreach e $clause_value { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - #$t = clause column - - #for {set clausecolumn 0} {$clausecolumn < [llength $typelist]} {incr clausecolumn} {} - set clausecolumn -1 - foreach typespec $typelist { - incr clausecolumn - if {[dict exists $thisarg -typedefaults]} { - set tds [dict get $thisarg -typedefaults] - if {[lindex $vlist $clausecolumn] eq [lindex $tds $clausecolumn]} { - continue - } - } - - set type_expression [string trim $typespec ?] - if {$type_expression in {any none unknown}} { - continue - } - #puts "$argname - switch on type_expression: $type_expression v:[lindex $vlist $clausecolumn]" - #set typespec [lindex $typelist $clausecolumn] - #todo - handle type-alternates e.g -type char|double - #------------------------------------------------------------------------------------ - #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist - _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs - #------------------------------------------------------------------------------------ - - - #todo - pass validation if matches an entry in -typedefaults - #has_typedefault? - #set typedefault [lindex $typedefaults $clausecolumn] - - - } - - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname_or_ident $stripped_list - } - option { - tcl::dict::set opts $argname_or_ident $stripped_list - } - value { - tcl::dict::set values_dict $argname_or_ident $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname_or_ident [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname_or_ident [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict $argname_or_ident [lindex $stripped_list 0] - } - } - } - } - } - - set finalopts [dict create] - dict for {o v} $opts { - if {[string first | $o] > -1} { - #set parsekey [lindex [split $o |] end] - dict set finalopts [lindex [split $o |] end] $v - } else { - dict set finalopts $o $v - } - } - set docid [dict get $argspecs id] - return [tcl::dict::create leaders $leaders_dict opts $finalopts values $values_dict received $received_posns solos $solosreceived multis $multisreceived id $docid] - } - - lappend PUNKARGS [list { - @id -id ::punk::args::forms - @cmd -name punk::args::forms\ - -summary\ - "List command forms."\ - -help\ - "Return names for each form of a command identified by 'id'. - Most commands are single-form and will only return the name '_default'." - @leaders -min 0 -max 0 - @opts - @values -min 1 -max 1 - id -multiple 0 -optional 0 -help\ - "Exact id of command" - }] - proc forms {id} { - set spec [get_spec $id] - if {[dict size $spec]} { - return [dict get $spec form_names] - } else { - return [list] - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::eg - @cmd -name punk::args::eg\ - -summary\ - "Command examples."\ - -help\ - "Return command examples from -help in @examples - directive of a command definition." - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc eg {args} { - set argd [punk::args::parse $args withid ::punk::args::eg] - lassign [dict values $argd] leaders opts values received - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - return [dict get $spec examples_info -help] - } - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis\ - -summary\ - "Command synopsis"\ - -help\ - "Return synopsis for each form of a command - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -noheader -type none - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set NI [punk::ansi::a+ noitalic] - #for inner question marks marking optional type - set IS [punk::ansi::a+ italic strike] - set NIS [punk::ansi::a+ noitalic nostrike] - #set RST [punk::ansi::a] - set RST "\x1b\[m" - } else { - set I "" - set NI "" - set IS "" - set NIS "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values received - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {[string is integer -strict $form]} { - set f [lindex $form_names $form] - if {$f ne ""} { - set form_names [list $f] - } else { - set form_names [list] - } - } else { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - } - - set SYND [dict create] - dict set SYND cmd_info [dict get $spec cmd_info] - #leading "# " required (punk::ns::synopsis will pass through) - if {![dict exists $received -noheader]} { - set syn "# [Dict_getdef $spec cmd_info -summary ""]\n" - } - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND FORMS $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - #foreach argname [dict get $forminfo LEADER_NAMES] { - # set arginfo [dict get $forminfo ARG_INFO $argname] - # set ARGD [dict create argname $argname class leader] - # if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display [lindex [dict get $arginfo -choices] 0] - # } elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - # } else { - # set display $I$argname$RST - # } - # if {[dict get $arginfo -optional]} { - # append syn " ?$display?" - # } else { - # append syn " $display" - # } - # dict set ARGD type [dict get $arginfo -type] - # dict set ARGD optional [dict get $arginfo -optional] - # dict set ARGD display $display - # dict lappend SYND $f $ARGD - #} - set FORMARGS [list] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set typelist [dict get $arginfo -type] - if {[llength $typelist] == 1} { - set tp [lindex $typelist 0] - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - #set arg_display [dict get $arginfo -typesynopsis] - set clause $ts - } else { - #set arg_display $argname - set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - set type_alternatives [_split_type_expression $tp] - foreach tp_alternative $type_alternatives { - set firstword [lindex $tp_alternative 0] - switch -exact -- $firstword { - literal { - set match [lindex $tp_alternative 1] - lappend alternates $match - } - literalprefix { - #todo - trie styling on prefix calc - set match [lindex $tp_alternative 1] - lappend alternates $match - } - stringstartswith { - set match [lindex $tp_alternative 1] - lappend alternates $match* - } - stringendswith { - set match [lindex $tp_alternative 1] - lappend alternates *$match - } - default { - lappend alternates $I$argname$NI - } - } - - #if {$tp_alternative eq "literal"} { - # lappend alternates [lindex $argname end] - #} elseif {[string match literal(*) $tp_alternative]} { - # set match [string range $tp_alternative 8 end-1] - # lappend alternates $match - #} elseif {[string match literalprefix(*) $tp_alternative]} { - # set match [string range $tp_alternative 14 end-1] - # lappend alternates $match - #} else { - # lappend alternates $I$argname$NI - #} - } - #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) - #todo - trie prefixes display - set alternates [punk::args::lib::lunique $alternates] - set clause [join $alternates |] - } - } else { - set n [expr {[llength $typelist]-1}] - set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types - set clause "" - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - set tp_displaylist $ts - } else { - set tp_displaylist [lrepeat [llength $typelist] ""] - } - - foreach typespec $typelist td $tp_displaylist elementname $name_tail { - #elementname will commonly be empty - if {[string match {\?*\?} $typespec]} { - set tp [string range $typespec 1 end-1] - set member_optional 1 - } else { - set tp $typespec - set member_optional 0 - } - if {$tp eq "literal"} { - set c $elementname - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set c $match - } else { - if {$td eq ""} { - set c $I$tp$NI - } else { - set c $td - } - } - if {$member_optional} { - append clause " " "(?$c?)" - } else { - append clause " " $c - } - } - set clause [string trimleft $clause] - } - - set ARGD [dict create argname $argname class leader] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - #set display "?$I$argname$NI?..." - set display "?$clause?..." - } else { - set display "?$clause?" - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "?[lindex [dict get $arginfo -choices] 0]?" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display "?$argname?" - #} else { - # set display "?$I$argname$NI?" - #} - } - } else { - if {[dict get $arginfo -multiple]} { - #set display "$I$argname$NI ?$I$argname$NI?..." - set display "$clause ?$clause?..." - } else { - set display $clause - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "[lindex [dict get $arginfo -choices] 0]" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - #} else { - # set display "$I$argname$NI" - #} - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD multiple [dict get $arginfo -multiple] - dict set ARGD display $display - - #dict lappend SYND $f $ARGD - lappend FORMARGS $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {$tp eq "none"} { - #assert - argname may have aliases delimited by | - but no aliases end with = - #(disallowed in punk::args::define) - set argdisplay $argname - } else { - #assert [llength $tp] == 1 (multiple values for flag unspported in punk::args::define) - if {[string match {\?*\?} $tp]} { - set tp [string range $tp 1 end-1] - set value_is_optional true - } else { - set value_is_optional false - } - - - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - set tp_display $ts - #user may or may not have remembered to match the typesynopsis with the optionality by wrapping with ? - #review - if user wrapped with ?*? and also leading/trailing ANSI - we won't properly strip - #todo - enforce no wrapping '?*?' in define for -typesynopsis? - set tp_display [string trim $tp_display ?] - } else { - - set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_alternative [split $tp |] { - #-type literal not valid for opt - review - if {[string match literal(*) $tp_alternative]} { - set match [string range $tp_alternative 8 end-1] - lappend alternates $match - } elseif {[string match literalprefix(*) $tp_alternative]} { - set match [string range $tp_alternative 14 end-1] - lappend alternates $match - } else { - lappend alternates <$I$tp_alternative$NI> - } - } - #todo - trie prefixes display? - set alternates [punk::args::lib::lunique $alternates] - set tp_display [join $alternates |] - } - if {[string first | $tp_display] >=0} { - #need to bracket alternate-types to distinguish pipes delimiting flag aliases - set tp_display "($tp_display)" - } - set argdisplay "" - foreach aliasflag [split $argname |] { - if {[string match --* $aliasflag]} { - if {[string index $aliasflag end] eq "="} { - set alias [string range $aliasflag 0 end-1] - if {$value_is_optional} { - append argdisplay "$alias$IS?$NIS=$tp_display$IS?$NIS|" - } else { - append argdisplay "$alias=$tp_display|" - } - } else { - if {$value_is_optional} { - append argdisplay "$aliasflag $IS?$NIS$tp_display$IS?$NIS|" - } else { - append argdisplay "$aliasflag $tp_display|" - } - } - } else { - if {$value_is_optional} { - #single dash flag can't accept optional value - append argdisplay "$aliasflag|" - } else { - append argdisplay "$aliasflag $tp_display|" - } - } - } - set argdisplay [string trimright $argdisplay |] - } - - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - set display "?$argdisplay?..." - } else { - set display "?$argdisplay?" - } - } else { - if {[dict get $arginfo -multiple]} { - set display "$argdisplay ?$argdisplay?..." - } else { - set display $argdisplay - } - } - - #if {[string index $argname end] eq "="} { - # set __ "" - #} else { - # set __ " " - #} - #if {[dict get $arginfo -optional]} { - # if {[dict get $arginfo -multiple]} { - # if {$tp eq "none"} { - # set display "?$argname?..." - # } else { - # set display "?$argname$__$tp_display?..." - # } - # } else { - # if {$tp eq "none"} { - # set display "?$argname?" - # } else { - # set display "?$argname$__$tp_display?" - # } - # } - #} else { - # if {[dict get $arginfo -multiple]} { - # if {$tp eq "none"} { - # set display "$argname ?$argname...?" - # } else { - # set display "$argname$__$tp_display ?$argname$__$tp_display?..." - # } - # } else { - # if {$tp eq "none"} { - # set display $argname - # } else { - # set display "$argname$__$tp_display" - # } - # } - #} - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD multiple [dict get $arginfo -multiple] - dict set ARGD display $display - #dict lappend SYND $f $ARGD - lappend FORMARGS $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set typelist [dict get $arginfo -type] - if {[llength $typelist] == 1} { - set tp [lindex $typelist 0] - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - #set arg_display [dict get $arginfo -typesynopsis] - set clause $ts - } else { - #set arg_display $argname - set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_alternative [split $tp |] { - if {$tp_alternative eq "literal"} { - lappend alternates [lindex $argname end] - } elseif {[string match literal(*) $tp_alternative]} { - set match [string range $tp_alternative 8 end-1] - lappend alternates $match - } elseif {[string match literalprefix(*) $tp_alternative]} { - set match [string range $tp_alternative 14 end-1] - lappend alternates $match - } else { - lappend alternates $I$argname$NI - } - } - #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) - #todo - trie prefixes display - set alternates [punk::args::lib::lunique $alternates] - set clause [join $alternates |] - } - } else { - set n [expr {[llength $typelist]-1}] - set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types - set clause "" - set ts [Dict_getdef $arginfo -typesynopsis ""] - if {$ts ne ""} { - set tp_displaylist $ts - } else { - set tp_displaylist [lrepeat [llength $typelist] ""] - } - - foreach typespec $typelist td $tp_displaylist elementname $name_tail { - #elementname will commonly be empty - if {[string match {\?*\?} $typespec]} { - set tp [string range $typespec 1 end-1] - set member_optional 1 - } else { - set tp $typespec - set member_optional 0 - } - #handle alternate-types e.g literal(text)|literal(binary) - set alternates [list] - foreach tp_alternative [split $tp |] { - if {$tp_alternative eq "literal"} { - lappend alternates $elementname - } elseif {[string match literal(*) $tp_alternative]} { - set match [string range $tp_alternative 8 end-1] - lappend alternates $match - } elseif {[string match literalprefix(*) $tp_alternative]} { - set match [string range $tp_alternative 14 end-1] - lappend alternates $match - } else { - if {$td eq ""} { - lappend alternates $I$tp$NI - } else { - lappend alternates $td - } - } - } - set alternates [punk::args::lib::lunique $alternates] - set c [join $alternates |] - if {$member_optional} { - append clause " " "(?$c?)" - } else { - append clause " " $c - } - } - set clause [string trimleft $clause] - } - - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - #set display "?$I$argname$NI?..." - set display "?$clause?..." - } else { - set display "?$clause?" - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "?[lindex [dict get $arginfo -choices] 0]?" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display "?$argname?" - #} else { - # set display "?$I$argname$NI?" - #} - } - } else { - if {[dict get $arginfo -multiple]} { - #set display "$I$argname$NI ?$I$argname$NI?..." - set display "$clause ?$clause?..." - } else { - set display $clause - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "[lindex [dict get $arginfo -choices] 0]" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - #} else { - # set display "$I$argname$NI" - #} - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD multiple [dict get $arginfo -multiple] - dict set ARGD display $display - #dict lappend SYND $f $ARGD - lappend FORMARGS $ARGD - } - - #accepts unnamed extra arguments e.g toplevel docid for ensembles and ensemble-like commands - if {[dict get $forminfo VAL_UNNAMED]} { - set display "?...?" - append syn " $display" - set ARGD [dict create argname "" class value] - dict set ARGD type any - dict set ARGD optional 1 - dict set ARGD multiple 1 - dict set ARGD display $display - lappend FORMARGS $ARGD - } - append syn \n - dict set SYND FORMS $f $FORMARGS - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - set FORMS [dict get $SYND FORMS] - dict for {form arglist} $FORMS { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::ensemble_subcommands_definition - @cmd -name ::punk::args::ensemble_subcommands_definition -help\ - "Helper function to return a punk::args definition snippet for ensemble subcommands" - @leaders -max 0 -min 0 - -groupdict -default {} -type dict -help\ - "Dictionary keyed on arbitrary groupname, where value - is a list of known subcommands that should be displayed - by groupname. Each groupname forms the title of a subtable - in the choices list. - Subcommands not assigned to a groupname will appear first - in an untitled subtable." - -columns -default 4 -type integer -help\ - "Max number of columns for all subtables in the choices - display area" - @values -min 1 -max 1 - ensemble -optional 0 -help\ - "Name of ensemble command" - - }] - proc ensemble_subcommands_definition {args} { - #args manually parsed - with use of argdef for unhappy-path only - if {![llength $args]} { - punk::args::parse $args -errorstyle minimal withid ::punk::args::ensemble_subcommands_definition - return - } - set ensemble [lindex $args end] - set optlist [lrange $args 0 end-1] - if {[llength $optlist] % 2} { - punk::args::parse $args -errorstyle minimal withid ::punk::args::ensemble_subcommands_definition - return - } - set defaults [dict create\ - -groupdict {}\ - -columns 4\ - ] - set optlist [dict merge $defaults $optlist] - dict for {k v} $optlist { - switch -- $k { - -groupdict - -columns {} - default { - punk::args::parse $args -errorstyle minimal withid ::punk::args::ensemble_subcommands_definition - return - } - } - } - set opt_groupdict [dict get $optlist -groupdict] - set opt_columns [dict get $optlist -columns] - - #warning - circular package dependency if we try to use this function on punk::ns! - package require punk::ns - set subdict [punk::ns::ensemble_subcommands -return dict $ensemble] - set allsubs [dict keys $subdict] - # ---------------------------------------------- - # manually defined group members may have subcommands that are obsoleted/missing - # we choose to make the situation obvious by re-classifying into a corresponding group with the " - MISSING" suffix - set checked_groupdict [dict create] - dict for {g members} $opt_groupdict { - set validmembers {} - set invalidmembers {} - foreach m $members { - if {$m in $allsubs} { - lappend validmembers $m - } else { - lappend invalidmembers $m - } - } - dict set checked_groupdict $g $validmembers - if {[llength $invalidmembers]} { - dict set checked_groupdict "${g}_MISSING" $invalidmembers - } - } - if {[dict exists $checked_groupdict ""]} { - set others [dict get $checked_groupdict ""] - dict unset checked_groupdict "" - } else { - set others [list] - } - - #REVIEW - set debug 0 - if {$debug} { - puts "punk::args::ensemble_subcommands_definition" - if {[catch { - ::punk::lib::pdict checked_groupdict - } msg]} { - puts stderr "punk::args::ensemble_subcommands_definition Cannot call pdict\n$msg" - } - puts -------------------- - puts "$checked_groupdict" - puts -------------------- - } - - set opt_groupdict $checked_groupdict - # ---------------------------------------------- - set allgrouped [list] - dict for {g members} $opt_groupdict { - lappend allgrouped {*}$members - } - set choiceinfodict [dict create] - set choicelabelsdict [dict create] - foreach {sc cmd} $subdict { - if {$sc ni $allgrouped} { - if {$sc ni $others} { - lappend others $sc - } - } - - #don't use full cmdinfo if $cmd is a single element - if {[llength $cmd] == 1} { - set cinfo [punk::ns::cmdwhich $cmd] - set tp [dict get $cinfo whichtype] - } else { - puts stderr "WARNING ==ensemble_subcommands_definition== cmdinfo $cmd\n$cinfo" - set cinfo [punk::ns::cmdinfo {*}$cmd] - set tp [dict get $cinfo cmdtype] - } - - dict set choiceinfodict $sc [list [list resolved $cmd]] - - switch -- $tp { - ensemble - native { - dict lappend choiceinfodict $sc [list doctype $tp] - } - object { - dict lappend choiceinfodict $sc [list doctype oo] - } - } - - - - #could be more than one punk::args id - choose a precedence by how we order the id_exists checks. - set id_checks [list\ - "$ensemble $sc"\ - $cmd\ - [dict get $cinfo origin]\ - ] - foreach checkid $id_checks { - if {[punk::args::id_exists $checkid]} { - dict lappend choiceinfodict $sc {doctype punkargs} - dict lappend choiceinfodict $sc [list subhelp {*}$checkid] - #dict set choicelabelsdict $sc [punk::ansi::a+ normal][punk::ns::synopsis $checkid][punk::ansi::a] - dict set choicelabelsdict $sc [punk::ansi::a+ normal][punk::args::synopsis $checkid][punk::ansi::a] - break - } - } - - #if {[punk::args::id_exists [list $ensemble $sc]]} { - # dict lappend choiceinfodict $sc {doctype punkargs} - # dict lappend choiceinfodict $sc [list subhelp {*}$ensemble $sc] - #} elseif {[punk::args::id_exists $cmd]} { - # dict lappend choiceinfodict $sc {doctype punkargs} - # dict lappend choiceinfodict $sc [list subhelp {*}$cmd] - #} elseif {[punk::args::id_exists [dict get $cinfo origin]]} { - # dict lappend choiceinfodict $sc {doctype punkargs} - # dict lappend choiceinfodict $sc [list subhelp {*}[dict get $cinfo origin]] - #} - - #foreach id [punk::args::get_ids "::package *"] { - # if {[llength $id] == 2} { - # lassign $id _ sub - # dict set PACKAGE_CHOICEINFO $sub {{doctype native} {doctype punkargs}} - # #override manual synopsis entry - # dict set PACKAGE_CHOICELABELS $sub [punk::ns::synopsis "::package $sub"] - # } - #} - - #if {[punk::args::id_exists [dict get $cinfo origin]] || [punk::args::id_exists [list $ensemble $sc]]} { - # dict lappend choiceinfodict $sc {doctype punkargs} - #} - } - - set argdef "" - append argdef "subcommand -choicegroups \{" \n - append argdef " \"\" \{$others\}" \n - dict for {g members} $opt_groupdict { - append argdef " \"$g\" \{$members\}" \n - } - append argdef " \} -choicecolumns $opt_columns -choicelabels {$choicelabelsdict} -choiceinfo {$choiceinfodict}" \n - - #todo -choicelabels - #detect subcommand further info available e.g if oo or ensemble or punk::args id exists.. - #consider a different mechanism to add a label on rhs of same line as choice (for (i) marker) - - return $argdef - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #tcl86 compat for string is dict - but without -strict or -failindex options - if {[catch {string is dict {}} errM]} { - proc string_is_dict {args} { - #ignore opts - set str [lindex $args end] - if {[catch {llength $str} len]} { - return 0 - } - if {$len % 2 == 0} { - return 1 - } - return 0 - } - } else { - proc string_is_dict {args} { - string is dict {*}$args - } - } - - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - classmethod { - lappend marks [punk::ns::Cmark classmethod term-orange1] - } - coremethod { - lappend marks [punk::ns::Cmark coremethod term-plum1] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - objectmethod { - lappend marks [punk::ns::Cmark objectmethod term-orange1] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr\ - -summary\ - "Templating with placeholders such as: \$\{$varName\}"\ - -help\ - "Roughly analogous to js template literals - - Placeholder Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - #JJJ - #REVIEW - #lappend params [subst -nocommands -novariables $expression] - lappend params $expression - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - }] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #order-preserving - #(same as punk::lib) - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::args [tcl::namespace::eval punk::args { - tcl::namespace::path {::punk::args::lib ::punk::args::system} - variable pkg punk::args - variable version - set version 0.2 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/iocp-2.0.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/iocp-2.0.2.tm new file mode 100644 index 00000000..33e43d17 --- /dev/null +++ b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/iocp-2.0.2.tm @@ -0,0 +1,144 @@ +# -*- 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: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.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) 2026 +# +# @@ Meta Begin +# Application punk::args::moduledoc::iocp 2.0.2 +# Meta platform tcl +# Meta license BSD +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +package require Tcl 8.6- + + + +tcl::namespace::eval punk::args::moduledoc::iocp { + variable PUNKARGS + + namespace eval argdoc { + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + variable PUNKARGS + variable PUNKARGS_aliases + + lappend PUNKARGS_aliases {::iocp::inet::socket ::socket} + + lappend PUNKARGS [list { + @id -id "(package)punk::args::moduledoc::iocp" + @package -name "punk::args::moduledoc::iocp" -help\ + "punk::args documentation for magicsplat iocp package" + }] + + } + +} + + + +# == === === === === === === === === === === === === === === +# Sample 'about' function with punk::args documentation +# == === === === === === === === === === === === === === === +tcl::namespace::eval punk::args::moduledoc::iocp { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + + namespace eval argdoc { + #namespace for custom argument documentation + proc package_name {} { + return punk::args::moduledoc::iocp + } + proc about_topics {} { + #info commands results are returned in an arbitrary order (like array keys) + set topic_funs [info commands [namespace current]::get_topic_*] + set about_topics [list] + foreach f $topic_funs { + set tail [namespace tail $f] + lappend about_topics [string range $tail [string length get_topic_] end] + } + #Adjust this function or 'default_topics' if a different order is required + return [lsort $about_topics] + } + proc default_topics {} {return [list Description *]} + + # ------------------------------------------------------------- + # get_topic_ functions add more to auto-include in about topics + # ------------------------------------------------------------- + proc get_topic_Description {} { + punk::args::lib::tstr [string trim { + package punk::args::moduledoc::iocp + } \n] + } + proc get_topic_License {} { + return "BSD" + } + proc get_topic_Version {} { + return "$::punk::args::moduledoc::iocp::version" + } + proc get_topic_Contributors {} { + set authors {} + set contributors "" + foreach a $authors { + append contributors $a \n + } + if {[string index $contributors end] eq "\n"} { + set contributors [string range $contributors 0 end-1] + } + return $contributors + } + # ------------------------------------------------------------- + } + + # we re-use the argument definition from punk::args::standard_about and override some items + set overrides [dict create] + dict set overrides @id -id "::punk::args::moduledoc::iocp::about" + dict set overrides @cmd -name "punk::args::moduledoc::iocp::about" + dict set overrides @cmd -help [string trim [punk::args::lib::tstr { + About punk::args::moduledoc::iocp + }] \n] + dict set overrides topic -choices [list {*}[punk::args::moduledoc::iocp::argdoc::about_topics] *] + dict set overrides topic -choicerestricted 1 + dict set overrides topic -default [punk::args::moduledoc::iocp::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict + set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *] + lappend PUNKARGS [list $newdef] + proc about {args} { + package require punk::args + #standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on + set argd [punk::args::parse $args withid ::punk::args::moduledoc::iocp::about] + lassign [dict values $argd] _leaders opts values _received + punk::args::package::standard_about -package_about_namespace ::punk::args::moduledoc::iocp::argdoc {*}$opts {*}[dict get $values topic] + } +} +# end of sample 'about' function +# == === === === === === === === === === === === === === === + + +# ----------------------------------------------------------------------------- +# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked +# ----------------------------------------------------------------------------- +# variable PUNKARGS +# variable PUNKARGS_aliases +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + lappend ::punk::args::register::NAMESPACES ::punk::args::moduledoc::iocp ::punk::args::moduledoc::iocp::argdoc +} +# ----------------------------------------------------------------------------- + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::args::moduledoc::iocp [tcl::namespace::eval punk::args::moduledoc::iocp { + variable pkg punk::args::moduledoc::iocp + variable version + set version 2.0.2 +}] +return + diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/tkcore-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/tkcore-0.1.1.tm index bbbb6a81..31db5f77 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/tkcore-0.1.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/tkcore-0.1.1.tm @@ -368,7 +368,6 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- lappend PUNKARGS_aliases {::button ::tk::button} punk::args::define { - @dynamic @id -id ::tk::button @cmd -name "Tk Builtin: tk::button"\ -summary\ @@ -392,8 +391,7 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { @opts -type string -parsekey "" -group "STANDARD OPTIONS" -grouphelp\ "" - }\ - { + } { ${[punk::args::resolved_def -types opts (default)::punk::args::moduledoc::tkcore::tk_standardoptions\ -activebackground\ -activeforeground\ @@ -419,8 +417,8 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { -textvariable\ -underline\ -wraplength\ - ]}}\ - { + ]} + } { @opts -type string -parsekey "" -group "WIDGET-SPECIFIC OPTIONS" -grouphelp\ "" -command -type script -help\ @@ -508,7 +506,8 @@ tcl::namespace::eval punk::args::moduledoc::tkcore { dict set CLASS_BUTTON_CHOICEINFO $sub {{doctype native} {doctype punkargs}} #override manual synopsis entry #puts stderr "override manual synopsis entry with [punk::ns::synopsis "::package $sub"]" - dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::ns::synopsis "(widgetcommand)Class_Button $sub"] + #dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::ns::synopsis "(widgetcommand)Class_Button $sub"] + dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::args::synopsis "(widgetcommand)Class_Button $sub"] } } diff --git a/src/vfs/_vfscommon.vfs/modules/punk/cap/handlers/templates-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/cap/handlers/templates-0.1.0.tm index 7a6e4416..45327f96 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/cap/handlers/templates-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/cap/handlers/templates-0.1.0.tm @@ -628,7 +628,7 @@ namespace eval punk::cap::handlers::templates { #we will ignore any .tm files that don't have versions that tcl understands - but warn #this reduces the cases we have to test later set fname [file tail $tf] - lassign [split [punk::mix::cli::lib::split_modulename_version $fname]] mname ver + lassign [split [punk::mix::util::split_modulename_version $fname]] mname ver if {[catch {punk::mix::cli::lib::validate_modulename $mname} errM]} { puts stderr "Invalid module name/version $tf - please rename with standard Tcl .tm module name and version (or leave out version)" if {[string match *-* $mname]} { diff --git a/src/vfs/_vfscommon.vfs/modules/punk/cesu-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/cesu-0.1.1.tm index 872cf36e..416bbed6 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/cesu-0.1.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/cesu-0.1.1.tm @@ -128,11 +128,12 @@ tcl::namespace::eval punk::cesu { #binary scan $4 c 4 incr 1 ;#// Effectively adds 0x10000 to the codepoint ? - return [binary format ccca \ - [expr {0xF0 | (($1 & 0xC) >> 2)}] \ - [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] \ - [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] \ - $4] + return [binary format ccca {*}{ + } [expr {0xF0 | (($1 & 0xC) >> 2)}] {*}{ + } [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] {*}{ + } [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] {*}{ + } $4 + ] } # @@ -147,14 +148,15 @@ tcl::namespace::eval punk::cesu { #binary scan $4 c 4 incr 1 - return [binary format ccca \ - [expr {0xF0 | (($1 & 0xC) >> 2)}] \ - [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] \ - [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] \ - $4] + return [binary format ccca {*}{ + } [expr {0xF0 | (($1 & 0xC) >> 2)}] {*}{ + } [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] {*}{ + } [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] {*}{ + } $4 + ] } else { - puts "Invalid sequence: $char" + puts "Invalid sequence: $char" return $char } } diff --git a/src/vfs/_vfscommon.vfs/modules/punk/char-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/char-0.1.0.tm index 0bdc60a0..53ef8ec1 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/char-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/char-0.1.0.tm @@ -2468,10 +2468,10 @@ tcl::namespace::eval punk::char { #short-circuit basic cases #support tcl pre 2023-11 - see regexp bug below - #if {![regexp {[\uFF-\U10FFFF]} $text]} { + #if {![regexp {[\u100-\U10FFFF]} $text]} { # return [tcl::string::length $text] #} - if {![regexp "\[\uFF-\U10FFFF\]" $text]} { + if {![regexp "\[\u100-\U10FFFF\]" $text]} { return [tcl::string::length $text] #punk::char::wcswidth has to split and examine dec value of each code #By stripping controls + 7F (leaving tab) we've already eliminated the non-printable ascii - REVIEW diff --git a/src/vfs/_vfscommon.vfs/modules/punk/config-0.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/config-0.1.tm index 43edd6a3..61885dcc 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/config-0.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/config-0.1.tm @@ -563,7 +563,7 @@ tcl::namespace::eval punk::config { lappend PUNKARGS [list { @dynamic @id -id ::punk::config::show - @cmd -name punk::config::get -help\ + @cmd -name punk::config::show -help\ "Display configuration values from a config. Accepts globs eg XDG*" @leaders -min 1 -max 1 diff --git a/src/vfs/_vfscommon.vfs/modules/punk/console-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/console-0.1.1.tm index 5271bbfb..b7c4cd7a 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/console-0.1.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/console-0.1.1.tm @@ -44,7 +44,14 @@ #[list_begin itemized] package require Tcl 8.6- + +#---------------------------------------------------- +#Although we need to be in an environment with Thread available to use punk::console, +# we don't want to require Thread as a hard dependency in the interp we're running in. +# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp. +#TODO. package require Thread ;#tsv required to sync is_raw +#---------------------------------------------------- package require punk::ansi package require punk::args #*** !doctools @@ -282,7 +289,7 @@ namespace eval punk::console { ignore the regex match 'ok' response and keep going." -return -type string -default payload -choices {payload dict} -choicelabels { - dict\ + dict "dict with keys prefix,response,payload,all" } -help\ "Return format" @@ -290,12 +297,12 @@ namespace eval punk::console { -console -default {stdin stdout} -type list -help\ "console/terminal (currently list of in/out channels) (todo - object?)" -passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels { - none\ + none { ANSI sent without any passthrough wrapping. A terminal multiplexer such as tmux,screen,zellij may not pass the request through to the underlying terminal(s) This is the recommended/normal value for the option.} - tmux\ + tmux { Wrap ANSI sequence with tmux passthrough sequence. \x1bPtmux\;\x1b\\ Note that a tmux session could be connected to multiple @@ -304,7 +311,7 @@ namespace eval punk::console { Passthrough should generally be avoided except for debug/test purposes. } - auto\ + auto { Use existence of ::env(TMUX) to detect tmux and send tmux passthrough sequence. Not recommended except for debug/test purposes. @@ -1012,6 +1019,7 @@ namespace eval punk::console { #e.g \033\[46;1R set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$} ;#must capture prefix,entire-response,response-payload + #This is all 'tput u7' does (emits CSI 6n on stdout) set request "\033\[6n" set payload [punk::console::internal::get_ansi_response_payload -console $inoutchannels $request $capturingregex] #some terminals fail to respond properly to \x1b\[6n but do respond to \x1b\[?6n and vice-versa :/ @@ -1021,6 +1029,9 @@ namespace eval punk::console { return $payload } + + + proc get_checksum_rect {id page t l b r {inoutchannels {stdin stdout}}} { #e.g \x1b\[P44!~E797\x1b\\ #re e.g {(.*)(\x1b\[P44!~([[:alnum:]])\x1b\[\\)$} @@ -1295,7 +1306,7 @@ namespace eval punk::console { default {set keyboard_name "unknown"} } - return [dict create {*} { + return [dict create {*}{ } class $class_name {*}{ } version $version {*}{ } keyboard $keyboard_name {*}{ @@ -1522,17 +1533,114 @@ namespace eval punk::console { } #todo - determine cursor on/off state before the call to restore properly. + variable get_size_mechanism + set get_size_mechanism [dict create] + proc get_size {{inoutchannels {stdin stdout}}} { lassign $inoutchannels in out - #we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810 - #chan eof is faster whether chan exists or not than - if {[catch {chan eof $out} is_eof]} { - error "punk::console::get_size output channel $out seems to be closed ([info level 1])" - } else { - if {$is_eof} { - error "punk::console::get_size eof on output channel $out ([info level 1])" + set tried_mechlist [list] + + #fastest mechanism if available - use Tcl's inbuilt -winsize key from chan configure if available - this is much faster than any ANSI mechanism + #unknown which platforms support this. + if {![catch {get_size_using_chanconfigure $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist "chanconfigure" + + + variable is_vt52 + if {$is_vt52} { + #vt52 doesn't support cursor save/restore or cursor position reports. + if {![catch {get_size_using_tput $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist "tput" + error "can't get console size. Tried mechanisms: $mechlist" + } + + variable get_size_mechanism ;#dict keyed on terminal ident. (currently just list of inoutchannels but may be something else in future such as terminal object or ident string) + if {![dict exists $get_size_mechanism $inoutchannels]} { + set try_order [list] + #call each mechanism once to see if it works - and to ensure we don't include initial run in our timings. + #we will also use the results for our initial return of the size. + set successful_mechs [list] + set sizedict [dict create] + if {![catch {get_size_using_cursorrestore $inoutchannels} result]} { + lappend successful_mechs "cursorrestore" + if {![dict size $sizedict]} { + set sizedict $result + } + } + if {![catch {get_size_using_cursormove $inoutchannels} result]} { + lappend successful_mechs "cursormove" + if {![dict size $sizedict]} { + set sizedict $result + } } + if {![catch {get_size_using_tput $inoutchannels} result] } { + lappend successful_mechs "tput" + if {![dict size $sizedict]} { + set sizedict $result + } + } + set timings [list] + set t_ms 250 ;#default timerate is 1000ms - we are trying at least 3 mechanisms so 1000ms is a bit long for this test as it can slow down the first call to get_size significantly. + foreach sm $successful_mechs { + catch { + set timing_result [timerate {get_size_using_$sm $inoutchannels} $t_ms] + set micros [expr {int([lindex $timing_result 0])}] + lappend timings [list $micros $sm] + } + } + set sorted [lsort -integer -index 0 $timings] + if {[llength $sorted] > 0} { + set try_order [lmap t $sorted {lindex $t 1}] + } else { + set try_order [list] + } + dict set get_size_mechanism $inoutchannels $try_order + return $sizedict + } + + foreach mech [dict get $get_size_mechanism $inoutchannels] { + if {![catch {get_size_using_$mech $inoutchannels} sizedict]} { + return $sizedict + } + lappend tried_mechlist $mech } + + #if {![catch {get_size_using_cursorrestore $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "cursorrestore" + + #if {![catch {get_size_using_cursormove $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "cursormove" + + #if {![catch {get_size_using_tput $inoutchannels} sizedict]} { + # return $sizedict + #} + #lappend tried_mechlist "tput" + + error "can't get console size. Tried mechanisms: $tried_mechlist" + } + + proc get_size_using_chanconfigure {{inoutchannels {stdin stdout}}} { + set out [lindex $inoutchannels 1] + set outconf [chan configure $out] + if {[dict exists $outconf -winsize]} { + #this mechanism is much faster than ansi cursor movements + #REVIEW check if any x-platform anomalies with this method? + #can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least + lassign [dict get $outconf -winsize] cols lines + if {[string is integer -strict $cols] && [string is integer -strict $lines]} { + return [dict create columns $cols rows $lines] + } + } + error "chan configure method of getting console size not supported or failed to get valid size info" + #we don't need to care about the input channel if chan configure on the output can give us the info. #short circuit ansi cursor movement method if chan configure supports the -winsize value set outconf [chan configure $out] @@ -1542,43 +1650,45 @@ namespace eval punk::console { #can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least lassign [dict get $outconf -winsize] cols lines if {[string is integer -strict $cols] && [string is integer -strict $lines]} { - return [list columns $cols rows $lines] + return [dict create columns $cols rows $lines] } #continue on to ansi mechanism if we didn't get 2 ints } - if {[catch {chan eof $in} is_eof]} { - error "punk::console::get_size input channel $in seems to be closed ([info level 1])" + } + + proc get_size_using_tput {{inoutchannels {stdin stdout}}} { + set tputcmd [auto_execok tput] + if {$tputcmd eq ""} { + error "tput command not found - cannot use tput method to get console size" + } + lassign [exec {*}$tputcmd lines cols] lines cols + return [dict create columns $cols rows $lines] + } + + proc get_size_using_cursormove {{inoutchannels {stdin stdout}}} { + set out [lindex $inoutchannels 1] + #we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810 + #chan eof is faster whether chan exists or not than + if {[catch {chan eof $out} is_eof]} { + error "punk::console::get_size_using_cursormove output channel $out seems to be closed ([info level 1])" } else { if {$is_eof} { - error "punk::console::get_size eof on input channel $in ([info level 1])" + error "punk::console::get_size_using_cursormove eof on output channel $out ([info level 1])" } } - #keep out of catch - no point in even trying a restore move if we can't get start position - just fail here. - #no vt52 equiv? may as well strip all vt52 from here? lassign [get_cursor_pos_list $inoutchannels] start_row start_col - variable is_vt52 - if {!$is_vt52} { - set movefunc "punk::ansi::move" - set func_coff "punk::ansi::cursor_off" - set func_con "punk::ansi::cursor_on" - } else { - set movefunc "punk::ansi::vt52move" - set func_coff "punk::ansi::vt52cursor_off" - set func_con "punk::ansi::vt52cursor_on" - } if {[catch { #some terminals (conemu on windows) scroll the viewport when we make a big move down like this - a move to 1 1 immediately after cursor_save doesn't seem to fix that. #This issue also occurs when switching back from the alternate screen buffer - so perhaps that needs to be addressed elsewhere. - - puts -nonewline $out [$func_coff][$movefunc 2000 2000] + puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::move 2000 2000] lassign [get_cursor_pos_list $inoutchannels] lines cols - puts -nonewline $out [$movefunc $start_row $start_col][$func_con];flush stdout - set result [list columns $cols rows $lines] + puts -nonewline $out [punk::ansi::move $start_row $start_col][punk::ansi::cursor_on];flush stdout + set result [dict create columns $cols rows $lines] } errM]} { - puts -nonewline $out [$movefunc $start_row $start_col] - puts -nonewline $out [$func_con] + puts -nonewline $out [punk::ansi::move $start_row $start_col] + puts -nonewline $out [punk::ansi::cursor_on] error "$errM" } else { return $result @@ -1586,14 +1696,16 @@ namespace eval punk::console { } #faster than get_size when it is using ansi mechanism - but uses cursor_save - which we may want to avoid if calling during another operation which uses cursor save/restore - proc get_size_cursorrestore {{inoutchannels {stdin stdout}}} { + proc get_size_using_cursorrestore {{inoutchannels {stdin stdout}}} { lassign $inoutchannels in out - #we use the same shortcircuit mechanism as get_size to avoid ansi at all if the output channel will give us the info directly set outconf [chan configure $out] - if {[dict exists $outconf -winsize]} { - lassign [dict get $outconf -winsize] cols lines - if {[string is integer -strict $cols] && [string is integer -strict $lines]} { - return [list columns $cols rows $lines] + #don't use shortcut mechanisms - this function is intended to specificall use the cursor_save/restore method + + if {[catch {chan eof $out} is_eof]} { + error "punk::console::get_size_using_cursorrestore output channel $out seems to be closed ([info level 1])" + } else { + if {$is_eof} { + error "punk::console::get_size_using_cursorrestore eof on output channel $out ([info level 1])" } } @@ -1603,7 +1715,7 @@ namespace eval punk::console { puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::cursor_save_dec][punk::ansi::move 2000 2000] lassign [get_cursor_pos_list $inoutchannels] lines cols puts -nonewline $out [punk::ansi::cursor_restore][punk::console::cursor_on];flush $out - set result [list columns $cols rows $lines] + set result [dict create columns $cols rows $lines] } errM]} { puts -nonewline $out [punk::ansi::cursor_restore_dec] puts -nonewline $out [punk::ansi::cursor_on] @@ -1612,10 +1724,15 @@ namespace eval punk::console { return $result } } + + proc get_dimensions {{inoutchannels {stdin stdout}}} { lassign [get_size $inoutchannels] _c cols _l lines return "${cols}x${lines}" } + + + #the (xterm?) CSI 18t query is supported by *some* terminals proc get_xterm_size {{inoutchannels {stdin stdout}}} { set capturingregex {(.*)(\x1b\[8;([0-9]+;[0-9]+)t)$} ;#must capture prefix,entire-response,response-payload @@ -2669,6 +2786,7 @@ namespace eval punk::console { This allows querying the current style and then re-setting it after temporarily changing it." }] } + proc cursor_style {args} { set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style] lassign [dict values $argd] leaders opts values diff --git a/src/vfs/_vfscommon.vfs/modules/punk/du-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/du-0.1.0.tm index e67acfda..8dd91089 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/du-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/du-0.1.0.tm @@ -1641,7 +1641,7 @@ namespace eval punk::du { #convert time from windows (100ns units since jan 1, 1601) to Tcl time (seconds since Jan 1, 1970) #We lose some precision by not passing the boolean to the large_system_time_to_secs_since_1970 function which returns fractional seconds #but we need to maintain compatibility with other platforms and other tcl functions so if we want to return more precise times we will need another flag and/or result dict - dict set alltimes $fullname [dict create {*} { + dict set alltimes $fullname [dict create {*}{ } c [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo ctime]] {*}{ } a [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo atime]] {*}{ } m [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo mtime]] {*}{ @@ -1947,15 +1947,15 @@ namespace eval punk::du { #e.g we can populate compsizes for files (compressed size) proc du_dirlisting_zipfs {folderpath args} { puts stderr "zipfs: $folderpath" - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] @@ -2114,15 +2114,15 @@ namespace eval punk::du { } proc du_dirlisting_tclvfs {folderpath args} { - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] diff --git a/src/vfs/_vfscommon.vfs/modules/punk/fileline-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/fileline-0.1.0.tm deleted file mode 100644 index ce47841f..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/fileline-0.1.0.tm +++ /dev/null @@ -1,1737 +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 punk::fileline 0.1.0 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::fileline 0 0.1.0] -#[copyright "2024"] -#[titledesc {file line-handling utilities}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk fileline}] [comment {-- Description at end of page heading --}] -#[require punk::fileline] -#[keywords module text parse file encoding BOM] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para]Utilities for in-memory analysis of text file data as both line data and byte/char-counted data whilst preserving the line-endings (even if mixed) -#[para]This is important for certain text files where examining the number of chars/bytes is important -#[para]For example - windows .cmd/.bat files need some byte counting to determine if labels lie on chunk boundaries and need to be moved. -#[para]This chunk-size counting will depend on the character encoding. -#[para]Despite including the word 'file', the library doesn't necessarily deal with reading/writing to the filesystem - -#[para]The raw data can be supplied as a string, or loaded from a file using punk::fileline::get_textinfo -file -#[subsection Concepts] -#[para]A chunk of textfile data (possibly representing a whole file - but usually at least a complete set of lines) is loaded into a punk::fileline::class::textinfo instance at object creation. -#[example_begin] -# package require punk::fileline -# package require fileutil -# set rawdata [lb]fileutil::cat data.txt -translation binary[rb] -# punk::fileline::class::textinfo create obj_data $rawdata -# puts stdout [lb]obj_data linecount[rb] -#[example_end] -#[subsection Notes] -#[para]Line records are referred to by a zero-based index instead of a one-based index as is commonly used when displaying files. -#[para]This is for programming consistency and convenience, and the module user should do their own conversion to one-based indexing for line display or messaging if desired. -#[para]No support for lone carriage-returns being interpreted as line-endings. -#[para]CR line-endings that are intended to be interpreted as such should be mapped to something else before the data is supplied to this module. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages needed by punk::fileline -#[list_begin itemized] - - package require Tcl 8.6- - package require punk::args - #*** !doctools - #[item] [package {Tcl 8.6-}] - #[item] [package {punk::args}] - - - # #package require frobz - # #*** !doctools - # #[item] [package {frobz}] - -#*** !doctools -#[list_end] [comment {- end dependencies list -}] - -#*** !doctools -#[subsection {optional dependencies}] -#[para] packages that add functionality but aren't strictly required -#[list_begin itemized] - - #*** !doctools - #[item] [package {punk::ansi}] - #[para] - recommended for class::textinfo [method chunk_boundary_display] - #[item] [package {punk::char}] - #[para] - recommended for class::textinfo [method chunk_boundary_display] - #[item] [package {overtype}] - #[para] - recommended for class::textinfo [method chunk_boundary_display] - - -#*** !doctools -#[list_end] [comment {- end optional dependencies list -}] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# oo::class namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::fileline::class { - namespace export * - #*** !doctools - #[subsection {Namespace punk::fileline::class}] - #[para] class definitions - if {[info commands [namespace current]::textinfo] 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}] - # } - - - #uses zero based indexing. Caller can add 1 for line numbers - oo::class create [namespace current]::textinfo { - #*** !doctools - #[enum] CLASS [class textinfo] - #[list_begin definitions] - # [para] [emph METHODS] - - variable o_chunk ;#current state - variable o_chunkop_store - variable o_lineop_store - - variable o_chunk_epoch - variable o_line_epoch - variable o_payloadlist - variable o_linemap - variable o_LF_C - variable o_CRLF_C - - - variable o_bom_id - variable o_bom - variable o_bom_map - - #review - for now we expect datachunk to be data without BOM and already encoded appropriately - #fileline::get_textinfo has support for interpreting BOM - but we currently have no way to do that for data not coming from a file - #refactor to allow that code to be called from here? - constructor {datachunk args} { - #*** !doctools - #[call class::textinfo [method constructor] [arg datachunk] [opt {option value...}]] - #[para] Constructor for textinfo object which represents a chunk or all of a file - #[para] datachunk should be passed with the file data including line-endings as-is for full functionality. ie use something like: - #[example_begin] - # chan configure $fd -translation binary - # set chunkdata [lb]read $fd[rb]] - #or - # set chunkdata [lb]fileutil::cat -translation binary[rb] - #[example_end] - #[para] when loading the data - namespace eval [namespace current] { - set nspath [namespace path] - foreach p [list ::punk::fileline ::punk::fileline::ansi] { - if {$p ni $nspath} { - lappend nspath $p - } - } - namespace path $nspath - } - - set o_bom_map [list\ - utf-8 \u00ef\u00bb\u00bf\ - utf-16be \u00fe\u00ff\ - utf-16le \u00ff\u00fe\ - utf-32be \u0000\u0000\u00fe\u00ff\ - utf-32le \u00ff\u00fe\u0000\u0000\ - utf-7 \u002b\u002f\u0076\ - utf-1 \u00f7\u0064\u004c\ - utf-ebcdic \u00dd\u0073\u0066\u0073\ - utf-scsu \u0003\u00fe\u00ff\ - utf-bocu-1 \u00fb\u00ee\u0028\ - utf-gb18030 \u0084\u0031\u0095\u0033\ - ] - set o_bom_id "" - set o_bom "" ;#review - - set o_chunk $datachunk - set o_line_epoch [list] - set o_chunk_epoch [list "fromchunkchange-at-[clock micros]"] - set crlf_lf_placeholders [list \uFFFF \uFFFE] ;#defaults - if already exist in file - error out with message - set defaults [dict create\ - -substitutionmap {}\ - -crlf_lf_placeholders $crlf_lf_placeholders\ - -userid ""\ - ] - set known_opts [dict keys $defaults] - foreach {k v} $args { - if {$k ni $known_opts} { - error "[self] constructor error: unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $args] - # -- --- --- --- --- --- --- - set opt_substitutionmap [dict get $opts -substitutionmap] ;#review - can be done by caller - or a loadable -policy - set opt_crlf_lf_placeholders [dict get $opts -crlf_lf_placeholders] - set opt_userid [dict get $opts -userid] - # -- --- --- --- --- --- --- - - if {[llength $opt_crlf_lf_placeholders] != 2 || [string length [lindex $opt_crlf_lf_placeholders 0]] !=1 || [string length [lindex $opt_crlf_lf_placeholders 1]] !=1} { - error "textinfo::constructor error: -crlf_lf_placeholders requires a list of exactly 2 chars" - } - lassign $opt_crlf_lf_placeholders o_LF_C o_CRLF_C - if {[string first $o_LF_C $o_chunk] >=0} { - set decval [scan $o_LF_C %c] - if {$decval < 32 || $decval > 127} { - set char_desc "(decimal value $decval)" - } else { - set char_desc "'$o_LF_C' (decimal value $decval)" - } - error "textinfo::constructor error: rawfiledata already contains linefeed substitution character $char_desc specified as first element of -crlf_lf_placeholders" - } - if {[string first $o_CRLF_C $o_chunk] >=0} { - set decval [scan $o_CRLF_C %c] - if {$decval < 32 || $decval > 127} { - set char_desc "(decimal value $decval)" - } else { - set char_desc "'$o_CRLF_C' (decimal value $decval)" - } - error "textinfo::constructor error: rawfiledata already contains carriagereturn-linefeed substitution character $char_desc specified as second element of -crlf_lf_placeholders" - } - if {$o_LF_C eq $o_CRLF_C} { - puts stderr "WARNING: same substitution character used for both elements of -crlf_lf_placeholders - byte counting may be off if file contains mixed line-endings" - } - - my regenerate_lines - - } - - method set_bomid {bomid} { - if {$bomid ni [dict keys $o_bom_map]} { - error "Unrecognised bom-id $bomid. Known values: [dict keys $o_bom_map]" - } - set o_bom_id $bomid - set o_bom [dict get $o_bom_map $bomid] - } - method get_bomid {} { - return $o_bom_id - } - method get_bom {} { - return $o_bom - } - - method chunk {chunkstart chunkend} { - #*** !doctools - #[call class::textinfo [method chunk] [arg chunkstart] [arg chunkend]] - #[para]Return a range of bytes from the underlying raw chunk data. - #[para] e.g The following retrieves the entire chunk - #[para] objName chunk 0 end - return [string range $o_chunk $chunkstart $chunkend] - } - method chunklen {} { - #*** !doctools - #[call class::textinfo [method chunklen]] - #[para] Number of bytes/characters in the raw data of the file - return [string length $o_chunk] - } - method chunk_boundary_display {chunkstart chunkend chunksize args} { - #*** !doctools - #[call class::textinfo [method chunk_boundary_display]] - #[para]Returns a string displaying the boundaries at chunksize bytes between chunkstart and chunkend - #[para]Defaults to using ansi colour if punk::ansi module is available. Use -ansi 0 to disable colour - set opts [dict create\ - -ansi $::punk::fileline::ansi::enabled\ - -offset 0\ - -displaybytes 200\ - -truncatedmark "..."\ - -completemark "---"\ - -moremark " + "\ - -continuemark " > "\ - -linemaxwidth 100\ - -linebase 0\ - -limit -1\ - -boundaries {}\ - -showconfig 0\ - -boundaryheader {Boundary %i% at %b%}\ - ] - foreach {k v} $args { - switch -- $k { - -ansi - -offset - -displaybytes - -truncatedmark - -completemark - -moremark - -continuemark - -linemaxwidth - -linebase - -limit - -boundaries - -showconfig - -boundaryheader { - dict set opts $k $v - } - default { - error "[self]::chunk_boundary error: unknown option '$k'. Known options: [dict keys $opts]" - } - } - } - # -- --- --- --- --- --- - set opt_ansi [dict get $opts -ansi] - set opt_offset [dict get $opts -offset] - set opt_displaybytes [dict get $opts -displaybytes] - set opt_tmark [dict get $opts -truncatedmark] - set opt_cmark [dict get $opts -completemark] - set opt_linemax [dict get $opts -linemaxwidth] - set opt_linebase [dict get $opts -linebase] - set opt_linebase [string map [list _ ""] $opt_linebase] - set opt_limit [dict get $opts -limit] ;#limit number of boundaries to display - set opt_boundaries [dict get $opts -boundaries] ;#use pre-calculated boundaries if supplied - set opt_showconfig [dict get $opts -showconfig] - set opt_boundaryheader [dict get $opts -boundaryheader] - # -- --- --- --- --- --- - package require overtype - # will require punk::char and punk::ansi - - if {"::punk::fileline::ansi::ansistrip" ne [info commands ::punk::fileline::ansi::ansistrip]} { - namespace eval ::punk::fileline::ansi { - namespace import ::punk::ansi::* - } - } - - #This mechanism for enabling/disabling ansi is a bit clumsy - prone to errors with regard to keeping in sync with any api changes in punk ansi - #It's done here to allow this to be used without the full set of punk modules and/or shell - REVIEW - - #risk of failing to reset on error - set pre_ansi_enabled $::punk::fileline::ansi::enabled - if {$opt_ansi} { - set ::punk::fileline::ansi::enabled 1 - } else { - set ::punk::fileline::ansi::enabled 0 - } - if {"::punk::fileline::ansistrip" ne [info commands ::punk::fileline::ansistrip]} { - proc ::punk::fileline::a {args} { - if {$::punk::fileline::ansi::enabled} { - tailcall ::punk::fileline::ansi::a {*}$args - } else { - return "" - } - } - proc ::punk::fileline::a+ {args} { - if {$::punk::fileline::ansi::enabled} { - tailcall ::punk::fileline::ansi::a+ {*}$args - } else { - return "" - } - } - proc ::punk::fileline::ansistrip {str} { - if {$::punk::fileline::ansi::enabled} { - tailcall ::punk::fileline::ansi::ansistrip $str - } else { - return $str - } - } - } - set maxline [lindex [my chunkrange_to_linerange $chunkend $chunkend] 0] - set minline [lindex [my chunkrange_to_linerange $chunkstart $chunkstart] 0] - - #suport simple end+-int (+-)start(+-)int to set linebase to line corresponding to chunkstart or chunkend - #also simple int+int and int-int - nothing more complicated (similar to Tcl lrange etc in that regard) - #commonly this will be something like -start or -end - if {![string is integer -strict $opt_linebase]} { - set sign "" - set errunrecognised "unrecognised -linebase value '$opt_linebase'. Expected positive or negative integer or -start -start-int -start+int -end -end-int -end+int or -eof (where leading - is optional but probably desirable) " - if {[string index $opt_linebase 0] eq "-"} { - set sign - - set tail [string range $opt_linebase 1 end] - } else { - set tail [string trimleft $opt_linebase +];#ignore + - } - #todo - switch -glob -- $tail - if {[string match eof* $tail]} { - set endmath [string range $tail 3 end] - #todo endmath? - if {$tail eq "eof"} { - set lastline [lindex [my chunkrange_to_linerange end end] 0] - set linebase ${sign}$lastline - } else { - error $errunrecognised - } - } elseif {[string match end* $tail]} { - set endmath [string range $tail 3 end] - if {[string length $endmath]} { - set op [string index $endmath 0] - if {$op in {+ -}} { - set operand [string range $endmath 1 end] - if {[string is integer -strict $operand]} { - if {$op eq "+"} { - set linebase [expr {$maxline + $operand}] - } else { - set linebase [expr {$maxline - $operand}] - } - } else { - error $errunrecognised - } - } else { - error $errunrecognised - } - } else { - set linebase $maxline - } - set linebase ${sign}$linebase - } elseif {[string match start* $tail]} { - set endmath [string range $tail 5 end] - if {[string length $endmath]} { - set op [string index $endmath 0] - if {$op in {+ -}} { - set operand [string range $endmath 1 end] - if {[string is integer -strict $operand]} { - if {$op eq "+"} { - set linebase [expr {$minline + $operand}] - } else { - set linebase [expr {$minline - $operand}] - } - } else { - error $errunrecognised - } - } else { - error $errunrecognised - } - } else { - set linebase $minline - } - set linebase ${sign}$linebase - } elseif {[string match *-* $tail]} { - set extras [lassign [split $tail -] int1 int2] - if {[llength $extras]} { - error $errunrecognised - } - if {![string is integer -strict $int1] || ![string is integer -strict $int2]} { - error $errunrecognised - } - set linebase [expr {$int1 - $int2}] - set linebase ${sign}$linebase - } elseif {[string match *+* $tail]} { - set extras [lassign [split $tail +] int1 int2] - if {[llength $extras]} { - error $errunrecognised - } - if {![string is integer -strict $int1] || ![string is integer -strict $int2]} { - error $errunrecognised - } - set linebase [expr {$int1 + $int2}] - set linebase ${sign}$linebase - } else { - error $errunrecognised - } - - } else { - set linebase $opt_linebase - } - - lassign [my numeric_chunkrange $chunkstart $chunkend] chunkstart chunkend - - if {![llength $opt_boundaries]} { - set binfo [lib::range_spans_chunk_boundaries $chunkstart $chunkend $chunksize -offset $opt_offset] - set boundaries [dict get $binfo boundaries] - } else { - set boundaries [list] - foreach b $opt_boundaries { - if {$chunkstart <= $b && $chunkend >= $b} { - lappend boundaries [expr {$b + $opt_offset}] - } - } - } - - - if {![llength $boundaries]} { - return "No boundaries found between $chunkstart and $chunkend for chunksize $chunksize (when offset $opt_offset)" - } - if {$opt_showconfig} { - set result "chunk range $chunkstart $chunkend line range $minline $maxline linebase $linebase limit $opt_limit\n" - } else { - set result "" - } - set pre_bytes [expr {$opt_displaybytes /2}] - set post_bytes $pre_bytes - set max_bytes [expr {[my chunklen] -1}] - if {$opt_limit > 0} { - set boundaries [lrange $boundaries[unset boundaries] 0 $opt_limit-1] - } - - set i 0 - foreach b $boundaries { - if {$opt_boundaryheader ne ""} { - set j [expr {$i+1}] - append result [string map [list %b% $b %i% $i %j% $j] $opt_boundaryheader] \n - } - set low [expr {max(($b - $pre_bytes),0)}] - set high [expr {min(($b + $post_bytes),$max_bytes)}] - - set lineinfolist [my chunkrange_to_lineinfolist $low $high -show_truncated 1] - set le_map [list \r\n \r \n ] - set result_list [list] - foreach lineinfo $lineinfolist { - set lineidx [dict get $lineinfo lineindex] - - set linenum [expr {$lineidx + $linebase}] - set s [dict get $lineinfo start] - set e [dict get $lineinfo end] - - set boundarymarker "" - set displayidx "" - set linenum_display $linenum - if {$s <= $b && $e >= $b} { - set idx [expr {$b - $s}] ;#index into whole position in whole line - not so useful if we're viewing a small section of a line - set char [string index [my line $lineidx] $idx] - set char_display [string map [list \r \n ] $char] - if {[dict get $lineinfo is_truncated]} { - set tside [dict get $lineinfo truncatedside] - set truncated [dict get $lineinfo truncated] - set tlen [string length $truncated] - if {"left" in $tside} { - set tleft [dict get $lineinfo truncatedleft] - set tleftlen [string length $tleft] - set displayidx [expr {$idx - $tleftlen}] - } elseif {"right" in $tside} { - set displayidx $idx - } - } else { - set displayidx $idx - } - set boundarymarker "'[a+ green bold]$char_display[a]'@$displayidx" - set linenum_display ${linenum_display},$idx - } - - set lhs_status $opt_cmark ;#default - set rhs_status $opt_cmark ;#default - if {[dict get $lineinfo is_truncated]} { - set line [dict get $lineinfo truncated] - set tside [dict get $lineinfo truncatedside] - if {"left" in $tside && "right" in $tside } { - set lhs_status $opt_tmark - set rhs_status $opt_tmark - } elseif {"left" in $tside} { - set lhs_status $opt_tmark - } elseif {"right" in $tside} { - set rhs_status $opt_tmark - } - - - } else { - set line [my line $lineidx] - } - if {$displayidx ne ""} { - set line [string replace $line $displayidx $displayidx [a+ White green bold]$char_display[a]] - } - set displayline [string map $le_map $line] - lappend result_list [list $linenum_display $boundarymarker $lhs_status $displayline $rhs_status] - } - set title_linenum "LNUM" - set linenums [lsearch -index 0 -all -inline -subindices $result_list *] - set markers [lsearch -index 1 -all -inline -subindices $result_list *] - set lines [lsearch -index 3 -all -inline -subindices $result_list *] - set title_marker "" - set title_line "Line" - #todo - use punk::char for unicode support of wide chars etc? - set widest_linenum [tcl::mathfunc::max {*}[lmap v [concat [list $title_linenum] $linenums] {string length $v}]] - set widest_marker [tcl::mathfunc::max {*}[lmap v [concat [list $title_marker] $markers] {string length [ansistrip $v]}]] - set widest_status [expr {max([string length $opt_cmark], [string length $opt_tmark])}] - set widest_line [tcl::mathfunc::max {*}[lmap v [concat [list $title_line] $lines] {string length $v}]] - foreach row $result_list { - lassign $row linenum marker lhs_status line rhs_status - append result [format " %-*s " $widest_linenum $linenum] - append result [format " %-*s " $widest_marker $marker] - append result [format " %-*s " $widest_status $lhs_status] - append result [format " %-*s " $widest_line $line] - append result [format " %-*s " $widest_status $rhs_status] \n - } - incr i - } - set ::punk::fileline::ansi::enabled $pre_ansi_enabled - return $result - } - method linecount {} { - #*** !doctools - #[call class::textinfo [method linecount]] - #[para] Number of lines in the raw data of the file, counted as per the policy in effect - return [llength $o_payloadlist] - } - - - method line {lineindex} { - #*** !doctools - #[call class::textinfo [method line] [arg lineindex]] - #[para]Reconstructs and returns the raw line using the payload and per-line stored line-ending metadata - #[para]A 'line' may be returned without a line-ending if the unerlying chunk had trailing data without a line-ending (or the chunk was loaded under a non-standard -policy setting) - #[para]Whilst such data may not conform to definitions (e.g POSIX) of the terms 'textfile' and 'line' - it is useful here to represent it as a line with metadata le set to "none" - #[para]To return just the data which might more commonly be needed for dealing with lines, use the [method linepayload] method - which returns the line data minus line-ending - - lassign [my numeric_linerange $lineindex 0] lineindex - - set le [dict get $o_linemap $lineindex le] - set le_chars [dict get [dict create lf \n crlf \r\n none ""] $le] - return [lindex $o_payloadlist $lineindex]$le_chars - } - method chunk_find_glob {globsearch args} { - #todo - use linepayload_find_glob when -ignore_lineendings is 0 - but check truncations for 1st and last line - error "unimplemented" - } - method linepayload_find_glob {globsearch args} { - #*** !doctools - #[call class::textinfo [method linepayload_find_glob] [arg globsearch] [opt {option value...}]] - #[para]Return a lineinfolist (see [method lineinfo] and [method lineinfolist]) of lines where payload matches the [arg globsearch] string - #[para]To limit the returned results use the -limit n option - where -limit 0 means return all matches. - #[para]For example: [method linepayload_find_glob] "*test*" -limit 1 - #[para]The result is always a list of lineinfo dictionaries even if one item is returned - #[para] -limitfrom can be start|end - #[para]The order of results is always the order as they occur in the data - even if -limitfrom end is specified. - #[para]-limitfrom end means that only the last -limit items are returned - #[para]Note that as glob accepts [lb]chars[rb]] to mean match any character in the set given by chars, searching for literal square brackets should be done by escaping the bracket with a backslash - #[para]This is true even if only a single square bracket is being searched for. e.g {*[lb]file*} will not find the word file followed by a left square-bracket - even though the search didn't close the square brackets. - #[para]In the above case - the literal search should be {*\[lb]file*} - - set opts [dict create\ - -limit 0\ - -strategy 1\ - -start 0\ - -end end\ - -limitfrom start\ - ] - foreach {k v} $args { - switch -- $k { - -limit - -strategy - -start - -end - -limitfrom { - dict set opts $k $v - } - default { - error "linepayload_find_glob unknown option '$k'. Known options: [dict keys $opts]" - } - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - set opt_limit [dict get $opts -limit] - if {![string is integer -strict $opt_limit] || $opt_limit < 0} { - error "linepayload_find_glob -limit must be positive integer" - } - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - set opt_strategy [dict get $opts -strategy] - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - set opt_start [dict get $opts -start] - set opt_start [expr {$opt_start}] - if {$opt_start != 0} {error "-start unimplemented"} - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - set opt_end [dict get $opts -end] - set max_line_index [expr {[llength $o_payloadlist]-1}] - if {$opt_end eq "end"} { - set opt_end $max_line_index - } - #TODO - if {$opt_end < $max_line_index} {error "-end less than max_line_index unimplemented"} - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - set opt_limitfrom [dict get $opts -limitfrom] - #-limitfrom start|end only - #TODO - if {$opt_limitfrom ne "start"} {error "-limitfrom unimplemented"} - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - - - set lineinfolist [list] - - if {$opt_limit == 1} { - set idx [lsearch -glob [lrange $o_payloadlist $opt_start $opt_end] $globsearch] - if {$idx >=0} { - set i [expr {$opt_start + $idx}] - lappend lineinfolist [list lineindex $i [dict get $o_linemap $i] payload [lindex $o_payloadlist $i]] - } - } elseif {$opt_limit == 0} { - set indices [lsearch -all -glob [lrange $o_payloadlist $opt_start $opt_end] $globsearch] - foreach irel $indices { - set i [expr {$opt_start + $irel}] - lappend lineinfolist [list lineindex $i [dict get $o_linemap $i] payload [lindex $o_payloadlist $i]] - } - } else { - #todo - auto-strategy based on limit vs number of lines - if {$opt_strategy == 0} { - set posn 0 - for {set r 0} {$r < $opt_limit} {incr r} { - set n [lsearch [lrange $o_payloadlist $posn+$opt_start end] $globsearch] - if {$n >=0} { - set irel [expr {$posn + $n}] - set i [expr {$irel + $opt_start}] - lappend lineinfolist [list lineindex $i {*}[dict get $o_linemap $i] payload [lindex $o_payloadlist $i]] - set posn [expr {$irel+1}] - } - } - } else { - set indices [lsearch -all -glob [lrange $o_payloadlist $opt_start $opt_end] $globsearch] - set limited [lrange $indices 0 $opt_limit-1] - foreach irel $limited { - set i [expr {$opt_start + $irel}] - lappend lineinfolist [list lineindex $i {*}[dict get $o_linemap $i] payload [lindex $o_payloadlist $i]] - } - } - } - return $lineinfolist - } - method linepayload {lineindex} { - #*** !doctools - #[call class::textinfo [method linepayload] [arg lineindex]] - #[para]Return the text of the line indicated by the zero-based lineindex - #[para]The line-ending is not returned in the data - but is still stored against this lineindex - #[para]Line Metadata such as the line-ending for a particular line and the byte/character range it occupies within the chunk can be retrieved with the [method linemeta] method - #[para]To retrieve both the line text and metadata in a single call the [method lineinfo] method can be used - #[para]To retrieve an entire line including line-ending use the [method line] method. - lassign [my numeric_linerange $lineindex 0] lineindex - return [lindex $o_payloadlist $lineindex] - } - method linepayloads {startindex endindex} { - #*** !doctools - #[call class::textinfo [method linepayloads] [arg startindex] [arg endindex]] - #[para]Return a list of just the payloads in the specified linindex range, with no metadata. - return [lrange $o_payloadlist $startindex $endindex] - } - method linemeta {lineindex} { - #*** !doctools - #[call class::textinfo [method linemeta] [arg lineindex]] - #[para]Return a dict of the metadata for the line indicated by the zero-based lineindex - #[para]Keys returned include - #[list_begin itemized] - #[item] le - #[para] A string representing the type of line-ending: crlf|lf|none - #[item] linelen - #[para] The number of characters/bytes in the whole line including line-ending if any - #[item] payloadlen - #[para] The number of character/bytes in the line excluding line-ending - #[item] start - #[para] The zero-based index into the associated raw file data indicating at which byte/character index this line begins - #[item] end - #[para] The zero-based index into the associated raw file data indicating at which byte/character index this line ends - #[para] This end-point corresponds to the last character of the line-ending if any - not necessarily the last character of the line's payload - #[list_end] - lassign [my numeric_linerange $lineindex 0] lineindex - dict get $o_linemap $lineindex - } - method lineinfo {lineindex} { - #*** !doctools - #[call class::textinfo [method lineinfo] [arg lineindex]] - #[para]Return a dict of the metadata and text for the line indicated by the zero-based lineindex - #[para]This returns the same info as the [method linemeta] with an added key of 'payload' which is the text of the line without line-ending. - #[para]The 'payload' value is the same as is returned from the [method linepayload] method. - lassign [my numeric_linerange $lineindex 0] lineindex ;#convert lineindex to canonical number e.g 1_000 -> 1000 end -> highest index - return [dict create lineindex $lineindex {*}[dict get $o_linemap $lineindex] payload [lindex $o_payloadlist $lineindex]] - } - method lineinfolist {startidx endidx} { - #*** !doctools - #[call class::textinfo [method lineinfolist] [arg startidx] [arg endidx]] - #[para]Returns list of lineinfo dicts for each line in line index range startidx to endidx - lassign [my numeric_linerange $startidx $endidx] startidx endidx - set chunkstart [dict get $o_linemap $startidx start] - set chunkend [dict get $o_linemap $endidx end] - set line_list [my chunkrange_to_lineinfolist $chunkstart $chunkend] ;# assertion - no need to view truncations as we've picked start and end of complete lines - #verify sanity - set l_start [lindex $line_list 0] - if {[set idx_start [dict get $l_start lineindex]] ne $startidx} { - error "lineinfolist first lineindex $idx_start doesn't match startidx $startidx" - } - set l_end [lindex $line_list end] - if {[set idx_end [dict get $l_end lineindex]] ne $endidx} { - error "lineinfolist last lineindex $idx_end doesn't match endidx $endidx" - } - return $line_list - } - - method linerange_to_chunkrange {startidx endidx} { - #*** !doctools - #[call class::textinfo [method linerange_to_chunkrange] [arg startidx] [arg endidx]] - - lassign [my numeric_linerange $startidx $endidx] startidx endidx - #inclusive range - return [list [dict get $o_linemap $startidx start] [dict get $o_linemap $endidx end]] - } - method linerange_to_chunk {startidx endidx} { - #*** !doctools - #[call class::textinfo [method linerange_to_chunk] [arg startidx] [arg endidx]] - set chunkrange [my linerange_to_chunkrange $startidx $endidx] - return [string range $o_chunk [lindex $chunkrange 0] [lindex $chunkrange 1]] - } - method lines {startidx endidx} { - #*** !doctools - #[call class::textinfo [method lines] [arg startidx] [arg endidx]] - lassign [my numeric_linerange $startidx $endidx] startidx endidx - set linelist [list] - set le_map [dict create lf \n crlf \r\n none ""] - for {set i $startidx} {$i <= $endidx} {incr i} { - lappend linelist "[lindex $o_payloadlist $i][dict get $le_map [dict get $o_linemap $i le]]" - } - return $linelist - } - method linepayloads {startidx endidx} { - #*** !doctools - #[call class::textinfo [method linepayloads] [arg startidx] [arg endidx]] - return [lrange $o_payloadlist $startidx $endidx] - } - method chunkrange_to_linerange {chunkstart chunkend} { - #*** !doctools - #[call class::textinfo [method chunkrange_to_linerange] [arg chunkstart] [arg chunkend]] - lassign [my numeric_chunkrange $chunkstart $chunkend] chunkstart chunkend - - set linestart -1 - for {set i 0} {$i < [llength $o_payloadlist]} {incr i} { - if {($chunkstart >= [dict get $o_linemap $i start]) && ($chunkstart <= [dict get $o_linemap $i end])} { - set linestart $i - break - } - } - if {$linestart == -1} { - error "Line with range in chunk spanning start index $chunkstart not found" - } - set lineend -1 - for {set i [expr {[llength $o_payloadlist] -1}]} {$i >=0} {incr i -1} { - if {($chunkend >= [dict get $o_linemap $i start]) && ($chunkend <= [dict get $o_linemap $i end])} { - set lineend $i - break - } - } - if {$lineend == -1} { - error "Line with range spanning end index $chunkend not found" - } - return [list $linestart $lineend] - } - method chunkrange_to_lineinfolist {chunkstart chunkend args} { - #*** !doctools - #[call class::textinfo [method chunkrange_to_lineinfolist] [arg chunkstart] [arg chunkend] [opt {option value...}]] - #[para]Return a list of dicts each with structure like the result of the [method lineinfo] method - but possibly with extra keys for truncation information if -show_truncated 1 is supplied - #[para]The truncation key in a lineinfo dict may be returned for first and/or last line in the resulting list. - #[para]truncation shows the shortened (missing bytes on left and/or right side) part of the entire line (potentially including line-ending or even partial line-ending) - #[para]Note that this truncation info is only in the return value of this method - and will not be reflected in [method lineinfo] queries to the main chunk. - - lassign [my numeric_chunkrange $chunkstart $chunkend] chunkstart chunkend - set defaults [dict create\ - -show_truncated 0\ - ] - set known_opts [dict keys $defaults] - foreach {k v} $args { - if {$k ni $known_opts} { - error "chunkrange_to_lines error: unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $args] - # -- --- --- --- --- --- --- --- - set opt_show_truncated [dict get $opts -show_truncated] - # -- --- --- --- --- --- --- --- - - set infolist [list] - set linerange [my chunkrange_to_linerange $chunkstart $chunkend] - lassign $linerange start_lineindex end_lineindex - - #if -show_truncated - #return extra keys for first and last items (which may be the same item if chunkrange is entirely within a line) - #add is_truncated 0|1 to all lines - #Even if the start/end line is not fully within the chunkrange ie truncated - the 'payload' key will contain the original untruncated data - ########################### - # first line may have payload tail truncated - or just linefeed, or even a split linefeed - ########################### - set first [dict create lineindex $start_lineindex {*}[dict get $o_linemap $start_lineindex] payload [lindex $o_payloadlist $start_lineindex]] - set start_info [dict get $o_linemap $start_lineindex] - - - if {$chunkstart > [dict get $start_info start]} { - dict set first is_truncated 1 - dict set first truncatedside [list left] ;#truncatedside is a list which may have 'right' added if last line is same as first line - } else { - dict set first is_truncated 0 - } - - if {$opt_show_truncated} { - #line1 - if {$chunkstart > [dict get $start_info start]} { - #there is lhs truncation - set payload [lindex $o_payloadlist $start_lineindex] - set line_start [dict get $start_info start] - set le_chars [dict get [dict create lf \n crlf \r\n none ""] [dict get $start_info le]] - set payload_and_le "${payload}${le_chars}" - set split [expr {$chunkstart - $line_start}] - set truncated [string range $payload_and_le $split end] - set lhs [string range $payload_and_le 0 $split-1] - - dict set first truncated $truncated - dict set first truncatedleft $lhs - } - } - ########################### - - ########################### - # middle lines if any - no truncation - ########################### - #difference in indexes of 1 would only mean 2 items to return - set middle_list [list] - if {($end_lineindex - $start_lineindex) > 1} { - for {set i [expr {$start_lineindex +1}]} {$i <= [expr {$end_lineindex -1}] } {incr i} { - #lineindex is key into main list - lappend middle_list [dict create lineindex $i {*}[dict get $o_linemap $i] payload [lindex $o_payloadlist $i] is_truncated 0] - } - } - ########################### - - ########################### - # tail line may have beginning or all of payload truncated - linefeed may be split if crlf - # may be same line as first line - in which case truncation at beginning as well - if {$end_lineindex == $start_lineindex} { - #same record - set end_info $start_info - - - if {$chunkend < [dict get $end_info end]} { - #there is rhs truncation - if {[dict get $first is_truncated]} { - dict set first truncatedside [list left right] - } else { - dict set first is_truncated 1 - dict set first truncatedside [list right] - } - } - - if {$opt_show_truncated} { - if {$chunkend < [dict get $end_info end]} { - #there is rhs truncation and we need to return the splits - #do rhs truncation - possibly in addition to existing lhs truncation - # ... - set payload [lindex $o_payloadlist $end_lineindex] - set line_start [dict get $end_info start] - set le_chars [dict get [dict create lf \n crlf \r\n none ""] [dict get $end_info le]] - set payload_and_le "${payload}${le_chars}" - set split [expr {$chunkend - $line_start}] - set truncated [string range $payload_and_le 0 $split] - set rhs [string range $payload_and_le $split+1 end] - dict set first truncatedright $rhs - if {"left" ni [dict get $first truncatedside]} { - #rhs truncation only - puts "payload_and_le: $payload_and_le" - puts "LENGTH: [string length $payload_and_le]" - #--- - #--- - dict set first truncated $truncated - dict set first truncatedside [list right] - } else { - #truncated on both sides - set lhslen [string length [dict get $first truncatedleft]] - #re-truncate the truncation to reapply the original lhs truncation - set truncated [string range $truncated $lhslen end] - dict set first truncated $truncated - } - } - } - #no middle or last to append - lappend infolist $first - } else { - set last [dict create lineindex $end_lineindex {*}[dict get $o_linemap $end_lineindex] payload [lindex $o_payloadlist $end_lineindex]] - set end_info [dict get $o_linemap $end_lineindex] - - - if {$chunkend < [dict get $end_info end]} { - dict set last is_truncated 1 - dict set last truncatedside [list right] - } else { - dict set last is_truncated 0 - } - - if {$opt_show_truncated} { - if {$chunkend < [dict get $end_info end]} { - #there is rhs truncation - and last line in range is a different line to first one - set payload [lindex $o_payloadlist $end_lineindex] - set line_start [dict get $end_info start] - set line_end [dict get $end_info end] - set le [dict get $end_info le] - set le_size [dict get {lf 1 crlf 2 none 0} $le] - set le_chars [dict get [dict create lf \n crlf \r\n none ""] $le] - set payload_and_le "${payload}${le_chars}" - - set split [expr {$chunkend - $line_start}] - set truncated [string range $payload_and_le 0 $split] - set rhs [string range $payload_and_le $split+1 end] - - dict set last truncated $truncated - dict set last truncatedright $rhs - #this has the effect that truncating the rhs by 1 can result in truncated being larger than original payload for crlf lines - as payload now sees the cr - #this is a bit unintuitive - but probably best reflects the reality. The truncated value is the truncated 'line' rather than the truncated 'payload' - } - } - - - lappend infolist $first - if {[llength $middle_list]} { - lappend infolist {*}$middle_list - } - lappend infolist $last - } - ########################### - #assertion all records have is_truncated key. - #assertion if is_truncated == 1 truncatedside should contain a list of either left, right or both left and right - #assertion If not opt_show_truncated - then truncated records will not have truncated,truncatedleft,truncatedright keys. - return $infolist - } - - #need to check truncations so that any split \r\n is counted precisely todo - method chunk_le_counts {chunkstart chunkend} { - set infolines [my chunkrange_to_lineinfolist $chunkstart $chunkend -show_truncated 1] - set lf_count 0 - set crlf_count 0 - set none_count 0 - foreach d $infolines { - set le [dict get $d le] - if {$le eq "lf"} { - incr lf_count - } elseif {$le eq "crlf"} { - incr crlf_count - } else { - incr none_count - } - } - #even without split crlf - this can overcount by counting the lf or crlf in a line which had an ending not in the chunk range specified - - #check first and last infoline for truncations - #Also check if the truncation is directly between an crlf - #both an lhs split and an rhs split could land between cr and lf - #to be precise - we should presumably count the part within our chunk as either a none for cr or an lf - #This means a caller counting chunk by chunk using this method will sometimes get the wrong answer depending on where crlfs lie relative to their chosen chunk size - #This is presumably ok - as it should be a well known thing to watch out for. - #If we're only receiving chunk by chunk we can't reliably detect splits vs lone s in the data - #There are surely more efficient ways for a caller to count line-endings in the way that makes sense for them - #but we should makes things as easy as possible for users of this line/chunk structure anyway. - - set first [lindex $infolines 0] - if {[dict get $first is_truncated]} { - #could be the only line - and truncated at one or both ends. - #both a left and a right truncation could split a crlf - - } - set last [lindex $infolines end] - if {[dict get $first lineindex] != [dict get $last lineindex]} { - #only need to process last if it is a different line - #if so - then split can only be left side - - } - - - return [dict create lf $lf_count crlf $crlf_count unterminated $none_count warning line_ending_splits_unimplemented] - } - - #todo - test last line and merge as necessary with first line from new chunk - generate line data only for appended chunk - method append_chunk {rawchunk} { - error "sorry - unimplemented" - } - - method numeric_linerange {startidx endidx} { - #*** !doctools - #[call class::textinfo [method numeric_linerange] [arg startidx] [arg endidx]] - #[para]A helper to return any Tcl-style end end-x values given to startidx or endidx; converted to their specific values based on the current state of the underlying line data - #[para]This is used internally by API functions such as [method line] to enable it to accept more expressive indices - return [my normalize_indices $startidx $endidx [expr {[dict size $o_linemap]-1}]] - } - method numeric_chunkrange {startidx endidx} { - #*** !doctools - #[call class::textinfo [method numeric_chunkrange] [arg startidx] [arg endidx]] - #[para]A helper to return any Tcl-style end end-x entries supplied to startidx or endidx; converted to their specific values based on the current state of the underlying chunk data - return [my normalize_indices $startidx $endidx [expr {[string length $o_chunk]-1}]] - } - method normalize_indices {startidx endidx max} { - #*** !doctools - #[call class::textinfo [method normalize_indices] [arg startidx] [arg endidx] [arg max]] - #[para]A utility to convert some of the of Tcl-style list-index expressions such as end, end-1 etc to valid indices in the range 0 to the supplied max - #[para]Basic addition and subtraction expressions such as 4-1 5+2 are accepted - #[para]startidx higher than endidx is allowed - #[para]Unlike Tcl's index expressions - we raise an error if the calculated index is out of bounds 0 to max - set original_startidx $startidx - set original_endidx $endidx - set startidx [string map [list _ ""] $startidx] ;#don't barf on Tcl 8.7+ underscores in numbers - we can't just use expr because it will not handle end-x - set endidx [string map [list _ ""] $endidx] - if {![string is digit -strict "$startidx$endidx"]} { - foreach whichvar [list start end] { - upvar 0 ${whichvar}idx index - if {![string is digit -strict $index]} { - switch -glob -- $index { - end { - set index $max - } - "*-*" { - #end-int or int-int - like lrange etc we don't accept arbitrarily complex expressions - lassign [split $index -] A B - if {$A eq "end"} { - set index [expr {$max - $B}] - } else { - set index [expr {$A - $B}] - } - } - "*+*" { - lassign [split $index +] A B - if {$A eq "end"} { - #review - this will just result in out of bounds error in final test - as desired - #By calculating here - we will see the result in the error message - but it's probably not particularly useful - as we don't really need end+ support at all. - set index [expr {$max + $B}] - } else { - set index [expr {$A + $B}] - } - } - default { - #May be something like +2 or -0 which braced expr can hanle - #we would like to avoid unbraced expr here - as we're potentially dealing with ranges that may come from external sources. - if {[catch {expr {$index}} index]} { - #could be end+x - but we don't want out of bounds to be valid - #set it to something that the final bounds expr test can deal with - set index Inf - } - } - } - } - } - } - #Unlike Tcl lrange,lindex etc - we don't want to support out of bound indices. - #show the supplied index and what it was mapped to in the error message. - if {$startidx < 0 || $startidx > $max} { - error "Bad start index '$original_startidx'. $startidx out of bounds 0 - $max" - } - if {$endidx < 0 || $endidx > $max} { - error "Bad end index '$original_endidx'. $endidx out of bounds 0 - $max (try $max or end)" - } - return [list $startidx $endidx] - } - - method regenerate_lines {args} { - #*** !doctools - #[call class::textinfo [method regenerate_lines]] - #[para]generate a list of lines from the current state of the stored raw data chunk and keep a map of line-endings indexed by lineindex - #[para]This is called automatically by the Constructor during object creation - #[para]It is exposed in the API experimentally - as chunk and line manipulation functions are considered. - #[para]TODO - review whether such manual control will be necessary/desirable - - #we don't store the actual line-endings as characters (for better layout of debug/display of data) - instead we store names lf|crlf|none - - # first split on lf - then crlf. As we've replaced with single substution chars - the order doesn't matter. - set o_payloadlist [list] - set o_linemap [dict create] - set crlf_replace [list \r\n $o_CRLF_C \n $o_LF_C] - set normalised_data [string map $crlf_replace $o_chunk] - - set lf_lines [split $normalised_data $o_LF_C] - - set idx 0 - set lf_count 0 - set crlf_count 0 - set filedata_offset 0 - set i 0 - set imax [expr {[llength $lf_lines]-1}] - foreach lfln $lf_lines { - set crlf_parts [split $lfln $o_CRLF_C] - if {[llength $crlf_parts] <= 1} { - #no crlf - set payloadlen [string length $lfln] - set le_size 1 - set le lf - if {$i == $imax} { - #no more lf segments - and no crlfs - if {$payloadlen > 0} { - #last line in split has chars - therefore there was no trailing line-ending - set le_size 0 - set le none - } else { - #empty space after last line-ending - #not really a line - we get here from splitting on our lf-replacement char - #An editor might display this pseudo-line with a line number - but we won't treat it as one here - break - } - } - lappend o_payloadlist $lfln - set linelen [expr {$payloadlen + $le_size}] - #we include line-ending in byte count for a line. - dict set o_linemap $idx [list le $le linelen $linelen payloadlen $payloadlen start $filedata_offset end [expr {$filedata_offset + $linelen -1}]] - incr filedata_offset $linelen - incr lf_count - incr idx - } else { - foreach crlfpart [lrange $crlf_parts 0 end-1] { - lappend o_payloadlist $crlfpart - set payloadlen [string length $crlfpart] - set linelen [expr {$payloadlen + 2}] - dict set o_linemap $idx [list le crlf linelen $linelen payloadlen $payloadlen start $filedata_offset end [expr {$filedata_offset + $linelen -1}]] - incr filedata_offset $linelen - incr crlf_count - incr idx - } - set lfpart [lindex $crlf_parts end] - set payloadlen [string length $lfpart] - if {$i == $imax} { - #no more lf segments - but we did find crlf in last (or perhaps only) lf line - #last element in our split has no le - if {$payloadlen > 0} { - set le_size 0 - set le none - } else { - #set le_size 2 - #set le crlf - break - } - } else { - #more lf segments to come - set le_size 1 - set le lf - } - - lappend o_payloadlist $lfpart - set linelen [expr {$payloadlen + $le_size}] - dict set o_linemap $idx [list le $le linelen $linelen payloadlen $payloadlen start $filedata_offset end [expr {$filedata_offset + $linelen -1}]] - incr filedata_offset $linelen - incr lf_count - incr idx - } - incr i - #incr filedata_offset ;#move up 1 so start entry for next line is greater than end entry for previous line - } - set le_count [expr {$lf_count + $crlf_count}] - if {$le_count != [llength $o_payloadlist]} { - puts stderr "fileline::class::textinfo warning. regenerate_lines lf_count: $lf_count + crlf_count: $crlf_count does not equal length of lines stored: [llength $o_payloadlist]" - } - - } - method regenerate_chunk {} { - #o_payloadlist - #o_linemap - set oldsize [string length $o_chunk] - set newchunk "" - #review - what was the intention here? - puts stderr "regenerate_chunk -warning code incomplete" - dict for {idx lineinfo} $o_linemap { - #??? - #set - - } - - return [list newsize [string length $newchunk] oldsize $oldsize] - } - - - #*** !doctools - #[list_end] - } - #*** !doctools - #[list_end] [comment {--- end class enumeration ---}] - } -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::fileline { - namespace export * - #variable xyz - - #*** !doctools - #[subsection {Namespace punk::fileline}] - #[para] Core API functions for punk::fileline - #[list_begin definitions] - - punk::args::define { - @id -id ::punk::fileline::get_textinfo - @cmd -name punk::fileline::get_textinfo -help\ - "return: textinfo object instance" - -file -default {} -type existingfile - -translation -default iso8859-1 - -encoding -default "\uFFFF" - -includebom -default 0 - @values -min 0 -max 1 - } - proc get_textinfo {args} { - #*** !doctools - #[call get_textinfo [opt {option value...}] [opt datachunk]] - #[para]Returns textinfo object instance representing data in string datachunk or if -file filename supplied - data loaded from a file - #[para]The encoding used is as specified in the -encoding option - or from the Byte Order Mark (bom) at the beginning of the data - #[para]For Tcl 8.6 - encodings such as utf-16le may not be available - so the bytes are swapped appropriately depending on the platform byteOrder and encoding 'unicode' is used. - #[para]encoding defaults to utf-8 if no -encoding specified and no BOM was found - #[para]Whether -encoding was specified or not - by default the BOM characters are not retained in the line-data - #[para]If -includebom 1 is specified - the bom will be retained in the stored chunk and the data for line 1, but will undergo the same encoding transformation as the rest of the data - #[para]The get_bomid method of the returned object will contain an identifier for any BOM encountered. - #[para] e.g utf-8,utf-16be, utf-16le, utf-32be, utf32-le, SCSU, BOCU-1,GB18030, UTF-EBCDIC, utf-1, utf-7 - #[para]If the encoding specified in the BOM isn't recognised by Tcl - the resulting data is likely to remain as the raw bytes of whatever encoding that is. - #[para]Currently only utf-8, utf-16* and utf-32* are properly supported even though the other BOMs are detected, reported via get_bomid, and stripped from the data. - #[para]GB18030 falls back to cp936/gbk (unless a gb18030 encoding has been installed). Use -encoding iso8859-1 if this isn't suitable and you need to do your own processing of the bytes. - - lassign [dict values [punk::args::parse $args withid ::punk::fileline::get_textinfo]] leaders opts values - # -- --- --- --- - set opt_file [dict get $opts -file] - set opt_translation [dict get $opts -translation] - set opt_encoding [dict get $opts -encoding] - set opt_includebom [dict get $opts -includebom] - # -- --- --- --- - - if {$opt_file ne ""} { - set filename $opt_file - set fd [open $filename r] - chan configure $fd -translation binary -encoding $opt_translation - #should use translation binary to get actual line-endings - but we allow caller to override - #Always read encoding in binary - check for bom below and/or apply chosen opt_encoding - set rawchunk [read $fd] - close $fd - if {[llength $values]} { - puts stderr "Ignoring trailing argument [string length [lindex $values 0]] bytes. Not used when -file is specified" - } - } else { - set rawchunk [lindex $values 0] - } - set rawlen [string length $rawchunk] - #examine first 4 bytes for possible BOM - #big-endian BOMs - # ----------------------------------- - #EFBBBF - utf-8 reliabletxt - #FEFF - utf-16be reliabletxt - #FFFE - utf-16le reliabletxt - #0000FEFF - utf-32be reliabletxt - #FFFE0000 - utf-32le - #0000FFFE - utf-32be(2143) non-standard! (not supported) - #FEFF0000 - utf-32le(3412) non-standard! (not supported - will detect as utf-16be) - #2B2F76 - utf-7 (not supported) - #F7644C - utf-1 (not supported) - #DD736673 - UTF-EBCDIC (not supported) - #0EFEFF - SCSU (not supported) - #FBEE28 - BOCU-1 Binary Ordered Compression for Unicode (mime-compatible) - (not supported - fall back to utf-8) - #84319533 - GB18030 - Chinese gov standard (fall back to cp936 with warning if no encoding name) - # ----------------------------------- - - set first32 [string range $rawchunk 0 3] - #scan using capital H for big-endian order - set first32_be [binary scan $first32 H* maybe_bom] ;#we use H* instead of H8 for 8 nibbles (4 bytes) - because our first32 may contain less than 4 bytes - in which case we won't match - set bomid "" - set bomenc "" - set is_reliabletxt 0 ;#see http://reliabletxt.com - only utf-8 with bom, utf-16be, utf-16le, utf-32be supported as at 2024 - set startdata 0 - #todo switch -glob - if {[string match "efbbbf*" $maybe_bom]} { - set bomid utf-8 - set bomenc utf-8 - set is_reliabletxt 1 - set startdata 3 - } elseif {$maybe_bom eq "0000feff"} { - set bomid utf-32be - set bomenc utf-32be - set is_reliabletxt 1 - set startdata 4 - } elseif {$maybe_bom eq "fffe0000"} { - #Technically ambiguous - could be utf-16le bom followed by utf-16 null character (2 byte null) - puts stderr "WARNING - ambiguous BOM fffe0000 found. Treating as utf-32le - but could be utf-16le - consider manually setting -encoding or converting data to another encoding." - set bomid utf-32le - set bomenc utf-32le - set startdata 4 - } elseif {[string match "feff*" $maybe_bom]} { - set bomid utf-16be - set bomenc utf-16be - set is_reliabletxt 1 - set startdata 2 - } elseif {[string match "fffe*" $maybe_bom]} { - set bomid utf-16le - set bomenc utf-16le - set is_reliabletxt 1 - set startdata 2 - } elseif {$maybe_bom eq "0efeff"} { - set bomid scsu - set bomenc "binary" - set startdata 3 - } elseif {$maybe_bom eq "fbee28"} { - set bomid bocu-1 - puts stderr "WARNING - bocu-1 BOM FBEE28 found. Not supported - Falling back to binary" - set bomenc "binary" ;# utf-8??? - set startdata 3 - } elseif {$maybe_bom eq "84319533"} { - if {![dict exists [punk::char::page_names_dict gb18030] gb18030]} { - puts stderr "WARNING - no direct support for GB18030 (chinese) - Falling back to cp936/gbk" - set bomenc cp936 - } else { - set bomenc [dict get [punk::char::page_names_dict gb18030]] ;#review - this may never exist in Tcl or may be named differently - create a handler? - } - set bomid gb18030 - set startdata 4 - } elseif {$maybe_bom eq "f7644c"} { - puts stderr "WARNING utf-1 BOM F7644C found - not supported. Falling back to binary" - set bomid utf-1 - set bomenc binary - set startdata 3 - } elseif {[string match "2b2f76*" $maybe_bom]} { - puts stderr "WARNING utf-7 BOM 2b2f76 found - not supported. Falling back to binary and leaving BOM in data!" - #review - work out how to strip bom - last 2 bits of 4th byte belong to following character - set bomid utf-7 - set bomenc binary - set startdata 0 - } - - #todo - check xml encoding attribute / html content-type - #todo - a separate chardet (https://chardet.readthedocs.io/ ) or mozilla like mechanism that can be manually called to autodetect character encoding - #This should be an explicit operation - not automatially done here unless we provide a flag for it. - - - if {$opt_includebom} { - set startdata 0 - } - - if {$opt_encoding eq "\uFFFF"} { - if {$bomenc ne "" && $bomenc ne "binary"} { - if {[package vcompare [package provide Tcl] 8.7] < 0} { - #tcl 8.6 has unicode encoding but not utf-16le etc - if {$bomenc ni [encoding names]} { - if {$bomenc eq "utf-16le"} { - if {$::tcl_platform(byteOrder) eq "littleEndian"} { - set datachunk [encoding convertfrom unicode [string range $rawchunk $startdata end]] - set encoding_selected unicode - } else { - set datachunk [encoding convertfrom unicode [system::wordswap16 [string range $rawchunk $startdata end]]] - set encoding_selected "unicode (wordswapped 16)" - } - } elseif {$bomenc eq "utf-16be"} { - if {$::tcl_platform(byteOrder) eq "littleEndian"} { - set datachunk [encoding convertfrom unicode [system::wordswap16 [string range $rawchunk $startdata end]]] - set encoding_selected "unicode (wordswapped 16)" - } else { - set datachunk [encoding convertfrom unicode [string range $rawchunk $startdata end]] - set encoding_selected unicode - } - } elseif {$bomenc eq "utf-32le"} { - if {$::tcl_platform(byteOrder) eq "littleEndian"} { - set datachunk [encoding convertfrom unicode [string range $rawchunk $startdata end]] - set encoding_selected unicode - } else { - set datachunk [encoding convertfrom unicode [system::wordswap32 [string range $rawchunk $startdata end]]] - set encoding_selected "unicode (wordswapped 32)" - } - } elseif {$bomenc eq "utf-32be"} { - if {$::tcl_platform(byteOrder) eq "littleEndian"} { - set datachunk [encoding convertfrom unicode [system::wordswap32 [string range $rawchunk $startdata end]]] - set encoding_selected "unicode (wordswapped 32)" - } else { - set datachunk [encoding convertfrom unicode [string range $rawchunk $startdata end]] - set encoding_selected unicode - } - } else { - error "Encoding $bomenc unavailable in this version of Tcl" - } - } else { - set datachunk [encoding convertfrom $bomenc [string range $rawchunk $startdata end]] - set encoding_selected $bomenc - } - } else { - #tcl 8.7 plus has utf-16le etc - set datachunk [encoding convertfrom $bomenc [string range $rawchunk $startdata end]] - set encoding_selected $bomenc - } - } else { - #!? - if {$bomenc eq "binary"} { - set datachunk [string range $rawchunk $startdata end] - set encoding_selected binary - } else { - set datachunk [encoding convertfrom utf-8 [string range $rawchunk $startdata end]] - set encoding_selected utf-8 - } - } - } else { - #manually specified encoding overrides bom - but still remove bom-chars REVIEW - #e.g we still want bom info - but specify binary encoding - - if {$opt_encoding eq "binary"} { - set datachunk [string range $rawchunk $startdata end] - } else { - set datachunk [encoding convertfrom $opt_encoding [string range $rawchunk $startdata end]] - } - set encoding_selected $opt_encoding - } - - set textobj [class::textinfo new $datachunk] - if {$bomid ne ""} { - $textobj set_bomid $bomid - } - - - - - set summary "" - append summary "Bytes loaded : $rawlen" \n - append summary "BOM ID : $bomid" \n - append summary "Encoding selected : $encoding_selected" \n - append summary "Characters : [$textobj chunklen]" \n - append summary "Lines recognised : [$textobj linecount]" \n - set leinfo [$textobj chunk_le_counts 0 end] - append summary "crlf endings (windows) : [dict get $leinfo crlf]" \n - append summary "lf endings (unix) : [dict get $leinfo lf]" \n - append summary "unterminated lines : [dict get $leinfo unterminated]" \n - puts stdout $summary - return $textobj - } - - proc file_boundary_display {filename startbyte endbyte chunksize args} { - set fd [open $filename r] ;#use default error if file not readable - chan configure $fd -translation binary - set rawfiledata [read $fd] - close $fd - set textobj [class::textinfo new $rawfiledata] - set result [$textobj chunk_boundary_display $startbyte $endbyte $chunksize {*}$args] - $textobj destroy - return $result - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::fileline ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::fileline::lib { - namespace export * - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace punk::fileline::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - - proc range_spans_chunk_boundaries {start end chunksize args} { - #*** !doctools - #[call [fun lib::range_spans_chunk_boundaries] [arg start] [arg end] [arg chunksize]] - #[para]Takes start and end offset, generally representing bytes or character indices, and computes a list of boundaries at multiples of the chunksize that are spanned by the start and end range. - #[list_begin arguments] - # [arg_def integer start] - # [para] zero-based start index of range - # [arg_def integer end] - # [para] zero-based end index of range - # [arg_def integer chunksize] - # [para] Number of bytes/characters in chunk - must be positive and > 0 - #[list_end] - #[para]returns a dict with the keys is_span and boundaries - #[para]is_span 0|1 indicates if the range specified spans a boundary of chunksize - #[para]boundaries contains a list of the spanned boundaries - which are always multiples of the chunksize - #[para]e.g - #[example_begin] - # range_spans_chunk_boundaries 10 1750 512 - # is_span 1 boundaries {512 1024 1536} - #[example_end] - #[para]The -offset option - #[example_begin] - # range_spans_chunk_boundaries 10 1750 512 -offset 2 - # is_span 1 boundaries {514 1026 1538} - #[example_end] - #[para] This function automatically uses lseq (if Tcl >= 8.7) when number of boundaries spanned is approximately greater than 75 - if {[catch {package require Tcl 8.7-}]} { - #only one implementation available for older Tcl - tailcall punk::fileline::system::_range_spans_chunk_boundaries_tcl $start $end $chunksize {*}$args - } - if {$chunksize < 1} { - error "range_spans_chunk_boundaries chunksize must be >= 1" - } - - if {(abs($end - $start) / $chunksize) < 75} { - tailcall punk::fileline::system::_range_spans_chunk_boundaries_tcl $start $end $chunksize {*}$args - } else { - tailcall punk::fileline::system::_range_spans_chunk_boundaries_lseq $start $end $chunksize {*}$args - } - } - - proc range_boundaries {start end chunksizes args} { - set argd [punk::args::parse $args withdef { - -offset -default 0 - }] - lassign [dict values $argd] leaders opts remainingargs - - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::fileline::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -namespace eval punk::fileline::system { - #*** !doctools - #[subsection {Namespace punk::fileline::system}] - #[para] Internal functions that are not part of the API - - proc wordswap16 {data} { - #scan in one endianness - format in the other. Whether we scan le/be first doesn't matter as long as we format using the opposite endianness - binary scan $data s* elements ;#scan little endian - return [binary format S* $elements] ;#format big endian - } - proc wordswap32 {data} { - binary scan $data i* elements - return [binary format I* $elements] - } - - proc scan32bit_be {i32} { - if {[binary scan $i32 I x]} { - return $x - } else { - error "couldn't scan $i32" - } - } - - #for 8.7+ using lseq - #much faster when resultant boundary size is large (at least when offset 0) - proc _range_spans_chunk_boundaries_lseq {start end chunksize args} { - if {$chunksize < 1} {error "chunksize must be > 0"} ;#sanitycheck in case called directly - set opts [dict create\ - -offset 0\ - ] - foreach {k v} $args { - switch -- $k { - -offset { - dict set opts $k $v - } - default { - error "unknown option '$k'. Known options: [dict keys $opts]" - } - } - } - # -- --- --- --- - set opt_offset [dict get $opts -offset] - # -- --- --- --- - - set smod [expr {$start % $chunksize}] - if {$smod != 0} { - set start [expr {$start + ($chunksize - $smod)}] - if {$start > $end} { - return [list is_span 0 boundaries {}] - } - } - set boundaries [lseq $start to $end $chunksize] - #offset can be negative - if {$opt_offset} { - if {$opt_offset + [lindex $boundaries end] > $end || $opt_offset + [lindex $boundaries 0] < $start} { - set overflow 1 - } else { - set overflow 0 - } - set boundaries [lmap v $boundaries[unset boundaries] {expr {$v + $opt_offset}}] - if {$overflow} { - #we don't know how many overflowed.. - set inrange [list] - foreach b $boundaries { - if {$b >= $start && $b <= $end} { - lappend inrange $b - } - } - set boundaries $inrange - } - } - return [list is_span [expr {[llength $boundaries]>0}] boundaries $boundaries] - } - - #faster than lseq for small number of resultant boundaries (~< 75) (which is a common use case) - #gets very slow (comparitively) with large resultsets - proc _range_spans_chunk_boundaries_tcl {start end chunksize args} { - if {$chunksize < 1} {error "chunksize must be > 0"} ;#sanitycheck in case called directly - set opts [dict create\ - -offset 0\ - ] - foreach {k v} $args { - switch -- $k { - -offset { - dict set opts $k $v - } - default { - error "unknown option '$k'. Known options: [dict keys $opts]" - } - } - } - # -- --- --- --- - set opt_offset [dict get $opts -offset] - # -- --- --- --- - - set is_span 0 - set smod [expr {$start % $chunksize}] - if {$smod != 0} { - set start [expr {$start + ($chunksize - $smod)}] - } - set boundaries [list] - - #we only need to pre-check the result-range for negative offsets - as our main loop stops before end? - if {$opt_offset < 0} { - #set btrack [expr {$start + $opt_offset}] ;#start back one to make sure we catch the first boundary - set btrack $bstart - set boff [expr {$btrack + $opt_offset}] ;#must be growing even if start and offset are negative - as chunksize is at least 1 - while {$boff < $start} { - incr btrack $chunksize - set boff [expr {$btrack + $opt_offset}] - } - set bstart $btrack - } else { - set bstart $start - } - for {set b $bstart} {[set boff [expr {$b + $opt_offset}]] <= $end} {incr b $chunksize} { - lappend boundaries $boff - } - - return [list is_span [expr {[llength $boundaries]>0}] boundaries $boundaries offset $opt_offset] - } - - proc _range_spans_chunk_boundaries_TIMEIT {start end chunksize {repeat 1}} { - puts "main : [time {punk::fileline::lib::range_spans_chunk_boundaries $start $end $chunksize} $repeat]" - puts "tcl : [time {punk::fileline::system::_range_spans_chunk_boundaries_tcl $start $end $chunksize} $repeat]" - if {![catch {package require Tcl 8.7-}]} { - puts "lseq : [time {punk::fileline::system::_range_spans_chunk_boundaries_lseq $start $end $chunksize} $repeat]" - } - } -} -namespace eval punk::fileline::ansi { - #*** !doctools - #[subsection {Namespace punk::fileline::ansi}] - #[para]These are ansi functions imported from punk::ansi - or no-ops if that package is unavailable - #[para]See [package punk::ansi] for documentation - #[list_begin definitions] - variable enabled 1 - #*** !doctools - #[call [fun ansi::a]] - #[call [fun ansi::a+]] - #[call [fun ansi::ansistrip]] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::fileline::ansi ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::fileline [namespace eval punk::fileline { - variable pkg punk::fileline - variable version - set version 0.1.0 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.1.tm deleted file mode 100644 index 92cab7e9..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.1.tm +++ /dev/null @@ -1,4237 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.1 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.1] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - return true - } else { - return false - } - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - return 0 - } - return [expr {$result ne "a2"}] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - return [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - return $has_bug - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - #NOTE: an alias may match in a namespace - but not have a corresponding command that matches that name (alias renamed) - proc aliases {{glob *}} { - set ns [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command - set ns_mapped [string map {:: \uFFFF} $ns] - #puts stderr "aliases ns: $ns_mapped" - set segments [split $ns_mapped \uFFFF] ;#include empty string before leading :: - if {![string length [lindex $segments end]]} { - #special case for :: only include leading segment rather thatn {} {} - set segments [lrange $segments 0 end-1] - } - set segcount [llength $segments] ;#only match number of segments matching current ns - - - set all_aliases [interp aliases {}] - set matched [list] - foreach a $all_aliases { - #normalize with leading :: - if {![string match ::* $a]} { - set abs ::$a - } else { - set abs $a - } - - set asegs [split [string map {:: \uFFFF} $abs] \uFFFF] - set acount [llength $asegs] - #puts "alias $abs acount:$acount asegs:$asegs segcount:$segcount segments: $segments" - if {($acount - 1) == $segcount} { - if {[lrange $asegs 0 end-1] eq $segments} { - if {[string match $glob [lindex $asegs end]]} { - #report this alias in the current namespace - even though there may be no matching command - lappend matched $a ;#add raw alias token which may or may not have leading :: - } - } - } - } - #set matched_abs [lsearch -all -inline $all_aliases $glob] - - return $matched - } - - proc alias {{aliasorglob ""} args} { - set nsthis [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command - if {[llength $args]} { - if {$aliasorglob in [interp aliases ""]} { - set existing [interp alias "" $aliasorglob] - puts stderr "Overwriting existing alias $aliasorglob -> $existing with $aliasorglob -> $args (in current session only)" - } - if {([llength $args] ==1) && [string trim [lindex $args 0]] eq ""} { - #use empty string/whitespace as intention to delete alias - return [interp alias "" $aliasorglob ""] - } - return [interp alias "" $aliasorglob "" {*}$args] - } else { - if {![string length $aliasorglob]} { - set aliaslist [punk::lib::aliases] - puts -nonewline stderr $aliaslist - return - } - #we need to first check for exact match of alias that happens to have glob chars i.e the supplied aliasorglob looks like a glob but is actually directly an alias - set target [interp alias "" $aliasorglob] - if {[llength $target]} { - return $target - } - - if {([string first "*" $aliasorglob] >= 0) || ([string first "?" $aliasorglob] >= 0)} { - set aliaslist [punk::lib::aliases $aliasorglob] - puts -nonewline stderr $aliaslist - return - } - return [list] - } - } - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - proc lswap {lvar a z} { - upvar $lvar l - if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { - #lindex_resolve_basic returns only -1 if out of range - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned) - set a_index [lindex_resolve $l $a] - set a_msg "" - switch -- $a_index { - -2 { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - -3 { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - } - set z_index [lindex_resolve $l $z] - set z_msg "" - switch -- $z_index { - -2 { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - -3 { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [lzip2lists {*}$args]} - 3 {return [lzip3lists {*}$args]} - 4 {return [lzip4lists {*}$args]} - 5 {return [lzip5lists {*}$args]} - 6 {return [lzip6lists {*}$args]} - 7 {return [lzip7lists {*}$args]} - 8 {return [lzip8lists {*}$args]} - 9 {return [lzip9lists {*}$args]} - 10 {return [lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } else { - return [lzipn {*}$args] - } - } - } - } - - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - puts "proc punk::lib::lzip${n}lists {$arglist} \{" - puts "$body" - puts "\}" - proc ::punk::lib::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [punk::lib::check::has_tclbug_lsearch_strideallinline ]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::lzipn_tcl9a] - } - - - namespace import ::punk::args::lib::tstr - - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segement in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list array exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list array get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - set sep_mismatch " mismatch " - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - set lower_resolve [punk::lib::lindex_resolve $dval $a] ;#-2 for too low, -1 for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == -2} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -3} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve $dval $b] - if {$upper == -3} { - ##x - #upper bound is below list range - - if {$lower_resolve >=-2} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == -2} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - append result [textblock::join_basic -- $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list] - proc is_list_all_in_list {small large} $body - } - - proc is_list_all_ni_list {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list] - proc is_list_all_ni_list {a b} $body - } - - #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist - #struct::set difference may happen to preserve ordering when items are integers, but order can't be relied on, - # especially as struct::set has 2 differing implementations (tcl vs critcl) which return results with different ordering to each other and different deduping behaviour in some cases (e.g empty 2nd arg) - proc ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - proc ldiff2 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - puts stderr "WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - #order-preserving - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {(.*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bunds on upper vs lower side - proc lindex_resolve {list index} { - #*** !doctools - #[call [fun lindex_resolve] [arg list] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list - #[para]Users may define procs which accept a list index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out or bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which we will resolve (hopefully safely) with expr - #if {![llength $list]} { - # #review - # return ??? - #} - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - if {$index < 0} { - return -3 - } elseif {$index >= [llength $list]} { - return -2 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$op eq "+" && $offset != 0} { - return -2 - } - } else { - #index is 'end' - set index [expr {[llength $list]-1}] - if {$index < 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return -2 - } else { - return $index - } - } - if {$offset == 0} { - set index [expr {[llength $list]-1}] - if {$index < 0} { - return -2 ;#special case as above - } else { - return $index - } - } else { - #by now, if op = + then offset = 0 so we only need to handle the minus case - set index [expr {([llength $list]-1) - $offset}] - } - if {$index < 0} { - return -3 - } else { - return $index - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - if {[regexp {(.*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < 0} { - return -3 - } elseif {$index >= [llength $list]} { - return -2 - } - return $index - } - } - } - proc lindex_resolve_basic {list index} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg list] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -1 for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - #set indices [list] ;#building this may be somewhat expensive in terms of storage and compute for large lists - we could use lseq in Tcl 8.7+ - # - which - #for {set i 0} {$i < [llength $list]} {incr i} { - # lappend indices $i - #} - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= [llength $list])} { - #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. - return -1 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {[llength $list]} { - set indices [punk::lib::range 0 [expr {[llength $list]-1}]] ;# uses lseq if available, has fallback. - #if lseq was available - $indices is an 'arithseries' - theoretically not taking up ram(?) - } else { - set indices [list] - } - set idx [lindex $indices $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -1 - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - proc lcm {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the lowest common multiple of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para] - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [string trim $ln] - if {!$in_jt} { - if {[string match *jumpTable* $ln]} { - append result $ln \n - set in_jt 1 - } - } else { - if {[string match Command* $tln] || [string match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc objclone {obj} { - append obj2 $obj {} - } - proc set_clone {varname obj} { - #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] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [objclone $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [objclone $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.1 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.2.tm deleted file mode 100644 index 22977429..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.2.tm +++ /dev/null @@ -1,4533 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.2 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.2] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - return true - } else { - return false - } - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - return 0 - } - return [expr {$result ne "a2"}] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - return [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - return $has_bug - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - punk::args::set_alias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - if {"::ledit" ni [info commands ::ledit]} { - interp alias {} ledit {} ::punk::lib::compat::ledit - punk::args::set_alias ::punk::lib::compat::ledit ::ledit - } - proc ledit {lvar first last args} { - upvar $lvar l - #use lindex_resolve to support for example: ledit lst end+1 end+1 h i - set fidx [punk::lib::lindex_resolve [llength $l] $first] - switch -exact -- $fidx { - -3 { - #index below lower bound - set pre [list] - set fidx -1 - } - -2 { - #first index position is greater than index of last element in the list - set pre [lrange $l 0 end] - set fidx [llength $l] - } - default { - set pre [lrange $l 0 $first-1] - } - } - set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -3 { - #index below lower bound - set post [lrange $l 0 end] - } - -2 { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - set post [lrange $l $last+1 end] - } - } - } - set l [list {*}$pre {*}$args {*}$post] - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - proc lswap {lvar a z} { - upvar $lvar l - set len [llength $l] - if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { - #lindex_resolve_basic returns only -1 if out of range - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned) - set a_index [lindex_resolve $len $a] - set a_msg "" - switch -- $a_index { - -2 { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - -3 { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - } - set z_index [lindex_resolve $len $z] - set z_msg "" - switch -- $z_index { - -2 { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - -3 { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [lzip2lists {*}$args]} - 3 {return [lzip3lists {*}$args]} - 4 {return [lzip4lists {*}$args]} - 5 {return [lzip5lists {*}$args]} - 6 {return [lzip6lists {*}$args]} - 7 {return [lzip7lists {*}$args]} - 8 {return [lzip8lists {*}$args]} - 9 {return [lzip9lists {*}$args]} - 10 {return [lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } else { - return [lzipn {*}$args] - } - } - } - } - - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - puts "proc punk::lib::lzip${n}lists {$arglist} \{" - puts "$body" - puts "\}" - proc ::punk::lib::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [punk::lib::check::has_tclbug_lsearch_strideallinline ]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::lzipn_tcl9a] - } - - - namespace import ::punk::args::lib::tstr - - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list array exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list array get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - -- -type none -optional 1 - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-2 for too low, -1 for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == -2} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -3} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve [llength $dval] $b] - if {$upper == -3} { - ##x - #upper bound is below list range - - if {$lower_resolve >=-2} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == -2} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact -- $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - #append result [textblock::join_basic -- $kblock $sblock $vblock] \n - append result [textblock::join_basic_raw $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - if {[llength $small] > [llength $large]} {return 0} - foreach x $large { - ::set ($x) {} - } - foreach x $small { - if {![info exists ($x)]} { - return 0 - } - } - return 1 - } - #v2 generally seems slower - proc is_list_all_in_list2 {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list2] - proc is_list_all_in_list2 {small large} $body - } - - proc is_list_all_ni_list {A B} { - foreach x $B { - ::set ($x) {} - } - foreach x $A { - if {[info exists ($x)]} { - return 0 - } - } - return 1 - } - proc is_list_all_ni_list2 {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list2] - proc is_list_all_ni_list2 {a b} $body - } - - #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist - #struct::set difference may happen to preserve ordering when items are integers, but order can't be relied on, - # especially as struct::set has 2 differing implementations (tcl vs critcl) which return results with different ordering to each other and different deduping behaviour in some cases (e.g empty 2nd arg) - proc ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - #with ledit (also avail in 8.6 using punk::lib::compat::ledit - proc ldiff2 {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - foreach item $removeitems { - set posns [lsearch -all -exact $fromlist $item] - foreach p $posns {ledit fromlist $p $p} - } - return $fromlist - } - proc ldiff3 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - puts stderr "WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - #order-preserving - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {(.*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - punk::args::define { - @id -id ::punk::lib::is_indexset - @cmd -name punk::lib::is_indexset\ - -summary\ - "Validate string is a comma-delimited 'indexset'."\ - -help\ - "Validate that a string is an 'indexset' - - An indexset consists of a comma delimited list of indexes or index-ranges. - The indexes are 0-based. - Ranges must be specified with .. as the separator. - Common whitespace elements space,tab,newlines are ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - see indexset_resolve" - @values -min 2 -max 2 - indexset -type string - } - proc is_indexset {indexset} { - #collapse internal whitespace (for basic whitespace set we allow) - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] - if {![regexp {^[\-\+_end,\.0-9]*$} $indexset]} { - return 0 - } - set ranges [split $indexset ,] - foreach r $ranges { - set validateindices [list] - set rposn [string first .. $r] - if {$rposn >= 0} { - lappend validateindices {*}[string range $r 0 $rposn-1] {*}[string range $r $rposn+2 end] - } else { - #'range' is just an index - set validateindices [list $r] - } - foreach v $validateindices { - if {$v eq "" || $v eq "end"} {continue} - if {[string is integer -strict $v]} {continue} - if {[catch {lindex {} $v}]} { - return 0 - } - } - } - return 1 - } - #review - compare to IMAP4 methods of specifying ranges? - punk::args::define { - @id -id ::punk::lib::indexset_resolve - @cmd -name punk::lib::indexset_resolve\ - -summary\ - "Resolve an indexset to a list of integers based on supplied list or string length."\ - -help\ - "Resolve an 'indexset' to a list of actual indices within the range of the provided numitems value. - e.g in a basic case: for a list of 10 items, 'indexset_resolve 10 end' will return the index 9 - - An indexset consists of a comma delimited list of indexes or index-ranges. - The indexes are 0-based. - Ranges must be specified with .. as the separator. - Whitespace is ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - end means the last item. - end-1 means the second last item. - 0.. is the same as 0..end. - indexset examples: - 1,3.. - output the index 1 (2nd item) followed by all from index 3 to the end. - 'indexset_resolve 4 1,3..' -> 1 3 - 'indexset_resolve 10 1,3..' -> 1 3 4 5 6 7 8 9 - 0-2,end - output the first 3 indices, and the last index. - end-1..0 - output the indexes in reverse order from 2nd last item to first item." - @values -min 2 -max 2 - numitems -type integer - indexset -type indexset -help "comma delimited specification for indices to return" - } - proc indexset_resolve {numitems indexset} { - if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { - #use parser on unhappy path only - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] ;#collapse basic whitespace - set index_list [list] ;#list of actual indexes within the range - set iparts [split $indexset ,] - set index_list [list] - foreach ipart $iparts { - set ipart [string trim $ipart] - set rposn [string first .. $ipart] - if {$rposn>=0} { - #range - lassign [punk::lib::string_splitbefore_indices $ipart $rposn $rposn+2] rawa _ rawb - set rawa [string trim $rawa] - set rawb [string trim $rawb] - if {$rawa eq ""} {set rawa 0} - set a [punk::lib::lindex_resolve $numitems $rawa] - if {$a == -3} { - #undershot - leave negative - } elseif {$a == -2 && $rawa ne "-2"} { - #overshot - set a [expr {$numitems}] ;#put it outside the range on the upper side - } - - if {$rawb eq ""} { - if {$a > $numitems-1} { - set rawb $a ;#make sure .. doesn't return last item - should return nothing - } else { - set rawb end - } - } - set b [punk::lib::lindex_resolve $numitems $rawb] - if {$b == -3} { - #undershot - leave negative - } elseif {$b == -2 && $rawb ne "-2"} { - set b [expr {$numitems}] ;#overshot - put it outside the range on the upper side - } - - #e.g make sure .. doesn't return last item - should return nothing as both are above the range. - if {$a >= 0 && $a <= $numitems-1 && $b >=0 && $b <= $numitems-1} { - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - if {$a >= 0 && $a <= $numitems-1} { - #only a is in the range - if {$b < 0} { - set b 0 - } else { - set b [expr {$numitems-1}] - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$b >=0 && $b <= $numitems-1} { - #only b is in the range - if {$a < 0} { - set a 0 - } else { - set a [expr {$numitems-1}] - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - #both outside the range - if {$a < 0 && $b > 0} { - #spans the range in forward order - set a 0 - set b [expr {$numitems-1}] - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$a > 0 && $b < 0} { - #spans the range in reverse order - set a [expr {$numitems-1}] - set b 0 - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } - #both outside of range on same side - } - } - } else { - set idx [punk::lib::lindex_resolve_basic $numitems $ipart] - if {$idx >= 0} { - lappend index_list $idx - } - } - } - return $index_list - } - # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bounds on upper vs lower side - #REVIEW: This shouldn't really need the list itself - just the length would suffice - punk::args::define { - @id -id ::punk::lib::lindex_resolve - @cmd -name punk::lib::lindex_resolve\ - -summary\ - "Resolve an indexexpression to an integer based on supplied list or string length."\ - -help\ - "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 - to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating - whether the index was below or above the range of possible indices for the length supplied. - - Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - This means the proc may be called with something like $x+2 end-$y etc - Sometimes the actual integer index is desired. - - We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - - which could have security risks. - lindex_resolve will parse the index expression and return: - a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) - b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - lindex_resolve never returns -1 - as the similar function lindex_resolve_basic uses this to denote - out of range at either end of the list/string. - Otherwise it will return an integer corresponding to the position in the data. - This is in stark contrast to Tcl list/string function indices which will return empty strings for out of - bounds indices, or in the case of lrange, return results anyway. - Like Tcl list commands - it will produce an error if the form of the index is not acceptable. - For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side - - thus returning -2 - - Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. - We will get something like 10+1 - which can be resolved safely with expr - " - @values -min 2 -max 2 - datalength -type integer - index -type indexexpression - } - proc lindex_resolve {len index} { - #*** !doctools - #[call [fun lindex_resolve] [arg len] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length - #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr - #if {![llength $list]} { - # #review - # return ??? - #} - if {![string is integer -strict $len]} { - #<0 ? - error "lindex_resolve len must be an integer" - } - set index [tcl::string::map {_ {}} $index] ;#basic forward compatibility with integers such as 1_000 for 8.6 - #todo - be stricter about malformations such as 1000_ - if {[string is integer -strict $index]} { - #can match +i -i - if {$index < 0} { - return -3 - } elseif {$index >= $len} { - return -2 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$op eq "+" && $offset != 0} { - return -2 - } - } else { - #index is 'end' - set index [expr {$len-1}] - if {$index < 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return -2 - } else { - return $index - } - } - if {$offset == 0} { - set index [expr {$len-1}] - if {$index < 0} { - return -2 ;#special case as above - } else { - return $index - } - } else { - #by now, if op = + then offset = 0 so we only need to handle the minus case - set index [expr {($len-1) - $offset}] - } - if {$index < 0} { - return -3 - } else { - return $index - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - if {[regexp {(.*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < 0} { - return -3 - } elseif {$index >= $len} { - return -2 - } - return $index - } - } - } - proc lindex_resolve_basic {len index} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg len] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -1 for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - if {![string is integer -strict $len]} { - error "lindex_resolve_basic len must be an integer" - } - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= $len)} { - #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. - return -1 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {$len > 0} { - #For large len - this is a wasteful allocation if no true lseq available in Tcl version. - #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) - set testlist [punk::lib::range 0 [expr {$len-1}]] ;# uses lseq if available, has fallback. - } else { - set testlist [list] - #we want to call 'lindex' even in this case - to get the appropriate error message - } - set idx [lindex $testlist $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -1 - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - proc string_splitbefore {str index} { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -2 { - return [list $str ""] - } - -3 { - return [list "" $str] - } - } - } - return [list [string range $str 0 $index-1] [string range $str $index end]] - #scan %s stops at whitespace - not useful here. - #scan $s %${p}s%s - } - proc string_splitbefore_indices {str args} { - set parts [list $str] - set sizes [list [string length $str]] - set s 0 - foreach index $args { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -2 { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - -3 { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - } - } - if {$index <= 0} { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - if {$index >= [string length $str]} { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - set i -1 - set a 0 - foreach sz $sizes { - incr i - if {$a + $sz > $index} { - set p [lindex $parts $i] - #puts "a:$a index:$index" - if {$a == $index} { - break - } - ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] - ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] - break - } - incr a $sz - } - #puts "->parts:$parts" - #puts "->sizes:$sizes" - } - return $parts - } - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - proc lcm {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the lowest common multiple of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para] - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - ;#package require punk::ansi - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list/detectcode_in_list will check at first level. (not intended for detecting ansi in deeper structures) - - #we use detectcode_in_list instead of detect_in_list - #detectcode_in_list will detect unclosed (or unopened) paired sequences such as PM (privacy message) - # - but the main reason is it is slightly faster. - if {![punk::ansi::ta::detectcode_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [string trim $ln] - if {!$in_jt} { - if {[string match *jumpTable* $ln]} { - append result $ln \n - set in_jt 1 - } - } else { - if {[string match Command* $tln] || [string match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc objclone {obj} { - append obj2 $obj {} - } - proc set_clone {varname obj} { - #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] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [objclone $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [objclone $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.2 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.3.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.3.tm deleted file mode 100644 index 234dbcb2..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.3.tm +++ /dev/null @@ -1,4842 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.3 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.3] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - - #NOTE - the extension ns becomes the '-namespace ' for the original routine name, - #with -unknown handling the original subcommands. - #This makes the original ensemble harder to introspect! - #e.g (the original -map or -namespace not visible) - #In this specific case (which, being published on the wiki might be common in the wild) - #we could call {*}[namespace ensemble configure $routine -unknown] $routine - #and then detect that the first resulting word is an ensemble - #For arbitrary '-unknown scripts' - sensible introspection is likely not possible - - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - return true - } else { - return false - } - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - return 0 - } - return [expr {$result ne "a2"}] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - return [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - return $has_bug - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - punk::args::set_idalias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - if {"::ledit" ni [info commands ::ledit]} { - interp alias {} ledit {} ::punk::lib::compat::ledit - punk::args::set_idalias ::punk::lib::compat::ledit ::ledit - } - proc ledit {lvar first last args} { - upvar $lvar l - #use lindex_resolve to support for example: ledit lst end+1 end+1 h i - set fidx [punk::lib::lindex_resolve [llength $l] $first] - switch -exact -- $fidx { - -3 { - #index below lower bound - set pre [list] - set fidx -1 - } - -2 { - #first index position is greater than index of last element in the list - set pre [lrange $l 0 end] - set fidx [llength $l] - } - default { - #set pre [lrange $l 0 $first-1] - set pre [lrange $l 0 $fidx-1] - } - } - set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -3 { - #index below lower bound - set post [lrange $l 0 end] - } - -2 { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] - set post [lrange $l $lidx+1 end] - } - } - } - set l [list {*}$pre {*}$args {*}$post] - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list ::tcl::info::vars]] - set nscaller [uplevel 1 [list ::tcl::namespace::current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - namespace eval argdoc { - #non-colour SGR codes - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - } - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - proc lswap {lvar a z} { - upvar $lvar l - set len [llength $l] - if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { - #lindex_resolve_basic returns only -1 if out of range - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned) - set a_index [lindex_resolve $len $a] - set a_msg "" - switch -- $a_index { - -2 { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - -3 { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - } - set z_index [lindex_resolve $len $z] - set z_msg "" - switch -- $z_index { - -2 { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - -3 { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [lzip2lists {*}$args]} - 3 {return [lzip3lists {*}$args]} - 4 {return [lzip4lists {*}$args]} - 5 {return [lzip5lists {*}$args]} - 6 {return [lzip6lists {*}$args]} - 7 {return [lzip7lists {*}$args]} - 8 {return [lzip8lists {*}$args]} - 9 {return [lzip9lists {*}$args]} - 10 {return [lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } else { - return [lzipn {*}$args] - } - } - } - } - - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - puts "proc punk::lib::lzip${n}lists {$arglist} \{" - puts "$body" - puts "\}" - proc ::punk::lib::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [punk::lib::check::has_tclbug_lsearch_strideallinline ]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::lzipn_tcl9a] - } - - - namespace import ::punk::args::lib::tstr - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::tclscript_to_scriptlist - @cmd -name punk::lib::tclscript_to_scriptlist\ - -summary\ - "Parse tcl script to toplevel list of lists."\ - -help\ - "Get topmost list of tcl language elements in script. - produces a list of lists where each sublist is a commandlist or - a comment string." - @values -min 1 -max 1 - script -type string - }] - } - proc tclscript_to_scriptlist {script} { - set scriptlist [list] - set cmdlist [list] - set scrlen [string length $script] - set token "" - set in_token 0 - set in_cmdlist 0 - set in_comment 0 - set charmap [list \t TB \n LF \r CR \\ BSL] ;#for switch 'jump' preservation - review - may be slower than escapes in switch statement? - for {set i 0} {$i < $scrlen} {incr i} { - set ch [string index $script $i] - set chswitch [string map $charmap $ch] - if {!$in_token} { - switch -- $chswitch { - { } - TB { - #ignore - continue being a non token - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {$in_cmdlist} { - #no active token - newline ends cmdlist - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - incr i - } - } - LF - ";" { - #no active token - newline or semicolon ends cmdlist - if {$in_cmdlist} { - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation of whitespace while no token - boring - incr i - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation of whitespace while no token - boring - incr i 2 - } else { - #an uncommon possibility, a command wth surrounding spaces called in an strange way - # e.g \ cmdname\ arg - set in_token 1 - set token "\\[string index $script $i+1]" - incr i - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - # { - if {$in_cmdlist} { - #ordinary data - set in_token 1 - set token # - } else { - if {!$in_comment} { - set in_token 1 - set in_comment 1 - set token # - } else { - #wnen in comment - all will be a single token until comment ends - append token # - } - } - } - default { - #for completeness.. we should exclude other possible whitespace chars - if {![string is space $ch]} { - set in_token 1 - set token $ch - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - } - } else { - #if we're in a token, we must be in a cmdlist or a comment (single token) - #review - not preserving whitespace in list of commands is ok, but for comments it should ideally be preserved - #note that unbalanced curly in *toplevel* comment will still 'info complete' to true - switch -- $chswitch { - LF { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ends a comment - lappend scriptlist $token ;#single token for comment - set token "" - set in_token 0 - set in_comment 0 - set in_cmdlist 0 ;#shouldn't be necessary, but included for clarity - } - } - ";" { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ordinary char for comment - append token ";" - } - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {[tcl::info::complete $token]} { - #ends token and commandlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \r\n - incr i - } - } else { - append token \r - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation - lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i ;#skip LF - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation - cr-lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i 2 ;#skip CRLF - } else { - append token "\\[string index $script $i+1]" - incr i - } - } - default { - if {![string is space $ch]} { - append token $ch - } else { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token $ch - } - } else { - append token $ch - } - } - } - } - } - } - #eof - if {!$in_comment} { - if {$in_token} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - lappend scriptlist $cmdlist - } else { - error "Eof reached whilst script incomplete. Unbalanced braces?\ntoken: '$token'" - } - } else { - if {$in_cmdlist} { - lappend scriptlist $cmdlist - } - } - } else { - lappend scriptlist $token - } - return $scriptlist - } - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list ::tcl::array::exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list ::tcl::array::get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - -- -type none -optional 1 - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-2 for too low, -1 for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == -2} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -3} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve [llength $dval] $b] - if {$upper == -3} { - ##x - #upper bound is below list range - - if {$lower_resolve >=-2} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == -2} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact -- $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - #append result [textblock::join_basic -- $kblock $sblock $vblock] \n - append result [textblock::join_basic_raw $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - if {[llength $small] > [llength $large]} {return 0} - foreach x $large { - ::set ($x) {} - } - foreach x $small { - if {![info exists ($x)]} { - return 0 - } - } - return 1 - } - #v2 generally seems slower - proc is_list_all_in_list2 {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list2] - proc is_list_all_in_list2 {small large} $body - } - - proc is_list_all_ni_list {A B} { - foreach x $B { - ::set ($x) {} - } - foreach x $A { - if {[info exists ($x)]} { - return 0 - } - } - return 1 - } - proc is_list_all_ni_list2 {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list2] - proc is_list_all_ni_list2 {a b} $body - } - - #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist - #struct::set difference may happen to preserve ordering when items are integers, but order can't be relied on, - # especially as struct::set has 2 differing implementations (tcl vs critcl) which return results with different ordering to each other and different deduping behaviour in some cases (e.g empty 2nd arg) - proc ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - #with ledit (also avail in 8.6 using punk::lib::compat::ledit - proc ldiff2 {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - foreach item $removeitems { - set posns [lsearch -all -exact $fromlist $item] - foreach p $posns {ledit fromlist $p $p} - } - return $fromlist - } - proc ldiff3 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - puts stderr "WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - #order-preserving - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {([^+-]*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - punk::args::define { - @id -id ::punk::lib::is_indexset - @cmd -name punk::lib::is_indexset\ - -summary\ - "Validate string is a comma-delimited 'indexset'."\ - -help\ - "Validate that a string is an 'indexset' - - An indexset consists of a comma delimited list of indexes or index-ranges. - The indexes are 0-based. - The normal 'range' specifier is .. - The range specifier can appear at the beginning, middle or end, or even alone to indicate the entire - range of valid values. - e.g the following are all valid ranges - 1.. - (index 1 to max) - ..10 - (index 0 to 10) - 2..11 - (index 2 to 11) - .. - (all indices) - Common whitespace elements space,tab,newlines are ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - see indexset_resolve" - @values -min 1 -max 1 - indexset -type string - } - proc is_indexset {indexset} { - #collapse internal whitespace (for basic whitespace set we allow) - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] - if {![regexp {^[\-\+_end,\.0-9]*$} $indexset]} { - return 0 - } - set ranges [split $indexset ,] - foreach r $ranges { - set validateindices [list] - set rposn [string first .. $r] - if {$rposn >= 0} { - set sepsize 2 - set step 1 - } else { - #check for .n. 'stepped' range - set fdot [string first . $r] - set ldot [string last . $r] - set step [string range $r $fdot+1 $ldot-1] - #todo - allow basic mathops for step: 2+1 2+-1 etc same as tcl lindex, lseq - if {![string is integer -strict $step]} { - } - } - - if {$rposn >= 0} { - lappend validateindices {*}[string range $r 0 $rposn-1] {*}[string range $r $rposn+2 end] - } else { - #'range' is just an index - set validateindices [list $r] - } - foreach v $validateindices { - if {$v eq "" || $v eq "end"} {continue} - if {[string is integer -strict $v]} {continue} - if {[catch {lindex {} $v}]} { - return 0 - } - } - } - return 1 - } - #review - compare to IMAP4 methods of specifying ranges? - punk::args::define { - @id -id ::punk::lib::indexset_resolve - @cmd -name punk::lib::indexset_resolve\ - -summary\ - "Resolve an indexset to a list of integers based on supplied list or string length."\ - -help\ - "Resolve an 'indexset' to a list of actual indices within the range of the provided numitems value. - e.g in a basic case: for a list of 10 items, 'indexset_resolve 10 end' will return the index 9 - - An indexset consists of a comma delimited list of indexes or index-ranges. - The indexes are 0-based. - Ranges must be specified with .. as the separator. - Whitespace is ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - end means the last item. - end-1 means the second last item. - 0.. is the same as 0..end. - indexset examples: - 1,3.. - output the index 1 (2nd item) followed by all from index 3 to the end. - 'indexset_resolve 4 1,3..' -> 1 3 - 'indexset_resolve 10 1,3..' -> 1 3 4 5 6 7 8 9 - 0-2,end - output the first 3 indices, and the last index. - end-1..0 - output the indexes in reverse order from 2nd last item to first item." - @values -min 2 -max 2 - numitems -type integer - indexset -type indexset -help "comma delimited specification for indices to return" - } - proc indexset_resolve {numitems indexset} { - if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { - #use parser on unhappy path only - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] ;#collapse basic whitespace - set index_list [list] ;#list of actual indexes within the range - set iparts [split $indexset ,] - set index_list [list] - foreach ipart $iparts { - set ipart [string trim $ipart] - set rposn [string first .. $ipart] - if {$rposn>=0} { - #range - lassign [punk::lib::string_splitbefore_indices $ipart $rposn $rposn+2] rawa _ rawb - set rawa [string trim $rawa] - set rawb [string trim $rawb] - if {$rawa eq ""} {set rawa 0} - set a [punk::lib::lindex_resolve $numitems $rawa] - if {$a == -3} { - #undershot - leave negative - } elseif {$a == -2 && $rawa ne "-2"} { - #overshot - set a [expr {$numitems}] ;#put it outside the range on the upper side - } - - if {$rawb eq ""} { - if {$a > $numitems-1} { - set rawb $a ;#make sure .. doesn't return last item - should return nothing - } else { - set rawb end - } - } - set b [punk::lib::lindex_resolve $numitems $rawb] - if {$b == -3} { - #undershot - leave negative - } elseif {$b == -2 && $rawb ne "-2"} { - set b [expr {$numitems}] ;#overshot - put it outside the range on the upper side - } - - #e.g make sure .. doesn't return last item - should return nothing as both are above the range. - if {$a >= 0 && $a <= $numitems-1 && $b >=0 && $b <= $numitems-1} { - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - if {$a >= 0 && $a <= $numitems-1} { - #only a is in the range - if {$b < 0} { - set b 0 - } else { - set b [expr {$numitems-1}] - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$b >=0 && $b <= $numitems-1} { - #only b is in the range - if {$a < 0} { - set a 0 - } else { - set a [expr {$numitems-1}] - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - #both outside the range - if {$a < 0 && $b > 0} { - #spans the range in forward order - set a 0 - set b [expr {$numitems-1}] - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$a > 0 && $b < 0} { - #spans the range in reverse order - set a [expr {$numitems-1}] - set b 0 - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } - #both outside of range on same side - } - } - } else { - set idx [punk::lib::lindex_resolve_basic $numitems $ipart] - if {$idx >= 0} { - lappend index_list $idx - } - } - } - return $index_list - } - # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bounds on upper vs lower side - #REVIEW: This shouldn't really need the list itself - just the length would suffice - punk::args::define { - @id -id ::punk::lib::lindex_resolve - @cmd -name punk::lib::lindex_resolve\ - -summary\ - "Resolve an indexexpression to an integer based on supplied list or string length."\ - -help\ - "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 - to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating - whether the index was below or above the range of possible indices for the length supplied. - - Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - This means the proc may be called with something like $x+2 end-$y etc - Sometimes the actual integer index is desired. - - We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - - which could have security risks. - lindex_resolve will parse the index expression and return: - a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) - b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - lindex_resolve never returns -1 - as the similar function lindex_resolve_basic uses this to denote - out of range at either end of the list/string. - Otherwise it will return an integer corresponding to the position in the data. - This is in stark contrast to Tcl list/string function indices which will return empty strings for out of - bounds indices, or in the case of lrange, return results anyway. - Like Tcl list commands - it will produce an error if the form of the index is not acceptable. - For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side - - thus returning -2 - - Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. - We will get something like 10+1 - which can be resolved safely with expr - " - @values -min 2 -max 2 - datalength -type integer - index -type indexexpression - } - proc lindex_resolve {len index} { - #*** !doctools - #[call [fun lindex_resolve] [arg len] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length - #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr - - - #REVIEW - we need compat for 1_000 etc to handle things like toml even in 8.6? - #A basic string map means we aren't properly validating - #todo - be stricter about malformations such as 1000_ - if {![string is integer -strict 1_0]} { - #basic forward compatibility with integers such as 1_000 for 8.6.x - set index [tcl::string::map {_ {}} $index] - set len [tcl::string::map {_ {}} $len] - } - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve len must be a positive integer" - } - - - if {[string is integer -strict $index]} { - #can match +i -i - if {$index < 0} { - return -3 - } elseif {$index >= $len} { - return -2 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - #note - offset could have leading + or - - # 'string is integer -strict +1' ==> true - #e.g end+-1 is valid (end++-1 is not) - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$offset == 0} { - #(offset +0, -0 or 0 or 000 0_0 etc) - #op either + or - is irrelevant - set index [expr {$len-1}] - if {$index < 0} { - return -2 ;#special case - equivalent to 'end', with empty list - treat like a positive number out of bounds - } else { - return $index - } - } - - set index [if {$op eq "+"} {expr {($len-1) + $offset}} else {expr {($len-1) - $offset}}] - if {$index < 0} { - return -3 - } elseif {$index > $len-1} { - return -2 - } else { - return $index - } - } else { - #index is 'end' - if {$len == 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return -2 - } - return [expr {$len - 1}] - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - #regexp must split a++b to a + +b (not a+ + b) ie first +/- is the op - if {[regexp {([^+-]*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < 0} { - return -3 - } elseif {$index >= $len} { - return -2 - } - return $index - } - } - } - proc lindex_resolve_basic {len index} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg len] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -1 for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - if {![string is integer -strict $len]} { - error "lindex_resolve_basic len must be an integer" - } - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= $len)} { - #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. - return -1 - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {$len > 0} { - #For large len - this is a wasteful allocation if no true lseq available in Tcl version. - #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) - set testlist [punk::lib::range 0 [expr {$len-1}]] ;# uses lseq if available, has fallback. - } else { - set testlist [list] - #we want to call 'lindex' even in this case - to get the appropriate error message - } - set idx [lindex $testlist $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -1 - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - proc string_splitbefore {str index} { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -2 { - return [list $str ""] - } - -3 { - return [list "" $str] - } - } - } - return [list [string range $str 0 $index-1] [string range $str $index end]] - #scan %s stops at whitespace - not useful here. - #scan $s %${p}s%s - } - proc string_splitbefore_indices {str args} { - set parts [list $str] - set sizes [list [string length $str]] - set s 0 - foreach index $args { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -2 { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - -3 { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - } - } - if {$index <= 0} { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - if {$index >= [string length $str]} { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - set i -1 - set a 0 - foreach sz $sizes { - incr i - if {$a + $sz > $index} { - set p [lindex $parts $i] - #puts "a:$a index:$index" - if {$a == $index} { - break - } - ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] - ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] - break - } - incr a $sz - } - #puts "->parts:$parts" - #puts "->sizes:$sizes" - } - return $parts - } - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - proc lcm {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the lowest common multiple of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para] - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text {max -1}} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - if {$max != -1} { - set len [expr {min($len,$max)}] - } - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - ;#package require punk::ansi - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list/detectcode_in_list will check at first level. (not intended for detecting ansi in deeper structures) - - #we use detectcode_in_list instead of detect_in_list - #detectcode_in_list will detect unclosed (or unopened) paired sequences such as PM (privacy message) - # - but the main reason is it is slightly faster. - if {![punk::ansi::ta::detectcode_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [::tcl::string::trim $ln] - if {!$in_jt} { - if {[::tcl::string::match *jumpTable* $ln]} { - punk::ns::call_frame - append result $ln \n - set in_jt 1 - } - } else { - if {[::tcl::string::match Command* $tln] || [::tcl::string::match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - #a test - # punk::ns::cmdtracereturn punk::lib::disassemble ::punk::ns::test_switch4 - # Note the different disassemble result when trace is running. - proc disassemble {procname} { - tcl::unsupported::disassemble proc $procname - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc valcopy {obj} { - append obj2 $obj {} - } - proc set_valcopy {varname obj} { - #used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_valcopy varnmame $val] - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [valcopy $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [valcopy $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [valcopy $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - #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? - if {$has_twapi} { - interp alias "" ::punk::lib::uuid "" twapi::new_uuid - } else { - catch {package require uuid} - interp alias "" ::punk::lib::uuid "" uuid::uuid generate - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.3 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.4.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.4.tm deleted file mode 100644 index 5f5b7f0d..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.4.tm +++ /dev/null @@ -1,5008 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.4 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.4] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - - #NOTE - the extension ns becomes the '-namespace ' for the original routine name, - #with -unknown handling the original subcommands. - #This makes the original ensemble harder to introspect! - #e.g (the original -map or -namespace not visible) - #In this specific case (which, being published on the wiki might be common in the wild) - #we could call {*}[namespace ensemble configure $routine -unknown] $routine - #and then detect that the first resulting word is an ensemble - #For arbitrary '-unknown scripts' - sensible introspection is likely not possible - - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - #These are just a selection of bugs relevant to punk behaviour (or of specific interest to the author) - #Not any sort of comprehensive check of known tcl bugs. - #These are reported in warning output of 'help tcl' - or used for workarounds in some cases. - proc has_tclbug_regexp_emptystring {} { - #The regexp {} [...] trick - code in brackets only runs when non byte-compiled ie in traces - #This was usable as a hack to create low-impact calls that only ran in an execution trace context - handy for debugger logic, - #but as an apparent violation of Tcl's normal parsing rules - was evidently seen as a bug and fixed in: - #https://core.tcl-lang.org/tcl/info/cb03e57a (tcl 9.0.3+ ?) - set bug [expr {![catch {regexp {} [error should_error]}]}] - return [dict create bug $bug bugref cb03e57a description {regexp emptystring first argument over-optimised - difference in compiled vs traced behaviour.} level minor] - } - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - set bug true - } else { - set bug false - } - set description "string rep for list variable in script generated when script changed\n(not an acknowledged/reported bug)" - return [dict create bug $bug bugref "" description $description level minor] - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - set bug 0 - } else { - set bug [expr {$result ne "a2"}] - } - set description "lsearch -stride with -subindices -inline -all and single index - incorrect results." - return [dict create bug $bug bugref 5a1aaa201d description $description level major] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - set bug [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - set description "lists elements not properly quoted in some cases. e.g 'list {*}[lindex {etc #foo} 1] {*}[list]' (#foo not braced)" - return [dict create bug $bug bugref e38dc74e2 description $description level medium] - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - #https://core.tcl-lang.org/tcl/tktview/1095bf7f756f9aed6bde - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - set description "ensemble commands not compiled in safe interps - heavy performance impact in safe interps" - return [dict create bug $has_bug bugref 1095bf7f756f9aed6bde description $description level major] - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - punk::args::set_idalias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - if {"::ledit" ni [info commands ::ledit]} { - interp alias {} ledit {} ::punk::lib::compat::ledit - punk::args::set_idalias ::punk::lib::compat::ledit ::ledit - } - proc ledit {lvar first last args} { - upvar $lvar l - #use lindex_resolve to support for example: ledit lst end+1 end+1 h i - set fidx [punk::lib::lindex_resolve [llength $l] $first] - switch -exact -- $fidx { - -Inf { - #index below lower bound - set pre [list] - set fidx -1 - } - Inf { - #first index position is greater than index of last element in the list - set pre [lrange $l 0 end] - set fidx [llength $l] - } - default { - #set pre [lrange $l 0 $first-1] - set pre [lrange $l 0 $fidx-1] - } - } - set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -Inf { - #index below lower bound - set post [lrange $l 0 end] - } - Inf { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] - set post [lrange $l $lidx+1 end] - } - } - } - set l [list {*}$pre {*}$args {*}$post] - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list ::tcl::info::vars]] - set nscaller [uplevel 1 [list ::tcl::namespace::current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - namespace eval argdoc { - #non-colour SGR codes - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::is_main_script - @cmd -name punk::lib::is_main_script\ - -summary\ - "Test if current script is primary script being executed."\ - -help\ - "" - @values -min 0 -max 0 - }] - } - proc is_main_script {} { - #see https://wiki.tcl-lang.org/page/main+script - if {[info script] ne "" && [info exists ::argv0] - && - [file dirname [file normalize [file join [info script] ...]]] - eq - [file dirname [file normalize [file join $::argv0 ...]]] - } { - return true - } else { - return false - } - } - - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - proc lswap {lvar a z} { - upvar $lvar l - set len [llength $l] - if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { - #lindex_resolve_basic returns only -Inf if out of range at either bound - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -Inf and Inf special results being lower and upper bound breaches respectively - set a_index [lindex_resolve $len $a] - set a_msg "" - switch -- $a_index { - -Inf { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - Inf { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - } - set z_index [lindex_resolve $len $z] - set z_msg "" - switch -- $z_index { - -Inf { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - Inf { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to} { - lseq $from $to - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - proc range {from to} { - set to [offset_expr $to] - set from [offset_expr $from] - if {$to > $from} { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - set count [expr {$from - $to} + 1] - #1) - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [lzip2lists {*}$args]} - 3 {return [lzip3lists {*}$args]} - 4 {return [lzip4lists {*}$args]} - 5 {return [lzip5lists {*}$args]} - 6 {return [lzip6lists {*}$args]} - 7 {return [lzip7lists {*}$args]} - 8 {return [lzip8lists {*}$args]} - 9 {return [lzip9lists {*}$args]} - 10 {return [lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::lzip${n}lists] eq ""} { - puts "calling ::punk::lib::Build_lzipn $n" - ::punk::lib::Build_lzipn $n - } - return [lzip${n}lists {*}$args] - } else { - return [lzipn {*}$args] - } - } - } - } - - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - puts "proc punk::lib::lzip${n}lists {$arglist} \{" - puts "$body" - puts "\}" - proc ::punk::lib::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [dict get [punk::lib::check::has_tclbug_lsearch_strideallinline] bug]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::lzipn_tcl9a] - } - - - namespace import ::punk::args::lib::tstr - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::tclscript_to_scriptlist - @cmd -name punk::lib::tclscript_to_scriptlist\ - -summary\ - "Parse tcl script to toplevel list of lists."\ - -help\ - "Get topmost list of tcl language elements in script. - produces a list of lists where each sublist is a commandlist or - a comment string." - @values -min 1 -max 1 - script -type string - }] - } - proc tclscript_to_scriptlist {script} { - set scriptlist [list] - set cmdlist [list] - set scrlen [string length $script] - set token "" - set in_token 0 - set in_cmdlist 0 - set in_comment 0 - set charmap [list \t TB \n LF \r CR \\ BSL] ;#for switch 'jump' preservation - review - may be slower than escapes in switch statement? - for {set i 0} {$i < $scrlen} {incr i} { - set ch [string index $script $i] - set chswitch [string map $charmap $ch] - if {!$in_token} { - switch -- $chswitch { - { } - TB { - #ignore - continue being a non token - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {$in_cmdlist} { - #no active token - newline ends cmdlist - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - incr i - } - } - LF - ";" { - #no active token - newline or semicolon ends cmdlist - if {$in_cmdlist} { - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation of whitespace while no token - boring - incr i - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation of whitespace while no token - boring - incr i 2 - } else { - #an uncommon possibility, a command wth surrounding spaces called in an strange way - # e.g \ cmdname\ arg - set in_token 1 - set token "\\[string index $script $i+1]" - incr i - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - # { - if {$in_cmdlist} { - #ordinary data - set in_token 1 - set token # - } else { - if {!$in_comment} { - set in_token 1 - set in_comment 1 - set token # - } else { - #wnen in comment - all will be a single token until comment ends - append token # - } - } - } - default { - #for completeness.. we should exclude other possible whitespace chars - if {![string is space $ch]} { - set in_token 1 - set token $ch - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - } - } else { - #if we're in a token, we must be in a cmdlist or a comment (single token) - #review - not preserving whitespace in list of commands is ok, but for comments it should ideally be preserved - #note that unbalanced curly in *toplevel* comment will still 'info complete' to true - switch -- $chswitch { - LF { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ends a comment - lappend scriptlist $token ;#single token for comment - set token "" - set in_token 0 - set in_comment 0 - set in_cmdlist 0 ;#shouldn't be necessary, but included for clarity - } - } - ";" { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ordinary char for comment - append token ";" - } - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {[tcl::info::complete $token]} { - #ends token and commandlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \r\n - incr i - } - } else { - append token \r - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation - lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i ;#skip LF - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation - cr-lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i 2 ;#skip CRLF - } else { - append token "\\[string index $script $i+1]" - incr i - } - } - default { - if {![string is space $ch]} { - append token $ch - } else { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token $ch - } - } else { - append token $ch - } - } - } - } - } - } - #eof - if {!$in_comment} { - if {$in_token} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - lappend scriptlist $cmdlist - } else { - error "Eof reached whilst script incomplete. Unbalanced braces?\ntoken: '$token'" - } - } else { - if {$in_cmdlist} { - lappend scriptlist $cmdlist - } - } - } else { - lappend scriptlist $token - } - return $scriptlist - } - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - (todo - change to indexset syntax @1..3 @1..end-1 etc) - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list ::tcl::array::exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list ::tcl::array::get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - -- -type none -optional 1 - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - - #TODO - change to indexset notation 0..1,3..end-1 etc - - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - #TODO - fix terminology. 'lower_resolve' is confusing here as range can be in descending order - #change to start/end terminology? - - set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-Inf for too low, Inf for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == Inf} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -Inf} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve [llength $dval] $b] - if {$upper == -Inf} { - ##x - #upper bound is below list range - - if {$lower_resolve > -Inf} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == Inf} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact -- $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - #append result [textblock::join_basic -- $kblock $sblock $vblock] \n - append result [textblock::join_basic_raw $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - if {[llength $small] > [llength $large]} {return 0} - foreach x $large { - ::set ($x) {} - } - foreach x $small { - if {![info exists ($x)]} { - return 0 - } - } - return 1 - } - #v2 generally seems slower - proc is_list_all_in_list2 {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list2] - proc is_list_all_in_list2 {small large} $body - } - - proc is_list_all_ni_list {A B} { - foreach x $B { - ::set ($x) {} - } - foreach x $A { - if {[info exists ($x)]} { - return 0 - } - } - return 1 - } - proc is_list_all_ni_list2 {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list2] - proc is_list_all_ni_list2 {a b} $body - } - - #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist - #struct::set difference may happen to preserve ordering when items are integers, but order can't be relied on, - # especially as struct::set has 2 differing implementations (tcl vs critcl) which return results with different ordering to each other and different deduping behaviour in some cases (e.g empty 2nd arg) - proc ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - #with ledit (also avail in 8.6 using punk::lib::compat::ledit - proc ldiff2 {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - foreach item $removeitems { - set posns [lsearch -all -exact $fromlist $item] - foreach p $posns {ledit fromlist $p $p} - } - return $fromlist - } - proc ldiff3 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - puts stderr "WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - #order-preserving - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {([^+-]*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - punk::args::define { - @id -id ::punk::lib::is_indexset - @cmd -name punk::lib::is_indexset\ - -summary\ - "Validate string is a comma-delimited 'indexset'."\ - -help\ - "Validate that a string is an 'indexset' - - An indexset consists of a comma delimited list of indexes or index-ranges. - No particular base is assumed for the purposes of validating an indexset here. - While in Tcl, lists are zero-based - an indexset can be applied to lists of any base. - e.g -10..-1 is an indexset that just won't resolve any results for a list with a base >= 0. - To validate if an indexset is strictly within range, both the length of the data and the base would - need to be considered. - - The normal 'range' specifier is .. - The range specifier can appear at the beginning, middle or end, or even alone to indicate the entire - range of valid values. - e.g the following are all valid ranges - 1.. - (index 1 to 'max') - ..10 - (index 'base' to 10) - 2..11 - (index 2 to 11) - .. - (all indices) - Common whitespace elements space,tab,newlines are ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - see indexset_resolve" - @values -min 1 -max 1 - indexset -type string - } - proc is_indexset {indexset} { - #collapse internal whitespace (for basic whitespace set we allow) - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] - if {![regexp {^[\-\+_end,\.0-9]*$} $indexset]} { - return 0 - } - set ranges [split $indexset ,] - foreach r $ranges { - set validateindices [list] - set rposn [string first .. $r] - if {$rposn >= 0} { - set sepsize 2 - set step 1 - } else { - #check for .n. 'stepped' range - set fdot [string first . $r] - set ldot [string last . $r] - set step [string range $r $fdot+1 $ldot-1] - #todo - allow basic mathops for step: 2+1 2+-1 etc same as tcl lindex, lseq - if {![string is integer -strict $step]} { - } - } - - if {$rposn >= 0} { - lappend validateindices {*}[string range $r 0 $rposn-1] {*}[string range $r $rposn+2 end] - } else { - #'range' is just an index - set validateindices [list $r] - } - foreach v $validateindices { - if {$v eq "" || $v eq "end"} {continue} - if {[string is integer -strict $v]} {continue} - if {[catch {lindex {} $v}]} { - return 0 - } - } - } - return 1 - } - #review - compare to IMAP4 methods of specifying ranges? - punk::args::define { - @id -id ::punk::lib::indexset_resolve - @cmd -name punk::lib::indexset_resolve\ - -summary\ - "Resolve an indexset to a list of integers based on supplied list or string length."\ - -help\ - "Resolve an 'indexset' to a list of actual indices within the range of the provided numitems value. - e.g in a basic case: for a list of 10 items, 'indexset_resolve 10 end' will return the index 9 - - An indexset consists of a comma delimited list of indexes or index-ranges. - Ranges must be specified with .. as the separator, with an empty value at either side of the - separator representing beginning and end of the index range respectively. - - The indexes are 0-based by default, but the base can be specified. - indexset_resolve 7 .. - -> 0 1 2 3 4 5 6 - indexset_resolve 7 .. -3 - -> -3 -2 -1 0 1 2 3 - - Whitespace is ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - end means the last item. - end-1 means the second last item. - 0.. is the same as 0..end - - indexset examples: - - These assume the default 0-based indices (base == 0) - - 1,3.. - output the index 1 (2nd item) followed by all from index 3 to the end. - indexset_resolve 4 1,3.. - -> 1 3 - indexset_resolve 10 1,3.. - -> 1 3 4 5 6 7 8 9 - 0..2,end - output the first 3 indices, and the last index. - end-1..0 - output the indexes in reverse order from 2nd last item to first item." - @values -min 2 -max 3 - numitems -type integer - indexset -type indexset -help "comma delimited specification for indices to return" - base -type integer -default 0 -help\ - "This is the starting index. It can be positive, negative or zero. - This affects the start and end calculations, limiting what indices will be - returned. - e.g with base 1 'end' will give a different value from base 0 - - for 10 items 'end' is 10 when 1-based - for 10 items 'end' is 9 when 0-based - - For base 1, index 0 is considered to be below the range. - ie - indexset_resolve 10 0..3 1 - -> 1 2 3 - indexset_resolve 10 0..3 0 - -> 0 1 2 3 - - It does not *convert* integers within the range. - - indexset_resolve 10 5 1 - -> 5 - indexset_resolve 10 5 0 - -> 5 - - ie if you ask for a 1 based indexset the integers that are within the - range will come out the same, so the result needs to be treated as a - 1-based set of indices when performing further operations. - " - } - proc indexset_resolve {numitems indexset {base 0}} { - if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { - #use parser on unhappy path only - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] ;#collapse basic whitespace - set index_list [list] ;#list of actual indexes within the range - set iparts [split $indexset ,] - set based_max [expr {$numitems -1 + $base}] - - foreach ipart $iparts { - set ipart [string trim $ipart] - set rposn [string first .. $ipart] - if {$rposn>=0} { - #range - lassign [punk::lib::string_splitbefore_indices $ipart $rposn $rposn+2] rawa _ rawb - set rawa [string trim $rawa] - set rawb [string trim $rawb] - if {$rawa eq ""} {set rawa $base} - set a [punk::lib::lindex_resolve $numitems $rawa $base] - if {$a == -Inf} { - #(was -3) - #undershot - leave negative - } elseif {$a == Inf} { - #overshot - set a [expr {$based_max + 1}] ;#put it outside the range on the upper side - } - #review - a may be -Inf - - if {$rawb eq ""} { - if {$a > $based_max} { - set rawb $a ;#make sure .. doesn't return last item - should return nothing - } else { - set rawb end - } - } - set b [punk::lib::lindex_resolve $numitems $rawb $base] - if {$b == -Inf} { - #undershot - leave negative - } elseif {$b == Inf} { - #set b [expr {$numitems}] ;#overshot - put it outside the range on the upper side - set b [expr {$based_max + 1}] ;#overshot - put it outside the range on the upper side - } - - #JJJ - - #e.g make sure .. doesn't return last item - should return nothing as both are above the range. - if {$a >= $base && $a <= $based_max && $b >=$base && $b <= $based_max} { - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - if {$a >= $base && $a <= $based_max} { - #only a is in the range - if {$b < $base} { - set b $base - } else { - set b $based_max - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$b >=$base && $b <= $based_max} { - #only b is in the range - if {$a < $base} { - set a $base - } else { - set a $based_max - } - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - #both outside the range - if {$a < $base && $b > $base} { - #spans the range in forward order - set a $base - set b $based_max - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$a > $base && $b < $base} { - #spans the range in reverse order - set a $based_max - set b $base - lappend index_list {*}[punk::lib::range $a $b] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } - #both outside of range on same side - } - } - } else { - set idx [punk::lib::lindex_resolve_basic $numitems $ipart $base] - #returns only -Inf for out of range at either end - if {$idx >= $base} { - #index within the range - lappend index_list $idx - } - } - } - return $index_list - } - # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side - #This doesn't need the list itself - just the length suffices. - punk::args::define { - @id -id ::punk::lib::lindex_resolve - @cmd -name punk::lib::lindex_resolve\ - -summary\ - "Resolve an indexexpression to an integer based on supplied list or string length."\ - -help\ - "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 - to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating - whether the index was below or above the range of possible indices for the length supplied. - - Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - This means the proc may be called with something like $x+2 end-$y etc - Sometimes the actual integer index is desired. - - We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - - which could have security risks. - lindex_resolve will parse the index expression and return: - a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - The similar function lindex_resolve_basic uses -Inf to denote - out of range at either end of the list/string. - Otherwise it will return an integer corresponding to the position in the data. - This is in stark contrast to Tcl list/string function indices which will return empty strings for out of - bounds indices, or in the case of lrange, return results anyway. - Like Tcl list commands - it will produce an error if the form of the index is not acceptable. - For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side - - thus returning -2 - - Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. - We will get something like 10+1 - which can be resolved safely with expr - " - @values -min 2 -max 2 - datalength -type integer - index -type indexexpression - } - proc lindex_resolve {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve] [arg len] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length - #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr - - - #REVIEW - we need compat for 1_000 etc to handle things like toml even in 8.6? - #A basic string map means we aren't properly validating - #todo - be stricter about malformations such as 1000_ - if {![string is integer -strict 1_0]} { - #basic forward compatibility with integers such as 1_000 for 8.6.x - set index [tcl::string::map {_ {}} $index] - set len [tcl::string::map {_ {}} $len] - } - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve len must be a positive integer." - } - set based_max [expr {$len -1 + $base}] - - if {[string is integer -strict $index]} { - #review - base? - #can match +i -i - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - #note - offset could have leading + or - - # 'string is integer -strict +1' ==> true - #e.g end+-1 is valid (end++-1 is not) - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$offset == 0} { - #(offset +0, -0 or 0 or 000 0_0 etc) - #op either + or - is irrelevant - #set index [expr {$len-1}] ;#+ base ? - set index $based_max - if {$index < $base} { - #return -2 ;#special case - equivalent to 'end', with empty list - treat like a positive number out of bounds - return Inf - } else { - return $index - } - } - - #set index [if {$op eq "+"} {expr {($len-1) + $offset}} else {expr {($len-1) - $offset}}] - set index [if {$op eq "+"} {expr {$based_max + $offset}} else {expr {$based_max - $offset}}] - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - return $index - } - } else { - #index is 'end' - if {$len == 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return Inf - } - #return [expr {$len - 1 + $base}] - return $based_max - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - #regexp must split a++b to a + +b (not a+ + b) ie first +/- is the op - if {[regexp {([^+-]*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } - return $index - } - } - } - proc lindex_resolve_basic {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg len] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -Inf for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve_basic len must be an integer greater than or equal to zero" - } - if {![string is integer -strict $base]} { - #base can be negative - error "lindex_resolve_basic base must be an integer" - } - set based_max [expr {$len -1 + $base}] - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < $base || ($index > $based_max)} { - #even though in this case we could return -Inf or Inf like lindex_resolve; - #for consistency we don't return Inf for upper-boudn violation, - #as which bound is violated is not always directly determinable for compound index expressions (such as end-x) using the lseq+lindex mechanism. - return -Inf - } else { - #!NOTE! index within range is unchanged - no matter the base - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {$len > 0} { - #For large len - this is a wasteful allocation if no true lseq available in Tcl version. - #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) - set testlist [punk::lib::range $base $based_max] ;# uses lseq if available, has fallback of creating a potentially large list of numbers. - } else { - set testlist [list] - #we want to call 'lindex' even in this case - to get the appropriate error message - } - set idx [lindex $testlist $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -Inf - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - proc string_splitbefore {str index} { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - return [list "" $str] - } - Inf { - return [list $str ""] - } - } - } - return [list [string range $str 0 $index-1] [string range $str $index end]] - #scan %s stops at whitespace - not useful here. - #scan $s %${p}s%s - } - proc string_splitbefore_indices {str args} { - set parts [list $str] - set sizes [list [string length $str]] - set s 0 - foreach index $args { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - Inf { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - } - } - if {$index <= 0} { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - if {$index >= [string length $str]} { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - set i -1 - set a 0 - foreach sz $sizes { - incr i - if {$a + $sz > $index} { - set p [lindex $parts $i] - #puts "a:$a index:$index" - if {$a == $index} { - break - } - ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] - ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] - break - } - incr a $sz - } - #puts "->parts:$parts" - #puts "->sizes:$sizes" - } - return $parts - } - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::factors - @cmd -name punk::lib::factors\ - -summary\ - "Sorted factors of positive integer x"\ - -help\ - "Return a sorted list of the positive factors of x where x > 0 - For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. - (including zero itself in this context)* - - This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - Comparisons were done with some numbers below 17 digits long - For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - but has the disadvantage of being slower for 'small' numbers and using more memory. - If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get - there than computing the whole list, even for small values of x - * Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - In other mathematical contexts zero may be considered not to divide anything." - @values -min 1 -max 1 - x -type integer - }] - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::gcd - @cmd -name punk::lib::gcd\ - -summary\ - "Gretest common divisor of m and n."\ - -help\ - "Return the greatest common divisor of m and n. - Straight from Lars Hellström's math::numtheory library in Tcllib - - Graphical use: - An a by b rectangle can be covered with square tiles of side-length c, - only if c is a common divisor of a and b" - @values -min 2 -max 2 - m -type integer - n -type integer - }] - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - proc lcm {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the lowest common multiple of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para] - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text {max -1}} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - if {$max != -1} { - set len [expr {min($len,$max)}] - } - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - ;#package require punk::ansi - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list/detectcode_in_list will check at first level. (not intended for detecting ansi in deeper structures) - - #we use detectcode_in_list instead of detect_in_list - #detectcode_in_list will detect unclosed (or unopened) paired sequences such as PM (privacy message) - # - but the main reason is it is slightly faster. - if {![punk::ansi::ta::detectcode_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [::tcl::string::trim $ln] - if {!$in_jt} { - if {[::tcl::string::match *jumpTable* $ln]} { - punk::ns::call_frame - append result $ln \n - set in_jt 1 - } - } else { - if {[::tcl::string::match Command* $tln] || [::tcl::string::match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - #a test - # punk::ns::cmdtracereturn punk::lib::disassemble ::punk::ns::test_switch4 - # Note the different disassemble result when trace is running. - proc disassemble {procname} { - tcl::unsupported::disassemble proc $procname - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc valcopy {obj} { - append obj2 $obj {} - } - proc set_valcopy {varname obj} { - #used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_valcopy varnmame $val] - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [valcopy $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [valcopy $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [valcopy $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - #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? - if {$has_twapi} { - interp alias "" ::punk::lib::uuid "" twapi::new_uuid - } else { - catch {package require uuid} - interp alias "" ::punk::lib::uuid "" uuid::uuid generate - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.4 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.5.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.5.tm deleted file mode 100644 index 5138ac6d..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.5.tm +++ /dev/null @@ -1,5463 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix 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) 2024 -# -# @@ Meta Begin -# Application punk::lib 0.1.5 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::lib 0 0.1.5] -#[copyright "2024"] -#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk library}] [comment {-- Description at end of page heading --}] -#[require punk::lib] -#[keywords module utility lib] -#[description] -#[para]This is a set of utility functions that are commonly used across punk modules or are just considered to be general-purpose functions. -#[para]The base set includes string and math functions but has no specific theme - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::lib -#[subsection Concepts] -#[para]The punk::lib modules should have no strong dependencies other than Tcl -#[para]Dependendencies that only affect display or additional functionality may be included - but should fail gracefully if not present, and only when a function is called that uses one of these soft dependencies. -#[para]This requirement for no strong dependencies, means that many utility functions that might otherwise seem worthy of inclusion here are not present. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::lib -#[list_begin itemized] - -package require Tcl 8.6- -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] -#[item] [package {punk::args}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - - -tcl::namespace::eval punk::lib::ensemble { - #wiki.tcl-lang.org/page/ensemble+extend - # extend an ensemble-like routine with the routines in some namespace - - #NOTE - the extension ns becomes the '-namespace ' for the original routine name, - #with -unknown handling the original subcommands. - #This makes the original ensemble harder to introspect! - #e.g (the original -map or -namespace not visible) - #In this specific case (which, being published on the wiki might be common in the wild) - #we could call {*}[namespace ensemble configure $routine -unknown] $routine - #and then detect that the first resulting word is an ensemble - #For arbitrary '-unknown scripts' - sensible introspection is likely not possible - - proc extend {routine extension} { - if {![string match ::* $routine]} { - set resolved [uplevel 1 [list ::tcl::namespace::which $routine]] - if {$resolved eq {}} { - error [list {no such routine} $routine] - } - set routine $resolved - } - set routinens [tcl::namespace::qualifiers $routine] - if {$routinens eq {::}} { - set routinens {} - } - set routinetail [tcl::namespace::tail $routine] - - if {![string match ::* $extension]} { - set extension [uplevel 1 [ - list [tcl::namespace::which namespace] current]]::$extension - } - - if {![tcl::namespace::exists $extension]} { - error [list {no such namespace} $extension] - } - - set extension [tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] current]] - - tcl::namespace::eval $extension [ - list [tcl::namespace::which namespace] export *] - - while 1 { - set renamed ${routinens}::${routinetail}_[clock clicks] ;#clock clicks unlikely to collide when not directly consecutive such as: list [clock clicks] [clock clicks] - if {[tcl::namespace::which $renamed] eq {}} break - } - - rename $routine $renamed - - tcl::namespace::eval $extension [ - list namespace ensemble create -command $routine -unknown [ - list apply {{renamed ensemble routine args} { - list $renamed $routine - }} $renamed - ] - ] - - return $routine - } -} - -# some (?) tcl bug check procs needed to exist before main punk::lib namespaces are evaluated -tcl::namespace::eval punk::lib::check { - #These are just a selection of bugs relevant to punk behaviour (or of specific interest to the author) - #Not any sort of comprehensive check of known tcl bugs. - #These are reported in warning output of 'help tcl' - or used for workarounds in some cases. - proc has_tclbug_regexp_emptystring {} { - #The regexp {} [...] trick - code in brackets only runs when non byte-compiled ie in traces - #This was usable as a hack to create low-impact calls that only ran in an execution trace context - handy for debugger logic, - #but as an apparent violation of Tcl's normal parsing rules - was evidently seen as a bug and fixed in: - #https://core.tcl-lang.org/tcl/info/cb03e57a (tcl 9.0.3+ ?) - set bug [expr {![catch {regexp {} [error should_error]}]}] - return [dict create bug $bug bugref cb03e57a description {regexp emptystring first argument over-optimised - difference in compiled vs traced behaviour.} level minor] - } - proc has_tclbug_lsearch_sorted_inline_subindices {} { - if {[catch {lsearch -sorted -subindices -inline -index 0 {{a 1} {a 2} {b 3} {c 4} {c 5}} b} result]} { - #probably tcl version doesn't support all options - set bug 0 - } else { - set bug [expr {$result ne "b"}] - } - set description "lsearch -sorted with -subindices -inline - incorrect result." - return [dict create bug $bug bugref bc4ac0 description $description level minor] - } - proc has_tclbug_script_var {} { - - set script {set j [list spud] ; list} - append script \n - uplevel #0 $script - set rep1 [tcl::unsupported::representation $::j] - set script "" - set rep2 [tcl::unsupported::representation $::j] - - set nostring1 [string match "*no string" $rep1] - set nostring2 [string match "*no string" $rep2] - - #we assume it should have no string rep in either case - #Review: check Tcl versions for behaviour/consistency - if {!$nostring2} { - set bug true - } else { - set bug false - } - set description "string rep for list variable in script generated when script changed\n(not an acknowledged/reported bug)" - return [dict create bug $bug bugref "" description $description level minor] - } - proc has_tclbug_lsearch_strideallinline {} { - #bug only occurs with single -index value combined with -stride -all -inline -subindices - #https://core.tcl-lang.org/tcl/tktview/5a1aaa201d - if {[catch {lsearch -stride 3 -all -inline -index 1 -subindices {a1 a2 a3} *} result]} { - #we aren't looking for an error result - error most likely indicates tcl too old to support -stride - set bug 0 - } else { - set bug [expr {$result ne "a2"}] - } - set description "lsearch -stride with -subindices -inline -all and single index - incorrect results." - return [dict create bug $bug bugref 5a1aaa201d description $description level major] - } - proc has_tclbug_lseq_sign {} { - #https://core.tcl-lang.org/tcl/tktview/999b6966b2 - if {[catch {lseq 1 10}]} { - set bug 0 - } else { - set r1 [lseq 1 10 -9] - set r2 [lseq 1 10 -10] - set bug [expr {$r1 ne $r2}] - } - set description "lseq step sign not matching sequence direction - inconsistent results." - return [dict create bug $bug bugref 999b6966b2 description $description level minor] - } - - proc has_tclbug_list_quoting_emptyjoin {} { - #https://core.tcl-lang.org/tcl/tktview/e38dce74e2 - set v1 [list {*}[lindex #foo] {*}[]] ;#can return "#foo" instead of "{#foo}" under some beta 9 releases - set v2 [list #foo] ;#normal tcl list quoting for 1st element that looks like a comment -> "{#foo}" - set bug [expr {![string equal $v1 $v2]}] ;#if they're not equal - we have the bug. - set description "lists elements not properly quoted in some cases. e.g 'list {*}[lindex {etc #foo} 1] {*}[list]' (#foo not braced)" - return [dict create bug $bug bugref e38dc74e2 description $description level medium] - } - - proc has_tclbug_safeinterp_compile {{show 0}} { - #ensemble calls within safe interp not compiled - #https://core.tcl-lang.org/tcl/tktview/1095bf7f756f9aed6bde - namespace eval [namespace current]::testcompile { - proc ensembletest {} {string index a 0} - } - - set has_bug 0 - - set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] - if {$show} { - puts outer: - puts $bytecode_outer - } - if {![interp issafe]} { - #test of safe subinterp only needed if we aren't already in a safe interp - if {![catch { - interp create x -safe - } errMsg]} { - x eval {proc ensembletest {} {string index a 0}} - set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] - if {$show} { - puts safe: - puts $bytecode_safe - } - interp delete x - #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) - #It's possible the interp we're running in is also not compiling ensembles. - #we could then get a result of 2 - which still indicates a problem - if {[string last "invokeStk" $bytecode_safe] >= 1} { - incr has_bug - } - } else { - #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? - #unlikely - but we should warn - puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" - } - } - - namespace delete [namespace current]::testcompile - - if {[string last "invokeStk" $bytecode_outer] >= 1} { - incr has_bug - } - set description "ensemble commands not compiled in safe interps - heavy performance impact in safe interps" - return [dict create bug $has_bug bugref 1095bf7f756f9aed6bde description $description level major] - } -} - -tcl::namespace::eval punk::lib::compat { - #*** !doctools - #[subsection {Namespace punk::lib::compat}] - #[para] compatibility functions for features that may not be available in earlier Tcl versions - #[para] These are generally 'forward compatibility' functions ie allowing earlier versions to use later features/idioms by using a Tcl-only version of a missing builtin. - #[para] Such Tcl-only versions will inevitably be less performant - perhaps significantly so. - - #*** !doctools - #[list_begin definitions] - - - - - if {"::lremove" ne [info commands ::lremove]} { - #puts stderr "Warning - no built-in lremove" - interp alias {} lremove {} ::punk::lib::compat::lremove - } - proc lremove {list args} { - #*** !doctools - #[call [fun lremove] [arg list] [opt {index ...}]] - #[para] Forwards compatible lremove for versions 8.6 or less to support equivalent 8.7 lremove - - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lsearch -all -inline -index 1 -subindices $keep *] - } - #not significantly different in performance over test of 100 elements - getting somewhere near 10% for 1k integers - proc lremove2 {list args} { - set data [lmap v $list {list data $v}] - foreach doomed_index $args { - if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"} - lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value - } - set keep [lsearch -all -inline -not -exact $data x] - return [lmap v $keep {lindex $v 1}] - } - #outside of lmap - don't know of any particularly nice ways to flatten to subindex 1 of each element.. - #flattening then lsearch with -stride and * would be nice - but it's not avail in 8.6 - - if {![info exists ::auto_index(readFile)]} { - if {[info commands ::readFile] eq ""} { - proc ::readFile {filename {mode text}} { - #readFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - - # Read the file - set f [open $filename [dict get {text r binary rb} $mode]] - try { - return [read $f] - } finally { - close $f - } - } - } - } - if {![info exists ::auto_index(writeFile)]} { - if {[info commands ::writeFile] eq ""} { - proc ::writeFile {args} { - #writeFile not seen in auto_index or as command: installed by punk::lib - # Parse the arguments - switch [llength $args] { - 2 { - lassign $args filename data - set mode text - } - 3 { - lassign $args filename mode data - set MODES {binary text} - set ERR [list -level 1 -errorcode [list TCL LOOKUP MODE $mode]] - set mode [tcl::prefix match -message "mode" -error $ERR $MODES $mode] - } - default { - set COMMAND [lindex [info level 0] 0] - return -code error -errorcode {TCL WRONGARGS} "wrong # args: should be \"$COMMAND filename ?mode? data\"" - } - } - - # Write the File - set f [open $filename [dict get {text w binary wb} $mode]] - try { - puts -nonewline $f $data - } finally { - close $f - } - } - } - } - - if {"::lpop" ne [info commands ::lpop]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lpop {} ::punk::lib::compat::lpop - punk::args::set_idalias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore - } - proc lpop {lvar args} { - #*** !doctools - #[call [fun lpop] [arg listvar] [opt {index}]] - #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l - if {![llength $args]} { - set args [list end] - } - set v [lindex $l {*}$args] - set newlist $l - - set path [list] - set subl $l - for {set i 0} {$i < [llength $args]} {incr i} { - set idx [lindex $args $i] - if {![llength [lrange $subl $idx $idx]]} { - error "tcl_lpop index \"$idx\" out of range" - } - lappend path [lindex $args $i] - set subl [lindex $l {*}$path] - } - - set sublist_path [lrange $args 0 end-1] - set tailidx [lindex $args end] - if {![llength $sublist_path]} { - #set newlist [lremove $newlist $tailidx] - set newlist [lreplace $newlist $tailidx $tailidx] - } else { - set sublist [lindex $newlist {*}$sublist_path] - #set sublist [lremove $sublist $tailidx] - set sublist [lreplace $sublist $tailidx $tailidx] - lset newlist {*}$sublist_path $sublist - } - #puts "[set l] -> $newlist" - set l $newlist - return $v - } - if {"::ledit" ni [info commands ::ledit]} { - interp alias {} ledit {} ::punk::lib::compat::ledit - punk::args::set_idalias ::punk::lib::compat::ledit ::ledit - } - proc ledit {lvar first last args} { - upvar $lvar l - #use lindex_resolve to support for example: ledit lst end+1 end+1 h i - set fidx [punk::lib::lindex_resolve [llength $l] $first] - switch -exact -- $fidx { - -Inf { - #index below lower bound - set pre [list] - set fidx -1 - } - Inf { - #first index position is greater than index of last element in the list - set pre [lrange $l 0 end] - set fidx [llength $l] - } - default { - #set pre [lrange $l 0 $first-1] - set pre [lrange $l 0 $fidx-1] - } - } - set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -Inf { - #index below lower bound - set post [lrange $l 0 end] - } - Inf { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] - set post [lrange $l $lidx+1 end] - } - } - } - set l [list {*}$pre {*}$args {*}$post] - } - - - #slight isolation - varnames don't leak - but calling context vars can be affected - proc lmaptcl2 {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list ::tcl::info::vars]] - set nscaller [uplevel 1 [list ::tcl::namespace::current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result [apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - if {"::lmap" ne [info commands ::lmap]} { - #puts stderr "Warning - no built-in lpop" - interp alias {} lmap {} ::punk::lib::compat::lmaptcl - } - #lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway - proc lmaptcl {varnames list script} { - set result [list] - set varlist [list] - foreach varname $varnames { - upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc - lappend varlist var_$varname - } - foreach $varlist $list { - lappend result [uplevel 1 $script] - } - return $result - } - - #tcl8.7/9 compatibility for 8.6 - if {[info commands ::tcl::string::insert] eq ""} { - #https://wiki.tcl-lang.org/page/string+insert - # Pure Tcl implementation of [string insert] command. - proc ::tcl::string::insert {string index insertString} { - # Convert end-relative and TIP 176 indexes to simple integers. - if {[regexp -expanded { - ^(end(?![\t\n\v\f\r ]) # "end" is never followed by whitespace - |[\t\n\v\f\r ]*[+-]?\d+) # m, with optional leading whitespace - (?:([+-]) # op, omitted when index is "end" - ([+-]?\d+))? # n, omitted when index is "end" - [\t\n\v\f\r ]*$ # optional whitespace (unless "end") - } $index _ m op n]} { - # Convert first index to an integer. - switch $m { - end {set index [string length $string]} - default {scan $m %d index} - } - - # Add or subtract second index, if provided. - switch $op { - + {set index [expr {$index + $n}]} - - {set index [expr {$index - $n}]} - } - } elseif {![string is integer -strict $index]} { - # Reject invalid indexes. - return -code error "bad index \"$index\": must be\ - integer?\[+-\]integer? or end?\[+-\]integer?" - } - - # Concatenate the pre-insert, insertion, and post-insert strings. - string cat [string range $string 0 [expr {$index - 1}]] $insertString\ - [string range $string $index end] - } - - # Bind [string insert] to [::tcl::string::insert]. - tcl::namespace::ensemble configure string -map [tcl::dict::replace\ - [tcl::namespace::ensemble configure string -map]\ - insert ::tcl::string::insert] - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::compat ---}] -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::lib { - variable PUNKARGS - tcl::namespace::export * - variable has_struct_list - set has_struct_list [expr {![catch {package require struct::list}]}] - variable has_struct_set - set has_struct_set [expr {![catch {package require struct::set}]}] - variable has_punk_ansi - set has_punk_ansi [expr {![catch {package require punk::ansi}]}] - set has_twapi 0 - if {"windows" eq $::tcl_platform(platform)} { - set has_twapi [expr {![catch {package require twapi}]}] - } - - namespace eval argdoc { - #non-colour SGR codes - set I "\x1b\[3m" ;# [a+ italic] - set NI "\x1b\[23m" ;# [a+ noitalic] - set B "\x1b\[1m" ;# [a+ bold] - set N "\x1b\[22m" ;# [a+ normal] - set T "\x1b\[1\;4m" ;# [a+ bold underline] - set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::is_main_script - @cmd -name punk::lib::is_main_script\ - -summary\ - "Test if current script was launched directly."\ - -help\ - "The ${$B}main script${$N} is the primary script that is executed - by the interpreter, e.g. tclsh or wish. - (as opposed to being loaded by the 'source' command) - - see https://wiki.tcl-lang.org/page/main+script" - @values -min 0 -max 0 - }] - } - proc is_main_script {} { - #see https://wiki.tcl-lang.org/page/main+script - if {[info script] ne "" && [info exists ::argv0] - && - [file dirname [file normalize [file join [info script] ...]]] - eq - [file dirname [file normalize [file join $::argv0 ...]]] - } { - return true - } else { - return false - } - } - - - - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - # Maintenance - This is the primary source for tm_version... functions - # - certain packages script require these but without package dependency - # - 1 punk boot script - # - 2 packagetrace module - # - These should be updated to sync with this - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - proc tm_version_isvalid {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionpart $versionpart]]} { - return 1 - } else { - return 0 - } - } - proc tm_version_major {version} { - if {![tm_version_isvalid $version]} { - error "Invalid version '$version' is not a proper Tcl module version number" - } - set firstpart [lindex [split $version .] 0] - #check for a/b in first segment - if {[string is integer -strict $firstpart]} { - return $firstpart - } - if {[string first a $firstpart] > 0} { - return [lindex [split $firstpart a] 0] - } - if {[string first b $firstpart] > 0} { - return [lindex [split $firstpart b] 0] - } - error "tm_version_major unable to determine major version from version number '$version'" - } - proc tm_version_canonical {ver} { - #accepts a single valid version only - not a bounded or unbounded spec - if {![tm_version_isvalid $ver]} { - error "tm_version_canonical version '$ver' is not valid for a package version" - } - set parts [split $ver .] - set newparts [list] - foreach o $parts { - set trimmed [string trimleft $o 0] - set firstnonzero [string index $trimmed 0] - switch -exact -- $firstnonzero { - "" { - lappend newparts 0 - } - a - b { - #e.g 000bnnnn -> bnnnnn - set tailtrimmed [string trimleft [string range $trimmed 1 end] 0] - if {$tailtrimmed eq ""} { - set tailtrimmed 0 - } - lappend newparts 0$firstnonzero$tailtrimmed - } - default { - #digit - if {[string is integer -strict $trimmed]} { - #e.g 0100 -> 100 - lappend newparts $trimmed - } else { - #e.g 0100b003 -> 100b003 (still need to process tail) - if {[set apos [string first a $trimmed]] > 0} { - set lhs [string range $trimmed 0 $apos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $apos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}a${rhs} - } elseif {[set bpos [string first b $trimmed]] > 0} { - set lhs [string range $trimmed 0 $bpos-1] ;#assert lhs non-empty and only digits or wouldn't be in this branch - set rhs [string range $trimmed $bpos+1 end] ;#assert rhs non-empty and only digits - set rhs [string trimleft $rhs 0] - if {$rhs eq ""} { - set rhs 0 - } - lappend newparts ${lhs}b${rhs} - } else { - #assert - shouldn't get here trimmed val should have been empty, an int or contained an a or b - error "tm_version_canonical error - trimfail - unexpected" - } - } - } - } - } - return [join $newparts .] - } - proc tm_version_required_canonical {versionspec} { - #also trim leading zero from any dottedpart? - #Tcl *allows* leading zeros in any of the dotted parts - but they are not significant. - #e.g 1.01 is equivalent to 1.1 and 01.001 - #also 1b3 == 1b0003 - - if {[string trim $versionspec] eq ""} {return ""} ;#unspecified = any version - set errmsg "tm_version_required_canonical - invalid version specification" - if {[string first - $versionspec] < 0} { - #no dash - #looks like a minbounded version (ie a single version with no dash) convert to min-max form - set from $versionspec - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionpec'" - } - if {![catch {tm_version_major $from} majorv]} { - set from [tm_version_canonical $from] - return "${from}-[expr {$majorv +1}]" - } else { - error "$errmsg '$versionspec'" - } - } else { - # min- or min-max - #validation and canonicalisation (strip leading zeroes from each segment, including either side of a or b) - set parts [split $versionspec -] ;#we expect only 2 parts - lassign $parts from to - if {![tm_version_isvalid $from]} { - error "$errmsg '$versionspec'" - } - set from [tm_version_canonical $from] - if {[llength $parts] == 2} { - if {$to ne ""} { - if {![tm_version_isvalid $to]} { - error "$errmsg '$versionspec'" - } - set to [tm_version_canonical $to] - return $from-$to - } else { - return $from- - } - } else { - error "$errmsg '$versionspec'" - } - error "tm_version_required_canonical should have already returned a canonicalised versionspec - or produced an error with reason before this point" - } - } - # end tm_version... functions - # == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == - - - - # -- --- - #https://stackoverflow.com/questions/17631269/whats-the-best-way-to-join-two-lists - #DKF's 2013 recommendation of using list {*}$first {*}$second seems not to apply in 2024 - #8.6,8.7,9.0 - 'lappend first {*}$second' is many times faster - especially as list grows - # Review and retest as new versions come out. - # -- --- - proc list_multi_append1 {lvar1 lvar2} { - #clear winner in 2024 - upvar $lvar1 l1 $lvar2 l2 - lappend l1 {*}$l2 - return $l1 - } - proc list_multi_append2 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [list {*}$l1 {*}$l2] - } - proc list_multi_append3 {lvar1 lvar2} { - upvar $lvar1 l1 $lvar2 l2 - set l1 [lindex [list [list {*}$l1 {*}$l2] [unset l1]] 0] - } - #testing e.g - #set l1_reset {a b c} - #set l2 {a b c d e f g} - #set l1 $l1_reset - #time {list_multi_append1 l1 l2} 1000 - #set l1 $l1_reset - #time {list_multi_append2 l1 l2} 1000 - # -- --- - - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lswap - @cmd -name punk::lib::lswap\ - -summary\ - "Swap list values in-place"\ - -help\ - "Similar to struct::list swap, except it fully supports basic - list index expressions such as 7-2 end-1 etc. - - struct::list swap doesn't support 'end' offsets, and only - sometimes appears to support basic expressions, depending on the - expression compared to the list length." - @values -min 3 -max 3 - lvar -type string -help\ - "name of list variable" - a -type indexexpression - z -type indexexpression - }] - } - proc lswap {lvar a z} { - upvar $lvar l - set len [llength $l] - if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { - #lindex_resolve_basic returns only -Inf if out of range at either bound - #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred - #(e.g using: lswap mylist end-2 end on a two element list) - - #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report - #use full 'lindex_resolve' which can report which side via -Inf and Inf special results being lower and upper bound breaches respectively - set a_index [lindex_resolve $len $a] - set a_msg "" - switch -- $a_index { - -Inf { - set a_msg "1st supplied index $a is below the lower bound for the list (0)" - } - Inf { - set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])" - } - } - set z_index [lindex_resolve $len $z] - set z_msg "" - switch -- $z_index { - -Inf { - set z_msg "2nd supplied index $z is below the lower bound for the list (0)" - } - Inf { - set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])" - } - } - set errmsg "lswap cannot swap indices $a and $z" - if {$a_msg ne ""} { - append errmsg \n $a_msg - } - if {$z_msg ne ""} { - append errmsg \n $z_msg - } - error $errmsg - } - set item2 [lindex $l $z] - lset l $z [lindex $l $a] - lset l $a $item2 - return $l - } - #proc lswap2 {lvar a z} { - # upvar $lvar l - # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] - #} - - proc lswap2 {lvar a z} { - upvar $lvar l - #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower - set l [list {*}[lrange $l 0 $a-1] [lindex $l $z] {*}[lrange $l $a+1 $z-1] [lindex $l $a] {*}[lrange $l $z+1 end]] - } - - #an experimental test of swapping vars without intermediate variables - #It's an interesting idea - but probably of little to no practical use - # - the swap_intvars3 version using intermediate var is faster in Tcl - # - This is probably unsurprising - as it's simpler code. - # Even if we tried this technique in c - the compiler would probably do a better job with the intermediate variable than with the math tricks. - #proc swap_intvars {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {[expr {$_x + $_y}] - [set _y $_x]}] - #} - #proc swap_intvars2 {swapv1 swapv2} { - # upvar $swapv1 _x $swapv2 _y - # set _x [expr {$_x ^ $_y}] - # set _y [expr {$_x ^ $_y}] - # set _x [expr {$_x ^ $_y}] - #} - #proc swap_intvars3 {swapv1 swapv2} { - # #using intermediate variable - # upvar $swapv1 _x $swapv2 _y - # set z $_x - # set _x $_y - # set _y $z - #} - - #*** !doctools - #[subsection {Namespace punk::lib}] - #[para] Core API functions for punk::lib - #[list_begin definitions] - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - #support minimal set from to - proc range {from to {by 1}} { - #note inconsistency with lseq 1 10 by -9 vs lseq 1 10 by -10 - #https://core.tcl-lang.org/tcl/tktview/999b6966b2 - lseq $from $to by $by - } - } else { - #lseq accepts basic expressions e.g 4-2 for both arguments - #e.g we can do lseq 0 [llength $list]-1 - #if range is to be consistent with the lseq version above - it should support that, even though we don't support most lseq functionality in either wrapper. - #our range function doesn't support double like lseq does. (deliberate) review - proc range {from to {by ""}} { - if {$by eq "0"} { - #as per lseq, step (by) zero always gives no result - return [list] - } - set to [offset_expr $to] - set from [offset_expr $from] - if {$by ne ""} { - set by [offset_expr $by] - } - #assert $by is now empty string or an integer - if {$to > $from} { - switch -- $by { - "" - 1 { - set count [expr {($to -$from) + 1}] - if {$from == 0} { - return [lsearch -all [lrepeat $count 0] *] - } else { - incr from -1 - return [lmap v [lrepeat $count 0] {incr from}] - } - } - default { - set count [expr {($to - $from + $by) / $by}] - if {$count <= 0} { - #return [list] - #https://core.tcl-lang.org/tcl/tktview/999b6966b2 - return [list $from] ;#review - } - set result [list] - for {set i $from} {$i <= $to} {incr i $by} { - lappend result $i - } - return $result - - #if we don't have lseq, we probably don't have lsearch -stride, which would make things simpler. - #set count [expr {($to -$from) + 1}] - #if {$from == 0} { - # set fullrange [lsearch -all [lrepeat $count 0] *] - #} else { - # incr from -1 - # set fullrange [lmap v [lrepeat $count 0] {incr from}] - #} - #set result [list] - #for {set i 0} {$i < $count} {incr i} { - # if {$i % $by == 0} { - # lappend result [lindex $fullrange $i] - # } - #} - #return $result - } - } - #slower methods. - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from] - #} - #return $L - } elseif {$from > $to} { - switch -- $by { - "" - -1 { - set count [expr {$from - $to} + 1] - if {$to == 0} { - return [lreverse [lsearch -all [lrepeat $count 0] *]] - } else { - incr from - return [lmap v [lrepeat $count 0] {incr from -1}] - } - } - default { - set count [expr {($to - $from + $by) / $by}] - if {$count <= 0} { - #return [list] - return [list $from] ;#review - } - set result [list] - for {set i $from} {$i >= $to} {incr i $by} { - lappend result $i - } - return $result - } - } - - #2) - #set i -1 - #set L [lrepeat $count 0] - #lmap v $L {lset L [incr i] [incr from -1];lindex {}} - #return $L - #3) - #set L {} - #for {set i 0} {$i < $count} {incr i} { - # lappend L [incr from -1] - #} - #return $L - } else { - return [list $from] - } - } - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lzip - @cmd -name punk::lib::lzip\ - -summary\ - "zip any number of lists together."\ - -help\ - "Conceptually equivalent to converting a list of rows - to a list of columns. - - The number of returned lists (columns) will be equal to - the length of the longest supplied list (row). - If lengths of supplied lists don't match, empty strings - will be inserted in the resulting lists. - - e.g lzip {a b c d e} {1 2 3 4} {x y z} - -> {a 1 x} {b 2 y} {c 3 z} {d 4 {}} {3 {} {}} - " - @values -min 0 -max -1 - list -type list -multiple 1 -optional 1 - }] - } - proc lzip {args} { - switch -- [llength $args] { - 0 {return {}} - 1 {return [lindex $args 0]} - 2 {return [::punk::lib::system::lzip2lists {*}$args]} - 3 {return [::punk::lib::system::lzip3lists {*}$args]} - 4 {return [::punk::lib::system::lzip4lists {*}$args]} - 5 {return [::punk::lib::system::lzip5lists {*}$args]} - 6 {return [::punk::lib::system::lzip6lists {*}$args]} - 7 {return [::punk::lib::system::lzip7lists {*}$args]} - 8 {return [::punk::lib::system::lzip8lists {*}$args]} - 9 {return [::punk::lib::system::lzip9lists {*}$args]} - 10 {return [::punk::lib::system::lzip10lists {*}$args]} - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 { - set n [llength $args] - if {[info commands ::punk::lib::system::lzip${n}lists] eq ""} { - #puts "calling ::punk::lib::system::Build_lzipn $n" - ::punk::lib::system::Build_lzipn $n - } - return [::punk::lib::system::lzip${n}lists {*}$args] - } - default { - if {[llength $args] < 4000} { - set n [llength $args] - if {[info commands ::punk::lib::system::lzip${n}lists] eq ""} { - #puts "calling ::punk::lib::system::Build_lzipn $n" - ::punk::lib::system::Build_lzipn $n - } - return [::punk::lib::system::lzip${n}lists {*}$args] - } else { - return [::punk::lib::lzipn {*}$args] - } - } - } - } - - namespace eval system { - proc Build_lzipn {n} { - set arglist [list] - #use punk::lib::range which defers to lseq if available - set vars [lmap i [punk::lib::range 0 $n] {string cat v$i}] ;#v0 v1 v2.. (v0 ignored) - set body "\nlmap " - for {set i 1} {$i <= $n} {incr i} { - lappend arglist l$i - append body "[lindex $vars $i] \$l$i " - } - append body "\{list " - for {set i 1} {$i <= $n} {incr i} { - append body "\$[lindex $vars $i] " - } - append body "\}" \n - #puts "proc punk::lib::system::lzip${n}lists {$arglist} \{" - #puts "$body" - #puts "\}" - proc ::punk::lib::system::lzip${n}lists $arglist $body - } - - #fastest is to know the number of lists to be zipped - proc lzip2lists {l1 l2} { - lmap a $l1 b $l2 {list $a $b} - } - proc lzip3lists {l1 l2 l3} { - lmap a $l1 b $l2 c $l3 {list $a $b $c} - } - proc lzip4lists {l1 l2 l3 l4} { - lmap a $l1 b $l2 c $l3 d $l4 {list $a $b $c $d} - } - proc lzip5lists {l1 l2 l3 l4 l5} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 {list $a $b $c $d $e} - } - proc lzip6lists {l1 l2 l3 l4 l5 l6} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 {list $a $b $c $d $e $f} - } - proc lzip7lists {l1 l2 l3 l4 l5 l6 l7} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 {list $a $b $c $d $e $f $g} - } - proc lzip8lists {l1 l2 l3 l4 l5 l6 l7 l8} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 {list $a $b $c $d $e $f $g $h} - } - proc lzip9lists {l1 l2 l3 l4 l5 l6 l7 l8 l9} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 {list $a $b $c $d $e $f $g $h $i} - } - proc lzip10lists {l1 l2 l3 l4 l5 l6 l7 l8 l9 l10} { - lmap a $l1 b $l2 c $l3 d $l4 e $l5 f $l6 g $l7 h $l8 i $l9 j $l10 {list $a $b $c $d $e $f $g $h $i $j} - } - - #neat algorithm - but while lmap seems better than foreach - it seems the script is evaluated a little slowly - # review - - proc lzipn_alt args { - #stackoverflow - courtesy glenn jackman (modified) - foreach l $args { - lappend vars [incr n] - lappend lmap_args $n $l - } - lmap {*}$lmap_args {lmap v $vars {set $v}} - } - - #2024 - outperforms lmap version - presumably because list sizes reduced as it goes(?) - proc lzipn_tcl8 {args} { - #For tcl pre 9 (without lsearch -stride) - #wiki - courtesy JAL - set list_l $args - set zip_l [] - while {1} { - set cur [lmap a_l $list_l { lindex $a_l 0 }] - set list_l [lmap a_l $list_l { lrange $a_l 1 end }] - - if {[join $cur {}] eq {}} { - break - } - lappend zip_l $cur - } - return $zip_l - } - proc lzipn_tcl9a {args} { - #For Tcl 9+ (with lsearch -stride) - #compared to wiki version - #comparable for lists len <3 or number of args < 3 - #approx 2x faster for large lists or more lists - #needs -stride single index bug fix to use empty string instead of NULL - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [lrepeat [expr {$numcolumns * [llength $args]}] {}] - set outlist [lrepeat $numcolumns {}] - set s 0 - foreach len $lens list $args { - #ledit flatlist $s $e {*}$l {*}[lrepeat [expr {($numcolumns -([llength $l] % $numcolumns)) % $numcolumns}] NULL] - ledit flatlist $s [expr {$s + $len - 1}] {*}$list - incr s $numcolumns - } - #needs single index lstride bugfix - for {set c 0} {$c < $numcolumns} {incr c} { - ledit outlist $c $c [lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *] - } - return $outlist - } - proc lzipn_tcl9b {args} { - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - lmap c [lseq 0 $numcolumns-1] {lsearch -stride $numcolumns -index $c -inline -all -subindices $flatlist *} - } - proc lzipn_tcl9c {args} { - #SLOW - if {![llength $args]} {return {}} - set lens [lmap l $args {llength $l}] - set numcolumns [::tcl::mathfunc::max {*}$lens] - set flatlist [list] - foreach len $lens list $args { - lappend flatlist {*}$list {*}[lrepeat [expr {($numcolumns - ($len % $numcolumns)) % $numcolumns}] ""] - } - set zip_l {} - set cols_remaining $numcolumns - for {set c 0} {$c < $numcolumns} {incr c} { - if {$cols_remaining == 1} { - return [list {*}$zip_l $flatlist] - } - lappend zip_l [lsearch -stride $cols_remaining -index 0 -inline -all -subindices $flatlist *] - set flen [llength $flatlist] - set flatlist [lremove $flatlist {*}[lseq 0 to $flen-1 by $cols_remaining]] - incr cols_remaining -1 - } - return $zip_l - } - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lzipn - @cmd -name punk::lib::lzipn\ - -summary\ - "zip any number of lists together (unoptimised)."\ - -help\ - "Conceptually equivalent to converting a list of rows - to a list of columns. - - See lzip which provides the same functionality but with - optimisations depending on the number of supplied lists. - " - @values -min 1 -max 1 - lvar -type string -help\ - "name of list variable" - a -type indexexpression - z -type indexexpression - }] - } - #keep both lzipn_tclX functions available for side-by-side testing in Tcl versions where it's possible - if {![package vsatisfies [package present Tcl] 9.0-] || [dict get [punk::lib::check::has_tclbug_lsearch_strideallinline] bug]} { - #-stride either not available - or has bug preventing use of main algorithm below - proc lzipn {args} [info body ::punk::lib::system::lzipn_tcl8] - } else { - proc lzipn {args} [info body ::punk::lib::system::lzipn_tcl9a] - } - - - - namespace import ::punk::args::lib::tstr - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::tclscript_to_scriptlist - @cmd -name punk::lib::tclscript_to_scriptlist\ - -summary\ - "Parse tcl script to toplevel list of lists."\ - -help\ - "Get topmost list of tcl language elements in script. - produces a list of lists where each sublist is a commandlist or - a comment string." - @values -min 1 -max 1 - script -type string - }] - } - proc tclscript_to_scriptlist {script} { - set scriptlist [list] - set cmdlist [list] - set scrlen [string length $script] - set token "" - set in_token 0 - set in_cmdlist 0 - set in_comment 0 - set charmap [list \t TB \n LF \r CR \\ BSL] ;#for switch 'jump' preservation - review - may be slower than escapes in switch statement? - for {set i 0} {$i < $scrlen} {incr i} { - set ch [string index $script $i] - set chswitch [string map $charmap $ch] - if {!$in_token} { - switch -- $chswitch { - { } - TB { - #ignore - continue being a non token - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {$in_cmdlist} { - #no active token - newline ends cmdlist - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - incr i - } - } - LF - ";" { - #no active token - newline or semicolon ends cmdlist - if {$in_cmdlist} { - set in_cmdlist 0 - lappend scriptlist $cmdlist - set cmdlist [list] - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation of whitespace while no token - boring - incr i - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation of whitespace while no token - boring - incr i 2 - } else { - #an uncommon possibility, a command wth surrounding spaces called in an strange way - # e.g \ cmdname\ arg - set in_token 1 - set token "\\[string index $script $i+1]" - incr i - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - # { - if {$in_cmdlist} { - #ordinary data - set in_token 1 - set token # - } else { - if {!$in_comment} { - set in_token 1 - set in_comment 1 - set token # - } else { - #wnen in comment - all will be a single token until comment ends - append token # - } - } - } - default { - #for completeness.. we should exclude other possible whitespace chars - if {![string is space $ch]} { - set in_token 1 - set token $ch - if {!$in_cmdlist} { - set in_cmdlist 1 - } - } - } - } - } else { - #if we're in a token, we must be in a cmdlist or a comment (single token) - #review - not preserving whitespace in list of commands is ok, but for comments it should ideally be preserved - #note that unbalanced curly in *toplevel* comment will still 'info complete' to true - switch -- $chswitch { - LF { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ends a comment - lappend scriptlist $token ;#single token for comment - set token "" - set in_token 0 - set in_comment 0 - set in_cmdlist 0 ;#shouldn't be necessary, but included for clarity - } - } - ";" { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - #ends token and cmdlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \n - } - } else { - #ordinary char for comment - append token ";" - } - } - CR { - if {[string index $script $i+1] eq "\n"} { - if {[tcl::info::complete $token]} { - #ends token and commandlist - lappend cmdlist $token - lappend scriptlist $cmdlist - set cmdlist "" - set in_cmdlist 0 - set token "" - set in_token 0 - } else { - append token \r\n - incr i - } - } else { - append token \r - } - } - BSL { - if {[string index $script $i+1] eq "\n"} { - #continuation - lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i ;#skip LF - } elseif {[string range $script $i+1 $i+2] eq "\r\n"} { - #continuation - cr-lf effectively becomes a space - if {!$in_comment} { - #token may end - but cmdlist goes on - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token " " - } - } else { - append token " " - } - incr i 2 ;#skip CRLF - } else { - append token "\\[string index $script $i+1]" - incr i - } - } - default { - if {![string is space $ch]} { - append token $ch - } else { - if {!$in_comment} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - set token "" - set in_token 0 - } else { - append token $ch - } - } else { - append token $ch - } - } - } - } - } - } - #eof - if {!$in_comment} { - if {$in_token} { - if {[tcl::info::complete $token]} { - lappend cmdlist $token - lappend scriptlist $cmdlist - } else { - error "Eof reached whilst script incomplete. Unbalanced braces?\ntoken: '$token'" - } - } else { - if {$in_cmdlist} { - lappend scriptlist $cmdlist - } - } - } else { - lappend scriptlist $token - } - return $scriptlist - } - - - proc invoke command { - #*** !doctools - #[call [fun invoke] [arg command]] - #[para]Invoke an external command (using tcl open command) capturing stdout,stderr and the exitcode - #[example { - # set script { - # puts stdout {hello on stdout} - # puts stderr {hello on stderr} - # exit 42 - # } - # invoke [list tclsh <<$script] - #}] - - #see https://wiki.tcl-lang.org/page/open - lassign [chan pipe] chanout chanin - lappend command 2>@$chanin - set fh [open |$command] - set stdout [read $fh] - close $chanin - set stderr [read $chanout] - close $chanout - if {[catch {close $fh} cres e]} { - dict with e {} - lassign [set -errorcode] sysmsg pid exit - if {$sysmsg eq {NONE}} { - #output to stderr caused [close] to fail. Do nothing - } elseif {$sysmsg eq {CHILDSTATUS}} { - return [list $stdout $stderr $exit] - } else { - return -options $e $stderr - } - } - return [list $stdout $stderr 0] - } - - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - (todo - change to indexset syntax @1..3 @1..end-1 etc) - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list ::tcl::array::exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list ::tcl::array::get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock - - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { - @id -id ::punk::lib::showdict - @cmd -name punk::lib::showdict -help "display dictionary keys and values" - #todo - table tableobject - -return -default "tailtohead" -choices {tailtohead sidebyside} - -channel -default none - -trimright -default 1 -type boolean -help\ - "Trim whitespace off rhs of each line. - This can help prevent a single long line that wraps in terminal from making - every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" - -roottype -default "dict" -help\ - "list,dict,string" - -ansibase_keys -default "" -help\ - "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" - -substructure -default {} - -ansibase_values -default "" - -keytemplates -default {\$\{$key\}} -type list -help\ - "list of templates for keys at each level" - -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default increasing -choices {increasing decreasing} - -debug -default 0 -type boolean -help\ - "When enabled, produces some rudimentary debug output on stderr" - -- -type none -optional 1 - @values -min 1 -max -1 - dictvalue -type list -help\ - "dict or list value" - patterns -default "*" -type string -multiple 1 -help\ - "key or key glob pattern" - }]] - - #for punk::lib - we want to reduce pkg dependencies. - # - so we won't even use the tcllib debug pkg here - set opt_debug [dict get $argd opts -debug] - if {$opt_debug} { - if {[info body debug::showdict] eq ""} { - proc ::punk::lib::debug::showdict {args} { - catch {puts stderr "punk::lib::showdict-> [string cat {*}$args]"} - } - } - } else { - if {[info body debug::showdict] ne ""} { - proc ::punk::lib::debug::showdict {args} {} - } - } - - set opt_sep [dict get $argd opts -separator] - set opt_mismatch_sep [dict get $argd opts -separator_mismatch] - set opt_keysorttype [dict get $argd opts -keysorttype] - set opt_keysortdirection [dict get $argd opts -keysortdirection] - set opt_trimright [dict get $argd opts -trimright] - set opt_keytemplates [dict get $argd opts -keytemplates] - debug::showdict "keytemplates ---> $opt_keytemplates <---" - set opt_ansibase_keys [dict get $argd opts -ansibase_keys] - set opt_ansibase_values [dict get $argd opts -ansibase_values] - set opt_return [dict get $argd opts -return] - set opt_roottype [dict get $argd opts -roottype] - set opt_structure [dict get $argd opts -substructure] - - set dval [dict get $argd values dictvalue] - set patterns [dict get $argd values patterns] - - set result "" - - #pattern hierarchy - # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest - # * @1 @0,%#,%str - segments - # a b 1 0 %# %str - keys - - set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated - set pattern_next_substructure [dict create] - set pattern_this_structure [dict create] - - # -- --- --- --- - #REVIEW - #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. - #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). - #todo - determine if there is a more consistent rule-based way to do this rather than adhoc - #e.g pdict something * - #we want the keys from the result as individual lines on lhs - #e.g pdict something @@ - #we want on lhs result on rhs - # = v0 - #e.g pdict something @0-2,@4 - #we currently return: - #0 = v0 - #1 = v1 - #2 = v2 - #4 = v4 - #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) - #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. - #this is a tradeoff that could create surprises and make things messy and/or inconsistent. - #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. - #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys - #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment - #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) - # -- --- --- --- - - set filtered_keys [list] - if {$opt_roottype in {dict list string}} { - #puts "getting keys for roottype:$opt_roottype" - if {[llength $dval]} { - - #TODO - change to indexset notation 0..1,3..end-1 etc - - set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} - set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} - foreach pattern_nest $patterns { - set keyset [list] - set keyset_structure [list] - - set segments [split $pattern_nest /] - set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns - #we need to use _split_patterns to separate (e.g to protect commas that appear within quotes) - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - #puts stderr "showdict-->_split_patterns: $patterninfo" - foreach v_idx $patterninfo { - lassign $v_idx v idx - #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) - set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern - if {[string index $p 0] eq "!"} { - set get_not 1 - set p [string range $p 1 end] - } else { - set get_not 0 - } - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - dict set pattern_this_structure $p list - } elseif {$opt_roottype eq "dict"} { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } else { - lappend keyset %string - lappend keyset_structure string - dict set pattern_this_structure $p string - } - } - %# { - dict set pattern_this_structure $p string - lappend keyset %# - lappend keyset_structure string - } - # { - #todo get_not !# is test for listiness (see punk) - dict set pattern_this_structure $p list - lappend keyset # - lappend keyset_structure list - } - ## { - dict set pattern_this_structure $p dict - lappend keyset [list ## query] - lappend keyset_structure dict - } - @* { - #puts "showdict ---->@*<----" - dict set pattern_this_structure $p list - set keys [punk::lib::range 0 [llength $dval]-1] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $p dict - lappend keyset [list @@ query] - lappend keyset_structure dict - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @*.@* { - set keys [dict keys $dval] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - default { - #puts stderr "===p:$p" - #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! - #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful - #@@"key,etc" should allow any non-whitespace key - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {$get_not} { - if {[dict exists $dval $k]} { - set keys [dict keys [dict remove $dval $k]] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - lappend keyset {*}[dict keys $dval] - lappend keyset_structure {*}[lrepeat [dict size $dval] dict] - } - } else { - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - } - dict set pattern_this_structure $p dict - } - @k@* - @K@* { - #TODO get_not - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k - lappend keyset_structure dict - } - dict set pattern_this_structure $p dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*.@*} { - #TODO get_not - set k [string range $p 4 end] - set keys [dict keys $dval $k] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - dict set pattern_this_structure $p dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - if {$get_not} { - lappend keyset [list !$p query] - } else { - lappend keyset [list $p query] - } - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - lappend keyset_structure dict - dict set pattern_this_structure $p dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $p list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $p string - lappend keyset $p - lappend keyset_structure string - set this_type string - } - if {$this_type eq "list"} { - dict set pattern_this_structure $p list - if {[string is integer -strict $p]} { - if {$get_not} { - set keys [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $keys $p] - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - lappend keyset $p - lappend keyset_structure list - } - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - #TODO - fix terminology. 'lower_resolve' is confusing here as range can be in descending order - #change to start/end terminology? - - set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-Inf for too low, Inf for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == Inf} { - ##x - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -Inf} { - ##x - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve [llength $dval] $b] - if {$upper == -Inf} { - ##x - #upper bound is below list range - - if {$lower_resolve > -Inf} { - ##x - set upper 0 - } else { - continue - } - } elseif {$upper == Inf} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - set keys [punk::lib::range $lower $upper] - if {$get_not} { - set fullrange [punk::lib::range 0 [llength $dval]-1] - set keys [lremove $fullrange {*}$keys] - if {$lower > $upper} { - set keys [lreverse $keys] - } - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] list] - } else { - if {$get_not} { - lappend keyset [list !@$p query] - } else { - lappend keyset [list @$p query] - } - lappend keyset_structure list - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $p string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $p dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - set keys [dict keys $dval $p] - if {$get_not} { - set keys [dict keys [dict remove $dval {*}$keys]] - } - lappend keyset {*}$keys - lappend keyset_structure {*}[lrepeat [llength $keys] dict] - } else { - puts stderr "list: unrecognised pattern $p" - } - } - } - } - } - } - - # -- --- --- --- - #check next pattern-segment for substructure type to use - # -- --- --- --- - set substructure "" - set pnext [lindex $segments 1] - set patterninfo [punk::pipe::lib::_split_patterns $levelpatterns] - if {[llength $patterninfo] == 0} { - # // ? -review - what does this mean? for xpath this would mean at any level - set substructure [lindex $pattern_this_structure end] - } elseif {[llength $patterninfo] == 1} { - #ignore the NOT operator for purposes of query-type detection - if {[string index $pnext 0] eq "!"} { - set pnext [string range $pnext 1 end] - } - # single type in segment e.g /@@something/ - switch -exact -- $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - #set substructure [dict get $pattern_this_structure $pattern_nest] - set substructure [lindex $pattern_this_structure end] - } - } - } - } - } - } else { - #e.g /@0,%str,.../ - #doesn't matter what the individual types are - we have a list result - set substructure list - } - #puts "--pattern_nest: $pattern_nest substructure: $substructure" - dict set pattern_next_substructure $pattern_nest $substructure - # -- --- --- --- - - if {$opt_keysorttype ne "none"} { - set int_keyset 1 - foreach k $keyset { - if {![string is integer -strict $k]} { - set int_keyset 0 - break - } - } - if {$int_keyset} { - set sortindices [lsort -indices -integer $keyset] - #set keyset [lsort -integer $keyset] - } else { - #set keyset [lsort -$opt_keysorttype $keyset] - set sortindices [lsort -indices -$opt_keysorttype $keyset] - } - set keyset [lmap i $sortindices {lindex $keyset $i}] - set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] - } - - foreach k $keyset { - lappend pattern_key_index $pattern_nest - } - - lappend filtered_keys {*}$keyset - lappend all_keyset_structure {*}$keyset_structure - - #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" - } - } - #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" - } else { - puts stdout "unrecognised roottype: $opt_roottype" - return $dval - } - - if {[llength $filtered_keys]} { - #both keys and values could have newline characters. - #simple use of 'format' won't cut it for more complex dict keys/values - #use block::width or our columns won't align in some cases - switch -- $opt_return { - "tailtohead" { - #last line of key is side by side (possibly with separator) with first line of value - #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values - #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt {${$key}} - } - #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] - set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] - set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] - - set kidx 0 - set last_hidekey 0 - foreach keydisplay $display_keys key $filtered_keys { - set thisval "?" - set hidekey 0 - set pattern_nest [lindex $pattern_key_index $kidx] - set pattern_nest_list [split $pattern_nest /] - #set this_type [dict get $pattern_this_structure $pattern_nest] - #set this_type [dict get $pattern_this_structure $key] - set this_type [lindex $all_keyset_structure $kidx] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" - - set is_match 1 ;#whether to display the normal separator or bad-match separator - switch -- $this_type { - dict { - #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict - # - default highlight dupes (ansi underline?) - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - % thisval.= $qry= $dval - } else { - set thisval [tcl::dict::get $dval $key] - } - - #set substructure [lrange $opt_structure 1 end] - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - - set subansibasekeys [lrange $opt_ansibase_keys 1 end] - set nextkeytemplates [lrange $opt_keytemplates 1 end] - #dict set nextopts -substructure $nextsub - dict set nextopts -keytemplates $nextkeytemplates - dict set nextopts -ansibase_keys $subansibasekeys - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" - - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" - set is_match 0 - } - } - } - list { - if {[string is integer -strict $key]} { - set thisval [lindex $dval $key] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - % thisval.= $qry= $dval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - set nextopts [dict get $argd opts] - - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - #if {![llength $nextpatterns]} { - # set nextpatterns * - #} - if {[llength $nextpatterns]} { - if {[catch { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } errMsg]} { - set is_match 0 - } - } - } - string { - set hidekey 1 - if {$key eq "%string"} { - set hidekey 1 - set thisval $dval - } elseif {$key eq "%ansiview"} { - set thisval [ansistring VIEW -lf 1 $dval] - } elseif {$key eq "%ansiviewstyle"} { - set thisval [ansistring VIEWSTYLE -lf 1 $dval] - } elseif {[string match *lpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which left -width $width] - } elseif {[string match *lpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which left -width $width -padchar $extra] - } elseif {[string match *rpad-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + $extra}] - set thisval [textblock::pad $dval -which right -width $width] - } elseif {[string match *rpadstr-* $key]} { - set hidekey 1 - lassign [split $key -] _ extra - set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] - set thisval [textblock::pad $dval -which right -width $width -padchar $extra] - } else { - if {[lindex $key 1] eq "query"} { - set qry [lindex $key 0] - } else { - set qry $key - } - set thisval $dval - if {[string index $key 0] ne "%"} { - set key %$key - } - % thisval.= $key= $thisval - } - - set nextpatterns [list] - #which pattern nest applies to this branch - set nextsub [dict get $pattern_next_substructure $pattern_nest] - if {[llength $pattern_nest_list]} { - set nest [lrange $pattern_nest_list 1 end] - lappend nextpatterns {*}[join $nest /] - } - #set nextopts [dict get $argd opts] - dict set nextopts -roottype $nextsub - dict set nextopts -channel none - - if {[llength $nextpatterns]} { - set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] - } - - } - } - if {$this_type eq "string" && $hidekey} { - lassign [textblock::size $thisval] _vw vwidth _vh vheight - #set blanks_above [string repeat \n [expr {$kheight -1}]] - set vblock $opt_ansibase_values$thisval$RST - #append result [textblock::join_basic -- $vblock] - #review - we wouldn't need this space if we had a literal %sp %sp-x ?? - append result " $vblock" - } else { - set ansibase_key [lindex $opt_ansibase_keys 0] - - lassign [textblock::size $keydisplay] _kw kwidth _kh kheight - lassign [textblock::size $thisval] _vw vwidth _vh vheight - - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - - if {$is_match} { - set use_sep $opt_sep - } else { - set use_sep $opt_mismatch_sep - } - - - set sepwidth [textblock::width $use_sep] - set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_values$thisval$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - if {$last_hidekey} { - append result \n - } - #append result [textblock::join_basic -- $kblock $sblock $vblock] \n - append result [textblock::join_basic_raw $kblock $sblock $vblock] \n - } - set last_hidekey $hidekey - incr kidx - } - } - "sidebyside" { - # TODO - fix - #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. - #use ansibase_key etc to make the output more comprehensible in that situation. - #This is why it is not the default. (review - terminal width detection and wrapping?) - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - foreach key $filtered_keys { - set kt [lindex $opt_keytemplates 0] - if {$kt eq ""} { - set kt "%k%" - } - set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST - #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n - #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n - } - } - } - } - if {$opt_trimright} { - set result [::join [lines_as_list -line trimright $result] \n] - } - if {[string last \n $result] == [string length $result]-1} { - set result [string range $result 0 end-1] - } - #stdout/stderr can exist but not be in 'chan names' (e.g when transforms in place) - set chan [dict get $argd opts -channel] - switch -- $chan { - stderr - stdout { - puts $chan $result - } - none { - return $result - } - default { - #review - check member of chan names? - #just try outputting to the supplied channel for now - puts $chan $result - } - } - } - - proc is_list_all_in_list {small large} { - if {[llength $small] > [llength $large]} {return 0} - foreach x $large { - ::set ($x) {} - } - foreach x $small { - if {![info exists ($x)]} { - return 0 - } - } - return 1 - } - #v2 generally seems slower - proc is_list_all_in_list2 {small large} { - set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] - return [struct::list equal [lsort $small] $small_in_large] - } - if {!$has_struct_list || !$has_struct_set} { - set body { - package require struct::list - package require struct::set - } - append body [info body is_list_all_in_list2] - proc is_list_all_in_list2 {small large} $body - } - - proc is_list_all_ni_list {A B} { - foreach x $B { - ::set ($x) {} - } - foreach x $A { - if {[info exists ($x)]} { - return 0 - } - } - return 1 - } - proc is_list_all_ni_list2 {a b} { - set i [struct::set intersect $a $b] - return [expr {[llength $i] == 0}] - } - if {!$has_struct_set} { - set body { - package require struct::list - } - append body [info body is_list_all_ni_list2] - proc is_list_all_ni_list2 {a b} $body - } - proc is_cachedlist_all_ni_list {a b} { - upvar 0 ::punk::lib::caches::funcs_ni_list funcs - if {[info exists funcs($a)]} { - return [[set funcs($a)] $b] - } - set keybytes [encoding convertto utf-8 $a] - set key [binary encode base64 $keybytes] ;#one single-line base64 string - - set expression "" - foreach t $a { - #append expression "({$t} ni \$b) && " - append expression "{$t} ni \$b && " - } - set expression [string trimright $expression " &"] ;#trim trailing spaces and ampersands - proc ::punk::lib::caches::ni_list_$key {b} [string map [list @expression@ $expression] { - return [expr {@expression@}] - }] - - set funcs($a) ::punk::lib::caches::ni_list_$key - return [punk::lib::caches::ni_list_$key $b] - } - proc is_cachedlist_all_ni_list2 {a b} { - upvar 0 ::punk::lib::caches::funcs_ni_list funcs - if {[info exists funcs($a)]} { - return [[set funcs($a)] $b] - } - set keybytes [encoding convertto utf-8 $a] - set key [binary encode base64 $keybytes] ;#one single-line base64 string - - set d [dict create] - foreach x $a { - dict set d $x "" - } - #constructing a switch statement could be an option - # - but would need to avoid using escapes in order to get a jump-table - # - this would need runtime mapping of values - unlikely to be a win - proc ::punk::lib::caches::ni_list_$key {b} [string map [list @d@ $d] { - foreach x $b { - if {[::tcl::dict::exists {@d@} $x]} { - return 0 - } - } - return 1 - }] - - set funcs($a) ::punk::lib::caches::ni_list_$key - return [punk::lib::caches::ni_list_$key $b] - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::ldiff - @cmd -name punk::lib::ldiff\ - -summary\ - "Difference consisting of items with removeitems removed."\ - -help\ - "Somewhat like struct::set difference, but order preserving, and doesn't - treat as a 'set' so preserves any duplicates in items. - - struct::set difference may happen to preserve ordering when items are - integers, but order can't be relied on, especially as struct::set has - 2 differening implementations (tcl vs critcl) which return results with - different ordering to each other and different deduping behaviour in - some cases (e.g when 2nd arg is empty)" - @values -min 2 -max 2 - items -type list - removeitems -type list - }] - } - proc ldiff {items removeitems} { - if {[llength $removeitems] == 0} {return $items} - set result {} - foreach item $items { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - #with ledit (also avail in 8.6 using punk::lib::compat::ledit - proc ldiff2 {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - foreach item $removeitems { - set posns [lsearch -all -exact $fromlist $item] - foreach p $posns {ledit fromlist $p $p} - } - return $fromlist - } - proc ldiff3 {fromlist removeitems} { - set doomed [list] - foreach item $removeitems { - lappend doomed {*}[lsearch -all -exact $fromlist $item] - } - lremove $fromlist {*}$doomed - } - - #fix for tcl impl of struct::set::diff which doesn't dedupe - proc struct_set_diff_unique {A B} { - package require struct::set ;#require even if tcl impl - so the dependency isn't missed accidentally due to Loaded state of programmers machine. - if {[struct::set::Loaded] eq "tcl"} { - return [punk::lib::setdiff $A $B] - } else { - #use (presumably critcl) implementation for speed - return [struct::set difference $A $B] - } - } - - - #non-dupe preserving - for consistency in dedupe behaviour we don't shortcircuit empty B - #consistent dedupe and order-maintenance of remaining items in A differentiate this from struct::set difference (dedupes with critcl, doesn't with tcl implementation 2024) - #also struct::set difference with critcl is faster - proc setdiff {A B} { - if {[llength $A] == 0} {return {}} - set d [dict create] - foreach x $A {dict set d $x {}} - foreach x $B {dict unset d $x} - return [dict keys $d] - } - #bulk dict remove is slower than a foreach with dict unset - #proc setdiff2 {fromlist removeitems} { - # #if {[llength $fromlist] == 0} {return {}} - # set d [dict create] - # foreach x $fromlist { - # dict set d $x {} - # } - # return [dict keys [dict remove $d {*}$removeitems]] - #} - #array is about 15% faster than dict - but unordered. (which is ok for sets - but we have struct::set for that) - proc setdiff_unordered {A B} { - if {[llength $A] == 0} {return {}} - array set tmp {} - foreach x $A {::set tmp($x) .} - foreach x $B {catch {unset tmp($x)}} - return [array names tmp] - } - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lunique_unordered - @cmd -name punk::lib::lunique_unordered\ - -summary\ - "unique values in list"\ - -help\ - "Return unique values in provided list. - This removes duplicates but *may* rearrange the - order of the returned elements compared to the - original list. - - When struct::set is available this will be used - for the implementation, as it can be *slightly* - faster if acceleration is present. When struct::set - is not available it will fallback to lunique and - provide the same functionality with order preserved." - @values -min 1 -max 1 - list -type list - }] - } - #default/fallback implementation - proc lunique_unordered {list} { - lunique $list - } - if {$has_struct_set} { - if {[struct::set equal [struct::set union {a a} {}] {a}]} { - proc lunique_unordered {list} { - struct::set union $list {} - } - } else { - #struct::set union operates on a 'set' - so this probably won't change, and hopefully is - #consistent across unacelerated versions and those implemented in accelerators, - #but if it ever does change - be a little noisy about it. - puts stderr "punk::lib WARNING: struct::set union no longer dedupes!" - #we could also test a sequence of: struct::set add - } - } - - - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lunique - @cmd -name punk::lib::lunique\ - -summary\ - "Order-preserving unique values in list"\ - -help\ - "Return unique values in provided list. - This removes duplicates whilst preserving the - original order of the provided list. - - When struct::set is available with acceleration, - lunique_unordered may be slightly faster." - @values -min 1 -max 1 - list -type list - }] - } - proc lunique {list} { - set new {} - foreach item $list { - if {$item ni $new} { - lappend new $item - } - } - return $new - } - proc lunique2 {list} { - set doomed [list] - #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) - for {set i 0} {$i < [llength $list]} {} { - set item [lindex $list $i] - lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] - while {[incr i] in $doomed} {} - } - lremove $list {*}$doomed - } - #The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env - proc lmapflat_closure {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - # -- --- --- - #capture - use uplevel 1 or namespace eval depending on context - set capture [uplevel 1 { - apply { varnames { - set capturevars [tcl::dict::create] - set capturearrs [tcl::dict::create] - foreach fullv $varnames { - set v [tcl::namespace::tail $fullv] - upvar 1 $v var - if {[info exists var]} { - if {(![array exists var])} { - tcl::dict::set capturevars $v $var - } else { - tcl::dict::set capturearrs capturedarray_$v [array get var] - } - } else { - #A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set - } - } - return [tcl::dict::create vars $capturevars arrs $capturearrs] - } } [info vars] - } ] - # -- --- --- - set cvars [tcl::dict::get $capture vars] - set carrs [tcl::dict::get $capture arrs] - set apply_script "" - foreach arrayalias [tcl::dict::keys $carrs] { - set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - append apply_script [string map [list %realname% $realname %arrayalias% $arrayalias] { - array set %realname% [set %arrayalias%][unset %arrayalias%] - }] - } - - append apply_script [string map [list %script% $script] { - #foreach arrayalias [info vars capturedarray_*] { - # set realname [string range $arrayalias [string first _ $arrayalias]+1 end] - # array set $realname [set $arrayalias][unset arrayalias] - #} - #return [eval %script%] - %script% - }] - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - [concat $varnames [tcl::dict::keys $cvars] [tcl::dict::keys $carrs] ]\ - $apply_script\ - ] {*}[subst $values] {*}[tcl::dict::values $cvars] {*}[tcl::dict::values $carrs] ] - } - return $result - } - #link version - can write to vars in calling context - but keeps varnames themselves isolated - #performance much better than capture version - but still a big price to pay for the isolation - proc lmapflat_link {varnames list script} { - set result [list] - set values [list] - foreach v $varnames { - lappend values "\$$v" - } - set linkvars [uplevel 1 [list info vars]] - set nscaller [uplevel 1 [list namespace current]] - - set apply_script "" - foreach vname $linkvars { - append apply_script [string map [list %vname% $vname]\ - {upvar 2 %vname% %vname%}\ - ] \n - } - append apply_script $script \n - - #puts "--> $apply_script" - foreach $varnames $list { - lappend result {*}[apply\ - [list\ - $varnames\ - $apply_script\ - $nscaller\ - ] {*}[subst $values]\ - ] - } - return $result - } - - #proc lmapflat {varnames list script} { - # concat {*}[uplevel 1 [list lmap $varnames $list $script]] - #} - #lmap can accept multiple var list pairs - proc lmapflat {args} { - concat {*}[uplevel 1 [list lmap {*}$args]] - } - proc lmapflat2 {args} { - concat {*}[uplevel 1 lmap {*}$args] - } - - #proc dict_getdef {dictValue args} { - # if {[llength $args] < 1} { - # error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - # } - # set keys [lrange $args -1 end-1] - # if {[tcl::dict::exists $dictValue {*}$keys]} { - # return [tcl::dict::get $dictValue {*}$keys] - # } else { - # return [lindex $args end] - # } - #} - if {[info commands ::tcl::dict::getdef] eq ""} { - proc dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::lib::dict_getdef "" ::tcl::dict::getdef - } - - - #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" - #} - - #supports *safe* ultra basic offset expressions as used by lindex etc, but without the 'end' features - #safe in that we don't evaluate the expression as a string. - proc offset_expr {expression} { - set expression [tcl::string::map {_ {}} $expression] ;#review - this is for 8.6 to understand underscored ints - if {[tcl::string::is integer -strict $expression]} { - return [expr {$expression}] - } - if {[regexp {([^+-]*)([+-])(.*)} $expression _match a op b] && [tcl::string::is integer -strict $a] && [tcl::string::is integer -strict $b]} { - if {$op eq "-"} { - return [expr {$a - $b}] - } else { - return [expr {$a + $b}] - } - } else { - error "bad expression '$expression': must be integer?\[+-\]integer?" - } - } - - punk::args::define { - @id -id ::punk::lib::is_indexset - @cmd -name punk::lib::is_indexset\ - -summary\ - "Validate string is a comma-delimited 'indexset'."\ - -help\ - "Validate that a string is an 'indexset' - - An indexset consists of a comma delimited list of indexes or index-ranges. - No particular base is assumed for the purposes of validating an indexset here. - While in Tcl, lists are zero-based - an indexset can be applied to lists of any base. - e.g -10..-1 is an indexset that just won't resolve any results for a list with a base >= 0. - To validate if an indexset is strictly within range, both the length of the data and the base would - need to be considered. - - The normal 'range' specifier is .. but can be of the form .x. where x is the step value. - The range specifier can appear at the beginning, middle or end, or even alone to indicate the entire - range of valid values. - e.g the following are all valid ranges - 1.. - (index 1 to 'max') - ..10 - (index 'base' to 10) - 2..11 - (index 2 to 11) - .. - (all indices) - .3. - (1st index and every 3rd index thereafter) - - Common whitespace elements space,tab,newlines are ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - see indexset_resolve" - @values -min 1 -max 1 - indexset -type string - } - proc is_indexset {indexset} { - #collapse internal whitespace (for basic whitespace set we allow) - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] - if {![regexp {^[\-\+_end,\.0-9]*$} $indexset]} { - return 0 - } - set ranges [split $indexset ,] - foreach r $ranges { - set validateindices [list] - set rposn [string first .. $r] - if {$rposn >= 0} { - set sepsize 2 - set step 1 - #review - whitespace between ints? - lappend validateindices {*}[string range $r 0 $rposn-1] {*}[string range $r $rposn+2 end] - } elseif {[string first . $r] >= 0} { - set stripped [string map {. ""} $r] - if {[tcl::string::length $stripped] != [tcl::string::length $r]-2} { - #if one dot exists - must be exactly 2 dots in total - possibly separated by positive/negative int (not zero) - return 0 - } - #assert - we have exactly 2 dots separated by something. - #check for .n. 'stepped' range - set fdot [string first . $r] - set ldot [string last . $r] - set step [string range $r $fdot+1 $ldot-1] - #todo - allow basic mathops for step: 2+1 2+-1 etc same as tcl lindex, lseq - #1.0.10 should be valid but behave similarly to lseq 1 0 by 0 ie returns nothing - - #1.end.10 or similar shouldn't be valid - but we need to allow other basic index expressions. - if {[string match *end* $step] || [catch {lindex {} $step}]} { - return 0 - } - #if {![string is integer -strict $step] || $step == 0} { - # return 0 - #} - lappend validateindices {*}[string range $r 0 $fdot-1] {*}[string range $r $ldot+1 end] - } else { - #'range' is just an index - set validateindices [list $r] - } - - foreach v $validateindices { - if {$v eq "" || $v eq "end"} {continue} - if {[string is integer -strict $v]} {continue} - if {[catch {lindex {} $v}]} { - return 0 - } - } - } - return 1 - } - #review - compare to IMAP4 methods of specifying ranges? - #TODO add tests to test::punk::lib indexset_resolve is a little tricky - punk::args::define { - @id -id ::punk::lib::indexset_resolve - @cmd -name punk::lib::indexset_resolve\ - -summary\ - "Resolve an indexset to a list of integers based on supplied list or string length."\ - -help\ - "Resolve an 'indexset' to a list of actual indices within the range of the provided numitems value. - e.g in a basic case: for a list of 10 items, 'indexset_resolve 10 end' will return the index 9 - - An indexset consists of a comma delimited list of indexes or index-ranges. - Ranges must be specified with a range-indicator such as .. as the separator, with an empty value at - either side of the separator representing beginning and end of the index range respectively. - The range-separator can be of the form .x. where x is an integer or basic expression - (single +/- operation) that indicates the step value to use. This is equivalent to the 'by' value - in the tcl9 lseq command. - - When the start index is lower than the end, the step value defaults to 1. - ie indexset_resolve 0..7 is equivalent to indexset_resolve 0.1.7 - When the start index is higher than the end, the step value defaults to -1. - ie indexset_resolve 7..0 is equivalent to indexset_resolve 0.-1.7 - - If start and end are ommitted, increasing order is assumed if the step isn't specified. - eg - .. represents the range from the base to the end - .-1. would represent end to base with step -1 - - If start is omitted and only the end is supplied: - The default step is 1 indicating ascension and the missing start is equivalent to the base. - indexset_resolve 5 ..2 - -> 0 1 2 - The default start is 'end' if the step is negative - indexset_resolve 5 .-1.2 - -> 4 3 2 - If end is omitted and only the start is supplied: - The default step is 1 indicating ascension and the missing end is equivalent to 'end' - indexset_resolve 5 2.. - -> 2 3 4 - The default end is the base if the step is negative - indexset_resolve 5 2.-1. - -> 2 1 0 - - Like the tcl9 lseq command - a step (by) value of zero produces no results. - - The indexes are 0-based by default, but the base can be specified. - indexset_resolve 7 .. - -> 0 1 2 3 4 5 6 - indexset_resolve -base -3 7 .. - -> -3 -2 -1 0 1 2 3 - - Whitespace is ignored. - Each index (or endpoint of an index-range) can be of the forms accepted by Tcl list or string commands, - e.g end-2 or 2+2. - - end means the last item. - end-1 means the second last item. - 0.. is the same as 0..end - - indexset examples: - - These assume the default 0-based indices (-base 0) - - 1,3.. - output the index 1 (2nd item) followed by all from index 3 to the end. - indexset_resolve 4 1,3.. - -> 1 3 - indexset_resolve 10 1,3.. - -> 1 3 4 5 6 7 8 9 - 0..2,end - output the first 3 indices, and the last index. - end-1..0 - output the indexes in reverse order from 2nd last item to first item." - @leaders -min 0 -max 0 - @opts - -base -type integer -prefix 1 -default 0 -help\ - "This is the starting index. It can be positive, negative or zero. - This affects the start and end calculations, limiting what indices will be - returned. - e.g with base 1 'end' will give a different value from base 0 - - for 10 items 'end' is 10 when 1-based - for 10 items 'end' is 9 when 0-based - - For base 1, index 0 is considered to be below the range. - ie - indexset_resolve -base 1 10 0..3 - -> 1 2 3 - indexset_resolve -base 0 10 0..3 - -> 0 1 2 3 - - It does not *convert* indexes within the range. - - indexset_resolve -base 1 10 5 - -> 5 - indexset_resolve -base 0 10 5 - -> 5 - - ie if you ask for a 1-based resolution of an indexset the integers that are within - the range will come out the same, so the result needs to be treated as a 1-based - set of indices when performing further operations. - " - @values -min 2 -max 3 - numitems -type integer - indexset -type indexset -help "comma delimited specification for indices to return" - } - - #limit punk::args parsing to unhappy paths where possible - proc indexset_resolve {args} { - # -------------------------------------------------- - # Manual parsing of happy path args instead of using punk::args::parse $args withid ::punk::lib::indexset_resolve - # This is because indexset_resolve is *somewhat* low level, has only a few args, and we don't want any overhead. - # for the unhappy path - the punk::args::parse is fine to generate the usage/error information. - # -------------------------------------------------- - if {[llength $args] < 2} { - punk::args::resolve $args withid ::punk::lib::indexset_resolve - } - set indexset [lindex $args end] - set numitems [lindex $args end-1] - if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { - #use parser on unhappy path only - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - #assert we have 2 or more args - set base 0 ;#default - if {[llength $args] > 2} { - #if more than just numitems and indexset - we expect only -base ie 4 args in total - if {[llength $args] != 4} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set optname [lindex $args 0] - set optval [lindex $args 1] - set fulloptname [tcl::prefix::match -error "" -base $optname] - if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set base $optval - } - # -------------------------------------------------- - - - set indexset [string map [list " " "" \t "" \r\n "" \n ""] $indexset] ;#collapse basic whitespace - set index_list [list] ;#list of actual indexes within the range - set iparts [split $indexset ,] - set based_max [expr {$numitems -1 + $base}] - - #we already did is_indexset check above, so we can make assumptions about well-formedness of each part - foreach ipart $iparts { - set ipart [string trim $ipart] - #we need to cater for n..m as well as n.s.m where s is 'step' - set rposn [string first . $ipart] - if {$rposn>=0} { - #if we found one dot - there must be exactly 2 dots in the ipart, separated by nothing, or a basic integer-expression - set rposn2 [string last . $ipart] - if {$rposn2 == $rposn+1} { - #.. - set step "default" ;#could be 1 or -1 - } else { - set step [tcl::string::range $ipart $rposn+1 $rposn2-1] - } - lassign [punk::lib::string_splitbefore_indices $ipart $rposn $rposn2+1] rawa _ rawb - - set rawa [string trim $rawa] - set rawb [string trim $rawb] - if {$rawa eq "" && $rawb eq ""} { - if {$step eq "default"} { - set step 1 ;#default ascending when no start and no end - } - if {$step < 0} { - set rawa end - set rawb $base - } else { - set rawa $base - set rawb end - } - #if neither start nor end specified - we won't get out of range results from lindex_resolve - set a [punk::lib::lindex_resolve $numitems $rawa $base] - set b [punk::lib::lindex_resolve $numitems $rawb $base] - } else { - if {$rawa eq ""} { - if {$step eq "default"} { - #when start not specified, but end is - default direction always ascending - #(even if end is base or below range) - set step 1 - } - if {$step < 0} { - set rawa end - } else { - set rawa $base - } - } - set a [punk::lib::lindex_resolve $numitems $rawa $base] - if {$a == -Inf} { - #undershot - leave negative - } elseif {$a == Inf} { - #overshot - set a [expr {$based_max + 1}] ;#put it outside the range on the upper side - } - #review - a may be -Inf - - if {$rawb eq ""} { - if {$step eq "default"} { - set step 1 - } - if {$step < 0} { - if {$a < $base} { - #make sure both - #mathfunc::isinf is tcl9+ - if {[catch { - if {[::tcl::mathfunc::isinf $a]} { - set a [expr {$base -1}] - } - }]} { - if {[string match -nocase *inf* $a]} { - set a [expr {$base -1}] - } - } - set rawb $a - } else { - set rawb $base - } - } else { - if {$a > $based_max} { - set rawb $a ;#make sure .. doesn't return last item - should return nothing - } else { - set rawb end - } - } - } - set b [punk::lib::lindex_resolve $numitems $rawb $base] - if {$b == -Inf} { - #undershot - leave negative - } elseif {$b == Inf} { - #set b [expr {$numitems}] ;#overshot - put it outside the range on the upper side - set b [expr {$based_max + 1}] ;#overshot - put it outside the range on the upper side - } - } - - #JJJ - - #e.g make sure .. doesn't return last item - should return nothing as both are above the range. - if {$a >= $base && $a <= $based_max && $b >=$base && $b <= $based_max} { - #assert a & b are integers within the range - if {$step eq "default"} { - #unspecified step - base direction on order of a & b - if {$a <= $b} { - set step 1 - } else { - set step -1 - } - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - if {$a >= $base && $a <= $based_max} { - #only a is in the range - if {$b < $base} { - set b $base - } else { - set b $based_max - } - if {$step eq "default"} { - if {$a <= $b} { - set step 1 - } else { - set step -1 - } - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$b >=$base && $b <= $based_max} { - #only b is in the range - if {$step eq "default"} { - if {$a <= $b} { - set step 1 - } else { - set step -1 - } - } - if {$step < 0} { - if {$a < $base} { - #negative step from below - doesn't matter if b is in range - recast both to an int below $base - #(a may be -Inf) - set a [expr {$base -1}] - set b $a - set step 0 ;#we should return nothing - } - } else { - if {$a < $base} { - set a $base - } else { - set a $based_max - } - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } else { - #both outside the range - if {$a < $base && $b > $base} { - #spans the range in forward order - set a $base - set b $based_max - if {$step eq "default"} { - set step 1 - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } elseif {$a > $base && $b < $base} { - #spans the range in reverse order - set a $based_max - set b $base - if {$step eq "default"} { - set step -1 - } - lappend index_list {*}[punk::lib::range $a $b $step] ;#required for tcl8.6, on tcl9 this will call lseq internally. - } - #both outside of range on same side - } - } - } else { - set idx [punk::lib::lindex_resolve_basic $numitems $ipart $base] - #returns only -Inf for out of range at either end - if {$idx >= $base} { - #index within the range - lappend index_list $idx - } - } - } - return $index_list - } - # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side - #This doesn't need the list itself - just the length suffices. - punk::args::define { - @id -id ::punk::lib::lindex_resolve - @cmd -name punk::lib::lindex_resolve\ - -summary\ - "Resolve an indexexpression to an integer based on supplied list or string length."\ - -help\ - "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 - to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating - whether the index was below or above the range of possible indices for the length supplied. - - Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - This means the proc may be called with something like $x+2 end-$y etc - Sometimes the actual integer index is desired. - - We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - - which could have security risks. - lindex_resolve will parse the index expression and return: - a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - The similar function lindex_resolve_basic uses -Inf to denote - out of range at either end of the list/string. - Otherwise it will return an integer corresponding to the position in the data. - This is in stark contrast to Tcl list/string function indices which will return empty strings for out of - bounds indices, or in the case of lrange, return results anyway. - Like Tcl list commands - it will produce an error if the form of the index is not acceptable. - For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side - - thus returning -2 - - Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. - We will get something like 10+1 - which can be resolved safely with expr - " - @values -min 2 -max 2 - datalength -type integer - index -type indexexpression - } - proc lindex_resolve {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve] [arg len] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length - #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. - #[para]This means the proc may be called with something like $x+2 end-$y etc - #[para]Sometimes the actual integer index is desired. - #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return: - #[para] a) -Inf if the supplied index expression is below the lower bound for the supplied list. (< 0) - #[para] b) Inf if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string - #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. - #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable - #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr - - - #REVIEW - we need compat for 1_000 etc to handle things like toml even in 8.6? - #A basic string map means we aren't properly validating - #todo - be stricter about malformations such as 1000_ - if {![string is integer -strict 1_0]} { - #basic forward compatibility with integers such as 1_000 for 8.6.x - set index [tcl::string::map {_ {}} $index] - set len [tcl::string::map {_ {}} $len] - } - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve len must be a positive integer." - } - set based_max [expr {$len -1 + $base}] - - if {[string is integer -strict $index]} { - #review - base? - #can match +i -i - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } else { - if {[string match end* $index]} { - if {$index ne "end"} { - set op [string index $index 3] - set offset [string range $index 4 end] - #note - offset could have leading + or - - # 'string is integer -strict +1' ==> true - #e.g end+-1 is valid (end++-1 is not) - if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} - if {$offset == 0} { - #(offset +0, -0 or 0 or 000 0_0 etc) - #op either + or - is irrelevant - #set index [expr {$len-1}] ;#+ base ? - set index $based_max - if {$index < $base} { - #return -2 ;#special case - equivalent to 'end', with empty list - treat like a positive number out of bounds - return Inf - } else { - return $index - } - } - - #set index [if {$op eq "+"} {expr {($len-1) + $offset}} else {expr {($len-1) - $offset}}] - set index [if {$op eq "+"} {expr {$based_max + $offset}} else {expr {$based_max - $offset}}] - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } else { - return $index - } - } else { - #index is 'end' - if {$len == 0} { - #special case - 'end' with empty list - treat end like a positive number out of bounds - return Inf - } - #return [expr {$len - 1 + $base}] - return $based_max - } - } else { - #plain +- already handled above. - #we are trying to avoid evaluating unbraced expr of potentially insecure origin - #regexp must split a++b to a + +b (not a+ + b) ie first +/- is the op - if {[regexp {([^+-]*)([+-])(.*)} $index _match a op b]} { - if {[string is integer -strict $a] && [string is integer -strict $b]} { - if {$op eq "-"} { - set index [expr {$a - $b}] - } else { - set index [expr {$a + $b}] - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - } else { - error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" - } - if {$index < $base} { - return -Inf - } elseif {$index > $based_max} { - return Inf - } - return $index - } - } - } - proc lindex_resolve_basic {len index {base 0}} { - #*** !doctools - #[call [fun lindex_resolve_basic] [arg len] [arg index]] - #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) - #[para] returns -Inf for out of range at either end, or a valid integer index - #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command - #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 - #[para] For pure integer indices the performance should be equivalent - - if {![string is integer -strict $len] || $len < 0} { - error "lindex_resolve_basic len must be an integer greater than or equal to zero" - } - if {![string is integer -strict $base]} { - #base can be negative - error "lindex_resolve_basic base must be an integer" - } - set based_max [expr {$len -1 + $base}] - - set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 - if {[string is integer -strict $index]} { - #can match +i -i - #avoid even the lseq overhead when the index is simple - if {$index < $base || ($index > $based_max)} { - #even though in this case we could return -Inf or Inf like lindex_resolve; - #for consistency we don't return Inf for upper-boudn violation, - #as which bound is violated is not always directly determinable for compound index expressions (such as end-x) using the lseq+lindex mechanism. - return -Inf - } else { - #!NOTE! index within range is unchanged - no matter the base - #integer may still have + sign - normalize with expr - return [expr {$index}] - } - } - if {$len > 0} { - #For large len - this is a wasteful allocation if no true lseq available in Tcl version. - #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) - set testlist [punk::lib::range $base $based_max] ;# uses lseq if available, has fallback of creating a potentially large list of numbers. - } else { - set testlist [list] - #we want to call 'lindex' even in this case - to get the appropriate error message - } - set idx [lindex $testlist $index] - if {$idx eq ""} { - #we have no way to determine if out of bounds is at lower vs upper end - return -Inf - } else { - return $idx - } - } - proc lindex_get {list index} { - set resultlist [lrange $list $index $index] - if {![llength $resultlist]} { - return -1 - } else { - #we still don't know the actual integer index for an index such as end-x or int-int without parsing and evaluating ourself. - #we can return the value - but only in a way that won't collide with our -1 out-of-range indicator - return [tcl::dict::create value [lindex $resultlist 0]] - } - } - - proc string_splitbefore {str index} { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - return [list "" $str] - } - Inf { - return [list $str ""] - } - } - } - return [list [string range $str 0 $index-1] [string range $str $index end]] - #scan %s stops at whitespace - not useful here. - #scan $s %${p}s%s - } - proc string_splitbefore_indices {str args} { - set parts [list $str] - set sizes [list [string length $str]] - set s 0 - foreach index $args { - if {![string is integer -strict $index]} { - set index [punk::lib::lindex_resolve [string length $str] $index] - switch -- $index { - -Inf { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - Inf { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - } - } - if {$index <= 0} { - if {[lindex $sizes 0] != 0} { - ledit parts 0 0 {} [lindex $parts 0] - ledit sizes 0 0 0 [lindex $sizes 0] - } - continue - } - if {$index >= [string length $str]} { - if {[lindex $sizes end] != 0} { - ledit parts end end [lindex $parts end] {} - ledit sizes end end [lindex $sizes end] 0 - } - continue - } - set i -1 - set a 0 - foreach sz $sizes { - incr i - if {$a + $sz > $index} { - set p [lindex $parts $i] - #puts "a:$a index:$index" - if {$a == $index} { - break - } - ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] - ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] - break - } - incr a $sz - } - #puts "->parts:$parts" - #puts "->sizes:$sizes" - } - return $parts - } - - proc K {x y} {return $x} - #*** !doctools - #[call [fun K] [arg x] [arg y]] - #[para]The K-combinator function - returns the first argument, x and discards y - #[para]see [uri https://wiki.tcl-lang.org/page/K] - #[para]It is used in cases where command-substitution at the calling-point performs some desired effect. - - - proc is_utf8_multibyteprefix {bytes} { - #*** !doctools - #[call [fun is_utf8_multibyteprefix] [arg str]] - #[para] Returns a boolean if str is potentially a prefix for a multibyte utf-8 character - #[para] ie - tests if it is possible that appending more data will result in a utf-8 codepoint - #[para] Will return false for an already complete utf-8 codepoint - #[para] It is assumed the incomplete sequence is at the beginning of the bytes argument - #[para] Suitable input for this might be from the unreturned tail portion of get_utf8_leading $testbytes - #[para] e.g using: set head [lb]get_utf8_leading $testbytes[rb] ; set tail [lb]string range $testbytes [lb]string length $head[rb] end[rb] - regexp {(?x) - ^ - (?: - [\xC0-\xDF] | #possible prefix for two-byte codepoint - [\xE0-\xEF] [\x80-\xBF]{0,1} | #possible prefix for three-byte codepoint - [\xF0-\xF4] [\x80-\xBF]{0,2} #possible prefix for - ) - $ - } $bytes - } - - proc is_utf8_first {str} { - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - } $str - } - proc is_utf8_single {1234bytes} { - #*** !doctools - #[call [fun is_utf8_single] [arg 1234bytes]] - #[para] Tests input of 1,2,3 or 4 bytes and responds with a boolean indicating if it is a valid utf-8 character (codepoint) - regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - ^ - (?: - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) - $ - } $1234bytes - } - proc get_utf8_leading {rawbytes} { - #*** !doctools - #[call [fun get_utf8_leading] [arg rawbytes]] - #[para] return the leading portion of rawbytes that is a valid utf8 sequence. - #[para] This will stop at the point at which the bytes can't be interpreted as a complete utf-8 codepoint - #[para] e.g It will not return the first byte or 2 of a 3-byte utf-8 character if the last byte is missing, and will return only the valid utf-8 string from before the first byte of the incomplete character. - #[para] It will also only return the prefix before any bytes that cannot be part of a utf-8 sequence at all. - #[para] Note that while this will return valid utf8 - it has no knowledge of grapheme clusters or diacritics - #[para] This means if it is being used to process bytes split at some arbitrary point - the trailing data that isn't returned could be part of a grapheme cluster that belongs with the last character of the leading string already returned - #[para] The utf-8 BOM \xEF\xBB\xBF is a valid UTF8 3-byte sequence and so can also be returned as part of the leading utf8 bytes - if {[regexp {(?x) # Expanded regexp syntax, so I can put in comments :-) - \A ( - [\x00-\x7F] | # Single-byte chars (ASCII range) - [\xC0-\xDF] [\x80-\xBF] | # Two-byte chars (\u0080-\u07FF) - [\xE0-\xEF] [\x80-\xBF]{2} | # Three-byte chars (\u0800-\uFFFF) - [\xF0-\xF4] [\x80-\xBF]{3} # Four-byte chars (U+10000-U+10FFFF, not supported by Tcl 8.5) - ) + - } $rawbytes completeChars]} { - return $completeChars - } - return "" - } - proc hex2dec {args} { - #*** !doctools - #[call [fun hex2dec] [opt {option value...}] [arg list_largeHex]] - #[para]Convert a list of (possibly large) unprefixed hex strings to their decimal values - #[para]hex2dec accepts and ignores internal underscores in the same manner as Tcl 8.7+ numbers e.g hex2dec FF_FF returns 65535 - #[para]Leading and trailing underscores are ignored as a matter of implementation convenience - but this shouldn't be relied upon. - #[para]Leading or trailing whitespace in each list member is allowed e.g hex2dec " F" returns 15 - #[para]Internal whitespace e.g "F F" is not permitted - but a completely empty element "" is allowed and will return 0 - - set list_largeHex [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::hex2dec arguments prior to list_largeHex must be option/value pairs - received '$argopts'" - } - set opts [tcl::dict::create\ - -validate 1\ - -empty_as_hex "INVALID set -empty_as_hex to a hex string e.g FF if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $opts] - foreach {k v} $argopts { - tcl::dict::set opts [tcl::prefix match -message "options for hex2dec. Unexpected option" $known_opts $k] $v - } - # -- --- --- --- - set opt_validate [tcl::dict::get $opts -validate] - set opt_empty [tcl::dict::get $opts -empty_as_hex] - # -- --- --- --- - - set list_largeHex [lmap h $list_largeHex[unset list_largeHex] {string map {_ ""} [string trim $h]}] - if {$opt_validate} { - #Note appended F so that we accept list of empty strings as per the documentation - if {![string is xdigit -strict [join $list_largeHex ""]F ]} { - error "[namespace current]::hex2dec error: non-hex digits encountered after stripping underscores and leading/trailing whitespace for each element\n $list_largeHex" - } - } - if {![string is xdigit -strict [string map {_ ""} $opt_empty]]} { - #mapping empty string to a value destroys any advantage of -scanonly - #todo - document that -scanonly has 2 restrictions - each element must be valid hex and less than 7 chars long - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {0} : {[set v]}}] - if {[lsearch $list_largeHex ""] >=0} { - error "[namespace current]::hex2dec error: empty values in list cannot be mapped to non-hex $opt_empty" - } - } else { - set opt_empty [string trim [string map {_ ""} $opt_empty]] - if {[set first_empty [lsearch $list_largeHex ""]] >= 0} { - #set list_largeHex [lmap v $list_largeHex[set list_largeHex {}] {expr {$v eq ""} ? {$opt_empty} : {$v}}] - set nonempty_head [lrange $list_largeHex 0 $first_empty-1] - set list_largeHex [concat $nonempty_head [lmap v [lrange $list_largeHex $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [scan $list_largeHex [string repeat %llx [llength $list_largeHex]]] - } - - proc dec2hex {args} { - #*** !doctools - #[call [fun dex2hex] [opt {option value...}] [arg list_decimals]] - #[para]Convert a list of decimal integers to a list of hex values - #[para] -width can be used to make each hex value at least int characters wide, with leading zeroes. - #[para] -case upper|lower determines the case of the hex letters in the output - set list_decimals [lindex $args end] - set argopts [lrange $args 0 end-1] - if {[llength $argopts]%2 !=0} { - error "[namespace current]::dec2hex arguments prior to list_decimals must be option/value pairs - received '$argopts'" - } - set defaults [tcl::dict::create\ - -width 1\ - -case upper\ - -empty_as_decimal "INVALID set -empty_as_decimal to a number if empty values should be replaced"\ - ] - set known_opts [tcl::dict::keys $defaults] - set fullopts [tcl::dict::create] - foreach {k v} $argopts { - tcl::dict::set fullopts [tcl::prefix match -message "options for [tcl::namespace::current]::dec2hex. Unexpected option" $known_opts $k] $v - } - set opts [tcl::dict::merge $defaults $fullopts] - # -- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_case [tcl::dict::get $opts -case] - set opt_empty [tcl::dict::get $opts -empty_as_decimal] - # -- --- --- --- - - - set resultlist [list] - switch -- [string tolower $opt_case] { - upper { - set spec X - } - lower { - set spec x - } - default { - error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower" - } - } - set fmt "%${opt_width}.${opt_width}ll${spec}" - - set list_decimals [lmap d $list_decimals[unset list_decimals] {string map {_ ""} [string trim $d]}] - if {![string is digit -strict [string map {_ ""} $opt_empty]]} { - if {[lsearch $list_decimals ""] >=0} { - error "[namespace current]::dec2hex error: empty values in list cannot be mapped to non-decimal $opt_empty" - } - } else { - set opt_empty [string map {_ ""} $opt_empty] - if {[set first_empty [lsearch $list_decimals ""]] >= 0} { - set nonempty_head [lrange $list_decimals 0 $first_empty-1] - set list_decimals [concat $nonempty_head [lmap v [lrange $list_decimals $first_empty end] {expr {$v eq ""} ? {$opt_empty} : {$v}}]] - } - } - return [format [lrepeat [llength $list_decimals] $fmt] {*}$list_decimals] - } - - proc log2 x "expr {log(\$x)/[expr log(2)]}" - #*** !doctools - #[call [fun log2] [arg x]] - #[para]log base2 of x - #[para]This uses a 'live' proc body - the divisor for the change of base is computed once at definition time - #[para](courtesy of RS [uri https://wiki.tcl-lang.org/page/Additional+math+functions]) - - proc logbase {b x} { - #*** !doctools - #[call [fun logbase] [arg b] [arg x]] - #[para]log base b of x - #[para]This function uses expr's natural log and the change of base division. - #[para]This means for example that we can get results like: logbase 10 1000 = 2.9999999999999996 - #[para]Use expr's log10() function or tcl::mathfunc::log10 for base 10 - expr {log($x)/log($b)} - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::factors - @cmd -name punk::lib::factors\ - -summary\ - "Sorted factors of positive integer x"\ - -help\ - "Return a sorted list of the positive factors of x where x > 0 - For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. - (including zero itself in this context)* - - This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - Comparisons were done with some numbers below 17 digits long - For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - but has the disadvantage of being slower for 'small' numbers and using more memory. - If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get - there than computing the whole list, even for small values of x - * Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - In other mathematical contexts zero may be considered not to divide anything." - @values -min 1 -max 1 - x -type integer - }] - } - proc factors {x} { - #*** !doctools - #[call [fun factors] [arg x]] - #[para]Return a sorted list of the positive factors of x where x > 0 - #[para]For x = 0 we return only 0 and 1 as technically any number divides zero and there are an infinite number of factors. (including zero itself in this context)* - #[para]This is a simple brute-force implementation that iterates all numbers below the square root of x to check the factors - #[para]Because the implementation is so simple - the performance is very reasonable for numbers below at least a few 10's of millions - #[para]See tcllib math::numtheory::factors for a more complex implementation - which seems to be slower for 'small' numbers - #[para]Comparisons were done with some numbers below 17 digits long - #[para]For seriously big numbers - this simple algorithm would no doubt be outperformed by more complex algorithms. - #[para]The numtheory library stores some data about primes etc with each call - so may become faster when being used on more numbers - #but has the disadvantage of being slower for 'small' numbers and using more memory. - #[para]If the largest factor below x is needed - the greatestOddFactorBelow and GreatestFactorBelow functions are a faster way to get there than computing the whole list, even for small values of x - #[para]* Taking x=0; Notion of x being divisible by integer y being: There exists an integer p such that x = py - #[para] In other mathematical contexts zero may be considered not to divide anything. - set factors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {($x % $j) == 0} { - lappend factors $j [expr {$x / $j}] - } - incr j - } - lappend factors $x - return [lsort -unique -integer $factors] - } - proc oddFactors {x} { - #*** !doctools - #[call [fun oddFactors] [arg x]] - #[para]Return a list of odd integer factors of x, sorted in ascending order - set j 2 - set max [expr {sqrt($x)}] - set factors [list 1] - while {$j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2} { - if {$other ni $factors} { - lappend factors $other - } - } - if {$j % 2} { - if {$j ni $factors} { - lappend factors $j - } - } - } - incr j - } - return [lsort -integer -increasing $factors] - } - proc greatestFactorBelow {x} { - #*** !doctools - #[call [fun greatestFactorBelow] [arg x]] - #[para]Return the largest factor of x excluding itself - #[para]factor functions can be useful for console layout calculations - #[para]See Tcllib math::numtheory for more extensive implementations - if {$x % 2 == 0 || $x == 0} { - return [expr {$x / 2}] - } - set j 3 - set max [expr {sqrt($x)}] - while {$j <= $max} { - if {$x % $j == 0} { - return [expr {$x / $j}] - } - incr j 2 - } - return 1 - } - proc greatestOddFactorBelow {x} { - #*** !doctools - #[call [fun greatestOddFactorBelow] [arg x]] - #[para]Return the largest odd integer factor of x excluding x itself - if {$x %2 == 0} { - return [greatestOddFactor $x] - } - set j 3 - #dumb brute force - time taken to compute is wildly variable on big numbers - #todo - use a (memoized?) generator of primes to reduce the search space - #tcllib math::numtheory has suitable functions - but do we want that dependency here? Testing shows brute-force often faster for small numbers. - set god 1 - set max [expr {sqrt($x)}] - while { $j <= $max} { - if {$x % $j == 0} { - set other [expr {$x / $j}] - if {$other % 2 == 0} { - set god $j - } else { - set god [expr {$x / $j}] - #lowest j - so other side must be highest - break - } - } - incr j 2 - } - return $god - } - proc greatestOddFactor {x} { - #*** !doctools - #[call [fun greatestOddFactor] [arg x]] - #[para]Return the largest odd integer factor of x - #[para]For an odd value of x - this will always return x - if {$x % 2 != 0 || $x == 0} { - return $x - } - set r [expr {$x / 2}] - while {$r % 2 == 0} { - set r [expr {$r / 2}] - } - return $r - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::gcd - @cmd -name punk::lib::gcd\ - -summary\ - "Greatest common divisor of m and n."\ - -help\ - "Return the greatest common divisor of m and n. - Straight from Lars Hellström's math::numtheory library in Tcllib - - Graphical use: - An a by b rectangle can be covered with square tiles of side-length c, - only if c is a common divisor of a and b" - @values -min 2 -max 2 - m -type integer - n -type integer - }] - } - proc gcd {n m} { - #*** !doctools - #[call [fun gcd] [arg n] [arg m]] - #[para]Return the greatest common divisor of m and n - #[para]Straight from Lars Hellström's math::numtheory library in Tcllib - #[para]Graphical use: - #[para]An a by b rectangle can be covered with square tiles of side-length c, - #[para]only if c is a common divisor of a and b - - # - # Apply Euclid's good old algorithm - # - if { $n > $m } { - set t $n - set n $m - set m $t - } - - while { $n > 0 } { - set r [expr {$m % $n}] - set m $n - set n $r - } - - return $m - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::lib::lcm - @cmd -name punk::lib::lcm\ - -summary\ - "Lowest common multiple of m and n."\ - -help\ - "Return the lowest common multiple of m and n. - Straight from Lars Hellström's math::numtheory library in Tcllib" - @values -min 2 -max 2 - m -type integer - n -type integer - }] - } - proc lcm {n m} { - set gcd [gcd $n $m] - return [expr {$n*$m/$gcd}] - } - proc commonDivisors {x y} { - #*** !doctools - #[call [fun commonDivisors] [arg x] [arg y]] - #[para]Return a list of all the common factors of x and y - #[para](equivalent to factors of their gcd) - return [factors [gcd $x $y]] - } - - #experimental only - there are better/faster ways - proc sieve n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - tcl::dict::for {next -} $nums break - } - return [concat $primes [tcl::dict::keys $nums]] - } - proc sieve2 n { - set primes [list] - if {$n < 2} {return $primes} - set nums [tcl::dict::create] - for {set i 2} {$i <= $n} {incr i} { - tcl::dict::set nums $i "" - } - set next 2 - set limit [expr {sqrt($n)}] - while {$next <= $limit} { - for {set i $next} {$i <= $n} {incr i $next} {tcl::dict::unset nums $i} - lappend primes $next - #dict for {next -} $nums break - set next [lindex $nums 0] - } - return [concat $primes [tcl::dict::keys $nums]] - } - - proc hasglobs {str} { - #*** !doctools - #[call [fun hasglobs] [arg str]] - #[para]Return a boolean indicating whether str contains any of the glob characters: * ? [lb] [rb] - #[para]hasglobs uses append to preserve Tcls internal representation for str - so it should help avoid shimmering in the few cases where this may matter. - regexp {[*?\[\]]} [append obj2 $str {}] ;# int-rep preserving - } - - proc trimzero {number} { - #*** !doctools - #[call [fun trimzero] [arg number]] - #[para]Return number with left-hand-side zeros trimmed off - unless all zero - #[para]If number is all zero - a single 0 is returned - set trimmed [string trimleft $number 0] - if {[string length $trimmed] == 0} { - set trimmed 0 - } - return $trimmed - } - proc substring_count {str substring} { - #*** !doctools - #[call [fun substring_count] [arg str] [arg substring]] - #[para]Search str and return number of occurrences of substring - - #faster than lsearch on split for str of a few K - if {$substring eq ""} {return 0} - set occurrences [expr {[string length $str]-[string length [string map [list $substring {}] $str]]}] - return [expr {$occurrences / [string length $substring]}] - } - - proc dict_merge_ordered {defaults main} { - #*** !doctools - #[call [fun dict_merge_ordered] [arg defaults] [arg main]] - #[para]The standard dict merge accepts multiple dicts with values from dicts to the right (2nd argument) taking precedence. - #[para]When merging with a dict of default values - this means that any default key/vals that weren't in the main dict appear in the output before the main data. - #[para]This function merges the two dicts whilst maintaining the key order of main followed by defaults. - - #1st merge (inner merge) with wrong values taking precedence - but right key-order - then (outer merge) restore values - return [tcl::dict::merge [tcl::dict::merge $main $defaults] $main] - } - - proc askuser {question} { - #*** !doctools - #[call [fun askuser] [arg question]] - #[para]A basic utility to read an answer from stdin - #[para]The prompt is written to the terminal and then it waits for a user to type something - #[para]stdin is temporarily configured to blocking and then put back in its original state in case it wasn't already so. - #[para]If the terminal is using punk::console and is in raw mode - the terminal will temporarily be put in line mode. - #[para](Generic terminal raw vs linemode detection not yet present) - #[para]The user must hit enter to submit the response - #[para]The return value is the string if any that was typed prior to hitting enter. - #[para]The question argument can be manually colourised using the various punk::ansi funcitons - #[example_begin] - # set answer [lb]punk::lib::askuser "[lb]a+ green bold[rb]Do you want to proceed? (Y|N)[lb]a[rb]"[rb] - # if {[lb]string match y* [lb]string tolower $answer[rb][rb]} { - # puts "Proceeding" - # } else { - # puts "Cancelled by user" - # } - #[example_end] - puts stdout $question - flush stdout - set stdin_state [chan configure stdin] - if {[catch { - package require punk::console - set console_raw [tsv::get console is_raw] - } err_console]} { - #assume normal line mode - set console_raw 0 - } - try { - chan configure stdin -blocking 1 - if {$console_raw} { - punk::console::disableRaw - set answer [gets stdin] - punk::console::enableRaw - } else { - set answer [gets stdin] - } - } finally { - chan configure stdin -blocking [tcl::dict::get $stdin_state -blocking] - } - return $answer - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text {max -1}} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - if {$max != -1} { - set len [expr {min($len,$max)}] - } - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - #e.g linesort -decreasing $data - proc linesort {args} { - #*** !doctools - #[call [fun linesort] [opt {sortoption ?val?...}] [arg textblock]] - #[para]Sort lines in textblock - #[para]Returns another textblock with lines sorted - #[para]options are flags as accepted by lsort ie -ascii -command -decreasing -dictionary -index -indices -integer -nocase -real -stride -unique - if {[llength $args] < 1} { - error "linesort missing lines argument" - } - set lines [lindex $args end] - set opts [lrange $args 0 end-1] - #.= list $lines |@0,sortopts/1> linelist |> .=data>1,sortopts>1* lsort |> list_as_lines <| {*}$opts - list_as_lines [lsort {*}$opts [linelist $lines]] - } - - proc list_as_lines {args} { - #*** !doctools - #[call [fun list_as_lines] [opt {-joinchar char}] [arg linelist]] - #[para]This simply joins the elements of the list with -joinchar - #[para]It is mainly intended for use in pipelines where the primary argument comes at the end - but it can also be used as a general replacement for join $lines - #[para]The sister function lines_as_list takes a block of text and splits it into lines - but with more options related to trimming the block and/or each line. - if {[set eop [lsearch $args --]] == [llength $args]-2} { - #end-of-opts not really necessary - except for consistency with lines_as_list - set args [concat [lrange $args 0 $eop-1] [lrange $args $eop+1 end]] - } - if {[llength $args] == 3 && [lindex $args 0] eq "-joinchar"} { - set joinchar [lindex $args 1] - set lines [lindex $args 2] - } elseif {[llength $args] == 1} { - set joinchar "\n" - set lines [lindex $args 0] - } else { - error "list_as_lines usage: list_as_lines ?-joinchar ? " - } - return [join $lines $joinchar] - } - proc list_as_lines2 {args} { - #eat or own dogfood version - shows the implementation is simpler - but unfortunately not suitable for a simple function like this which should be as fast as possible? - lassign [tcl::dict::values [punk::args::parse $args withdef { - -joinchar -default \n - @values -min 1 -max 1 - }]] leaders opts values - - return [join [tcl::dict::get $values 0] [tcl::dict::get $opts -joinchar]] - } - - proc lines_as_list {args} { - #*** !doctools - #[call [fun lines_as_list] [opt {option value ...}] [arg text]] - #[para]Returns a list of possibly trimmed lines depeding on options - #[para]The concept of lines is raw lines from splitting on newline after crlf is mapped to lf - #[para]- not console lines which may be entirely different due to control characters such as vertical tabs or ANSI movements - - #The underlying function linelist has the validation code which gives nicer usage errors. - #we can't use a dict merge here without either duplicating the underlying validation somewhat, or risking a default message from dict merge error - #..because we don't know what to say if there are odd numbers of args - #we can guess that it's ok to insert our default if no -block found in $args - but as a general principle this mightn't always work - #e.g if -block is also a valid value for the textblock itself. Which in this case it is - although unlikely, and our -block {} default is irrelevant in that case anyway - - if {[lsearch $args "--"] == [llength $args]-2} { - set opts [lrange $args 0 end-2] - } else { - set opts [lrange $args 0 end-1] - } - #set opts [tcl::dict::merge {-block {}} $opts] - set bposn [lsearch $opts -block] - if {$bposn < 0} { - lappend opts -block {} - } - set text [lindex $args end] - #tailcall linelist {*}$opts $text - return [linelist {*}$opts $text] - } - #this demonstrates the ease of using an args processor - but as lines_as_list is heavily used in terminal output - we can't afford the extra microseconds - proc lines_as_list2 {args} { - #pass -anyopts 1 so we can let the next function decide what arguments are valid - but still pass our defaults - #-anyopts 1 avoids having to know what to say if odd numbers of options passed etc - #we don't have to decide what is an opt vs a value - #even if the caller provides the argument -block without a value the next function's validation will report a reasonable error because there is now nothing in $values (consumed by -block) - lassign [tcl::dict::values [punk::args::parse $args withdef { - @opts -any 1 - -block -default {} - }]] leaderdict opts valuedict - tailcall linelist {*}$opts {*}[tcl::dict::values $valuedict] - } - - # important for pipeline & match_assign - # -line trimline|trimleft|trimright -block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty -commandprefix {string length} ? - # -block trimming only trims completely empty lines. use -line trimming to remove whitespace e.g -line trimright will clear empty lines without affecting leading whitespace on other lines that aren't pure whitespace - set linelist_body { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - ;#package require punk::ansi - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list/detectcode_in_list will check at first level. (not intended for detecting ansi in deeper structures) - - #we use detectcode_in_list instead of detect_in_list - #detectcode_in_list will detect unclosed (or unopened) paired sequences such as PM (privacy message) - # - but the main reason is it is slightly faster. - if {![punk::ansi::ta::detectcode_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - #set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - #get_codes_single lists only the codes. no plaintext or empty elements - set ansisplits [punk::ansi::ta::get_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits] == 0} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end] ;#may or may not be SGR - set lastcodeoffset [expr {[string length $lastcode]-1}] - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[string range $ln end-$lastcodeoffset end] eq $lastcode} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[string range $ln end-$lastcodeoffset end] eq $lastcode && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #code is at tail (no trailing plaintext) - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach code $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - - set linelist_body_original { - set usage "linelist ?-ansiresets auto|? ?-ansireplays 0|1? ?-line trimline|trimleft|trimright? ?-block trimhead|trimtail|triminner|trimall|trimhead1|trimtail1|collateempty? -commandprefix text" - if {[llength $args] == 0} { - error "linelist missing textchunk argument usage:$usage" - } - set text [lindex $args end] - set text [string map {\r\n \n} $text] ;#review - option? - - set arglist [lrange $args 0 end-1] - set opts [tcl::dict::create\ - -block {trimhead1 trimtail1}\ - -line {}\ - -commandprefix ""\ - -ansiresets auto\ - -ansireplays 0\ - ] - foreach {o v} $arglist { - switch -- $o { - -block - -line - -commandprefix - -ansiresets - -ansireplays { - tcl::dict::set opts $o $v - } - default { - error "linelist: Unrecognized option '$o' usage:$usage" - } - } - } - # -- --- --- --- --- --- - set opt_block [tcl::dict::get $opts -block] - if {[llength $opt_block]} { - foreach bo $opt_block { - switch -- $bo { - trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {} - default { - set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty] - error "linelist: unknown -block option value: $bo known values: $known_blockopts" - } - } - } - #normalize certain combos - if {"trimhead" in $opt_block && [set posn [lsearch $opt_block trimhead1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimtail" in $opt_block && [set posn [lsearch $opt_block trimtail1]] >=0} { - set opt_block [lreplace $opt_block $posn $posn] - } - if {"trimall" in $opt_block} { - #no other block options make sense in combination with this - set opt_block [list "trimall"] - } - - #TODO - if {"triminner" in $opt_block } { - error "linelist -block triminner not implemented - sorry" - } - - } - - - # -- --- --- --- --- --- - set opt_line [tcl::dict::get $opts -line] - set tl_left 0 - set tl_right 0 - set tl_both 0 - foreach lo $opt_line { - switch -- $lo { - trimline { - set tl_both 1 - } - trimleft { - set tl_left 1 - } - trimright { - set tl_right 1 - } - default { - set known_lineopts [list trimline trimleft trimright] - error "linelist: unknown -line option value: $lo known values: $known_lineopts" - } - } - } - #normalize trimleft trimright combo - if {$tl_left && $tl_right} { - set opt_line [list "trimline"] - set tl_both 1 - } - # -- --- --- --- --- --- - set opt_commandprefix [tcl::dict::get $opts -commandprefix] - # -- --- --- --- --- --- - set opt_ansiresets [tcl::dict::get $opts -ansiresets] - # -- --- --- --- --- --- - set opt_ansireplays [tcl::dict::get $opts -ansireplays] - if {$opt_ansireplays} { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 1 - } - } else { - if {$opt_ansiresets eq "auto"} { - set opt_ansiresets 0 - } - } - # -- --- --- --- --- --- - set linelist [list] - set nlsplit [split $text \n] - if {![llength $opt_line]} { - set linelist $nlsplit - #lappend linelist {*}$nlsplit - } else { - #already normalized trimleft+trimright to trimline - if {$tl_both} { - foreach ln $nlsplit { - lappend linelist [string trim $ln] - } - } elseif {$tl_left} { - foreach ln $nlsplit { - lappend linelist [string trimleft $ln] - } - } elseif {$tl_right} { - foreach ln $nlsplit { - lappend linelist [string trimright $ln] - } - } - } - - if {"collateempty" in $opt_block} { - set inputlist $linelist[set linelist [list]] - set last "-" - foreach input $inputlist { - if {$input ne ""} { - lappend linelist $input - set last "-" - } else { - if {$last ne ""} { - lappend linelist "" - } - set last "" - } - } - } - - if {"trimall" in $opt_block} { - set linelist [lsearch -all -inline -not -exact $linelist[set linelist {}] ""] - } else { - set start 0 - if {"trimhead" in $opt_block} { - set idx 0 - set lastempty -1 - foreach ln $linelist { - if {[lindex $linelist $idx] ne ""} { - break - } else { - set lastempty $idx - } - incr idx - } - if {$lastempty >=0} { - set start [expr {$lastempty +1}] - } - } - set linelist [lrange $linelist $start end] - - if {"trimtail" in $opt_block} { - set revlinelist [lreverse $linelist][set linelist {}] - set i 0 - foreach ln $revlinelist { - if {$ln ne ""} { - set linelist [lreverse [lrange $revlinelist $i end]] - break - } - incr i - } - } - - # --- --- - set start 0 - set end "end" - if {"trimhead1" in $opt_block} { - if {[lindex $linelist 0] eq ""} { - set start 1 - } - } - if {"trimtail1" in $opt_block} { - if {[lindex $linelist end] eq ""} { - set end "end-1" - } - } - set linelist [lrange $linelist $start $end] - } - - #review - we need to make sure ansiresets don't accumulate/grow on any line - #Each resulting line should have a reset of some type at start and a pure-reset at end to stop - #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansireplays} { - #package require punk::ansi - - if {$opt_ansiresets} { - set RST "\x1b\[0m" - } else { - set RST "" - } - set replaycodes $RST ;#todo - default? - set transformed [list] - #shortcircuit common case of no ansi - #NOTE: running ta::detect on a list (or dict) as a whole can be problematic if items in the list have backslash escapes due to Tcl list quoting and escaping behaviour. - #This commonly happens if there is an unbalanced brace (which is a normal occurrence and needs to be handled) - #ta::detect on a list of ansi-containing string may appear to work for some simple inputs but is not reliable - #detect_in_list will check at first level. (not intended for detecting ansi in deeper structures) - if {![punk::ansi::ta::detect_in_list $linelist]} { - if {$opt_ansiresets} { - foreach ln $linelist { - lappend transformed $RST$ln$RST - } - set linelist $transformed - } - } else { - - #INLINE punk::ansi::codetype::is_sgr_reset - #regexp {\x1b\[0*m$} $code - set re_is_sgr_reset {\x1b\[0*m$} - #INLINE punk::ansi::codetype::is_sgr - #regexp {\033\[[0-9;:]*m$} $code - set re_is_sgr {\x1b\[[0-9;:]*m$} - - foreach ln $linelist { - #set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable - - set ansisplits [punk::ansi::ta::split_codes_single $ln] ;#REVIEW - this split accounts for a large portion of the time taken to run this function. - if {[llength $ansisplits]<= 1} { - #plaintext only - no ansi codes in line - lappend transformed [string cat $replaycodes $ln $RST] - #leave replaycodes as is for next line - set nextreplay $replaycodes - } else { - set tail $RST - set lastcode [lindex $ansisplits end-1] ;#may or may not be SGR - if {[punk::ansi::codetype::is_sgr_reset $lastcode]} { - if {[lindex $ansisplits end] eq ""} { - #last plaintext is empty. So the line is already suffixed with a reset - set tail "" - set nextreplay $RST - } else { - #trailing text has been reset within line - but no tail reset present - #we normalize by putting a tail reset on anyway - set tail $RST - set nextreplay $RST - } - } elseif {[lindex $ansisplits end] ne "" && [punk::ansi::codetype::has_sgr_leadingreset $lastcode]} { - #No tail reset - and no need to examine whole line to determine stack that is in effect - set tail $RST - set nextreplay $lastcode - } else { - #last codeset doesn't reset from earlier codes or isn't SGR - so we have to look at whole line to determine codes in effect - #last codeset doesn't end in a pure-reset - #whether code was at very end or not - add a reset tail - set tail $RST - #determine effective replay for line - set codestack [list start] - foreach {pt code} $ansisplits { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list] ;#different from 'start' marked - this means we've had a reset - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } else { - if {[punk::ansi::codetype::is_sgr $code]} { - #todo - proper test of each code - so we only take latest background/foreground etc. - #requires handling codes with varying numbers of parameters. - #basic simplification - remove straight dupes. - set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars. - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } ;#else gx0 or other code - we don't want to stack it with SGR codes - } - } - if {$codestack eq [list start]} { - #No SGRs - may have been other codes - set line_has_sgr 0 - } else { - #list is either empty or begins with start - empty means it had SGR reset - so it still invalidates current state of replaycodes - set line_has_sgr 1 - if {[lindex $codestack 0] eq "start"} { - set codestack [lrange $codestack 1 end] - } - } - - #set newreplay [join $codestack ""] - set newreplay [punk::ansi::codetype::sgr_merge_list {*}$codestack] - - if {$line_has_sgr && $newreplay ne $replaycodes} { - #adjust if it doesn't already does a reset at start - if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} { - set nextreplay $newreplay - } else { - set nextreplay $RST$newreplay - } - } else { - set nextreplay $replaycodes - } - } - if {[punk::ansi::codetype::has_sgr_leadingreset $ln]} { - #no point attaching any replay - lappend transformed [string cat $ln $tail] - } else { - lappend transformed [string cat $replaycodes $ln $tail] - } - } - set replaycodes $nextreplay - } - set linelist $transformed - } - } - - if {[llength $opt_commandprefix]} { - set transformed [list] - foreach ln $linelist { - lappend transformed [{*}$opt_commandprefix $ln] - } - set linelist $transformed - } - - return $linelist - } - if {$has_punk_ansi} { - #optimise linelist as much as possible - set linelist_body [string map { ""} $linelist_body] - } else { - #punk ansi not avail at time of package load. - #by putting in calls to punk::ansi the user will get appropriate error messages - set linelist_body [string map { "package require punk::ansi"} $linelist_body] - } - proc linelist {args} $linelist_body - - - interp alias {} errortime {} punk::lib::errortime - proc errortime {script groupsize {iters 2}} { - #by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance - set i 0 - set times {} - if {$iters < 2} {set iters 2} - - for {set i 0} {$i < $iters} {incr i} { - set result [uplevel [list time $script $groupsize]] - lappend times [lindex $result 0] - } - - set average 0.0 - set s2 0.0 - - foreach time $times { - set average [expr {$average + double($time)/$iters}] - } - - foreach time $times { - set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}] - } - - set sigma [expr {int(sqrt($s2))}] - set average [expr {int($average)}] - - return "$average +/- $sigma microseconds per iteration" - } - - #test function to use with show_jump_tables - #todo - check if switch compilation to jump tables differs by Tcl version - proc switch_char_test {c} { - set dec [scan $c %c] - foreach t [list 1 2 3] { - switch -- $c { - x { - return [list $dec x $t] - } - y { - return [list $dec y $t] - } - z { - return [list $dec z $t] - } - } - } - - #tcl 8.6/8.7 (at least) - #curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable - switch -- $c { - a { - return [list $dec a] - } - {"} { - return [list $dec dquote] - } - {[} {return [list $dec lb]} - {]} {return [list $dec rb]} - "{" { - return [list $dec lbrace] - } - "}" { - return [list $dec rbrace] - } - default { - return [list $dec $c] - } - } - - - - } - - #we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164) - proc show_jump_tables {args} { - #avoiding use of 'info cmdtype' as unavaliable in safe interps as at 2024-06. - if {[llength $args] == 1} { - set data [tcl::unsupported::disassemble proc [lindex $args 0]] - } elseif {[llength $args] == 2} { - #review - this looks for direct methods on the supplied object/class, and then tries to disassemble method on the supplied class or class of supplied object if it isn't a class itself. - #not sure if this handles more complex hierarchies or mixins etc. - lassign $args obj method - if {![info object isa object $obj]} { - error "show_jump_tables unable to examine '$args'. $obj is not an oo object" - } - #classes are objects too and can have direct methods - if {$method in [info object methods $obj]} { - set data [tcl::unsupported::disassemble objmethod $obj $method] - } else { - if {![info object isa class $obj]} { - set obj [info object class $obj] - } - set data [tcl::unsupported::disassemble method $obj $method] - } - } else { - error "show_jump_tables expected a procname or a class/object and method" - } - set result "" - set in_jt 0 - foreach ln [split $data \n] { - set tln [::tcl::string::trim $ln] - if {!$in_jt} { - if {[::tcl::string::match *jumpTable* $ln]} { - punk::ns::call_frame - append result $ln \n - set in_jt 1 - } - } else { - if {[::tcl::string::match Command* $tln] || [::tcl::string::match "(*) *" $tln]} { - set in_jt 0 - } else { - append result $ln \n - } - } - } - return $result - } - - #a test - # punk::ns::cmdtracereturn punk::lib::disassemble ::punk::ns::test_switch4 - # Note the different disassemble result when trace is running. - proc disassemble {procname} { - tcl::unsupported::disassemble proc $procname - } - - proc temperature_f_to_c {deg_fahrenheit} { - return [expr {($deg_fahrenheit -32) * (5/9.0)}] - } - proc temperature_c_to_f {deg_celsius} { - return [expr {($deg_celsius * (9/5.0)) + 32}] - } - - proc interp_sync_package_paths {interp} { - if {![interp exists $interp]} { - error "interp_sync_package_paths error. interp '$interp' not found. Create it first with \[interp create $interp\]" - } - interp eval $interp [list set ::auto_path $::auto_path] - interp eval $interp {tcl::tm::remove {*}[tcl::tm::list]} - interp eval $interp [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] - } - - proc valcopy {obj} { - append obj2 $obj {} - } - proc set_valcopy {varname obj} { - #used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_valcopy varnmame $val] - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - - - - proc format_number {numbers_or_commaformattednumbers {delim ""} {groupsize ""}} { - variable has_twapi - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - set localeid [twapi::get_system_default_lcid] - } - } - #when using twapi we currently only get the localeid - not the specific defaults - #when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this - set default_delim "," - set default_groupsize 3 - - set results [list] - set nums [valcopy $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list - foreach inputnum $nums { - set number [valcopy $inputnum] - #also handle tcl 8.7+ underscores in numbers - set number [string map [list _ "" , ""] $number] - #normalize e.g 2e4 -> 20000.0 - set number [expr {$number}] - - if {$has_twapi} { - if {$delim eq "" && $groupsize eq ""} { - lappend results [twapi::format_number $number $localeid -idigits -1] - continue - } else { - #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one - #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified? - if {$delim eq ""} {set delim $default_delim} - if {$groupsize eq ""} {set groupsize $default_groupsize} - lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] - continue - } - } - #todo - get configured user defaults - if {$delim eq ""} { - set delim $default_delim - } - if {$groupsize eq ""} { - set groupsize $default_groupsize - } - - lappend results [delimit_number $number $delim $groupsize] - } - - if {[llength $results] == 1} { - #keep intrep as string rather than list - return [lindex $results 0] - } - return $results - } - - - #from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse - # Given a number represented as a string, insert delimiters to break it up for - # readability. Normally, the delimiter will be a comma which will be inserted every - # three digits. However, the delimiter and groupsize are optional arguments, - # permitting use in other locales. - # - # The string is assumed to consist of digits, possibly preceded by spaces, - # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]* - - proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { - set number [valcopy $unformattednumber] - set number [string map {_ ""} $number] - #normalize using expr - e.g 2e4 -> 20000.0 - set number [expr {$number}] - # First, extract right hand part of number, up to and including decimal point - set point [string last "." $number]; - if {$point >= 0} { - set PostDecimal [string range $number $point+1 end]; - set PostDecimalP 1; - } else { - set point [expr {[string length $number] + 1}] - set PostDecimal ""; - set PostDecimalP 0; - } - - # Now extract any leading spaces. review - regex for whitespace instead of just ascii space? - set ind 0; - while {[string equal [string index $number $ind] \u0020]} { - incr ind; - } - set FirstNonSpace $ind; - set LastSpace [expr {$FirstNonSpace - 1}]; - set LeadingSpaces [string range $number 0 $LastSpace]; - - # Now extract the non-fractional part of the number, omitting leading spaces. - set MainNumber [string range $number $FirstNonSpace $point-1]; - - # Insert commas into the non-fractional part. - set Length [string length $MainNumber]; - set Phase [expr {$Length % $GroupSize}] - set PhaseMinusOne [expr {$Phase -1}]; - set DelimitedMain ""; - - #First we deal with the extra stuff. - if {$Phase > 0} { - append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne]; - } - set FirstInGroup $Phase; - set LastInGroup [expr {$FirstInGroup + $GroupSize -1}]; - while {$LastInGroup < $Length} { - if {$FirstInGroup > 0} { - append DelimitedMain $delim; - } - append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup]; - incr FirstInGroup $GroupSize - incr LastInGroup $GroupSize - } - - # Reassemble the number. - if {$PostDecimalP} { - return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal]; - } else { - return [format "%s%s" $LeadingSpaces $DelimitedMain]; - } - } - - #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? - if {$has_twapi} { - interp alias "" ::punk::lib::uuid "" twapi::new_uuid - } else { - catch {package require uuid} - interp alias "" ::punk::lib::uuid "" uuid::uuid generate - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::lib::flatgrid { - namespace export filler_count rows cols col row block - - #WARNING - requires lseq and 'lsearch -stride' - #WARNING - lsearch -stride oddity with empty strings https://core.tcl-lang.org/tcl/tktview/edebb6a4 - #todo - 8.6 fallback? - - proc filler_count {listlen numcolumns} { - #if {$numcolumns <= 0} {error "filler_count requires 1 or more numcolumns"} ;#or allow divide by zero error - #if {$listlen == 0} {return $numcolumns} ;#an option - but returning zero might make more sense - expr {($numcolumns - ($listlen % $numcolumns)) % $numcolumns} - } - proc rows {list numcolumns {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set rows [list] - set i 1 - foreach s [lrange $splits 0 end-1] { - lappend rows [lrange $padded_list $s [lindex $splits $i]-1] - incr i - } - return $rows - } - proc cols {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] *] - } - return $cols - } - proc cols2 {list numcolumns {blank NULL}} { - set cols [list] - foreach colindex [lseq 0 $numcolumns-1] { - lappend cols [col2 $list $numcolumns $colindex $blank] - } - return $cols - } - proc col {list numcolumns colindex {blank NULL}} { - lsearch -stride $numcolumns -index [list $colindex 0] -subindices -all -inline [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] * - } - proc col2 {list numcolumns colindex {blank NULL}} { - set numblanks [filler_count [llength $list] $numcolumns] - set padded_list [list {*}$list {*}[lrepeat $numblanks $blank]] - set splits [lseq 0 to [llength $padded_list] by $numcolumns] - set col [list] - foreach s [lrange $splits 0 end-1] { - lappend col [lindex $padded_list $s+$colindex] - } - return $col - } - proc col3 {list numcolumns colindex {blank NULL}} { - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap s [lrange [lseq 0 to [llength $padded_list] by $numcolumns] 0 end-1] {lindex $padded_list $s+$colindex} - } - proc col4 {list numcolumns colindex {blank NULL}} { - #slow - set vars [lrepeat $numcolumns _] - lset vars $colindex v - if {$blank eq ""} { - return [lmap $vars $list {set v}] - } - set padded_list [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] - lmap $vars [list {*}$list {*}[lrepeat [filler_count [llength $list] $numcolumns] $blank]] {set v} - } - - proc block {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } - proc block2 {list numcolumns {blank NULL}} { - set colblocks [list] - foreach c [cols2 $list $numcolumns $blank] { - lappend colblocks [join $c \n] " " - } - textblock::join -- {*}$colblocks - } -} - -tcl::namespace::eval punk::lib::test { - - - -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#todo - way to generate 'internal' docs separately? -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::lib::system { - #*** !doctools - #[subsection {Namespace punk::lib::system}] - #[para] Internal functions that are not part of the API - #[list_begin definitions] - - - proc mostFactorsBelow {n} { - ##*** !doctools - #[call [fun mostFactorsBelow] [arg n]] - #[para]Find the number below $n which has the greatest number of factors - #[para]This will get slow quickly as n increases (100K = 1s+ 2024) - set most 0 - set mostcount 0 - for {set i 1} {$i < $n} {incr i} { - set fc [llength [punk::lib::factors $i]] - if {$fc > $mostcount} { - set most $i - set mostcount $fc - } - } - return [list number $most numfactors $mostcount] - } - proc factorCountBelow_punk {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [punk::lib::factors $i]] - } - return $tally - } - proc factorCountBelow_numtheory {n} { - ##*** !doctools - #[call [fun factorCountBelow] [arg n]] - #[para]For numbers 1 to n - keep a tally of the total count of factors - #[para]This is not useful other than a quick and dirty check that different algorithms return *probably* the same result - #[para]and as a rudimentary performance comparison - #[para]gets slow quickly! (significantly slower than factorCountBelow_punk) - package require math::numtheory - set tally 0 - for {set i 1} {$i <= $n} {incr i} { - incr tally [llength [math::numtheory::factors $i]] - } - return $tally - } - - proc factors2 {x} { - ##*** !doctools - #[call [fun factors2] [arg x]] - #[para]Return a sorted list of factors of x - #[para]A similar brute-force mechanism to factors - but keeps result ordering as we go. - set smallfactors [list 1] - set j 2 - set max [expr {sqrt($x)}] - while {$j < $max} { - if {($x % $j) == 0} { - lappend smallfactors $j - lappend largefactors [expr {$x / $j}] - } - incr j - } - #handle sqrt outside loop so we don't have to sort/dedup or check list membership in main loop - if {($x % $j) == 0} { - if {$j == ($x / $j)} { - lappend smallfactors $j - } - } - return [concat $smallfactors [lreverse $largefactors] $x] - } - - - - # incomplete - report which is the innermost bracket/quote etc awaiting completion for a Tcl command - #important - used by punk::repl - proc incomplete {partial} { - #we can apparently get away without concatenating current innerpartial to previous in list - REVIEW. - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - #puts stderr "-->$clist<--" - set waiting [list ""] - set innerpartials [list ""] - set escaped 0 - set i 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - incr i - continue - } ;# set escaped 0 at end - set p [lindex $innerpartials end] - if {$escaped == 0} { - #NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least) - switch -- $c { - {"} { - if {![info complete ${p}]} { - lappend waiting {"} - lappend innerpartials "" - } else { - if {[lindex $waiting end] eq {"}} { - #this quote is endquote - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - if {![info complete ${p}$c]} { - lappend waiting {"} - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } - {[} { - if {![info complete ${p}$c]} { - lappend waiting "\]" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "{" { - if {![info complete ${p}$c]} { - lappend waiting "\}" - lappend innerpartials "" - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - "}" - - default { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - set innerpartials [lrange $innerpartials 0 end-1] - } else { - set p ${p}${c} - lset innerpartials end $p - } - } - } - } else { - set p ${p}${c} - lset innerpartials end $p - } - set escaped 0 - incr i - } - set incomplete [list] - foreach w $waiting { - #to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm. - switch -- $w { - {"} { - lappend incomplete $w - } - {]} { - lappend incomplete "\[" - } - "{" {} - "}" { - lappend incomplete "\{" - } - } - } - set debug 0 - if {$debug} { - foreach w $waiting p $innerpartials { - puts stderr "->awaiting:'$w' partial: $p" - } - } - return $incomplete - } - #This only works for very simple cases will get confused with for example: - # {set x "a["""} - proc incomplete_naive {partial} { - if {[info complete $partial]} { - return [list] - } - set clist [split $partial ""] - set waiting [list] - set escaped 0 - foreach c $clist { - if {$c eq "\\"} { - set escaped [expr {!$escaped}] - continue - } - if {!$escaped} { - if {$c eq {"}} { - if {[lindex $waiting end] eq {"}} { - set waiting [lrange $waiting 0 end-1] - } else { - lappend waiting {"} - } - } elseif {$c eq "\["} { - lappend waiting "\]" - } elseif {$c eq "\{"} { - lappend waiting "\}" - } else { - set waitingfor [lindex $waiting end] - if {$c eq "$waitingfor"} { - set waiting [lrange $waiting 0 end-1] - } - } - } - } - set incomplete [list] - foreach w $waiting { - if {$w eq {"}} { - lappend incomplete $w - } elseif {$w eq "\]"} { - lappend incomplete "\[" - } elseif {$w eq "\}"} { - lappend incomplete "\{" - } - } - return $incomplete - } - - #get info about punk nestindex key ie type: list,dict,undetermined - # pdict devel - proc nestindex_info {args} { - set argd [punk::args::parse $args withdef { - -parent -default "" - nestindex - }] - set opt_parent [dict get $argd opts -parent] - if {$opt_parent eq ""} { - set parent_type undetermined - } else { - set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing - } - - #??? - - } - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::lib::system ---}] -} - -tcl::namespace::eval punk::lib::caches { - -} - -tcl::namespace::eval punk::lib::debug { - proc showdict {args} {} -} - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::lib -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::lib [tcl::namespace::eval punk::lib { - variable pkg punk::lib - variable version - set version 0.1.5 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.6.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.6.tm index aee51015..ada0f900 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.6.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.6.tm @@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check { if {"windows" ne $::tcl_platform(platform)} { set bug 0 } else { - set tmpdir [file tempdir] ;#tcl 9+ - set testfile [file join $tmpdir "bugtest"] + if {![catch {file tempdir} tmpdir]} { + #tcl 9+ has 'file tempdir' + set testfile [file join $tmpdir "bugtest"] + } else { + #fallback for older tcl versions - use env TEMP/TMP or current directory + set tmpdir "" + foreach e {TEMP TMP} { + if {[info exists ::env($e)] && [file isdirectory ::env($e)]} { + set tmpdir ::env($e) + break + } + } + if {$tmpdir eq ""} { + #no env vars - fallback to current directory + set tmpdir [pwd] + } + set testfile [file join $tmpdir "bugtest"] + } + set fd [open $testfile w] puts $fd test close $fd set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*] + if {[file exists $testfile]} { + file delete $testfile + } foreach r $globresult { if {$r ne "bugtest"} { set bug 1 @@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat { #*** !doctools #[call [fun lpop] [arg listvar] [opt {index}]] #[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop - upvar $lvar l + #upvar $lvar l + upvar 1 $lvar l if {![llength $args]} { set args [list end] } @@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat { #set newlist [lremove $newlist $tailidx] #set newlist [lreplace $newlist $tailidx $tailidx] set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx] - #don't use ledit here! + #we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin. } else { set sublist [lindex $newlist {*}$sublist_path] #set sublist [lremove $sublist $tailidx] @@ -459,26 +480,46 @@ tcl::namespace::eval punk::lib::compat { } } set lidx [punk::lib::lindex_resolve [llength $l] $last] - switch -exact -- $lidx { - -Inf { - #index below lower bound - set post [lrange $l 0 end] - } - Inf { - #index above upper bound - set post [list] - } - default { - if {$lidx < $fidx} { - #from ledit man page: - #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. - set post [lrange $l $fidx end] - } else { - #set post [lrange $l $last+1 end] + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + #set post [lrange $l $last+1 end] + switch -exact -- $lidx { + -Inf { + #index below lower bound + set post [lrange $l 0 end] + } + Inf { + #index above upper bound + set post [list] + } + default { set post [lrange $l $lidx+1 end] } } } + #switch -exact -- $lidx { + # -Inf { + # #index below lower bound + # set post [lrange $l 0 end] + # } + # Inf { + # #index above upper bound + # set post [list] + # } + # default { + # if {$lidx < $fidx} { + # #from ledit man page: + # #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + # set post [lrange $l $fidx end] + # } else { + # #set post [lrange $l $last+1 end] + # set post [lrange $l $lidx+1 end] + # } + # } + #} set l [list {*}$pre {*}$args {*}$post] } @@ -1970,6 +2011,7 @@ namespace eval punk::lib { #for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution. #--------------------------------- proc tclscript_info {script {nscontext ""}} { + package require parser #if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing. if {[punk::ansi::ta::detect $script]} { #we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts. @@ -2481,7 +2523,7 @@ namespace eval punk::lib { #no parse tree - This is likely for an empty argument with expansion e.g {*}{ } #This construct occurs when using {*} in place of line continuation for long lists or dicts, e.g #dict create {*}{ - # } key1 $dynamic {*} { + # } key1 $dynamic {*}{ # key2 value2 #} #review - the 'empty' argument will still have an entry in cmdlineranges - as although 'empty' in terms of how it expands it may be whitespace across multiple lines. @@ -3373,16 +3415,51 @@ namespace eval punk::lib { return [list $stdout $stderr 0] } - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " + namespace eval argdoc { + variable PUNKARGS + upvar ::punk::lib::has_punk_ansi has_punk_ansi + + #if {!$has_punk_ansi} { + # set RST "" + # set sep " = " + # set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + #} else { + # set RST [punk::ansi::a] + # #set sep " [a+ Web-seagreen]=[a] " + # set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support + # #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + # #NOTE that \u2260 not suitable for non utf-8 terminals. + # set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " + #} + #todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8. + # (safe detection methods for utf-8 support?) + #if colour is disabled we want to refresh this. + #therefore we use @dynamic + proc get_sep {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep " = " + } else { + #set sep " [a+ Web-seagreen]=[a] " + set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support + } + return $sep + } + proc get_sep_mismatch {} { + upvar ::punk::lib::has_punk_ansi has_punk_ansi + if {!$has_punk_ansi} { + set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) + } else { + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #NOTE that \u2260 not suitable for non utf-8 terminals. + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] " + } + return $sep_mismatch } - set argspec [string map [list %sep% $sep] { + set DYN_SEP {${[get_sep]}} + set DYN_SEP_MISMATCH {${[get_sep_mismatch]}} + lappend PUNKARGS [list { + @dynamic @id -id ::punk::lib::pdict @cmd -name pdict -help\ "Print dict keys,values to channel @@ -3392,7 +3469,7 @@ namespace eval punk::lib { @opts -any 1 #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" + -separator -default "${$DYN_SEP}" -roottype -default "dict" -substructure -default {} -channel -default stdout -help\ @@ -3403,33 +3480,36 @@ namespace eval punk::lib { dictvar -type string -help "name of variable. Can be a dict, list or array" patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - (todo - change to indexset syntax @1..3 @1..end-1 etc) - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. + Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) + The system uses similar patterns to the punk pipeline pattern-matching system. + The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. + Segments are classified into list,dict and string operations. + Leading % indicates a string operation - e.g %# gives string length + A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 + (todo - change to indexset syntax @1..3 @1..end-1 etc) + A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' + The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. + e.g1 pdict env */%# + the pattern starts with default type dict, so * retrieves all keys & values, + the next hierarchy switches to a string operation to get the length of each value. + e.g2 pdict env W* S* + Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns + e.g3 pdict punk_testd */* + This displays 2 levels of the dict hierarchy. + Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) + - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. + e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 + Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent + The second level segment in each pattern switches to a dict operation to retrieve the value by key. + When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. } }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] + } + proc pdict {args} { + package require punk::args + variable has_punk_ansi + set argd [punk::args::parse $args withid ::punk::lib::pdict] set opts [dict get $argd opts] set dvar [dict get $argd values dictvar] set patterns [dict get $argd values patterns] @@ -3447,32 +3527,11 @@ namespace eval punk::lib { showdict {*}$opts $dvalue {*}$patterns } - #TODO - much. - #showdict needs to be able to show different branches which share a root path - #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) - # - specify ansi colour per pattern so different branches can be highlighted? - # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc - # - The current version is incomplete but passably usable. - # - Copy proc and attempt rework so we can get back to this as a baseline for functionality - proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - #set sep " [a+ Web-seagreen]=[a] " - variable has_punk_ansi - if {!$has_punk_ansi} { - set RST "" - set sep " = " - #set sep_mismatch " mismatch " - set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) - } else { - set RST [punk::ansi::a] - set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " - } - package require punk::pipe - #package require punk ;#we need pipeline pattern matching features - package require textblock + namespace eval argdoc { + variable PUNKARGS - set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { + lappend PUNKARGS [list { + @dynamic @id -id ::punk::lib::showdict @cmd -name punk::lib::showdict -help "display dictionary keys and values" #todo - table tableobject @@ -3482,10 +3541,8 @@ namespace eval punk::lib { "Trim whitespace off rhs of each line. This can help prevent a single long line that wraps in terminal from making every line wrap due to long rhs padding." - -separator -default {%sep%} -help\ - "Separator column between keys and values" - -separator_mismatch -default {%sep_mismatch%} -help\ - "Separator to use when patterns mismatch" + -separator -default "${$DYN_SEP}" -help "Separator column between keys and values" + -separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch" -roottype -default "dict" -help\ "list,dict,string" -ansibase_keys -default "" -help\ @@ -3504,7 +3561,23 @@ namespace eval punk::lib { "dict or list value" patterns -default "*" -type string -multiple 1 -help\ "key or key glob pattern" - }]] + }] + } + #TODO - much. + #showdict needs to be able to show different branches which share a root path + #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) + # - specify ansi colour per pattern so different branches can be highlighted? + # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc + # - The current version is incomplete but passably usable. + # - Copy proc and attempt rework so we can get back to this as a baseline for functionality + proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) + package require punk::pipe + #package require punk ;#we need pipeline pattern matching features + package require textblock + + set RST [punk::ansi::a] + + set argd [punk::args::parse $args withid ::punk::lib::showdict] #for punk::lib - we want to reduce pkg dependencies. # - so we won't even use the tcllib debug pkg here @@ -4859,6 +4932,23 @@ namespace eval punk::lib { the range will come out the same, so the result needs to be treated as a 1-based set of indices when performing further operations. " + -return -type string -default indices -choices {indices pairs} -choicecolumns 1 -choicelabels { + indices + " return a list of all indices in the order specified by the indexset, + with duplicates if specified by the indexset. + So for example + indexset_resolve 6 3..0,2,4,end + would return 3 2 1 0 2 4 5" + pairs + " return a list of index pairs representing the start and end of each range, + which may be increasing or decreasing, or just a single index + (where start and end are the same). + So for example + indexset_resolve -return pairs 6 3..0,2,4,end + would return {3 0} {2 2} {4 5} + indexset_resolve -return pairs 7 3..0,2,4,end + would return {3 0} {2 2} {4 4} {6 6}" + } @values -min 2 -max 3 numitems -type integer indexset -type indexset -help "comma delimited specification for indices to return" @@ -4872,32 +4962,66 @@ namespace eval punk::lib { # for the unhappy path - the punk::args::parse is fine to generate the usage/error information. # -------------------------------------------------- if {[llength $args] < 2} { + #too few args - use parser to generate error message punk::args::resolve $args withid ::punk::lib::indexset_resolve } - set indexset [lindex $args end] set numitems [lindex $args end-1] + set indexset [lindex $args end] if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { #use parser on unhappy path only set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] } #assert we have 2 or more args + set optlist [lrange $args 0 end-2] + if {[llength $optlist] % 2 != 0} { + #options should come in pairs + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype "indices" ;#default set base 0 ;#default - if {[llength $args] > 2} { - #if more than just numitems and indexset - we expect only -base ie 4 args in total - if {[llength $args] != 4} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set optname [lindex $args 0] - set optval [lindex $args 1] - set fulloptname [tcl::prefix::match -error "" -base $optname] - if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + dict for {opt val} $optlist { + set fulloptname [tcl::prefix::match -error "" {-base -return} $opt] + switch -exact -- $fulloptname { + -return { + set fullval [tcl::prefix::match -error "" {indices pairs} $val] + if {$fullval ni {indices pairs}} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype $fullval + } + -base { + if {![string is integer -strict $val]} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set base $val + } + default { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } } - set base $optval } + + #set base 0 ;#default + #if {[llength $args] > 2} { + # #if more than just numitems and indexset - we expect only -base ie 4 args in total + # if {[llength $args] != 4} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set optname [lindex $args 0] + # set optval [lindex $args 1] + # set fulloptname [tcl::prefix::match -error "" -base $optname] + # if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set base $optval + #} # -------------------------------------------------- @@ -5084,8 +5208,84 @@ namespace eval punk::lib { } } } + if {$returntype eq "pairs"} { + return [indices_to_pairs $index_list] + } return $index_list } + proc indices_to_pairs {indices} { + #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing. + #if the direction of the run changes - we end the previous run and start a new one + if {[llength $indices] == 0} { + return [list] + } + set pairs [list] + set start [lindex $indices 0] + set prev $start + set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing + for {set i 1} {$i < [llength $indices]} {incr i} { + set idx [lindex $indices $i] + if {![string is integer -strict $idx]} { + error "non-integer index '$idx' in indices list" + } + if {$idx == $prev + 1} { + #increase since prev + if {$direction == 0} { + set direction 1 + } elseif {$direction == -1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } else { + #still increasing + } + set prev $idx + } elseif {$idx == $prev - 1} { + #decrease since prev + if {$direction == 0} { + set direction -1 + } elseif {$direction == 1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } + set prev $idx + } else { + #run ended - add pair to list + lappend pairs [list $start $prev] + set start $idx + set prev $idx + set direction 0 + } + } + # add final run + lappend pairs [list $start $prev] + return $pairs + } + + #proc indices_to_pairs {indices} { + # #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices + # set pairs [list] + # set start [lindex $indices 0] + # set prev $start + # for {set i 1} {$i < [llength $indices]} {} { + # set idx [lindex $indices $i] + # if {$idx == $prev + 1} { + # #still in a run + # set prev $idx + # } else { + # #run ended - add pair to list + # lappend pairs [list $start $prev] + # set start $idx + # set prev $idx + # } + # } + # # add final run + # lappend pairs [list $start $prev] + # return $pairs + #} # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side #This doesn't need the list itself - just the length suffices. punk::args::define { @@ -7759,6 +7959,77 @@ namespace eval punk::lib { } } + #sugar + #exclusive end - more intuitive for some cases and more consistent with other languages + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::lib::FOR + @cmd -name punk::lib::FOR\ + -summary\ + "BASIC style integer 'for loop'"\ + -help\ + "Sugar syntax for a common looping pattern. + The loop variable takes on values from start to end (exclusive) in increments of step. + If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end. + This is a common looping pattern that isn't directly supported by Tcl's built in control structures, + and this syntax is more concise for convenient interactive usage. For example: + + FOR i 0 10 {puts $i} + + will print the numbers 0 to 9. + + This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops, + so may not be suitable for performance critical inner loops. + + See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop + " + @values -min 3 -max 4 + varname -type string -help "loop variable name" + start -type integer -help "initial value for loop variable" + end -type integer -help "end value for loop variable (exclusive)" + step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)" + script -type script -help "script to execute for each loop iteration" + }] + } + proc FOR { var args } { + switch -- [llength $args] { + 3 { + # FOR x start end {} + lassign $args start end script + set step [ expr {$start > $end ? - 1 : 1} ] + } + 4 { + # FOR x start end step {} + lassign $args start end step script + } + default { + error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script" + } + } + if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} { + error "FOR: start,end and step values must be integers" + } + upvar $var loopVar + set loopVar [expr {$start - $step}] + #to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition + if {$start < $end} { + if {$step <= 0} { + error "FOR: step value must be positive when start < end" + } + while {[incr loopVar $step] < $end} { + uplevel $script + } + } else { + if {$step >= 0} { + error "FOR: step value must be negative when start > end" + } + while {[incr loopVar $step] > $end} { + uplevel $script + } + } + } + #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 diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.3.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.3.1.tm index 05e8724f..793736b8 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.3.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.3.1.tm @@ -333,11 +333,11 @@ namespace eval punk::mix::cli { set defaults [list {*}{ -errorprefix projectname }] - if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"} + if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"} set known_opts [dict keys $defaults] foreach k [dict keys $args] { if {$k ni $known_opts} { - error "validate_modulename error: unknown option $k. known options: $known_opts" + error "validate_projectname error: unknown option $k. known options: $known_opts" } } set opts [dict merge $defaults $args] @@ -381,34 +381,6 @@ namespace eval punk::mix::cli { return $name } - #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path - #ignore trailing .tm .TM if present - #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error - #Up to caller to validate. - proc split_modulename_version {fullmodulename} { - set lastpart [namespace tail $fullmodulename] - set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components - if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { - set fileparts [split [file rootname $lastpart] -] - } else { - set fileparts [split $lastpart -] - } - if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} { - set versionsegment [lindex $fileparts end] - set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch - } else { - # - set namesegment [join $fileparts -] - set versionsegment "" - } - set base [namespace qualifiers $fullmodulename] - if {$base ne ""} { - set modulename "${base}::$namesegment" - } else { - set modulename $namesegment - } - return [list $modulename $versionsegment] - } proc get_status {{workingdir ""} args} { set result "" @@ -566,6 +538,7 @@ namespace eval punk::mix::cli { #lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list # -- --- set installer [punkcheck::installtrack new $installername $punkcheck_file] + #set installer [punkcheck::installtrack new $installername $punkcheck_file stderr] ;#with debugchannel $installer set_source_target $srcdir $basedir set event [$installer start_event $config] # -- --- @@ -601,9 +574,9 @@ namespace eval punk::mix::cli { } #debug for issues on non-windows platforms. - if {$::tcl_platform(platform) ne "windows"} { - set is_interesting 1 - } + #if {$::tcl_platform(platform) ne "windows"} { + # set is_interesting 1 + #} if {$is_interesting} { puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath" @@ -662,6 +635,7 @@ namespace eval punk::mix::cli { # -max_depth -1 for no limit set build_installername pods_in_$current_source_dir set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck] + #set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel $build_installer set_source_target $current_source_dir/$modpath $buildfolder set build_event [$build_installer start_event $config] # -- --- diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/module-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/module-0.1.0.tm index 3b40d838..4c75b10e 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/module-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/module-0.1.0.tm @@ -9,7 +9,7 @@ # @@ Meta Begin # Application punk::mix::commandset::module 0.1.0 # Meta platform tcl -# Meta license BSD +# Meta license BSD # @@ Meta End @@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_version_supplied [dict get $opts -version] set opt_version $opt_version_supplied - if {![util::is_valid_tm_version $opt_version]} { + if {![punk::mix::util::is_valid_tm_version $opt_version]} { error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version" } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module { set mversion_supplied "" ;#version supplied directly in module argument if {[string first - $module]> 0} { #if it has a dash then version is required to be valid - lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion - if {![util::is_valid_tm_version $mversion]} { + lassign [punk::mix::util::split_modulename_version $module] modulename mversion + if {![punk::mix::util::is_valid_tm_version $mversion]} { error "deck module.new error - unable to determine modulename-version from supplied value '$module'" } set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name @@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module { } else { set module [string range [string length $vendor.] end] } - lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion + lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion lappend key_version_list [list $m $mversion] } if {[llength $matches]} { @@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module { } else { error "module.new error: Unable to interpret filename components of template file '$templatefile'" } - lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version + lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version #t_version may be empty string if template is unversioned e.g template_whatever.tm set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd @@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module { } else { # - if {[util::is_valid_tm_version $t_version]} { + if {[punk::mix::util::is_valid_tm_version $t_version]} { if {$mversion_supplied eq ""} { set build_version $t_version } else { @@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module { set name_version_pairs [list] lappend name_version_pairs [list $moduletail $infile_version] foreach existing $existing_versions { - lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored + lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored if {[string match #modpod-* $namepart]} { set namepart [string range $namepart 8 end] } diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.1.3.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.1.3.tm index 726c2ba173e2a97ac09212c3bde4c29fb651d942..f4044b053ddb634c8023fecc4f7d057bb1dd4809 100644 GIT binary patch delta 28 icmZqN&eFJ@WrL^`^J9)Zn?eFrhN?-XP_!3hAR$_se_ delta 28 icmZqN&eFJ@WrL^`^V+?OH%my(`wn7k-zms=f)fC*h73gj diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/util-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/util-0.1.0.tm index 9de4a33d..7f55005b 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/util-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/util-0.1.0.tm @@ -330,6 +330,35 @@ namespace eval punk::mix::util { return 0 } } + + #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path + #ignore trailing .tm .TM if present + #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error + #Up to caller to validate. + proc split_modulename_version {fullmodulename} { + set lastpart [namespace tail $fullmodulename] + set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components + if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { + set fileparts [split [file rootname $lastpart] -] + } else { + set fileparts [split $lastpart -] + } + if {[is_valid_tm_version [lindex $fileparts end]]} { + set versionsegment [lindex $fileparts end] + set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch + } else { + set namesegment [join $fileparts -] + set versionsegment "" + } + set base [namespace qualifiers $fullmodulename] + if {$base ne ""} { + set modulename "${base}::$namesegment" + } else { + set modulename $namesegment + } + return [list $modulename $versionsegment] + } + #Note that semver only has a small overlap with tcl tm versions. #todo - work out what overlap and whether it's even useful #see also TIP #439: Semantic Versioning (tcl 9??) @@ -337,6 +366,7 @@ namespace eval punk::mix::util { set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$} } #todo - semver conversion/validation for other systems? + proc magic_tm_version {} { set magicbase 999999 ;#deliberately large so given load-preference when testing! #we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version diff --git a/src/vfs/_vfscommon.vfs/modules/punk/nav/fs-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/nav/fs-0.1.0.tm index 5635cd09..7d85e311 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/nav/fs-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/nav/fs-0.1.0.tm @@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) if {$linktarget_file_type eq "directory"} { - set linktarget [file readlink $cdtarget] - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { @@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs { link { file stat $cdtarget cdtargetinfo set linktarget_file_type $cdtargetinfo(type) - set linktarget [file readlink $cdtarget] if {$linktarget_file_type eq "directory"} { - cd $linktarget - #set VIRTUAL_CWD $cdtarget - tailcall punk::nav::fs::d/ $v + if {[catch {file readlink $cdtarget} linktarget]} { + #if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does. + #review - exact type of link? + #we can probably still cd to it - but the path will appear to be within the parent directory even though + #the actual target may be elsewhere on the filesystem. + #This may be the intention of such links anyway. + cd $cdtarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } else { + set linktarget [file readlink $cdtarget] + cd $linktarget + #set VIRTUAL_CWD $cdtarget + tailcall punk::nav::fs::d/ $v + } } } directory { diff --git a/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm deleted file mode 100644 index b19355e0..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm +++ /dev/null @@ -1,3114 +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.3.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) 2025 -# -# @@ Meta Begin -# Application punk::netbox 0.1.0 -# Meta platform tcl -# Meta license MIT -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::netbox 0 0.1.0] -#[copyright "2025"] -#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] -#[moddesc {-}] [comment {-- Description at end of page heading --}] -#[require punk::netbox] -#[keywords module] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::netbox -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::netbox -#[list_begin itemized] - -package require Tcl 8.6- -package require http -package require rest -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package {http}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# oo::class namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#tcl::namespace::eval punk::netbox::class { - #*** !doctools - #[subsection {Namespace punk::netbox::class}] - #[para] class definitions - #if {[tcl::info::commands [tcl::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 ---}] - #} -#} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::netbox {} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::netbox::system { - #*** !doctools - #[subsection {Namespace punk::netbox::system}] - #[para] Internal functions that are not part of the API - - tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - - punk::args::define { - @id -id ::punk::netbox::system::make_rest_func - @leaders -min 2 -max 2 - commandname -help\ - "Fully qualified commandname. - There must be an existing punk::args definition with @id - directive -id matching the name" - endpoint -help\ - "The subpath to be appended to the base url. - e.g api/ipam/ip-addresses/ - api/ipam/ip-addresses/{id}/" - -verb -default get -choices {get post patch head put delete} - -body -default optional -choicecolumns 2 -choices {none optional required mime_multipart}\ - -choicelabels { - none\ - " The call has no request body, - none must be supplied." - optional\ - " A request body can be supplied, - but is not required" - required\ - " A request body must be supplied." - mime_multipart\ - " A request body must be supplied - and will be interpreted as each - argument representing one part of - a mime/multipart document. Arguments - must be lists containing 2 elements, - a list of header keys and values, - and the mime part body, in this order." - } - } - - #A somewhat sanitized config for outputting to stderr etc - #Obscure at least full Authorization Token - #todo - other headers? - proc obscured_config {cfg} { - set sanconfig $cfg - if {[dict exists $cfg headers]} { - set hdrs [dict get $cfg headers] - if {[dict exists $hdrs Authorization]} { - set auth [dict get $hdrs Authorization] - if {[dict exists $auth Token]} { - dict set auth Token "[string range [dict get $auth Token] 0 5]..." - dict set hdrs Authorization $auth - dict set sanconfig headers $hdrs - } - } - } - return $sanconfig - } - variable call_data - array unset call_data - - proc make_rest_func {args} { - set argd [punk::args::parse $args withid ::punk::netbox::system::make_rest_func] - lassign [dict values $argd] leaders opts values received - - set commandname [dict get $leaders commandname] - set endpoint [dict get $leaders endpoint] - set verb [dict get $opts -verb] - set body [dict get $opts -body] - - set custom [dict create\ - %commandname% $commandname\ - %endpoint% $endpoint\ - %verb% $verb\ - %body% $body\ - %showpagedict% {!@@results @@results/@*/@*.@*}\ - %showpagedict2% {@@results/@*/@*.@* !@@results}\ - %showdict% {*}\ - %showdict2% {*/*}\ - %showlistofdicts% {@*/@*.@*}\ - ] - if {$commandname eq "::punk::netbox::status"} { - #we get duplicate django-version for %showdict% - todo - something. - dict set custom %showdict% {@@django-version @@installed-apps/@*.@* !@@installed-apps} - dict set custom %showdict2% {@@installed-apps/@*.@* !@@installed-apps} - } - - set procbody [string map $custom { - set argd [punk::args::parse $args withid %commandname%] - lassign [dict values $argd] leaders opts values received solos multis - set apicontextid [dict get $leaders apicontextid] - if {[dict exists $received -RETURN]} { - set returntype [dict get $opts -RETURN] - } else { - if {[dict exists $opts -RETURN]} { - #not received - but has default - set returntype [dict get $opts -RETURN] - } else { - #fallback if -RETURN was defined without a default or was omitted - set returntype dict - } - } - - set FORCE 1 - if {[dict exists $opts -FORCE]} { - set FORCE [dict get $opts -FORCE] - } - - set query [list] ;#use list not dict - allows repeated params - dict for {k val} $opts { - switch -- $k { - -CUSTOM_PARAM { - #'-multiple true' - foreach kv $val { - lassign $kv paramname value - lappend query $paramname $value - } - } - -RETURN { - #ignore - already handled - } - default { - if {[string match *_FILTER $k]} { - #all _FILTER methods are '-multiple true' - so are present in $opts as a single key with a value that is a list - #e.g -X_Y_FILTER "blah" - set parts [split $k _] - set name1 [string range [string tolower [lindex $parts 0]] 1 end] ;#strip leading dash off first part - set nametail [string tolower [lrange $parts 1 end-1]] - set paramname [join [list $name1 {*}$nametail] _] - foreach fv $val { - lassign $fv filter value ;#filter is n,gte,lte etc - lappend query ${paramname}__$filter $value - } - } else { - set paramname [string range $k 1 end] - if {$paramname in $multis} { - foreach v $val { - lappend query $paramname $v - } - } else { - lappend query $paramname $val - } - } - } - } - - } - set body %body% - switch -- $body { - required { - set requestbody [dict get $values body] - } - optional { - if {[dict exists $received body]} { - set requestbody [dict get $values body] - } else { - set requestbody "" - } - } - } - upvar ::punk::netbox::contexts contexts - if {![dict exists $contexts $apicontextid]} { - error "specified contextid '$apicontextid' not found" - } - set config [dict create\ - format json\ - result json\ - ] - #rest api documentation is unclear on 'result' field - #note our default: result json - #this actually converts the json to a dict - - - dict set config headers [list Authorization [list Token [dict get $contexts $apicontextid token value]]] - if {$returntype in "json jsondump"} { - #if we set result json - we get a dict instead of json :/ - #The 'result' key seems to tell ::rest what post-processor to use to translate it *from* that to a tcl dict (or in xml case a nested list) - #sort of.. - #xml - takes xml produces a list - #json - takes json produces a dict - #discard - 'return -code ok' - #auto - *basic* detect xml or json and do as above - #tdom - takes xml - produces a tdom documentElement - #raw - just returns the data - dict set config result raw - } - if {$body in {required optional}} { - #content type for the request data - dict set config headers content-type "application/json" - } - - set url [dict get $contexts $apicontextid url value] - set endpoint "%endpoint%" - if {[string first {{id}} $endpoint] != -1} { - set id [dict get $values id] - set endpoint [string map [list {{id}} $id] $endpoint] - } - - #??? - dict set config error-body false - - #todo - only show if debug (and obscure Authorization Token) - set sanconfig [punk::netbox::system::obscured_config $config] - puts stderr "url:${url}$endpoint query:'$query' verb:%verb% config:'$sanconfig'" - if {$FORCE} { - #FORCE is true for most operations (and no -FORCE option even available) but the option exists and defaults to false for specifically unsafe - #e.g delete operations on entire endpoints - if {$body in {required optional}} { - set result [::rest::%verb% ${url}$endpoint $query $config $requestbody] - } else { - set result [::rest::%verb% ${url}$endpoint $query $config] - } - #puts "-----------" - #puts $result - #puts "-----------" - - } else { - puts stderr "%commandname% not called because -FORCE is false" - set sanconfig [punk::netbox::system::obscured_config $config] - puts "url:${url}$endpoint query:'$query' verb:%verb% config:'$sanconfig'" - return - } - switch -exact -- $returntype { - showpagedict { - #return [punk::lib::showdict $result !@@results @@results/@*/@*.@*] - return [punk::lib::showdict $result %showpagedict%] - } - showpagedict2 { - #return [punk::lib::showdict $result @@results/@*/@*.@* !@@results] - return [punk::lib::showdict $result %showpagedict2%] - } - showdict { - return [punk::lib::showdict $result %showdict%] - } - showdict2 { - return [punk::lib::showdict $result %showdict2%] - } - showlist { - return [punk::lib::showdict -roottype list $result] - } - showlistofdicts { - return [punk::lib::showdict $result %showlistofdicts%] - } - jsondump { - package require huddle::json - #pretty-print via huddle (inefficient review) - set h [huddle::json::json2huddle parse $result] - return [huddle::jsondump $h] - } - linelist { - set ret "" - foreach r $result { - append ret $r \n - } - return $ret - } - default { - #plain result: (list or dict) or json - the counterintuitive 'result' field set to raw above sets the rest resulting format to the original json - return $result - } - } - }] - proc $commandname {args} $procbody - } - #experiment - variable callid - set callid 0 - proc make_rest_func_async {args} { - set argd [punk::args::parse $args withid ::punk::netbox::system::make_rest_func] - lassign [dict values $argd] leaders opts values received - - set commandname [dict get $leaders commandname] - set endpoint [dict get $leaders endpoint] - set verb [dict get $opts -verb] - set bodycontrol [dict get $opts -body] - - set custom [dict create\ - %commandname% $commandname\ - %endpoint% $endpoint\ - %verb% $verb\ - %bodycontrol% $bodycontrol\ - %showpagedict% {!@@results @@results/@*/@*.@*}\ - %showpagedict2% {@@results/@*/@*.@* !@@results}\ - %showdict% {*}\ - %showdict2% {*/*}\ - %showlistofdicts% {@*/@*.@*}\ - ] - - - set procbody [string map $custom { - upvar ::punk::netbox::system::callid callid - incr callid - - set argd [punk::args::parse $args withid %commandname%] - lassign [dict values $argd] leaders opts values received solos multis - set apicontextid [dict get $leaders apicontextid] - if {[dict exists $received -RETURN]} { - set returntype [dict get $opts -RETURN] - } else { - if {[dict exists $opts -RETURN]} { - #not received - but has default - set returntype [dict get $opts -RETURN] - } else { - #fallback if -RETURN was defined without a default or was omitted - set returntype dict - } - } - - #-ASYNC is of -type none - solo flag - if {[dict exists $received -ASYNC]} { - set async true - } else { - set async false - } - - set FORCE 1 - if {[dict exists $opts -FORCE]} { - set FORCE [dict get $opts -FORCE] - } - - set query [list] ;#use list not dict - allows repeated params - dict for {k val} $opts { - switch -- $k { - -CUSTOM_PARAM { - #'-multiple true' - foreach kv $val { - lassign $kv paramname value - lappend query $paramname $value - } - } - -RETURN - -ASYNC - -FORCE { - #ignore - not options for endoint - already handled - } - default { - if {[string match *_FILTER $k]} { - #all _FILTER methods are '-multiple true' - so are present in $opts as a single key with a value that is a list - #e.g -X_Y_FILTER "blah" - set parts [split $k _] - set name1 [string range [string tolower [lindex $parts 0]] 1 end] ;#strip leading dash off first part - set nametail [string tolower [lrange $parts 1 end-1]] - set paramname [join [list $name1 {*}$nametail] _] - foreach fv $val { - lassign $fv filter value ;#filter is n,gte,lte etc - lappend query ${paramname}__$filter $value - } - } else { - set paramname [string range $k 1 end] - if {$k in $multis} { - foreach v $val { - lappend query $paramname $v - } - } else { - lappend query $paramname $val - } - } - } - } - - } - upvar ::punk::netbox::contexts contexts - if {![dict exists $contexts $apicontextid]} { - error "specified contextid '$apicontextid' not found" - } - - #rest api documentation is unclear on 'result' field - #note our default: result json - #this actually converts the json to a dict - - set config [dict create format json result json] - - dict set config headers [list Authorization [list Token [dict get $contexts $apicontextid token value]]] - set bodycontrol %bodycontrol% - switch -- $bodycontrol { - required { - set requestbody [dict get $values body] - #content type for the request data - dict set config headers content-type "application/json" - } - optional { - if {[dict exists $received body]} { - set requestbody [dict get $values body] - dict set config headers content-type "application/json" - } else { - set requestbody "" - } - } - default { - set requestbody "" - } - } - - - - set url [dict get $contexts $apicontextid url value] - set endpoint "%endpoint%" - if {[string first {{id}} $endpoint] != -1} { - set id [dict get $values id] - set endpoint [string map [list {{id}} $id] $endpoint] - } - append url $endpoint - - #??? - dict set config error-body false - - #todo - only show if debug (and obscure Authorization Token) - set sanconfig [punk::netbox::system::obscured_config $config] - puts stderr "url:$url query:'$query' verb:%verb% config:'$sanconfig'" - if {$FORCE} { - set thisproc [dict get [info frame 0] proc] - #FORCE is true for most operations (and no -FORCE option even available) but the option exists and defaults to false for specifically unsafe - #e.g delete operations on entire endpoints - - #---------------------------------------- - #from rest make_interface generated func: - set headers {} - dict for {key val} [dict get $config headers] {lappend headers $key [::rest::substitute $val query]} - set query [::http::formatQuery {*}$query] - #---------------------------------------- - set error_body true - - dict set config method %verb% ;#rest::_call does an upvar on config array - dict set config body $bodycontrol ;#this is required|optional not the requestbody data itself - #dict set config timeout ??? ;#todo -TIMEOUT flag? - - #============================================================================ - #rest::_call is just a small wrapper over http:: methods - #if {$bodycontrol in {required optional}} { - # #set result [::rest::%verb% ${url}$endpoint $query $config $requestbody] - # set httptok [::rest::_call [list ::punk::netbox::system::rest_call_complete $thisproc,$callid] $headers $url $query $requestbody $error_body] - #} else { - # #set result [::rest::%verb% ${url}$endpoint $query $config] - # set httptok [::rest::_call [list ::punk::netbox::system::rest_call_complete $thisproc,$callid] $headers $url $query "" $error_body] - #} - set method [string toupper %verb%] - - if {[string match no* $bodycontrol]} { - #never put the query in the body if the user said no body - } else { - if {![dict exists $received body] && $bodycontrol eq "required"} { - #our make_rest_func_async args say we need a body - but our punk::args validation didn't require it - #review - set requestbody $query - set query {} - } - } - - if {$query != ""} {append url ?$query} - #configure options to the http::geturl command - set opts [list] - lappend opts -method $method - if {[dict exists $headers content-type]} { - lappend opts -type [dict get $headers content-type] - set headers [dict remove $headers content-type] - } - if {$requestbody != ""} { - #-query arg for http::geturl causes it to do a POST with the value as the request payload. - lappend opts -query $requestbody - } - lappend opts -command [list ::punk::netbox::system::http_call_complete $thisproc $callid] - #if {[dict exists $config timeout]} { - # lappend opts -timeout [dict get $config timeout] - #} - set httptok [http::geturl $url -headers $headers {*}$opts] - #============================================================================ - - - - - #puts "tok: $httptok" - #puts "-----------" - #puts $result - #puts "-----------" - - upvar ::punk::netbox::system::call_data call_data - set call_data($thisproc,$callid,httptoken) $httptok - set call_data($thisproc,$callid,command) $thisproc - set call_data($thisproc,$callid,returntype) $returntype - set call_data($thisproc,$callid,config) $config - #todo - the actual async part - solo flag -ASYNC ? - if {$async} { - return $thisproc,$callid - } - #we need to then provide a $thisproc,$callid token to the caller and a retrieval/cleanup proc - http::wait $httptok - - return [punk::netbox::get_result $thisproc,$callid] - - } else { - puts stderr "%commandname% not called because -FORCE is false" - set sanconfig [punk::netbox::system::obscured_config $config] - puts "url:$url query:'$query' verb:%verb% config:'$sanconfig'" - return - } - }] - proc $commandname {args} $procbody - } - - proc rest_call_complete {key func status args} { - variable call_data - if {[catch { - upvar 1 token token - #parray token - #This assumes there is only one call in flight unless key is made unique per call - set call_data($ns) [array get token] - } errMsg]} { - puts "rest_call_complete error: $errMsg" - } - puts $args - #puts "->$ns $func $status $args" - } - - proc http_call_complete {func callid tok} { - variable call_data - set call_data($func,$callid,http) [array get $tok] - set rcode [http::ncode $tok] - if {![string match 2* $rcode]} { - upvar 0 $tok status - if {$rcode ne ""} { - set data [list NETBOX ERROR HTTP $rcode] - if {$rcode eq "302"} { - #is location always capitalised this way by http? It can vary in raw server responses - #tcllib rest had it as Location - which definitely breaks in at least some cases. - #(perhaps http lib normalized it?) - set meta [dict get $status(meta)] - set keys [dict keys $meta] - set keyname [lsearch -inline -nocase $keys location] - if {$keyname ne ""} { - lappend data location [dict get $meta $keyname] - } else { - lappend data location "(unknown)" ;#can this even happen? - } - #For a rest api - we probably shouldn't follow redirects (review) - #It's probably generally better to return the error and get the user to update their URL - #there may be cases where redirect-following is needed - add an option for it? - } else { - if {$status(totalsize) > 0} { - lappend data errorbody $status(body) - } - } - } else { - #no HTTP response code - may be a socket error - set data [list NETBOX ERROR OTHER $status(status)] - if {[info exists status(error)]} { - lappend data errortext $status(error) - } - } - set call_data($func,$callid,result) [list ERROR $data] - http::cleanup $tok - return - } - set call_data($func,$callid,result) [list OK [http::data $tok]] - http::cleanup $tok - return - } -} - -tcl::namespace::eval punk::netbox { - # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - # Base namespace - # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - #*** !doctools - #[subsection {Namespace punk::netbox}] - #[para] Core API functions for punk::netbox - #[list_begin definitions] - - variable PUNKARGS - - variable has_tls - set has_tls [expr {![catch {package require tls}]}] - - if {$has_tls} { - ::http::register https 443 ::tls::socket - } - - - variable contexts [dict create] - variable context_id 0 - - proc api_context_names {} { - variable contexts - return [dict keys $contexts] - } - - lappend PUNKARGS [list { - @id -id ::punk::netbox::api_contexts - @cmd -name punk::netbox::api_contexts -help\ - "Show in-memory api contexts. - These are named contexts for calling - the NETBOX rest api. - They are loaded using api_contexts_load from - a .toml configuration file, or created using - api_context_create." - -return -default table -choices {table tableobject dict} - -fields -type list -default {url tokentail comment} -choices {url token tokentail comment *} -choicemultiple {0 -1} -choicerestricted 0 -help\ - "The * token can be included in the list of specified - fields, and represents any other available fields found - from the matched contexts" - @values -min 0 -max 1 - globname -default * -help\ - "pattern to match the context name(s)" - }] - proc api_contexts {args} { - set argd [punk::args::parse $args withid ::punk::netbox::api_contexts] - lassign [dict values $argd] leaders opts values received - set returntype [dict get $opts -return] - set fields [dict get $opts -fields] - set globname [dict get $values globname] - - - variable contexts - set matches [dict keys $contexts $globname] - - - if {"*" in $fields} { - set starposn [lsearch -exact $fields *] - set before [lrange $fields 0 $starposn-1] - set after [lrange $fields $starposn+1 end] - set allspecified [list {*}$before {*}$after] - # use * as placeholder for all others not specified - retain order of specified columns - set fields [list] - #check fields in all matches - set starfields [list] - if {"tokentail" ni $allspecified} { - #calculated column - lappend starfields tokentail - } - foreach k $matches { - set contextinfo [dict get $contexts $k] - dict for {valkey valinfo} $contextinfo { - if {$valkey ni $allspecified && $valkey ni $starfields} { - lappend starfields $valkey - } - } - } - set fields [list {*}$before {*}$starfields {*}$after] - } - - - switch -- $returntype { - table - tableobject { - package require textblock - set t [textblock::table -return tableobject -minwidth 75 -headers [list contextid {*}$fields]] - foreach k $matches { - set contextinfo [dict get $contexts $k] - set tokentail "" - if {"tokentail" in $fields} { - #computed column - if {[dict exists $contextinfo token]} { - set tokentail [string range [dict get $contextinfo token value] end-5 end] - } - } - set rowdata [list $k] - foreach f $fields { - if {[dict exists $contextinfo $f value]} { - lappend rowdata [dict get $contextinfo $f value] - } else { - if {$f eq "tokentail"} { - lappend rowdata $tokentail - } else { - lappend rowdata "" - } - } - } - $t add_row $rowdata - } - if {$returntype eq "table"} { - set tableview [$t print] - $t destroy - return $tableview - } else { - return $t - } - } - dict { - set result [dict create] - foreach k $matches { - set contextinfo [dict get $contexts $k] - set tokentail "" - if {"tokentail" in $fields} { - #computed column - if {[dict exists $contextinfo token]} { - set tokentail [string range [dict get $contextinfo token value] end-5 end] - } - } - dict set result $k {} ;#ensure record is output even if empty fieldlist - foreach f $fields { - if {[dict exists $contextinfo $f value]} { - dict set result $k $f [dict get $contextinfo $f value] - } else { - if {$f eq "tokentail"} { - dict set result $k tokentail $tokentail - } - } - } - #dict for {valkey valinfo} $contextinfo { - # dict set result $k $valkey [dict get $valinfo value] - #} - } - return $result - } - } - } - - - - #get api handle(s) for a netbox server (with url and token) to pass to the punk::netbox api functions - namespace eval argdoc { - lappend PUNKARGS [list { - @id -id ::punk::netbox::api_context_load - @cmd -name punk::netbox::api_context_load\ - -summary\ - "Load API token contexts from storage."\ - -help\ - "Load API context information (url token) - from a .toml file in the data directory - or from a specified file. - - To create an initial file, use api_context - to create one or more named configurations - specifying the url and authentication token. - Then use api_context_save to persist them. - - Returns the names of the loaded contexts." - @opts - -contextname -default * -help\ - "Name of an API context or a pattern for - which contexts to load from the file." - -nowarnings -type none -help\ - "Suppress warnings emitted to stderr. - Will not stop error if datafile is not found" - @values - filepath -default "" -type file - }] - } - proc api_context_load {args} { - set argd [punk::args::parse $args withid ::punk::netbox::api_context_load] - lassign [dict values $argd] leaders opts values received - set contextglob [dict get $opts -contextname] - set filepath [dict get $values filepath] - set nowarnings [dict exists $received -nowarnings] - - if {$filepath eq ""} { - set filepath [_datafile] - } - if {![file exists $filepath]} { - error "No existing datafile at '$filepath'\nUse api_context_create/api_context_save to configure and save contexts" - } - package require tomlish - set tomldata [readFile $filepath] - set tomlish [tomlish::from_toml $tomldata] ;#intermediate (unvalidated) representation of toml data - maintaining whitespace and comments - set tomldict [tomlish::to_dict $tomlish] ;#lossy conversion to a programmatic structure (loss is of comments, whitespace) - variable contexts - #merge into any existing-in-memory loaded/created contexts - set loaded [list] - dict for {contextid contextinfo} $tomldict { - if {[string match $contextglob $contextid]} { - if {!$nowarnings} { - if {![dict exists $contextinfo url]} { - puts stderr "api_context_load warning: Loaded context $contextid is missing 'url' key" - } - if {![dict exists $contextinfo token]} { - puts stderr "api_context_load warning: Loaded context $contextid is missing 'token' key" - } - } - dict set contexts $contextid $contextinfo - lappend loaded $contextid - } - } - return $loaded - } - - namespace eval argdoc { - lappend PUNKARGS [list { - @id -id ::punk::netbox::init - @cmd -name punk::netbox::init\ - -summary\ - "Load all api token contexts."\ - -help\ - "This is a convenience function to call api_context_load - to load all available API token contexts with the default - data-file path being determined with the _datafile function." - @leaders - @opts - -quiet -type none -help\ - "Suppress status messages emitted to stdout. - Will not suppress the error raised if no datafile is available" - -nowarnings -type none -help\ - "Suppress warnings related to missing urls/tokens that are emitted - to stderr" - @values -min 0 -max 0 - }] - } - proc init {args} { - set argd [punk::args::parse $args withid ::punk::netbox::init] - set received [dict get $argd received] - set be_quiet [dict exists $received -quiet] - set nowarnings [dict exists $received -nowarnings] - set filepath [_datafile] - if {![file exists $filepath]} { - error "No existing datafile at '$filepath'\nCall api_context_load with a path to a .toml file with context records\nor use api_context_create/api_context_save." - } - if {!$be_quiet} { - puts stdout "Loading API token contexts from [_datafile]" - } - if {$nowarnings} { - api_context_load -nowarnings - } else { - api_context_load - } - if {!$be_quiet} { - puts stdout "Table produced by api_contexts" - puts stdout [api_contexts] - } - variable contexts - if {!$be_quiet} { - puts stdout "The contextid value is used as the 1st argument of most punk::netbox api functions" - puts stdout "If a raw token is required it can be retrieved with: api_context_get token" - puts stdout "[dict size $contexts] contexts loaded" - } - return [dict size $contexts] - } - - lappend PUNKARGS [list { - @id -id ::punk::netbox::api_context_create - @cmd -name punk::netbox::api_context_create -help\ - "Create an in-memory configuration for an API context. - This consists of a name (contextid) under which a - url and authentication token are stored. - It can optionally be persisted using api_context_save - to the file of your choice, or to a reasonable default - location. (see _datafile). - The api_context_load function can be used to retrieve - previously stored contextids instead of calling this - function each time. - - A contextid is required when calling the netbox rest api - functions such as ipam::vrfs - This allows easy intermixing of calls to either the same - or different servers using the different permissions - granted by each token. - " - @leaders -min 1 -max 1 - contextid -type string -help\ - "Name for the api context. - If saved to a .toml file, this - will be the name of a toplevel table - containing configuration elements such - as url and token." - @opts - -property_value -type list -minsize 2 -maxsize 2 -multiple 1 -help\ - "custom property and value. - e.g - property_value {comment {test comment}}" - @values -min 2 -max 2 - url -type string -help\ - "Base url of netbox server" - token -type string -help\ - "Netbox API authentication token" - }] - proc api_context_create {args} { - set argd [punk::args::parse $args withid ::punk::netbox::api_context_create] - lassign [dict values $argd] leaders opts values received - set contextid [dict get $leaders contextid] - if {[dict exists $received -property_value]} { - set propvals [dict get $opts -property_value] ;#multiple - as pairs - } else { - set propvals [list] - } - set baseurl [dict get $values url] - set token [dict get $values token] - - variable contexts - if {[dict exists $contexts $contextid]} { - error "api_context_create a context with id '$contextid' already exists." - } - set allprops [dict create url [dict create type STRING value $baseurl] token [dict create type STRING value $token]] - foreach pv $propvals { - lassign $pv p v - if {$p in {url token}} { - puts stderr "ignoring -property_value $p - invalid - already specified in arguments" - } - #todo - multiline? - dict set allprops $p [dict create type STRING value $v] - } - dict set contexts $contextid $allprops - return $contextid - } - namespace eval argdoc { - lappend PUNKARGS [list { - @id -id ::punk::netbox::api_context_get - @cmd -name punk::netbox::api_context_get -help\ - "Get api context fields for a single context. - A unique prefix of any loaded context may be - used." - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - Contextids can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${[punk::netbox::api_context_names]}} - @values -min 0 -max -1 - field -type string -multiple 1 -optional 1 -help\ - "If no field arguments are supplied, all will - be retrieved. - Note that a non-existant field name will produce - an empty string element in the result list for - that position. This is indistinguishable from an - existent field with an empty string as the value. - - As an alternative, the api_contexts function with - the -return dict argument can be used instead to - access multiple fields for multiple apicontextids. - " - }] - } - proc api_context_get {args} { - set argd [punk::args::parse $args withid ::punk::netbox::api_context_get] - lassign [dict values $argd] leaders opts values received - set contextid [dict get $leaders apicontextid] - if {[dict exists $received field]} { - set fields [dict get $values field] ;#-multiple true - so always a list - } else { - set fields [list] - } - - variable contexts - set cdata [dict get $contexts $contextid] - set result [list] - if {![llength $fields]} { - set fields [dict keys $cdata] - } - foreach f $fields { - if {[dict exists $cdata $f]} { - lappend result [dict get $cdata $f value] - } else { - lappend result "" - } - } - return $result - } - - lappend PUNKARGS [list { - @id -id ::punk::netbox::get_status - @cmd -name punk::netbox::get_status\ - -summary\ - "Get status of previous -ASYNC call."\ - -help\ - "Retrieve the status from a previous punk::netbox::?ns::? - call which was run with -ASYNC. - - An error will be raised if the asynctoken is not valid or - if its result has already been retrieved with get_result. - - Returns PENDING, OK or ERROR - - It will not return the error text or result. - - It will not clear the error/result. - " - @values -min 1 -max 1 - asynctoken -type string -help\ - "A token as returned from a punk::netbox command when - it was given the -ASYNC flag" - }] - proc get_status {asynctoken} { - upvar ::punk::netbox::system::call_data call_data - if {![info exists call_data($asynctoken,httptoken)]} { - error "No such asynctoken: $asynctoken" - } - if {![info exists call_data($asynctoken,http)]} { - return PENDING - } else { - #even though we could return the raw result here, - #we don't - because the user should call get_result for cleanup. - return [lindex $call_data($asynctoken,result) 0] ;#OK|ERROR - } - } - lappend PUNKARGS [list { - @id -id ::punk::netbox::is_finished - @cmd -name punk::netbox::is_finished\ - -summary\ - "Test if result/error of previous -ASYNC call available."\ - -help\ - "Return an integer 0,1 or 2 indicating whether the result/error from a - previous punk::netbox::?ns::? call which was run with -ASYNC - is ready. - 0 indicates the request is still pending. - 1 indicates there is a result available. - 2 indicates there the command completed with an error. - - An error will be raised if the token is not valid or if its - result has already been retrieved with get_result." - @values -min 1 -max 1 - asynctoken -type string -help\ - "A token as returned from a punk::netbox command when - it was given the -ASYNC flag" - }] - proc is_finished {asynctoken} { - upvar ::punk::netbox::system::call_data call_data - if {![info exists call_data($asynctoken,httptoken)]} { - error "No such asynctoken: $asynctoken" - } - if {![info exists call_data($asynctoken,http)]} { - return 0 - } else { - if {[lindex $call_data($asynctoken,result) 0] eq "OK"} { - return 1 - } else { - return 2 - } - } - } - - lappend PUNKARGS [list { - @id -id ::punk::netbox::get_result_http - @cmd -name punk::netbox::get_result_http\ - -summary\ - "Get results of previous -ASYNC call."\ - -help\ - "Retrieve the http information from a previous punk::netbox::?ns::? - call which was run with -ASYNC. - - An error will be raised if the asynctoken is not valid or - if its result has already been retrieved with get_result. - - If the call has not yet returned or timed out, this call - will wait until the http token's status has been set. - - This call does not invalidate/clean-up the asynctoken and - if used, should be called prior to get_result. - " - @values -min 1 -max 1 - asynctoken -type string -help\ - "A token as returned from a punk::netbox command when - it was given the -ASYNC flag" - }] - proc get_result_http {asynctoken} { - upvar ::punk::netbox::system::call_data call_data - if {![info exists call_data($asynctoken,httptoken)]} { - error "No such asynctoken: $asynctoken" - } - set httptok $call_data($asynctoken,httptoken) - if {![info exists call_data($asynctoken,http)]} { - http::wait $httptok - } - return $call_data($asynctoken,http) - } - - lappend PUNKARGS [list { - @id -id ::punk::netbox::get_result - @cmd -name punk::netbox::get_result\ - -summary\ - "Get results of previous -ASYNC call."\ - -help\ - "Retrieve the result from a previous punk::netbox::?ns::? - call which was run with -ASYNC. - - An error will be raised for a call that was unsuccessful. - An error will be raised if the asynctoken is not valid or - if its result has already been retrieved with get_result. - - If the call has not yet returned or timed out, this call - will wait until the http token's status has been set." - @values -min 1 -max 1 - asynctoken -type string -help\ - "A token as returned from a punk::netbox command when - it was given the -ASYNC flag" - }] - proc get_result {asynctoken} { - upvar ::punk::netbox::system::call_data call_data - - set debug 0 ;#todo -DEBUG flag? - - if {![info exists call_data($asynctoken,httptoken)]} { - error "No such asynctoken: $asynctoken" - } - set httptok $call_data($asynctoken,httptoken) - if {![info exists call_data($asynctoken,http)]} { - http::wait $httptok - } - - #set command [lindex [split $asynctoken ,] 0] ;#review - assumes no comma in command name - not a great assumption! - set command $call_data($asynctoken,command) - - set resultlist $call_data($asynctoken,result) - set returntype $call_data($asynctoken,returntype) - set config $call_data($asynctoken,config) - if {$returntype in "json jsondump"} { - #if we leave 'result' = json - we get a dict instead of json :/ - #The 'result' key seems to tell ::rest what post-processor to use to translate it *from* that to a tcl dict (or in xml case a nested list) - #sort of.. - #xml - takes xml produces a list - #json - takes json produces a dict - #discard - 'return -code ok' - #auto - *basic* detect xml or json and do as above - #tdom - takes xml - produces a tdom documentElement - #raw - just returns the data - dict set config result raw - } - - - #status is #OK|ERROR - lassign $resultlist status resulttext - #resulttext NETBOX ERROR ... - if {$status eq "ERROR"} { - set etype [lindex $resulttext 2] - #--------------------------------- - #this works - but the raw json seems more useful, and less prone to secondary errors - #--------------------------------- - #if {$etype eq "HTTP"} { - # #return -code error -errorcode [list NETBOX HTTP $ncode] $resulttext - # set errorbody [lindex $resulttext 5] - # if {$errorbody ne ""} { - # if {[catch { - # set result [list {*}[lrange $resulttext 0 4] [::rest::format_[dict get $config result] $errorbody]] ;#crude detection of xml/json - REVIEW - # #if xml - we don't get a dict - but netbox shouldn't output that anyway. - # #would be better just to output raw? - # } errM]} { - # set result $resulttext - # } - # } else { - # set result $resulttext - # } - #} else { - # set result $resulttext - #} - #--------------------------------- - #if we try to pass any of the error results to the requested returntypes below - it would only make sense for JSON anyway? - set result $resulttext - set errorcode [list NETBOX $etype [lindex $resulttext 3]] - if {!$debug} { - #clean up our copy of the http token data even on error - array unset call_data $asynctoken,* - } - return -code error -errorcode $errorcode $result - } - - #This is another main reason we couldn't just use ::rest lib as-is. - #Servers sometimes respond with empty body - #::rest lib tries to parse empty string as JSON and just emits an ugly error - #we may do the same if one of the returntype branches fails - but at least the user has a choice. - if {$resulttext ne ""} { - #review - could get nest structure from xml - but not relevant to netbox - #parsing json could fail here too - #eg when config result is 'json' - parse json to a dict - set result [::rest::format_[dict get $config result] $resulttext] - } else { - set result "" - } - - if {!$debug} { - #clean up our copy of the http token data - array unset call_data $asynctoken,* - } - - #try catch - return errorcode: NETBOX OUTPUTFORMATTING ? - switch -exact -- $returntype { - showpagedict { - return [punk::lib::showdict $result !@@results @@results/@*/@*.@*] - } - showpagedict2 { - return [punk::lib::showdict $result @@results/@*/@*.@* !@@results] - } - showdict { - if {$command eq "::punk::netbox::status"} { - #we get duplicate django-version for %showdict% - todo - something. - return [punk::lib::showdict $result @@django-version @@installed-apps/@*.@* !@@installed-apps] - } - return [punk::lib::showdict $result *] - } - showdict2 { - if {$command eq "::punk::netbox::status"} { - #we get duplicate django-version for %showdict% - todo - something. - return [punk::lib::showdict $result @@installed-apps/@*.@* !@@installed-apps] - } - return [punk::lib::showdict $result */*] - } - showlist { - return [punk::lib::showdict -roottype list $result] - } - showlistofdicts { - return [punk::lib::showdict $result @*/@*.@*] - } - jsondump { - package require huddle::json - #pretty-print via huddle (inefficient review) - set h [huddle::json::json2huddle parse $result] - return [huddle::jsondump $h] - } - linelist { - set ret "" - foreach r $result { - append ret $r \n - } - return $ret - } - default { - #plain result: (list or dict) or json - the counterintuitive 'result' field set to raw above sets the rest call's resulting format to json - return $result - } - } - - } - - - - proc _homedir {} { - if {[info exists ::env(HOME)]} { - set home [file normalize $::env(HOME)] - } else { - #not available on 8.6? ok will error out here. - set home [file tildeexpand ~] - } - return $home - } - lappend PUNKARGS [list { - @id -id ::punk::netbox::_datafile - @cmd -name punk::netbox::_datafile -help\ - "Get the path for the default storage file - used when an explicit path is not given by - the caller to the api_context load/save - functions. This file is in toml format. - - On any platform the XDG_DATA_HOME env var - can be used to override the location, but - on Windows the LOCALAPPDATA env var will - specifiy the location if XDG_DATA_HOME is - not set. - Interfacing with a proper secret store - should be considered as an alternative. - - On non Windows platforms: - The XDG_DATA_HOME env var is the preferred - choice of location - considered slightly more - secure than XDG_CONFIG_HOME. - A folder under the user's home directory, - at .local/share/punk/netbox is chosen if - XDG_DATA_HOME is not configured. - " - @leaders -min 0 -max 0 - @opts - -quiet -type none -help\ - "Suppress warning given when the folder does - not yet exist" - @values -min 0 -max 0 - }] - proc _datafile {args} { - set argd [punk::args::parse $args withid ::punk::netbox::_datafile] - lassign [dict values $argd] leaders opts values received - set be_quiet [dict exists $received -quiet] - - set was_noisy 0 - if {[info exists ::env(XDG_DATA_HOME)]} { - set data_home $::env(XDG_DATA_HOME) - } else { - if {$::tcl_platform(platform) eq "windows"} { - set data_home $::env(LOCALAPPDATA) - } else { - set data_home [file join [_homedir] .local share] - if {!$be_quiet} { - puts stderr "Environment variable XDG_DATA_HOME does not exist - consider setting it if $data_home is not a suitable location" - set was_noisy 1 - } - } - } - if {!$be_quiet && ![file exists $data_home]} { - #parent folder for 'punk' config dir doesn't exist - set msg "configuration location XDG_DATA_HOME or ~/.local/share (or LOCALAPPDATA on windows) at path '$data_home' does not yet exist" - append msg \n " - please create it and/or set the appropriate env var." - puts stderr $msg - set was_noisy 1 - } - set punk_netbox_data_dir [file join $data_home punk netbox] - if {!$be_quiet && ![file exists $punk_netbox_data_dir]} { - set msg "punk::netbox data storage folder at $punk_netbox_data_dir does not yet exist." - append msg \n " It will be created if api_context_save is called without specifying an alternate location." - puts stderr $msg - set was_noisy 1 - } - if {!$be_quiet && $was_noisy} { - puts stderr "punk::netbox::_datafile - call with -quiet option to suppress these messages" - } - return [file join $punk_netbox_data_dir netbox_api_contexts.toml] - } - - - lappend PUNKARGS [list { - @id -id ::punk::netbox::api_context_save - @cmd -name punk::netbox::api_context_save -help\ - "" - @values - contextid -type string -help\ - "Name for the api context. - If saved to a .toml file, this - will be the name of a toplevel table - containing configuration elements such - as url and token." - filepath -default "" -optional 1 -type file -help\ - "Path of .toml configuration file containing - API url and token information. - If empty it will store under XDG_DATA_DIR - if the env var is defined, or in the - corresponding location within ~/.local/share. - In both cases the subfolder netbox/punk will - be used. - These locations are fairly reasonable for - sensitive data - but as tokens are not - encrypted, a proper security store should be - used instead if your risk-policy requires - more serious security. - " - }] - proc api_context_save {args} { - set argd [punk::args::parse $args withid ::punk::netbox::api_context_save] - lassign [dict values $argd] leaders opts values received - set contextid [dict get $values contextid] - set filepath [dict get $values filepath] - - variable contexts - if {![dict exists $contexts $contextid]} { - error "punk::netbox::api_context_save error. No context with id '$contextid' exists. Load from file, or create it using punk::netbox::api_context" - } - if {$filepath eq ""} { - set filepath [_datafile -quiet] - set filefolder [file dirname $filepath] - if {![file exists $filefolder]} { - file mkdir $filefolder - } - } - set configdir [file dirname $filepath] - if {![file exists $configdir]} { - error "api_context_save error: folder $configdir doesn't exist" - } - package require tomlish - if {[file exists $filepath]} { - set existing_toml [readFile $filepath] - set tomlish [tomlish::from_toml $existing_toml] - set data_dict [tomlish::to_dict $tomlish] - if [dict exists $data_dict $contextid] { - #todo - nondestructive merge - don't destroy comments/formatting of existing records - #if we use to_dict on the existing tomlish - we lose comments etc - #also from_dict doesn't yet produce canonical nice-for-humans tomlish/toml - #merge - puts stderr "contextid '$contextid' exists in file $filepath" - puts stderr "Merge not implemented.." - set newfiledata "" - } else { - #append to existing toml data - set newdict [dict create $contextid [dict get $contexts $contextid]] - #we store our contexts in a structure already suitable for toml - # (ie one where we tag strings,ints e.g {type STRING value "etc"}) - set newtomlish [tomlish::from_dict $newdict] - set newtoml [tomlish::to_toml $newtomlish] - set newfiledata $existing_toml\n$newtoml - } - } else { - set newdict [dict create $contextid [dict get $contexts $contextid]] - set newtomlish [tomlish::from_dict $newdict] - set newtoml [tomlish::to_toml $newtomlish] - set newfiledata $newtoml - } - - if {$newfiledata ne ""} { - writeFile $filepath $newfiledata - puts stderr "saved [string length $newfiledata] bytes to '$filepath'" - } - } - - - namespace eval argdoc { - set _page_options { - -limit -default 100 -type integer -help\ - "Each REST query returns a maximum number - of results. This can be set to 0 to mean - no limit - but it is still restricted to - the max configured on the server. (1000?) - - This is effectively the page-size of the - results. To retrieve more than a page, the - next and previous urls can be iterated over." - -offset -default 0 -type integer - } - set _create_update_options { - -created - -created__gte - -created__lte - -last_updated - -last_updated__gte - -last_updated__lte - } - set _tenant_options { - -tenant_group_id - -tenant_group_id__n - -tenant_group - -tenant_group__n - -tenant_id - -tenant_id__n - -tenant - -tenant__n - } - set _region_options { - -region_id - -region - } - set _site_options { - -site_group_id - -site_group_id__n - -site_group - -site_group__n - -site_id - -site_id__n - -site - -site__n - } - set _group_options { - -group_id - -group_id__n - -group - -group__n - } - set _contact_options { - -contact - -contact__n - -contact_role - -contact_role__n - -contact_group - -contact_group__n - } - set _role_options { - -role_id - -role_id__n - -role - -role__n - } - set _filter_string [list\ - "ie \n Exact match\n(case-insensitive)"\ - "nie \n Inverse exact match\n(case-insensitive)"\ - "n \n Not equal to"\ - "ic \n Contains\n (case-insensitive)"\ - "nic \n Does not contain\n (case-insensitive)"\ - "isw \n Starts with\n (case-insensitive)"\ - "nisw \n Does not start with\n (case-insensitive)"\ - "iew \n Ends with\n (case-insensitive)"\ - "niew \n Does not end with\n (case-insensitive)"\ - "empty \n Is empty/null"\ - ] - set _filter_number [list\ - "n \n Not equal to"\ - "lte \n Less than or equal"\ - "lt \n Less than"\ - "gte \n Greater than or equal"\ - "gt \n Greater than"\ - ] - set _CUSTOM_PARAMS { - -CUSTOM_PARAM -type list -minsize 2 -maxsize 2 -multiple 1 -help\ - "Specify a parameter not in this API - e.g -CUSTOM_PARAM {mytag blah}" - } - set _RETURN_PAGEDICT { - -RETURN -type string -choices {dict showpagedict showpagedict2 json jsondump} -choicelabels { - dict\ - " Tcl dictionary - (fastest)" - showpagedict\ - " human readable dict display - with same order as dict." - showpagedict2\ - " human readable dict display - results first, page metadata last." - } -help\ - "Options for returned data. - Note that showdict results are relatively slow, especially for large resultsets" - } - set _RETURN_DICT { - -RETURN -type string -choices {dict showdict showdict2 json jsondump} -choicelabels { - dict\ - " Tcl dictionary - (fastest)" - showdict\ - " human readable dict display - with same order as dict." - showdict2\ - " human readable dict display - results first metadata last." - } -help\ - "Options for returned data. - Note that showdict results are relatively slow, especially for large resultsets" - } - set _RETURN_LIST { - -RETURN -type string -choices {list linelist showlist json jsondump} -choicelabels { - list\ - " Tcl list - (fastest)" - linelist\ - " raw list with newline after each item" - showlist\ - " human readable list display" - } -help\ - "Options for returned data. - Note that showlist results are relatively slow, especially for large resultsets" - } - set _RETURN_LISTOFDICTS { - -RETURN -type string -choices {list linelist showlist json jsondump} -choicelabels { - list\ - " Tcl list - (fastest)" - linelist\ - " raw list with newline after each item" - showlistofdicts\ - " human readable display list of dicts" - } -help\ - "Options for returned data. - Note that showlist results are relatively slow, especially for large resultsets" - } - set _RETURN_STATUS { - -RETURN -type string -default showdict2 -choices {dict showdict showdict2 json jsondump} -choicelabels { - dict\ - " Tcl dictionary" - showdict\ - " human readable dict display" - showdict2\ - " human readable dict display - installed-apps first." - } -help\ - "Options for returned data." - } - - set _name_filter_help "Paired search filter for name:\n" - append _name_filter_help [textblock::list_as_table -columns 4 -show_hseps 1 $_filter_string] - - set _description_filter_help "Paired search filter for description:\n" - append _description_filter_help [textblock::list_as_table -columns 4 -show_hseps 1 $_filter_string] - - set string_filter_help "Paired search filter for string:\n" - append _string_filter_help [textblock::list_as_table -columns 4 -show_hseps 1 $_filter_string] - - #n, lte, lt, gte, gt - #e.g virtualization/virtual-machine vcpus, memory, disk - set number_filter_help "Paired search filter for number:\n" - append _number_filter_help [textblock::list_as_table -columns 3 -show_hseps 1 $_filter_number] - } - - punk::args::define {*}[list\ - { - @id -id ::punk::netbox::status - @cmd -name punk::netbox::status -help\ - "status_list - GET request for endpoint /status/ - - Netbox's current operational status - " - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${[punk::netbox::api_context_names]}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_STATUS]\ - { - -ASYNC -type none - @values -min 0 -max 0 - }] - ::punk::netbox::system::make_rest_func_async ::punk::netbox::status api/status/ -verb get -body none - - #test - to see what the create_interface procs look like with a callback - #(they are not flexible enough argument-wise for this usecase) - #with a static callback baked into the function - it also gives no way to reliably determine - #which request matched which call if there are concurrent calls in flight for the same function/endpoint especially if args are the same - #(order of completion could presumably not always match call order) - #presumably an http::wait is required on each call, and they must be processed in order? - set RESTAPI(getstatus) { - url https://www.netbox1.intx.com.au/api/status/ - method get - headers {Authorization {Token bogus} content-type "application/json"} - callback {::punk::netbox::system::rest_call_complete RESTAPI} - error-body false - opt_args {test etc} - req_args {force} - } - #namespace eval ::punk::netbox::spud {} - ::rest::create_interface ::punk::netbox::RESTAPI - # - - - #test function - todo use punk::netbox::system::make_rest_func - #proc vrfs {args} { - # set argd [punk::args::parse $args withid ::punk::netbox::vrfs] - # lassign [dict values $argd] leaders opts values received - # set apicontextid [dict get $leaders apicontextid] - # set query [dict create] - # dict for {k v} $opts { - # if {$k eq "-CUSTOM_PARAM"} { - # foreach custval $v { - # lassign $custval param value - # dict set query $param $value - # } - # } elseif {[string match *_FILTER $k]} { - # set field [string range [string tolower [lindex [split $k _] 0]] 1 end] ;# -NAME_FILTER -> name - # foreach fv $v { - # lassign $fv filter value - # dict set query ${field}__$filter $value - # } - # } else { - # dict set query [string range $k 1 end] $v - # } - # } - # variable contexts - # if {![dict exists $contexts $apicontextid]} { - # error "specified contextid '$apicontextid' not found" - # } - # set config [dict create\ - # result json\ - # ] - # dict set config headers [list Authorization [list Token [dict get $contexts $apicontextid token value]]] - - # #variable headerdict - # #set config [dict create\ - # # headers $headerdict\ - # # result json\ - # #] - # #variable url - # set url [dict get $contexts $apicontextid url value] - - # puts "${url}api/ipam/vrfs/ '$query' '$config'" - # rest::get ${url}api/ipam/vrfs/ $query $config - #} - - #set ipam(vrfs) [dict create\ - # url https://www.netbox1.intx.com.au/api/ipam/vrfs/\ - # method get\ - # result json\ - # body none\ - # headers $headerdict\ - # opt_args {id: name: limit:100 offset:0} - #] - #set ipam(ip-addresses) [dict create\ - # url https://www.netbox1.intx.com.au/api/ipam/ip-addresses/\ - # method get\ - # headers $headerdict\ - # opt_args {parent: limit:100 offset:0} - #] - #set ipam(prefixes) [dict create\ - # url https://www.netbox1.intx.com.au/api/ipam/prefixes/\ - # method get\ - # headers $headerdict\ - # opt_args {prefix: limit:100 offset:0} - #] - #rest::create_interface ::punk::netbox::ipam - - - #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 punk::netbox ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -tcl::namespace::eval punk::netbox::dcim { - namespace export {[a-z]*} - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::dcim::devices_list - @cmd -name punk::netbox::dcim::devices_list -help\ - "tenancy_tenants_list - GET request for endpoint /dcim/devices/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${[punk::netbox::api_context_names]}} - @opts - -id -type integer - -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help\ - {${$::punk::netbox::argdoc::_number_filter_help}} - -name - -NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_name_filter_help}} - -asset_tag -type string - -ASSET_TAG_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_string_filter_help}} - -face -type string - -face__n -type string - -position -type integer - -POSITION_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -airflow -type string - -airflow__n -type string - -vc_position -type integer - -VC_POSITION_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -vc_priority -type integer - -VC_PRIORITY_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - }\ - [set ::punk::netbox::argdoc::_create_update_options]\ - { - -q -type string - -tag -type string -multiple true - -tag__n -type string - }\ - [set ::punk::netbox::argdoc::_tenant_options]\ - [set ::punk::netbox::argdoc::_contact_options]\ - { - -local_context_data - -manufacturer_id - -manufacturer_id__n - -manufacturer - -manufacturer__n - -device_type_id - -device_type_id__n - -role_id - -role_id__n - -role - -role__n - -parent_device_id - -parent_device_id__n - -platform_id - -platform_id__n - -platform - -platform__n - }\ - [set ::punk::netbox::argdoc::_group_options]\ - [set ::punk::netbox::argdoc::_region_options]\ - [set ::punk::netbox::argdoc::_site_options]\ - { - -location_id -type integer - -location_id__n -type integer - -rack_id -type integer - -rack_id__n -type integer - -cluster_id -type integer - -cluster_id__n -type integer - -model -type string - -model__n -type string - -status -type string - -status__n -type string - -mac_address -type string - -MAC_ADDRESS_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_string_filter_help}} - -serial -type string - -SERIAL_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_string_filter_help}} - -virtual_chassis_id -type integer - -virtual_chassis_id__n -type integer - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ - { - @values -min 0 -max 0 - }] - ::punk::netbox::system::make_rest_func_async ::punk::netbox::dcim::devices_list api/dcim/devices/ -verb get -body none -} - -tcl::namespace::eval punk::netbox::ipam { - namespace export {[a-z]*} - namespace eval argdoc { - set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::ipam::vrfs_list - @cmd -name punk::netbox::ipam::vrfs_list -help\ - "ipam_vrfs_list - GET request for endpoint /ipam/vrfs/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -id -type integer - -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -name - -NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_name_filter_help}} - -rd -type string -help\ - "Route distinguisher in any format" - -enforce_unique - -description -type string -help "Exact Match (case sensitive)" - -DESCRIPTION_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_description_filter_help}} - }\ - [set ::punk::netbox::argdoc::_create_update_options]\ - { - -q - -tag -type string -multiple true - }\ - [set ::punk::netbox::argdoc::_tenant_options]\ - [set ::punk::netbox::argdoc::_region_options]\ - [set ::punk::netbox::argdoc::_site_options]\ - [set ::punk::netbox::argdoc::_group_options]\ - [set ::punk::netbox::argdoc::_role_options]\ - { - -status - -available_on_device - -available_on_virtualmachine - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ - { - @values -min 0 -max 0 - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::vrfs_list api/ipam/vrfs/ -verb get -body none - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::ipam::vrfs_read - @cmd -name punk::netbox::ipam::vrfs_read -help\ - "ipam_vrfs_list - GET request for endpoint /ipam/vrfs/{id}" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying this VRF" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::vrfs_read api/ipam/vrfs/{id}/ -verb get -body none - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::prefixes_list - @cmd -name punk::netbox::ipam::prefixes_list -help\ - "ipam_prefixes_list - GET request for endpoint /ipam/prefixes/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -id -type integer - -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -is_pool - -mark_utilized - -description -type string -help "Exact Match (case sensitive)" - -DESCRIPTION_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_description_filter_help}} - }\ - [set ::punk::netbox::argdoc::_create_update_options]\ - { - -q -type string -help\ - "Query prefixes by substring" - -tag -type string -multiple true - }\ - [set ::punk::netbox::argdoc::_tenant_options]\ - [set ::punk::netbox::argdoc::_region_options]\ - [set ::punk::netbox::argdoc::_site_options]\ - [set ::punk::netbox::argdoc::_group_options]\ - [set ::punk::netbox::argdoc::_role_options]\ - { - -family - -prefix - -within - -within_include - -contains - -depth - -DEPTH_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -children - -CHILDREN_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -mask_length - -mask_length__gte - -mask_length__lte - -vlan_id -type integer - -vlan_id__n -type integer - -vlan_vid -type integer - -VLAN_VID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -vrf_id - -vrf - -status - -available_on_device - -available_on_virtualmachine - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ - { - @values -min 0 -max 0 - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_list api/ipam/prefixes/ -verb get -body none - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::prefixes_create - @cmd -name punk::netbox::ipam::prefixes_create -help\ - "ipam_prefixes_create - POST request for endpoint /ipam/prefixes/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - body -type string -help\ - "JSON string" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_create api/ipam/prefixes/{id}/ -verb post -body required - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::prefixes_read - @cmd -name punk::netbox::ipam::prefixes_read -help\ - "ipam_prefixes_read - GET request for endpoint /ipam/prefixes/{id}/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying this prefix" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_read api/ipam/prefixes/{id}/ -verb get -body none - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::prefixes_available-ips_list - @cmd -name punk::netbox::ipam::prefixes_available-ips_list -help\ - "ipam_prefixes_available-ips_list - GET request for endpoint /ipam/prefixes/{id}/available-ips/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_LISTOFDICTS]\ - { - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying this prefix" - }\ - ] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-ips_list api/ipam/prefixes/{id}/available-ips/ -verb get -body none - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::prefixes_available-ips_create - @cmd -name punk::netbox::ipam::prefixes_available-ips_create -help\ - "ipam_prefixes_available-ips_create - POST request for endpoint /ipam/prefixes/{id}/available-ips/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_LIST]\ - { - @values -min 1 -max 2 - id -type integer -help\ - "A unique integer value identifying this prefix" - body -type string -default "" -help\ - { - If empty create a single IP with default values. - (next available IP in prefix) - - e.g Create 2 IPs: - [ - {"description": "ip1"}, - { - "description": "ip2", - "tenant": 5, - "dns_name": "test.intx.com.au" - } - ] - NOTE1: tenant is the tenant_id (why?) - NOTE: This always uses next available IPs. - To create a specific IP, use api/ipam/ip-addresses endpoint. - - The returned json is just an object if one address created, - but a list if multiple. :/ - - } - }\ - ] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-ips_create api/ipam/prefixes/{id}/available-ips/ -verb post -body required - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::prefixes_available-prefixes_list - @cmd -name punk::netbox::ipam::prefixes_available-prefixes_list -help\ - "ipam_prefixes_available-prefixes_list - GET request for endpoint /ipam/prefixes/{id}/available-prefixes/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_LISTOFDICTS]\ - { - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying this prefix" - }\ - ] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-prefixes_list api/ipam/prefixes/{id}/available-prefixes/ -verb get -body none - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::prefixes_available-prefixes_create - @cmd -name punk::netbox::ipam::prefixes_available-prefixes_create -help\ - "ipam_prefixes_available-prefixes_create - POST request for endpoint /ipam/prefixes/{id}/available-prefixes/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_LIST]\ - { - @values -min 1 -max 2 - id -type integer -help\ - "A unique integer value identifying this prefix" - body -type string -default "" -help\ - { - { - "prefix_length": 0 - } - } - }\ - ] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-prefixes_create api/ipam/prefixes/{id}/available-prefixes/ -verb post -body required - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::ip-addresses_list - @cmd -name punk::netbox::ipam::ip-addresses_list -help\ - "ipam_ip-addresses_list - GET request for endpoint /ipam/ip-addresses/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -id -type integer - -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -dns_name - -DNS_NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_string_filter_help}} - -description -type string -help "Exact Match (case sensitive)" - -DESCRIPTION_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_description_filter_help}} - }\ - [set ::punk::netbox::argdoc::_create_update_options]\ - { - -q - -tag -multiple true -help\ - "If multiple -tag options supplied, - they are ANDed (review)" - }\ - [set ::punk::netbox::argdoc::_tenant_options]\ - [set ::punk::netbox::argdoc::_region_options]\ - [set ::punk::netbox::argdoc::_site_options]\ - [set ::punk::netbox::argdoc::_group_options]\ - [set ::punk::netbox::argdoc::_role_options]\ - { - -family - -parent - -address - -mask_length - -vrf_id - -vrf - -present_in_vrf_id - -present_in_vrf - -device - -device_id - -virtual_machine - -virtual_machine_id - -interface - -interface_id - -vminterface - -vminterface_id - -fhrpgroup_id - -assigned_to_interface - -status - -role - -available_on_device - -available_on_virtualmachine - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ - { - @values -min 0 -max 0 - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_list api/ipam/ip-addresses/ -verb get -body none - - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::ip-addresses_create - @cmd -name punk::netbox::ipam::ip-addresses_create -help\ - "ipam_ip-addresses_create - POST request for endpoint /ipam/ip-addresses/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - body -type string -help\ - {JSON string - Example: - { - "address": "string", - "vrf": 0, - "tenant": 0, - "status": "active", - "role": "loopback", - "assigned_object_type": "string", - "assigned_object_id": 0, - "nat_inside": 0, - "dns_name": "string", - "description": "string", - "tags": [ - { - "name": "string", - "slug": "string", - "color": "string" - } - ], - "custom_fields": {} - } - Required: address (IPv4 or IPV6 address with mask) - } - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_create api/ipam/ip-addresses/ -verb post -body required - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::ip-addresses_bulk_partial_update - @cmd -name punk::netbox::ipam::ip-addresses_bulk_partial_update -help\ - "ipam_ip-addresses_bulk_partical_update - PATCH request for endpoint /ipam/ip-addresses/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - body -type string -help\ - {JSON string - model: - { - "address": "string", - "vrf": 0, - "tenant": 0, - "status": "active", - "role": "loopback", - "assigned_object_type": "string", - "assigned_object_id": 0, - "nat_inside": 0, - "dns_name": "string", - "description": "string", - "tags": [ - { - "name": "string", - "slug": "string", - "color": "string" - } - ], - "custom_fields": {} - } - required: address - } - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_bulk_partial_update api/ipam/ip-addresses/ -verb patch -body required - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::ip-addresses_read - @cmd -name punk::netbox::ipam::ip-addresses_read -help\ - "ipam_ip-addresses_read - GET request for endpoint /ipam/ip-addresses/{id}/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_read api/ipam/ip-addresses/{id}/ -verb get -body none - - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::ip-addresses_delete - @cmd -name punk::netbox::ipam::ip-addresses_delete\ - -summary\ - "Delete an IP address by id."\ - -help\ - "ipam_ip-addresses_delete - DELETE request for endpoint /ipam/ip-addresses/{id}/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_delete api/ipam/ip-addresses/{id}/ -verb delete -body none - -} - - -tcl::namespace::eval punk::netbox::tenancy { - namespace export {[a-z]*} - variable PUNKARGS - - namespace eval argdoc { - variable PUNKARGS - set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::tenancy::tenants_list - @cmd -name punk::netbox::tenancy::tenants_list -help\ - "tenancy_tenants_list - GET request for endpoint /tenancy/tenants/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -id -type integer - -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -name - -NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_name_filter_help}} - -slug -type string - -SLUG_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_string_filter_help}} - -description -type string - -DESCRIPTION_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_string_filter_help}} - }\ - [set ::punk::netbox::argdoc::_create_update_options]\ - { - -q -type string - -tag -type string -multiple true -help\ - "If multiple -tag options supplied, - they are ANDed (review)" - -tag__n -type string - }\ - [set ::punk::netbox::argdoc::_contact_options]\ - { - }\ - { - }\ - [set ::punk::netbox::argdoc::_group_options]\ - { - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ - { - @values -min 0 -max 0 - }] - } - - ::punk::netbox::system::make_rest_func_async ::punk::netbox::tenancy::tenants_list api/tenancy/tenants/ -verb get -body none -} - -tcl::namespace::eval punk::netbox::virtualization { - namespace export {[a-z]*} - - namespace eval argdoc { - variable PUNKARGS - variable DYN_CONTEXTNAMES - set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_list - @cmd -name punk::netbox::virtualization::virtual-machines_list -help\ - "virtualization_virtual-machines_list - GET request for endpoint /virtualization/virtual-machines/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -id -type integer - -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -name - -NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_name_filter_help}} - -cluster -type string - -cluster_n -type string - -vcpus -type integer - -VCPUS_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -memory -type integer -help\ - "Whole number" - -MEMORY_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -disk -type integer - -DISK_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - }\ - [set ::punk::netbox::argdoc::_create_update_options]\ - { - -q - -tag -multiple true - }\ - [set ::punk::netbox::argdoc::_tenant_options]\ - [set ::punk::netbox::argdoc::_contact_options]\ - { - -local_context_data - -status - -status_n - -cluster_group_id - -cluster_group_id__n - -cluster_group - -cluster_group__n - -cluster_type_id - -cluster_type_id__n - -cluster_type - -cluster_type__n - -cluster_id - -cluster_id__n - }\ - [set ::punk::netbox::argdoc::_region_options]\ - [set ::punk::netbox::argdoc::_site_options]\ - { - -platform - -platform__n - -mac_address - -MAC_ADDRESS_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_string_filter_help}} - -has_primary_ip - }\ - [set ::punk::netbox::argdoc::_group_options]\ - [set ::punk::netbox::argdoc::_role_options]\ - { - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ - { - @values -min 0 -max 0 - }] - } - - ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_list api/virtualization/virtual-machines/ -verb get -body none - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_create - @cmd -name punk::netbox::virtualization::virtual-machines_create -help\ - "virtualization_virtual-machines_create - GET request for endpoint /virtualization/virtual-machines/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - body -type string -help\ - "JSON string" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_create api/virtualization/virtual-machines/ -verb post -body required - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_bulk_delete - @cmd -name punk::netbox::virtualization::virtual-machines_bulk_delete\ - -summary\ - "DELETE *ALL* virtual machines."\ - -help\ - "virtualization_virtual-machines_bulk_delete - DELETE request for endpoint /virtualization/virtual-machines/ - HTTP code: 204 - " - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -FORCE -default 0 -type boolean -help\ - "Set to true to BULK delete *all* items at this endpoint" - }\ - { - @values -min 0 -max 0 - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_bulk_delete api/virtualization/virtual-machines/ -verb delete -body none - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_read - @cmd -name punk::netbox::virtualization::virtual-machines_read -help\ - "virtualization_virtual-machines_read - GET request for endpoint /virtualization/virtual-machines/{id}" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying this virtual machine" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_read api/virtualization/virtual-machines/{id}/ -verb get -body none - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_update - @cmd -name punk::netbox::virtualization::virtual-machines_update -help\ - "virtualization_virtual-machines_update - PUT request for endpoint /virtualization/virtual-machines/{id}" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 2 -max 2 - id -type integer -help\ - "A unique integer value identifying this virtual machine" - body -type string -help\ - "JSON string" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_update api/virtualization/virtual-machines/{id}/ -verb put -body required - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_delete - @cmd -name punk::netbox::virtualization::virtual-machines_delete\ - -summary\ - "Delete a single VM by id."\ - -help\ - "virtualization_virtual-machines_delete - DELETE request for endpoint /virtualization/virtual-machines/{id}" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying the virtual machine - to DELETE." - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_delete api/virtualization/virtual-machines/{id}/ -verb delete -body none - -} - -tcl::namespace::eval punk::netbox::extras { - namespace export {[a-z]*} - - namespace eval argdoc { - variable PUNKARGS - variable DYN_CONTEXTNAMES - set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::extras::tags_list - @cmd -name punk::netbox::extras::tags_list -help\ - "extras_tags_list - GET request for endpoint /extras/tags/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -id -type integer - -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} - -name - -NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_name_filter_help}} - -slug -type string - -color -type string - -description -type string - }\ - [set ::punk::netbox::argdoc::_create_update_options]\ - { - -q - }\ - [set ::punk::netbox::argdoc::_page_options]\ - [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ - [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ - { - -ASYNC -type none - @values -min 0 -max 0 - }] - } - - ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_list api/extras/tags/ -verb get -body none - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::extras::tags_create - @cmd -name punk::netbox::extras::tags_create -help\ - "extras_tags_create - GET request for endpoint /extras/tags/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - -ASYNC -type none - @values -min 1 -max 1 - body -type string -help\ - "JSON string" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_create api/extras/tags/ -verb post -body required - - - set RESTAPI(create) { - url https://www.netbox1.intx.com.au/tags/create/ - method post - headers {Authorization {Token Bogus} content-type "application/json"} - callback {::punk::netbox::system::rest_call_complete RESTAPI} - body required - error-body false - } - ::rest::create_interface ::punk::netbox::extras::RESTAPI - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::extras::tags_bulk_delete - @cmd -name punk::netbox::extras::tags_bulk_delete -help\ - "extras_tags_bulk_delete - DELETE request for endpoint /extras/tags/ - HTTP code: 204 - " - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - -FORCE -default 0 -type boolean -help\ - "Set to true to BULK delete all items at this endpoint" - }\ - { - -ASYNC -type none - @values -min 0 -max 0 - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_bulk_delete api/extras/tags/ -verb delete -body none - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::extras::tags_read - @cmd -name punk::netbox::extras::tags_read -help\ - "extras_tags_read - GET request for endpoint /extras/tags/{id}" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying this tag" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_read api/extras/tags/{id}/ -verb get -body none - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::extras::tags_update - @cmd -name punk::netbox::extras::tags_update -help\ - "extras_tags_update - PUT request for endpoint /extras/tags/{id} - A PUT update is a complete update of the tag. - It will reset any non-supplied fields - in the body to their default" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 2 -max 2 - id -type integer -help\ - "A unique integer value identifying this tag" - body -type string -help\ - "JSON string" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_update api/extras/tags/{id}/ -verb put -body required - - - #namespace eval argdoc { - # lappend PUNKARGS [list\ - # { - # @dynamic - # @id -id ::punk::netbox::extras::tags_delete - # @cmd -name punk::netbox::extras::tags_delete -help\ - # "extras_tags_delete - # DELETE request for endpoint /extras/tags/{id} - # Deletes a specific tag identified by id." - # @leaders -min 1 -max 1 - # apicontextid -help\ - # "The name of the stored api context to use. - # A contextid can be created in-memory using - # api_context_create, or loaded from a .toml - # file using api_context_load."\ - # -choices {${$DYN_CONTEXTNAMES}} - # @opts - # }\ - # [set ::punk::netbox::argdoc::_RETURN_DICT]\ - # { - # @values -min 1 -max 1 - # id -type integer -help\ - # "A unique integer value identifying this tag" - # }] - #} - # 204 response with no JSON data on success - #The rest library will try to parse the empty string as json and fail - #it can parse "{}" but not "" - - - #404 for invalid id: - #allow: GET,PUT,PATCH,DELETE,HEAD,OPTIONS - #content-length: 23 - #content-type: application/json - #cross-origin-opener-policy: same-origin - #date: Tue,11 Nov 2025 17:46:04 GMT - #referrer-policy: same-origin - #server: nginx/1.26.2 - #vary: Accept,Cookie,Origin - #x-content-type-options: nosniff - #x-frame-options: SAMEORIGIN - - #{ - # "detail": "Not found." - #} - #::punk::netbox::system::make_rest_func ::punk::netbox::extras::tags_delete api/extras/tags/{id}/ -verb delete -body none - - - namespace eval argdoc { - lappend PUNKARGS [list\ - { - @dynamic - @id -id ::punk::netbox::extras::tags_delete - @cmd -name punk::netbox::extras::tags_delete -help\ - "extras_tags_delete - DELETE request for endpoint /extras/tags/{id} - Deletes a specific tag identified by id." - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - -ASYNC -type none - @values -min 1 -max 1 - id -type integer -help\ - "A unique integer value identifying this tag" - }] - } - ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_delete api/extras/tags/{id}/ -verb delete -body none - -} - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::netbox::lib { - tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - tcl::namespace::path [tcl::namespace::parent] - #*** !doctools - #[subsection {Namespace punk::netbox::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 punk::netbox::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - - - - -# == === === === === === === === === === === === === === === -# Sample 'about' function with punk::args documentation -# == === === === === === === === === === === === === === === -tcl::namespace::eval punk::netbox { - tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - variable PUNKARGS - variable PUNKARGS_aliases - - lappend PUNKARGS [list { - @id -id "(package)punk::netbox" - @package -name "punk::netbox" -help\ - "Package - Description" - }] - - namespace eval argdoc { - #namespace for custom argument documentation - proc package_name {} { - return punk::netbox - } - proc about_topics {} { - #info commands results are returned in an arbitrary order (like array keys) - set topic_funs [info commands [namespace current]::get_topic_*] - set about_topics [list] - foreach f $topic_funs { - set tail [namespace tail $f] - lappend about_topics [string range $tail [string length get_topic_] end] - } - #Adjust this function or 'default_topics' if a different order is required - return [lsort $about_topics] - } - proc default_topics {} {return [list Description *]} - - # ------------------------------------------------------------- - # get_topic_ functions add more to auto-include in about topicg - # ------------------------------------------------------------- - proc get_topic_Description {} { - punk::args::lib::tstr [string trim { - package punk::netbox - A library for calling netbox REST functions - } \n] - } - proc get_topic_License {} { - return "MIT" - } - proc get_topic_Version {} { - return "$::punk::netbox::version" - } - proc get_topic_Contributors {} { - set authors {{Julian Noble }} - set contributors "" - foreach a $authors { - append contributors $a \n - } - if {[string index $contributors end] eq "\n"} { - set contributors [string range $contributors 0 end-1] - } - return $contributors - } - proc get_topic_features {} { - punk::args::lib::tstr -return string { - netbox /status/ endpoint - beginnings of /ipam/ endpoints - beginnings of /virtualization/ endpoints - } - } - # ------------------------------------------------------------- - } - - # we re-use the argument definition from punk::args::standard_about and override some items - set overrides [dict create] - dict set overrides @id -id "::punk::netbox::about" - dict set overrides @cmd -name "punk::netbox::about" - dict set overrides @cmd -help [string trim [punk::args::lib::tstr { - About punk::netbox - }] \n] - dict set overrides topic -choices [list {*}[punk::netbox::argdoc::about_topics] *] - dict set overrides topic -choicerestricted 1 - dict set overrides topic -default [punk::netbox::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict - set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *] - lappend PUNKARGS [list $newdef] - proc about {args} { - package require punk::args - #standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on - set argd [punk::args::parse $args withid ::punk::netbox::about] - lassign [dict values $argd] _leaders opts values _received - punk::args::package::standard_about -package_about_namespace ::punk::netbox::argdoc {*}$opts {*}[dict get $values topic] - } -} -# end of sample 'about' function -# == === === === === === === === === === === === === === === - - -# ----------------------------------------------------------------------------- -# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked -# ----------------------------------------------------------------------------- -# variable PUNKARGS -# variable PUNKARGS_aliases -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES\ - ::punk::netbox\ - ::punk::netbox::dcim\ - ::punk::netbox::ipam\ - ::punk::netbox::tenancy\ - ::punk::netbox::virtualization\ - ::punk::netbox::extras\ -} -# ----------------------------------------------------------------------------- - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide punk::netbox [tcl::namespace::eval punk::netbox { - variable pkg punk::netbox - variable version - set version 0.1.0 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.1.tm index 196dba66..9d7e2314 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.1.tm @@ -2471,7 +2471,7 @@ tcl::namespace::eval punk::netbox::tenancy { namespace eval argdoc { variable PUNKARGS set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} - lappend PUNKARGS [list {*} { + lappend PUNKARGS [list {*}{ } { @dynamic @id -id ::punk::netbox::tenancy::tenants_list diff --git a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm index dc8c765c..db6acbb4 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm @@ -2167,8 +2167,13 @@ y" {return quirkykeyscript} puts stdout "leaving $target" puts stdout "call $commandstring\x1b\[m" - puts stdout "result:" - puts stdout $result + if {$code == 0} { + puts stdout "result:" + puts stdout $result + } else { + puts stdout "error message:" + puts stdout $::errorInfo + } puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset set cmdtype [dict get $linedict $target cmdtype] @@ -2647,6 +2652,8 @@ y" {return quirkykeyscript} upvar ::punk::ns::linedict linedict set ::punk::ns::linedict [::tcl::dict::create] + set body_cache [tcl::dict::create] + set resolved_targets [list] foreach tgt $targets { set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]] @@ -4179,7 +4186,7 @@ y" {return quirkykeyscript} #this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces) #we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace. nseval $targetns [list apply [list {procname argname} { - set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]] + set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]] if {$has_default} { set answer [dict create exists 1 default $::punk::ns::corp_defvar] } else { @@ -5018,6 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs + #puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -6618,18 +6626,19 @@ y" {return quirkykeyscript} separately calling 'info args ' 'info body ' etc. The body may display with an additional - comment inserted to display information such as the + comment inserted above the proc line to display information such as the namespace origin. Such a comment begins with #corp#. - Returns a list: proc - (as long as any syntax highlighter is written to - avoid breaking the structure. e.g by avoiding the - insertion of ANSI between an escaping backslash and - its target character) + Returns a string: proc If the output is to be used as a script to regenerate a procedure, '-syntax none' should be used to avoid ANSI colours, or the resulting arglist and body should be run through 'ansistrip'. + + (any syntax highlighter should be written to + avoid breaking the structure. e.g by avoiding the + insertion of ANSI between an escaping backslash and + its target character) " @opts #todo - make definition @dynamic - load highlighters as functions? @@ -6647,7 +6656,13 @@ y" {return quirkykeyscript} "Whether to replace tabs in the body with spaces or a visible Unicode symbol." -ranges -type indexset -default "0..end" -help\ "comma delimited set of line ranges. - Restrict output to the specified line ranges of the body. Lines are numbered starting at 1." + Restrict output to the specified line ranges of the body. Lines are numbered starting at 1. + For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body. + The special index 0 is used to specify the line before the first line of the body, + which is where the #corp# info comment is placed if it exists. + So the default range 0..end includes the info comment and all lines of the body. + Specifying -ranges 1..5,0 would include the info comment at the end of the output. + " -syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\ -choicelabels { none @@ -6690,11 +6705,6 @@ y" {return quirkykeyscript} set indent [string repeat " " $tw] ;#match #set indent [string repeat " " $tw] ;#A more sensible default for code - review - if {[info exists ::auto_index($path)]} { - set infoheader "\n${indent}#corp# auto_index $::auto_index($path)" - } else { - set infoheader "" - } #we want to handle edge cases of commands such as "" or :x #various builtins such as 'namespace which' won't work @@ -6753,13 +6763,29 @@ y" {return quirkykeyscript} return [list alias {*}$alias] } } - if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} { - append infoheader \n "${indent}#corp# namespace origin $origin" - } - if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { - append infoheader \n + #-------------------------------------------------------------------------- + if {[info exists ::auto_index($path)]} { + #set infoheader "${indent}#corp# auto_index $::auto_index($path)" + set infoheader "#corp# auto_index $::auto_index($path)" + } else { + set infoheader "" } + #puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'" + #if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {} + if {$origin ne $targetcmd} { + #append infoheader "${indent}#corp# namespace origin $origin" + append infoheader "#corp# namespace origin $origin" + } + if {$infoheader ne "" && $syntax eq "basic"} { + set infoheader [ansiwrap green $infoheader] + } + #if {$infoheader ne "" && [string index $infoheader end] ne "\n"} { + # append infoheader \n + #} + #-------------------------------------------------------------------------- + + set body "" #set bodytext [info body $origin] #relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname @@ -6851,47 +6877,126 @@ y" {return quirkykeyscript} } } - if {$ranges ni {"0..end" ".." "0.."}} { - set lines [split $body \n] - set linecount [llength $lines] + set lines [split $body \n] + set linecount [llength $lines] + + + #------------------------------------------------------------------------------------------------ + #When we resolve our 1-based indexset - the zero index used to specify the info comment is lost. + #we need to search for it manually and add it back in if it's in the specified ranges. + set rangelist [split $ranges ,] + set info_positions [list] + set lnum 0 + foreach range $rangelist { + lassign [split $range ..] start _ end + set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range] + if {$start eq "0"} { + lappend info_positions $lnum + } + incr lnum [llength $r_indices] + #if {$end eq "0"} { + # #ignore + #} + } + #puts "info_positions: $info_positions" + #------------------------------------------------------------------------------------------------ + + + set body "" + if {[lindex $info_positions 0] == 0 && $infoheader ne ""} { + append body "$infoheader" \n + } + + if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} { set w [string length $linecount] set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges] - set body "" + set outputlines [llength $indices] + if {$do_ln} { - set n 0 foreach idx $indices { - append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + if {$idx == 1} { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}" + } else { + append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } else { foreach idx $indices { - append body [lindex $lines $idx-1] \n + if {$idx == 1} { + set ln1 "proc $resolved [list $argl] \{" + append body "$ln1[lindex $lines 0]" + if {$linecount == 1} { + append body "\}" + } else { + append body "\n" + } + } elseif {$idx == $linecount} { + append body [lindex $lines end] "\}" + } else { + append body [lindex $lines $idx-1] \n + } + if {[set p [lsearch $info_positions $idx]] >= 0} { + append body $infoheader \n + set info_positions [lremove $info_positions $p] + } } } + #no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match + if {[string index $body end] eq "\n"} { + set body [string range $body 0 end-1] + } } else { + #range was specified in a standard way to mean 'all lines' + set outputlines $linecount if {$do_ln} { - set linebody "" - set n 0 - set lines [split $body \n] - set linecount [llength $lines] set w [string length $linecount] - foreach ln $lines { + set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + set n 2 + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$lnc[format %${w}s $n]$lnr $ln" incr n - append linebody "$lnc[format %${w}s $n]$lnr $ln" \n } - set body [string range $linebody 0 end-1] - #set body $linebody + if {$linecount > 1} { + append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody + } else { + set ln1 "proc $resolved [list $argl] \{" + set linebody "$ln1[lindex $lines 0]" + foreach ln [lrange $lines 1 end-1] { + append linebody \n "$ln" + } + if {$linecount > 1} { + append linebody \n "[lindex $lines end]\}" + } else { + append linebody "\}" + } + append body $linebody } } - - - if {$is_highlighted} { - #ansi colourised items in list format may not always have desired string representation (list escaping can occur) - #return as a string - which may not be a proper Tcl list! - return "proc $resolved {$argl} {\n$infoheader$body\n}" - } else { - list proc $resolved $argl $infoheader$body + #ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end? + if {[lindex $info_positions end] > 0} { + if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} { + append body "\n$infoheader" + } } + return $body } @@ -7086,6 +7191,7 @@ y" {return quirkykeyscript} nstest eval {package require punk::ns} set ns "" if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + #review set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] set ns [nstest eval $script] } else { diff --git a/src/vfs/_vfscommon.vfs/modules/punk/packagepreference-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/packagepreference-0.1.0.tm index e45cefa7..87e33d8c 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/packagepreference-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/packagepreference-0.1.0.tm @@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference { #[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names #[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall + if {![catch {commandstack::get_stack} cstack]} { + if {[dict exists $cstack ::package]} { + set pstack [dict get $cstack ::package] + foreach record $pstack { + if {[dict get $record rename] eq "punk::packagepreference"} { + #already installed - silently ignore. + return 0 + } + } + } + } + + #todo - review/update commandstack package #modern module/lib names should preferably be lower case #see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9) @@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference { if {[llength $pkgloadedinfo]} { if {[llength $available_versions] > 1} { - puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + #puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid" } lassign $pkgloadedinfo loaded_path name set lc_loadedpath [string tolower $loaded_path] @@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference { puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg" } } else { - puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid" } } #--------------------------------------------------------------- diff --git a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.1.tm deleted file mode 100644 index 9e909f40..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.1.tm +++ /dev/null @@ -1,3582 +0,0 @@ - -#punk linerepl -#todo - make repls configurable/pluggable packages - - -#list/string-rep bug -global run_commandstr "" - -set stdin_info [chan configure stdin] -if {[dict exists $stdin_info -inputmode]} { - #this is the only way I currently know to detect console on windows.. doesn't work on Alma linux. - # tcl_interactive used by repl to determine if stderr output prompt to be printed. - # (that way, piping commands into stdin should not produce prompts for each command) - set tcl_interactive 1 -} -#however, the -mode option only seems to appear on linux when a terminal exists.. -if {[dict exists $stdin_info -mode]} { - set tcl_interactive 1 -} -#give up for now -set tcl_interactive 1 - -#if {[info commands ::tcl::zipfs::root] ne ""} { -# set zr [::tcl::zipfs::root] -# if {[file join $zr app modules] in [tcl::tm::list]} { -# #todo - better way to find latest version - without package require -# set lib [file join $zr app modules punk libunknown.tm] -# if {[file exists $lib]} { -# source $lib -# punk::libunknown::init -# #package unknown {punk::libunknown::zipfs_tm_UnknownHandler punk::libunknown::zipfs_tclPkgUnknown} -# } -# } -#} -#------------------------------------------------------------------------------------- -if {[package provide punk::libunknown] eq ""} { - #maintenance - also in src/vfs/_config/punk_main.tcl - set libunks [list] - foreach tm_path [tcl::tm::list] { - set punkdir [file join $tm_path punk] - if {![file exists $punkdir]} {continue} - lappend libunks {*}[glob -nocomplain -dir $punkdir -type f libunknown-*.tm] - } - set libunknown "" - set libunknown_version_sofar "" - foreach lib $libunks { - #expecting to be of form libunknown-.tm - set vtail [lindex [split [file tail $lib] -] 1] - set thisver [file rootname $vtail] ;#file rootname x.y.z.tm - if {$libunknown_version_sofar eq ""} { - set libunknown_version_sofar $thisver - set libunknown $lib - } else { - if {[package vcompare $thisver $libunknown_version_sofar] == 1} { - set libunknown_version_sofar $thisver - set libunknown $lib - } - } - } - if {$libunknown ne ""} { - source $libunknown - if {[catch {punk::libunknown::init -caller repl} errM]} { - puts "error initialising punk::libunknown\n$errM" - } - } -} else { - #This should be reasonably common - a punk shell will generally have libunknown loaded - # but to start a subshell we load punk::repl - #puts stderr "loading repl package - punk::libunknown [package provide punk::libunknown] already loaded" -} -#------------------------------------------------------------------------------------- - - - - -package require Thread -package require shellfilter -#package require shellrun -#package require punk -package require punk::lib -package require punk::args -package require punk::aliascore -if {[catch {punk::aliascore::init} errM]} { - puts stderr "punk::aliascore::init error: $errM" -} -package require punk::config -package require punk::ns -package require punk::ansi -package require punk::console -package require textblock - - - - - -if {![info exists ::env(SHELL)]} { - set ::env(SHELL) punk86 -} -if {![info exists ::env(TERM)]} { - # tset -r seems to rely on env(TERM) - so this doesn't seem to work - #if {![catch {exec tset -r} result]} { - # #e.g Terminal type is xterm-256color. - # set t [string trimright [lindex $result end] .] - # set ::env(TERM) $t - #} else { - #fake it ? - #set ::env(TERM) vt100 - set ::env(TERM) xterm-256color - #} -} - -#todo - move to less generic namespace ie punk::repl -namespace eval repl { - variable codethread - if {![info exists codethread]} { - set codethread "" - } - variable codethread_cond - - variable screen_last_chars "" ;#a small sliding append buffer for last char of any screen output to detect \n vs string - variable screen_last_char_list [list] - - #variable last_unknown "" - tsv::set repl last_unknown "" - tsv::set console is_raw 0 - variable output "" - #important not to initialize - as it can be preset by cooperating package before app-punk has been package required - #(this is an example of a deaddrop) - variable post_script -} -namespace eval punk::repl::class { - oo::class create con { - variable o_data ;#dict - constructor {} { - set o_data [dict create] - } - method info {} { - return [dict info $o_data] - } - } -} -namespace eval punk::repl { - tsv::set repl runid 0 - - #todo - key on shell/subshell - tsv::set repl runchunks-0 [list] ;#last_run_display - - - variable debug_repl 0 - variable signal_control_c 0 - variable signal_control_c_msg "" - variable prompt_reset_flag 0 ;#trigger repl to re-retrieve prompt settings - - proc todo {} { - puts "tcl History" - puts "repltelemetry package" - puts "deaddrop package for a consistent way for modules to leave small notes to others that may come later." - } - - #since we are targeting Tcl 8.6+ - we should be using 'interp bgerror .' - #todo - make optional/configurable? - proc bgerror2 {args} { - puts stderr "====================" - puts stderr "punk::repl::bgerror" - puts stderr "====================" - puts stderr "[lindex $args 0]" - puts stderr "-------------------" - puts stderr "[lrange $args 1 end]" - puts stderr "====================" - puts stderr "^^^^^^^^^^^^^^^^^^^" - } - proc bgerror {args} { - set message [lindex $args 0] - set errdict [lindex $args 1] - puts stderr "\n*> repl background error: '$message'" - #puts stderr "*> [set ::errorInfo]" - puts stderr "*> errorinfo: [dict get $errdict -errorinfo]" - set stdinreader [chan event stdin readable] - if {![string length $stdinreader]} { - puts stderr "*> stdin reader inactive" - } else { - puts stderr "*> stdin reader active" - } - flush stderr - } - - if {![llength [info commands ::bgerror]]} { - #interp alias {} bgerror {} ::punk::repl::bgerror - } - interp bgerror "" ::punk::repl::bgerror - -} -namespace eval repl { - -} - -proc ::punk::repl::init_signal_handlers {} { - if {$::tcl_platform(platform) eq "windows"} { - #puts stdout "===============repl loading twapi===========" - if {![catch {package require twapi}]} { - - #If script launched with windows batch file - we have to be careful to stop a ctrl-c from eventually reaching the batch file when the program terminates, even if fully handled here. - #This is done from within the launching batch file - proc ::punk::repl::handler_console_control {args} { - variable signal_control_c - flush stderr - variable signal_control_c_msg - switch -- [lindex $args 0] { - ctrl-c { - #puts stderr "->event $args" - flush stderr - incr signal_control_c - #rputs stderr "* console_control: $args" - if {[tsv::get console is_raw]} { - if {[lindex $::errorCode 0] eq "CHILDKILLED"} { - #rputs stderr "\n|repl> ctrl-c errorCode: $::errorCode" - #avoid spurious triggers after interrupting a command.. - #review - dodgy.. we just want to interrupt child processes but then still be able to interrupt repl - set ::punk::repl::signal_control_c 0 - set preverr [string map {"child killed" "child_killed"} $::errorInfo] - catch {error $preverr} ;#for errorInfo display - return 42 - } else { - #how to let rawmode loop handle it? It doesn't seem to get through if we return 0 - #puts stderr "signal ctrl-c while in raw mode" - #flush stderr - set signal_control_c_msg "signal ctrl-c $signal_control_c rawmode" - if {[catch { - lassign [punk::console::get_size] _w console_width _h console_height - } errM]} { - puts stderr "signal ctrl-c error get_size error:$errM" - } - - if {$signal_control_c < 3} { - set remaining [expr {3 - $signal_control_c}] - if {[catch { - punk::repl::console_controlnotification "[a+ web-orange]ctrl-c ($remaining more to quit, enter to continue)[a]" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - } elseif {$signal_control_c == 3} { - #puts stderr "signal ctrl-c x3 received - quitting." - if {[catch { - punk::repl::console_controlnotification "ctrl-c x3 received - quitting punk shell" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - flush stderr - after 25 - quit - return 1 - } elseif {$signal_control_c > 5} { - #fallback if quit didn't work - #puts stderr "signal ctrl-c $signal_control_c received - sending to default handler" - if {[catch { - punk::repl::console_controlnotification "ctrl-c $signal_control_c received - sending to default handler" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - flush stderr - punk::console::mode line - return 0 - } - - return 1 - #after 200 {exit 42} ;#temp - #return 42 - } - } - - - if {[lindex $::errorCode 0] eq "CHILDKILLED"} { - set signal_control_c 0 - set preverr [string map {"child killed" "child_killed"} $::errorInfo] - catch {error $preverr} ;#for errorInfo display - return 42 - } - if {[catch { - lassign [punk::console::get_size] _w console_width _h console_height - } errM]} { - puts stderr "signal ctrl-c error get_size error:$errM" - } - - #note - returning 0 means pass event to other handlers including OS default handler - if {$signal_control_c <= 2} { - set remaining [expr {3 - $signal_control_c}] - #puts stderr "signal ctrl-c (perform $remaining more to quit, enter to return to repl)" - #flush stderr - if {[catch { - punk::repl::console_controlnotification "ctrl-c ($remaining more to quit, enter to continue)" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - return 1 - } elseif {$signal_control_c == 3} { - #puts stderr "signal ctrl-c x3 received - quitting." - if {[catch { - punk::repl::console_controlnotification "ctrl-c x3 received - quitting punk shell" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - flush stderr - after 25 - quit - return 1 - } elseif {$signal_control_c == 4} { - puts stderr "signal ctrl-c x4 received - one more to hard exit" - flush stderr - return 1 - } elseif {$signal_control_c >= 5} { - #a script that allows events to be processed could still be running - puts stderr "signal ctrl-c x5 received - hard exit" - flush stderr - after 25 - exit 499 ;# HTTP 'client closed request' - just for the hell of it. - } else { - puts stderr "signal ctrl-c $signal_control_c received" - flush stderr - #return 0 to fall through to default handler - return 0 - } - - } - default { - puts stderr "unhandled console signal $args" - return 1 - } - } - } - twapi::set_console_control_handler ::punk::repl::handler_console_control - #we can't yet emit from an event with proper prompt handling - - #repl::rputs stdout "twapi loaded" - } else { - #repl::rputs stderr " Failed to load twapi" - } - } else { - #TODO - } -} - -#console handler may already be set, but in another thread/interp - so we can't use existence of proc to test -#we're ok with an existing handler - just catch for now. REVIEW we should make sure it didn't fail the first time -catch {punk::repl::init_signal_handlers} - - -# moved to punk package.. -#set outdevice [shellfilter::stack::new punkout -settings [list -tag "punkout" -buffering none -raw 1 -syslog [dict get $::punk::config::running syslog_stdout] -file [dict get $::punk::config::running logfile_stdout]]] -#set out [dict get $outdevice localchan] -#set errdevice [shellfilter::stack::new punkerr -settings [list -tag "punkerr" -buffering none -raw 1 -syslog [dict get $::punk::config::running syslog_stderr] -file [dict get $::punk::config::running logfile_stderr]]] -#set err [dict get $errdevice localchan] - - - -# -#set indevice [shellfilter::stack::new commandin -settings {-tag "commandin" -readbuffering line -writebuffering none -raw 1 -direction in}] -#set program_read_stdin_pipe [dict get $indevice localchan] - - - - - -proc punk::repl::reset_prompt {} { - variable prompt_reset_flag - set prompt_reset_flag 1 -} - -#aliases c and clear to this by ::punk -proc punk::repl::reset_terminal {} { - set prompt_reset_flag 1 - #clear ;#call to external executable which may not be available - puts -nonewline stdout [::punk::ansi::reset] -} - -proc punk::repl::get_prompt_config {} { - if {[catch {punk::console::vt52} is_vt52]} { - set is_vt52 0 - } - if {$is_vt52} { - set resultprompt "52-" - set nlprompt "52." - set infoprompt "52*" - set debugprompt "52~" - } else { - if {$::tcl_interactive} { - set RST [a] - set resultprompt "[a green bold]-$RST " - set nlprompt "[a green bold].$RST " - set infoprompt "[a green bold]*$RST " - set debugprompt "[a purple bold]~$RST " - } else { - set resultprompt "" - set nlprompt "" - set infoprompt "" - set debugprompt "" - } - } - return [list resultprompt $resultprompt nlprompt $nlprompt infoprompt $infoprompt debugprompt $debugprompt] -} - -proc repl::start {inchan args} { - puts stderr "-->repl::start $inchan $args" - upvar ::punk::console::input_chunks_waiting input_chunks_waiting - if {![info exists input_chunks_waiting($inchan)]} { - set input_chunks_waiting($inchan) [list] - } - - variable codethread - #review - if {$codethread eq ""} { - error "start - no codethread. call init first. (options -safe 0|1)" - } - variable commandstr - - # --- - variable editbuf - variable editbuf_list ;#command history - variable editbuf_linenum_submitted - variable editbuf_active_index - # --- - - variable reading - variable done - set done 0 - variable startinstance - variable loopinstance - if {[namespace exists ::punkapp]} { - #review - document ? - if {[dict exists $args -defaultresult]} { - set ::punkapp::default_result [dict get $args -defaultresult] - } - } - incr startinstance - set loopinstance 0 - if {[info exists ::punk::ns::ns_current]} { - set start_in_ns $::punk::ns::ns_current - } else { - set start_in_ns :: - } - thread::send $codethread [string map [list %ns1% $start_in_ns] { - #set ::punk::repl::codethread::running 1 - - #the interp in which commands such as d/ run - #we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values - #punk::repl::codethread::running is required whether safe or not. - interp eval code { - namespace eval ::punk::repl::codethread {} - set ::punk::repl::codethread::running 1 - namespace eval ::punk::ns::ns_current {} - set ::punk::ns::ns_current %ns1% - } - }] - set commandstr "" - - # --- - set editbuf [punk::repl::class::class_editbuf new {}] - lappend editbuf_list $editbuf ;#current editbuf is always in the history - set editbuf_linenum_submitted 0 - set editbuf_active_index 0 - # --- - - if {$::punk::console::ansi_wanted == 2} { - if {[::punk::console::test_can_ansi]} { - set ::punk::console::ansi_wanted 1 - } else { - set ::punk::console::ansi_wanted -1 - } - } - puts stderr "-->repl::start active on $inchan $args replthread:[thread::id] codethread:$codethread" - set prompt_config [punk::repl::get_prompt_config] - doprompt "P% " - chan event $inchan readable [list [namespace current]::repl_handler $inchan $prompt_config] - set reading 1 - - #catch { - # set punk::console::tabwidth [punk::console::get_tabstop_apparent_width] - #} - vwait [namespace current]::done - chan event $inchan readable {} - - - #puts stderr "-->start done = $::repl::done" - #todo - override exit? - #after 0 ::repl::post_operations - after idle ::repl::post_operations - vwait repl::post_operations_done - #puts stderr "-->start post_operations_done = $::repl::post_operations_done" - if {[namespace exists ::punkapp]} { - #todo check and get punkapp::result array - but what key? - if {[info exists ::punkapp::result(shell)]} { - set temp $::punkapp::result(shell) - unset ::punkapp::result(shell) - return $temp - } elseif {[info exists ::punkapp::default_result]} { - set temp $::punkapp::default_result - unset ::punkapp::default_result - return $temp - } - } - - - variable codethread_cond - if {[catch { - tsv::unset codethread_$codethread - } errM]} { - puts stderr " repl::start temp warning - $errM" - } - thread::cancel $codethread - thread::cond destroy $codethread_cond ;#race if we destroy cond before child thread has exited - as it can send a -async quit - set codethread "" - set codethread_cond "" - punk::console::mode line ;#review - revert to line mode on final exit - but we may be exiting a nested repl - puts "end repl::start" - return 0 -} -proc repl::post_operations {} { - if {[info exists ::repl::post_script] && [string length $::repl::post_script]} { - #put aside post_script so the script has the option to add another post_script and restart the repl - set ::repl::running_script $::repl::post_script - set ::repl::post_script "" - uplevel #0 {eval $::repl::running_script} - } - #todo - tidyup so repl could be restarted - set ::repl::post_operations_done 0 -} - - -proc repl::reopen_stdin {} { - #variable reopen_stdin_attempts - if {$::tcl_platform(platform) eq "windows"} { - puts stderr "|repl> Attempting reconnection of console to stdin by opening 'CON'" - } else { - puts stderr "|repl> Attempting reconnection of console to stdin by opening '/dev/tty'" - } - #puts stderr "channels:[chan names]" - #flush stderr - #catch {chan close stdin} - chan close stdin - - if {$::tcl_platform(platform) eq "windows"} { - #set s [open "CON" r] - set s [open {CONIN$} r] - if {[package provide twapi] ne ""} { - set h [twapi::get_tcl_channel_handle $s in] - twapi::SetStdHandle -10 $h - } - puts stderr "restarting repl on inputchannel:$s" - return [repl::start $s -title "reopen_stdin a"] - } else { - #/dev/tty - reference to the controlling terminal for a process - #review/test - set s [open "/dev/tty" r] - } - - repl::start stdin -title "reopen_stdin b" -} - -#todo - avoid putting this in gobal namespace? -#collisions with other libraries apps? -proc punk::repl::quit {args} { - set ::repl::done "quit {*}$args" - #puts stderr "quit called" - return "" ;#make sure to return nothing so "quit" doesn't land on stdout -} - -#just a failed experiment.. tried various things -proc repl::reopen_stdinX {} { - #windows - todo unix - package require twapi - - if 0 { - if {[catch {package require Memchan} errM]} { - #package require tcl::chan::fifo2 - #lassign [tcl::chan::fifo2] a b - package require tcl::chan::fifo - set x [tcl::chan::fifo] - } else { - #lassign [fifo2] a b - set x [fifo] - } - #first channel opened after stdin closed becomes stdin - #use a fifo or fifo2 because [chan pipe] assigns the wrong end first! - #a will be stdin - } - #these can't replace proper stdin (filehandle 0) because they're not 'file backed' or 'os level' - #try opening a named pipe server to become stdin - set pipename {\\.\pipe\stdin_%id%} - set pipename [string map [list %id% [pid]] $pipename] - - - - package require tcl::chan::fifo - - chan close stdin - lassign [tcl::chan::fifo] a - - - puts stderr "newchan: $a" - puts stderr "|test> $a [chan conf $a]" - - #set server [twapi::namedpipe_server $pipename] - #set client [twapi::namedpipe_client $pipename] ;#open a client and connect to the server we just made - - puts stderr "chan names: [chan names]" - - #by now $server not valid? - #set server stdin - - #chan configure $server -buffering line -encoding unicode - #chan configure $client -buffering line -encoding unicode - - #puts stderr "|test>ns-server $server [chan conf $server]" - #puts stderr "|test>ns-client $client [chan conf $client]" - - set conin [twapi::get_console_handle stdin] - twapi::set_standard_handle stdin $conin - - set h_in [twapi::get_standard_handle stdin] - - puts stderr "|test> $a [chan conf $a]" - - #chan configure $client -blocking 0 - after 2 repl::start $a - -} - -#add to sliding buffer of last x chars emmitted to screen by repl -#(we could maintain only one char - more kept merely for debug assistance) -#will not detect emissions from exec with stdout redirected and presumably some extensions etc -proc repl::screen_last_char_add {c what {why ""}} { - variable screen_last_chars - variable screen_last_char_list - if {![string length $c]} { - return [string index $screen_last_chars end] - } - if {[string length $screen_last_chars] > 10} { - set screen_last_chars [string range $screen_last_chars 1 end] ;#evict first char - set screen_last_char_list [lrange $screen_last_char_list 1 end] - } - append screen_last_chars $c - lappend screen_last_char_list [list $c $what $why] - #return [string index $screen_last_chars end] - #return [lindex $screen_last_char_list 0 0] - return [lindex $screen_last_char_list end 0] -} -proc repl::screen_last_char_get {} { - variable screen_last_char_list - return [lindex $screen_last_char_list end 0] -} -proc repl::screen_last_char_getinfo {} { - variable screen_last_char_list - return [lindex $screen_last_char_list end] -} - -#-------------------------------------- -#another experiment -proc repl::newout {} { - namespace eval ::replout { - namespace ensemble create -map { - initialize init - finalize close - watch watch - write write - } - } - proc ::replout::init {id mode} { - return {initialize finalize watch write} - } - proc ::replout::close {id} { - - } - proc ::replout::watch {id spec} { - - } - proc ::replout::write {id data} { - puts -nonewline stderr $data - return [string length $data] - } - - close stdout - set fd [chan create write ::replout] - chan configure $fd -buffering none - return $fd -} -interp alias {} newout {} repl::newout -proc repl::newout2 {} { - close stdout - set s [open "CON" w] - chan configure $s -buffering none -} -#-------------------------------------- - -proc repl::doprompt {prompt {col {green bold}}} { - #prompt to stderr. - #We can pipe commands into repl's stdin without the prompt interfering with the output. - #Although all command output for each line goes to stdout - not just what is emitted with puts - - if {$::tcl_interactive} { - flush stdout; #we are writing this prompt on stderr, but stdout could still be writing to screen - #our first char on stderr is based on the 'lastchar' of stdout which we have recorded but may not have arrived on screen. - #The issue we're trying to avoid is the (stderr)prompt arriving midway through a large stdout chunk - #REVIEW - this basic attempt to get stderr/stdout to cooperate is experimental and unlikely to achieve the desired effect in all situations - #It the above flush does seem to help though. - #note that our 'flush stdout' tcl call does not wait if stdout is non-blocking - #todo - investigate if the overhead is reasonable for a special channel that accepts stdout and stderr records with a reader to send to console in chunk-sizes we know will be emitted correctly - # - reader of such channel could be ok to be blocking (on read? on write to real channels?)... except everything still needs to be interruptable by things like signals? - #? - we want ordinary puts to stderr to be prioritized? to arrive on-screen - just not at arbitrary locations within stdout, and still must be correctly ordered wrt all other stderr - # - in our repl and code threads we don't want to put stderr/stdout writes in blocking mode and have code waiting on it - - set last_char_info [screen_last_char_getinfo] - if {![llength $last_char_info]} { - set needs_clearance 1 - } else { - lassign $last_char_info c what why - if {$why eq "prompt"} { - set needs_clearance 0 - } else { - set needs_clearance [screen_needs_clearance] - #puts -nonewline "-->$needs_clearance $last_char_info" - } - } - if {$needs_clearance == 1} { - set c \n - } else { - set c "" - } - set pre "" - if {[string first \n $prompt] >=0} { - set plines [split $prompt \n] - set pre [join [lrange $plines 0 end-1] \n]\n - set prompt [lindex $plines end] - } - - #this sort of works - but steals some of our stdin data ? review - # - #lassign [punk::console::get_cursor_pos_list] column row - #if {$row != 1} { - # set c "\n" - #} - - set o [a {*}$col] - set r [a] - puts -nonewline stderr $c$pre$o$prompt$r - screen_last_char_add " " "prompt-stderr" prompt - flush stderr - } -} - -#use rputs in repl_handler instead of puts -# - to help ensure we don't emit extra blank lines in info or debug output -#rputs expects the standard tcl 'puts' command to be in place. -# all bets are off if this has been redefined with some other api -# rputs deliberately doesn't check screen_last_chars before emitting data (unless reporting an error in rputs itself) -proc repl::rputs {args} { - variable screen_last_chars - variable last_out_was_newline - variable last_repl_char - - set pseudo_map [dict create\ - debug stderr\ - debugreport stderr\ - ] - - if {[::tcl::mathop::<= 1 [llength $args] 3]} { - set out [lindex $args end] - append out ""; #copy on write - if {([llength $args] > 1) && [lindex $args 0] ne "-nonewline"} { - set this_tail \n - set rputschan [lindex $args 0] - #map pseudo-channels to real - if {$rputschan in [dict keys $pseudo_map]} { - lset args 0 [dict get $pseudo_map $rputschan] - } - } elseif {[llength $args] == 1} { - set this_tail \n - set rputschan "stdout" - } else { - #>1 arg with -nonewline - set this_tail [string index $out end] - set rputschan [lindex $args 1] - #map pseudo-channels to real - if {$rputschan in [dict keys $pseudo_map]} { - lset args 0 [dict get $pseudo_map $rputschan] - } - } - set last_char_info_width 60 - #review - string shouldn't be truncated prior to stripcodes - could chop ansi codes! - #set summary "[::shellfilter::ansi::stripcodes [string range $out 0 $last_char_info_width]]" - set out_plain_text [punk::ansi::ansistrip $out] - set summary [string range $out_plain_text 0 $last_char_info_width] - if {[string length $summary] > $last_char_info_width} { - append summary " ..." - } - - #make sure we use supplied rputschan in the screen_las_char_add 'what' - which may not be the underlying chan if it was a pseudo - screen_last_char_add $this_tail repl-$rputschan $summary - - try { - puts {*}$args - } on error {repl_error erropts} { - #possible error depending on -encoding and -profile of the channel - #(e.g -profile strict) - - #REVIEW - #TODO - something better - #failure case: - #set x \ud83c\udf1e - #(2 surrogate pairs - treated as single char in tcl8 - fixed in 9 but won't/can't be backported) - - #see also: https://core.tcl-lang.org/tips/doc/trunk/tip/619.md - puts stderr "$repl_error" - } - } else { - #looks like an invalid puts call - use the normal error produced by the puts command - #This should only occur if the repl itself is being rewritten/debugged, - #so we will use red "!" and not worry about the extra newlines before and after - if {[catch { puts {*}$args } err]} { - set c [a yellow bold] - set n [a] - #possibly non punk-compliant output because we're assuming the repl was the most recent emitter - #could be wrong, in which case we may emit an extra newline - #- shouldn't matter in this case - #set last_char [string range $screen_last_chars end] - set last_char [screen_last_char_get] - if {$last_char eq "\n"} { - set clear "" - } else { - set clear "\n" - } - puts -nonewline stderr "$clear[a red bold]! REPL ERROR IN rputs $c$err$n\n" - screen_last_char_add "\n" replerror "rputs err: '$err'" - return - } else { - #?? shouldn't happen with standard puts command - #do our best and assume final arg is still the data being emitted - #worst that will happen is we won't detect a trailing newline and will later emit an extra blank line. - set out [lindex $args end] - set this_tail [string index $out end] - screen_last_char_add $this_tail replunknown "rputs $args" - return - } - } -} -#whether we need a newline as clearance from previous output -#review - race with copy pasted data, hold-down of enter key -# and data from external process or background script that doesn't go through our stdout filter -#we probably can't use get_cursor_pos - as that needs to emit to stdout and read-loop on stdin which will possibly? make things worse -proc repl::screen_needs_clearance {} { - variable screen_last_chars - - #set last_char [string index $screen_last_chars end] - set last_char_info [screen_last_char_getinfo] - if {![llength $last_char_info]} { - #assumption - return 1 - } - lassign $last_char_info c what why - switch -- $what { - stdout - stderr - stdout/stderr { - return 1 - } - } - return [expr {$c ne "\n"}] -} - -namespace eval repl { - variable startinstance 0 - variable loopinstance 0 - variable in_repl_handler [list] - variable last_controlc_count 0 -} - -namespace eval punk::repl::class { - oo::class create class_bufman { - - } - - #multiline editing buffer - oo::class create class_editbuf { - variable o_context - variable o_config - - variable o_rendered_lines - variable o_remaining ;#? - - #o_chunk_list & o_chunk_info should make timed viewing of replays possible - variable o_chunk_list - variable o_chunk_info ;#arrival timing etc - variable o_cursor_row - variable o_cursor_col - variable o_insert_mode - constructor {configdict {contextdict {}}} { - my clear - set o_config $configdict - if {[dict exists $configdict rendered_initialchunk]} { - #pre-rendered initial chunk - #-- - set o_chunk_list "" ;#replace empty chunk from 'clear' call - set o_chunk_info [dict create] - #-- - set ch [dict get $configdict rendered_initialchunk] - my add_rendered_chunk $ch - } - - set o_context $contextdict - #error "[self class].constructor Unable to interpret config '$o_config'" - } - method cursor_row {} { - return $o_cursor_row - } - method cursor_column {} { - return $o_cursor_col - } - method insert_mode {} { - return $o_insert_mode - } - method clear {} { - set o_rendered_lines [list ""] - set o_chunk_list [list] - set o_chunk_info [dict create] - set o_cursor_row 1 - set o_cursor_col 1 - set o_insert_mode 1 ;#default to insert mode - lappend o_chunk_list "" - dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered] - } - method add_chunk {chunk} { - #we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info - lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok - dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw] - if {$chunk eq ""} { - return - } - - set firstnl [string first \n $chunk] - set newparts [split $chunk \n] - #attempt to render new 'lines' into the editbuffer - taking account of active cursor row & col & insertmode - - #merge based on current cursor row and col - #set lastrline [lindex $o_rendered_lines end] - #set n0 [lindex $newparts 0] - #set merged0 [string cat $lastrline $n0] - - #we should merge first row of newparts differently in case our chunks split a grapheme-combination? - # - if {$o_cursor_row < 1} { - puts stderr "add_chunk warning cursor_row < 1 - changing to minimum value 1" - set o_cursor_row 1 - } - set cursor_row_idx [expr {$o_cursor_row -1}] - set activeline [lindex $o_rendered_lines $cursor_row_idx] - set new0 [lindex $newparts 0] - #set combined [string cat $activeline $new0] - #use -cursor_row to tell renderline it's row context. - if {$firstnl >=0} { - #append combined \n - append new0 \n - } - set underlay [punk::ansi::ansistrip $activeline] - set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}] - if {$o_cursor_col > $line_nextchar_col} { - set o_cursor_col $line_nextchar_col - } - - set mergedinfo [overtype::renderline -info 1 -expand_right 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $underlay $new0] - - set result [dict get $mergedinfo result] - set o_insert_mode [dict get $mergedinfo insert_mode] - set result_col [dict get $mergedinfo cursor_column] - set result_row [dict get $mergedinfo cursor_row] - set overflow_right [dict get $mergedinfo overflow_right] ;#should be empty if no \v - set unapplied [dict get $mergedinfo unapplied] - set instruction [dict get $mergedinfo instruction] - - set insert_lines_below [dict get $mergedinfo insert_lines_below] - set insert_lines_above [dict get $mergedinfo insert_lines_above] - - # -- --- --- --- --- --- - set debug_first_row 2 - #puts "merged: $mergedinfo" - set debug "add_chunk0" - append debug \n $mergedinfo - append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col" - package require textblock - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] - if {![punk::console::vt52]} { - catch {punk::console::move_emitblock_return $debug_first_row 1 $debug} - } else { - #?? - } - # -- --- --- --- --- --- - - set o_cursor_col $result_col - set cursor_row_idx [expr {$o_cursor_row-1}] - lset o_rendered_lines $cursor_row_idx $result - - set nextrow $result_row - switch -- $instruction { - lf_start { - #for normal commandline - we just add a line below - lappend o_rendered_lines "" - incr nextrow - set o_cursor_col 1 - } - } - - if {$insert_lines_below == 1} { - if {[string length $overflow_right]} { - lappend o_rendered_lines $overflow_right - set o_cursor_col [expr {[punk::ansi::printing_length $overflow_right] +1}] - } else { - lappend o_rendered_lines "" - set o_cursor_col 1 - } - } elseif {$insert_lines_above == 1} { - #for {set i 0} {$i < $insert_lines_above} {incr i} { - # set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""] - # incr nextrow -1 - #} - set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""] - set o_cursor_col 1 - } - - set o_cursor_row $nextrow - set cursor_row_idx [expr {$o_cursor_row-1}] - if {$cursor_row_idx < [llength $o_rendered_lines]} { - set activeline [lindex $o_rendered_lines $cursor_row_idx] - } else { - lappend o_rendered_lines "" - set activeline "" - } - - - set i 1 - foreach p [lrange $newparts 1 end] { - if {$i < [llength $newparts]-1} { - append p \n - } else { - if {$p eq ""} { - break - } - } - #puts stderr "overtype::renderline -info 1 -expand_right 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $activeline '$p'" - set underlay $activeline - set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}] - if {$o_cursor_col > $line_nextchar_col} { - set o_cursor_col $line_nextchar_col - } - set mergedinfo [overtype::renderline -info 1 -expand_right 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $underlay $p] - set debug "add_chunk$i" - append debug \n $mergedinfo - append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]" - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] - #catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug} - - set result [dict get $mergedinfo result] - set o_insert_mode [dict get $mergedinfo insert_mode] - set o_cursor_col [dict get $mergedinfo cursor_column] - set cmove [dict get $mergedinfo cursor_row] - set overflow_right [dict get $mergedinfo overflow_right] ;#should be empty if no \v - set unapplied [dict get $mergedinfo unapplied] - set insert_lines_below [dict get $mergedinfo insert_lines_below] - if {[string is integer -strict $cmove]} { - if {$cmove == 0} { - set nextrow [expr {$o_cursor_row + 1}] - set o_cursor_col 1 - } elseif {$cmove == 1} { - #check for overflow_right and unapplied - #leave cursor_column - } elseif {$cmove >= 1} { - - } - - } else { - # = - absolute - set nextrow [string range $cmove 1 end] - } - if {$nextrow eq $o_cursor_row} { - incr nextrow - } - set o_cursor_row $nextrow - if {$insert_lines_below} { - - } - - set cursor_row_idx [expr {$o_cursor_row-1}] - if {$cursor_row_idx < [llength $o_rendered_lines]} { - set activeline [lindex $o_rendered_lines $cursor_row_idx] - } else { - lappend o_rendered_lines "" - set activeline "" - } - lset o_rendered_lines $cursor_row_idx $result - - incr i - } - } - method add_rendered_chunk {rchunk} { - #split only on lf newlines - movement codes and \b \v \r not expected - #check only for \v \r as chars we don't expect/want in rendered lines - #chunk as been pre-rendered (or is known to be plain ascii without ANSI or \b \v \r) - #but we don't yet have grapheme split info for it - - if {[regexp {[\v\b\r]} $rchunk]} { - error "[self class].add_rendered_chunk chunk contains \\v or \\b or \\r. Rendered chunk shouldn't contain these characters or ANSI movement codes" - } - lappend o_chunk_list $rchunk ;#rchunk may contain newlines - that's ok - dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type rendered] - - set newparts [split $rchunk \n] - #lappend o_chunk_list $rchunk - set lastrline [lindex $o_rendered_lines end] - - #in renderedlines list merge last line of old with first line of new - #we can't just cat the newpart on to existing rendered line - the chunk could have split a grapheme (e.g char+combiner(s)) - #we - #todo - redo grapheme split on merged line - set merged [string cat $lastrline [lindex $newparts 0]] - lset o_rendered_lines end $merged - - #todo - #each newpart needs its grapheme split info to be stored - #jmn - #set o_rendered_lines [concat $o_rendered_lines [lrange $newparts 1 end]] - lappend o_rendered_lines {*}[lrange $newparts 1 end] - - } - method linecount {} { - return [llength $o_rendered_lines] - } - method line {idx} { - if {[string is integer -strict $idx]} { - incr idx -1 - } - return [lindex $o_rendered_lines $idx] - } - method lines {args} { - switch -- [llength $args] { - 0 {return $o_rendered_lines} - 1 { - set idx [lindex $args 0] - if {[string is integer -strict $idx]} { - incr idx -1 - } - return [list [lindex $o_rendered_lines $idx]] - } - 2 { - lassign $args idx1 idx2 - if {[string is integer -strict $idx1]} { - incr idx1 -1 - } - if {[string is integer -strict $idx2]} { - incr idx2 -1 - } - return [lrange $o_rendered_lines $idx1 $idx2] - } - default {error "lines expected 0,1 or 2 indices"} - } - } - - #todo - index base??? - method lines_numbered {args} { - #build a paired list so we don't have to do various calcs on end+ end- etc checking llength - #punk::lib::range will use lseq if available - else use it's own slower code - set max [llength $o_rendered_lines] ;#assume >=1 - set nums [punk::lib::range 1 $max] - set numline_list [list] - foreach n $nums ln $o_rendered_lines { - lappend numline_list [list $n $ln] - } - - switch -- [llength $args] { - 0 {return $numline_list} - 1 {return [lindex $numline_list [lindex $args 0]]} - 2 {return [lrange $numline_list {*}$args]} - default {error "lines expected 0,1 or 2 indices"} - } - } - - #1-based - method delete_line {linenum} { - error "unimplemented" - if {$linenum eq "end"} { - set linenum [llength $o_rendered_lines] - } - if {![string is integer -strict $linenum]} { - error "delete_line accepts only end or an integer from 1 to linecount" - } - if {$linenum == 0} { - error "minimum line is 1" - } - set o_rendered_lines [lreplace $o_rendered_lines $index $index] - } - #clear data from last line only - method clear_tail {} { - set o_cursor_row [llength $o_rendered_lines] - set o_cursor_col 1 - lset o_rendered_lines end "" - } - #1-based - method view_lines {args} { - set llist [my lines {*}$args] - return [join $llist \n] - } - method view_lines_numbered {args} { - set ANSI_linenum [a+ green] - set RST [a] - set llist [my lines_numbered {*}$args] - set nums [lsearch -all -inline -index 0 -subindices $llist *] - lset nums $o_cursor_row-1 "[a+ bold underline]$o_cursor_row${RST}$ANSI_linenum" - set lines [lsearch -all -inline -index 1 -subindices $llist *] - - set cursorline [lindex $lines $o_cursor_row-1] - set charindex_at_cursor [ansistring COLUMNINDEX $cursorline $o_cursor_col] - if {$charindex_at_cursor ne ""} { - lassign [ansistring INDEXCOLUMNS $cursorline $charindex_at_cursor] col0 col1 - #we now have the column extents of the possibly double-wide character at the cursor - #we can apply ansi just to those columns using a transparent overtype - set prefix [string repeat " " [expr {$col0 -1}]] - set linecols [punk::ansi::printing_length $cursorline] - set suffix [string repeat " " [expr {$linecols -$col1}]] - #capitalised INDEX - for grapheme/control-char index e.g a with diacritic a\u0300 has a single index - set char_at_cursor [ansistring INDEX $cursorline $charindex_at_cursor] ;#this is the char with appropriate ansireset codes - set rawchar [punk::ansi::ansistrip $char_at_cursor] - if {$rawchar eq " "} { - set charhighlight "[punk::ansi::a+ White]_[a]" - } else { - set charhighlight [punk::ansi::a+ reverse]$char_at_cursor[a] - } - set cursorline [overtype::renderline -transparent 1 -insert_mode 0 -expand_right 0 $cursorline $prefix$charhighlight$suffix] - lset lines $o_cursor_row-1 $cursorline - } - - set numcol "$ANSI_linenum[join $nums \n][a]" - set linecol [join $lines \n] - return [textblock::join -- $numcol " " $linecol] - } - method debugview_lines {} { - set result "" - foreach ln $o_rendered_lines { - append result [ansistring VIEW -lf 1 -vt 1 $ln] \n ;#should be no lf or vt - but if there is.. we'd better show it - } - append result \n "cursor row: $o_cursor_row col: $o_cursor_col" - return $result - } - method last_char {} { - return [string index [lindex $o_chunk_list end] end] - } - - #more strictly - last non-ansi? - method last_grapheme {} { - set lastchunk [lindex $o_chunk_list end] - set plaintext_parts [punk::ansi::ta::split_at_codes $lastchunk] - set pt [lindex $plaintext_parts end] - if {$pt eq ""} { - set pt [lindex $plaintext_parts end-1] - } - set graphemes [punk::char::grapheme_split $pt] - return [lindex $graphemes end] - } - method last_ansi {} { - set lastchunk [lindex $o_chunk_list end] - set parts [punk::ansi::ta::split_codes_single $lastchunk] - set lastcode [lindex $parts end-1] - return $lastcode - #return [ansistring VIEW -lf 1 $lastcode] - } - method chunks {args} { - switch -- [llength $args] { - 0 {return $o_chunk_list} - 1 {return [lindex $o_chunk_list [lindex $args 0]]} - 2 {return [lrange $o_chunk_list {*}$args]} - default {error "chunks expected 0,1 or 2 arguments (index or range)"} - } - } - method view_chunks {} { - set result "" - set dashes [string repeat - 20] - foreach arrival_chunk $o_chunk_list chunk_id [dict keys $o_chunk_info] { - set chunk_info [dict get $o_chunk_info $chunk_id] - append result $dashes \n - set micros [dict get $chunk_info micros] - append result "$chunk_id arrival: [clock format [expr {$micros / 1000000}] -format "%Y-%m-%d %H:%M:%S"] ($micros)" \n - append result $dashes \n - append result $arrival_chunk \n - } - return $result - } - - method debugview_chunks {} { - set result "" - foreach ln $o_chunk_list { - append result [ansistring VIEW -lf 1 -vt 1 $ln] \n - } - append result \n "cursor row: $o_cursor_row col: $o_cursor_col" - return $result - } - method view_raw {} { - return [join $o_chunk_list ""] - } - method debugview_raw {} { - set sublf [ansistring VIEW -lf 1 \n] - #set subvt [ansistring VIEW -lvt 1 \v] ;#vt replacement with $subvt\v will not align accurately.. todo ? - return [string map [list $sublf $sublf\n] [ansistring VIEW -lf 1 -vt 0 [join $o_chunk_list ""]]] - } - } - -} - -proc ::punk::repl::repl_handler_checkchannel {inputchan} { - if {[catch {chan eof $inputchan} is_eof]} { - ::repl::rputs stderr "\n|repl> repl_handler_checkchannel error on $inputchan. (closed?) msg:$is_eof" - } else { - if {$is_eof} { - if {$::tcl_interactive} { - ::repl::rputs stderr "\n|repl> repl_handler_checkchannel EOF on $inputchan." - } - } - } -} -proc ::punk::repl::repl_handler_checkcontrolsignal_linemode {inputchan} { - #todo - what? - return - variable signal_control_c - if {$signal_control_c > 0} { - if {$::tcl_interactive} { - ::repl::rputs stderr "\n|repl> repl_handler_checkcontrolsignal_linemode ctrl-c errorCode 0: [lindex $::errorCode 0]" - } - } -} - -#This is not called from the signal handler - so we can't affect the signal handling with return -# -proc ::punk::repl::repl_handler_checkcontrolsignal_rawmode {inputchan} { - variable signal_control_c - variable signal_control_c_msg - if {$signal_control_c > 0 && $signal_control_c_msg ne "" } { - #if {$::tcl_interactive} { - # ::repl::rputs stderr "\n|repl> repl_handler_checkcontrolsignal_rawmode ctrl-c errorCode 0: [lindex $::errorCode 0]" - #} - set msg $signal_control_c_msg - set signal_control_c_msg "" - } else { - set msg "" - } - return [list count $signal_control_c msg $msg] -} - - -proc punk::repl::repl_handler_restorechannel_if_not_eof {inputchan previous_input_state} { - if {$inputchan ni [chan names] || [eof $inputchan]} { - return - } - if {[chan conf $inputchan] ne $previous_input_state} { - set restore_input_conf [dict remove $previous_input_state -inputmode] ;#Attempting to set input mode often gives permission denied on windows - why? - if {[catch { - chan conf $inputchan {*}$restore_input_conf - } errM]} { - rputs stderr "|repl>original: [ansistring VIEW $previous_input_state]" - rputs stderr "|repl>current : [ansistring VIEW [chan conf $inputchan]]" - rputs stderr "\n|repl> Failed to return $inputchan to original state" - rputs stderr "|repl>ERR: $errM" - } - } - return [chan conf $inputchan] -} -proc repl::repl_handler {inputchan prompt_config} { - # -- review - variable in_repl_handler - set in_repl_handler [list $inputchan $prompt_config] - # -- - variable last_controlc_count - - upvar ::punk::repl::prompt_reset_flag prompt_reset_flag - if {$prompt_reset_flag == 1} { - set prompt_config [punk::repl::get_prompt_config] - set prompt_reset_flag 0 - } - - chan event $inputchan readable {} - upvar ::punk::console::input_chunks_waiting input_chunks_waiting - #note -inputmode not available in Tcl 8.6 for chan configure! - #According to DKF - -buffering option doesn't affect input channels - set rawmode 0 - set original_input_conf [chan configure $inputchan] ;#whether repl is in line or raw mode - we restore the inputchan (stdin) state - if {[dict exists $original_input_conf -inputmode]} { - if {[dict get $original_input_conf -inputmode] eq "raw"} { - #user or script has apparently put stdin into raw mode - update punk::console::is_raw to match - set rawmode 1 - #set ::punk::console::is_raw 1 - tsv::set console is_raw 1 - } else { - #set ::punk::console::is_raw 0 - tsv::set console is_raw 0 - } - #what about enable/disable virtualTerminal ? - #using stdin -inputmode to switch modes won't set virtualterminal input state appropriately - #we expect the state of -inputmode to be 'normal' even though we flip it during the read part of our repl loop - #if it's been set to raw - assume it is deliberately done this way as the user could have alternatively called punk::mode raw or punk::console::enableVirtualTerminal - #by not doing this automatically - we assume the caller has a reason. - } else { - #JMN FIX! - #this returns 0 in rawmode on 8.6 after repl thread changes - #set rawmode [set ::punk::console::is_raw] - set rawmode [tsv::get console is_raw] - } - - if {!$rawmode} { - #linemode - - #stdin with line-mode readable events (at least on windows for Tcl 8.7a6 to 9.0a) can get stuck with bytes pending when input longer than 100chars - even though there is a linefeed further on than that. - #This potentially affects a reasonable number of Tcl8.7 kit/tclsh binaries out in the wild. - #see bug https://core.tcl-lang.org/tcl/tktview/bda99f2393 (gets stdin problem when non-blocking - Windows) - #when in non-blocking mode we will have to read that in to get further - but we don't know if that was the end of line or if there is more - and we may not get a newline even though one was present originally on stdin. - #presence of 8.7 buffering bug will result in unrecoverable situation - even switching to raw and using read will not be able to retrieve tail data. - #the readable event only gives us 200 bytes (same problem may be at 4k/8k in other versions) - #This occurs whether we use gets or read - - set stdinlines [list] - if {[dict get $original_input_conf -blocking] ne "0"} { - chan configure $inputchan -blocking 0 - } - - set waitingchunk "" - #review - input_chunks_waiting in line mode - - if {[info exists input_chunks_waiting($inputchan)] && [llength $input_chunks_waiting($inputchan)]} { - #puts stderr "repl_handler input_chunks_waiting($inputchan) while in line mode. Had data:[ansistring VIEW -lf 1 $input_chunks_waiting($inputchan)]" - set allwaiting [join $input_chunks_waiting($inputchan) ""] - set input_chunks_waiting($inputchan) [list] - set yellow [punk::ansi::a+ yellow bold] - set waitinglines [split $allwaiting \n] - foreach ln [lrange $waitinglines 0 end-1] { - lappend stdinlines $ln - } - set waitingchunk [lindex $waitinglines end] - # -- - #set chunksize [gets $inputchan chunk] - set chunk [read $inputchan] - set chunksize [string length $chunk] - # -- - if {$chunksize > 0} { - if {[string index $chunk end] eq "\n"} { - lappend stdinlines $waitingchunk[string range $chunk 0 end-1] - #punk::console::cursorsave_move_emitblock_return 30 30 "repl_handler num_stdinlines [llength $stdinlines] chunk:$yellow[ansistring VIEW -lf 1 $chunk][a] fblocked:[fblocked $inputchan] pending:[chan pending input stdin]" - - punk::repl::repl_handler_restorechannel_if_not_eof $inputchan $original_input_conf - uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] - } else { - set input_chunks_waiting($inputchan) [list $allwaiting] - lappend input_chunks_waiting($inputchan) $chunk - } - } else { - #'chan blocked' docs state: 'Note that this only ever returns 1 when the channel has been configured to be non-blocking..' - if {[chan blocked $inputchan]} { - #REVIEW - - #todo - figure out why we're here. - #can we even put a spinner so we don't keep emitting lines? We probably can't use any ansi functions that need to get a response on stdin..(like get_cursor_pos) - #punk::console::get_size is problematic if -winsize not available on the stdout channel - which is the case for certain 8.6 versions at least.. platform variances? - ## can't do this: set screeninfo [punk::console::get_size]; lassign $screeninfo _c cols _r rows - set outconf [chan configure stdout] - set RED [punk::ansi::a+ red bold]; set RST [punk::ansi::a] - if {"windows" eq $::tcl_platform(platform)} { - set msg "${RED}$inputchan chan blocked is true. (line-length Tcl windows channel bug?)$RST \{$allwaiting\}" - } else { - set msg "${RED}$inputchan chan blocked is true.$RST \{$allwaiting\}" - } - set cols "" - set rows "" - if {[dict exists $outconf -winsize]} { - lassign [dict get $outconf -winsize] cols rows - } else { - #fallback - try external executable. Which is a bit ugly - #tput can't seem to get dimensions (on FreeBSD at least) when not run interactively - ie via exec. (always returns 80x24 no matter if run with <@stdin) - - #bizarrely - tput can work with exec on windows if it's installed e.g from msys2 - #but can be *slow* compared to unix e.g 400ms+ vs <2ms on FreeBSD ! - #stty -a is 400ms+ vs 500us+ on FreeBSD - - if {"windows" eq $::tcl_platform(platform)} { - set tputcmd [auto_execok tput] - if {$tputcmd ne ""} { - if {![catch {exec {*}$tputcmd cols lines} values]} { - lassign $values cols rows - } - } - } - - if {![string is integer -strict $cols] || ![string is integer -strict $rows]} { - #same for all platforms? tested on windows, wsl, FreeBSD - #exec stty -a gives a result on the first line like: - #speed xxxx baud; rows rr; columns cc; - #review - more robust parsing - do we know it's first line? - set sttycmd [auto_execok stty] - if {$sttycmd ne ""} { - #the more parseable: stty -g doesn't give rows/columns - if {![catch {exec {*}$sttycmd -a} result]} { - lassign [split $result \n] firstline - set lineparts [split $firstline {;}] ;#we seem to get segments that look well behaved enough to be treated as tcl lists - review - regex? - set rowinfo [lsearch -index end -inline $lineparts rows] - if {[llength $rowinfo] == 2} { - set rows [lindex $rowinfo 0] - } - set colinfo [lsearch -index end -inline $lineparts columns] - if {[llength $colinfo] == 2} { - set cols [lindex $colinfo 0] - } - } - } - } - } - if {[string is integer -strict $cols] && [string is integer -strict $rows]} { - #got_dimensions - todo - try spinner? - #puts -nonewline stdout [punk::ansi::move $rows 4]$msg - #use cursorsave_ version which avoids get_cursor_pos_list call - set msglen [ansistring length $msg] - punk::console::cursorsave_move_emitblock_return $rows [expr {$cols - $msglen -1}] $msg - } else { - #no mechanism to get console dimensions - #we are reduced to continuously spewing lines. - puts stderr $msg - } - - after 100 - } - set input_chunks_waiting($inputchan) [list $allwaiting] - } - - } else { - punk::repl::repl_handler_checkchannel $inputchan - punk::repl::repl_handler_checkcontrolsignal_linemode $inputchan - # -- --- --- - #set chunksize [gets $inputchan chunk] - # -- --- --- - set chunk [read $inputchan] - set chunksize [string length $chunk] - # -- --- --- - if {$chunksize > 0} { - #punk::console::cursorsave_move_emitblock_return 35 120 "chunk: [ansistring VIEW -lf 1 "...[string range $chunk end-10 end]"]" - set ln $chunk ;#temp - #punk::console::cursorsave_move_emitblock_return 25 30 [textblock::frame -title line "[a+ green]$waitingchunk[a][a+ red][ansistring VIEW -lf 1 $ln][a+ green]pending:[chan pending input stdin][a]"] - if {[string index $ln end] eq "\n"} { - lappend stdinlines [string range $ln 0 end-1] - punk::repl::repl_handler_restorechannel_if_not_eof $inputchan $original_input_conf - uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] - } else { - lappend input_chunks_waiting($inputchan) $ln - } - } - } - - } else { - #rawmode - if {[info exists input_chunks_waiting($inputchan)] && [llength $input_chunks_waiting($inputchan)]} { - #we could concat and process as if one chunk - but for now at least - we want to preserve the 'chunkiness' - set chunkwaiting_zero [lpop input_chunks_waiting($inputchan) 0] ;#pop off lhs of wait list (tcl 8.6 is tcl imp of lpop - a little slower) - uplevel #0 [list repl::repl_process_data $inputchan raw-waiting $chunkwaiting_zero [list] $prompt_config] - } else { - - punk::repl::repl_handler_checkchannel $inputchan - set signalinfo [punk::repl::repl_handler_checkcontrolsignal_rawmode $inputchan] - if {[dict get $signalinfo count] > $last_controlc_count} { - set continue 0 - set last_controlc_count [dict get $signalinfo count] - } else { - set continue 1 - } - - if {$continue} { - if {[dict get $original_input_conf -blocking] ne "0" || [dict get $original_input_conf -translation] ne "lf"} { - chan configure $inputchan -blocking 0 - chan configure $inputchan -translation lf - } - set chunk [read $inputchan] - #we expect a chan configured with -blocking 0 to be blocked immediately after reads - #test - just bug console for now - try to understand when/how/if a non blocking read occurs. - if {![chan blocked $inputchan]} { - puts stderr "repl_handler->$inputchan not blocked after read" - } - - punk::repl::repl_handler_restorechannel_if_not_eof $inputchan $original_input_conf - uplevel #0 [list repl::repl_process_data $inputchan raw-read $chunk [list] $prompt_config] - while {[llength $input_chunks_waiting($inputchan)]} { - set chunkzero [lpop input_chunks_waiting($inputchan) 0] - if {$chunkzero eq ""} {continue} ;#why empty waiting - and is there any point passing on? - uplevel #0 [list repl::repl_process_data $inputchan raw-waiting $chunkzero [list] $prompt_config] - } - } - } - } - - if {![chan eof $inputchan]} { - ################################################################################## - #Re-enable channel read handler only if no waiting chunks - must process in order - ################################################################################## - if {![llength $input_chunks_waiting($inputchan)]} { - chan event $inputchan readable [list ::repl::repl_handler $inputchan $prompt_config] - } else { - after idle [list ::repl::repl_handler $inputchan $prompt_config] - } - #################################################### - } else { - #repl_handler_checkchannel $inputchan - chan event $inputchan readable {} - set reading 0 - thread::send -async $::repl::codethread {set ::punk::repl::codethread::running 0} - if {$::tcl_interactive} { - rputs stderr "\nrepl_handler EOF inputchannel:[chan conf $inputchan]" - #rputs stderr "\n|repl> ctrl-c EOF on $inputchan." - } - set [namespace current]::done 1 - after 1 [list repl::reopen_stdin] - } - set in_repl_handler [list] -} - -proc punk::repl::editbuf {index args} { - set editbuf [lindex $::repl::editbuf_list $index] - if {$editbuf ne ""} { - $editbuf {*}$args - } else { - return "No such index in editbuf list" - } -} -interp alias {} editbuf {} ::punk::repl::editbuf - - -proc punk::repl::console_debugview {editbuf consolewidth args} { - if {[punk::console::vt52]} { - #topleft? - return [dict create width 0 height 0 topleft 0] - } - package require textblock - variable debug_repl - if {$debug_repl <= 0} { - return [dict create width 0 height 0 topleft {}] - } - set defaults {-row 10 -rightmargin 2 -chunktype raw-read} - #dict for {k v} $args { - # switch -- $k { - # -row - -chunktype {} - # default { - # error "console_debugview unrecognised option '$k'. Known-options [dict keys $defaults]" - # } - # } - #} - set opts [dict merge $defaults $args] - set opt_row [dict get $opts -row] - set opt_chunktype [dict get $opts -chunktype] - set opt_rightmargin [dict get $opts -rightmargin] - - #debugview_raw frame - set RST [a] - if {[catch { - set info [$editbuf debugview_raw] - if {$opt_chunktype eq "raw-waiting"} { - set info [a+ bold yellow]$info$RST - } else { - set info [a+ green]$info$RST - } - #set lines [lines_as_list -ansireplays 1 $info] - set lines [lines_as_list -ansireplays 0 $info] - if {[llength $lines] > 20} { - set lines [lrange $lines end-19 end] - set info [::join $lines \n] - } - set debug_height [expr {[llength $lines]+2}] ;#framed height - } errM]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] - set debug_height [textblock::height $info] - } else { - #treat as ephemeral (unreusable) frames due to varying width & height - therefore set -buildcache 0 - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] - } - - set debug_width [textblock::widthtopline $info] - set patch_height [expr {2 + $debug_height + 2}] - set spacepatch [textblock::block $debug_width $patch_height " "] - #puts -nonewline [punk::ansi::cursor_off] - punk::console::cursor_off - #use non cursorsave versions - cursor save/restore will interfere with any concurrent ansi rendering that uses save/restore - because save/restore is a single item, not a stack. - set debug_offset [expr {$consolewidth - $debug_width - $opt_rightmargin}] - set row_clear [expr {$opt_row -2}] - punk::console::move_emitblock_return $row_clear $debug_offset $spacepatch - punk::console::move_emitblock_return $opt_row $debug_offset $info - set topleft [list $debug_offset $opt_row] ;#col,row REVIEW - #puts -nonewline [punk::ansi::cursor_on] - punk::console::cursor_on - flush stdout - - return [dict create width $debug_width height $debug_height topleft $topleft] -} -proc punk::repl::console_editbufview {editbuf consolewidth args} { - if {[punk::console::vt52]} { - return [dict create width 0] - } - package require textblock - upvar ::repl::editbuf_list editbuf_list - - set defaults {-row 10 -rightmargin 0} - set opts [dict merge $defaults $args] - set opt_row [dict get $opts -row] - set opt_rightmargin [dict get $opts -rightmargin] - - if {[catch { - set info [$editbuf view_lines_numbered] - set lines [lines_as_list -ansireplays 1 $info] - if {[llength $lines] > 20} { - set lines [lrange $lines end-19 end] - set info [punk::lib::list_as_lines $lines] - } - } editbuf_error]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] - } else { - set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]" - append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]" - set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]" - set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]" - set info [a+ green bold]$row1\n$row2[a]\n$info - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] - } - set editbuf_width [textblock::widthtopline $info] - set spacepatch [textblock::block $editbuf_width 2 " "] - - #set editbuf_offset [expr {$consolewidth - $editbuf_width - $debug_width - 2}] - set editbuf_offset [expr {$consolewidth - $editbuf_width - $opt_rightmargin}] - - set row_clear [expr {$opt_row -2}] - punk::console::cursorsave_move_emitblock_return $row_clear $editbuf_offset $spacepatch - punk::console::cursorsave_move_emitblock_return $opt_row $editbuf_offset $info - - return [dict create width $editbuf_width] -} -proc punk::repl::console_controlnotification {message consolewidth consoleheight args} { - package require textblock - set defaults {-bottommargin 0 -rightmargin 0} - set opts [dict merge $defaults $args] - set opt_bottommargin [dict get $opts -bottommargin] - set opt_rightmargin [dict get $opts -rightmargin] - set messagelines [split $message \n] - set message [lindex $messagelines 0] ;#only allow single line - set info "[a+ bold red]$message[a]" - set hlt [dict get [textblock::framedef light] hlt] - set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] - set notification_width [textblock::widthtopline $info] - set box_offset [expr {$consolewidth - $notification_width - $opt_rightmargin}] - set row [expr {$consoleheight - $opt_bottommargin}] - punk::console::cursorsave_move_emitblock_return $row $box_offset $info - return [dict create width $notification_width] -} - -proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config} { - if {[info exists ::punk::console::is_vt52]} { - set is_vt52 $::punk::console::is_vt52 - } else { - set is_vt52 0 - } - - variable loopinstance - incr loopinstance - upvar ::punk::console::input_chunks_waiting input_chunks_waiting - - upvar ::punk::repl::prompt_reset_flag prompt_reset_flag - - variable last_repl_char "" ;#last char emitted by this handler to stdout/stderr - variable lastoutchar "" - variable lasterrchar "" - variable commandstr - # --- - variable editbuf - variable editbuf_list - variable editbuf_linenum_submitted - - # --- - variable reading - variable id_outstack - #upvar ::punk::config::configdata configd - #set running_config [dict get $configd running] - - try { - #catch {puts stderr "xx--->[rep $::arglej]"} - if {$prompt_reset_flag == 1} { - set prompt_config [punk::repl::get_prompt_config] - set prompt_reset_flag 0 - } - - set resultprompt [dict get $prompt_config resultprompt] - set nlprompt [dict get $prompt_config nlprompt] - set infoprompt [dict get $prompt_config infoprompt] - set debugprompt [dict get $prompt_config debugprompt] - - - #set rawmode [set ::punk::console::is_raw] - set rawmode [tsv::get console is_raw] - if {!$rawmode} { - #puts stderr "-->got [ansistring VIEW -lf 1 $stdinlines]<--" - - } else { - #raw - set chunklen [string length $chunk] - set onetime 1 - #single loop while to allow break on escape - while {$onetime && [string length $chunk] >= 0 } { - set onetime 0 - #punk::console::move_emitblock_return 20 120 $chunklen-->[chan conf stdin]<-- - - #if {$chunklen == 0} { - # #document examples of when we expect zero-byte chunk - # #1) ctrl-z - # #review - # rputs stderr "->0byte read stdin" - # if {[chan eof $inputchan]} { - # chan event $inputchan readable {} - # set reading 0 - # #set running 0 - # if {$::tcl_interactive} { - # rputs stderr "\n|repl> EOF on $inputchan." - # } - # set [namespace current]::done 1 - # #test - # #JMN - # #tailcall repl::reopen_stdin - # } - # break - #} - - #set info1 "read $chunklen bytes->[ansistring VIEW -lf 1 -vt 1 $chunk]" - #consider also the terminal linefeed mode. - #https://vt100.net/docs/vt510-rm/LNM.html - # terminals (by default) generally use a lone cr to represent enter (LNM reset ie CSI 20l) - #(as per above doc: "For compatibility with Digital's software you should keep LNM reset (line feed)") - - #You can insert an lf using ctrl-j - and of course stdin could have crlf or lf - #pasting from notepad++ with mixed line endings seems to paste everything ok - #we don't really know the source of input keyboard vs paste vs pipe - and whether a read has potentially chopped a crl in half.. - #possibly no real way to determine that. We could wait a small time to see if there's more data coming.. and potentially impact performance. - #Instead we'll try to make sense of it here. - - if {$chunklen == 1} { - #presume it's a keypress from terminal - set chunk [string map {\r \n} $chunk] - } else { - #maybe a paste? (or stdin to active shell loop - possibly with no terminal ? ) - #we'd better check for crlf and/or plain lf. If found - presume any lone CR is to be left as is. - if {[string first \n $chunk] < 0} { - set chunk [string map {\r \n} $chunk] - } - #else - - #has lf - but what if last char is cr? - #It may require user to hit enter - probably ok. - #could be a sequence of cr's from holding enter key - } - - #review - we can receive chars such as escapes or arrow inline with other data even from keyboard if keys are pushed quickly (or automated?) - # - so we probably shouldn't really rely on whether a char arrives alone in a chunk as a factor in its behaviour - #On the other hand - timing of keystrokes could be legitimate indications of intention in a cli ? - - #esc or ctrl-lb - if {$chunk eq "\x1b"} { - #return - set stdinlines [list "\x1b"] - set commandstr "" - set chunk "" - $editbuf clear_tail - screen_last_char_add \x1b stdin escape - break - } - - #if ProcessedInput is disabled - we can get ctrl-c, but then we wouldn't be in raw mode and wouldn't be here. - #e.g with punk::console::disableProcessedInput - #if we get just ctrl-c in one chunk - #ctrl-c - if {$chunk eq "\x03"} { - #::punk::repl::handler_console_control "ctrl-c_via_rawloop" - error "character 03 -> ctrl-c" - } - - if {$chunk eq "\x7f"} { - #review - configurable? - #translate raw del to backspace del for those terminals that send plain del - set chunk "\b\x7f" - } elseif {$chunk eq "\x7f\x7f"} { - #commonly if key held down we will get 2 dels in a row - #review - could get more in a row depending on hardware/os - set chunk "\b\x7f\b\x7f" - } elseif {$chunk eq "\x1c"} { - #ctrl-bslash - #try to brutally terminate process - #attempt to leave terminal in a reasonable state - mode line ;#may be aliased to ::repl::interphelpers::mode - after 250 {exit 42} - return - } elseif {$chunk eq "\x1a"} { - #for now - exit with small delay for tidyup - #ctrl-z - #::punk::repl::handler_console_control "ctrl-z_via_rawloop" - if {[catch {mode line}]} { - interp eval code {mode line} - } - after 1000 {exit 43} - return - } - - #we *could* intercept arrow keys here before they are handled in the editbuf - #but there should only be the need to do so for situations where we aren't editing a commandline - #if {$chunk eq "\x1b\[D"} { - # #rputs stderr "${debugprompt}arrow-left D" - # #set commandstr "" - # #punk::console::move_back 1 ;#terminal does it anyway? - #} - #if {$chunk eq "\x1b\[C"} { - #} - - $editbuf add_chunk $chunk - - #-------------------------- - # editbuf and debugview rhs frames - #for now disable entirely on vt52 - we can only do cursor save restore - nothing that requires responses on stdin (?) - if {!$is_vt52 && [set ::punk::console::ansi_available]} { - #experimental - use punk::console::get_size to determine current visible width. - #This should ideally be using sigwinch or some equivalent to set a value somewhere. - #testing each time is very inefficient (1+ms) - #unfortunately there isn't an easy way to get such an event on windows console based systems - REVIEW. - set do_checkwidth 1 ;#make configurable if performance hit is too severe? TODO - set consolewidth 132 - if {$do_checkwidth} { - if {[catch {set consolewidth [dict get [punk::console::get_size] columns]} errM]} { - #review - if {!$is_vt52} { - puts stderr "repl_process_data failed on call to punk::console::get_size :$errM" - } - } - #if chan conf stdout doesn't give dimensions and console doesn't respond to queries - we can get empty results in get_size dict - if {$consolewidth eq ""} { - set consolewidth 132 - } - } - set debug_width 0 - set rightmargin 0 - set space_occupied [punk::repl::console_debugview $editbuf $consolewidth -row 10 -chunktype $chunktype -rightmargin $rightmargin] ;#contains cursor movements - set debug_width [dict get $space_occupied width] - set clearance [expr {$debug_width + $rightmargin}] - set space_occupied [punk::repl::console_editbufview $editbuf $consolewidth -row 10 -rightmargin $clearance] - } - #-------------------------- - - - set lines_unsubmitted [expr {[$editbuf linecount] - $editbuf_linenum_submitted}] - #there is always one 'line' unsubmitted - although it may be the current one being built, which may be empty string - if {$lines_unsubmitted < 1} { - puts stderr "repl editbuf_linenum_submitted out of sync with editbuf" - } - - #set trailing_line_index [expr {[$editbuf linecount] -1}] - set last_line_num [$editbuf linecount] - #set nextsubmit_index [expr {$editbuf_lineindex_submitted + 1}] - set nextsubmit_line_num [expr {$editbuf_linenum_submitted + 1}] - - set cursor_row [$editbuf cursor_row] - set cursor_index [expr {$cursor_row -1}] - set lastansi [$editbuf last_ansi] - if {$lastansi eq "\x1b\[A"} { - if {$cursor_row > 1} { - puts -nonewline stdout "\x1b\[A" - } - } elseif {$lastansi eq "\x1b\[B"} { - puts -nonewline stdout "\x1b\[B" - } - flush stdout - - - - set leftmargin 3 - if {!$is_vt52} { - puts -nonewline stdout [a+ cyan][punk::ansi::move_column [expr {$leftmargin +1}]][punk::ansi::erase_eol][$editbuf line $cursor_row][a][punk::ansi::move_column [expr {$leftmargin + [$editbuf cursor_column]}]] - } else { - puts -nonewline stdout [a+ cyan][punk::ansi::vt52move_column [expr {$leftmargin +1}]][punk::ansi::vt52erase_eol][$editbuf line $cursor_row][punk::ansi::vt52move_column [expr {$leftmargin + [$editbuf cursor_column]}]] - } - #puts -nonewline stdout $chunk - flush stdout - if {[$editbuf last_char] eq "\n"} { - set linelen [punk::ansi::printing_length [$editbuf line $nextsubmit_line_num]] - if {!$is_vt52} { - puts -nonewline stdout [a+ cyan bold][punk::ansi::move_column [expr {$leftmargin +1}]][$editbuf line $nextsubmit_line_num][a][punk::ansi::move_column [expr {$leftmargin + $linelen +1}]] - #screen_last_char_add "\n" input inputline - puts -nonewline stdout [punk::ansi::erase_eol]\n - } else { - puts -nonewline stdout [a+ cyan bold][punk::ansi::vt52move_column [expr {$leftmargin +1}]][$editbuf line $nextsubmit_line_num][punk::ansi::vt52move_column [expr {$leftmargin + $linelen +1}]] - puts -nonewline stdout [punk::ansi::vt52erase_eol]\n - } - - - #puts -nonewline stdout \n - screen_last_char_add "\n" input inputline - set waiting [$editbuf line end] - if {[string length $waiting] > 0} { - set waiting [a+ yellow bold]$waiting[a] - #puts stderr "waiting $waiting" - $editbuf clear_tail - lappend input_chunks_waiting($inputchan) $waiting - } - } - if {$editbuf_linenum_submitted == 0} { - #(there is no line 0 - lines start at 1) - if {[$editbuf last_char] eq "\n"} { - lappend stdinlines [$editbuf line 1] - set editbuf_linenum_submitted 1 - } - } else { - if {$nextsubmit_line_num < $last_line_num} { - foreach ln [$editbuf lines $nextsubmit_line_num end-1] { - lappend stdinlines $ln - incr editbuf_linenum_submitted - } - } - } - set last_cursor_column [$editbuf cursor_column] - } - } - - } trap {POSIX} {e eopts} { - rputs stderr "trap1 POSIX '$e' eopts:'$eopts" - flush stderr - } on error {repl_error erropts} { - rputs stderr "error1 in repl_handler: $repl_error" - rputs stderr "-------------" - rputs stderr "$::errorInfo" - rputs stderr "-------------" - set stdinreader [chan event $inputchan readable] - if {![string length $stdinreader]} { - rputs stderr "*> $inputchan reader inactive" - } else { - rputs stderr "*> $inputchan reader active" - } - if {[chan eof $inputchan]} { - rputs stderr "todo - attempt restart of repl on input channel: $inputchan in next loop" - catch {set ::punk::ns::ns_current "::"} - #todo set flag to restart repl ? - } else { - rputs stderr "continuing.." - } - flush stderr - } - - - try { - set maxlinenum [expr {[llength $stdinlines] -1}] - set linenum 0 - foreach line $stdinlines { - #puts stderr "----->line: [ansistring VIEW -lf 1 $line] commandstr:[ansistring VIEW -lf 1 $commandstr]" - set last_repl_char "" ;#last char emitted by this handler to stdout/stderr - set lastoutchar "" - set lasterrchar "" - - - #consider \x1b as text on console vs \x1b the character - #review - if we're getting these actual escape characters in line mode.. something is off - let's emit something instead of trying to interpret as a command and failing. - #This tends to happen when some sort of readline not avaialbe ie on unix or mintty in windows - #this only captures leading escape.. as an aid to diagnosis e.g won't be caught and the user will need to close the right bracket to complete the bogus command - #we may need to think about legitimate raw escapes in commands e.g from pipes or script files, vs via console? - - #esc key or ctrl-lb followed by enter - if {$line eq "\x1b"} { - #abort current command - if {$linenum == 0} { - doprompt "E% " {yellow bold} - set line "" - #screen_last_char_add " " empty empty - } else { - doprompt "\nE% " {yellow bold} - #screen_last_char_add "\n" empty empty ;#add \n to indicate noclearance required - } - incr linenum - continue - } else { - if {$line eq "\x1b\[C"} { - rputs stderr "${debugprompt}arrow-right C" - #set commandstr "" - } - if {$line eq "\x1b\[D"} { - #rputs stderr "${debugprompt}arrow-left D" - #set commandstr "" - #punk::console::move_back 1 - } - if {$line eq "\x1b\[A"} { - rputs stderr "${debugprompt}arrow-up A" - } - if {$line eq "\x1b\[B"} { - rputs stderr "arrow-down B" - } - if {[string match "\x1b*" $line]} { - rputs stderr "${debugprompt}esc - '[punk::ansi::ansistring::VIEW $line]'" - #set commandstr [punk::ansi::ansistrip $commandstr] - } - } - - if {$commandstr ne ""} { - append commandstr \n - } - - set stdinconf [fconfigure $inputchan] - if {$::tcl_platform(platform) eq "windows" && [dict get $stdinconf -encoding] ni [list unicode utf-16 utf-8]} { - #some long console inputs are split weirdly when -encoding and -translation are left at defaults - requiring extra enter-key to get repl to process. - #experiment to see if using iso8859-1 (raw bytes) and handling line endings manually gives insight. - # - do: chan conf stdin -encoding iso859-1 -translation lf - - #first command after configuring stdin this way seems to be interpreted with wrong encoding - subsequent commands work - review - - #this branch only works on tcl8.7+ - #It seems to fix the issue with holding down enter-key and getting extra blank lines, but - # it breaks copy-paste (encoding issue?) - - - #puts "--inputchan:$inputchan> [fconfigure $inputchan]" - - append commandstr $line - puts "1=============>[string length $commandstr] bytes , [ansistring VIEW $commandstr] , info complete:[info complete $line] stdinconf:$stdinconf" - set commandstr [string range $commandstr 0 end-3] - set commandstr [encoding convertfrom utf-16be $commandstr] ;#This is weird - but it seems to be big endian? - set commandstr [string trimright $commandstr] - #puts "2=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" - } else { - #append commandstr $line - #puts "0=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" - append commandstr $line - } - - #puts "=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" - set last_repl_char "\n" ;#this is actually the eol from stdin - screen_last_char_add "\n" stdin $line - - - - #append commandstr \n - - - if {[info complete $commandstr] && [string index $commandstr end] ne "\\"} { - #set commandstr [overtype::renderline -expand_right 1 "" $commandstr] - - - set ::repl::output_stdout "" - set ::repl::output_stderr "" - set outstack [list] - set errstack [list] - - - #oneshot repl debug - set wordparts [regexp -inline -all {\S+} $commandstr] - lassign $wordparts cmd_firstword cmd_secondword - if {$cmd_firstword eq "debugrepl"} { - if {$cmd_secondword in [list 0 cancel]} { - set ::punk::repl::debug_repl 0 - } else { - if {[string is integer -strict $cmd_secondword]} { - incr ::punk::repl::debug_repl $cmd_secondword - } else { - incr ::punk::repl::debug_repl - } - } - #set commandstr "set ::punk::repl::debug_repl" - set commandstr "" - } - if {$::punk::repl::debug_repl > 100} { - proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { - set p %p% - #don't auto-append \n even if missing. - #we may want to use debug_repl_emit with multiple calls for one output line - #if {[string index $msg end] ne "\n"} { - # set msg "$msg\n" - #} - #set last_char [string index $::repl::screen_last_chars end] - set last_char [screen_last_char_get] - if {$last_char ne "\n"} { - set clearance "\n" - } else { - set clearance "" - } - #use pseudo-channel debugreport - rputs debugreport $clearance$p[string map [list \n \n$p] $msg] - }] - set info "" - append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" - append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" - set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]] - append info "lastrunchunks\n" - append info "chunks: [llength $lastrunchunks]\n" - append info "namespace: $::punk::ns::ns_current" - debug_repl_emit $info - } else { - proc debug_repl_emit {msg} {return} - } - - #----------------------------------------- - #review! - #work around weird behaviour in tcl 8.6 & 8.7a5 (at least) part1 - #https://wiki.tcl-lang.org/page/representation - #/scriptlib/tests/listrep_bug.tcl - #after the uplevel #0 $commandstr call - # vars within the script that were set to a list, and have no string-rep, will generate a string-rep once the script (commandstr) is unset, or set to another value - #review - although the rep change is weird - what actual problem was caused aside from an unexpected shimmer? - #probably just that the repl can't then be used to debug representation issues and possibly that the performance is not ideal for list pipeline commands(?) - #now that we eval in another thread and interp - we seem to lose the list rep anyway. - #(unless we also save the script in that interp too in a run_command_cache) - global run_command_string - set run_command_string "$commandstr\n" ;#add anything that won't affect script. - global run_command_cache - #----------------------------------------- - - set repl_runid [tsv::incr repl runid] - tsv::set repl runchunks-$repl_runid [list] ;#last_run_display - catch { - #REVIEW - when we launch a subshell and run more than 10 commands, - #we delete runchunks from the outer shell that we'll return to! - #we should use a toplevel key pertaining to the shell/subshell instead of just 'repl' - tsv::unset repl runchunks-[expr {$repl_runid - 10}] - } - - #set ::repl::last_unknown "" - tsv::set repl last_unknown "" - #*********************************************************** - #don't use puts,rputs or debug_repl_emit in this block - #*********************************************************** - - #if {[string length [dict get $running_config color_stdout]] && [punk::console::colour]} { - # lappend outstack [shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]]] - #} - #lappend outstack [shellfilter::stack::add stdout tee_to_var -settings {-varname ::repl::output_stdout}] - #if {[string length [dict get $running_config color_stderr]] && [punk::console::colour]} { - # lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] - #} - - variable codethread - variable codethread_cond - variable codethread_mutex - - lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] - #thread::transfer $codethread stderr - - #chan configure stdout -buffering none - #JMN - chan event $inputchan readable {} - set reading 0 - #don't let unknown use 'args' to convert commandstr to list - #=============================================================================== - #Actual command call - #puts "____>[rep $commandstr]" - #=============================================================================== - if {[string equal -length [string length "repl_runraw "] "repl_runraw " $commandstr]} { - #pass unevaluated command to runraw - set status [catch {uplevel #0 [list runraw $commandstr]} raw_result] - } else { - #puts stderr "repl uplevel 0 '$run_command_string'" - #JMN - #puts stderr "sending to codethread::runscript $run_command_string" - tsv::set codethread_$codethread status -1 - thread::send -async $codethread [list punk::repl::codethread::runscript $run_command_string] - thread::mutex lock $codethread_mutex - while {[set status [tsv::get codethread_$codethread status]] == -1} { - thread::cond wait $codethread_cond $codethread_mutex 50 - update ;#we need a full update here to allow interrupts to be processed - #While update is often considered risky - here we know our input channel readable event has been disabled - so re-entrancy shouldn't be possible. - #however - child thread can send quit - transferring processing from here back to repl::start - which then ends - making a mess (child not yet finished when trying to tidy up) - #we should give the child a way to quit by setting a tsv we pick up here *after the while loop* - and then we can set done. - } - thread::mutex unlock $codethread_mutex - set raw_result [tsv::get codethread_$codethread result] - lassign [tsv::get codethread_$codethread info] _o lastoutchar_codethread _e lasterrchar_codethread - - #set status [catch { - # thread::send $ - # uplevel 1 {namespace inscope $::punk::ns::ns_current $run_command_string} - #} raw_result] - - } - #puts stderr "repl raw_result: $raw_result" - #set result $raw_result - #append result ""; #copy on write - #copy on write - - #append result $raw_result "" - set result [string cat $raw_result ""] - #puts stderr "-->>$result<--" - #=============================================================================== - - flush stdout - flush stderr - #foreach s [lreverse $outstack] { - # shellfilter::stack::remove stdout $s - #} - #foreach s [lreverse $errstack] { - # shellfilter::stack::remove stderr $s - #} - - #----------------------------------------- - #list/string-rep bug workaround part 2 - #todo - set flag based on punk::lib::check::has_tclbug_script_var - lappend run_command_cache $run_command_string - #puts stderr "run_command_string rep: [rep $run_command_string]" - if {[llength $run_command_cache] > 2000} { - set run_command_cache [lrange $run_command_cache 1750 end] - } - #----------------------------------------- - - #screen_last_char_add [string index $lastoutchar_codethread$lasterrchar_codethread end] "stdout/stderr" - - - #set lastoutchar [string index [punk::ansi::ansistrip $::repl::output_stdout] end] - #set lasterrchar [string index [punk::ansi::ansistrip $::repl::output_stderr] end] - - #to determine whether cursor is back at col0 of newline - #screen_last_char_add [string index $lastoutchar$lasterrchar end] "stdout/stderr" - - - #??? - #screen_last_char_add [string index $lastoutchar$lastoutchar_codethread$lasterrchar$lasterrchar_codethread end] "stdout/stderr" - screen_last_char_add [string index $lastoutchar_codethread$lasterrchar_codethread end] "stdout/stderr" - - - set result_is_chunk_list 0 - #------ - #todo - fix. It doesn't make much sense to only detect if the unknown command occurred in first word. - #e.g set x [something arg] not detected vs something arg - #also - unknown commands aren't the only things that can write directly to the os handles stderr & stdout - set last_unknown [tsv::get repl last_unknown] - if { - [string length $last_unknown] && \ - [string equal -length [string length $last_unknown] $last_unknown $line] - } { - #can't currently detect stdout/stderr writes from unknown's call to exec - #add a clearance newline for direct unknown calls for now - #there is usually output anyway - but we will get an extra blank line now even for a call that only had an exit code - # - # - set unknown_clearance "\n* repl newline" - screen_last_char_add "\uFFFF" clearance "clearance after direct unknown call" - if {[tsv::llength repl runchunks-$repl_runid]} { - if {$status == 0} { - set result [tsv::get repl runchunks-$repl_runid] ;#last_run_display - } else { - - } - set result_is_chunk_list 1 - } - } - #------ - #ok to use repl::screen_needs_clearance from here down.. (code smell - proc only valid use in narrow context) - #*********************************************************** - #rputs -nonewline stderr $unknown_clearance - if {$::punk::repl::debug_repl > 0} { - set lastcharinfo "\n" - set whatcol [string repeat " " 12] - foreach cinfo $::repl::screen_last_char_list { - lassign $cinfo c whatinfo whyinfo - set cdisplay [punk::ansi::ansistring::VIEW -lf 1 -vt 1 $c] - #assertion cdisplay has no raw newlines - if {[punk::char::ansifreestring_width $cdisplay] == 1} { - set cdisplay "$cdisplay " ;#make 2 wide - } - if {[string match repl-debugreport* $whatinfo]} { - #exclude noise debug_repl_emit - but still show the last_char - set whysummary "" - } else { - #set whysummary [string map [list \n "-n-"] $whyinfo] - set whysummary [punk::ansi::ansistring::VIEW -lf 1 -vt 1 $whyinfo] - } - set whatinfo [string range $whatinfo$whatcol 0 [string length $whatcol]] - append lastcharinfo "$cdisplay $whatinfo $whysummary\n" - } - debug_repl_emit "screen_last_chars: $lastcharinfo" - } - - debug_repl_emit "lastoutchar:'$lastoutchar' lasterrchar: '$lasterrchar'" - if {$status == 0} { - debug_repl_emit "command call status: $status OK" - } else { - debug_repl_emit "command call status: $status ERR" - } - - - - - #puts stderr "'$::repl::output_stdout' lastoutchar:'$lastoutchar' result:'$result'" - #$command is an unevaluated script at this point - # so may not be a well formed list e.g 'set x [list a "b"]' - #- lindex $command would sometimes fail - #if {[lindex $command 0] eq "runx"} {} - - - if { - [string equal -length [string length "d/ "] "d/ " $commandstr] || \ - [string equal "d/\n" $commandstr] || \ - [string equal -length [string length "dd/ "] "dd/ " $commandstr] || \ - [string equal "dd/\n" $commandstr] || \ - [string equal -length [string length "./ "] "./ " $commandstr] || \ - [string equal "./\n" $commandstr] || \ - [string equal -length [string length "../ "] "../ " $commandstr] || \ - [string equal "../\n" $commandstr] || \ - [string equal -length [string length "runx "] "runx " $commandstr] || \ - [string equal -length [string length "sh_runx "] "sh_runx " $commandstr] || \ - [string equal -length [string length "runout "] "runout " $commandstr] || \ - [string equal -length [string length "sh_runout "] "sh_runout " $commandstr] || \ - [string equal -length [string length "runerr "] "runerr " $commandstr] || \ - [string equal -length [string length "sh_runerr "] "sh_runerr " $commandstr] - - } { - if {[tsv::llength repl runchunks-$repl_runid]} { - set result [tsv::get repl runchunks-$repl_runid] ;#last_run_display - set result_is_chunk_list 1 - } - } - - # -- --- --- --- --- --- --- --- --- --- - ##an attempt to preserve underlying rep - ##this is not for performance - just to be less disruptive to underlying rep to aid in learning/debugging - # -- --- --- --- --- --- --- --- --- --- - # JN 2023 - The lrange operation is destructive to path internal representation - # The lrange operation is destructive to strings with leading/trailing newlines - # -- --- --- --- --- --- --- --- --- --- - #set saved_errorCode $::errorCode - #set saved_errorInfo $::errorInfo - #if {[catch {lrange $result 0 end} result_as_list]} { - # set is_result_empty [expr {$result eq ""}] - # set ::errorCode $saved_errorCode - # set ::errorInfo $saved_errorInfo - #} else { - # set is_result_empty [expr {[llength $result_as_list] == 0}] - #} - # -- --- --- --- --- --- --- --- --- --- - #set resultrep [::tcl::unsupported::representation $result] - - set is_result_empty [expr {$result eq ""}] - - #catch {puts stderr "yy--->[rep $::arglej]"} - - set reading 1 - if {!$is_result_empty} { - if {$status == 0} { - if {[screen_needs_clearance]} { - rputs -nonewline stderr \n - } - if {$result_is_chunk_list} { - foreach c $result { - lassign $c termchan text - if {[string length $text]} { - switch -- $termchan { - result { - #rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text] - set h [textblock::height $text] - set promptcol [string repeat $resultprompt\n $h] - set promptcol [string range $promptcol 0 end-1] - rputs [textblock::join_basic -- $promptcol $text] - - #puts -nonewline stdout $text - } - resulterr { - rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text] - } - info { - rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text] - } - default { - #rputs -nonewline $termchan $text - set chanprompt "_ " - rputs $termchan ${chanprompt}[string map [list \n "\n${chanprompt}"] $text] - } - } - } - } - - } else { - #----------------------------------------------------------- - # avoid repl forcing string rep of simple results. This is just to aid analysis using tcl::unsupported::representation - #set rparts [split $result {}] - #if {[lsearch $rparts \n] < 0} { - # #type of $result unaffected - # rputs "$resultprompt $result" - #} else { - # #$result will be a string due to use of string map - # rputs $resultprompt[string map [list \n "\n$resultprompt"] $result] - #} - #----------------------------------------------------------- - - #we have copied rawresult using append with empty string - so our string interaction with result var here shouldn't affect the returned value - #empty-string result handled in other branch - if {![tsv::llength repl runchunks-$repl_runid]} { - #write back to tsv var for use by punk::get_runchunks (underscore command) - tsv::set repl runchunks-$repl_runid [list [list result $result]] - } - set flat [string map [list \r\n "" \n ""] $result] - if {[string length $flat] == [string length $result]} { - #no line-endings in data - rputs "$resultprompt$result" - } else { - #if {[string index $result end] eq "\n"} { - # set result [string range $result 0 end-1] - #} - if {[string length $flat] == 0} { - if {[string range $result end-1 end] eq "\r\n"} { - set result [string range $result 0 end-2] - } else { - set result [string range $result 0 end-1] - } - } - #NOTE - textblock::height is the line height - not reflective of height of data with ansi-moves or things like sixels - set h [textblock::height $result] - set promptcol [string repeat $resultprompt\n $h] - set promptcol [string range $promptcol 0 end-1] - #promptcol is uniform-width lines, result may not be. We are ok to join with ragged rhs col here, so use join_basic instead of join - rputs [textblock::join_basic -- $promptcol $result] - - #orig - #rputs $resultprompt[string map [list \r\n "\n$resultprompt" \n "\n$resultprompt"] $result] - } - } - doprompt "P% " - } else { - #tcl err - if {$result_is_chunk_list} { - foreach c [tsv::get repl runchunks-$repl_runid] { - #last_run_display - lassign $c termchan text - if {[string length $text]} { - switch -- $termchan { - result { - rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text] - #puts -nonewline stdout $text - } - resulterr { - rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text] - } - info { - rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text] - } - default { - rputs -nonewline $termchan $text - } - } - } - } - } - - set c [a yellow bold] - set n [a] - rputs stderr $c$result$n - #tcl err hint prompt - lowercase - doprompt "p% " - } - } else { - #doprompt "P% " "green normal" - if {$linenum == 0} { - doprompt "P% " "green normal" - screen_last_char_add " " empty empty - } else { - doprompt "\nP% " "green normal" - screen_last_char_add "\n" empty empty ;#add \n to indicate noclearance required - } - } - #catch {puts stderr "zz1--->[rep $::arglej]"} - #puts stderr "??? $commandstr" - if {$::punk::repl::debug_repl > 0} { - incr ::punk::repl::debug_repl -1 - } - set commandstr "" - #catch {puts stderr "zz2---->[rep $::arglej]"} - - set lines [$editbuf lines] - set buf_has_data 0 - foreach ln $lines { - if {[string trim $ln] ne ""} { - set buf_has_data 1 - } - } - if {$buf_has_data} { - set editbufnext [punk::repl::class::class_editbuf new {}] - lappend editbuf_list $editbufnext - set editbuf_linenum_submitted 0 - set editbuf $editbufnext - } - #editbuf - - } else { - #append commandstr \n - if {$::punk::repl::signal_control_c} { - set ::punk::repl::signal_control_c 0 - chan event $inputchan readable {} - rputs stderr "* console_control: control-c" - flush stderr - set c [a yellow bold] - set n [a] - rputs stderr "${c}repl interrupted$n" - #set commandstr [list error "repl interrupted"] - set commandstr "" - doprompt ">_ " - flush stdout - - } else { - #Incomplete command - # parse and determine outermost unclosed quote/bracket and include in prompt - if {$linenum == $maxlinenum} { - if {$rawmode} { - #review - #we haven't put the data following last le into commandstr - but we want to display proper completion status prior to enter being hit or more data coming in. - #this could give spurious results for large pastes where buffering chunks it in odd places.? - #it does however give sensible output for the common case of a small paste where the last line ending wasn't included - set waiting [punk::lib::system::incomplete $commandstr[$editbuf line end]] - } else { - set waiting [punk::lib::system::incomplete $commandstr] - } - if {[llength $waiting]} { - set c [lindex $waiting end] - } else { - #set c " " - set c \u240a - } - doprompt ">$c " - } - } - } - - incr linenum - } - - - if {$maxlinenum == -1} { - #when in raw mode - no linefeed yet received - #rputs stderr "repl: no complete input line: $commandstr" - #screen_last_char_add "\n" empty empty - - screen_last_char_add [string index [$editbuf line end] end] stdinchunk stdinchunk - - } - - - #chan event $inputchan readable [list repl::repl_handler $inputchan $prompt_config] - #catch {puts stderr "zend--->[rep $::arglej]"} - - - } trap {POSIX} {e eopts} { - rputs stderr "trap POSIX '$e' eopts:'$eopts" - flush stderr - } on error {repl_error erropts} { - rputs stderr "error in repl_handler: $repl_error" - rputs stderr "-------------" - rputs stderr "$::errorInfo" - rputs stderr "-------------" - set stdinreader [chan event $inputchan readable] - if {![string length $stdinreader]} { - rputs stderr "*> $inputchan reader inactive" - } else { - rputs stderr "*> $inputchan reader active" - } - if {[chan eof $inputchan]} { - rputs stderr "todo - attempt restart of repl on input channel: $inputchan in next loop" - catch {set ::punk::ns::ns_current "::"} - #todo set flag to restart repl ? - } else { - rputs stderr "continuing.." - } - flush stderr - } -} - -proc repl::completion {context ebuf} { - -} - -namespace eval repl { - - - proc init {args} { - if {![info exists ::argv0]} { - #error out before we create a thread - punk requires this - review - error "::argv0 not set" - } - #in case -callback_interp wasn't explicitly defined - we make a guess based on how init was called as to whether this is being launched from a 'code' or root ("") interp. - if {[catch {info level -1} caller]} { - #todo logger - #puts "repl::init from: global" - set default_callback_interp "" - } else { - #puts "repl::init from: $caller" - set default_callback_interp "code" - } - variable codethread - variable codethread_cond - variable codethread_mutex - - set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] - foreach {k v} $args { - switch -- $k { - -force - -safe - -safelog - -paths - -callback_interp { - dict set opts $k $v - } - default { - error "repl::init unknown option '$k'. Known-options: [dict keys $opts]" - } - } - } - set opt_force [dict get $opts -force] - set opt_safe [dict get $opts -safe] - set opt_safelog [dict get $opts -safelog] - if {$opt_safelog eq "0"} { - set opt_safelog "" - } - if {[string is boolean -strict $opt_safelog]} { - if {$opt_safelog} { - set opt_safelog ::repl::interpextras::safe_msg - } - } - dict set opts -safelog $opt_safelog - - #If we are launching a repl from within an interp - we need to tell the childthread how to call-back to the parent repl - set opt_callback_interp [dict get $opts -callback_interp] - - if {$codethread ne "" && !$opt_force && [thread::exists $codethread] } { - error "repl:init codethread: $codethread already exists. use -force 1 to override" - } - set codethread [thread::create -preserved] - #review - naming of the possibly 2 cond variables parent and child thread - set codethread_cond [thread::cond create] ;#repl::codethread_cond held by parent(repl) vs punk::repl::codethread::replthread_cond held by child(codethread) - set codethread_mutex [thread::mutex create] - - - set scriptmap [list %args% [list $opts] \ - %argv0% [list $::argv0] \ - %argv% [list $::argv] \ - %argc% [list $::argc] \ - %replthread% [thread::id] \ - %replthread_cond% $codethread_cond \ - %replthread_interp% [list $opt_callback_interp] \ - %tmlist% [list [tcl::tm::list]] \ - %autopath% [list $::auto_path] \ - %lib_epoch% [list $::punk::libunknown::epoch]\ - ] - #scriptmap applied at end to satisfy silly editor highlighting. - set init_script { - set ::argv0 %argv0% - set ::argv %argv% - set ::argc %argc% - tcl::tm::remove {*}[tcl::tm::list] - tcl::tm::add {*}[lreverse %tmlist%] ;#Must be added in reverse order to get same order as original list! - - #this sets the auto_path in the thread but outside of the code interp that will be created. - #It will also need to be added in that interp - set ::auto_path %autopath% - set tclmajorv [lindex [split [tcl::info::tclversion] .] 0] - #jmn2 - #puts stdout "CODETHREAD tm list" - #puts stdout [join [tcl::tm::list] \n] - #puts stdout "====================" - #flush stdout - #puts stdout "CODETHREAD autopath" - #puts stdout [join $::auto_path \n] - #puts stdout "====================" - #flush stdout - #if {[llength [info commands tcl::zipfs::root]]} { - # set zipbase [file join [tcl::zipfs::root] app] ;#zipfs root has trailing slash - but file join does the right thing - # if {"$zipbase" in [tcl::zipfs::mount]} { - # puts stdout "//zipfs:/app/modules_tcl$tclmajorv exists: [file exists //zipfs:/app/modules_tcl$tclmajorv]" - # } - #} - #puts stdout "====================" - #----------------------------------------------------------------------------- - - namespace eval ::punk::libunknown {} - set ::punk::libunknown::epoch %lib_epoch% - - set libunks [list] - foreach tm_path [tcl::tm::list] { - set punkdir [file join $tm_path punk] - if {![file exists $punkdir]} {continue} - lappend libunks {*}[glob -nocomplain -dir $punkdir -type f libunknown-*.tm] - } - set libunknown "" - set libunknown_version_sofar "" - foreach lib $libunks { - #expecting to be of form libunknown-.tm - set vtail [lindex [split [file tail $lib] -] 1] - set thisver [file rootname $vtail] ;#file rootname x.y.z.tm - if {$libunknown_version_sofar eq ""} { - set libunknown_version_sofar $thisver - set libunknown $lib - } else { - if {[package vcompare $thisver $libunknown_version_sofar] == 1} { - set libunknown_version_sofar $thisver - set libunknown $lib - } - } - } - if {$libunknown ne ""} { - source $libunknown - if {[catch {punk::libunknown::init -caller "repl init_script"} errM]} { - puts "error initialising punk::libunknown\n$errM" - } - } - #----------------------------------------------------------------------------- - - package require punk::packagepreference - punk::packagepreference::install - - package require punk::args - package require Thread - package require snit - if {[catch {package require punk::icomm} errM]} { - puts stdout "---icomm $errM" - } - namespace eval ::punk::repl::codethread {} - #todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) - catch {package require tcl::chan::fifo2} - if {[catch { - #first use can raise error being a version number e.g 0.1.0 - why? - lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside - } errMsg]} { - #puts stdout "---tcl::chan::fifo2 error: $errM" - } else { - #puts stdout "transferring chan $replside to thread %replthread%" - #flush stdout - if {[catch { - #after 0 [list thread::transfer %replthread% $replside] - } errMsg]} { - #puts stdout "---thread::transfer error: $errMsg" - } - } - - package require punk::console - package require punk::repl::codethread - package require shellfilter - #package require shellrun - package require textblock - - #md5 uses open so can't be directly called in a safe interp - #it will need to delegate to a call here in the main interp of the codethread using an installed alias - set md5version [package require md5] - #we also need to 'package provide md5 $md5version' in the safe interp itself so that it won't override - - - #punk::configure_unknown ;#must be called because we hacked the tcl 'unknown' proc - - #child codethread (outside of code interp) needs to know details of the calling repl - set ::punk::repl::codethread::replthread %replthread% ;#point to thread id of parent thread (repl) - set ::punk::repl::codethread::replthread_cond %replthread_cond% - set ::punk::repl::codethread::replthread_interp %replthread_interp% - - # -- --- --- --- - #procs to alias into the codethread interp - #as we are doing string substitution on the whole block anyway, and these values are contant for the life of the thread, we may as well substitute hard values for things like replthread into these procs where possible - # -- --- --- --- - namespace eval ::repl::interphelpers { - proc quit {args} { - #child codethreads run in a 'code' interp therefore if they started another repl - it is within the 'code' interp in that thread - # whereas the first repl launched in the process runs in root interp "" - thread::send -async %replthread% [list interp eval %replthread_interp% ::punk::repl::quit] - } - proc editbuf args { - thread::send %replthread% [list punk::repl::editbuf {*}$args] - } - proc escapeeval {script} { - eval $script - } - proc do_after {args} { - if {[llength $args] == 1} { - return after {*}$args - } - set scr [lindex $args 1] - after [lindex $args 0] [list punk::repl::codethread::runscript $scr] - } - proc repl_ensemble_unknown args { - puts $args - if {[llength $args] == 1} { - return [namespace ensemble configure ::repl::interphelpers::repl_ensemble] - } - } - proc colour args { - set colour_state [thread::send %replthread% [list punk::console::colour]] - if {[llength $args]} { - #colour call was not a query - set new_state [thread::send %replthread% [list punk::console::colour {*}$args]] - if {[expr {$new_state}] ne [expr {$colour_state}]} { - interp eval code [list punk::console::colour {*}$args] ;#align code interp's view of colour state with repl thread - - #we don't want to run a raw script directly in our code interp if we're using variables - #because we will potentially collide with user vars in that context (or create vars there) - so use apply - interp eval code [list apply {docolour { - #adjust channel transform stack - if {!$docolour} { - set stackinfo [dict get [shellfilter::stack item stdout] stack] - set topstack [lindex $stackinfo 0] - if {[string match *::ansiwrap [dict get $topstack -transform]]} { - set sid [dict get $topstack -id] - shellfilter::stack::remove stdout $sid - } - set stackinfo [dict get [shellfilter::stack item stderr] stack] - set topstack [lindex $stackinfo 0] - if {[string match *::ansiwrap [dict get $topstack -transform]]} { - set sid [dict get $topstack -id] - shellfilter::stack::remove stderr $sid - } - - } else { - set configd $::punk::config::configdata - if {[string length [dict get $configd running color_stdout]]} { - #lappend ::codeinterp::outstack [shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]]] - shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $configd running color_stdout]] - } - if {[string length [dict get $configd running color_stderr]]} { - #lappend ::codeinterp::errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] - shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $configd running color_stderr]] - } - - } - }} $new_state] - } - return $new_state - } else { - return $colour_state - } - #todo - add/remove shellfilter stacked ansiwrap - } - proc vt52 {args} { - return [thread::send %replthread% [list punk::console::vt52 {*}$args]] - } - proc mode args { - #with tsv::set console is_raw we don't need to call mode in both the replthread and the codethread - # REVIEW - call in local interp? how about if codethread is safe interp? - #interp eval code [list ::punk::console::mode {*}$args] - thread::send %replthread% [list punk::console::mode {*}$args] - } - proc cmdtype cmd { - code invokehidden tcl:info:cmdtype $cmd - } - - #punk repl tsv wrappers - proc set_repl_last_unknown args { - tsv::set repl last_unknown {*}$args - } - proc get_repl_runid args { - if {[tsv::exists repl runid]} { - return [tsv::get repl runid] - } else { - return 0 - } - } - proc md5 args { - ::md5::md5 {*}$args - } - proc fconfigure {args} { - code invokehidden fconfigure {*}$args - } - proc fnormalize name { - code invokehidden tcl:file:normalize $name - } - proc fdirname name { - code invokehidden tcl:file:dirname $name - } - } - namespace eval ::repl::interpextras { - #install using safe::setLogCmd - proc safe_msg {msg} { - puts stderr "safebase: $msg" - } - } - - namespace eval ::repl::interphelpers::repl_ensemble { - namespace export {[a-z]*} - namespace ensemble create - namespace ensemble configure [namespace current] -unknown ::repl::interphelpers::repl_ensemble_unknown - variable replinfo - set replinfo [dict create thread %replthread% interp %replthread_interp%] - proc thread {} { - return %replthread% - } - proc info {} { - variable replinfo - return $replinfo - } - proc eval {script} { - thread::send %replthread% $script - } - proc stack {} { - set iname %replthread_interp% - set tid %replthread% - lappend stack [list thread $tid interp $iname] - while {$iname eq "code"} { - set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}] - set tid [thread::send $tid {set ::punk::repl::codethread::replthread}] - lappend stack [list thread $tid interp $iname] - } - return $stack - } - } - namespace eval ::repl::interphelpers::subshell_ensemble { - namespace export {[a-z]*} - namespace ensemble create - proc punk {} { - set ts_start [clock seconds] - set replresult [interp eval code { - package require punk::repl - repl::init -safe punk - repl::start stdin - }] - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - proc safe {args} { - set ts_start [clock seconds] - interp eval code { - package require punk::repl - } - interp eval code [list repl::init -safe safe {*}$args] - set replresult [interp eval code [list repl::start stdin]] - - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - proc safebase {args} { - set ts_start [clock seconds] - interp eval code { - package require punk::repl - } - set codethread [interp eval code [list repl::init -safe safebase {*}$args]] - puts stdout "safebase codethread:$codethread" - set replresult [interp eval code [list repl::start stdin]] - - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - proc punksafe {args} { - set ts_start [clock seconds] - interp eval code { - package require punk::repl - } - interp eval code [list repl::init -safe punksafe {*}$args] - set replresult [interp eval code [list repl::start stdin]] - - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - } - # -- --- --- --- --- - - #puts "codethread:[thread::id] parent replthread:%replthread%" - #flush stdout - - set args %args% - set safe [dict get $args -safe] - set safelog [dict get $args -safelog] - set paths [list] - if {[dict exists $args -paths]} { - set paths [dict get $args -paths] - } - - switch -- $safe { - safe { - interp create -safe -- code - package require punk::args - } - safebase { - safe::interpCreate code -nested 1 -autoPath %autopath% - code alias "file normalize" "file normalize" - code alias "file dirname" "file dirname" - code alias "file exists" "file exists" - code alias ::tcl::file::normalize ::tcl::file::normalize - code alias ::tcl::file::dirname ::tcl::file::dirname - code alias ::tcl::file::exists ::tcl::file::exists - #code alias ::punk::console::colour ::punk::console::colour - } - punksafe { - #less safe than safebase - we need file normalize and info script to handle modpod? - package require punk::safe - punk::safe::interpCreate code -autoPath %autopath% - code alias "file normalize" "file normalize" - code alias "file dirname" "file dirname" - code alias "file exists" "file exists" - code alias ::tcl::file::normalize ::tcl::file::normalize - code alias ::tcl::file::dirname ::tcl::file::dirname - code alias ::tcl::file::exists ::tcl::file::exists - code alias ::punk::console::colour ::punk::console::colour - } - punk - 0 { - interp create code - code eval [list namespace eval ::punk::libunknown {}] - catch { - #JJJ REVIEW. - #If libunknown was loaded when packages already in the package database - #then the epoch info may be wrong. - code eval [list set ::punk::libunknown::epoch $::punk::libunknown::epoch] - } - } - punkisland { - interp create code - #todo - #when no island paths specified - should be like safebase, but without folder hiding and with expanded read to ::auto_path folders - } - } - - interp eval code { - namespace eval codeinterp { - variable errstack {} - variable outstack {} - variable run_command_cache - proc set_clone {varname obj} { - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - } - } - - switch -- $safe { - safe { - if {[llength $paths]} { - package require punk::island - foreach p $paths { - punk::island::add code $p - } - } - interp share "" stdout code - interp share "" stderr code - interp share "" stdin code ;#needed for ANSI queries - - set codehidden [code hidden] - code alias file file - if {"source" in $codehidden} { - code expose source - } - if {"encoding" in $codehidden} { - code expose encoding ;#leave enabled - } - if {"tcl:encoding:system" in $codehidden} { - code expose tcl:encoding:system - code eval {rename ::tcl::encoding::system ""} - code eval {rename tcl:encoding:system ::tcl::encoding::system} - } - #interp alias is available in safe - so it seems unreasonable to disallow 'info cmdtype' - if {"tcl:info:cmdtype" in $codehidden} { - code eval {rename ::tcl::info::cmdtype ""} - code expose tcl:info:cmdtype - code eval {rename tcl:info:cmdtype ::tcl::info::cmdtype} - } - set pkgs [list\ - punk::args\ - punk::pipe\ - cmdline\ - struct::list\ - struct::set\ - textutil::wcswidth\ - textutil::trim\ - textutil::repeat\ - textutil::tabify\ - textutil::split\ - textutil::string\ - textutil::adjust\ - textutil\ - punk::encmime\ - punk::char\ - punk::assertion\ - punk::ansi\ - punk::lib\ - overtype\ - dictutils\ - debug\ - punk::ns\ - textblock\ - punk::args::tclcore\ - punk::aliascore\ - ] - - #pattern looks up versions available of patternlib before loading (but we don't have an index for tm files) todo fix pattern. - # patterncmd\ - # metaface\ - # patternpredator2\ - # patternlib\ - # pattern - - # - no longer required by textblock - # term::ansi::code\ - # term::ansi::code::attr\ - # term::ansi::code::ctrl\ - # term::ansi::code::macros - - #---------- - #all this scanning and loading core packages - we should possibly cache the file data for other interps? - #make sure codethread has scanned for packages - must do for each namespace level - #catch {package require flubber_nonexistent} - set ns_scanned [dict create] - #---------- - set prior_infoscript [code eval {info script}] ;#probably empty that's ok - foreach pkg $pkgs { - if {[catch { - set nsquals [namespace qualifiers $pkg] - if {$nsquals ne ""} { - if {![dict exists $ns_scanned $nsquals]} { - catch {package require $pkg 1-0} ;#force scan with nonsatisfiable version - dict set ns_scanned $nsquals 1 - } - } - set versions [lsort -command {package vcompare} [package versions $pkg]] - if {[llength $versions]} { - set v [lindex $versions end] - set path [lindex [package ifneeded $pkg $v] end] - if {[file extension $path] in {.tcl .tm}} { - if {![catch {readFile $path} data]} { - code eval [list info script $path] - code eval $data - code eval [list info script $prior_infoscript] - } else { - error "safe - failed to read $path" - } - } else { - error "safe - refusing to attempt load of $pkg from $path - (allowed extensions .tcl .tm)" - } - } else { - error "safe - no versions of $pkg found" - } - } errMsg]} { - puts stderr "safe - failed to load package $pkg\n$errMsg\n$::errorInfo" - } else { - #puts stdout "safe - loaded $pkg from $path" - } - } - code alias file "" - code hide source - - - #review argv0,argv,argc - #interp eval code { - # set ::argv0 %argv0% - # set ::auto_path %autopath% - #} - interp eval code [list set ::tcl_platform(os) $::tcl_platform(os)] - interp eval code [list set ::tcl_platform(osVersion) $::tcl_platform(osVersion)] - interp eval code [list set ::tcl_platform(machine) $::tcl_platform(machine)] - if {"stdout" in [chan names]} { - interp share {} stdout code - } else { - interp share {} [shellfilter::stack::item_tophandle stdout] code - } - if {"stderr" in [chan names]} { - interp share {} stderr code - } else { - interp share {} [shellfilter::stack::item_tophandle stderr] code - } - - #review - code alias ::shellfilter::stack ::shellfilter::stack - #code alias ::punk::lib::set_clone ::punk::lib::set_clone - #code alias ::aliases ::punk::lib::aliases - code alias ::punk::lib::aliases ::punk::lib::aliases - namespace eval ::codeinterp {} - - code alias ::md5::md5 ::repl::interphelpers::md5 - code alias exit ::repl::interphelpers::quit - } - safebase { - #safebase - #safe::setLogCmd ::repl::interpextras::safe_msg ;#using setLogcmd early will show the auto_path notice - which is *verbose* - #while it may conceivably be useful in debugging safe itself - auto_path and tcl::tm::list can be inspected to show these values in the safe interp itself anyway - so early logging is of limited utility here. - if {[llength $paths]} { - package require punk::island - foreach p $paths { - punk::island::add code $p - } - } - interp eval code { - set ::argv0 %argv0% - set ::argc 0 - set ::argv {} - #puts stdout "safebase interp" - #flush stdout - } - interp eval code [list set ::tcl_platform(os) $::tcl_platform(os)] - interp eval code [list set ::tcl_platform(osVersion) $::tcl_platform(osVersion)] - interp eval code [list set ::tcl_platform(machine) $::tcl_platform(machine)] - - code alias ::fconfigure ::repl::interphelpers::fconfigure ;#needed for shellfilter - - #code invokehidden package require punk::lib - if {"stdout" in [chan names]} { - interp share {} stdout code - } else { - interp share {} [shellfilter::stack::item_tophandle stdout] code - } - if {"stderr" in [chan names]} { - interp share {} stderr code - } else { - interp share {} [shellfilter::stack::item_tophandle stderr] code - } - interp eval code { - package require punk::lib - package require textblock - } - - #JMN - interp eval code { - package require shellfilter - } - - - #work around bug in safe base which won't load Tcl libs that have deeper nesting - #(also affects tcllib page/plugins folder) - set termversions [package versions term] - set termv [lindex $termversions end] - if {$termv ne ""} { - set path [lindex [package ifneeded term $termv] end] ;#assuming path at end of something like "source .../term.tcl" - set termbase [file dirname $path] - safe::interpAddToAccessPath code [file join $termbase ansi] - safe::interpAddToAccessPath code [file join $termbase ansi code] - } - #safe::interpAddToAccessPath code NUL - if {$safelog ne ""} { - #setting setLogCmd here gives potentially interesting feedback regarding behaviour of things such as glob - safe::setLogCmd $safelog - } - #code invokehidden source c:/repo/jn/shellspy/modules/punk/lib-0.1.1.tm - - code alias detok ::safe::DetokPath code ;#temp - this violates the point of the {$p(:X:)} paths - - #review - exit should do something slightly different - # see ::safe::interpDelete - code alias exit ::repl::interphelpers::quit - - code alias ::md5::md5 ::repl::interphelpers::md5 - code alias ::file ::file - interp eval code [list package provide md5 $md5version] - - } - punksafe { - interp eval code { - set ::argv0 %argv0% - set ::argc 0 - set ::argv {} - #set ::auto_path %autopath% ;#jmn - #tcl::tm::remove {*}[tcl::tm::list] - #tcl::tm::add {*}[lreverse %tmlist%] - } - interp eval code [list set ::tcl_platform(os) $::tcl_platform(os)] - interp eval code [list set ::tcl_platform(osVersion) $::tcl_platform(osVersion)] - interp eval code [list set ::tcl_platform(machine) $::tcl_platform(machine)] - - - code alias ::fconfigure ::repl::interphelpers::fconfigure ;#needed for shellfilter - - if {"stdout" in [chan names]} { - interp share {} stdout code - } else { - interp share {} [shellfilter::stack::item_tophandle stdout] code - } - if {"stderr" in [chan names]} { - interp share {} stderr code - } else { - interp share {} [shellfilter::stack::item_tophandle stderr] code - } - - interp eval code { - package require punk::lib - package require punk::args - package require punk::args::tclcore - package require textblock - } - - interp eval code { - if {[catch { - #package require packagetrace - #packagetrace::init - } errM]} { - puts stderr "========================" - puts stderr "code interp error 1:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - #error "$errM" - } - } - - - interp eval code { - if {[catch { - package require punk::config ;#requires: none - #package require punk::console ;#requires: Thread,punk::ansi,punk::args - #set running_config $::punk::config::running - package require shellfilter ;#requires: shellthread,Thread - apply {running_config { - if {[string length [dict get $running_config color_stderr]] && [punk::console::colour]} { - shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]] - } - if {[string length [dict get $running_config color_stdout]] && [punk::console::colour]} { - shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]] - } - }} [punk::config::configure running] - - } errM]} { - puts stderr "========================" - puts stderr "code interp error 2:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - error "$errM" - } - } - - interp eval code { - if {[catch { - - #puts stderr "loading natsort" - #natsort has 'application mode' which can exit. - #Requiring it shouldn't trigger application - but zipfs/vfs interactions confused it in some early versions - package require natsort - - #package require punk ;# Thread - #package require shellrun ;#subcommand exists of file - - - #----------------------------------------------------------------------------------------------------------------------------------------- - package require punk::ns ;#requires:punk::lib,punk::args,struct::list,cmdline+(tcllibc),struct::set,punk::ansi,punk::char, - #textutil,textutil::string,textutil::adjust,textutil::repeat,textutil::string,textutil::split,textutil::tabify,textutil::wcswidth - #punk::encmime,punk::assertion - #twapi,platform,registry,debug,overtype,patternpunk,pattern,patterncmd,metaface,patternpredator2,patternlib,dictutils - #----------------------------------------------------------------------------------------------------------------------------------------- - - #package require textblock - } errM]} { - puts stderr "========================" - puts stderr "code interp error 3:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - error "$errM" - } - - } - - } - punk - 0 { - interp eval code { - #safe !=1 and safe !=2, tmlist: %tmlist% - set ::argv0 %argv0% - set ::argv %argv% - set ::argc %argc% - set ::auto_path %autopath% - tcl::tm::remove {*}[tcl::tm::list] - tcl::tm::add {*}[lreverse %tmlist%] - #puts "code interp chan names-->[chan names]" - - #----------------------------------------------------------------------------- - if {[package provide punk::libunknown] eq ""} { - set libunks [list] - foreach tm_path [tcl::tm::list] { - set punkdir [file join $tm_path punk] - if {![file exists $punkdir]} {continue} - lappend libunks {*}[glob -nocomplain -dir $punkdir -type f libunknown-*.tm] - } - set libunknown "" - set libunknown_version_sofar "" - foreach lib $libunks { - #expecting to be of form libunknown-.tm - set vtail [lindex [split [file tail $lib] -] 1] - set thisver [file rootname $vtail] ;#file rootname x.y.z.tm - if {$libunknown_version_sofar eq ""} { - set libunknown_version_sofar $thisver - set libunknown $lib - } else { - if {[package vcompare $thisver $libunknown_version_sofar] == 1} { - set libunknown_version_sofar $thisver - set libunknown $lib - } - } - } - if {$libunknown ne ""} { - source $libunknown - if {[catch {punk::libunknown::init -caller "repl init_script punk"} errM]} { - puts "error initialising punk::libunknown\n$errM" - } - } - } else { - puts stderr "punk::libunknown [package provide punk::libunknown] already loaded" - } - #----------------------------------------------------------------------------- - - # -- --- - #review - #we have to blow some time on a rescan to provide more deterministic ordering (match behaviour of initial thread regarding pkg precedence) - #review - can we speed that scan up? - ##catch {package require flobrudder-nonexistant} - # -- --- - - set tsstart [clock millis] - if {[catch { - package require vfs - package require vfs::zip - } errM]} { - puts stderr "repl code interp FAILED to load vfs,vfs::zip lag:[expr {[clock millis] - $tsstart}]" - } else { - puts stderr "repl code interp loaded vfs,vfs::zip lag:[expr {[clock millis] - $tsstart}]" - } - #puts stderr "package unknown: [package unknown]" - - #puts stderr ----- - #puts stderr [join $::auto_path \n] - #puts stderr ----- - - if {[catch { - package require punk::args - package require punk::config - package require punk::ns - #puts stderr "loading natsort" - #natsort has 'application mode' which can exit. - #Requiring it shouldn't trigger application - but zipfs/vfs interactions confused it in some early versions - package require natsort - #catch {package require packageTrace} - package require punk - #package require punk::args - package require punk::args::tclcore - package require shellrun - package require shellfilter - #set running_config $::punk::config::running - apply {running_config { - if {[string length [dict get $running_config color_stderr]] && [punk::console::colour]} { - shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]] - } - if {[string length [dict get $running_config color_stdout]] && [punk::console::colour]} { - shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]] - } - }} [punk::config::configure running] - - package require textblock - } errM]} { - puts stderr "========================" - puts stderr "code interp error:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - error "$errM" - } - } - } - default { - } - } - code alias repl ::repl::interphelpers::repl_ensemble - code alias subshell ::repl::interphelpers::subshell_ensemble - code alias quit ::repl::interphelpers::quit - code alias editbuf ::repl::interphelpers::editbuf - code alias colour ::repl::interphelpers::colour - code alias color ::repl::interphelpers::colour - code alias mode ::repl::interphelpers::mode - code alias vt52 ::repl::interphelpers::vt52 - #code alias after ::repl::interphelpers::do_after - - code alias ::punk::set_repl_last_unknown ::repl::interphelpers::set_repl_last_unknown - code alias ::punk::get_repl_runid ::repl::interphelpers::get_repl_runid - - #JMN - #code alias cmdtype ::repl::interphelpers::cmdtype - #temporary debug aliases - deliberate violation of safety provided by safe interp - code alias escapeeval ::repl::interphelpers::escapeeval - - - #experiment - #code alias ::shellfilter::stack ::shellfilter::stack - - #puts stderr "returning threadid" - #puts stderr [thread::id] - - thread::id - } - set init_script [string map $scriptmap $init_script] - - - #thread::send $codethread $init_script - if {![catch { - thread::send $codethread $init_script result ;#use a result var when wrapping in a catch - without it we can get a return code of 2 (TCL_RETURN) - } errMsg]} { - return $result - } else { - puts stderr "repl::init Failed during thread::send" - puts stderr "$::errorInfo" - thread::release $codethread - error $errMsg - } - } - #init - don't auto init - require init with possible options e.g -safe -} -package provide punk::repl [namespace eval punk::repl { - variable version - set version 0.1.1 -}] - -#repl::start $program_read_stdin_pipe - - - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm index c8f7fc3a..91f7a31a 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm @@ -21,6 +21,7 @@ unset stdin_info # ----------------------------------- + #------------------------------------------------------------------------------------- if {[package provide punk::libunknown] eq ""} { #maintenance - also in src/vfs/_config/punk_main.tcl @@ -162,6 +163,13 @@ namespace eval punk::repl { #todo - key on shell/subshell tsv::set repl runchunks-0 [list] ;#last_run_display + variable frametype + set frametype ascii; #conservative default + #if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} { + # if {$testcharwidth == 1} { + # set frametype light + # } + #} variable debug_repl 0 variable signal_control_c 0 @@ -424,7 +432,7 @@ proc repl::start {inchan args} { variable codethread #review if {$codethread eq ""} { - error "start - no codethread. call init first. (options -safe 0|1)" + error "start - no codethread. call init first. (options -type 0|1)" } variable commandstr @@ -470,7 +478,7 @@ proc repl::start {inchan args} { #set ::punk::repl::codethread::running 1 #the interp in which commands such as d/ run - #we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values + #we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values #punk::repl::codethread::running is required whether safe or not. interp eval code { namespace eval ::punk::repl::codethread {} @@ -682,7 +690,7 @@ proc repl::reopen_stdinX {} { } -#add to sliding buffer of last x chars emmitted to screen by repl +# add to sliding buffer of last x chars emmitted to screen by repl #(we could maintain only one char - more kept merely for debug assistance) #will not detect emissions from exec with stdout redirected and presumably some extensions etc proc repl::screen_last_char_add {c what {why ""}} { @@ -991,6 +999,8 @@ namespace eval punk::repl::class { dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered] } method add_chunk {chunk} { + upvar ::punk::repl::frametype frametype + #we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw] @@ -1065,7 +1075,7 @@ namespace eval punk::repl::class { append debug \n [showdict $dinfo] append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col" package require textblock - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] + set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug] if {![punk::console::vt52]} { catch {punk::console::move_emitblock_return $debug_first_row 1 $debug} } else { @@ -1148,7 +1158,7 @@ namespace eval punk::repl::class { set debug "add_chunk$i" append debug \n $mergedinfo append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]" - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] + set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug] #catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug} set result [dict get $mergedinfo result] @@ -1816,6 +1826,8 @@ proc punk::repl::console_debugview {editbuf consolewidth args} { } package require textblock variable debug_repl + variable frametype + if {$debug_repl <= 0} { return [dict create width 0 height 0 topleft {}] } @@ -1850,11 +1862,11 @@ proc punk::repl::console_debugview {editbuf consolewidth args} { } set debug_height [expr {[llength $lines]+2}] ;#framed height } errM]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] set debug_height [textblock::height $info] } else { #treat as ephemeral (unreusable) frames due to varying width & height - therefore set -buildcache 0 - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] } set debug_width [textblock::widthtopline $info] @@ -1881,6 +1893,8 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { package require textblock upvar ::repl::editbuf_list editbuf_list + variable frametype + set defaults {-row 10 -rightmargin 0} set opts [dict merge $defaults $args] set opt_row [dict get $opts -row] @@ -1894,14 +1908,14 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { set info [punk::lib::list_as_lines $lines] } } editbuf_error]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] } else { set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]" append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]" set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]" set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]" set info [a+ green bold]$row1\n$row2[a]\n$info - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] + set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] } set editbuf_width [textblock::widthtopline $info] set spacepatch [textblock::block $editbuf_width 2 " "] @@ -1915,8 +1929,10 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} { return [dict create width $editbuf_width] } + proc punk::repl::console_controlnotification {message consolewidth consoleheight args} { package require textblock + variable frametype set defaults {-bottommargin 0 -rightmargin 0} set opts [dict merge $defaults $args] set opt_bottommargin [dict get $opts -bottommargin] @@ -1924,8 +1940,14 @@ proc punk::repl::console_controlnotification {message consolewidth consoleheight set messagelines [split $message \n] set message [lindex $messagelines 0] ;#only allow single line set info "[a+ bold red]$message[a]" - set hlt [dict get [textblock::framedef light] hlt] - set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] + + if {$frametype eq "ascii"} { + set box [textblock::frame -type ascii -checkargs 0 -boxmap [list tlc + trc +] -title $message -height 1] + } else { + set hlt [dict get [textblock::framedef light] hlt] + set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] + } + set notification_width [textblock::widthtopline $info] set box_offset [expr {$consolewidth - $notification_width - $opt_rightmargin}] set row [expr {$consoleheight - $opt_bottommargin}] @@ -1939,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config } else { set is_vt52 0 } + variable codethread variable loopinstance incr loopinstance @@ -2069,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config set chunk "\b\x7f\b\x7f" } elseif {$chunk eq "\x1c"} { #ctrl-bslash + #This is commonly used in terminals as a 'harder' ctrl-c. #try to brutally terminate process #attempt to leave terminal in a reasonable state - mode line ;#may be aliased to ::repl::interphelpers::mode - after 250 {exit 42} + punk::console::mode line + #for now - exit with small delay for tidyup + after 1000 {exit 43} return } elseif {$chunk eq "\x1a"} { - #for now - exit with small delay for tidyup #ctrl-z #::punk::repl::handler_console_control "ctrl-z_via_rawloop" - if {[catch {punk::console::mode line}]} { - #REVIEW - interp eval code {punk::console::mode line} + #JMN + #set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}] + set iname $::punk::repl::codethread::replthread_interp + #only the highest level subshell has an interp name of empty string (lower levels are named 'code') + if {$iname eq ""} { + punk::console::mode line } - after 1000 {exit 43} + after 250 [list thread::send $codethread [list interp eval code {quit 42}]] return } @@ -2096,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #if {$chunk eq "\x1b\[C"} { #} + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_off flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- $editbuf add_chunk $chunk @@ -2208,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config lappend input_chunks_waiting($inputchan) $waiting } } + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_on flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- if {$editbuf_linenum_submitted == 0} { #(there is no line 0 - lines start at 1) @@ -2237,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap1 POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error1 in repl_handler: $repl_error" + rputs stderr "error1 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "erroropts: $erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2375,6 +2406,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #set commandstr "set ::punk::repl::debug_repl" set commandstr "" } + + + if {$::punk::repl::debug_repl > 100} { proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { set p %p% @@ -2394,12 +2428,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs debugreport $clearance$p[string map [list \n \n$p] $msg] }] set info "" - append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" - append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" + append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" + append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n" set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]] append info "lastrunchunks\n" - append info "chunks: [llength $lastrunchunks]\n" - append info "namespace: $::punk::nav::ns::ns_current" + append info "chunks : [llength $lastrunchunks]\n" + #JMN + set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]] + append info "codethread namespace: $codethread_ns\n" + append info "stdinlines : [llength $stdinlines] lines\n" + foreach ln $stdinlines { + append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n" + } + append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n" + #append info "namespace: $::punk::nav::ns::ns_current" debug_repl_emit $info } else { proc debug_repl_emit {msg} {return} @@ -2444,11 +2486,16 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config # lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] #} - variable codethread variable codethread_cond variable codethread_mutex - lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + #JJJJ JMN test + #REVIEW + #lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + + #thread::transfer $codethread stderr #chan configure stdout -buffering none @@ -2873,8 +2920,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config if {[llength $waiting]} { set c [lindex $waiting end] } else { - #set c " " - set c \u240a + set c \u240a ;#unicode linefeed symbol. } doprompt ">$c " } @@ -2903,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error in repl_handler: $repl_error" + rputs stderr "error2 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "$erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2950,10 +2996,10 @@ namespace eval repl { variable codethread_cond variable codethread_mutex - set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] + set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] foreach {k v} $args { switch -- $k { - -force - -safe - -safelog - -paths - -callback_interp { + -force - -type - -safelog - -paths - -callback_interp { dict set opts $k $v } default { @@ -2962,7 +3008,7 @@ namespace eval repl { } } set opt_force [dict get $opts -force] - set opt_safe [dict get $opts -safe] + set opt_repltype [dict get $opts -type] set opt_safelog [dict get $opts -safelog] if {$opt_safelog eq "0"} { set opt_safelog "" @@ -2986,16 +3032,17 @@ namespace eval repl { set codethread_mutex [thread::mutex create] - set scriptmap [list %args% [list $opts] \ - %argv0% [list $::argv0] \ - %argv% [list $::argv] \ - %argc% [list $::argc] \ - %replthread% [thread::id] \ - %replthread_cond% $codethread_cond \ - %replthread_interp% [list $opt_callback_interp] \ - %tmlist% [list [tcl::tm::list]] \ - %autopath% [list $::auto_path] \ - %lib_epoch% [list $::punk::libunknown::epoch]\ + set scriptmap [list %args% [list $opts] {*}{ + } %argv0% [list $::argv0] {*}{ + } %argv% [list $::argv] {*}{ + } %argc% [list $::argc] {*}{ + } %replthread% [thread::id] {*}{ + } %replthread_cond% $codethread_cond {*}{ + } %replthread_interp% [list $opt_callback_interp] {*}{ + } %tmlist% [list [tcl::tm::list]] {*}{ + } %autopath% [list $::auto_path] {*}{ + } %lib_epoch% [list $::punk::libunknown::epoch] {*}{ + } ] #scriptmap applied at end to satisfy silly editor highlighting. set init_script { @@ -3076,46 +3123,50 @@ namespace eval repl { package require punk::args #package require Thread - if {[catch {package require thread} errM]} { - puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" + #if {[catch {package require thread} errM]} { + # puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" if {[catch {package require Thread} errM2]} { puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2" puts stdout ">>repl::init auto_path : $::auto_path" puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]" } - } + #} + + namespace eval ::punk::repl::codethread {} + # ------------------------------------------------------------------------------------------------------------------------------------------------------- #----- #review - icomm as a possible way to talk to thread outside of the code interp. #thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow #thread::send to caller defined interp targets (reference?) #snit required for icomm - if {[catch {package require snit} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" - } - if {[catch {package require punk::icomm} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" - } + #if {[catch {package require snit} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" + #} + #if {[catch {package require punk::icomm} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" + #} #----- - - namespace eval ::punk::repl::codethread {} - #todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) - catch {package require tcl::chan::fifo2} - if {[catch { - #first use can raise error being a version number e.g 0.1.0 - why? - lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside - } errMsg]} { - puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" - } else { - #experimental? - #puts stdout "transferring chan $replside to thread %replthread%" - #flush stdout - #if {[catch { - # #after 0 [list thread::transfer %replthread% $replside] - #} errMsg]} { - # #puts stdout "---thread::transfer error: $errMsg" - #} - } + #---------------------------------------------------------------------------------------------------------------------- + # todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) + # catch {package require tcl::chan::fifo2} + # if {[catch { + # #first use can raise error being a version number e.g 0.1.0 - why? + # lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside + # } errMsg]} { + # puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" + # } else { + # #experimental? + # #puts stdout "transferring chan $replside to thread %replthread%" + # #flush stdout + # # if {[catch { + # # #after 0 [list thread::transfer %replthread% $replside] + # # } errMsg]} { + # # #puts stdout "---thread::transfer error: $errMsg" + # # } + # } + #---------------------------------------------------------------------------------------------------------------------- + # ------------------------------------------------------------------------------------------------------------------------------------------------------- package require punk::console package require punk::repl::codethread @@ -3296,7 +3347,7 @@ namespace eval repl { set ts_start [clock seconds] set replresult [interp eval code { package require punk::repl - repl::init -safe punk + repl::init -type punk repl::start stdin }] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3306,7 +3357,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe safe {*}$args] + interp eval code [list repl::init -type safe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3316,7 +3367,7 @@ namespace eval repl { interp eval code { package require punk::repl } - set codethread [interp eval code [list repl::init -safe safebase {*}$args]] + set codethread [interp eval code [list repl::init -type safebase {*}$args]] puts stdout "safebase codethread:$codethread" set replresult [interp eval code [list repl::start stdin]] @@ -3327,7 +3378,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe punksafe {*}$args] + interp eval code [list repl::init -type punksafe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3339,14 +3390,14 @@ namespace eval repl { #flush stdout set args %args% - set safe [dict get $args -safe] + set repltype [dict get $args -type] set safelog [dict get $args -safelog] set paths [list] if {[dict exists $args -paths]} { set paths [dict get $args -paths] } - switch -- $safe { + switch -- $repltype { safe { interp create -safe -- code code eval [list namespace eval ::punk::libunknown {}] @@ -3407,7 +3458,7 @@ namespace eval repl { #todo a specific punk::libunknown 'package unknown' handler for safe interps #pull in code via calls to source cached code? - switch -- $safe { + switch -- $repltype { safe { if {[llength $paths]} { package require punk::island @@ -3760,8 +3811,27 @@ namespace eval repl { } punk - 0 { + #------------------------------------------------------------------------ + #Test - experimental 2026 + if {"stdout" in [chan names]} { + interp share {} stdout code + } else { + interp share {} [shellfilter::stack::item_tophandle stdout] code + } + if {"stderr" in [chan names]} { + interp share {} stderr code + } else { + interp share {} [shellfilter::stack::item_tophandle stderr] code + } + foreach nm {shellspyout shellspyerr} { + set thandle [shellfilter::stack::item_tophandle $nm] + if {$thandle ne ""} { + interp share {} $thandle code + } + } + #------------------------------------------------------------------------ interp eval code { - #safe !=1 and safe !=2, tmlist: %tmlist% + #repltype !=1 and repltype !=2, tmlist: %tmlist% set ::argv0 %argv0% set ::argv %argv% set ::argc %argc% @@ -3941,8 +4011,8 @@ namespace eval repl { thread::id } set init_script [string map $scriptmap $init_script] - #REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args% - #we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that? + #REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args% + #we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that? #thread::send $codethread $init_script if {![catch { @@ -3957,7 +4027,7 @@ namespace eval repl { error $errMsg } } - #init - don't auto init - require init with possible options e.g -safe + #init - don't auto init - require init with possible options e.g -type } package provide punk::repl [namespace eval punk::repl { variable version diff --git a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.tm deleted file mode 100644 index a99f4757..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.tm +++ /dev/null @@ -1,3415 +0,0 @@ - -#punk linerepl -#todo - make repls configurable/pluggable packages - - -#list/string-rep bug -global run_commandstr "" - -set stdin_info [chan configure stdin] -if {[dict exists $stdin_info -inputmode]} { - #this is the only way I currently know to detect console on windows.. doesn't work on Alma linux. - # tcl_interactive used by repl to determine if stderr output prompt to be printed. - # (that way, piping commands into stdin should not produce prompts for each command) - set tcl_interactive 1 -} -#however, the -mode option only seems to appear on linux when a terminal exists.. -if {[dict exists $stdin_info -mode]} { - set tcl_interactive 1 -} -#give up for now -set tcl_interactive 1 - - - - - - -package require Thread -package require shellfilter -#package require shellrun -#package require punk -package require punk::lib -package require punk::args -package require punk::aliascore -if {[catch {punk::aliascore::init} errM]} { - puts stderr "punk::aliascore::init error: $errM" -} -package require punk::config -package require punk::ns -package require punk::ansi -package require punk::console -package require textblock - - - - - -if {![info exists ::env(SHELL)]} { - set ::env(SHELL) punk86 -} -if {![info exists ::env(TERM)]} { - # tset -r seems to rely on env(TERM) - so this doesn't seem to work - #if {![catch {exec tset -r} result]} { - # #e.g Terminal type is xterm-256color. - # set t [string trimright [lindex $result end] .] - # set ::env(TERM) $t - #} else { - #fake it ? - #set ::env(TERM) vt100 - set ::env(TERM) xterm-256color - #} -} - -#todo - move to less generic namespace ie punk::repl -namespace eval repl { - variable codethread - if {![info exists codethread]} { - set codethread "" - } - variable codethread_cond - - variable screen_last_chars "" ;#a small sliding append buffer for last char of any screen output to detect \n vs string - variable screen_last_char_list [list] - - #variable last_unknown "" - tsv::set repl last_unknown "" - tsv::set console is_raw 0 - variable output "" - #important not to initialize - as it can be preset by cooperating package before app-punk has been package required - #(this is an example of a deaddrop) - variable post_script -} -namespace eval punk::repl::class { - oo::class create con { - variable o_data ;#dict - constructor {} { - set o_data [dict create] - } - method info {} { - return [dict info $o_data] - } - } -} -namespace eval punk::repl { - tsv::set repl runid 0 - - #todo - key on shell/subshell - tsv::set repl runchunks-0 [list] ;#last_run_display - - - variable debug_repl 0 - variable signal_control_c 0 - variable signal_control_c_msg "" - variable prompt_reset_flag 0 ;#trigger repl to re-retrieve prompt settings - - proc todo {} { - puts "tcl History" - puts "repltelemetry package" - puts "deaddrop package for a consistent way for modules to leave small notes to others that may come later." - } - - #since we are targeting Tcl 8.6+ - we should be using 'interp bgerror .' - #todo - make optional/configurable? - proc bgerror2 {args} { - puts stderr "====================" - puts stderr "punk::repl::bgerror" - puts stderr "====================" - puts stderr "[lindex $args 0]" - puts stderr "-------------------" - puts stderr "[lrange $args 1 end]" - puts stderr "====================" - puts stderr "^^^^^^^^^^^^^^^^^^^" - } - proc bgerror {args} { - set message [lindex $args 0] - set errdict [lindex $args 1] - puts stderr "\n*> repl background error: '$message'" - #puts stderr "*> [set ::errorInfo]" - puts stderr "*> errorinfo: [dict get $errdict -errorinfo]" - set stdinreader [fileevent stdin readable] - if {![string length $stdinreader]} { - puts stderr "*> stdin reader inactive" - } else { - puts stderr "*> stdin reader active" - } - flush stderr - } - - if {![llength [info commands ::bgerror]]} { - #interp alias {} bgerror {} ::punk::repl::bgerror - } - interp bgerror "" ::punk::repl::bgerror - -} -namespace eval repl { - -} - -proc ::punk::repl::init_signal_handlers {} { - if {$::tcl_platform(platform) eq "windows"} { - #puts stdout "===============repl loading twapi===========" - if {![catch {package require twapi}]} { - - #If script launched with windows batch file - we have to be careful to stop a ctrl-c from eventually reaching the batch file when the program terminates, even if fully handled here. - #This is done from within the launching batch file - proc ::punk::repl::handler_console_control {args} { - variable signal_control_c - flush stderr - variable signal_control_c_msg - switch -- [lindex $args 0] { - ctrl-c { - #puts stderr "->event $args" - flush stderr - incr signal_control_c - #rputs stderr "* console_control: $args" - if {[tsv::get console is_raw]} { - if {[lindex $::errorCode 0] eq "CHILDKILLED"} { - #rputs stderr "\n|repl> ctrl-c errorCode: $::errorCode" - #avoid spurious triggers after interrupting a command.. - #review - dodgy.. we just want to interrupt child processes but then still be able to interrupt repl - set ::punk::repl::signal_control_c 0 - set preverr [string map {"child killed" "child_killed"} $::errorInfo] - catch {error $preverr} ;#for errorInfo display - return 42 - } else { - #how to let rawmode loop handle it? It doesn't seem to get through if we return 0 - #puts stderr "signal ctrl-c while in raw mode" - #flush stderr - set signal_control_c_msg "signal ctrl-c $signal_control_c rawmode" - if {[catch { - lassign [punk::console::get_size] _w console_width _h console_height - } errM]} { - puts stderr "signal ctrl-c error get_size error:$errM" - } - - if {$signal_control_c < 3} { - set remaining [expr {3 - $signal_control_c}] - if {[catch { - punk::repl::console_controlnotification "[a+ web-orange]ctrl-c ($remaining more to quit, enter to continue)[a]" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - } elseif {$signal_control_c == 3} { - #puts stderr "signal ctrl-c x3 received - quitting." - if {[catch { - punk::repl::console_controlnotification "ctrl-c x3 received - quitting punk shell" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - flush stderr - after 25 - quit - return 1 - } elseif {$signal_control_c > 5} { - #fallback if quit didn't work - #puts stderr "signal ctrl-c $signal_control_c received - sending to default handler" - if {[catch { - punk::repl::console_controlnotification "ctrl-c $signal_control_c received - sending to default handler" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - flush stderr - punk::console::mode line - return 0 - } - - return 1 - #after 200 {exit 42} ;#temp - #return 42 - } - } - - - if {[lindex $::errorCode 0] eq "CHILDKILLED"} { - set signal_control_c 0 - set preverr [string map {"child killed" "child_killed"} $::errorInfo] - catch {error $preverr} ;#for errorInfo display - return 42 - } - if {[catch { - lassign [punk::console::get_size] _w console_width _h console_height - } errM]} { - puts stderr "signal ctrl-c error get_size error:$errM" - } - - #note - returning 0 means pass event to other handlers including OS default handler - if {$signal_control_c <= 2} { - set remaining [expr {3 - $signal_control_c}] - #puts stderr "signal ctrl-c (perform $remaining more to quit, enter to return to repl)" - #flush stderr - if {[catch { - punk::repl::console_controlnotification "ctrl-c ($remaining more to quit, enter to continue)" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - return 1 - } elseif {$signal_control_c == 3} { - #puts stderr "signal ctrl-c x3 received - quitting." - if {[catch { - punk::repl::console_controlnotification "ctrl-c x3 received - quitting punk shell" $console_width $console_height - } errM]} { - puts stderr "signal ctrl-c error console_controlnotification error:$errM" - } - flush stderr - after 25 - quit - return 1 - } elseif {$signal_control_c == 4} { - puts stderr "signal ctrl-c x4 received - one more to hard exit" - flush stderr - return 1 - } elseif {$signal_control_c >= 5} { - #a script that allows events to be processed could still be running - puts stderr "signal ctrl-c x5 received - hard exit" - flush stderr - after 25 - exit 499 ;# HTTP 'client closed request' - just for the hell of it. - } else { - puts stderr "signal ctrl-c $signal_control_c received" - flush stderr - #return 0 to fall through to default handler - return 0 - } - - } - default { - puts stderr "unhandled console signal $args" - return 1 - } - } - } - twapi::set_console_control_handler ::punk::repl::handler_console_control - #we can't yet emit from an event with proper prompt handling - - #repl::rputs stdout "twapi loaded" - } else { - #repl::rputs stderr " Failed to load twapi" - } - } else { - #TODO - } -} - -#console handler may already be set, but in another thread/interp - so we can't use existence of proc to test -#we're ok with an existing handler - just catch for now. REVIEW we should make sure it didn't fail the first time -catch {punk::repl::init_signal_handlers} - - -# moved to punk package.. -#set outdevice [shellfilter::stack::new punkout -settings [list -tag "punkout" -buffering none -raw 1 -syslog [dict get $::punk::config::running syslog_stdout] -file [dict get $::punk::config::running logfile_stdout]]] -#set out [dict get $outdevice localchan] -#set errdevice [shellfilter::stack::new punkerr -settings [list -tag "punkerr" -buffering none -raw 1 -syslog [dict get $::punk::config::running syslog_stderr] -file [dict get $::punk::config::running logfile_stderr]]] -#set err [dict get $errdevice localchan] - - - -# -#set indevice [shellfilter::stack::new commandin -settings {-tag "commandin" -readbuffering line -writebuffering none -raw 1 -direction in}] -#set program_read_stdin_pipe [dict get $indevice localchan] - - - - - -proc punk::repl::reset_prompt {} { - variable prompt_reset_flag - set prompt_reset_flag 1 -} - -#aliases c and clear to this by ::punk -proc punk::repl::reset_terminal {} { - set prompt_reset_flag 1 - #clear ;#call to external executable which may not be available - puts -nonewline stdout [::punk::ansi::reset] -} - -proc punk::repl::get_prompt_config {} { - if {[catch {punk::console::vt52} is_vt52]} { - set is_vt52 0 - } - if {$is_vt52} { - set resultprompt "52-" - set nlprompt "52." - set infoprompt "52*" - set debugprompt "52~" - } else { - if {$::tcl_interactive} { - set RST [a] - set resultprompt "[a green bold]-$RST " - set nlprompt "[a green bold].$RST " - set infoprompt "[a green bold]*$RST " - set debugprompt "[a purple bold]~$RST " - } else { - set resultprompt "" - set nlprompt "" - set infoprompt "" - set debugprompt "" - } - } - return [list resultprompt $resultprompt nlprompt $nlprompt infoprompt $infoprompt debugprompt $debugprompt] -} - -proc repl::start {inchan args} { - puts stderr "-->repl::start $inchan $args" - upvar ::punk::console::input_chunks_waiting input_chunks_waiting - if {![info exists input_chunks_waiting($inchan)]} { - set input_chunks_waiting($inchan) [list] - } - - variable codethread - #review - if {$codethread eq ""} { - error "start - no codethread. call init first. (options -safe 0|1)" - } - variable commandstr - - # --- - variable editbuf - variable editbuf_list ;#command history - variable editbuf_linenum_submitted - variable editbuf_active_index - # --- - - variable reading - variable done - set done 0 - variable startinstance - variable loopinstance - if {[namespace exists ::punkapp]} { - #review - document ? - if {[dict exists $args -defaultresult]} { - set ::punkapp::default_result [dict get $args -defaultresult] - } - } - incr startinstance - set loopinstance 0 - if {[info exists ::punk::ns::ns_current]} { - set start_in_ns $::punk::ns::ns_current - } else { - set start_in_ns :: - } - thread::send $codethread [string map [list %ns1% $start_in_ns] { - #set ::punk::repl::codethread::running 1 - - #the interp in which commands such as d/ run - #we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values - #punk::repl::codethread::running is required whether safe or not. - interp eval code { - namespace eval ::punk::repl::codethread {} - set ::punk::repl::codethread::running 1 - namespace eval ::punk::ns::ns_current {} - set ::punk::ns::ns_current %ns1% - } - }] - set commandstr "" - - # --- - set editbuf [punk::repl::class::class_editbuf new {}] - lappend editbuf_list $editbuf ;#current editbuf is always in the history - set editbuf_linenum_submitted 0 - set editbuf_active_index 0 - # --- - - if {$::punk::console::ansi_wanted == 2} { - if {[::punk::console::test_can_ansi]} { - set ::punk::console::ansi_wanted 1 - } else { - set ::punk::console::ansi_wanted -1 - } - } - puts stderr "-->repl::start active on $inchan $args replthread:[thread::id] codethread:$codethread" - set prompt_config [punk::repl::get_prompt_config] - doprompt "P% " - fileevent $inchan readable [list [namespace current]::repl_handler $inchan $prompt_config] - set reading 1 - - #catch { - # set punk::console::tabwidth [punk::console::get_tabstop_apparent_width] - #} - vwait [namespace current]::done - fileevent $inchan readable {} - - - #puts stderr "-->start done = $::repl::done" - #todo - override exit? - #after 0 ::repl::post_operations - after idle ::repl::post_operations - vwait repl::post_operations_done - #puts stderr "-->start post_operations_done = $::repl::post_operations_done" - if {[namespace exists ::punkapp]} { - #todo check and get punkapp::result array - but what key? - if {[info exists ::punkapp::result(shell)]} { - set temp $::punkapp::result(shell) - unset ::punkapp::result(shell) - return $temp - } elseif {[info exists ::punkapp::default_result]} { - set temp $::punkapp::default_result - unset ::punkapp::default_result - return $temp - } - } - - - variable codethread_cond - if {[catch { - tsv::unset codethread_$codethread - } errM]} { - puts stderr " repl::start temp warning - $errM" - } - thread::cancel $codethread - thread::cond destroy $codethread_cond ;#race if we destroy cond before child thread has exited - as it can send a -async quit - set codethread "" - set codethread_cond "" - punk::console::mode line ;#review - revert to line mode on final exit - but we may be exiting a nested repl - puts "end repl::start" - return 0 -} -proc repl::post_operations {} { - if {[info exists ::repl::post_script] && [string length $::repl::post_script]} { - #put aside post_script so the script has the option to add another post_script and restart the repl - set ::repl::running_script $::repl::post_script - set ::repl::post_script "" - uplevel #0 {eval $::repl::running_script} - } - #todo - tidyup so repl could be restarted - set ::repl::post_operations_done 0 -} - - -proc repl::reopen_stdin {} { - #variable reopen_stdin_attempts - if {$::tcl_platform(platform) eq "windows"} { - puts stderr "|repl> Attempting reconnection of console to stdin by opening 'CON'" - } else { - puts stderr "|repl> Attempting reconnection of console to stdin by opening '/dev/tty'" - } - #puts stderr "channels:[chan names]" - #flush stderr - #catch {chan close stdin} - chan close stdin - - if {$::tcl_platform(platform) eq "windows"} { - #set s [open "CON" r] - set s [open {CONIN$} r] - if {[package provide twapi] ne ""} { - set h [twapi::get_tcl_channel_handle $s in] - twapi::SetStdHandle -10 $h - } - puts stderr "restarting repl on inputchannel:$s" - return [repl::start $s -title "reopen_stdin a"] - } else { - #/dev/tty - reference to the controlling terminal for a process - #review/test - set s [open "/dev/tty" r] - } - - repl::start stdin -title "reopen_stdin b" -} - -#todo - avoid putting this in gobal namespace? -#collisions with other libraries apps? -proc punk::repl::quit {args} { - set ::repl::done "quit {*}$args" - #puts stderr "quit called" - return "" ;#make sure to return nothing so "quit" doesn't land on stdout -} - -#just a failed experiment.. tried various things -proc repl::reopen_stdinX {} { - #windows - todo unix - package require twapi - - if 0 { - if {[catch {package require Memchan} errM]} { - #package require tcl::chan::fifo2 - #lassign [tcl::chan::fifo2] a b - package require tcl::chan::fifo - set x [tcl::chan::fifo] - } else { - #lassign [fifo2] a b - set x [fifo] - } - #first channel opened after stdin closed becomes stdin - #use a fifo or fifo2 because [chan pipe] assigns the wrong end first! - #a will be stdin - } - #these can't replace proper stdin (filehandle 0) because they're not 'file backed' or 'os level' - #try opening a named pipe server to become stdin - set pipename {\\.\pipe\stdin_%id%} - set pipename [string map [list %id% [pid]] $pipename] - - - - package require tcl::chan::fifo - - chan close stdin - lassign [tcl::chan::fifo] a - - - puts stderr "newchan: $a" - puts stderr "|test> $a [chan conf $a]" - - #set server [twapi::namedpipe_server $pipename] - #set client [twapi::namedpipe_client $pipename] ;#open a client and connect to the server we just made - - puts stderr "chan names: [chan names]" - - #by now $server not valid? - #set server stdin - - #chan configure $server -buffering line -encoding unicode - #chan configure $client -buffering line -encoding unicode - - #puts stderr "|test>ns-server $server [chan conf $server]" - #puts stderr "|test>ns-client $client [chan conf $client]" - - set conin [twapi::get_console_handle stdin] - twapi::set_standard_handle stdin $conin - - set h_in [twapi::get_standard_handle stdin] - - puts stderr "|test> $a [chan conf $a]" - - #chan configure $client -blocking 0 - after 2 repl::start $a - -} - -#add to sliding buffer of last x chars emmitted to screen by repl -#(we could maintain only one char - more kept merely for debug assistance) -#will not detect emissions from exec with stdout redirected and presumably some extensions etc -proc repl::screen_last_char_add {c what {why ""}} { - variable screen_last_chars - variable screen_last_char_list - if {![string length $c]} { - return [string index $screen_last_chars end] - } - if {[string length $screen_last_chars] > 10} { - set screen_last_chars [string range $screen_last_chars 1 end] ;#evict first char - set screen_last_char_list [lrange $screen_last_char_list 1 end] - } - append screen_last_chars $c - lappend screen_last_char_list [list $c $what $why] - #return [string index $screen_last_chars end] - #return [lindex $screen_last_char_list 0 0] - return [lindex $screen_last_char_list end 0] -} -proc repl::screen_last_char_get {} { - variable screen_last_char_list - return [lindex $screen_last_char_list end 0] -} -proc repl::screen_last_char_getinfo {} { - variable screen_last_char_list - return [lindex $screen_last_char_list end] -} - -#-------------------------------------- -#another experiment -proc repl::newout {} { - namespace eval ::replout { - namespace ensemble create -map { - initialize init - finalize close - watch watch - write write - } - } - proc ::replout::init {id mode} { - return {initialize finalize watch write} - } - proc ::replout::close {id} { - - } - proc ::replout::watch {id spec} { - - } - proc ::replout::write {id data} { - puts -nonewline stderr $data - return [string length $data] - } - - close stdout - set fd [chan create write ::replout] - chan configure $fd -buffering none - return $fd -} -interp alias {} newout {} repl::newout -proc repl::newout2 {} { - close stdout - set s [open "CON" w] - chan configure $s -buffering none -} -#-------------------------------------- - -proc repl::doprompt {prompt {col {green bold}}} { - #prompt to stderr. - #We can pipe commands into repl's stdin without the prompt interfering with the output. - #Although all command output for each line goes to stdout - not just what is emitted with puts - - if {$::tcl_interactive} { - flush stdout; #we are writing this prompt on stderr, but stdout could still be writing to screen - #our first char on stderr is based on the 'lastchar' of stdout which we have recorded but may not have arrived on screen. - #The issue we're trying to avoid is the (stderr)prompt arriving midway through a large stdout chunk - #REVIEW - this basic attempt to get stderr/stdout to cooperate is experimental and unlikely to achieve the desired effect in all situations - #It the above flush does seem to help though. - #note that our 'flush stdout' tcl call does not wait if stdout is non-blocking - #todo - investigate if the overhead is reasonable for a special channel that accepts stdout and stderr records with a reader to send to console in chunk-sizes we know will be emitted correctly - # - reader of such channel could be ok to be blocking (on read? on write to real channels?)... except everything still needs to be interruptable by things like signals? - #? - we want ordinary puts to stderr to be prioritized? to arrive on-screen - just not at arbitrary locations within stdout, and still must be correctly ordered wrt all other stderr - # - in our repl and code threads we don't want to put stderr/stdout writes in blocking mode and have code waiting on it - - set last_char_info [screen_last_char_getinfo] - if {![llength $last_char_info]} { - set needs_clearance 1 - } else { - lassign $last_char_info c what why - if {$why eq "prompt"} { - set needs_clearance 0 - } else { - set needs_clearance [screen_needs_clearance] - #puts -nonewline "-->$needs_clearance $last_char_info" - } - } - if {$needs_clearance == 1} { - set c \n - } else { - set c "" - } - set pre "" - if {[string first \n $prompt] >=0} { - set plines [split $prompt \n] - set pre [join [lrange $plines 0 end-1] \n]\n - set prompt [lindex $plines end] - } - - #this sort of works - but steals some of our stdin data ? review - # - #lassign [punk::console::get_cursor_pos_list] column row - #if {$row != 1} { - # set c "\n" - #} - - set o [a {*}$col] - set r [a] - puts -nonewline stderr $c$pre$o$prompt$r - screen_last_char_add " " "prompt-stderr" prompt - flush stderr - } -} - -#use rputs in repl_handler instead of puts -# - to help ensure we don't emit extra blank lines in info or debug output -#rputs expects the standard tcl 'puts' command to be in place. -# all bets are off if this has been redefined with some other api -# rputs deliberately doesn't check screen_last_chars before emitting data (unless reporting an error in rputs itself) -proc repl::rputs {args} { - variable screen_last_chars - variable last_out_was_newline - variable last_repl_char - - set pseudo_map [dict create\ - debug stderr\ - debugreport stderr\ - ] - - if {[::tcl::mathop::<= 1 [llength $args] 3]} { - set out [lindex $args end] - append out ""; #copy on write - if {([llength $args] > 1) && [lindex $args 0] ne "-nonewline"} { - set this_tail \n - set rputschan [lindex $args 0] - #map pseudo-channels to real - if {$rputschan in [dict keys $pseudo_map]} { - lset args 0 [dict get $pseudo_map $rputschan] - } - } elseif {[llength $args] == 1} { - set this_tail \n - set rputschan "stdout" - } else { - #>1 arg with -nonewline - set this_tail [string index $out end] - set rputschan [lindex $args 1] - #map pseudo-channels to real - if {$rputschan in [dict keys $pseudo_map]} { - lset args 0 [dict get $pseudo_map $rputschan] - } - } - set last_char_info_width 60 - #review - string shouldn't be truncated prior to stripcodes - could chop ansi codes! - #set summary "[::shellfilter::ansi::stripcodes [string range $out 0 $last_char_info_width]]" - set out_plain_text [punk::ansi::ansistrip $out] - set summary [string range $out_plain_text 0 $last_char_info_width] - if {[string length $summary] > $last_char_info_width} { - append summary " ..." - } - - #make sure we use supplied rputschan in the screen_las_char_add 'what' - which may not be the underlying chan if it was a pseudo - screen_last_char_add $this_tail repl-$rputschan $summary - - try { - puts {*}$args - } on error {repl_error erropts} { - #possible error depending on -encoding and -profile of the channel - #(e.g -profile strict) - - #REVIEW - #TODO - something better - #failure case: - #set x \ud83c\udf1e - #(2 surrogate pairs - treated as single char in tcl8 - fixed in 9 but won't/can't be backported) - - #see also: https://core.tcl-lang.org/tips/doc/trunk/tip/619.md - puts stderr "$repl_error" - } - } else { - #looks like an invalid puts call - use the normal error produced by the puts command - #This should only occur if the repl itself is being rewritten/debugged, - #so we will use red "!" and not worry about the extra newlines before and after - if {[catch { puts {*}$args } err]} { - set c [a yellow bold] - set n [a] - #possibly non punk-compliant output because we're assuming the repl was the most recent emitter - #could be wrong, in which case we may emit an extra newline - #- shouldn't matter in this case - #set last_char [string range $screen_last_chars end] - set last_char [screen_last_char_get] - if {$last_char eq "\n"} { - set clear "" - } else { - set clear "\n" - } - puts -nonewline stderr "$clear[a red bold]! REPL ERROR IN rputs $c$err$n\n" - screen_last_char_add "\n" replerror "rputs err: '$err'" - return - } else { - #?? shouldn't happen with standard puts command - #do our best and assume final arg is still the data being emitted - #worst that will happen is we won't detect a trailing newline and will later emit an extra blank line. - set out [lindex $args end] - set this_tail [string index $out end] - screen_last_char_add $this_tail replunknown "rputs $args" - return - } - } -} -#whether we need a newline as clearance from previous output -#review - race with copy pasted data, hold-down of enter key -# and data from external process or background script that doesn't go through our stdout filter -#we probably can't use get_cursor_pos - as that needs to emit to stdout and read-loop on stdin which will possibly? make things worse -proc repl::screen_needs_clearance {} { - variable screen_last_chars - - #set last_char [string index $screen_last_chars end] - set last_char_info [screen_last_char_getinfo] - if {![llength $last_char_info]} { - #assumption - return 1 - } - lassign $last_char_info c what why - switch -- $what { - stdout - stderr - stdout/stderr { - return 1 - } - } - return [expr {$c ne "\n"}] -} - -namespace eval repl { - variable startinstance 0 - variable loopinstance 0 - variable in_repl_handler [list] - variable last_controlc_count 0 -} - -namespace eval punk::repl::class { - oo::class create class_bufman { - - } - - #multiline editing buffer - oo::class create class_editbuf { - variable o_context - variable o_config - - variable o_rendered_lines - variable o_remaining ;#? - - #o_chunk_list & o_chunk_info should make timed viewing of replays possible - variable o_chunk_list - variable o_chunk_info ;#arrival timing etc - variable o_cursor_row - variable o_cursor_col - variable o_insert_mode - constructor {configdict {contextdict {}}} { - my clear - set o_config $configdict - if {[dict exists $configdict rendered_initialchunk]} { - #pre-rendered initial chunk - #-- - set o_chunk_list "" ;#replace empty chunk from 'clear' call - set o_chunk_info [dict create] - #-- - set ch [dict get $configdict rendered_initialchunk] - my add_rendered_chunk $ch - } - - set o_context $contextdict - #error "[self class].constructor Unable to interpret config '$o_config'" - } - method cursor_row {} { - return $o_cursor_row - } - method cursor_column {} { - return $o_cursor_col - } - method insert_mode {} { - return $o_insert_mode - } - method clear {} { - set o_rendered_lines [list ""] - set o_chunk_list [list] - set o_chunk_info [dict create] - set o_cursor_row 1 - set o_cursor_col 1 - set o_insert_mode 1 ;#default to insert mode - lappend o_chunk_list "" - dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered] - } - method add_chunk {chunk} { - #we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info - lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok - dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw] - if {$chunk eq ""} { - return - } - - set firstnl [string first \n $chunk] - set newparts [split $chunk \n] - #attempt to render new 'lines' into the editbuffer - taking account of active cursor row & col & insertmode - - #merge based on current cursor row and col - #set lastrline [lindex $o_rendered_lines end] - #set n0 [lindex $newparts 0] - #set merged0 [string cat $lastrline $n0] - - #we should merge first row of newparts differently in case our chunks split a grapheme-combination? - # - if {$o_cursor_row < 1} { - puts stderr "add_chunk warning cursor_row < 1 - changing to minimum value 1" - set o_cursor_row 1 - } - set cursor_row_idx [expr {$o_cursor_row -1}] - set activeline [lindex $o_rendered_lines $cursor_row_idx] - set new0 [lindex $newparts 0] - #set combined [string cat $activeline $new0] - #use -cursor_row to tell renderline it's row context. - if {$firstnl >=0} { - #append combined \n - append new0 \n - } - set underlay [punk::ansi::ansistrip $activeline] - set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}] - if {$o_cursor_col > $line_nextchar_col} { - set o_cursor_col $line_nextchar_col - } - - set mergedinfo [overtype::renderline -info 1 -expand_right 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $underlay $new0] - - set result [dict get $mergedinfo result] - set o_insert_mode [dict get $mergedinfo insert_mode] - set result_col [dict get $mergedinfo cursor_column] - set result_row [dict get $mergedinfo cursor_row] - set overflow_right [dict get $mergedinfo overflow_right] ;#should be empty if no \v - set unapplied [dict get $mergedinfo unapplied] - set instruction [dict get $mergedinfo instruction] - - set insert_lines_below [dict get $mergedinfo insert_lines_below] - set insert_lines_above [dict get $mergedinfo insert_lines_above] - - # -- --- --- --- --- --- - set debug_first_row 2 - #puts "merged: $mergedinfo" - set debug "add_chunk0" - append debug \n $mergedinfo - append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col" - package require textblock - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] - if {![punk::console::vt52]} { - catch {punk::console::move_emitblock_return $debug_first_row 1 $debug} - } else { - #?? - } - # -- --- --- --- --- --- - - set o_cursor_col $result_col - set cursor_row_idx [expr {$o_cursor_row-1}] - lset o_rendered_lines $cursor_row_idx $result - - set nextrow $result_row - switch -- $instruction { - lf_start { - #for normal commandline - we just add a line below - lappend o_rendered_lines "" - incr nextrow - set o_cursor_col 1 - } - } - - if {$insert_lines_below == 1} { - if {[string length $overflow_right]} { - lappend o_rendered_lines $overflow_right - set o_cursor_col [expr {[punk::ansi::printing_length $overflow_right] +1}] - } else { - lappend o_rendered_lines "" - set o_cursor_col 1 - } - } elseif {$insert_lines_above == 1} { - #for {set i 0} {$i < $insert_lines_above} {incr i} { - # set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""] - # incr nextrow -1 - #} - set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""] - set o_cursor_col 1 - } - - set o_cursor_row $nextrow - set cursor_row_idx [expr {$o_cursor_row-1}] - if {$cursor_row_idx < [llength $o_rendered_lines]} { - set activeline [lindex $o_rendered_lines $cursor_row_idx] - } else { - lappend o_rendered_lines "" - set activeline "" - } - - - set i 1 - foreach p [lrange $newparts 1 end] { - if {$i < [llength $newparts]-1} { - append p \n - } else { - if {$p eq ""} { - break - } - } - #puts stderr "overtype::renderline -info 1 -expand_right 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $activeline '$p'" - set underlay $activeline - set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}] - if {$o_cursor_col > $line_nextchar_col} { - set o_cursor_col $line_nextchar_col - } - set mergedinfo [overtype::renderline -info 1 -expand_right 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $underlay $p] - set debug "add_chunk$i" - append debug \n $mergedinfo - append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]" - set debug [textblock::frame -checkargs 0 -buildcache 0 $debug] - #catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug} - - set result [dict get $mergedinfo result] - set o_insert_mode [dict get $mergedinfo insert_mode] - set o_cursor_col [dict get $mergedinfo cursor_column] - set cmove [dict get $mergedinfo cursor_row] - set overflow_right [dict get $mergedinfo overflow_right] ;#should be empty if no \v - set unapplied [dict get $mergedinfo unapplied] - set insert_lines_below [dict get $mergedinfo insert_lines_below] - if {[string is integer -strict $cmove]} { - if {$cmove == 0} { - set nextrow [expr {$o_cursor_row + 1}] - set o_cursor_col 1 - } elseif {$cmove == 1} { - #check for overflow_right and unapplied - #leave cursor_column - } elseif {$cmove >= 1} { - - } - - } else { - # = - absolute - set nextrow [string range $cmove 1 end] - } - if {$nextrow eq $o_cursor_row} { - incr nextrow - } - set o_cursor_row $nextrow - if {$insert_lines_below} { - - } - - set cursor_row_idx [expr {$o_cursor_row-1}] - if {$cursor_row_idx < [llength $o_rendered_lines]} { - set activeline [lindex $o_rendered_lines $cursor_row_idx] - } else { - lappend o_rendered_lines "" - set activeline "" - } - lset o_rendered_lines $cursor_row_idx $result - - incr i - } - - } - method add_rendered_chunk {rchunk} { - #split only on lf newlines - movement codes and \b \v \r not expected - #check only for \v \r as chars we don't expect/want in rendered lines - #chunk as been pre-rendered (or is known to be plain ascii without ANSI or \b \v \r) - #but we don't yet have grapheme split info for it - - if {[regexp {[\v\b\r]} $rchunk]} { - error "[self class].add_rendered_chunk chunk contains \\v or \\b or \\r. Rendered chunk shouldn't contain these characters or ANSI movement codes" - } - lappend o_chunk_list $rchunk ;#rchunk may contain newlines - that's ok - dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type rendered] - - set newparts [split $rchunk \n] - #lappend o_chunk_list $rchunk - set lastrline [lindex $o_rendered_lines end] - - #in renderedlines list merge last line of old with first line of new - #we can't just cat the newpart on to existing rendered line - the chunk could have split a grapheme (e.g char+combiner(s)) - #we - #todo - redo grapheme split on merged line - set merged [string cat $lastrline [lindex $newparts 0]] - lset o_rendered_lines end $merged - - #todo - #each newpart needs its grapheme split info to be stored - #jmn - #set o_rendered_lines [concat $o_rendered_lines [lrange $newparts 1 end]] - lappend o_rendered_lines {*}[lrange $newparts 1 end] - - } - method linecount {} { - return [llength $o_rendered_lines] - } - method line {idx} { - if {[string is integer -strict $idx]} { - incr idx -1 - } - return [lindex $o_rendered_lines $idx] - } - method lines {args} { - switch -- [llength $args] { - 0 {return $o_rendered_lines} - 1 { - set idx [lindex $args 0] - if {[string is integer -strict $idx]} { - incr idx -1 - } - return [list [lindex $o_rendered_lines $idx]] - } - 2 { - lassign $args idx1 idx2 - if {[string is integer -strict $idx1]} { - incr idx1 -1 - } - if {[string is integer -strict $idx2]} { - incr idx2 -1 - } - return [lrange $o_rendered_lines $idx1 $idx2] - } - default {error "lines expected 0,1 or 2 indices"} - } - } - - #todo - index base??? - method lines_numbered {args} { - #build a paired list so we don't have to do various calcs on end+ end- etc checking llength - #punk::lib::range will use lseq if available - else use it's own slower code - set max [llength $o_rendered_lines] ;#assume >=1 - set nums [punk::lib::range 1 $max] - set numline_list [list] - foreach n $nums ln $o_rendered_lines { - lappend numline_list [list $n $ln] - } - - switch -- [llength $args] { - 0 {return $numline_list} - 1 {return [lindex $numline_list [lindex $args 0]]} - 2 {return [lrange $numline_list {*}$args]} - default {error "lines expected 0,1 or 2 indices"} - } - } - - #1-based - method delete_line {linenum} { - error "unimplemented" - if {$linenum eq "end"} { - set linenum [llength $o_rendered_lines] - } - if {![string is integer -strict $linenum]} { - error "delete_line accepts only end or an integer from 1 to linecount" - } - if {$linenum == 0} { - error "minimum line is 1" - } - set o_rendered_lines [lreplace $o_rendered_lines $index $index] - } - #clear data from last line only - method clear_tail {} { - set o_cursor_row [llength $o_rendered_lines] - set o_cursor_col 1 - lset o_rendered_lines end "" - } - #1-based - method view_lines {args} { - set llist [my lines {*}$args] - return [join $llist \n] - } - method view_lines_numbered {args} { - set ANSI_linenum [a+ green] - set RST [a] - set llist [my lines_numbered {*}$args] - set nums [lsearch -all -inline -index 0 -subindices $llist *] - lset nums $o_cursor_row-1 "[a+ bold underline]$o_cursor_row${RST}$ANSI_linenum" - set lines [lsearch -all -inline -index 1 -subindices $llist *] - - set cursorline [lindex $lines $o_cursor_row-1] - set charindex_at_cursor [ansistring COLUMNINDEX $cursorline $o_cursor_col] - if {$charindex_at_cursor ne ""} { - lassign [ansistring INDEXCOLUMNS $cursorline $charindex_at_cursor] col0 col1 - #we now have the column extents of the possibly double-wide character at the cursor - #we can apply ansi just to those columns using a transparent overtype - set prefix [string repeat " " [expr {$col0 -1}]] - set linecols [punk::ansi::printing_length $cursorline] - set suffix [string repeat " " [expr {$linecols -$col1}]] - #capitalised INDEX - for grapheme/control-char index e.g a with diacritic a\u0300 has a single index - set char_at_cursor [ansistring INDEX $cursorline $charindex_at_cursor] ;#this is the char with appropriate ansireset codes - set rawchar [punk::ansi::ansistrip $char_at_cursor] - if {$rawchar eq " "} { - set charhighlight "[punk::ansi::a+ White]_[a]" - } else { - set charhighlight [punk::ansi::a+ reverse]$char_at_cursor[a] - } - set cursorline [overtype::renderline -transparent 1 -insert_mode 0 -expand_right 0 $cursorline $prefix$charhighlight$suffix] - lset lines $o_cursor_row-1 $cursorline - } - - set numcol "$ANSI_linenum[join $nums \n][a]" - set linecol [join $lines \n] - return [textblock::join -- $numcol " " $linecol] - } - method debugview_lines {} { - set result "" - foreach ln $o_rendered_lines { - append result [ansistring VIEW -lf 1 -vt 1 $ln] \n ;#should be no lf or vt - but if there is.. we'd better show it - } - append result \n "cursor row: $o_cursor_row col: $o_cursor_col" - return $result - } - method last_char {} { - return [string index [lindex $o_chunk_list end] end] - } - - #more strictly - last non-ansi? - method last_grapheme {} { - set lastchunk [lindex $o_chunk_list end] - set plaintext_parts [punk::ansi::ta::split_at_codes $lastchunk] - set pt [lindex $plaintext_parts end] - if {$pt eq ""} { - set pt [lindex $plaintext_parts end-1] - } - set graphemes [punk::char::grapheme_split $pt] - return [lindex $graphemes end] - } - method last_ansi {} { - set lastchunk [lindex $o_chunk_list end] - set parts [punk::ansi::ta::split_codes_single $lastchunk] - set lastcode [lindex $parts end-1] - return $lastcode - #return [ansistring VIEW -lf 1 $lastcode] - } - method chunks {args} { - switch -- [llength $args] { - 0 {return $o_chunk_list} - 1 {return [lindex $o_chunk_list [lindex $args 0]]} - 2 {return [lrange $o_chunk_list {*}$args]} - default {error "chunks expected 0,1 or 2 arguments (index or range)"} - } - } - method view_chunks {} { - set result "" - set dashes [string repeat - 20] - foreach arrival_chunk $o_chunk_list chunk_id [dict keys $o_chunk_info] { - set chunk_info [dict get $o_chunk_info $chunk_id] - append result $dashes \n - set micros [dict get $chunk_info micros] - append result "$chunk_id arrival: [clock format [expr {$micros / 1000000}] -format "%Y-%m-%d %H:%M:%S"] ($micros)" \n - append result $dashes \n - append result $arrival_chunk \n - } - return $result - } - - method debugview_chunks {} { - set result "" - foreach ln $o_chunk_list { - append result [ansistring VIEW -lf 1 -vt 1 $ln] \n - } - append result \n "cursor row: $o_cursor_row col: $o_cursor_col" - return $result - } - method view_raw {} { - return [join $o_chunk_list ""] - } - method debugview_raw {} { - set sublf [ansistring VIEW -lf 1 \n] - #set subvt [ansistring VIEW -lvt 1 \v] ;#vt replacement with $subvt\v will not align accurately.. todo ? - return [string map [list $sublf $sublf\n] [ansistring VIEW -lf 1 -vt 0 [join $o_chunk_list ""]]] - } - } - -} - -proc ::punk::repl::repl_handler_checkchannel {inputchan} { - if {[catch {chan eof $inputchan} is_eof]} { - ::repl::rputs stderr "\n|repl> repl_handler_checkchannel error on $inputchan. (closed?) msg:$is_eof" - } else { - if {$is_eof} { - if {$::tcl_interactive} { - ::repl::rputs stderr "\n|repl> repl_handler_checkchannel EOF on $inputchan." - } - } - } -} -proc ::punk::repl::repl_handler_checkcontrolsignal_linemode {inputchan} { - #todo - what? - return - variable signal_control_c - if {$signal_control_c > 0} { - if {$::tcl_interactive} { - ::repl::rputs stderr "\n|repl> repl_handler_checkcontrolsignal_linemode ctrl-c errorCode 0: [lindex $::errorCode 0]" - } - } -} - -#This is not called from the signal handler - so we can't affect the signal handling with return -# -proc ::punk::repl::repl_handler_checkcontrolsignal_rawmode {inputchan} { - variable signal_control_c - variable signal_control_c_msg - if {$signal_control_c > 0 && $signal_control_c_msg ne "" } { - #if {$::tcl_interactive} { - # ::repl::rputs stderr "\n|repl> repl_handler_checkcontrolsignal_rawmode ctrl-c errorCode 0: [lindex $::errorCode 0]" - #} - set msg $signal_control_c_msg - set signal_control_c_msg "" - } else { - set msg "" - } - return [list count $signal_control_c msg $msg] -} - - -proc punk::repl::repl_handler_restorechannel_if_not_eof {inputchan previous_input_state} { - if {$inputchan ni [chan names] || [eof $inputchan]} { - return - } - if {[chan conf $inputchan] ne $previous_input_state} { - set restore_input_conf [dict remove $previous_input_state -inputmode] ;#Attempting to set input mode often gives permission denied on windows - why? - if {[catch { - chan conf $inputchan {*}$restore_input_conf - } errM]} { - rputs stderr "|repl>original: [ansistring VIEW $previous_input_state]" - rputs stderr "|repl>current : [ansistring VIEW [chan conf $inputchan]]" - rputs stderr "\n|repl> Failed to return $inputchan to original state" - rputs stderr "|repl>ERR: $errM" - } - } - return [chan conf $inputchan] -} -proc repl::repl_handler {inputchan prompt_config} { - # -- review - variable in_repl_handler - set in_repl_handler [list $inputchan $prompt_config] - # -- - variable last_controlc_count - - upvar ::punk::repl::prompt_reset_flag prompt_reset_flag - if {$prompt_reset_flag == 1} { - set prompt_config [punk::repl::get_prompt_config] - set prompt_reset_flag 0 - } - - fileevent $inputchan readable {} - upvar ::punk::console::input_chunks_waiting input_chunks_waiting - #note -inputmode not available in Tcl 8.6 for chan configure! - #According to DKF - -buffering option doesn't affect input channels - set rawmode 0 - set original_input_conf [chan configure $inputchan] ;#whether repl is in line or raw mode - we restore the inputchan (stdin) state - if {[dict exists $original_input_conf -inputmode]} { - if {[dict get $original_input_conf -inputmode] eq "raw"} { - #user or script has apparently put stdin into raw mode - update punk::console::is_raw to match - set rawmode 1 - #set ::punk::console::is_raw 1 - tsv::set console is_raw 1 - } else { - #set ::punk::console::is_raw 0 - tsv::set console is_raw 0 - } - #what about enable/disable virtualTerminal ? - #using stdin -inputmode to switch modes won't set virtualterminal input state appropriately - #we expect the state of -inputmode to be 'normal' even though we flip it during the read part of our repl loop - #if it's been set to raw - assume it is deliberately done this way as the user could have alternatively called punk::mode raw or punk::console::enableVirtualTerminal - #by not doing this automatically - we assume the caller has a reason. - } else { - #JMN FIX! - #this returns 0 in rawmode on 8.6 after repl thread changes - #set rawmode [set ::punk::console::is_raw] - set rawmode [tsv::get console is_raw] - } - - if {!$rawmode} { - #linemode - - #stdin with line-mode readable events (at least on windows for Tcl 8.7a6 to 9.0a) can get stuck with bytes pending when input longer than 100chars - even though there is a linefeed further on than that. - #This potentially affects a reasonable number of Tcl8.7 kit/tclsh binaries out in the wild. - #see bug https://core.tcl-lang.org/tcl/tktview/bda99f2393 (gets stdin problem when non-blocking - Windows) - #when in non-blocking mode we will have to read that in to get further - but we don't know if that was the end of line or if there is more - and we may not get a newline even though one was present originally on stdin. - #presence of 8.7 buffering bug will result in unrecoverable situation - even switching to raw and using read will not be able to retrieve tail data. - #the readable event only gives us 200 bytes (same problem may be at 4k/8k in other versions) - #This occurs whether we use gets or read - - set stdinlines [list] - if {[dict get $original_input_conf -blocking] ne "0"} { - chan configure $inputchan -blocking 0 - } - - set waitingchunk "" - #review - input_chunks_waiting in line mode - - if {[info exists input_chunks_waiting($inputchan)] && [llength $input_chunks_waiting($inputchan)]} { - #puts stderr "repl_handler input_chunks_waiting($inputchan) while in line mode. Had data:[ansistring VIEW -lf 1 $input_chunks_waiting($inputchan)]" - set allwaiting [join $input_chunks_waiting($inputchan) ""] - set input_chunks_waiting($inputchan) [list] - set yellow [punk::ansi::a+ yellow bold] - set waitinglines [split $allwaiting \n] - foreach ln [lrange $waitinglines 0 end-1] { - lappend stdinlines $ln - } - set waitingchunk [lindex $waitinglines end] - # -- - #set chunksize [gets $inputchan chunk] - set chunk [read $inputchan] - set chunksize [string length $chunk] - # -- - if {$chunksize > 0} { - if {[string index $chunk end] eq "\n"} { - lappend stdinlines $waitingchunk[string range $chunk 0 end-1] - #punk::console::cursorsave_move_emitblock_return 30 30 "repl_handler num_stdinlines [llength $stdinlines] chunk:$yellow[ansistring VIEW -lf 1 $chunk][a] fblocked:[fblocked $inputchan] pending:[chan pending input stdin]" - - punk::repl::repl_handler_restorechannel_if_not_eof $inputchan $original_input_conf - uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] - } else { - set input_chunks_waiting($inputchan) [list $allwaiting] - lappend input_chunks_waiting($inputchan) $chunk - } - } else { - #'chan blocked' docs state: 'Note that this only ever returns 1 when the channel has been configured to be non-blocking..' - if {[chan blocked $inputchan]} { - #REVIEW - - #todo - figure out why we're here. - #can we even put a spinner so we don't keep emitting lines? We probably can't use any ansi functions that need to get a response on stdin..(like get_cursor_pos) - #punk::console::get_size is problematic if -winsize not available on the stdout channel - which is the case for certain 8.6 versions at least.. platform variances? - ## can't do this: set screeninfo [punk::console::get_size]; lassign $screeninfo _c cols _r rows - set outconf [chan configure stdout] - set RED [punk::ansi::a+ red bold]; set RST [punk::ansi::a] - if {"windows" eq $::tcl_platform(platform)} { - set msg "${RED}$inputchan chan blocked is true. (line-length Tcl windows channel bug?)$RST \{$allwaiting\}" - } else { - set msg "${RED}$inputchan chan blocked is true.$RST \{$allwaiting\}" - } - set cols "" - set rows "" - if {[dict exists $outconf -winsize]} { - lassign [dict get $outconf -winsize] cols rows - } else { - #fallback - try external executable. Which is a bit ugly - #tput can't seem to get dimensions (on FreeBSD at least) when not run interactively - ie via exec. (always returns 80x24 no matter if run with <@stdin) - - #bizarrely - tput can work with exec on windows if it's installed e.g from msys2 - #but can be *slow* compared to unix e.g 400ms+ vs <2ms on FreeBSD ! - #stty -a is 400ms+ vs 500us+ on FreeBSD - - if {"windows" eq $::tcl_platform(platform)} { - set tputcmd [auto_execok tput] - if {$tputcmd ne ""} { - if {![catch {exec {*}$tputcmd cols lines} values]} { - lassign $values cols rows - } - } - } - - if {![string is integer -strict $cols] || ![string is integer -strict $rows]} { - #same for all platforms? tested on windows, wsl, FreeBSD - #exec stty -a gives a result on the first line like: - #speed xxxx baud; rows rr; columns cc; - #review - more robust parsing - do we know it's first line? - set sttycmd [auto_execok stty] - if {$sttycmd ne ""} { - #the more parseable: stty -g doesn't give rows/columns - if {![catch {exec {*}$sttycmd -a} result]} { - lassign [split $result \n] firstline - set lineparts [split $firstline {;}] ;#we seem to get segments that look well behaved enough to be treated as tcl lists - review - regex? - set rowinfo [lsearch -index end -inline $lineparts rows] - if {[llength $rowinfo] == 2} { - set rows [lindex $rowinfo 0] - } - set colinfo [lsearch -index end -inline $lineparts columns] - if {[llength $colinfo] == 2} { - set cols [lindex $colinfo 0] - } - } - } - } - } - if {[string is integer -strict $cols] && [string is integer -strict $rows]} { - #got_dimensions - todo - try spinner? - #puts -nonewline stdout [punk::ansi::move $rows 4]$msg - #use cursorsave_ version which avoids get_cursor_pos_list call - set msglen [ansistring length $msg] - punk::console::cursorsave_move_emitblock_return $rows [expr {$cols - $msglen -1}] $msg - } else { - #no mechanism to get console dimensions - #we are reduced to continuously spewing lines. - puts stderr $msg - } - - after 100 - } - set input_chunks_waiting($inputchan) [list $allwaiting] - } - - } else { - punk::repl::repl_handler_checkchannel $inputchan - punk::repl::repl_handler_checkcontrolsignal_linemode $inputchan - # -- --- --- - #set chunksize [gets $inputchan chunk] - # -- --- --- - set chunk [read $inputchan] - set chunksize [string length $chunk] - # -- --- --- - if {$chunksize > 0} { - #punk::console::cursorsave_move_emitblock_return 35 120 "chunk: [ansistring VIEW -lf 1 "...[string range $chunk end-10 end]"]" - set ln $chunk ;#temp - #punk::console::cursorsave_move_emitblock_return 25 30 [textblock::frame -title line "[a+ green]$waitingchunk[a][a+ red][ansistring VIEW -lf 1 $ln][a+ green]pending:[chan pending input stdin][a]"] - if {[string index $ln end] eq "\n"} { - lappend stdinlines [string range $ln 0 end-1] - punk::repl::repl_handler_restorechannel_if_not_eof $inputchan $original_input_conf - uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] - } else { - lappend input_chunks_waiting($inputchan) $ln - } - } - } - - } else { - #rawmode - if {[info exists input_chunks_waiting($inputchan)] && [llength $input_chunks_waiting($inputchan)]} { - #we could concat and process as if one chunk - but for now at least - we want to preserve the 'chunkiness' - set chunkwaiting_zero [lpop input_chunks_waiting($inputchan) 0] ;#pop off lhs of wait list (tcl 8.6 is tcl imp of lpop - a little slower) - uplevel #0 [list repl::repl_process_data $inputchan raw-waiting $chunkwaiting_zero [list] $prompt_config] - } else { - - punk::repl::repl_handler_checkchannel $inputchan - set signalinfo [punk::repl::repl_handler_checkcontrolsignal_rawmode $inputchan] - if {[dict get $signalinfo count] > $last_controlc_count} { - set continue 0 - set last_controlc_count [dict get $signalinfo count] - } else { - set continue 1 - } - - if {$continue} { - if {[dict get $original_input_conf -blocking] ne "0" || [dict get $original_input_conf -translation] ne "lf"} { - chan configure $inputchan -blocking 0 - chan configure $inputchan -translation lf - } - set chunk [read $inputchan] - #we expect a chan configured with -blocking 0 to be blocked immediately after reads - #test - just bug console for now - try to understand when/how/if a non blocking read occurs. - if {![chan blocked $inputchan]} { - puts stderr "repl_handler->$inputchan not blocked after read" - } - - punk::repl::repl_handler_restorechannel_if_not_eof $inputchan $original_input_conf - uplevel #0 [list repl::repl_process_data $inputchan raw-read $chunk [list] $prompt_config] - while {[llength $input_chunks_waiting($inputchan)]} { - set chunkzero [lpop input_chunks_waiting($inputchan) 0] - if {$chunkzero eq ""} {continue} ;#why empty waiting - and is there any point passing on? - uplevel #0 [list repl::repl_process_data $inputchan raw-waiting $chunkzero [list] $prompt_config] - } - } - } - } - - if {![chan eof $inputchan]} { - ################################################################################## - #Re-enable channel read handler only if no waiting chunks - must process in order - ################################################################################## - if {![llength $input_chunks_waiting($inputchan)]} { - fileevent $inputchan readable [list ::repl::repl_handler $inputchan $prompt_config] - } else { - after idle [list ::repl::repl_handler $inputchan $prompt_config] - } - #################################################### - } else { - #repl_handler_checkchannel $inputchan - fileevent $inputchan readable {} - set reading 0 - thread::send -async $::repl::codethread {set ::punk::repl::codethread::running 0} - if {$::tcl_interactive} { - rputs stderr "\nrepl_handler EOF inputchannel:[chan conf $inputchan]" - #rputs stderr "\n|repl> ctrl-c EOF on $inputchan." - } - set [namespace current]::done 1 - after 1 [list repl::reopen_stdin] - } - set in_repl_handler [list] -} - -proc punk::repl::editbuf {index args} { - set editbuf [lindex $::repl::editbuf_list $index] - if {$editbuf ne ""} { - $editbuf {*}$args - } else { - return "No such index in editbuf list" - } -} -interp alias {} editbuf {} ::punk::repl::editbuf - - -proc punk::repl::console_debugview {editbuf consolewidth args} { - if {[punk::console::vt52]} { - #topleft? - return [dict create width 0 height 0 topleft 0] - } - package require textblock - variable debug_repl - if {$debug_repl <= 0} { - return [dict create width 0 height 0 topleft {}] - } - set defaults {-row 10 -rightmargin 2 -chunktype raw-read} - #dict for {k v} $args { - # switch -- $k { - # -row - -chunktype {} - # default { - # error "console_debugview unrecognised option '$k'. Known-options [dict keys $defaults]" - # } - # } - #} - set opts [dict merge $defaults $args] - set opt_row [dict get $opts -row] - set opt_chunktype [dict get $opts -chunktype] - set opt_rightmargin [dict get $opts -rightmargin] - - #debugview_raw frame - set RST [a] - if {[catch { - set info [$editbuf debugview_raw] - if {$opt_chunktype eq "raw-waiting"} { - set info [a+ bold yellow]$info$RST - } else { - set info [a+ green]$info$RST - } - #set lines [lines_as_list -ansireplays 1 $info] - set lines [lines_as_list -ansireplays 0 $info] - if {[llength $lines] > 20} { - set lines [lrange $lines end-19 end] - set info [::join $lines \n] - } - set debug_height [expr {[llength $lines]+2}] ;#framed height - } errM]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM] - set debug_height [textblock::height $info] - } else { - #treat as ephemeral (unreusable) frames due to varying width & height - therefore set -buildcache 0 - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info] - } - - set debug_width [textblock::widthtopline $info] - set patch_height [expr {2 + $debug_height + 2}] - set spacepatch [textblock::block $debug_width $patch_height " "] - #puts -nonewline [punk::ansi::cursor_off] - punk::console::cursor_off - #use non cursorsave versions - cursor save/restore will interfere with any concurrent ansi rendering that uses save/restore - because save/restore is a single item, not a stack. - set debug_offset [expr {$consolewidth - $debug_width - $opt_rightmargin}] - set row_clear [expr {$opt_row -2}] - punk::console::move_emitblock_return $row_clear $debug_offset $spacepatch - punk::console::move_emitblock_return $opt_row $debug_offset $info - set topleft [list $debug_offset $opt_row] ;#col,row REVIEW - #puts -nonewline [punk::ansi::cursor_on] - punk::console::cursor_on - flush stdout - - return [dict create width $debug_width height $debug_height topleft $topleft] -} -proc punk::repl::console_editbufview {editbuf consolewidth args} { - if {[punk::console::vt52]} { - return [dict create width 0] - } - package require textblock - upvar ::repl::editbuf_list editbuf_list - - set defaults {-row 10 -rightmargin 0} - set opts [dict merge $defaults $args] - set opt_row [dict get $opts -row] - set opt_rightmargin [dict get $opts -rightmargin] - - if {[catch { - set info [$editbuf view_lines_numbered] - set lines [lines_as_list -ansireplays 1 $info] - if {[llength $lines] > 20} { - set lines [lrange $lines end-19 end] - set info [punk::lib::list_as_lines $lines] - } - } editbuf_error]} { - set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] - } else { - set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]" - append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]" - set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]" - set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]" - set info [a+ green bold]$row1\n$row2[a]\n$info - set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info] - } - set editbuf_width [textblock::widthtopline $info] - set spacepatch [textblock::block $editbuf_width 2 " "] - - #set editbuf_offset [expr {$consolewidth - $editbuf_width - $debug_width - 2}] - set editbuf_offset [expr {$consolewidth - $editbuf_width - $opt_rightmargin}] - - set row_clear [expr {$opt_row -2}] - punk::console::cursorsave_move_emitblock_return $row_clear $editbuf_offset $spacepatch - punk::console::cursorsave_move_emitblock_return $opt_row $editbuf_offset $info - - return [dict create width $editbuf_width] -} -proc punk::repl::console_controlnotification {message consolewidth consoleheight args} { - package require textblock - set defaults {-bottommargin 0 -rightmargin 0} - set opts [dict merge $defaults $args] - set opt_bottommargin [dict get $opts -bottommargin] - set opt_rightmargin [dict get $opts -rightmargin] - set messagelines [split $message \n] - set message [lindex $messagelines 0] ;#only allow single line - set info "[a+ bold red]$message[a]" - set hlt [dict get [textblock::framedef light] hlt] - set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1] - set notification_width [textblock::widthtopline $info] - set box_offset [expr {$consolewidth - $notification_width - $opt_rightmargin}] - set row [expr {$consoleheight - $opt_bottommargin}] - punk::console::cursorsave_move_emitblock_return $row $box_offset $info - return [dict create width $notification_width] -} - -proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config} { - if {[info exists ::punk::console::is_vt52]} { - set is_vt52 $::punk::console::is_vt52 - } else { - set is_vt52 0 - } - - variable loopinstance - incr loopinstance - upvar ::punk::console::input_chunks_waiting input_chunks_waiting - - upvar ::punk::repl::prompt_reset_flag prompt_reset_flag - - variable last_repl_char "" ;#last char emitted by this handler to stdout/stderr - variable lastoutchar "" - variable lasterrchar "" - variable commandstr - # --- - variable editbuf - variable editbuf_list - variable editbuf_linenum_submitted - - # --- - variable reading - variable id_outstack - upvar ::punk::config::running running_config - - try { - #catch {puts stderr "xx--->[rep $::arglej]"} - if {$prompt_reset_flag == 1} { - set prompt_config [punk::repl::get_prompt_config] - set prompt_reset_flag 0 - } - - set resultprompt [dict get $prompt_config resultprompt] - set nlprompt [dict get $prompt_config nlprompt] - set infoprompt [dict get $prompt_config infoprompt] - set debugprompt [dict get $prompt_config debugprompt] - - - #set rawmode [set ::punk::console::is_raw] - set rawmode [tsv::get console is_raw] - if {!$rawmode} { - #puts stderr "-->got [ansistring VIEW -lf 1 $stdinlines]<--" - - } else { - #raw - set chunklen [string length $chunk] - set onetime 1 - #single loop while to allow break on escape - while {$onetime && [string length $chunk] >= 0 } { - set onetime 0 - #punk::console::move_emitblock_return 20 120 $chunklen-->[chan conf stdin]<-- - - #if {$chunklen == 0} { - # #document examples of when we expect zero-byte chunk - # #1) ctrl-z - # #review - # rputs stderr "->0byte read stdin" - # if {[chan eof $inputchan]} { - # fileevent $inputchan readable {} - # set reading 0 - # #set running 0 - # if {$::tcl_interactive} { - # rputs stderr "\n|repl> EOF on $inputchan." - # } - # set [namespace current]::done 1 - # #test - # #JMN - # #tailcall repl::reopen_stdin - # } - # break - #} - - #set info1 "read $chunklen bytes->[ansistring VIEW -lf 1 -vt 1 $chunk]" - #consider also the terminal linefeed mode. - #https://vt100.net/docs/vt510-rm/LNM.html - # terminals (by default) generally use a lone cr to represent enter (LNM reset ie CSI 20l) - #(as per above doc: "For compatibility with Digital's software you should keep LNM reset (line feed)") - - #You can insert an lf using ctrl-j - and of course stdin could have crlf or lf - #pasting from notepad++ with mixed line endings seems to paste everything ok - #we don't really know the source of input keyboard vs paste vs pipe - and whether a read has potentially chopped a crl in half.. - #possibly no real way to determine that. We could wait a small time to see if there's more data coming.. and potentially impact performance. - #Instead we'll try to make sense of it here. - - if {$chunklen == 1} { - #presume it's a keypress from terminal - set chunk [string map {\r \n} $chunk] - } else { - #maybe a paste? (or stdin to active shell loop - possibly with no terminal ? ) - #we'd better check for crlf and/or plain lf. If found - presume any lone CR is to be left as is. - if {[string first \n $chunk] < 0} { - set chunk [string map {\r \n} $chunk] - } - #else - - #has lf - but what if last char is cr? - #It may require user to hit enter - probably ok. - #could be a sequence of cr's from holding enter key - } - - #review - we can receive chars such as escapes or arrow inline with other data even from keyboard if keys are pushed quickly (or automated?) - # - so we probably shouldn't really rely on whether a char arrives alone in a chunk as a factor in its behaviour - #On the other hand - timing of keystrokes could be legitimate indications of intention in a cli ? - - #esc or ctrl-lb - if {$chunk eq "\x1b"} { - #return - set stdinlines [list "\x1b"] - set commandstr "" - set chunk "" - $editbuf clear_tail - screen_last_char_add \x1b stdin escape - break - } - - #if ProcessedInput is disabled - we can get ctrl-c, but then we wouldn't be in raw mode and wouldn't be here. - #e.g with punk::console::disableProcessedInput - #if we get just ctrl-c in one chunk - #ctrl-c - if {$chunk eq "\x03"} { - #::punk::repl::handler_console_control "ctrl-c_via_rawloop" - error "character 03 -> ctrl-c" - } - - if {$chunk eq "\x7f"} { - #review - configurable? - #translate raw del to backspace del for those terminals that send plain del - set chunk "\b\x7f" - } elseif {$chunk eq "\x7f\x7f"} { - #commonly if key held down we will get 2 dels in a row - #review - could get more in a row depending on hardware/os - set chunk "\b\x7f\b\x7f" - } elseif {$chunk eq "\x1c"} { - #ctrl-bslash - #try to brutally terminate process - #attempt to leave terminal in a reasonable state - mode line ;#may be aliased to ::repl::interphelpers::mode - after 250 {exit 42} - return - } elseif {$chunk eq "\x1a"} { - #for now - exit with small delay for tidyup - #ctrl-z - #::punk::repl::handler_console_control "ctrl-z_via_rawloop" - if {[catch {mode line}]} { - interp eval code {mode line} - } - after 1000 {exit 43} - return - } - - #we *could* intercept arrow keys here before they are handled in the editbuf - #but there should only be the need to do so for situations where we aren't editing a commandline - #if {$chunk eq "\x1b\[D"} { - # #rputs stderr "${debugprompt}arrow-left D" - # #set commandstr "" - # #punk::console::move_back 1 ;#terminal does it anyway? - #} - #if {$chunk eq "\x1b\[C"} { - #} - - $editbuf add_chunk $chunk - - #-------------------------- - # editbuf and debugview rhs frames - #for now disable entirely on vt52 - we can only do cursor save restore - nothing that requires responses on stdin (?) - if {!$is_vt52 && [set ::punk::console::ansi_available]} { - #experimental - use punk::console::get_size to determine current visible width. - #This should ideally be using sigwinch or some equivalent to set a value somewhere. - #testing each time is very inefficient (1+ms) - #unfortunately there isn't an easy way to get such an event on windows console based systems - REVIEW. - set do_checkwidth 1 ;#make configurable if performance hit is too severe? TODO - set consolewidth 132 - if {$do_checkwidth} { - if {[catch {set consolewidth [dict get [punk::console::get_size] columns]} errM]} { - #review - if {!$is_vt52} { - puts stderr "repl_process_data failed on call to punk::console::get_size :$errM" - } - } - #if chan conf stdout doesn't give dimensions and console doesn't respond to queries - we can get empty results in get_size dict - if {$consolewidth eq ""} { - set consolewidth 132 - } - } - set debug_width 0 - set rightmargin 0 - set space_occupied [punk::repl::console_debugview $editbuf $consolewidth -row 10 -chunktype $chunktype -rightmargin $rightmargin] ;#contains cursor movements - set debug_width [dict get $space_occupied width] - set clearance [expr {$debug_width + $rightmargin}] - set space_occupied [punk::repl::console_editbufview $editbuf $consolewidth -row 10 -rightmargin $clearance] - } - #-------------------------- - - - set lines_unsubmitted [expr {[$editbuf linecount] - $editbuf_linenum_submitted}] - #there is always one 'line' unsubmitted - although it may be the current one being built, which may be empty string - if {$lines_unsubmitted < 1} { - puts stderr "repl editbuf_linenum_submitted out of sync with editbuf" - } - - #set trailing_line_index [expr {[$editbuf linecount] -1}] - set last_line_num [$editbuf linecount] - #set nextsubmit_index [expr {$editbuf_lineindex_submitted + 1}] - set nextsubmit_line_num [expr {$editbuf_linenum_submitted + 1}] - - set cursor_row [$editbuf cursor_row] - set cursor_index [expr {$cursor_row -1}] - set lastansi [$editbuf last_ansi] - if {$lastansi eq "\x1b\[A"} { - if {$cursor_row > 1} { - puts -nonewline stdout "\x1b\[A" - } - } elseif {$lastansi eq "\x1b\[B"} { - puts -nonewline stdout "\x1b\[B" - } - flush stdout - - - - set leftmargin 3 - if {!$is_vt52} { - puts -nonewline stdout [a+ cyan][punk::ansi::move_column [expr {$leftmargin +1}]][punk::ansi::erase_eol][$editbuf line $cursor_row][a][punk::ansi::move_column [expr {$leftmargin + [$editbuf cursor_column]}]] - } else { - puts -nonewline stdout [a+ cyan][punk::ansi::vt52move_column [expr {$leftmargin +1}]][punk::ansi::vt52erase_eol][$editbuf line $cursor_row][punk::ansi::vt52move_column [expr {$leftmargin + [$editbuf cursor_column]}]] - } - #puts -nonewline stdout $chunk - flush stdout - if {[$editbuf last_char] eq "\n"} { - set linelen [punk::ansi::printing_length [$editbuf line $nextsubmit_line_num]] - if {!$is_vt52} { - puts -nonewline stdout [a+ cyan bold][punk::ansi::move_column [expr {$leftmargin +1}]][$editbuf line $nextsubmit_line_num][a][punk::ansi::move_column [expr {$leftmargin + $linelen +1}]] - #screen_last_char_add "\n" input inputline - puts -nonewline stdout [punk::ansi::erase_eol]\n - } else { - puts -nonewline stdout [a+ cyan bold][punk::ansi::vt52move_column [expr {$leftmargin +1}]][$editbuf line $nextsubmit_line_num][punk::ansi::vt52move_column [expr {$leftmargin + $linelen +1}]] - puts -nonewline stdout [punk::ansi::vt52erase_eol]\n - } - - - #puts -nonewline stdout \n - screen_last_char_add "\n" input inputline - set waiting [$editbuf line end] - if {[string length $waiting] > 0} { - set waiting [a+ yellow bold]$waiting[a] - #puts stderr "waiting $waiting" - $editbuf clear_tail - lappend input_chunks_waiting($inputchan) $waiting - } - } - if {$editbuf_linenum_submitted == 0} { - #(there is no line 0 - lines start at 1) - if {[$editbuf last_char] eq "\n"} { - lappend stdinlines [$editbuf line 1] - set editbuf_linenum_submitted 1 - } - } else { - if {$nextsubmit_line_num < $last_line_num} { - foreach ln [$editbuf lines $nextsubmit_line_num end-1] { - lappend stdinlines $ln - incr editbuf_linenum_submitted - } - } - } - set last_cursor_column [$editbuf cursor_column] - } - } - - } trap {POSIX} {e eopts} { - rputs stderr "trap1 POSIX '$e' eopts:'$eopts" - flush stderr - } on error {repl_error erropts} { - rputs stderr "error1 in repl_handler: $repl_error" - rputs stderr "-------------" - rputs stderr "$::errorInfo" - rputs stderr "-------------" - set stdinreader [fileevent $inputchan readable] - if {![string length $stdinreader]} { - rputs stderr "*> $inputchan reader inactive" - } else { - rputs stderr "*> $inputchan reader active" - } - if {[chan eof $inputchan]} { - rputs stderr "todo - attempt restart of repl on input channel: $inputchan in next loop" - catch {set ::punk::ns::ns_current "::"} - #todo set flag to restart repl ? - } else { - rputs stderr "continuing.." - } - flush stderr - } - - - try { - set maxlinenum [expr {[llength $stdinlines] -1}] - set linenum 0 - foreach line $stdinlines { - #puts stderr "----->line: [ansistring VIEW -lf 1 $line] commandstr:[ansistring VIEW -lf 1 $commandstr]" - set last_repl_char "" ;#last char emitted by this handler to stdout/stderr - set lastoutchar "" - set lasterrchar "" - - - #consider \x1b as text on console vs \x1b the character - #review - if we're getting these actual escape characters in line mode.. something is off - let's emit something instead of trying to interpret as a command and failing. - #This tends to happen when some sort of readline not avaialbe ie on unix or mintty in windows - #this only captures leading escape.. as an aid to diagnosis e.g won't be caught and the user will need to close the right bracket to complete the bogus command - #we may need to think about legitimate raw escapes in commands e.g from pipes or script files, vs via console? - - #esc key or ctrl-lb followed by enter - if {$line eq "\x1b"} { - #abort current command - if {$linenum == 0} { - doprompt "E% " {yellow bold} - set line "" - #screen_last_char_add " " empty empty - } else { - doprompt "\nE% " {yellow bold} - #screen_last_char_add "\n" empty empty ;#add \n to indicate noclearance required - } - incr linenum - continue - } else { - if {$line eq "\x1b\[C"} { - rputs stderr "${debugprompt}arrow-right C" - #set commandstr "" - } - if {$line eq "\x1b\[D"} { - #rputs stderr "${debugprompt}arrow-left D" - #set commandstr "" - #punk::console::move_back 1 - } - if {$line eq "\x1b\[A"} { - rputs stderr "${debugprompt}arrow-up A" - } - if {$line eq "\x1b\[B"} { - rputs stderr "arrow-down B" - } - if {[string match "\x1b*" $line]} { - rputs stderr "${debugprompt}esc - '[punk::ansi::ansistring::VIEW $line]'" - #set commandstr [punk::ansi::ansistrip $commandstr] - } - } - - if {$commandstr ne ""} { - append commandstr \n - } - - set stdinconf [fconfigure $inputchan] - if {$::tcl_platform(platform) eq "windows" && [dict get $stdinconf -encoding] ni [list unicode utf-16 utf-8]} { - #some long console inputs are split weirdly when -encoding and -translation are left at defaults - requiring extra enter-key to get repl to process. - #experiment to see if using iso8859-1 (raw bytes) and handling line endings manually gives insight. - # - do: chan conf stdin -encoding iso859-1 -translation lf - - #first command after configuring stdin this way seems to be interpreted with wrong encoding - subsequent commands work - review - - #this branch only works on tcl8.7+ - #It seems to fix the issue with holding down enter-key and getting extra blank lines, but - # it breaks copy-paste (encoding issue?) - - - #puts "--inputchan:$inputchan> [fconfigure $inputchan]" - - append commandstr $line - puts "1=============>[string length $commandstr] bytes , [ansistring VIEW $commandstr] , info complete:[info complete $line] stdinconf:$stdinconf" - set commandstr [string range $commandstr 0 end-3] - set commandstr [encoding convertfrom utf-16be $commandstr] ;#This is weird - but it seems to be big endian? - set commandstr [string trimright $commandstr] - #puts "2=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" - } else { - #append commandstr $line - #puts "0=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" - append commandstr $line - } - - #puts "=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" - set last_repl_char "\n" ;#this is actually the eol from stdin - screen_last_char_add "\n" stdin $line - - - - #append commandstr \n - - - if {[info complete $commandstr] && [string index $commandstr end] ne "\\"} { - #set commandstr [overtype::renderline -expand_right 1 "" $commandstr] - - - set ::repl::output_stdout "" - set ::repl::output_stderr "" - set outstack [list] - set errstack [list] - - - #oneshot repl debug - set wordparts [regexp -inline -all {\S+} $commandstr] - lassign $wordparts cmd_firstword cmd_secondword - if {$cmd_firstword eq "debugrepl"} { - if {$cmd_secondword in [list 0 cancel]} { - set ::punk::repl::debug_repl 0 - } else { - if {[string is integer -strict $cmd_secondword]} { - incr ::punk::repl::debug_repl $cmd_secondword - } else { - incr ::punk::repl::debug_repl - } - } - #set commandstr "set ::punk::repl::debug_repl" - set commandstr "" - } - if {$::punk::repl::debug_repl > 100} { - proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { - set p %p% - #don't auto-append \n even if missing. - #we may want to use debug_repl_emit with multiple calls for one output line - #if {[string index $msg end] ne "\n"} { - # set msg "$msg\n" - #} - #set last_char [string index $::repl::screen_last_chars end] - set last_char [screen_last_char_get] - if {$last_char ne "\n"} { - set clearance "\n" - } else { - set clearance "" - } - #use pseudo-channel debugreport - rputs debugreport $clearance$p[string map [list \n \n$p] $msg] - }] - set info "" - append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" - append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" - set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]] - append info "lastrunchunks\n" - append info "chunks: [llength $lastrunchunks]\n" - append info "namespace: $::punk::ns::ns_current" - debug_repl_emit $info - } else { - proc debug_repl_emit {msg} {return} - } - - #----------------------------------------- - #review! - #work around weird behaviour in tcl 8.6 & 8.7a5 (at least) part1 - #https://wiki.tcl-lang.org/page/representation - #/scriptlib/tests/listrep_bug.tcl - #after the uplevel #0 $commandstr call - # vars within the script that were set to a list, and have no string-rep, will generate a string-rep once the script (commandstr) is unset, or set to another value - #review - although the rep change is weird - what actual problem was caused aside from an unexpected shimmer? - #probably just that the repl can't then be used to debug representation issues and possibly that the performance is not ideal for list pipeline commands(?) - #now that we eval in another thread and interp - we seem to lose the list rep anyway. - #(unless we also save the script in that interp too in a run_command_cache) - global run_command_string - set run_command_string "$commandstr\n" ;#add anything that won't affect script. - global run_command_cache - #----------------------------------------- - - set repl_runid [tsv::incr repl runid] - tsv::set repl runchunks-$repl_runid [list] ;#last_run_display - catch { - #REVIEW - when we launch a subshell and run more than 10 commands, - #we delete runchunks from the outer shell that we'll return to! - #we should use a toplevel key pertaining to the shell/subshell instead of just 'repl' - tsv::unset repl runchunks-[expr {$repl_runid - 10}] - } - - #set ::repl::last_unknown "" - tsv::set repl last_unknown "" - #*********************************************************** - #don't use puts,rputs or debug_repl_emit in this block - #*********************************************************** - - #if {[string length [dict get $running_config color_stdout]] && [punk::console::colour]} { - # lappend outstack [shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]]] - #} - #lappend outstack [shellfilter::stack::add stdout tee_to_var -settings {-varname ::repl::output_stdout}] - #if {[string length [dict get $running_config color_stderr]] && [punk::console::colour]} { - # lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] - #} - - variable codethread - variable codethread_cond - variable codethread_mutex - - lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] - #thread::transfer $codethread stderr - - #chan configure stdout -buffering none - #JMN - fileevent $inputchan readable {} - set reading 0 - #don't let unknown use 'args' to convert commandstr to list - #=============================================================================== - #Actual command call - #puts "____>[rep $commandstr]" - #=============================================================================== - if {[string equal -length [string length "repl_runraw "] "repl_runraw " $commandstr]} { - #pass unevaluated command to runraw - set status [catch {uplevel #0 [list runraw $commandstr]} raw_result] - } else { - #puts stderr "repl uplevel 0 '$run_command_string'" - #JMN - #puts stderr "sending to codethread::runscript $run_command_string" - tsv::set codethread_$codethread status -1 - thread::send -async $codethread [list punk::repl::codethread::runscript $run_command_string] - thread::mutex lock $codethread_mutex - while {[set status [tsv::get codethread_$codethread status]] == -1} { - thread::cond wait $codethread_cond $codethread_mutex 50 - update ;#we need a full update here to allow interrupts to be processed - #While update is often considered risky - here we know our input channel readable event has been disabled - so re-entrancy shouldn't be possible. - #however - child thread can send quit - transferring processing from here back to repl::start - which then ends - making a mess (child not yet finished when trying to tidy up) - #we should give the child a way to quit by setting a tsv we pick up here *after the while loop* - and then we can set done. - } - thread::mutex unlock $codethread_mutex - set raw_result [tsv::get codethread_$codethread result] - lassign [tsv::get codethread_$codethread info] _o lastoutchar_codethread _e lasterrchar_codethread - - #set status [catch { - # thread::send $ - # uplevel 1 {namespace inscope $::punk::ns::ns_current $run_command_string} - #} raw_result] - - } - #puts stderr "repl raw_result: $raw_result" - #set result $raw_result - #append result ""; #copy on write - #copy on write - - #append result $raw_result "" - set result [string cat $raw_result ""] - #puts stderr "-->>$result<--" - #=============================================================================== - - flush stdout - flush stderr - #foreach s [lreverse $outstack] { - # shellfilter::stack::remove stdout $s - #} - #foreach s [lreverse $errstack] { - # shellfilter::stack::remove stderr $s - #} - - #----------------------------------------- - #list/string-rep bug workaround part 2 - #todo - set flag based on punk::lib::check::has_tclbug_script_var - lappend run_command_cache $run_command_string - #puts stderr "run_command_string rep: [rep $run_command_string]" - if {[llength $run_command_cache] > 2000} { - set run_command_cache [lrange $run_command_cache 1750 end] - } - #----------------------------------------- - - #screen_last_char_add [string index $lastoutchar_codethread$lasterrchar_codethread end] "stdout/stderr" - - - #set lastoutchar [string index [punk::ansi::ansistrip $::repl::output_stdout] end] - #set lasterrchar [string index [punk::ansi::ansistrip $::repl::output_stderr] end] - - #to determine whether cursor is back at col0 of newline - #screen_last_char_add [string index $lastoutchar$lasterrchar end] "stdout/stderr" - - - #??? - #screen_last_char_add [string index $lastoutchar$lastoutchar_codethread$lasterrchar$lasterrchar_codethread end] "stdout/stderr" - screen_last_char_add [string index $lastoutchar_codethread$lasterrchar_codethread end] "stdout/stderr" - - - set result_is_chunk_list 0 - #------ - #todo - fix. It doesn't make much sense to only detect if the unknown command occurred in first word. - #e.g set x [something arg] not detected vs something arg - #also - unknown commands aren't the only things that can write directly to the os handles stderr & stdout - set last_unknown [tsv::get repl last_unknown] - if { - [string length $last_unknown] && \ - [string equal -length [string length $last_unknown] $last_unknown $line] - } { - #can't currently detect stdout/stderr writes from unknown's call to exec - #add a clearance newline for direct unknown calls for now - #there is usually output anyway - but we will get an extra blank line now even for a call that only had an exit code - # - # - set unknown_clearance "\n* repl newline" - screen_last_char_add "\uFFFF" clearance "clearance after direct unknown call" - if {[tsv::llength repl runchunks-$repl_runid]} { - if {$status == 0} { - set result [tsv::get repl runchunks-$repl_runid] ;#last_run_display - } else { - - } - set result_is_chunk_list 1 - } - } - #------ - #ok to use repl::screen_needs_clearance from here down.. (code smell - proc only valid use in narrow context) - #*********************************************************** - #rputs -nonewline stderr $unknown_clearance - if {$::punk::repl::debug_repl > 0} { - set lastcharinfo "\n" - set whatcol [string repeat " " 12] - foreach cinfo $::repl::screen_last_char_list { - lassign $cinfo c whatinfo whyinfo - set cdisplay [punk::ansi::ansistring::VIEW -lf 1 -vt 1 $c] - #assertion cdisplay has no raw newlines - if {[punk::char::ansifreestring_width $cdisplay] == 1} { - set cdisplay "$cdisplay " ;#make 2 wide - } - if {[string match repl-debugreport* $whatinfo]} { - #exclude noise debug_repl_emit - but still show the last_char - set whysummary "" - } else { - #set whysummary [string map [list \n "-n-"] $whyinfo] - set whysummary [punk::ansi::ansistring::VIEW -lf 1 -vt 1 $whyinfo] - } - set whatinfo [string range $whatinfo$whatcol 0 [string length $whatcol]] - append lastcharinfo "$cdisplay $whatinfo $whysummary\n" - } - debug_repl_emit "screen_last_chars: $lastcharinfo" - } - - debug_repl_emit "lastoutchar:'$lastoutchar' lasterrchar: '$lasterrchar'" - if {$status == 0} { - debug_repl_emit "command call status: $status OK" - } else { - debug_repl_emit "command call status: $status ERR" - } - - - - - #puts stderr "'$::repl::output_stdout' lastoutchar:'$lastoutchar' result:'$result'" - #$command is an unevaluated script at this point - # so may not be a well formed list e.g 'set x [list a "b"]' - #- lindex $command would sometimes fail - #if {[lindex $command 0] eq "runx"} {} - - - if { - [string equal -length [string length "d/ "] "d/ " $commandstr] || \ - [string equal "d/\n" $commandstr] || \ - [string equal -length [string length "dd/ "] "dd/ " $commandstr] || \ - [string equal "dd/\n" $commandstr] || \ - [string equal -length [string length "./ "] "./ " $commandstr] || \ - [string equal "./\n" $commandstr] || \ - [string equal -length [string length "../ "] "../ " $commandstr] || \ - [string equal "../\n" $commandstr] || \ - [string equal -length [string length "runx "] "runx " $commandstr] || \ - [string equal -length [string length "sh_runx "] "sh_runx " $commandstr] || \ - [string equal -length [string length "runout "] "runout " $commandstr] || \ - [string equal -length [string length "sh_runout "] "sh_runout " $commandstr] || \ - [string equal -length [string length "runerr "] "runerr " $commandstr] || \ - [string equal -length [string length "sh_runerr "] "sh_runerr " $commandstr] - - } { - if {[tsv::llength repl runchunks-$repl_runid]} { - set result [tsv::get repl runchunks-$repl_runid] ;#last_run_display - set result_is_chunk_list 1 - } - } - - # -- --- --- --- --- --- --- --- --- --- - ##an attempt to preserve underlying rep - ##this is not for performance - just to be less disruptive to underlying rep to aid in learning/debugging - # -- --- --- --- --- --- --- --- --- --- - # JN 2023 - The lrange operation is destructive to path internal representation - # The lrange operation is destructive to strings with leading/trailing newlines - # -- --- --- --- --- --- --- --- --- --- - #set saved_errorCode $::errorCode - #set saved_errorInfo $::errorInfo - #if {[catch {lrange $result 0 end} result_as_list]} { - # set is_result_empty [expr {$result eq ""}] - # set ::errorCode $saved_errorCode - # set ::errorInfo $saved_errorInfo - #} else { - # set is_result_empty [expr {[llength $result_as_list] == 0}] - #} - # -- --- --- --- --- --- --- --- --- --- - #set resultrep [::tcl::unsupported::representation $result] - - set is_result_empty [expr {$result eq ""}] - - #catch {puts stderr "yy--->[rep $::arglej]"} - - set reading 1 - if {!$is_result_empty} { - if {$status == 0} { - if {[screen_needs_clearance]} { - rputs -nonewline stderr \n - } - if {$result_is_chunk_list} { - foreach c $result { - lassign $c termchan text - if {[string length $text]} { - switch -- $termchan { - result { - #rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text] - set h [textblock::height $text] - set promptcol [string repeat $resultprompt\n $h] - set promptcol [string range $promptcol 0 end-1] - rputs [textblock::join_basic -- $promptcol $text] - - #puts -nonewline stdout $text - } - resulterr { - rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text] - } - info { - rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text] - } - default { - #rputs -nonewline $termchan $text - set chanprompt "_ " - rputs $termchan ${chanprompt}[string map [list \n "\n${chanprompt}"] $text] - } - } - } - } - - } else { - #----------------------------------------------------------- - # avoid repl forcing string rep of simple results. This is just to aid analysis using tcl::unsupported::representation - #set rparts [split $result {}] - #if {[lsearch $rparts \n] < 0} { - # #type of $result unaffected - # rputs "$resultprompt $result" - #} else { - # #$result will be a string due to use of string map - # rputs $resultprompt[string map [list \n "\n$resultprompt"] $result] - #} - #----------------------------------------------------------- - - #we have copied rawresult using append with empty string - so our string interaction with result var here shouldn't affect the returned value - #empty-string result handled in other branch - if {![tsv::llength repl runchunks-$repl_runid]} { - #write back to tsv var for use by punk::get_runchunks (underscore command) - tsv::set repl runchunks-$repl_runid [list [list result $result]] - } - set flat [string map [list \r\n "" \n ""] $result] - if {[string length $flat] == [string length $result]} { - #no line-endings in data - rputs "$resultprompt$result" - } else { - #if {[string index $result end] eq "\n"} { - # set result [string range $result 0 end-1] - #} - if {[string length $flat] == 0} { - if {[string range $result end-1 end] eq "\r\n"} { - set result [string range $result 0 end-2] - } else { - set result [string range $result 0 end-1] - } - } - #NOTE - textblock::height is the line height - not reflective of height of data with ansi-moves or things like sixels - set h [textblock::height $result] - set promptcol [string repeat $resultprompt\n $h] - set promptcol [string range $promptcol 0 end-1] - #promptcol is uniform-width lines, result may not be. We are ok to join with ragged rhs col here, so use join_basic instead of join - rputs [textblock::join_basic -- $promptcol $result] - - #orig - #rputs $resultprompt[string map [list \r\n "\n$resultprompt" \n "\n$resultprompt"] $result] - } - } - doprompt "P% " - } else { - #tcl err - if {$result_is_chunk_list} { - foreach c [tsv::get repl runchunks-$repl_runid] { - #last_run_display - lassign $c termchan text - if {[string length $text]} { - switch -- $termchan { - result { - rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text] - #puts -nonewline stdout $text - } - resulterr { - rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text] - } - info { - rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text] - } - default { - rputs -nonewline $termchan $text - } - } - } - } - } - - set c [a yellow bold] - set n [a] - rputs stderr $c$result$n - #tcl err hint prompt - lowercase - doprompt "p% " - } - } else { - #doprompt "P% " "green normal" - if {$linenum == 0} { - doprompt "P% " "green normal" - screen_last_char_add " " empty empty - } else { - doprompt "\nP% " "green normal" - screen_last_char_add "\n" empty empty ;#add \n to indicate noclearance required - } - } - #catch {puts stderr "zz1--->[rep $::arglej]"} - #puts stderr "??? $commandstr" - if {$::punk::repl::debug_repl > 0} { - incr ::punk::repl::debug_repl -1 - } - set commandstr "" - #catch {puts stderr "zz2---->[rep $::arglej]"} - - set lines [$editbuf lines] - set buf_has_data 0 - foreach ln $lines { - if {[string trim $ln] ne ""} { - set buf_has_data 1 - } - } - if {$buf_has_data} { - set editbufnext [punk::repl::class::class_editbuf new {}] - lappend editbuf_list $editbufnext - set editbuf_linenum_submitted 0 - set editbuf $editbufnext - } - #editbuf - - } else { - #append commandstr \n - if {$::punk::repl::signal_control_c} { - set ::punk::repl::signal_control_c 0 - fileevent $inputchan readable {} - rputs stderr "* console_control: control-c" - flush stderr - set c [a yellow bold] - set n [a] - rputs stderr "${c}repl interrupted$n" - #set commandstr [list error "repl interrupted"] - set commandstr "" - doprompt ">_ " - flush stdout - - } else { - #Incomplete command - # parse and determine outermost unclosed quote/bracket and include in prompt - if {$linenum == $maxlinenum} { - if {$rawmode} { - #review - #we haven't put the data following last le into commandstr - but we want to display proper completion status prior to enter being hit or more data coming in. - #this could give spurious results for large pastes where buffering chunks it in odd places.? - #it does however give sensible output for the common case of a small paste where the last line ending wasn't included - set waiting [punk::lib::system::incomplete $commandstr[$editbuf line end]] - } else { - set waiting [punk::lib::system::incomplete $commandstr] - } - if {[llength $waiting]} { - set c [lindex $waiting end] - } else { - #set c " " - set c \u240a - } - doprompt ">$c " - } - } - } - - incr linenum - } - - - if {$maxlinenum == -1} { - #when in raw mode - no linefeed yet received - #rputs stderr "repl: no complete input line: $commandstr" - #screen_last_char_add "\n" empty empty - - screen_last_char_add [string index [$editbuf line end] end] stdinchunk stdinchunk - - } - - - #fileevent $inputchan readable [list repl::repl_handler $inputchan $prompt_config] - #catch {puts stderr "zend--->[rep $::arglej]"} - - - } trap {POSIX} {e eopts} { - rputs stderr "trap POSIX '$e' eopts:'$eopts" - flush stderr - } on error {repl_error erropts} { - rputs stderr "error in repl_handler: $repl_error" - rputs stderr "-------------" - rputs stderr "$::errorInfo" - rputs stderr "-------------" - set stdinreader [fileevent $inputchan readable] - if {![string length $stdinreader]} { - rputs stderr "*> $inputchan reader inactive" - } else { - rputs stderr "*> $inputchan reader active" - } - if {[chan eof $inputchan]} { - rputs stderr "todo - attempt restart of repl on input channel: $inputchan in next loop" - catch {set ::punk::ns::ns_current "::"} - #todo set flag to restart repl ? - } else { - rputs stderr "continuing.." - } - flush stderr - } -} - -proc repl::completion {context ebuf} { - -} - -namespace eval repl { - - - proc init {args} { - if {![info exists ::argv0]} { - #error out before we create a thread - punk requires this - review - error "::argv0 not set" - } - #in case -callback_interp wasn't explicitly defined - we make a guess based on how init was called as to whether this is being launched from a 'code' or root ("") interp. - if {[catch {info level -1} caller]} { - #todo logger - #puts "repl::init from: global" - set default_callback_interp "" - } else { - #puts "repl::init from: $caller" - set default_callback_interp "code" - } - variable codethread - variable codethread_cond - variable codethread_mutex - - set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] - foreach {k v} $args { - switch -- $k { - -force - -safe - -safelog - -paths - -callback_interp { - dict set opts $k $v - } - default { - error "repl::init unknown option '$k'. Known-options: [dict keys $opts]" - } - } - } - set opt_force [dict get $opts -force] - set opt_safe [dict get $opts -safe] - set opt_safelog [dict get $opts -safelog] - if {$opt_safelog eq "0"} { - set opt_safelog "" - } - if {[string is boolean -strict $opt_safelog]} { - if {$opt_safelog} { - set opt_safelog ::repl::interpextras::safe_msg - } - } - dict set opts -safelog $opt_safelog - - #If we are launching a repl from within an interp - we need to tell the childthread how to call-back to the parent repl - set opt_callback_interp [dict get $opts -callback_interp] - - if {$codethread ne "" && !$opt_force && [thread::exists $codethread] } { - error "repl:init codethread: $codethread already exists. use -force 1 to override" - } - set codethread [thread::create -preserved] - #review - naming of the possibly 2 cond variables parent and child thread - set codethread_cond [thread::cond create] ;#repl::codethread_cond held by parent(repl) vs punk::repl::codethread::replthread_cond held by child(codethread) - set codethread_mutex [thread::mutex create] - - - set init_script [string map [list %args% [list $opts]\ - %argv0% [list $::argv0]\ - %argv% [list $::argv]\ - %argc% [list $::argc]\ - %replthread% [thread::id]\ - %replthread_cond% $codethread_cond\ - %replthread_interp% [list $opt_callback_interp]\ - %tmlist% [list [tcl::tm::list]]\ - %autopath% [list $::auto_path]\ - ] { - set ::argv0 %argv0% - set ::argv %argv% - set ::argc %argc% - tcl::tm::remove {*}[tcl::tm::list] - tcl::tm::add {*}[lreverse %tmlist%] ;#Must be added in reverse order to get same order as original list! - - #this sets the auto_path in the thread but outside of the code interp that will be created. - #It will also need to be added in that interp - set ::auto_path %autopath% - set tclmajorv [lindex [split [tcl::info::tclversion] .] 0] - #jmn2 - #puts stdout "CODETHREAD tm list" - #puts stdout [join [tcl::tm::list] \n] - #puts stdout "====================" - #flush stdout - #puts stdout "CODETHREAD autopath" - #puts stdout [join $::auto_path \n] - #puts stdout "====================" - #flush stdout - #if {[llength [info commands tcl::zipfs::root]]} { - # set zipbase [file join [tcl::zipfs::root] app] ;#zipfs root has trailing slash - but file join does the right thing - # if {"$zipbase" in [tcl::zipfs::mount]} { - # puts stdout "//zipfs:/app/modules_tcl$tclmajorv exists: [file exists //zipfs:/app/modules_tcl$tclmajorv]" - # } - #} - #puts stdout "====================" - package require punk::packagepreference - punk::packagepreference::install - - package require punk::console - package require punk::repl::codethread - package require shellfilter - #package require shellrun - package require textblock - - #md5 uses open so can't be directly called in a safe interp - #it will need to delegate to a call here in the main interp of the codethread using an installed alias - set md5version [package require md5] - #we also need to 'package provide md5 $md5version' in the safe interp itself so that it won't override - - - #punk::configure_unknown ;#must be called because we hacked the tcl 'unknown' proc - - #child codethread (outside of code interp) needs to know details of the calling repl - set ::punk::repl::codethread::replthread %replthread% ;#point to thread id of parent thread (repl) - set ::punk::repl::codethread::replthread_cond %replthread_cond% - set ::punk::repl::codethread::replthread_interp %replthread_interp% - - # -- --- --- --- - #procs to alias into the codethread interp - #as we are doing string substitution on the whole block anyway, and these values are contant for the life of the thread, we may as well substitute hard values for things like replthread into these procs where possible - # -- --- --- --- - namespace eval ::repl::interphelpers { - proc quit {args} { - #child codethreads run in a 'code' interp therefore if they started another repl - it is within the 'code' interp in that thread - # whereas the first repl launched in the process runs in root interp "" - thread::send -async %replthread% [list interp eval %replthread_interp% ::punk::repl::quit] - } - proc editbuf args { - thread::send %replthread% [list punk::repl::editbuf {*}$args] - } - proc escapeeval {script} { - eval $script - } - proc do_after {args} { - if {[llength $args] == 1} { - return after {*}$args - } - set scr [lindex $args 1] - after [lindex $args 0] [list punk::repl::codethread::runscript $scr] - } - proc repl_ensemble_unknown args { - puts $args - if {[llength $args] == 1} { - return [namespace ensemble configure ::repl::interphelpers::repl_ensemble] - } - } - proc colour args { - set colour_state [thread::send %replthread% [list punk::console::colour]] - if {[llength $args]} { - #colour call was not a query - set new_state [thread::send %replthread% [list punk::console::colour {*}$args]] - if {[expr {$new_state}] ne [expr {$colour_state}]} { - interp eval code [list punk::console::colour {*}$args] ;#align code interp's view of colour state with repl thread - - #we don't want to run a raw script directly in our code interp if we're using variables - #because we will potentially collide with user vars in that context (or create vars there) - so use apply - interp eval code [list apply {docolour { - #adjust channel transform stack - if {!$docolour} { - set s [lindex $::codeinterp::outstack end] - if {$s ne ""} { - shellfilter::stack::remove stdout $s - } - set s [lindex $::codeinterp::errstack end] - if {$s ne ""} { - shellfilter::stack::remove stderr $s - } - } else { - set running_config $::punk::config::running - if {[string length [dict get $running_config color_stdout]]} { - lappend ::codeinterp::outstack [shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]]] - } - if {[string length [dict get $running_config color_stderr]]} { - lappend ::codeinterp::errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] - } - - } - }} $new_state] - } - return $new_state - } else { - return $colour_state - } - #todo - add/remove shellfilter stacked ansiwrap - } - proc vt52 {args} { - return [thread::send %replthread% [list punk::console::vt52 {*}$args]] - } - proc mode args { - #with tsv::set console is_raw we don't need to call mode in both the replthread and the codethread - # REVIEW - call in local interp? how about if codethread is safe interp? - #interp eval code [list ::punk::console::mode {*}$args] - thread::send %replthread% [list punk::console::mode {*}$args] - } - proc cmdtype cmd { - code invokehidden tcl:info:cmdtype $cmd - } - - #punk repl tsv wrappers - proc set_repl_last_unknown args { - tsv::set repl last_unknown {*}$args - } - proc get_repl_runid args { - if {[tsv::exists repl runid]} { - return [tsv::get repl runid] - } else { - return 0 - } - } - proc md5 args { - ::md5::md5 {*}$args - } - proc fconfigure {args} { - code invokehidden fconfigure {*}$args - } - proc fnormalize name { - code invokehidden tcl:file:normalize $name - } - proc fdirname name { - code invokehidden tcl:file:dirname $name - } - } - namespace eval ::repl::interpextras { - #install using safe::setLogCmd - proc safe_msg {msg} { - puts stderr "safebase: $msg" - } - } - - namespace eval ::repl::interphelpers::repl_ensemble { - namespace export {[a-z]*} - namespace ensemble create - namespace ensemble configure [namespace current] -unknown ::repl::interphelpers::repl_ensemble_unknown - variable replinfo - set replinfo [dict create thread %replthread% interp %replthread_interp%] - proc thread {} { - return %replthread% - } - proc info {} { - variable replinfo - return $replinfo - } - proc eval {script} { - thread::send %replthread% $script - } - proc stack {} { - set iname %replthread_interp% - set tid %replthread% - lappend stack [list thread $tid interp $iname] - while {$iname eq "code"} { - set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}] - set tid [thread::send $tid {set ::punk::repl::codethread::replthread}] - lappend stack [list thread $tid interp $iname] - } - return $stack - } - } - namespace eval ::repl::interphelpers::subshell_ensemble { - namespace export {[a-z]*} - namespace ensemble create - proc punk {} { - set ts_start [clock seconds] - set replresult [interp eval code { - package require punk::repl - repl::init -safe punk - repl::start stdin - }] - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - proc safe {args} { - set ts_start [clock seconds] - interp eval code { - package require punk::repl - } - interp eval code [list repl::init -safe safe {*}$args] - set replresult [interp eval code [list repl::start stdin]] - - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - proc safebase {args} { - set ts_start [clock seconds] - interp eval code { - package require punk::repl - } - set codethread [interp eval code [list repl::init -safe safebase {*}$args]] - puts stdout "safebase codethread:$codethread" - set replresult [interp eval code [list repl::start stdin]] - - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - proc punksafe {args} { - set ts_start [clock seconds] - interp eval code { - package require punk::repl - } - interp eval code [list repl::init -safe punksafe {*}$args] - set replresult [interp eval code [list repl::start stdin]] - - return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] - } - } - # -- --- --- --- --- - - #puts "codethread:[thread::id] parent replthread:%replthread%" - #flush stdout - - set args %args% - set safe [dict get $args -safe] - set safelog [dict get $args -safelog] - set paths [list] - if {[dict exists $args -paths]} { - set paths [dict get $args -paths] - } - - switch -- $safe { - safe { - interp create -safe -- code - package require punk::args - } - safebase { - safe::interpCreate code -nested 1 -autoPath %autopath% - code alias "file normalize" "file normalize" - code alias "file dirname" "file dirname" - code alias "file exists" "file exists" - code alias ::tcl::file::normalize ::tcl::file::normalize - code alias ::tcl::file::dirname ::tcl::file::dirname - code alias ::tcl::file::exists ::tcl::file::exists - #code alias ::punk::console::colour ::punk::console::colour - } - punksafe { - #less safe than safebase - we need file normalize and info script to handle modpod? - package require punk::safe - punk::safe::interpCreate code -autoPath %autopath% - code alias "file normalize" "file normalize" - code alias "file dirname" "file dirname" - code alias "file exists" "file exists" - code alias ::tcl::file::normalize ::tcl::file::normalize - code alias ::tcl::file::dirname ::tcl::file::dirname - code alias ::tcl::file::exists ::tcl::file::exists - code alias ::punk::console::colour ::punk::console::colour - } - punk - 0 { - interp create code - } - punkisland { - #todo - #when no island paths specified - should be like safebase, but without folder hiding and with expanded read to ::auto_path folders - } - } - - interp eval code { - namespace eval codeinterp { - variable errstack {} - variable outstack {} - variable run_command_cache - proc set_clone {varname obj} { - append obj2 $obj {} - uplevel 1 [list set $varname $obj2] - } - } - } - - switch -- $safe { - safe { - if {[llength $paths]} { - package require punk::island - foreach p $paths { - punk::island::add code $p - } - } - interp share "" stdout code - interp share "" stderr code - interp share "" stdin code ;#needed for ANSI queries - - set codehidden [code hidden] - code alias file file - if {"source" in $codehidden} { - code expose source - } - if {"encoding" in $codehidden} { - code expose encoding ;#leave enabled - } - if {"tcl:encoding:system" in $codehidden} { - code expose tcl:encoding:system - code eval {rename ::tcl::encoding::system ""} - code eval {rename tcl:encoding:system ::tcl::encoding::system} - } - #interp alias is available in safe - so it seems unreasonable to disallow 'info cmdtype' - if {"tcl:info:cmdtype" in $codehidden} { - code eval {rename ::tcl::info::cmdtype ""} - code expose tcl:info:cmdtype - code eval {rename tcl:info:cmdtype ::tcl::info::cmdtype} - } - set pkgs [list\ - punk::args\ - punk::pipe\ - cmdline\ - struct::list\ - struct::set\ - textutil::wcswidth\ - textutil::trim\ - textutil::repeat\ - textutil::tabify\ - textutil::split\ - textutil::string\ - textutil::adjust\ - textutil\ - punk::encmime\ - punk::char\ - punk::assertion\ - punk::ansi\ - punk::lib\ - overtype\ - dictutils\ - debug\ - punk::ns\ - textblock\ - punk::args::tclcore\ - punk::aliascore\ - ] - - #pattern looks up versions available of patternlib before loading (but we don't have an index for tm files) todo fix pattern. - # patterncmd\ - # metaface\ - # patternpredator2\ - # patternlib\ - # pattern - - # - no longer required by textblock - # term::ansi::code\ - # term::ansi::code::attr\ - # term::ansi::code::ctrl\ - # term::ansi::code::macros - - #---------- - #all this scanning and loading core packages - we should possibly cache the file data for other interps? - #make sure codethread has scanned for packages - must do for each namespace level - #catch {package require flubber_nonexistent} - set ns_scanned [dict create] - #---------- - set prior_infoscript [code eval {info script}] ;#probably empty that's ok - foreach pkg $pkgs { - if {[catch { - set nsquals [namespace qualifiers $pkg] - if {$nsquals ne ""} { - if {![dict exists $ns_scanned $nsquals]} { - catch {package require ${nsquals}::flubber_nonexistant} ;#force scan - dict set ns_scanned $nsquals 1 - } - } - set versions [lsort -command {package vcompare} [package versions $pkg]] - if {[llength $versions]} { - set v [lindex $versions end] - set path [lindex [package ifneeded $pkg $v] end] - if {[file extension $path] in {.tcl .tm}} { - if {[file exists $path]} { - set data [readFile $path] - code eval [list info script $path] - code eval $data - code eval [list info script $prior_infoscript] - } else { - error "safe - failed to find $path" - } - } else { - error "safe - refusing to attempt load of $pkg from $path - (allowed extensions .tcl .tm)" - } - } else { - error "safe - no versions of $pkg found" - } - } errMsg]} { - puts stderr "safe - failed to load package $pkg\n$errMsg\n$::errorInfo" - } else { - #puts stdout "safe - loaded $pkg from $path" - } - } - code alias file "" - code hide source - - - #review argv0,argv,argc - #interp eval code { - # set ::argv0 %argv0% - # set ::auto_path %autopath% - #} - interp eval code [list set ::tcl_platform(os) $::tcl_platform(os)] - interp eval code [list set ::tcl_platform(osVersion) $::tcl_platform(osVersion)] - interp eval code [list set ::tcl_platform(machine) $::tcl_platform(machine)] - if {"stdout" in [chan names]} { - interp share {} stdout code - } else { - interp share {} [shellfilter::stack::item_tophandle stdout] code - } - if {"stderr" in [chan names]} { - interp share {} stderr code - } else { - interp share {} [shellfilter::stack::item_tophandle stderr] code - } - - #review - code alias ::shellfilter::stack ::shellfilter::stack - #code alias ::punk::lib::set_clone ::punk::lib::set_clone - #code alias ::aliases ::punk::lib::aliases - code alias ::punk::lib::aliases ::punk::lib::aliases - namespace eval ::codeinterp {} - - code alias ::md5::md5 ::repl::interphelpers::md5 - code alias exit ::repl::interphelpers::quit - } - safebase { - #safebase - #safe::setLogCmd ::repl::interpextras::safe_msg ;#using setLogcmd early will show the auto_path notice - which is *verbose* - #while it may conceivably be useful in debugging safe itself - auto_path and tcl::tm::list can be inspected to show these values in the safe interp itself anyway - so early logging is of limited utility here. - if {[llength $paths]} { - package require punk::island - foreach p $paths { - punk::island::add code $p - } - } - interp eval code { - set ::argv0 %argv0% - set ::argc 0 - set ::argv {} - #puts stdout "safebase interp" - #flush stdout - } - interp eval code [list set ::tcl_platform(os) $::tcl_platform(os)] - interp eval code [list set ::tcl_platform(osVersion) $::tcl_platform(osVersion)] - interp eval code [list set ::tcl_platform(machine) $::tcl_platform(machine)] - - code alias ::fconfigure ::repl::interphelpers::fconfigure ;#needed for shellfilter - - #code invokehidden package require punk::lib - if {"stdout" in [chan names]} { - interp share {} stdout code - } else { - interp share {} [shellfilter::stack::item_tophandle stdout] code - } - if {"stderr" in [chan names]} { - interp share {} stderr code - } else { - interp share {} [shellfilter::stack::item_tophandle stderr] code - } - interp eval code { - package require punk::lib - package require textblock - } - - #JMN - interp eval code { - package require shellfilter - } - - - #work around bug in safe base which won't load Tcl libs that have deeper nesting - #(also affects tcllib page/plugins folder) - set termversions [package versions term] - set termv [lindex $termversions end] - if {$termv ne ""} { - set path [lindex [package ifneeded term $termv] end] ;#assuming path at end of something like "source .../term.tcl" - set termbase [file dirname $path] - safe::interpAddToAccessPath code [file join $termbase ansi] - safe::interpAddToAccessPath code [file join $termbase ansi code] - } - #safe::interpAddToAccessPath code NUL - if {$safelog ne ""} { - #setting setLogCmd here gives potentially interesting feedback regarding behaviour of things such as glob - safe::setLogCmd $safelog - } - #code invokehidden source c:/repo/jn/shellspy/modules/punk/lib-0.1.1.tm - - code alias detok ::safe::DetokPath code ;#temp - this violates the point of the {$p(:X:)} paths - - #review - exit should do something slightly different - # see ::safe::interpDelete - code alias exit ::repl::interphelpers::quit - - code alias ::md5::md5 ::repl::interphelpers::md5 - code alias ::file ::file - interp eval code [list package provide md5 $md5version] - - } - punksafe { - interp eval code { - set ::argv0 %argv0% - set ::argc 0 - set ::argv {} - #set ::auto_path %autopath% ;#jmn - #tcl::tm::remove {*}[tcl::tm::list] - #tcl::tm::add {*}[lreverse %tmlist%] - } - interp eval code [list set ::tcl_platform(os) $::tcl_platform(os)] - interp eval code [list set ::tcl_platform(osVersion) $::tcl_platform(osVersion)] - interp eval code [list set ::tcl_platform(machine) $::tcl_platform(machine)] - - - code alias ::fconfigure ::repl::interphelpers::fconfigure ;#needed for shellfilter - - if {"stdout" in [chan names]} { - interp share {} stdout code - } else { - interp share {} [shellfilter::stack::item_tophandle stdout] code - } - if {"stderr" in [chan names]} { - interp share {} stderr code - } else { - interp share {} [shellfilter::stack::item_tophandle stderr] code - } - - interp eval code { - package require punk::lib - package require punk::args - package require punk::args::tclcore - package require textblock - } - - interp eval code { - if {[catch { - #package require packagetrace - #packagetrace::init - } errM]} { - puts stderr "========================" - puts stderr "code interp error 1:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - #error "$errM" - } - } - - - interp eval code { - if {[catch { - package require punk::config ;#requires: none - #package require punk::console ;#requires: Thread,punk::ansi,punk::args - #set running_config $::punk::config::running - package require shellfilter ;#requires: shellthread,Thread - apply {running_config { - if {[string length [dict get $running_config color_stderr]] && [punk::console::colour]} { - lappend ::codeinterp::errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] - } - if {[string length [dict get $running_config color_stdout]] && [punk::console::colour]} { - lappend ::codeinterp::outstack [shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]]] - } - }} $::punk::config::running - - } errM]} { - puts stderr "========================" - puts stderr "code interp error 2:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - error "$errM" - } - } - - interp eval code { - if {[catch { - - #puts stderr "loading natsort" - #natsort has 'application mode' which can exit. - #Requiring it shouldn't trigger application - but zipfs/vfs interactions confused it in some early versions - package require natsort - - #package require punk ;# Thread - #package require shellrun ;#subcommand exists of file - - - #----------------------------------------------------------------------------------------------------------------------------------------- - package require punk::ns ;#requires:punk::lib,punk::args,struct::list,cmdline+(tcllibc),struct::set,punk::ansi,punk::char, - #textutil,textutil::string,textutil::adjust,textutil::repeat,textutil::string,textutil::split,textutil::tabify,textutil::wcswidth - #punk::encmime,punk::assertion - #twapi,platform,registry,debug,overtype,patternpunk,pattern,patterncmd,metaface,patternpredator2,patternlib,dictutils - #----------------------------------------------------------------------------------------------------------------------------------------- - - #package require textblock - } errM]} { - puts stderr "========================" - puts stderr "code interp error 3:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - error "$errM" - } - - } - - } - punk - 0 { - interp eval code { - #safe !=1 and safe !=2, tmlist: %tmlist% - set ::argv0 %argv0% - set ::argv %argv% - set ::argc %argc% - set ::auto_path %autopath% - tcl::tm::remove {*}[tcl::tm::list] - tcl::tm::add {*}[lreverse %tmlist%] - #puts "code interp chan names-->[chan names]" - - # -- --- - #review - #we have to blow some time on a rescan to provide more deterministic ordering (match behaviour of initial thread regarding pkg precedence) - #review - can we speed that scan up? - ##catch {package require flobrudder-nonexistant} - # -- --- - - if {[catch { - package require vfs - package require vfs::zip - } errM]} { - puts stderr "repl code interp can't load vfs,vfs::zip" - } - - #puts stderr ----- - #puts stderr [join $::auto_path \n] - #puts stderr ----- - - if {[catch { - package require punk::config - package require punk::ns - #puts stderr "loading natsort" - #natsort has 'application mode' which can exit. - #Requiring it shouldn't trigger application - but zipfs/vfs interactions confused it in some early versions - package require natsort - #catch {package require packageTrace} - package require punk - package require punk::args - package require punk::args::tclcore - package require shellrun - package require shellfilter - #set running_config $::punk::config::running - apply {running_config { - if {[string length [dict get $running_config color_stderr]] && [punk::console::colour]} { - lappend ::codeinterp::errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] - } - if {[string length [dict get $running_config color_stdout]] && [punk::console::colour]} { - lappend ::codeinterp::outstack [shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]]] - } - }} $::punk::config::running - - package require textblock - } errM]} { - puts stderr "========================" - puts stderr "code interp error:" - puts stderr $errM - puts stderr $::errorInfo - puts stderr "========================" - error "$errM" - } - } - } - default { - } - } - code alias repl ::repl::interphelpers::repl_ensemble - code alias subshell ::repl::interphelpers::subshell_ensemble - code alias quit ::repl::interphelpers::quit - code alias editbuf ::repl::interphelpers::editbuf - code alias colour ::repl::interphelpers::colour - code alias mode ::repl::interphelpers::mode - code alias vt52 ::repl::interphelpers::vt52 - #code alias after ::repl::interphelpers::do_after - - code alias ::punk::set_repl_last_unknown ::repl::interphelpers::set_repl_last_unknown - code alias ::punk::get_repl_runid ::repl::interphelpers::get_repl_runid - - #JMN - #code alias cmdtype ::repl::interphelpers::cmdtype - #temporary debug aliases - deliberate violation of safety provided by safe interp - code alias escapeeval ::repl::interphelpers::escapeeval - - - #experiment - #code alias ::shellfilter::stack ::shellfilter::stack - - #puts stderr "returning threadid" - #puts stderr [thread::id] - - thread::id - }] - - #thread::send $codethread $init_script - if {![catch { - thread::send $codethread $init_script result ;#use a result var when wrapping in a catch - without it we can get a return code of 2 (TCL_RETURN) - } errMsg]} { - return $result - } else { - puts stderr "repl::init Failed during thread::send" - puts stderr "$::errorInfo" - thread::release $codethread - error $errMsg - } - } - #init - don't auto init - require init with possible options e.g -safe -} -package provide punk::repl [namespace eval punk::repl { - variable version - set version 0.1 -}] - -#repl::start $program_read_stdin_pipe - - - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/winlnk-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/winlnk-0.1.0.tm deleted file mode 100644 index 562aad09..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/winlnk-0.1.0.tm +++ /dev/null @@ -1,836 +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.3.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) 2024 -# -# @@ Meta Begin -# Application punk::winlnk 0.1.0 -# Meta platform tcl -# Meta license MIT -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::winlnk 0 0.1.0] -#[copyright "2024"] -#[titledesc {windows shortcut .lnk library}] [comment {-- Name section and table of contents description --}] -#[moddesc {punk::winlnk}] [comment {-- Description at end of page heading --}] -#[require punk::winlnk] -#[keywords module shortcut lnk parse windows crossplatform] -#[description] -#[para] Tools for reading windows shortcuts (.lnk files) on any platform - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of punk::winlnk -#[subsection Concepts] -#[para] Windows shortcuts are a binary format file with a .lnk extension -#[para] Shell Link (.LNK) Binary File Format is documented in [lb]MS_SHLLINK[rb].pdf published by Microsoft. -#[para] Revision 8.0 published 2024-04-23 - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::winlnk -#[list_begin itemized] - -package require Tcl 8.6- -#*** !doctools -#[item] [package {Tcl 8.6}] - -#TODO - logger - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::winlnk { - tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - #variable xyz - - #*** !doctools - #[subsection {Namespace punk::winlnk}] - #[para] Core API functions for punk::winlnk - #[list_begin definitions] - - - variable magic_HeaderSize "0000004C" ;#HeaderSize MUST equal this - variable magic_LinkCLSID "00021401-0000-0000-C000-000000000046" ;#LinkCLSID MUST equal this - - proc Get_contents {path {bytes all}} { - if {![file exists $path] || [file type $path] ne "file"} { - error "punk::winlnk::get_contents cannot find a filesystem object of type 'file' at location: $path" - } - set fd [open $path r] - chan configure $fd -translation binary -encoding iso8859-1 - if {$bytes eq "all"} { - set data [read $fd] - } else { - set data [read $fd $bytes] - } - close $fd - return $data - } - proc Contents_check_header {contents} { - variable magic_HeaderSize - variable magic_LinkCLSID - expr {[Header_Get_HeaderSize $contents] eq $magic_HeaderSize && [Header_Get_LinkCLSID $contents] eq $magic_LinkCLSID} - } - - #LinkFlags - 4 bytes - specifies information about the shell link and the presence of optional portions of the structure. - proc Show_LinkFlags {contents} { - set 4bytes [string range $contents 20 23] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - puts "val: $val" - set declist [scan [string reverse $4bytes] %c%c%c%c] - set fmt [string repeat %08b 4] - puts "LinkFlags:[format $fmt {*}$declist]" - - set r [binary scan $4bytes b32 val] - puts "bscan-le: $val" - set r [binary scan [string reverse $4bytes] b32 val] - puts "bscan-2 : $val" - } - variable LinkFlags - set LinkFlags [dict create\ - hasLinkTargetIDList 1\ - HasLinkInfo 2\ - HasName 4\ - HasRelativePath 8\ - HasWorkingDir 16\ - HasArguments 32\ - HasIconLocation 64\ - IsUnicode 128\ - ForceNoLinkInfo 256\ - HasExpString 512\ - RunInSeparateProcess 1024\ - Unused1 2048\ - HasDarwinID 4096\ - RunAsUser 8192\ - HasExpIcon 16394\ - NoPidlAlias 32768\ - Unused2 65536\ - RunWithShimLayer 131072\ - ForceNoLinkTrack 262144\ - EnableTargetMetadata 524288\ - DisableLinkPathTracking 1048576\ - DisableKnownFolderTracking 2097152\ - DisableKnownFolderAlias 4194304\ - AllowLinkToLink 8388608\ - UnaliasOnSave 16777216\ - PreferEnvironmentPath 33554432\ - KeepLocalIDListForUNCTarget 67108864\ - ] - variable LinkFlagLetters [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 AA] - proc Header_Has_LinkFlag {contents flagname} { - variable LinkFlags - variable LinkFlagLetters - if {[string length $flagname] <= 2} { - set idx [lsearch $LinkFlagLetters $flagname] - if {$idx < 0} { - error "punk::winlnk::Header_Has_LinkFlag error - flagname $flagname not known" - } - set binflag [expr {2**$idx}] - set allflags [Header_Get_LinkFlags $contents] - return [expr {$allflags & $binflag}] - } - if {[dict exists $LinkFlags $flagname]} { - set binflag [dict get $LinkFlags $flagname] - set allflags [Header_Get_LinkFlags $contents] - return [expr {$allflags & $binflag}] - } else { - error "punk::winlnk::Header_Has_LinkFlag error - flagname $flagname not known" - } - } - - #MS-SHLLINK.pdf documents the .lnk file format in detail, but here is a brief overview of the structure of a .lnk file: - #protocol revision 10.0 (November 2025) https://winprotocoldocs-bhdugrdyduf5h2e4.b02.azurefd.net/MS-SHLLINK/%5bMS-SHLLINK%5d.pdf - - - #SHELL_LINK_HEADER structure is 76 bytes long and starts at the beginning of the file - #offset hex:0x00 dec:0 4 bytes - #Header size (HeaderSize) (must be 0x0000004C for .lnk files) - proc Header_Get_HeaderSize {contents} { - set 4bytes [split [string range $contents 0 3] ""] - set hex4 "" - foreach b [lreverse $4bytes] { - set dec [scan $b %c] ;# 0-255 decimal - set HH [format %2.2llX $dec] - append hex4 $HH - } - return $hex4 - } - - - #offset hex:0x04 dec:4 16 bytes - #LinkCLSID (must be 00021401-0000-0000-C000-000000000046 for .lnk files) - proc Header_Get_LinkCLSID {contents} { - set 16bytes [string range $contents 4 19] - #CLSID hex textual representation is split as 4-2-2-2-6 bytes(hex pairs) - #e.g We expect 00021401-0000-0000-C000-000000000046 for .lnk files - #for endianness - it is little endian all the way but the split is 4-2-2-1-1-1-1-1-1-1-1 REVIEW - #(so it can appear as mixed endianness if you don't know the splits) - #https://devblogs.microsoft.com/oldnewthing/20220928-00/?p=107221 - #This is based on COM textual representation of GUIDS - #Apparently a CLSID is a GUID that identifies a COM object - set clsid "" - set s1 [tcl::string::range $16bytes 0 3] - set declist [scan [string reverse $s1] %c%c%c%c] - set fmt "%02X%02X%02X%02X" - append clsid [format $fmt {*}$declist] - - append clsid - - set s2 [tcl::string::range $16bytes 4 5] - set declist [scan [string reverse $s2] %c%c] - set fmt "%02X%02X" - append clsid [format $fmt {*}$declist] - - append clsid - - set s3 [tcl::string::range $16bytes 6 7] - set declist [scan [string reverse $s3] %c%c] - append clsid [format $fmt {*}$declist] - - append clsid - - #now treat bytes individually - so no endianness conversion - set declist [scan [tcl::string::range $16bytes 8 9] %c%c] - append clsid [format $fmt {*}$declist] - - append clsid - - set scan [string repeat %c 6] - set fmt [string repeat %02X 6] - set declist [scan [tcl::string::range $16bytes 10 15] $scan] - append clsid [format $fmt {*}$declist] - - return $clsid - } - - - #offset hex:0x14 dec:20 4 bytes - #Link flags (LinkFlags) - bit field specifying information about the shell link and the presence of optional portions of the structure. - #HasLinkTargetIDList bit 0 (0x00000001) - if set, a LinkTargetIDList structure is present immediately following the header - #HasLinkInfo bit 1 (0x00000002) - if set, a LinkInfo structure is present immediately following the header (or the LinkTargetIDList if that is present) - #HasName bit 2 (0x00000004) - if set, a null-terminated string containing the name of the link is present immediately following the header (or the LinkTargetIDList and LinkInfo if they are present) - #HasRelativePath bit 3 (0x00000008) - if set, a null-terminated string containing the relative path of the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo and Name if they are present) - #HasWorkingDir bit 4 (0x00000010) - if set, a null-terminated string containing the working directory of the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name and Relative Path if they are present) - #HasArguments bit 5 (0x00000020) - if set, a null-terminated string containing the command line arguments for the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path and Working Dir if they are present) - #HasIconLocation bit 6 (0x00000040) - if set, a null-terminated string containing the location of the icon for the link is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir and Arguments if they are present) - #IsUnicode bit 7 (0x00000080) - if set, the strings in the link are stored in Unicode (UTF-16LE) format; if not set, the strings are stored in ANSI format (usually the system's default code page) - #ForceNoLinkInfo bit 8 (0x00000100) - if set, the LinkInfo structure is not stored in the file even if the HasLinkInfo bit is set; this can be used to force the link to be resolved using only the information in the header and the optional strings, without using the LinkInfo structure - #HasExpString bit 9 (0x00000200) - if set, a null-terminated string containing an "environment variable" style string is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments and Icon Location if they are present); this string can contain environment variable references (e.g. %USERPROFILE%) that can be expanded to obtain the actual path of the link target - #RunInSeparateProcess bit 10 (0x00000400) - if set, the link target should be run in a separate process; if not set, the link target may be run in the same process as the caller - #Unused1 bit 11 (0x00000800) - reserved for future use; should be set to 0 - #HasDarwinID bit 12 (0x00001000) - if set, a null-terminated string containing a "Darwin ID" is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments, Icon Location and ExpString if they are present); this string can be used to identify the link target in a way that is independent of the file system (e.g. for links to Control Panel items or special folders) - #RunAsUser bit 13 (0x00002000) - if set, the link target should be run with the permissions of the user specified in the HasDarwinID string; if not set, the link target should be run with the permissions of the caller - #HasExpIcon bit 14 (0x00004000) - if set, a null-terminated string containing an "environment variable" style string for the icon location is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments, Icon Location, ExpString and DarwinID if they are present); this string can contain environment variable references that can be expanded to obtain the actual path of the icon for the link - #NoPidlAlias bit 15 (0x00008000) - if set, the link target should not be resolved using the PIDL alias mechanism; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed - #Unused2 bit 16 (0x00010000) - reserved for future use; should be set to 0 - #RunWithShimLayer bit 17 (0x00020000) - if set, the link target should be run with the application compatibility shim layer; if not set, the link target should be run without the shim layer - #ForceNoLinkTrack bit 18 (0x00040000) - if set, the link target should not be tracked by the shell's link tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed - #EnableTargetMetadata bit 19 (0x00080000) - if set, the link target should have metadata enabled; this can be used to allow the link to store additional information about the target (e.g. for links to files, the link can store the file's attributes, creation time, access time and modification time) - #DisableLinkPathTracking bit 20 (0x00100000) - if set, the link target should not be tracked by the shell's link path tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed based on its path - #DisableKnownFolderTracking bit 21 (0x00200000) - if set, the link target should not be tracked by the shell's known folder tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed based on its known folder ID - #DisableKnownFolderAlias bit 22 (0x00400000) - if set, the link target should not be aliased to a known folder; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on its known folder ID - #AllowLinkToLink bit 23 (0x00800000) - if set, the link target can be another link; if not set, the link target should not be another link (i.e. it should be a file or directory); this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on the fact that it is a link - #UnaliasOnSave bit 24 (0x01000000) - if set, the link should be unaliased when it is saved; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on the fact that it is a link - #PreferEnvironmentPath bit 25 (0x02000000) - if set, the link should prefer to resolve the target using environment variable references; this can be used to allow the link to be resolved correctly even if the target is moved or renamed, as long as the environment variable references still point to the correct location - #KeepLocalIDListForUNCTarget bit 26 (0x04000000) - if set, the link should keep the local ID list for UNC targets; this can be used to allow the link to be resolved correctly even if the target is moved or renamed, as long as the local ID list still points to the correct location - # - the presence of these flags indicates the presence of optional structures in the .lnk file and also provides information about how to interpret the data in the file - proc Header_Get_LinkFlags {contents} { - set 4bytes [string range $contents 20 23] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - #offset hex:0x18 dec:24 4 bytes - #File attributes (FileAttributes) - bit field specifying the file attributes of the link target (if the EnableTargetMetadata flag is set in the LinkFlags field); this field is a bitwise combination of the following values: - proc Header_Get_FileAttributes {contents} { - if {![Header_Has_LinkFlag $contents "EnableTargetMetadata"]} { - return {} - } - set 4bytes [string range $contents 24 27] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - set attrlist {} - if {$val & 0x00000001} {lappend attrlist "READONLY"} - if {$val & 0x00000002} {lappend attrlist "HIDDEN"} - if {$val & 0x00000004} {lappend attrlist "SYSTEM"} - if {$val & 0x00000010} {lappend attrlist "DIRECTORY"} - if {$val & 0x00000020} {lappend attrlist "ARCHIVE"} - if {$val & 0x00000040} {lappend attrlist "DEVICE"} - if {$val & 0x00000080} {lappend attrlist "NORMAL"} - if {$val & 0x00000100} {lappend attrlist "TEMPORARY"} - if {$val & 0x00000200} {lappend attrlist "SPARSE_FILE"} - if {$val & 0x00000400} {lappend attrlist "REPARSE_POINT"} - if {$val & 0x00000800} {lappend attrlist "COMPRESSED"} - if {$val & 0x00001000} {lappend attrlist "OFFLINE"} - if {$val & 0x00002000} {lappend attrlist "NOT_CONTENT_INDEXED"} - if {$val & 0x00004000} {lappend attrlist "ENCRYPTED"} - return $attrlist - } - proc Header_Get_FileAttributes_Raw {contents} { - if {![Header_Has_LinkFlag $contents "EnableTargetMetadata"]} { - return 0 - } - set 4bytes [string range $contents 24 27] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - - - - #offset hex:0x1C dec:28 8 bytes - #creation date and time (CreationTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)) - proc Header_Get_CreationTime {contents} { - set 8bytes [string range $contents 28 35] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - #convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC) - #we can convert it to seconds and then to a human readable format - set seconds [expr {$val / 10000000.0}] - set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970 - set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true] - return $human_time - } - proc Header_Get_CreationTime_Raw {contents} { - set 8bytes [string range $contents 28 35] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - return $val - } - - #offset 36 8 bytes - #last access date and time (AccessTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)) - proc Header_Get_AccessTime {contents} { - set 8bytes [string range $contents 36 43] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - #convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC) - #we can convert it to seconds and then to a human readable format - set seconds [expr {$val / 10000000.0}] - set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970 - set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true] - return $human_time - } - proc Header_Get_AccessTime_Raw {contents} { - set 8bytes [string range $contents 36 43] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - return $val - } - - #offset hex:0x2C dec:44 8 bytes - #last modification date and time (WriteTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)) - proc Header_Get_WriteTime {contents} { - set 8bytes [string range $contents 44 51] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - #convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC) - #we can convert it to seconds and then to a human readable format - set seconds [expr {$val / 10000000.0}] - set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970 - set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true] - return $human_time - } - proc Header_Get_WriteTime_Raw {contents} { - set 8bytes [string range $contents 44 51] - set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int - return $val - } - - #offset hex:0x34 dec:52 Bytes:4 - unsigned int - #file size in bytes (of target - low 32 bits if >4GB) - proc Header_Get_FileSize {contents} { - set 4bytes [string range $contents 52 55] - set r [binary scan $4bytes i val] - return $val - } - - #offset hex:0x38 dec:56 Bytes:4 - signed integer - #icon index value - proc Header_Get_IconIndex {contents} { - set 4bytes [string range $contents 56 59] - set r [binary scan $4bytes i val] - return $val - } - - #offset hex:0x3C dec:60 Bytes:4 - unsigned integer - #SW_SHOWNORMAL 0x00000001 - #SW_SHOWMAXIMIZED 0x00000001 - #SW_SHOWMINNOACTIVE 0x00000007 - # - all other values MUST be treated as SW_SHOWNORMAL - proc Header_Get_ShowCommand {contents} { - set 4bytes [string range $contents 60 63] - set r [binary scan $4bytes i val] - return $val - } - - #offset hex:0x40 dec:64 Bytes:2 - #Hot key - proc Header_Get_HotKey {contents} { - # Existing code that extracts the raw 16‑bit hotkey value: - set raw [Header_Get_HotKey_Raw $contents] - # The low byte holds the virtual‑key, high byte holds modifier flags - set vk [expr {$raw & 0xFF}] - set mods [expr {($raw >> 8) & 0xFF}] - set name [_vk_to_name $vk] - set modStr [_modifiers_to_string $mods] - if {$modStr eq ""} { - return $name - } else { - return "${modStr}+${name}" - } - } - proc Header_Get_HotKey_Raw {contents} { - set 2bytes [string range $contents 64 65] - set r [binary scan $2bytes s val] ;#short - return $val - } - proc _modifiers_to_string {mods} { - set parts {} - if {$mods & 0x01} {lappend parts "Shift"} - if {$mods & 0x02} {lappend parts "Ctrl"} - if {$mods & 0x04} {lappend parts "Alt"} - if {$mods & 0x08} {lappend parts "Win"} ;# optional - return [join $parts "+"] - } - proc _vk_to_name {vk} { - # Minimal map – extend as needed - array set vkMap { - 0x00 "No key assigned" - 0x08 Backspace 0x09 Tab 0x0D Return - 0x10 Shift 0x11 Control 0x12 Alt - 0x20 Space 0x21 PageUp 0x22 PageDown - 0x23 End 0x24 Home 0x25 Left - 0x26 Up 0x27 Right 0x28 Down - 0x2D Insert 0x2E Delete - 0x70 F1 0x71 F2 0x72 F3 - 0x73 F4 0x74 F5 0x75 F6 - 0x76 F7 0x77 F8 0x78 F9 - 0x79 F10 0x7A F11 0x7B F12 - 0x7c F13 0x7d F14 0x7e F15 - 0x7f F16 0x80 F17 0x81 F18 - 0x82 F19 0x83 F20 0x84 F21 - 0x85 F22 0x86 F23 0x87 F24 - 0x90 "NUM LOCK" 0x91 "SCROLL LOCK" - } - if {[info exists vkMap($vk)]} { - return $vkMap($vk) - } else { - if {$vk >= 0x30 && $vk <= 0x39} { - return [format "%c" $vk] ;# 0-9 - } elseif {$vk >= 0x41 && $vk <= 0x5A} { - return [format "%c" $vk] ;# A-Z - } - # fallback: hex representation - return [format "0x%02X" $vk] - } - } - - #offset hex:0x42 dec:66 Bytes:2 - reserved1 - proc Header_Get_Reserved1 {contents} { - set 2bytes [string range $contents 66 67] - set r [binary scan $2bytes s val] ;#short - return $val - } - - #offset hex:0x44 dec:68 Bytes:4 - reserved2 - proc Header_Get_Reserved2 {contents} { - set 4bytes [string range $contents 68 71] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - #offset hex:0x48 dec:72 Bytes:4 - reserved3 - proc Header_Get_Reserved3 {contents} { - set 4bytes [string range $contents 72 75] - set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int - return $val - } - - #end of 76 byte header - - proc Get_LinkTargetIDList_size {contents} { - if {[Header_Has_LinkFlag $contents "A"]} { - set 2bytes [string range $contents 76 77] - set r [binary scan $2bytes s val] ;#short - #logger - #puts stderr "LinkTargetIDList_size: $val" - return $val - } else { - return 0 - } - } - proc Get_LinkInfo_content {contents} { - set idlist_size [Get_LinkTargetIDList_size $contents] - if {$idlist_size == 0} { - set offset 0 - } else { - set offset [expr {2 + $idlist_size}] ;#LinkTargetIdList IDListSize field + value - } - set linkinfo_start [expr {76 + $offset}] - if {[Header_Has_LinkFlag $contents "B"]} { - #puts stderr "linkinfo_start: $linkinfo_start" - set 4bytes [string range $contents $linkinfo_start $linkinfo_start+3] - binary scan $4bytes i val ;#size *including* these 4 bytes - set linkinfo_content [string range $contents $linkinfo_start [expr {$linkinfo_start + $val -1}]] - return [dict create linkinfo_start $linkinfo_start size $val next_start [expr {$linkinfo_start + $val}] content $linkinfo_content] - } else { - return [dict create linkinfo_start $linkinfo_start size 0 next_start $linkinfo_start content ""] - } - } - - proc LinkInfo_get_fields {linkinfocontent} { - set 4bytes [string range $linkinfocontent 0 3] - binary scan $4bytes i val ;#size *including* these 4 bytes - set bytes_linkinfoheadersize [string range $linkinfocontent 4 7] - set bytes_linkinfoflags [string range $linkinfocontent 8 11] - set r [binary scan $4bytes i flags] ;# i for little endian 32-bit signed int - #puts "linkinfoflags: $flags" - - set localbasepath "" - set commonpathsuffix "" - - #REVIEW - flags problem? - if {$flags & 1} { - #VolumeIDAndLocalBasePath - #logger - #puts stderr "VolumeIDAndLocalBasePath" - } - if {$flags & 2} { - #logger - #puts stderr "CommonNetworkRelativeLinkAndPathSuffix" - } - set bytes_volumeid_offset [string range $linkinfocontent 12 15] - set bytes_localbasepath_offset [string range $linkinfocontent 16 19] ;# a - set bytes_commonnetworkrelativelinkoffset [string range $linkinfocontent 20 23] - set bytes_commonpathsuffix_offset [string range $linkinfocontent 24 27] ;# a - - binary scan $bytes_localbasepath_offset i bp_offset - if {$bp_offset > 0} { - set tail [string range $linkinfocontent $bp_offset end] - set stringterminator 0 - set i 0 - set localbasepath "" - #TODO - while {!$stringterminator & $i < 100} { - set c [string index $tail $i] - if {$c eq "\x00"} { - set stringterminator 1 - } else { - append localbasepath $c - } - incr i - } - } - binary scan $bytes_commonpathsuffix_offset i cps_offset - if {$cps_offset > 0} { - set tail [string range $linkinfocontent $cps_offset end] - set stringterminator 0 - set i 0 - set commonpathsuffix "" - #TODO - while {!$stringterminator && $i < 100} { - set c [string index $tail $i] - if {$c eq "\x00"} { - set stringterminator 1 - } else { - append commonpathsuffix $c - } - incr i - } - } - - - return [dict create localbasepath $localbasepath commonpathsuffix $commonpathsuffix] - } - - proc contents_get_info {contents} { - - #todo - return something like the perl lnk-parse-1.0.pl script? - - #Link File: C:/repo/jn/tclmodules/tomlish/src/modules/test/#modpod-tomlish-0.1.0/suites/all/arrays_1.toml#roundtrip+roundtrip_files+arrays_1.toml.fauxlink.lnk - #Link Flags: HAS SHELLIDLIST | POINTS TO FILE/DIR | NO DESCRIPTION | HAS RELATIVE PATH STRING | HAS WORKING DIRECTORY | NO CMD LINE ARGS | NO CUSTOM ICON | - #File Attributes: ARCHIVE - #Create Time: Sun Jul 14 2024 10:41:34 - #Last Accessed time: Sat Sept 21 2024 02:46:10 - #Last Modified Time: Tue Sept 10 2024 17:16:07 - #Target Length: 479 - #Icon Index: 0 - #ShowWnd: 1 SW_NORMAL - #HotKey: 0 - #(App Path:) Remaining Path: repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-0.1.0\suites\roundtrip\roundtrip_files\arrays_1.toml - #Relative Path: ..\roundtrip\roundtrip_files\arrays_1.toml - #Working Dir: C:\repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-0.1.0\suites\roundtrip\roundtrip_files - - variable LinkFlags - set flags_enabled [list] - dict for {k v} $LinkFlags { - if {[Header_Has_LinkFlag $contents $k] > 0} { - lappend flags_enabled $k - } - } - - set showcommand_val [Header_Get_ShowCommand $contents] - switch -- $showcommand_val { - 1 { - set showwnd [list 1 SW_SHOWNORMAL] - } - 3 { - set showwnd [list 3 SW_SHOWMAXIMIZED] - } - 7 { - set showwnd [list 7 SW_SHOWMINNOACTIVE] - } - default { - set showwnd [list $showcommand_val SW_SHOWNORMAL-effective] - } - } - - set linkinfo_content_dict [Get_LinkInfo_content $contents] - set localbase_path "" - set suffix_path "" - set linkinfocontent [dict get $linkinfo_content_dict content] - set link_target "" - if {$linkinfocontent ne ""} { - set linkfields [LinkInfo_get_fields $linkinfocontent] - set localbase_path [dict get $linkfields localbasepath] - set suffix_path [dict get $linkfields commonpathsuffix] - if {"windows" eq $::tcl_platform(platform)} { - set link_target [file join $localbase_path $suffix_path] - } else { - set suffix_path [string trimleft [string map {\\ /} $suffix_path] /] - if {[regexp {([a-zA-Z]):\\(.*)} $localbase_path _match drive_letter tail]} { - set localbase_path [string map {\\ /} $localbase_path] - set tail [string trimleft [string map {\\ /} $tail] /] - set link_target "" - #shortcut basepath is a windows path with drive letter - try to resolve it on unix by looking for a corresponding mount from fstab or a point under /mnt - set mountinfo [exec mount] - foreach line [split $mountinfo "\n"] { - #review - a more specific mount target might exist that includes the drive letter as part of the mount point name and is a longer prefix of the localbase_path - #- we should probably look for the longest prefix match rather than just the drive letter - if {[regexp -nocase -- [string cat ^$drive_letter {:\\\s+on\s+(\S+)}] $line _match mount_point]} { - set link_target [file join $mount_point $tail $suffix_path] - break - } - } - if {$link_target eq ""} { - #review - under what circumstances could this happen? If the drive letter doesn't match any mount points, then /mnt/drive_letter should generally already have been found above above - # - However, it may be possible for /mnt/drive_Letter to still exist even if it's not reflected in the output of mount or the output of mount is in an unexpected format. - - #nothing in mount result matches the drive letter - try looking for a mount point under /mnt with the drive letter as the name - if {[file exists /mnt/$drive_letter]} { - set link_target [file join /mnt/$drive_letter $tail $suffix_path] - } else { - if {$drive_letter eq [string tolower $drive_letter]]} { - set op_drive_letter [string toupper $drive_letter] - } else { - set op_drive_letter [string tolower $drive_letter] - } - if {[file exists /mnt/$op_drive_letter]} { - set link_target [file join /mnt/$op_drive_letter $tail $suffix_path] - } else { - #leave as is except for backslashes converted to forward - #- probably won't resolve correctly unless the unix system has a folder named drive_letter: in the current folder with a copy of the original filestructure. - set link_target [file join $localbase_path $suffix_path] - } - } - } else { - #shortcut basepath is a windows path with drive letter and we found a matching mount point - link_target is set to the resolved path - } - } else { - #shortcut basepath doesn't match expected windows path format - just join it with the suffix and hope for the best - #could be something like a network path or it could be something else entirely - set link_target [file join $localbase_path $suffix_path] - } - } - } - - set result [dict create\ - link_target $link_target\ - link_flags $flags_enabled\ - file_attributes [Header_Get_FileAttributes $contents]\ - creation_time [Header_Get_CreationTime $contents]\ - access_time [Header_Get_AccessTime $contents]\ - write_time [Header_Get_WriteTime $contents]\ - target_length [Header_Get_FileSize $contents]\ - icon_index ""\ - showwnd "$showwnd"\ - hotkey [Header_Get_HotKey $contents]\ - relative_path "?"\ - ] - } - - proc file_check_header {path} { - #*** !doctools - #[call [fun file_check_header] [arg path] ] - #[para]Return 0|1 - #[para]Determines if the .lnk file specified in path has a valid header for a windows shortcut - set c [Get_contents $path 20] - return [Contents_check_header $c] - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::winlnk::resolve - @cmd -name punk::winlnk::resolve\ - -summary\ - "Return information about a .lnk file (windows shortcut)"\ - -help\ - "Return a dict of info obtained by parsing the binary data in a windows .lnk file. - If the .lnk header check fails, then the .lnk file probably isn't really a shortcut - file and the dictionary will contain an 'error' key." - @values -min 1 -max 1 - path -type string -help "Path to the .lnk file to resolve" - }] - } - proc resolve {path} { - #*** !doctools - #[call [fun resolve] [arg path] ] - #[para] Return a dict of info obtained by parsing the binary data in a windows .lnk file - #[para] If the .lnk header check fails, then the .lnk file probably isn't really a shortcut file and the dictionary will contain an 'error' key - set c [Get_contents $path] - if {[Contents_check_header $c]} { - return [contents_get_info $c] - } else { - return [dict create error "lnk_header_check_failed"] - } - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::winlnk::file_show_info - @cmd -name punk::winlnk::file_show_info\ - -summary\ - "Show information about a .lnk file (windows shortcut)"\ - -help\ - "Print to stdout the information obtained by parsing the binary data in a windows .lnk file, in a human readable format. - If the .lnk header check fails, then the .lnk file probably isn't really a shortcut file and an error message will be printed." - @values -min 1 -max 1 - path -type string -help "Path to the .lnk file to resolve" - }] - } - proc file_show_info {path} { - package require punk::lib - punk::lib::showdict [resolve $path] * - } - namespace eval argdoc { - variable PUNKARGS - lappend PUNKARGS [list { - @id -id ::punk::winlnk::target - @cmd -name punk::winlnk::target\ - -summary\ - "Return the target path of a .lnk file (windows shortcut)"\ - -help\ - "Return the target path of the .lnk file specified in path. - This is a convenience function that extracts the target path from the .lnk file and returns it directly, - without all the additional information that resolve provides. If the .lnk header check fails, then - the .lnk file probably isn't really a shortcut file and an error message will be returned." - @values -min 1 -max 1 - path -type string -help "Path to the .lnk file to resolve" - }] - } - proc target {path} { - #*** !doctools - #[call [fun target] [arg path] ] - #[para]Return the target path of the .lnk file specified in path - set info [resolve $path] - if {[dict exists $info error]} { - error [dict get $info error] - } else { - return [dict get $info link_target] - } - } - - #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 punk::winlnk ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::winlnk::lib { - tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - tcl::namespace::path [tcl::namespace::parent] - #*** !doctools - #[subsection {Namespace punk::winlnk::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 punk::winlnk::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -#tcl::namespace::eval punk::winlnk::system { - #*** !doctools - #[subsection {Namespace punk::winlnk::system}] - #[para] Internal functions that are not part of the API - - - -#} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -namespace eval ::punk::args::register { - #use fully qualified so 8.6 doesn't find existing var in global namespace - lappend ::punk::args::register::NAMESPACES ::punk::winlnk -} -## Ready -package provide punk::winlnk [tcl::namespace::eval punk::winlnk { - variable pkg punk::winlnk - variable version - set version 0.1.0 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm index e147b4d1..86823a45 100644 --- a/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm @@ -81,16 +81,18 @@ namespace eval punkcheck { } return $record_list } - proc save_records_to_file {recordlist punkcheck_file {trigger {}}} { - if {$trigger ne ""} { - puts stderr "\x1b\[36mSaving [llength $recordlist] records to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m" - } + proc save_records_to_file {recordlist punkcheck_file {trigger {}} {debugchannel ""}} { set newtdl [punk::tdl::prettyprint $recordlist] set linecount [llength [split $newtdl \n]] + + if {$debugchannel ne "" && $trigger ne ""} { + puts $debugchannel "\x1b\[36mSaving [llength $recordlist] records as $linecount lines to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m" + } #puts stdout $newtdl set fd [open $punkcheck_file w] chan configure $fd -translation binary puts -nonewline $fd $newtdl + flush $fd close $fd return [list recordcount [llength $recordlist] linecount $linecount] } @@ -119,14 +121,6 @@ namespace eval punkcheck { } method as_record {} { - #set fields [list\ - # -targets $o_targets\ - # -keep_installrecords $o_keep_installrecords\ - # -keep_skipped $o_keep_skipped\ - # -keep_inprogress $o_keep_inprogress\ - # body $o_records\ - #] - dict create {*}{ } tag FILEINFO {*}{ } -targets $o_targets {*}{ @@ -170,8 +164,10 @@ namespace eval punkcheck { variable o_path_cksum_cache variable o_fileset_record variable o_installer ;#parent object + variable o_debugchannel constructor {installer rel_sourceroot rel_targetroot args} { set o_installer $installer + set o_debugchannel [$installer get_debugchannel] set o_operation_start_ts "" set o_path_cksum_cache [dict create] set o_operation "" @@ -212,18 +208,6 @@ namespace eval punkcheck { } else { set tsiso_end "" } - #set fields [list\ - # -tsiso_begin $tsiso_begin\ - # -ts_begin $o_ts_begin\ - # -tsiso_end $tsiso_end\ - # -ts_end $o_ts_end\ - # -id $o_id\ - # -source $o_rel_sourceroot\ - # -targets $o_rel_targetroot\ - # -types $o_types\ - # -config $o_configdict\ - #] - #set record [dict create tag EVENT {*}$fields] dict create {*}{ } tag EVENT {*}{ @@ -321,7 +305,7 @@ namespace eval punkcheck { set extractioninfo [punkcheck::recordlist::extract_or_create_fileset_record $o_targets $record_list] set o_fileset_record [dict get $extractioninfo record] - set record_list [dict get $extractioninfo recordset] + set record_list [dict get $extractioninfo recordset] ;#if fileset wasn't present, same as original record_list, otherwise full recordset with the fileset record removed, ready for reinsertion. set isnew [dict get $extractioninfo isnew] set oldposition [dict get $extractioninfo oldposition] unset extractioninfo @@ -530,7 +514,9 @@ namespace eval punkcheck { variable o_record_list variable o_active_event variable o_events - constructor {installername punkcheck_file} { + variable o_debugchannel + constructor {installername punkcheck_file {debugchannel ""}} { + set o_debugchannel $debugchannel set o_active_event "" set o_name $installername @@ -539,6 +525,8 @@ namespace eval punkcheck { set o_targetroot "" set o_rel_sourceroot "" set o_rel_targetroot "" + set o_record_list [list] + #todo - validate punkcheck file location further?? set punkcheck_folder [file dirname $o_checkfile] if {![file isdirectory $punkcheck_folder]} { @@ -546,6 +534,55 @@ namespace eval punkcheck { } my load_all_records + if {![llength $o_record_list] && $o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[32mNo existing records found in punkcheck file '$o_checkfile' for installer '$installername'. Starting with empty record list.\x1b\[m" + } else { + #verify no duplicate installer records for this installer. + #JMN + set sanity_dict [dict create] + set insane "" + foreach rec $o_record_list { + if {[dict get $rec tag] eq "INSTALLER"} { + set name [dict get $rec -name] + if {[dict exists $sanity_dict $name]} { + #todo - warn - duplicate record for same targetlist - shouldn't happen as we should be using get_file_record to find existing records + if {$o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[31mpunkcheck installtrack - multiple INSTALLER records with same name '$name'\x1b\[m" + } + set insane "$name" + break + } + dict set sanity_dict $name {} + } + } + if {$insane ne ""} { + set msg "Sanity check: punkcheck file '$o_checkfile' contains multiple records for INSTALLER -name '$insane'." + append msg \n "This may indicate a problem such as multiple concurrent installtrack instances using the same punkcheck file," + append msg \n " or a previous installtrack instance that did not complete properly." + append msg \n " Do you want to DELETE the .punkcheck file?" + append msg \n " It is safe to delete .punkcheck files, at the cost of loss of history and checksums used to optimize installs." + append msg \n " They are a record of installation events and checksums used to avoid unnecessary reinstalls." + append msg \n " If not confirmed, an error will be raised - likely aborting the current operation." + append msg \n "confirm deletion and continue by regenerating the file, by typing the 3 letters: 'yes'." + set answer [punk::lib::askuser $msg] + if {[string tolower $answer] ne "yes"} { + error "Failing due to sanity check failure. User did not confirm with 'yes'." + } + if {[file exists $o_checkfile] && [file isfile $o_checkfile]} { + file delete $o_checkfile + } + if {[file exists $o_checkfile]} { + error "Failed to delete punkcheck file '$o_checkfile' after sanity check failure. Please investigate and resolve the issue before proceeding." + } + set o_record_list [list] + } else { + if {$o_debugchannel ne ""} { + puts $o_debugchannel "\x1b\[32mSanity check passed: no duplicate INSTALLER records found for installer '$installername' in punkcheck file '$o_checkfile'.\x1b\[m" + } + } + unset sanity_dict + } + set resultinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list] set existing_header_posn [dict get $resultinfo position] if {$existing_header_posn == -1} { @@ -580,6 +617,9 @@ namespace eval punkcheck { method get_checkfile {} { return $o_checkfile } + method get_debugchannel {} { + return $o_debugchannel + } #call set_source_target before calling start_event/end_event #each event can have different source->target pairs - but may often have same, so set on installtrack as defaults. Only persisted in event records. @@ -2164,10 +2204,10 @@ namespace eval punkcheck { return [dict create changed $changed unchanged $unchanged] } - #assume only one for name - use first encountered + #assume only one for name - use first encountered? proc get_installer_record {name record_list} { set posn 0 - set found_posn -1 + set found_posns [list] set record "" #puts ">>>> checking [llength $record_list] punkcheck records" foreach rec $record_list { @@ -2175,12 +2215,20 @@ namespace eval punkcheck { if {[dict get $rec -name] eq $name} { set found_posn $posn set record $rec - break + lappend found_posns $posn } } incr posn } - return [list position $found_posn record $record] + if {[llength $found_posns] > 1} { + error "punkcheck::recordlist::get_installer_record - multiple installer records with name '$name' found at positions $found_posns" + } elseif {[llength $found_posns] == 0} { + return [list position -1 record ""] + } else { + #single record found + return [list position [lindex $found_posn 0] record $record] + } + } proc new_installer_record {name args} { @@ -2374,7 +2422,8 @@ namespace eval punkcheck { set fileset_record [dict create tag FILEINFO -targets $relative_target_paths body {}] } else { #set recordset [lreplace $recordset[unset recordset] $existing_posn $existing_posn] - set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn] + #set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn] + ledit recordset $existing_posn $existing_posn set isnew 0 set fileset_record [dict get $fetch_record_result record] } diff --git a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm b/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm index 6d357bd7..42ae92cd 100644 --- a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm @@ -2728,13 +2728,13 @@ namespace eval shellfilter { ::shellfilter::log::write $runtag "checking for redirections in $commandlist" #sometimes we see a redirection without a following space e.g >C:/somewhere #normalize - switch -regexp -- $lastitem\ - {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - }\ - {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} diff --git a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.2.tm b/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.2.tm new file mode 100644 index 00000000..6a948593 --- /dev/null +++ b/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.2.tm @@ -0,0 +1,3651 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + variable is_enabled 0 + + proc disable {} { + variable is_enabled + set is_enabled 0 + proc ::shellfilter::log::open {tag settingsdict} {} + proc ::shellfilter::log::write {tag msg} {} + proc ::shellfilter::log::write_sync {tag msg} {} + proc ::shellfilter::log::close {tag} {} + } + + proc enable {} { + variable is_enabled + set is_enabled 1 + #'tag' is an identifier for the log source. + #(well.. really it's a common *target* of file and/or syslog host:port which can be written to from any thread that uses the tag) + #the terminology here is kinda ratshit. + + # each tag will use it's own thread to write to the configured log target + proc ::shellfilter::log::open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #review new_worker/assign_worker? + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc ::shellfilter::log::write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc ::shellfilter::log::write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc ::shellfilter::log::close {tag} { + #shellthread::manager::close_worker $tag + + #unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag. + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + #why not use string is boolean? + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } + if {[catch {package require shellthread}]} { + shellfilter::log::disable + } else { + shellfilter::log::enable + } + +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + #---------------------------------------------------------------------------------------- + # Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations: + # (not necessarily a comprehensive list) + # - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms + # tcl::chan::fifo2 is a pure Tcl implementation. + # - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2. + #---------------------------------------------------------------------------------------- + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + #review + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them? + #these are not upvared - must be fully qualified variable names. + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + if {"read" in $mode} { + #this should raise an error in 'chan push' + error "shellfilter::chan::tee_to_var transform does not support read mode" + } + return [list initialize finalize write flush] + } + method finalize {ch} { + my destroy + } + #method clear {ch} { + # return + #} + #method read {ch count} { + # return ? + #} + #method flush {ch} { + # return "" + #} + method flush {transform_handle} { + #puts stdout "" + #review - just clear o_encbuf and emit nothing? + #we wouldn't have a value there if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } + set o_encbuf "" + return "" + } + method write {ch bytes} { + #test with set x [string repeat " \U1f6c8" 2043] + #or + #test with set x [string repeat " \U1f6c8" 683] + #most windows terminals (at least) may emit two unrecognised chars "??" at the end + + #Our goal with the while loop here is to avoid encoding conversion errors + #the source of the bogus chars in terminals is unclear. + #Alacritty on windows doesn't seem to have the problem, but wezterm,cmd,windows terminal do. + + #set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + foreach v $o_datavars { + append $v $stringdata + } + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_encbuf + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + #return [list initialize read drain write flush clear finalize] + return [list initialize write flush clear finalize] + } + method finalize {transform_handle} { + #Note that an error in the finalize can stop 'chan pop' from running properly. + #::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + set o_encbuf "" + return + } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set logdata [tcl::encoding::convertfrom $o_enc $bytes] + # #::shellfilter::log::write $o_logsource $logdata + # puts -nonewline $o_localchan $logdata + # return $bytes + #} + #method flush {transform_handle} { + # #return "" + # set clear $o_encbuf[set o_encbuf ""] + # if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + # #if we can't convert the buffer contents to a string - put it back and try again with more data later + # #REVIEW? + # set o_encbuf $clear + # puts -nonewline $o_localchan "" + # return "" + # } + # #jjj + # puts -nonewline $o_localchan $stringdata + # flush $o_localchan + # return $clear + #} + method flush {transform_handle} { + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } + set o_encbuf "" + return "" + } + method write {transform_handle bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + if {$inputbytes eq ""} { + #review - do we even get empty writes? + puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write." + return "" + } + + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + #nothing convertable in the buffer - put it back and try again with more data later + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes + } + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $stringdata + return $converted ;#same data as $stringdata but in bytes form. + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write flush finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes + } + #set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] + ::shellfilter::log::write $o_logsource $stringdata + #return $bytes + return $converted ;#same data as $stringdata but in bytes form. + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + #see TIP 230 Tcl Channel Transformation Reflection API + + #the logonly transform does not emit any data downwards towards the base channel - it only writes to the log. + # - ie we are redirecting all data to the log. + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set o_encbuf "" + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + #mode is a list containing any of the strings 'read' or 'write'. + if {"read" in $mode} { + #Note that raising an error prevents creation of the transformation. + #The thrown error will appear as a error thrown by 'chan push'. + error "logonly transform does not support read mode" + } + #return all methods supported by the handler + #(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter) + return [list initialize finalize write flush] + } + method finalize {transform_handle} { + #review - tip 230 states that any return value or error raised by finalize is ignored + #we wrap in catch to ensure the 'my destroy' is always called. + catch {::shellfilter::log::close $o_logsource} + my destroy + } + #clear? + method flush {transform_handle} { + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log? + #REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string. + #This may be useful for debugging issues, but it may also result in garbage data in the log. + ::shellfilter::log::write $o_logsource $o_encbuf + set o_encbuf "" + } + return + } + method write {transform_handle bytes} { + #set logdata [encoding convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return + } + } + + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $stringdata + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [::tcl::dict::get $tf -encoding] + set o_encbuf "" + if {[::tcl::dict::exists $tf -junction]} { + set o_is_junction [::tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + #return [list initialize read write clear flush drain finalize] + + #REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work. + #this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel. + #---------------------- + #don't do this + #---------------------- + #if {"read" in $mode} { + # #Note that raising an error prevents creation of the transformation. + # #The thrown error will appear as a error thrown by 'chan push'. + # error "shellfilter::chan::ansistrip transform does not support read mode" + #} + #---------------------- + return [list initialize read write flush finalize] + } + method finalize {transform_handle} { + my destroy + } + #method clear {transform_handle} { + # return + #} + #method drain {transform_handle} { + # return "" + #} + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + #method flush {transform_handle} { + # return "" + #} + method flush {transform_handle} { + #return "" + set clear $o_encbuf[set o_encbuf ""] + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - put it back and try again with more data later + #REVIEW? + set o_encbuf $clear + return "" + } + #review + return $stringdata + } + #method write {transform_handle bytes} { + # #broken due to occasional unexpected byte sequence + # set instring [encoding convertfrom $o_enc $bytes] + # set outstring [punk::ansi::ansistrip $instring] + # return [encoding convertto $o_enc $outstring] + #} + method write {transform_handle bytes} { + #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally + #bytes can break at arbitrary points making encoding conversions invalid. + + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [::tcl::string::length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [::tcl::string::range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + set outstring [punk::ansi::ansistrip $stringdata] + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + set sixelstart_re {\x1bP([;0-9]*)q} ;#7-bit - todo 8bit + #todo kitty graphics \x1b_G... + #todo iterm graphics + + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_encbuf ;#buffering for partial encoding bytes + variable o_colour + variable o_do_colour + variable o_do_colourlist + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered ;#buffering for partial ansi codes + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + #warning - we can't merge certain extended attributes such as undercurly into single SGR escape sequence + #while some terminals may handle these extended attributes even when merged - we need to cater for those that + #don't. Keeping them as a separate escape allows terminals that don't handle them to ignore just that code without + #affecting the interpretation of the other codes. + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_colourlist [punk::ansi::ta::get_codes_single $o_do_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_colourlist {} + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_encbuf "" + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + + + #todo - track when in sixel,iterm,kitty graphics data - can be very large + method Trackcodes {chunk} { + #note - caller can use 2 resets in a single unit to temporarily reset to no sgr (override ansiwrap filter) + #e.g [a+ reset reset] (0;0m vs 0;m) + + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + #Note 8-bit csi \x9b has already been mapped in the chunk to 7-bit form \x1b\[ by the caller - so we only need to check for \x1b here + #(under review - ideally we might not want to normalize 8-bit to 7-bit in a channel transform)) + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + #REVIEW - ta::detect won't detect SOS without paired ST for things like PM + # ta::detectcode will - but then split_codes_single will treat unpaired SOS as text? + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # append emit $o_do_colour$pt$o_do_normal + # #append emit $pt + #} else { + # append emit $pt + #} + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list {*}{ + \x1b\[ 7CSI + \x9b 8CSI + \x1b\( 7GFX + }] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + set code_endswith_m [expr {[tcl::string::index $code end] eq "m"}] + if {$code_endswith_m} { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + #review this normalizing of reset to a single form. + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } + } + + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$trailing_pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$trailing_pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$trailing_pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + #if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + # append emit $o_do_colour$trailing_pt$o_do_normal + #} else { + # append emit $trailing_pt + #} + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #REVIEW - this holding a buffer without emitting as we go is ugly. + # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. + # - we'd then need to detect the appropriate close to restart splitting and codestacking + # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. + + + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string index $buf end] eq "\x1b" && [string first \x1b $buf] == [string length $buf]-1} { + #string index in first part of && clause to avoid some unneeded scans of whole string for this test + #we can't use 'string last' - as we need to know only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit $o_do_colour[string range $buf 0 end-1]$o_do_normal + #set emit [string range $buf 0 end-1] + set buf "" + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + set buf "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } else { + set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + set o_buffered "" + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } + } + } + if {$emit_anyway} { + #assert: any time emit_anyway == 1 buf already contains all of previous o_buffered and o_buffered has been cleared. + + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # set emit $o_do_colour$buf$o_do_normal + #} else { + # set emit $buf + #} + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + #return [list initialize write flush read drain finalize] + if {$mode eq "read"} { + error "shellfilter::chan::ansiwrap channel transform does not support read mode" + } + return [list initialize write flush finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear out of the initialize call for now + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + #method flush {transform_handle} { + # #puts stdout "" + # set inputbytes $o_buffered$o_encbuf + # set emit [tcl::encoding::convertto $o_enc $inputbytes] + # set o_buffered "" + # set o_encbuf "" + # return $emit + #} + method flush {transform_handle} { + #return "" + set clear $o_buffered$o_encbuf + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes? + # - probably not. + #REVIEW? + return "" + } + set o_buffered "" + set o_encbuf "" + return $stringdata + } + method write {transform_handle bytes} { + #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally + #bytes can break at arbitrary points making encoding conversions invalid. + + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + #------------------------------------------------------ + # REVIEW + #Trackcodes logic is primarily designed for 7-bit codes + #It would be complex for it to support 8-bit as well + #- we can do a simple pre-map to convert 8-bit CSI to 7-bit CSI before processing + #we already normalize things like resets to a single 7-bit form anyway. + #review - is there a need for an ansiwrap channel that preserves 8-bit codes? + #8-bit are rarely used these days - and many terminals don't support them. + #We could take the view here that we should understand them but not emit them in general. + #Nonetheless - converting them on a channel transform like this is potentially suprising in some circumstances, + #and we don't necessarily know the intent of both the producer and consumer of the stream. + set stringdata [string map [list \x9b \x1b\[ ] $stringdata] + #------------------------------------------------------ + + set streaminfo [my Trackcodes $stringdata] + set emit [dict get $streaminfo emit] + + #review - wrapping already done in Trackcodes + #if {[dict get $streaminfo stacksize] == 0} { + # #no ansi on the stack - we can wrap + # #review + # set outstring "$o_do_colour$emit$o_do_normal" + #} else { + #} + #if {[llength $o_codestack]} { + # set outstring [punk::ansi::codetype::sgr_merge_singles $o_codestack]$emit + #} else { + # set outstring $emit + #} + #set outstring $emit + + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $emit] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set instring [tcl::encoding::convertfrom $o_enc $bytes] + # set outstring "$o_do_colour$instring$o_do_normal" + # return [tcl::encoding::convertto $o_enc $outstring] + #} + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + namespace export {[a-z]*} + namespace ensemble create + #todo - implement as oo ? + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + package require textblock + set pipecount [dict size $pipelines] + set tabletitle "$pipecount pipelines active" + set t [textblock::class::table new $tabletitle] + $t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts. + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[catch {chan configure $lc -encoding} lc_enc]} { + set lc_enc "" + } + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -frametype ascii + $tbl_inner configure -show_edge 0 + $tbl_inner add_column -headers id + $tbl_inner add_column -headers transform + $tbl_inner add_column -headers handle + $tbl_inner add_column -headers settings + $tbl_inner add_column -headers aside + foreach rec $stack { + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set handle [punk::lib::dict_getdef $rec -handle ""] + if {$handle ne ""} { + if {[catch {chan configure $handle -encoding} handle_enc]} { + set handle_enc "" + } + } else { + set handle_enc "" + } + set settings [punk::lib::dict_getdef $rec -settings ""] + set aside [punk::lib::dict_getdef $rec -aside ""] + if {$aside ne ""} { + set aside [punk::lib::showdict $aside] + } + $tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + if {[catch {chan eof $localchan} _eof]} { + #We don't actually care about eof state - but we use this to test if the channel exists. + #('chan names' doesn't reliably show all channels in some cases) + #do nothing. + } else { + #if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel + # but here we should only be calling it when there are transforms on the channel as indicated by the stack variable. + chan pop $localchan + } + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + #::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"] + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' + #- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed. + catch {chan close $localchan} + #if {[catch {chan close $localchan} errMsg]} { + # ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg" + #} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + #::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"] + variable pipelines + if {![dict exists $pipelines $pipename]} { + #puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + ::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set previous_blockingstate [chan configure $localchan -blocking] + if {$previous_blockingstate} { + chan configure $localchan -blocking 0 + } + + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + #::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'" + } else { + if {$idposn < 0} { + #::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + #puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + #set stack [lreplace $stack[set stack {}] $idposn end] + # 2026-05-19 + ledit stack $idposn end + + + #pop all chans before adding anything back in! + foreach p $poplist { + #review + #update idletasks + #puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]" + #::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---" + #after 0 [list chan pop $localchan] + + chan pop $localchan + #::shellfilter::log::write shellfilter-remove "POPPED" + #update idletasks + } + #after 5 + #::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" + #puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + #puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" + } + #after 5 + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + #update idletasks + #puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" + } + #after 5 + dict set pipelines $pipename stack $stack + } + + #puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE" + #::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE" + + if {$previous_blockingstate} { + chan configure $localchan -blocking 1 + } + #JMNJMN 2025 review! + #show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + #set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] + # 2026-05-19 + ledit stack $bottom_pop_posn end + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + # add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + + #JMNJMN + #show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + if {[catch { + ::shellfilter::log::open $tag {-syslog ""} + } err]} { + #e.g safebase interp can't load required modules such as shellthread (or Thread) + puts stderr "shellfilter::show_pipeline cannot open log" + return + } + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + #------------------------------------------------------------------------------------------------- + #tcllib logger infrastructure. + #------------------------------------------------------------------------------------------------- + namespace eval ::shellfilter::loggerprocs { + #container for procs/aliases to be pointed to by tcllib logger using log::logproc + + proc Dolog {lvl txt} { + #logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted + set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'" + puts stderr $msg + } + proc Runlog {lvl script} { + uplevel 1 $script + } + } + if {![catch { + package require logger + }]} { + logger::initNamespace ::shellfilter + foreach lvl [logger::levels] { + interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl + log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl + } + logger::setlevel warn + #namespace path ::shellfilter::log + } else { + #e.g tcllib not available, safe interp? + #fake out the logger calls + namespace eval ::shellfilter::log { + foreach lvl {debug info notice warn error critical alert emergency} { + proc $lvl {args} {} + } + } + } + #------------------------------------------------------------------------------------------------- + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create {*}{ + -tag redirected_$chan + -syslog "" + -file "" + }] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list {*}{ + \r cr + \n lf + {"} doublequote + {'} singlequote + "`" backquote + "^" caret + \t tab + " " sp + "\[" lsquare + "\]" rsquare + "(" lbracket + ")" rbracket + "\{" lbrace + "\}" rbrace + \\ backslash + / forwardslash + }] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list {*}{ + } "\"" [list "\"" "\"" "\""] {*}{ + } {^} [list "\"" "\"" "^"] {*}{ + } "'" [list "'" "'" "'"] {*}{ + } "\{" [list "\{" "\}" "\}"] {*}{ + } {[} [list {[} {]} {]}] {*}{ + } + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create {*}{ + -teehandle command + -outchan stdout + -errchan stderr + -inchan stdin + -tclscript 0 + -syslog "" + }] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + set syslog [dict get $opts -syslog] + dict unset opts -syslog + set runtag "shellfilter-run" + set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + #set tid [::shellfilter::log::open $runtag [list -syslog $syslog]] + log::info {::shellfilter::log::write $runtag " opts: $opts"} + log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"} + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #--------------------- + # #TEST + # chan configure $outpipechan -blocking 0 + # chan configure $errpipechan -blocking 0 + log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"} + log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"} + #--------------------- + + + log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"} + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + log::critical { + ::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]" + } + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if {$experiment} { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"} + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"} + } + #puts "shellfilter::run finished call" + + #------------------------- + #warning - without flush stdout - we can get hang, but only on some terminals + # - mechanism for this problem not understood! + #todo - test/document. + flush stdout + flush stderr + #update idletasks + #------------------------- + + + #---------------------------------------------------------------------------------------------- + # wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug. + # Logger allows the contents to be evaluated only if logging is switched on. + #---------------------------------------------------------------------------------------------- + #todo - change to log::debug + log::critical { + if {![catch {package require punk::ansi}]} { + set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]] + } else { + set stackstatus [shellfilter::stack status] + } + ::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]" + } + #---------------------------------------------------------------------------------------------- + + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"} + shellfilter::stack::remove $outchan $id_out + log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"} + shellfilter::stack::remove $errchan $id_err + + + log::info {::shellfilter::log::write $runtag " return '$exitinfo'"} + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + #unsubscribe from source tag s. + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list {*}{ + -timeout + -outprefix + -errprefix + -debug + -copytempfile + -outbuffering + -errbuffering + -inbuffering + -readprocesstranslation + -outtranslation + -stdinhandler + -outchan + -errchan + -inchan + -teehandle + }] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {[llength $args] % 2} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create {*}{ + -outchan stdout + -errchan stderr + -inchan stdin + -outbuffering none + -errbuffering none + -readprocesstranslation auto + -outtranslation lf + -inbuffering none + -timeout 900000 + -outprefix "" + -errprefix "" + -debug 0 + -copytempfile 0 + -stdinhandler "" + }] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set ::shellfilter::shellcommandvars($call_id,timeoutid) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + + #--------------- + #JMN 2025 + # e.g cannot run ansible cmdline tools if non-blocking + #chan configure $wrerr -blocking 0 + #------------------ + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem {*}{ + } {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + } {*}{ + } {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list {*}{ + } [chan configure $inchan -buffering] {*}{ + } [chan configure $outchan -buffering] {*}{ + } [chan configure $errchan -buffering] {*}{ + } + ] + + set remember_in_out_err_translation [list {*}{ + } [chan configure $inchan -translation] {*}{ + } [chan configure $outchan -translation] {*}{ + } [chan configure $errchan -translation] {*}{ + } + ] + + + + + #chan configure $inchan -buffering none -blocking 1 ;#test + #chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $inchan -buffering $inbuffering + + + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + #REVIEW! + #if the child process takes a while to begin reading stdin - the data on stdin between when we stopped the parent chan event handler and when the child gets data, + #seems to stay buffered somewhere. It is then read by the parent, after the child returns. (ie not lost, but out-of-order) + #This can be apparent sometimes even with fast typing upon calling an executable. (e.g occasionally even vim - but seems to be timing based so might only happen first time if at all) + # see scriptlib/stdin_race.tcl etc test files. + #similar problem with python & perl - issue seems to be in libc or OS buffering behaviour for standard channels. + #note that zig (repo/jn/zig/stdin_race) seems to avoid this issue - todo - make zig based binary extension for open/exec? + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + #if {$other ni [chan names]} { + # set $waitfor stderr + #} + if {[catch {chan configure $other}]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + #if {$other ni [chan names]} { + # set $waitfor stdout + #} + if {[catch {chan configure $other}]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + #jmn test 2025 + flush $outchan + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + #if {$other ni [chan names]} { + # set $waitfor stdout + #} + if {[catch {chan configure $other}]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + #review - stdin??? + set ::shellfilter::shellcommandvars($call_id,timeoutid) [after $timeout [string map [list %cpids% $command_pids %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + #killing the task (on windows) doesn't seem to work if done after we close the output channels + catch {puts stderr "timeout - closing.";flush stderr} + set command_pids "{%cpids%}" + if {[llength $command_pids]} { + set pid [lindex $command_pids 0] + if {$::tcl_platform(platform) eq "windows"} { + set killcmd [list [auto_execok taskkill] /F /PID $pid] + } else { + #set killcmd [list kill -9 $pid] + set killcmd [list kill -TERM $pid] + } + if {[catch { + exec {*}$killcmd + } errM]} { + puts stderr "Failed to kill '$pid': errMsg $errM" + flush stderr + } + } + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }]] + + + vwait $waitvar + after cancel $::shellfilter::shellcommandvars($call_id,timeoutid) + + #puts stderr "waitvar:[set $waitvar]" + #flush stderr + #if {[set $waitvar] eq "timeout"} { + # #note: attempting to kill a process here (after channels closed) doesn't work (on windows at least) + # puts stderr "command_pids: $command_pids" + # flush stderr + #} + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode closed_by:[set $waitvar]" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.2.2 +}] diff --git a/src/vfs/_vfscommon.vfs/modules/shellthread-1.6.2.tm b/src/vfs/_vfscommon.vfs/modules/shellthread-1.6.2.tm index 6338d1c6..79d12149 100644 --- a/src/vfs/_vfscommon.vfs/modules/shellthread-1.6.2.tm +++ b/src/vfs/_vfscommon.vfs/modules/shellthread-1.6.2.tm @@ -119,29 +119,58 @@ namespace eval shellthread::worker { set waitvar ::shellthread::worker::wait($inpipe,[clock micros]) #tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan - chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] - vwait $waitvar - } - proc pipe_read {chan source waitfor readbuffering writebuffering} { + #chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering - } else { - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunksize [chan gets $chan chunk] + if {$chunksize >= 0} { + if {![chan eof $chan]} { + ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + } else { + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + } } - } + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] + } else { - set chunk [chan read $chan] - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $chan + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunk [chan read $chan] + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] } + + vwait $waitvar } + #proc pipe_read {chan source waitfor readbuffering writebuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + # } else { + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # } + # } else { + # set chunk [chan read $chan] + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $chan + # } + #} proc start_pipe_write {source writechan args} { variable outpipe @@ -181,8 +210,8 @@ namespace eval shellthread::worker { chan configure $writechan -blocking 0 set waitvar ::shellthread::worker::wait($outpipe,[clock micros]) - chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { - if {$readbuffering eq "line"} { + if {$readbuffering eq "line"} { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunksize [chan gets $chan chunk] if {$chunksize >= 0} { if {![chan eof $chan]} { @@ -191,19 +220,55 @@ namespace eval shellthread::worker { puts -nonewline $writechan $chunk } } - } else { + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + flush $writechan ;#2026-05-19 - ensure all data is sent before closing + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } + } + }} $readchan $writechan $source $waitvar] + } else { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunk [chan read $chan] puts -nonewline $writechan $chunk - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $writechan - if {$chan ne "stdin"} { - chan close $chan + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } } - } - }} $readchan $writechan $source $waitvar $readbuffering] + }} $readchan $writechan $source $waitvar] + + } + + # chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # puts $writechan $chunk + # } else { + # puts -nonewline $writechan $chunk + # } + # } + # } else { + # set chunk [chan read $chan] + # puts -nonewline $writechan $chunk + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $writechan + # if {$chan ne "stdin"} { + # chan close $chan + # } + # } + # }} $readchan $writechan $source $waitvar $readbuffering] vwait $waitvar } @@ -479,6 +544,10 @@ namespace eval shellthread::manager { set sourcetag [lindex $sourcetaglist 0] ;#todo - use all set defaults [dict create {*}{ + -raw 0 + -file {} + -syslog {} + -direction out -workertype message }] set settingsdict [dict merge $defaults $settingsdict] @@ -501,6 +570,10 @@ namespace eval shellthread::manager { return [dict get $winfo tid] } elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} { #review - magic dict seems brittle - shouldn't hard code here.??? + + #existing worker has default settings - so we'll assume it's a placeholder and update it with our settings + #review - where/when do we override the default settings? + dict lappend winfo list_client_tids $tidclient dict set workers $sourcetag $winfo ;#writeback return [dict get $winfo tid] @@ -575,11 +648,11 @@ namespace eval shellthread::manager { package require Thread package require shellthread if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} { - unset ::settingsinfo - set ::shellthread_init "ok" + unset ::settingsinfo + set ::shellthread_init "ok" } else { - unset ::settingsinfo - set ::shellthread_init "err $errmsg" + unset ::settingsinfo + set ::shellthread_init "err $errmsg" } }] @@ -622,15 +695,23 @@ namespace eval shellthread::manager { proc write_log {source msg args} { variable workers - set ts_micros_sent [clock micros] - set defaults [list -async 1 -level info] - set opts [dict merge $defaults $args] - - if {[dict exists $workers $source]} { + if {[dict exists $workers $source tid]} { set tidworker [dict get $workers $source tid] if {$tidworker eq "noop"} { return } + } else { + set tidworker "" + } + + set ts_micros_sent [clock micros] + set defaults [list {*}{ + -async 1 + -level info + }] + set opts [dict merge $defaults $args] + + if {$tidworker ne ""} { if {![thread::exists $tidworker]} { # -syslog -file ? set tidworker [new_worker $source] @@ -674,10 +755,7 @@ namespace eval shellthread::manager { if {[dict exists $workers $source]} { set list_client_tids [dict get $workers $source list_client_tids] if {[set posn [lsearch $list_client_tids $mytid]] >= 0} { - #set list_client_tids [lreplace $list_client_tids $posn $posn] - #set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn] ledit list_client_tids $posn $posn - dict set workers $source list_client_tids $list_client_tids } if {![llength $list_client_tids]} { @@ -685,7 +763,6 @@ namespace eval shellthread::manager { } } } - #we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all. set subscriberless_workers [list] @@ -696,8 +773,8 @@ namespace eval shellthread::manager { set subscriber_count 0 set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed foreach taginfo $worker_tags { - incr subscriber_count [llength [dict get $taginfo list_client_tids]] - incr kill_count [llength [dict get $taginfo ts_end_list]] + incr subscriber_count [llength [dict get $taginfo list_client_tids]] + incr kill_count [llength [dict get $taginfo ts_end_list]] } if {$subscriber_count == 0} { lappend subscriberless_workers $workertid @@ -760,7 +837,7 @@ namespace eval shellthread::manager { set ::shellthread::waitfor waiting #after $timeout [list set ::shellthread::waitfor] #2025-07 timed-out untested review - set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]] + set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}] set waiting_for [list] set ended [list] @@ -769,7 +846,9 @@ namespace eval shellthread::manager { if {[thread::exists $tid]} { lappend waiting_for $tid #thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads) - thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor + set tid_client [thread::id] + #shellthread::worker::terminate will return thread id of terminating thread (or empty string) + thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor } } if {[llength $waiting_for]} { @@ -779,13 +858,13 @@ namespace eval shellthread::manager { set timedout 1 break } else { - after cancel $cancelid + after cancel $timeout_timer lappend ended $::shellthread::waitfor } } } set free_threads [list] - return [dict create existed $waiting_for ended $ended timedout $timedout] + return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]] } #TODO - important. diff --git a/src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm b/src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm new file mode 100644 index 00000000..2d815e67 --- /dev/null +++ b/src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm @@ -0,0 +1,873 @@ +#Do not update version of this file. Update sqids-buildversion.txt and run make.tcl to update the version in this file and copy to modules folder. +package require Tcl 8.6- + +#MIT license +#Julian Noble 2026 + +#example: +# % package require sqids +# % set s1 [sqids::idscope new] +# ::oo::Obj275 +# % $s1 encode {1 2 3} +# 86Rf07 +# % $s1 decode 86Rf07 +# 1 2 3 + + +namespace eval sqids { + oo::class create idscope { + variable o_alphabet + variable o_alphabet_configured + variable o_alpha_re + variable o_minlength + variable o_blocklist + #note that methods beginning with uppercase letters are private. + constructor {args} { + set defaults [dict create {*}{ + -alphabet "" + -minlength "" + -blocklist "" + }] + if {[llength $args] %2 !=0} { + error "sqids::idscope constructor: Require option value pairs. Known options:[dict keys $defaults]." + } + set useropts [dict create] + set explicit_empty_blocklist 0 ;#as opposed to default due to being unspecified. + dict for {k v} $args { + set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist} $k] + switch -exact -- $fullmatch { + -alphabet - -minlength { + dict set useropts $fullmatch $v + } + -blocklist { + if {[llength $v] == 0} { + set explicit_empty_blocklist 1 + } + dict set useropts -blocklist $v + } + default { + error "sqids::idscope constructor: unknown option '$k'. Known options:[dict keys $defaults]." + } + } + } + set opts [dict merge $defaults $useropts] + + set opt_alphabet [dict get $opts -alphabet] + if {$opt_alphabet eq ""} { + set o_alphabet $::sqids::data::default_alphabet + } else { + if {[string length $opt_alphabet] < 3} { + error "sqids::idscope constructor: -alphabet length must be at least 3." + } + #review - deny multibyte + if {[regexp {[^\u00-\u7F]} $opt_alphabet]} { + error "sqids::idscope constructor: -alphabet must not contain multibyte characters." + } + if {[regexp {(.).*\1} $opt_alphabet]} { + error "sqids::idscope constructor: -alphabet must contain unique characters." + } + set o_alphabet $opt_alphabet + } + set o_alphabet_configured $o_alphabet ;#for use in public method alphabet, which returns the configured alphabet in the order it was configured, not the shuffled order used for encoding. + set alphamatch [string map [list . \\. \[ \\\[ \] \\\] \{ \\\{ \} \\\}] $o_alphabet] ;#review + set o_alpha_re "^\[$alphamatch\]+\$" ;#independent of shuffled order. + + set o_alphabet [my shuffle $o_alphabet[set o_alphabet {}]] + + set opt_minlength [dict get $opts -minlength] + if {$opt_minlength eq ""} { + set o_minlength $::sqids::data::default_minlength + } else { + set maxval 255 + if {![string is integer -strict $opt_minlength] || $opt_minlength < 0 || $opt_minlength > $maxval} { + error "sqids constructor: -minlength must be an integer from 0 to $maxval inclusive." + } + set o_minlength $opt_minlength + } + set opt_blocklist [dict get $opts -blocklist] + if {!$explicit_empty_blocklist && $opt_blocklist eq ""} { + set o_blocklist $::sqids::data::default_blocklist + } else { + set o_blocklist $opt_blocklist + } + set o_blocklist [string tolower $o_blocklist] + + } + method config {{option {}}} { + #introspection method. + #return a dict of the configured options if no option specified, otherwise return the value of the specified option. + + #no facility is provided to change options after construction as a new idscope object should be used for different configurations (different scope of sqid ids). + #note that -alphabet refers to the configured alphabet in the order it was configured, not the shuffled order used for encoding. + if {$option eq ""} { + # + return [dict create {*}{ + } -blocklist $o_blocklist {*}{ + } -minlength $o_minlength {*}{ + } -alphabet $o_alphabet_configured {*}{ + } + ] + } + set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist} $option] + switch -exact -- $fullmatch { + -alphabet {return $o_alphabet_configured} + -minlength {return $o_minlength} + -blocklist {return $o_blocklist} + default { + error "sqids::idscope config: unknown option '$option'. Known options:-alphabet -minlength -blocklist." + } + } + } + method encode {numlist} { + if {[llength $numlist] == 0} {return} + #cannot encode negative numbers, or non-integers. + foreach num $numlist { + #tcl 9 'string is integer' supports bignums - which can be arbitrarily large. + #tcl 8.6 'string is integer' is limited to 2**32-1 + if {![string is integer -strict $num] || $num < 0} { + error "sqids encode: can only encode non-negative integers. Invalid value: '$num'" + } + } + return [my EncodeNumbers $numlist] + } + method EncodeNumbers {numlist {increment 0}} { + #assert number of letters in o_alphabet and number of letters in local alpha are the same and don't effectively change during this function. + #('set alpha {}' in calls to my shuffle is an optimization to avoid shared string and copy-on-write overhead. As alpha is set to the result, it doesn't violate the previous assertion.) + set alpha_len [string length $o_alphabet] + + if {$increment > $alpha_len} { + error "sqids EncodeNumbers: Reached max attempts to re-generate the ID" + } + + set offset [llength $numlist] + set i -1 + foreach v $numlist { + incr i + set x [scan [string index $o_alphabet [expr {$v % $alpha_len}]] %c] + set offset [expr {$offset + $x + $i}] + } + set offset [expr {$offset % $alpha_len}] + set offset [expr {($offset + $increment) % $alpha_len}] + set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] + set prefix [string index $alpha 0] + set alpha [string reverse $alpha] + set id $prefix + set i -1 + foreach num $numlist { + incr i + append id [my ToId $num [string range $alpha 1 end]] + if {$i < [llength $numlist]-1} { + append id [string index $alpha 0] + set alpha [my shuffle $alpha[set alpha {}]] + } + } + if {$o_minlength > [string length $id]} { + append id [string index $alpha 0] + while {$o_minlength - [string length $id] > 0} { + set alpha [my shuffle $alpha[set alpha {}]] + set numchars [expr {min($o_minlength - [string length $id],$alpha_len)}] + append id [string range $alpha 0 $numchars-1] + } + } + if {[my is_blocked $id]} { + set id [my EncodeNumbers $numlist [expr {$increment+1}]] + } + return $id + } + method is_blocked {id} { + #deliberately public method. + if {![llength $o_blocklist]} { + return 0 + } + #o_blocklist is stored in lowercase, so compare against lowercase id. + set idtest [string tolower $id] + set idlen [string length $idtest] + if {$idlen < 3} { + #sqids rule: short ids less than 3 chars will not be blocked. + return 0 + } + if {$idlen == 3} { + if {$idtest in $o_blocklist} { + return 1 + } + } else { + foreach blocked $o_blocklist { + if {[string length $blocked] <= 3} { + #sqids rule: blocklist entries of 3 chars will only be blocked if they match the entire id exactly, + #so skip them in this loop as we've already checked for exact matches of the whole id when idlen == 3. + continue + } + set posn [string first $blocked $idtest] + if {$posn == -1} { + continue + } + if {$posn == 0} { + #whether leetspeak or not, blocklist entries that match at the beginning of the id will be blocked. + return 1 + } + if {[regexp {[0-9]} $blocked]} { + #sqids rule: blocklist entries with digits (leetspeak) will only be blocked if the match is at the beginning or end of the id. + #we've already checked the beginning, so check the end now. + set endpos [expr {$idlen - [string length $blocked]}] + if {$posn == $endpos} { + return 1 + } + } else { + #sqids rule: blocklist entries without digits will be blocked if they match anywhere in the id. + return 1 + } + } + } + return 0 + } + method ToId {num alpha} { + set id "" + set alpha_len [string length $alpha] + while 1 { + set id [string index $alpha [expr {$num % $alpha_len}]]$id + set num [expr {$num / $alpha_len}] + if {$num == 0} break + } + return $id + } + method ToNumber {id alpha} { + set number 0 + set alpha_len [string length $alpha] + for {set i 0} {$i < [string length $id]} {incr i} { + set posn [string first [string index $id $i] $alpha] + set number [expr {($number * $alpha_len) + $posn}] + } + return $number + } + method shuffle {alpha} { + #public method. Primarily for internal use but can be used externally to examine the shuffled alphabet being used for encoding. + #e.g myscopeobject shuffle [myscopeobject config -alphabet] would show the shuffled alphabet being used for encoding. + #consistent shuffle (always produce the same result for same input) + set alpha_len [string length $alpha] + if {$alpha_len < 2} { + return $alpha + } + set chars [split $alpha ""] + + for {set i 0; set j [expr {$alpha_len-1}]} {$j > 0} {incr i; incr j -1} { + set iv [scan [lindex $chars $i] %c] + set jv [scan [lindex $chars $j] %c] + set r [expr {($i * $j + $iv + $jv) % $alpha_len}] + set item2 [lindex $chars $r] + lset chars $r [lindex $chars $i] + lset chars $i $item2 + } + return [join $chars ""] + } + method decode {id} { + if {$id eq ""} {return} + set result [list] + + if {![regexp $o_alpha_re $id]} { + puts stderr "sqids decode: ID contains characters not in the alphabet. re: $o_alpha_re id: $id" + return [list] + } + set prefix [string index $id 0] + set offset [string first $prefix $o_alphabet] + set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] + set alpha [string reverse $alpha] + set id [string range $id 1 end] + while {[string length $id] > 0} { + set separator [string index $alpha 0] + #split on first occurrence of separator only. + set sep_posn [string first $separator $id] + if {$sep_posn == -1} { + set parts [list $id] + } else { + set parts [list [string range $id 0 $sep_posn-1] [string range $id $sep_posn+1 end]] + } + #assert parts has 1 or 2 elements + + if {[lindex $parts 0] eq ""} { + #separator was at start of the id - done. + return $result + } + lappend result [my ToNumber [lindex $parts 0] [string range $alpha 1 end]] + if {[llength $parts] == 2} { + set alpha [my shuffle $alpha[set alpha {}]] + set id [lindex $parts 1] + } else { + set id "" + } + } + return $result + } + } +} +namespace eval sqids::data { + variable default_alphabet {abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789} + variable default_minlength 0 + variable default_blocklist { + 0rgasm + 1d10t + 1d1ot + 1di0t + 1diot + 1eccacu10 + 1eccacu1o + 1eccacul0 + 1eccaculo + 1mbec11e + 1mbec1le + 1mbeci1e + 1mbecile + a11upat0 + a11upato + a1lupat0 + a1lupato + aand + ah01e + ah0le + aho1e + ahole + al1upat0 + al1upato + allupat0 + allupato + ana1 + ana1e + anal + anale + anus + arrapat0 + arrapato + arsch + arse + ass + b00b + b00be + b01ata + b0ceta + b0iata + b0ob + b0obe + b0sta + b1tch + b1te + b1tte + ba1atkar + balatkar + bastard0 + bastardo + batt0na + battona + bitch + bite + bitte + bo0b + bo0be + bo1ata + boceta + boiata + boob + boobe + bosta + bran1age + bran1er + bran1ette + bran1eur + bran1euse + branlage + branler + branlette + branleur + branleuse + c0ck + c0g110ne + c0g11one + c0g1i0ne + c0g1ione + c0gl10ne + c0gl1one + c0gli0ne + c0glione + c0na + c0nnard + c0nnasse + c0nne + c0u111es + c0u11les + c0u1l1es + c0u1lles + c0ui11es + c0ui1les + c0uil1es + c0uilles + c11t + c11t0 + c11to + c1it + c1it0 + c1ito + cabr0n + cabra0 + cabrao + cabron + caca + cacca + cacete + cagante + cagar + cagare + cagna + cara1h0 + cara1ho + caracu10 + caracu1o + caracul0 + caraculo + caralh0 + caralho + cazz0 + cazz1mma + cazzata + cazzimma + cazzo + ch00t1a + ch00t1ya + ch00tia + ch00tiya + ch0d + ch0ot1a + ch0ot1ya + ch0otia + ch0otiya + ch1asse + ch1avata + ch1er + ch1ng0 + ch1ngadaz0s + ch1ngadazos + ch1ngader1ta + ch1ngaderita + ch1ngar + ch1ngo + ch1ngues + ch1nk + chatte + chiasse + chiavata + chier + ching0 + chingadaz0s + chingadazos + chingader1ta + chingaderita + chingar + chingo + chingues + chink + cho0t1a + cho0t1ya + cho0tia + cho0tiya + chod + choot1a + choot1ya + chootia + chootiya + cl1t + cl1t0 + cl1to + clit + clit0 + clito + cock + cog110ne + cog11one + cog1i0ne + cog1ione + cogl10ne + cogl1one + cogli0ne + coglione + cona + connard + connasse + conne + cou111es + cou11les + cou1l1es + cou1lles + coui11es + coui1les + couil1es + couilles + cracker + crap + cu10 + cu1att0ne + cu1attone + cu1er0 + cu1ero + cu1o + cul0 + culatt0ne + culattone + culer0 + culero + culo + cum + cunt + d11d0 + d11do + d1ck + d1ld0 + d1ldo + damn + de1ch + deich + depp + di1d0 + di1do + dick + dild0 + dildo + dyke + encu1e + encule + enema + enf01re + enf0ire + enfo1re + enfoire + estup1d0 + estup1do + estupid0 + estupido + etr0n + etron + f0da + f0der + f0ttere + f0tters1 + f0ttersi + f0tze + f0utre + f1ca + f1cker + f1ga + fag + fica + ficker + figa + foda + foder + fottere + fotters1 + fottersi + fotze + foutre + fr0c10 + fr0c1o + fr0ci0 + fr0cio + fr0sc10 + fr0sc1o + fr0sci0 + fr0scio + froc10 + froc1o + froci0 + frocio + frosc10 + frosc1o + frosci0 + froscio + fuck + g00 + g0o + g0u1ne + g0uine + gandu + go0 + goo + gou1ne + gouine + gr0gnasse + grognasse + haram1 + harami + haramzade + hund1n + hundin + id10t + id1ot + idi0t + idiot + imbec11e + imbec1le + imbeci1e + imbecile + j1zz + jerk + jizz + k1ke + kam1ne + kamine + kike + leccacu10 + leccacu1o + leccacul0 + leccaculo + m1erda + m1gn0tta + m1gnotta + m1nch1a + m1nchia + m1st + mam0n + mamahuev0 + mamahuevo + mamon + masturbat10n + masturbat1on + masturbate + masturbati0n + masturbation + merd0s0 + merd0so + merda + merde + merdos0 + merdoso + mierda + mign0tta + mignotta + minch1a + minchia + mist + musch1 + muschi + n1gger + neger + negr0 + negre + negro + nerch1a + nerchia + nigger + orgasm + p00p + p011a + p01la + p0l1a + p0lla + p0mp1n0 + p0mp1no + p0mpin0 + p0mpino + p0op + p0rca + p0rn + p0rra + p0uff1asse + p0uffiasse + p1p1 + p1pi + p1r1a + p1rla + p1sc10 + p1sc1o + p1sci0 + p1scio + p1sser + pa11e + pa1le + pal1e + palle + pane1e1r0 + pane1e1ro + pane1eir0 + pane1eiro + panele1r0 + panele1ro + paneleir0 + paneleiro + patakha + pec0r1na + pec0rina + pecor1na + pecorina + pen1s + pendej0 + pendejo + penis + pip1 + pipi + pir1a + pirla + pisc10 + pisc1o + pisci0 + piscio + pisser + po0p + po11a + po1la + pol1a + polla + pomp1n0 + pomp1no + pompin0 + pompino + poop + porca + porn + porra + pouff1asse + pouffiasse + pr1ck + prick + pussy + put1za + puta + puta1n + putain + pute + putiza + puttana + queca + r0mp1ba11e + r0mp1ba1le + r0mp1bal1e + r0mp1balle + r0mpiba11e + r0mpiba1le + r0mpibal1e + r0mpiballe + rand1 + randi + rape + recch10ne + recch1one + recchi0ne + recchione + retard + romp1ba11e + romp1ba1le + romp1bal1e + romp1balle + rompiba11e + rompiba1le + rompibal1e + rompiballe + ruff1an0 + ruff1ano + ruffian0 + ruffiano + s1ut + sa10pe + sa1aud + sa1ope + sacanagem + sal0pe + salaud + salope + saugnapf + sb0rr0ne + sb0rra + sb0rrone + sbattere + sbatters1 + sbattersi + sborr0ne + sborra + sborrone + sc0pare + sc0pata + sch1ampe + sche1se + sche1sse + scheise + scheisse + schlampe + schwachs1nn1g + schwachs1nnig + schwachsinn1g + schwachsinnig + schwanz + scopare + scopata + sexy + sh1t + shit + slut + sp0mp1nare + sp0mpinare + spomp1nare + spompinare + str0nz0 + str0nza + str0nzo + stronz0 + stronza + stronzo + stup1d + stupid + succh1am1 + succh1ami + succhiam1 + succhiami + sucker + t0pa + tapette + test1c1e + test1cle + testic1e + testicle + tette + topa + tr01a + tr0ia + tr0mbare + tr1ng1er + tr1ngler + tring1er + tringler + tro1a + troia + trombare + turd + twat + vaffancu10 + vaffancu1o + vaffancul0 + vaffanculo + vag1na + vagina + verdammt + verga + w1chsen + wank + wichsen + x0ch0ta + x0chota + xana + xoch0ta + xochota + z0cc01a + z0cc0la + z0cco1a + z0ccola + z1z1 + z1zi + ziz1 + zizi + zocc01a + zocc0la + zocco1a + zoccola + } +} + +package provide sqids [namespace eval sqids { + variable version + set version 0.3.0 +}] + diff --git a/src/vfs/_vfscommon.vfs/modules/sqids-0.3.1.tm b/src/vfs/_vfscommon.vfs/modules/sqids-0.3.1.tm new file mode 100644 index 00000000..b7fddc82 --- /dev/null +++ b/src/vfs/_vfscommon.vfs/modules/sqids-0.3.1.tm @@ -0,0 +1,931 @@ +#Do not update version of this file. Update sqids-buildversion.txt and run make.tcl to update the version in this file and copy to modules folder. +package require Tcl 8.6- + +#MIT license +#Julian Noble 2026 + +#example: +# % package require sqids +# % set s1 [sqids::idscope new] +# ::oo::Obj275 +# % $s1 encode {1 2 3} +# 86Rf07 +# % $s1 decode 86Rf07 +# 1 2 3 + + +namespace eval sqids { + oo::class create idscope { + variable o_alphabet + variable o_alphabet_configured + variable o_alpha_re + variable o_minlength + variable o_blocklist + variable o_maxsafeinteger + #note that methods beginning with uppercase letters are private. + constructor {args} { + set defaults [dict create {*}{ + -alphabet "" + -minlength "" + -blocklist "" + -maxsafeinteger "" + }] + if {[llength $args] %2 !=0} { + error "sqids::idscope constructor: Require option value pairs. Known options:[dict keys $defaults]." + } + set useropts [dict create] + set explicit_empty_blocklist 0 ;#as opposed to default due to being unspecified. + dict for {k v} $args { + set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist -maxsafeinteger} $k] + switch -exact -- $fullmatch { + -alphabet - -minlength - -maxsafeinteger { + dict set useropts $fullmatch $v + } + -blocklist { + if {[llength $v] == 0} { + set explicit_empty_blocklist 1 + } + dict set useropts -blocklist $v + } + default { + error "sqids::idscope constructor: unknown option '$k'. Known options:[dict keys $defaults]." + } + } + } + set opts [dict merge $defaults $useropts] + + set opt_alphabet [dict get $opts -alphabet] + if {$opt_alphabet eq ""} { + set o_alphabet $::sqids::data::default_alphabet + } else { + if {[string length $opt_alphabet] < 3} { + error "sqids::idscope constructor: -alphabet length must be at least 3." + } + #review - deny multibyte + if {[regexp {[^\u00-\u7F]} $opt_alphabet]} { + error "sqids::idscope constructor: -alphabet must not contain multibyte characters." + } + if {[regexp {(.).*\1} $opt_alphabet]} { + error "sqids::idscope constructor: -alphabet must contain unique characters." + } + set o_alphabet $opt_alphabet + } + set o_alphabet_configured $o_alphabet ;#for use in public method alphabet, which returns the configured alphabet in the order it was configured, not the shuffled order used for encoding. + set alphamatch [string map [list . \\. \[ \\\[ \] \\\] \{ \\\{ \} \\\}] $o_alphabet] ;#review + set o_alpha_re "^\[$alphamatch\]+\$" ;#independent of shuffled order. + + set o_alphabet [my shuffle $o_alphabet[set o_alphabet {}]] + + set opt_minlength [dict get $opts -minlength] + if {$opt_minlength eq ""} { + set o_minlength $::sqids::data::default_minlength + } else { + set maxval 255 + if {![string is integer -strict $opt_minlength] || $opt_minlength < 0 || $opt_minlength > $maxval} { + error "sqids constructor: -minlength must be an integer from 0 to $maxval inclusive." + } + set o_minlength $opt_minlength + } + set opt_blocklist [dict get $opts -blocklist] + if {!$explicit_empty_blocklist && $opt_blocklist eq ""} { + set o_blocklist $::sqids::data::default_blocklist + #default blocklist is already in lowercase. + } else { + set o_blocklist $opt_blocklist + set o_blocklist [string tolower $o_blocklist] + } + #Considered pruning blocklist entries that are 3 chars or less, + #or that contain characters not in the alphabet, as they will never match any id and just add overhead + #to the is_blocked method. + #This however adds some object instantiation overhead. + #counterpoint - caller should provide an appropriate blocklist for the supplied alphabet. + + set opt_maxsafeinteger [dict get $opts -maxsafeinteger] + if {$opt_maxsafeinteger eq ""} { + set o_maxsafeinteger $::sqids::data::MAX_SAFE_INTEGER + } else { + #accept arbitrarily large values as long as they're valid bignum integers. + if {[package vsatisfies [info tclversion] 8.7-]} { + if {![string is integer -strict $opt_maxsafeinteger] || $opt_maxsafeinteger < 0} { + error "sqids constructor: -maxsafeinteger must be a non-negative integer." + } + } else { + if {![string is entier -strict $opt_maxsafeinteger] || $opt_maxsafeinteger < 0} { + error "sqids constructor: -maxsafeinteger must be a non-negative integer." + } + } + set o_maxsafeinteger $opt_maxsafeinteger + } + + } + method config {{option {}}} { + #introspection method. + #return a dict of the configured options if no option specified, otherwise return the value of the specified option. + + #no facility is provided to change options after construction as a new idscope object should be used for different configurations (different scope of sqid ids). + #note that -alphabet refers to the configured alphabet in the order it was configured, not the shuffled order used for encoding. + if {$option eq ""} { + # + return [dict create {*}{ + } -blocklist $o_blocklist {*}{ + } -maxsafeinteger $o_maxsafeinteger {*}{ + } -minlength $o_minlength {*}{ + } -alphabet $o_alphabet_configured {*}{ + } + ] + } + set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist -maxsafeinteger} $option] + switch -exact -- $fullmatch { + -alphabet {return $o_alphabet_configured} + -minlength {return $o_minlength} + -blocklist {return $o_blocklist} + -maxsafeinteger {return $o_maxsafeinteger} + default { + error "sqids::idscope config: unknown option '$option'. Known options:-alphabet -minlength -blocklist -maxsafeinteger." + } + } + } + #review tcl8.7 behaves like tcl 9 + #tcl 8.7 wasn't ever officially released (and won't be) - but it was available for a while and may exist in the wild. + if {[package vsatisfies [info tclversion] 8.7-]} { + #'string is integer' for tcl versions 8.7 and above supports bignums, which can be arbitrarily large. + method encode {numlist} { + if {[llength $numlist] == 0} {return} + #cannot encode negative numbers, or non-integers. + foreach num $numlist { + if {![string is integer -strict $num] || $num < 0 || $num > $o_maxsafeinteger} { + error "sqids encode: can only encode integers from 0 to $o_maxsafeinteger. Invalid value: '$num'" + } + } + return [my EncodeNumbers $numlist] + } + } else { + #In tcl 8.6, 'string is integer' is limited to 2**32-1, use the now deprecated 'string is entier'. + #Otherwise - integer operations still support bignums. + #(versions below 8.6 not supported by this modules) + method encode {numlist} { + if {[llength $numlist] == 0} {return} + #cannot encode negative numbers, or non-integers. + foreach num $numlist { + if {![string is entier -strict $num] || $num < 0 || $num > $o_maxsafeinteger} { + error "sqids encode: can only encode integers from 0 to $o_maxsafeinteger. Invalid value: '$num'" + } + } + return [my EncodeNumbers $numlist] + } + } + method EncodeNumbers {numlist {increment 0}} { + #assert number of letters in o_alphabet and number of letters in local alpha are the same and don't effectively change during this function. + #('set alpha {}' in calls to my shuffle is an optimization to avoid shared string and copy-on-write overhead. As alpha is set to the result, it doesn't violate the previous assertion.) + set alpha_len [string length $o_alphabet] + + if {$increment > $alpha_len} { + error "sqids EncodeNumbers: Reached max attempts to re-generate the ID" + } + + set offset [llength $numlist] + set i -1 + foreach v $numlist { + incr i + set x [scan [string index $o_alphabet [expr {$v % $alpha_len}]] %c] + set offset [expr {$offset + $x + $i}] + } + set offset [expr {$offset % $alpha_len}] + set offset [expr {($offset + $increment) % $alpha_len}] + set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] + set prefix [string index $alpha 0] + set alpha [string reverse $alpha] + set id $prefix + set i -1 + foreach num $numlist { + incr i + append id [my ToId $num [string range $alpha 1 end]] + if {$i < [llength $numlist]-1} { + append id [string index $alpha 0] + set alpha [my shuffle $alpha[set alpha {}]] + } + } + if {$o_minlength > [string length $id]} { + append id [string index $alpha 0] + while {$o_minlength - [string length $id] > 0} { + set alpha [my shuffle $alpha[set alpha {}]] + set numchars [expr {min($o_minlength - [string length $id],$alpha_len)}] + append id [string range $alpha 0 $numchars-1] + } + } + if {[my is_blocked $id]} { + set id [my EncodeNumbers $numlist [expr {$increment+1}]] + } + return $id + } + method is_blocked {id} { + #deliberately public method. + if {![llength $o_blocklist]} { + return 0 + } + #o_blocklist is stored in lowercase, so compare against lowercase id. + set idtest [string tolower $id] + set idlen [string length $idtest] + if {$idlen < 3} { + #sqids rule: short ids less than 3 chars will not be blocked. + #(this is from the FAQ - but spec (code in isblocked) seems to contradict - saying <= 3 must match exactly) + #however - most implementations filter out blocklist entries shorter than 3 at construction time. + #- so effectively the FAQ seems right but the reference code implements it in a very roundabout and unintuitive way. + #REVIEW. Why are there no tests regarding such short ids? + return 0 + } + if {$idlen == 3} { + if {$idtest in $o_blocklist} { + return 1 + } + } else { + foreach blocked $o_blocklist { + if {[string length $blocked] <= 3} { + #sqids rule: blocklist entries of 3 chars will only be blocked if they match the entire id exactly, + #so skip them in this loop as we've already checked for exact matches of the whole id when idlen == 3. + #note blocklist entries of 0 1 or 2 chars will never match any id - but in this implementation we leave + #it to the caller to provide a sensible blocklist. Nevertheless if we encounter them we will just skip them here. + continue + } + set posn [string first $blocked $idtest] + if {$posn == -1} { + continue + } + if {$posn == 0} { + #whether leetspeak or not, blocklist entries that match at the beginning of the id will be blocked. + return 1 + } + if {[regexp {[0-9]} $blocked]} { + #sqids rule: blocklist entries with digits (leetspeak) will only be blocked if the match is at the beginning or end of the id. + #we've already checked the beginning, so check the end now. + set endpos [expr {$idlen - [string length $blocked]}] + if {$posn == $endpos} { + return 1 + } + } else { + #sqids rule: blocklist entries without digits will be blocked if they match anywhere in the id. + return 1 + } + } + } + return 0 + } + method ToId {num alpha} { + set id "" + set alpha_len [string length $alpha] + while 1 { + set id [string index $alpha [expr {$num % $alpha_len}]]$id + set num [expr {$num / $alpha_len}] + if {$num == 0} break + } + return $id + } + method ToNumber {id alpha} { + set number 0 + set alpha_len [string length $alpha] + for {set i 0} {$i < [string length $id]} {incr i} { + set posn [string first [string index $id $i] $alpha] + set number [expr {($number * $alpha_len) + $posn}] + } + return $number + } + method shuffle {alpha} { + #public method. Primarily for internal use but can be used externally to examine the shuffled alphabet being used for encoding. + #e.g myscopeobject shuffle [myscopeobject config -alphabet] would show the shuffled alphabet being used for encoding. + #consistent shuffle (always produce the same result for same input) + set alpha_len [string length $alpha] + if {$alpha_len < 2} { + return $alpha + } + set chars [split $alpha ""] + + for {set i 0; set j [expr {$alpha_len-1}]} {$j > 0} {incr i; incr j -1} { + set iv [scan [lindex $chars $i] %c] + set jv [scan [lindex $chars $j] %c] + set r [expr {($i * $j + $iv + $jv) % $alpha_len}] + set item2 [lindex $chars $r] + lset chars $r [lindex $chars $i] + lset chars $i $item2 + } + return [join $chars ""] + } + method decode {id} { + if {$id eq ""} {return} + set result [list] + + if {![regexp $o_alpha_re $id]} { + puts stderr "sqids decode: ID contains characters not in the alphabet. re: $o_alpha_re id: $id" + return [list] + } + set prefix [string index $id 0] + set offset [string first $prefix $o_alphabet] + set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] + set alpha [string reverse $alpha] + set id [string range $id 1 end] + while {[string length $id] > 0} { + set separator [string index $alpha 0] + #split on first occurrence of separator only. + set sep_posn [string first $separator $id] + if {$sep_posn == -1} { + set parts [list $id] + } else { + set parts [list [string range $id 0 $sep_posn-1] [string range $id $sep_posn+1 end]] + } + #assert parts has 1 or 2 elements + + if {[lindex $parts 0] eq ""} { + #separator was at start of the id - done. + return $result + } + lappend result [my ToNumber [lindex $parts 0] [string range $alpha 1 end]] + if {[llength $parts] == 2} { + set alpha [my shuffle $alpha[set alpha {}]] + set id [lindex $parts 1] + } else { + set id "" + } + } + return $result + } + } +} +namespace eval sqids::data { + variable default_alphabet {abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789} + + variable default_minlength 0 + + #arbitrary 1 googol limit (approx 2**332). We could go much higher e.g [string repeat 9 1000] + #Tcl bignums are limited by available memory and max string length (e.g approx 2**30 bytes?) + #- but speed of encoding and decoding will degrade as the number increases. + #Can be overridden by providing a -maxsafeinteger option to the idscope constructor. + variable MAX_SAFE_INTEGER [expr {"1[string repeat 0 100]"}] + + variable default_blocklist { + 0rgasm + 1d10t + 1d1ot + 1di0t + 1diot + 1eccacu10 + 1eccacu1o + 1eccacul0 + 1eccaculo + 1mbec11e + 1mbec1le + 1mbeci1e + 1mbecile + a11upat0 + a11upato + a1lupat0 + a1lupato + aand + ah01e + ah0le + aho1e + ahole + al1upat0 + al1upato + allupat0 + allupato + ana1 + ana1e + anal + anale + anus + arrapat0 + arrapato + arsch + arse + ass + b00b + b00be + b01ata + b0ceta + b0iata + b0ob + b0obe + b0sta + b1tch + b1te + b1tte + ba1atkar + balatkar + bastard0 + bastardo + batt0na + battona + bitch + bite + bitte + bo0b + bo0be + bo1ata + boceta + boiata + boob + boobe + bosta + bran1age + bran1er + bran1ette + bran1eur + bran1euse + branlage + branler + branlette + branleur + branleuse + c0ck + c0g110ne + c0g11one + c0g1i0ne + c0g1ione + c0gl10ne + c0gl1one + c0gli0ne + c0glione + c0na + c0nnard + c0nnasse + c0nne + c0u111es + c0u11les + c0u1l1es + c0u1lles + c0ui11es + c0ui1les + c0uil1es + c0uilles + c11t + c11t0 + c11to + c1it + c1it0 + c1ito + cabr0n + cabra0 + cabrao + cabron + caca + cacca + cacete + cagante + cagar + cagare + cagna + cara1h0 + cara1ho + caracu10 + caracu1o + caracul0 + caraculo + caralh0 + caralho + cazz0 + cazz1mma + cazzata + cazzimma + cazzo + ch00t1a + ch00t1ya + ch00tia + ch00tiya + ch0d + ch0ot1a + ch0ot1ya + ch0otia + ch0otiya + ch1asse + ch1avata + ch1er + ch1ng0 + ch1ngadaz0s + ch1ngadazos + ch1ngader1ta + ch1ngaderita + ch1ngar + ch1ngo + ch1ngues + ch1nk + chatte + chiasse + chiavata + chier + ching0 + chingadaz0s + chingadazos + chingader1ta + chingaderita + chingar + chingo + chingues + chink + cho0t1a + cho0t1ya + cho0tia + cho0tiya + chod + choot1a + choot1ya + chootia + chootiya + cl1t + cl1t0 + cl1to + clit + clit0 + clito + cock + cog110ne + cog11one + cog1i0ne + cog1ione + cogl10ne + cogl1one + cogli0ne + coglione + cona + connard + connasse + conne + cou111es + cou11les + cou1l1es + cou1lles + coui11es + coui1les + couil1es + couilles + cracker + crap + cu10 + cu1att0ne + cu1attone + cu1er0 + cu1ero + cu1o + cul0 + culatt0ne + culattone + culer0 + culero + culo + cum + cunt + d11d0 + d11do + d1ck + d1ld0 + d1ldo + damn + de1ch + deich + depp + di1d0 + di1do + dick + dild0 + dildo + dyke + encu1e + encule + enema + enf01re + enf0ire + enfo1re + enfoire + estup1d0 + estup1do + estupid0 + estupido + etr0n + etron + f0da + f0der + f0ttere + f0tters1 + f0ttersi + f0tze + f0utre + f1ca + f1cker + f1ga + fag + fica + ficker + figa + foda + foder + fottere + fotters1 + fottersi + fotze + foutre + fr0c10 + fr0c1o + fr0ci0 + fr0cio + fr0sc10 + fr0sc1o + fr0sci0 + fr0scio + froc10 + froc1o + froci0 + frocio + frosc10 + frosc1o + frosci0 + froscio + fuck + g00 + g0o + g0u1ne + g0uine + gandu + go0 + goo + gou1ne + gouine + gr0gnasse + grognasse + haram1 + harami + haramzade + hund1n + hundin + id10t + id1ot + idi0t + idiot + imbec11e + imbec1le + imbeci1e + imbecile + j1zz + jerk + jizz + k1ke + kam1ne + kamine + kike + leccacu10 + leccacu1o + leccacul0 + leccaculo + m1erda + m1gn0tta + m1gnotta + m1nch1a + m1nchia + m1st + mam0n + mamahuev0 + mamahuevo + mamon + masturbat10n + masturbat1on + masturbate + masturbati0n + masturbation + merd0s0 + merd0so + merda + merde + merdos0 + merdoso + mierda + mign0tta + mignotta + minch1a + minchia + mist + musch1 + muschi + n1gger + neger + negr0 + negre + negro + nerch1a + nerchia + nigger + orgasm + p00p + p011a + p01la + p0l1a + p0lla + p0mp1n0 + p0mp1no + p0mpin0 + p0mpino + p0op + p0rca + p0rn + p0rra + p0uff1asse + p0uffiasse + p1p1 + p1pi + p1r1a + p1rla + p1sc10 + p1sc1o + p1sci0 + p1scio + p1sser + pa11e + pa1le + pal1e + palle + pane1e1r0 + pane1e1ro + pane1eir0 + pane1eiro + panele1r0 + panele1ro + paneleir0 + paneleiro + patakha + pec0r1na + pec0rina + pecor1na + pecorina + pen1s + pendej0 + pendejo + penis + pip1 + pipi + pir1a + pirla + pisc10 + pisc1o + pisci0 + piscio + pisser + po0p + po11a + po1la + pol1a + polla + pomp1n0 + pomp1no + pompin0 + pompino + poop + porca + porn + porra + pouff1asse + pouffiasse + pr1ck + prick + pussy + put1za + puta + puta1n + putain + pute + putiza + puttana + queca + r0mp1ba11e + r0mp1ba1le + r0mp1bal1e + r0mp1balle + r0mpiba11e + r0mpiba1le + r0mpibal1e + r0mpiballe + rand1 + randi + rape + recch10ne + recch1one + recchi0ne + recchione + retard + romp1ba11e + romp1ba1le + romp1bal1e + romp1balle + rompiba11e + rompiba1le + rompibal1e + rompiballe + ruff1an0 + ruff1ano + ruffian0 + ruffiano + s1ut + sa10pe + sa1aud + sa1ope + sacanagem + sal0pe + salaud + salope + saugnapf + sb0rr0ne + sb0rra + sb0rrone + sbattere + sbatters1 + sbattersi + sborr0ne + sborra + sborrone + sc0pare + sc0pata + sch1ampe + sche1se + sche1sse + scheise + scheisse + schlampe + schwachs1nn1g + schwachs1nnig + schwachsinn1g + schwachsinnig + schwanz + scopare + scopata + sexy + sh1t + shit + slut + sp0mp1nare + sp0mpinare + spomp1nare + spompinare + str0nz0 + str0nza + str0nzo + stronz0 + stronza + stronzo + stup1d + stupid + succh1am1 + succh1ami + succhiam1 + succhiami + sucker + t0pa + tapette + test1c1e + test1cle + testic1e + testicle + tette + topa + tr01a + tr0ia + tr0mbare + tr1ng1er + tr1ngler + tring1er + tringler + tro1a + troia + trombare + turd + twat + vaffancu10 + vaffancu1o + vaffancul0 + vaffanculo + vag1na + vagina + verdammt + verga + w1chsen + wank + wichsen + x0ch0ta + x0chota + xana + xoch0ta + xochota + z0cc01a + z0cc0la + z0cco1a + z0ccola + z1z1 + z1zi + ziz1 + zizi + zocc01a + zocc0la + zocco1a + zoccola + } +} + +package provide sqids [namespace eval sqids { + variable version + set version 0.3.1 +}] + diff --git a/src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm b/src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm index 143c980f0120e3cb0b2948d828c5b325d121ec61..7b6c2e93d8936f52c542818198ecb824a42492ae 100644 GIT binary patch delta 25 dcmX@Nu delta 25 dcmX@>vkgTu3fcFk=s+$A&w}TZJIZ zFc^J^OT%NJj6SLWcXy+8UyPp=4gr+o}IhW+X!;E8_}L<{9X*;8w}ZKm(2QB<(BQLh=}+Dn=rNp@Gy z{T4I7-XZF`n7NLR>n`1e=`d_9GUj{NOwadH_X<5Mm86oCq@)y!`ZBfR)*?OrP|Bqb zj-r5260!1-DTRHWj75@in~W-xk)sd_xh5F9f1VyMP5D9 zk3hEc(^!Q8sgtJ?{+fx2X>l#)Eag_{N7&#}tX9qc0<3e(of*E@lJvQ7)-YNuP zhQa7VTpAt&W%N-6xVsyz`(pg0a2U|_*tFbB(flkul%9W_rGtx>x97sU(%8BqgO-)R(Chw-)K~hf*$m za1;fEl8BXuOeyU1WGs@D+hkOkj2wkf$Th*({qyvAY0AI4NLxX5+SApu2GW12G0-Wg zu{o_^mzm-JJ3^Z#&RAzx*6`Gmm7SHVW0Pr)D@YhUCOp9wVXDPQrKf)g0TE_aSeLjXeem3o#^%dze=e)?@q(4kL zPOxtyJw|>V>-{@WO9KQH006U2BiRK5abcB{Dk)11Y6Cf2ijwGYtN{Q3q_cu4-vZh; B)TRIc diff --git a/src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm b/src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm index cbca60b6f18e6c0469af03dd71ca85abe9b2945c..77956f00f9427b86785df3c559152e659ae00806 100644 GIT binary patch delta 27 fcmcb!jq%Pl#touU%uhJ>Y?hE>w*)hkf~`0Jo9qee delta 27 gcmcb!jq%Pl#touU%u`n^-7F!+ZV6^61zT|d0HCr8UjP6A diff --git a/src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm b/src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm index 121d1d77306068949da7b0189703922a4387ae95..a714947751a4790057fb5d94ae8fc72eb202ee1d 100644 GIT binary patch delta 1083 zcmaEzIzMegtqk)Mjy;D-xufCuqWz8H>DkE$i8Zc$|I?5UaDki7wlRSUg zScC6}(}hdX>d{QY;mBD-C6?`PP*jV_ORuxXuG-)d_O|+ZoCod5^$#~k2 zKkf`)#Miv%)>v?V{&RiJ7Nslml7E=Z%?rP@>u%2e<*%NH{EluDi4zyu^#4n+i*x;h z=Uoabb+;BA%2Oh_q%oWad|1CE?)neL z$ZOU2g3h&H0OiN~{$0L4B ztCdlXNh)95ub*{h0!MM^>At@T=T=2-{W^bRz49N1OHViLJ8Eb0Px^9c z#vlI&%QcwS-ea7spe@7522C_lZ2$iQ69?nwV(o9tpbR=WN8ghVHGfV%tY5<9#67vu zNON+6s>I|f10DsGl*PrsV8?(A9`ZpIvrN8hV9V${S5wo`P=E3g!$3x}$qhyZ dlOy!>C!aRtnLJrfh!cV$P%>GtU~ZV);^LHBte?+*eWs%N+U?X#{aB-f zss~?}HgUBbuKhpJ$#9{k%;W9vK3TmM_`dp&i|Lw`eEUt0{3v$3`TKm`s!JE-zbG%$ z4)A7X5dj7W2gBrWRUt-Lh^Vms|If(4uz91ZE3z^)S2L{?RE(QiWq<~{L z6s`v=JsNU5Z?S<$P5+d73$I@n!e)DU-dnN7{!++9X7z(z`Z{K7m5%IRufL;3MC;at zYy8I!9iI6y(MGbf|4GAy$NQU}aag5EO`YerOxV$4RnHNQA5Ic36YqY|KlPI3OoqUe zbffEHkN5X4s^+g$v{bcj6YxH_lq3Gu!T{;0(0T*j%`@v`Bja{&Fl`8I?9tlmTj0-= zv%2_{r6{|`YTw}WL@rUuJ6CwR=lb|Pw<$aGS}1M$$sZgcj+v{MdQ4KiF;#7fu6BiQ z=A0(4)Pq9b<)-@K7Vi=Hv?w-xmbiFdEi6&wF{odf?9P z3sZynug`e=mLYNHzKl8H@$$(}qra8)6m;J0wcEeK&{^qBsBU%rw`PmgX_v3vPjvsm zwB&rMb7S*HHrBbHtg6f6KJCm*ux5=d%1u5gaq-3fFVBlVOnqh2lCXNp{QR|tb=<_H zW&zVGw_@dUUNNVl_JtD;o?xEkai&*xD@*F4Sr4Z!dvod6;e|If6Dub4P0z7)s$?G_6CR09A23lRVSF_TayD3cs143i!u z2YMrj{*(WVjvsZzdU-&%x>V<4(yXKe?9qWt@g9TIY(-9l(LFDQ=3 zXXc#au}KMiBU=WF{mEj*z))_OHj{}U3DYT|4A`D<2tzwu4HOf==R6@!QmKrcQU!xD@)<=l!Nc4o-?t}!?_fiDaC|=Shp+g z&Xy{Q1yRQa%~8>rlPXC;r&$C=2?eH^DdFCIBj1=PJCiLLE~}SbiM~DxLvj#OFn<@l z?8I@Brc6XNkuMs_5L-xyp*!$w3ez*>)PqM0vYa$)v`M0z5rH&yi9sr`vZ|1iPW`|d zoX6U{c7kWk+p@21#5M$@&>tTNT#KuLKq~;vn6WR_w~_S_o81}wY-it90eUAh9-(}= z1I%Ul=TH>;8SG~#-MeO(k8{KPCVxB3`+m4`jaE5hJvG0{49K4bDNtDgRW#UW*htkI zv2`Qo)AC$M`8kC<2Yk@E;JNkv3bDS44q`ulzz54ga9>Q$nb@LNUv$c2Qz-|pV%d!> z;reGt&1LC(XL`_f!2Ph>{(tFy?s}*kHV;&T%1oG5<@kU5N84INmaLhwO~WprKW~)* zISs0wUDOqO7IRb4?lzimg9v5>Prg@lP)P3lTjzy1LX+3lP)Ny4m7l{ vTmzn+cT@ua01FY5<0x+eZVr>tDJ_$JCnS@lDJlZw4wKO-Uj|qY00000w(!yO delta 649 zcmV;40(SlSR<=~I6&eFL8ZomN8o>$^Wr##uPcwME5CQ-IjtT$(F#rdXkSQ9IuqX_Z zU@8og9wi5Vy_8LF;~)@*@BS4dx2H%xBu9S7-rA@lm^3IDSUy}wR{r}A#PQdzR*IdL zi)B2+J2TH~3}t8f$-(m9L0Ar+Nza|nGDR!y3}qa6O(hT#w{IJ52DpX*Q5kK047nu} zvPls&wSpcb2d1k^t1d4t$2}^#AU))H+P!A!5z{e$+?tOAb%H_rD{lA`^#TdV^GWst*-CARDoK-z-w1SkwS+`;(8fcVZmCiJP0@Lcp9y3 zDHOGrUM-Dc5@W1p;7ll?y+Vs+2CLG0;q{rT>m)c1BUSY_Yq4+_)fzgWQX0{4-cL(YG6b2n zI!I^2Z4l44-QT9k^YM+5wBsqujgsdN6P~HAp^WW3^WN~53W?=1Ewx(3*>HQJ#z9bD z!f|=&c=z~mL~$(L4UqJ{AAUo|@SCK%oP0wbCO?*3gB=@rCU%r6e}ua8)cT+2PMR?k z_axdZ=T4f3xsN5+aK}bgalifnlX)oi12`HnlX5Ai4rPc$TTe51y$}Kb0FDZiCn|3O jDh-p5DI1fpC^nPnDk=h?4U>;48= 0} { + #use standard (slower) mechanism for counting lines + #vertical tab on a proper terminal should move directly down. + #Whether or not the terminal in use actually does this - we need to calculate as if it does. (there might not even be a terminal) + set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list + return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le + } else { + return [expr {[parse countnewline $textblock {}] + 1}] + } + } + } else { + #parser not loaded - but might be loadable. + #install a 'height' function that will load parser + proc _height_tcl {textblock} { #This is the height as it will/would-be rendered - not the number of input lines purely in terms of le #empty string still has height 1 (at least for left-right/right-left languages) @@ -5119,8 +5135,7 @@ tcl::namespace::eval textblock { set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le } - } else { - proc height {textblock} { + proc _height_c {textblock} { if {[string first \v $textblock] >= 0} { #use standard (slower) mechanism for counting lines #vertical tab on a proper terminal should move directly down. @@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock { return [expr {[parse countnewline $textblock {}] + 1}] } } + + #oneshot height function - renames itself on first call to the appropriate implementation. + proc height {textblock} { + if {[catch {package require parser}]} { + #parser not available - use tcl implementation + rename ::textblock::height "" + rename ::textblock::_height_tcl ::textblock::height + } else { + #parser available - use c implementation + rename ::textblock::height "" + rename ::textblock::_height_c ::textblock::height + } + tailcall ::textblock::height $textblock + } } + #MAINTENANCE - same as overtype::blocksize? proc size {textblock} { if {$textblock eq ""} { @@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock { -etabs -default 0\ -help "expanding tabs - experimental/unimplemented." #review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels - -type -default light\ - -type dict\ - -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ - -choices {${$DYN_FRAMETYPES}}\ - -choicerestricted 0 -choicecolumns 8\ - -unindentedfields {-choicelabels}\ - -choicelabels { - ${$DYN_FRAMESAMPLES} - }\ - -help "Type of border for frame." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${$DYN_FRAMETYPES}}\ + -choicerestricted 0\ + -choicecolumns 8\ + -unindentedfields {-choicelabels}\ + -choicelabels { + ${$DYN_FRAMESAMPLES} + }\ + -help "Type of border for frame." -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. passing an empty string will result in no box, but title/subtitle will still appear if supplied. ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" diff --git a/src/vfs/_vfscommon.vfs/modules/zipper-0.14.tm b/src/vfs/_vfscommon.vfs/modules/zipper-0.14.tm index d0cb4f1c740f48ca0a5d46b092f71346ce4ef9e6..7f7817f1ddce7fa6abfdea66e3c84636d1fe44cd 100644 GIT binary patch delta 24 dcmX@+d(3x(s1)-pjy; LUP_RETURN_ADDR - set rec [LookupServiceNext $h 0x100] - if {[dict exists $rec RemoteAddress] && - [dict get $rec RemoteAddress AddressFamily] == 32} { - # 32 -> AF_BTH (Bluetooth) - # Further we are looking for RFCOMM (protocol 3) - if {[dict exists $rec Protocol] && - [dict get $rec Protocol] == 3} { - return [dict get $rec RemoteAddress Port] - } - } - } - } finally { - LookupServiceEnd $h - } - error "Could not resolve service \"$service_class\" to a port on device \"$device\"." -} - -proc iocp::bt::device::remove {device} { - # Removes cached authentication information for a device from the system cache. - # device - bluetooth address or name of a device. if specified as a name, - # it must resolve to a single address. - # The command will raise an error if $device is a name that cannot be - # resolved. - - RemoveDevice [ResolveDeviceUnique $device] -} - -proc iocp::bt::device::service_references {device service} { - # Retrieve service discovery records that refer to a specified service. - # device - Bluetooth address or name of a device. If specified as a name, - # it must resolve to a single address. - # service - the UUID of a service or service class or its mnemonic. - # The command will return all service discovery records that contain - # an attribute referring to the specified service. - # The returned service discovery records should be treated as - # opaque and accessed through the service record decoding commands. - # - # Returns a list of service discovery records. - - set h [LookupServiceBegin \ - [ResolveDeviceUnique $device] \ - [names::to_uuid $service]] - set recs {} - try { - while {1} { - # 0x0200 -> LUP_RETURN_BLOB. Returns {Blob BINDATA} - lappend recs [lindex [LookupServiceNext $h 0x200] 1] - } - } finally { - LookupServiceEnd $h - } - return $recs -} - -proc iocp::bt::device::services {device} { - # Retrieve the service discovery records for top level services - # advertised by a device. - # device - Bluetooth address or name of a device. If specified as a name, - # it must resolve to a single address. - # - # The command will return all service discovery records that reference - # the `PublicBrowseRoot` service class. This is not necessarily all the - # services on the device, only those the device advertises as the - # top-level services. - # - # The returned service discovery records should be treated as - # opaque and accessed through the service record decoding commands. - # - # Returns a list of service dscovery records. - - # TBD - add a browse group parameter - # TBD - perhaps check that the sdr acually refernces browse group in - # the appropriate attribute - return [service_references $device 00001002-0000-1000-8000-00805f9b34fb] -} - -# TBD - is this needed? Less functional version of services -proc iocp::bt::device::enumerate_services {device} { - # Get installed services on a device. - # device - Bluetooth address or name of device. If specified as a name, - # it must resolve to a single address. - # Returns a list of service UUID's. - - return [EnumerateInstalledServices [ResolveDeviceUnique $device]] -} - - -proc iocp::bt::device::print {devinfo} { - # Prints device information in human-readable form to stdout. - # devinfo - A device information record as returned by - # the [devices] command. - dict with devinfo { - puts "Device $Name" - puts "Address: $Address" - puts "Class: $Class ($MajorClassName:$MinorClassName)" - puts "Device categories: ([join $DeviceClasses {, }])" - puts "Authenticated: $Authenticated" - puts "Remembered: $Remembered" - puts "Connected: $Connected" - puts "Last seen: $LastSeen" - puts "Last used: $LastUsed" - } -} - -proc iocp::bt::device::printn {dinfolist {detailed false}} { - # Prints device information in human-readable form to stdout. - # dinfolist - A list of device information records as returned by - # the [devices] command. - # detailed - If a true value, detailed information about the device - # is printed. If false (default), only the address and - # name are printed in compact form. - set sep "" - foreach dinfo $dinfolist { - if {$detailed} { - puts -nonewline $sep - set sep "----------------------------------------------\n" - print $dinfo - } else { - dict with dinfo { - puts "$Address $Name" - } - } - } -} - -proc iocp::bt::IsAddress {addr} { - # Returns boolean indicating whether addr is a valid Bluetooth address - # addr - value to be checked - - # TBD - implement in C? - return [regexp {^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){5}$} $addr] - -} - -proc iocp::bt::Uuid16 {uuid16} { - if {![regexp {^[[:xdigit:]]{4}$} $uuid16]} { - error "\"$uuid16\" is not a valid 16 bit UUID." - } - return 0000${uuid16}-0000-1000-8000-00805f9b34fb -} - -proc iocp::bt::IsUuid {uuid} { - - # Check passed parameter for UUID syntax - - # Only checks syntax. Not RFC 4122 validity - # TBD - implement in C? - return [regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid] -} - -proc iocp::bt::IsBluetoothUuid {uuid} { - - # Check passed parameter for UUID syntax - - # TBD - implement in C? - return [regexp {^[[:xdigit:]]{8}-0000-1000-8000-00805f9b34fb$} $uuid] -} - - -proc iocp::bt::BinToHex {bin {start 0} {end end}} { - regexp -inline -all .. [binary encode hex [string range $bin $start $end]] -} - -proc iocp::bt::EncodingTclName {mibenum} { - set encoding_map { - 3 ascii - 2026 big5 - 437 cp437 - 2087 cp775 - 2009 cp850 - 2010 cp852 - 2046 cp855 - 2047 cp857 - 2048 cp860 - 2049 cp861 - 2013 cp862 - 2050 cp863 - 2051 cp864 - 2052 cp865 - 2086 cp866 - 2054 cp869 - 2109 cp874 - 113 cp936 - 2250 cp1250 - 2251 cp1251 - 2252 cp1252 - 2253 cp1253 - 2254 cp1254 - 2255 cp1255 - 2256 cp1256 - 2257 cp1257 - 2258 cp1258 - 2088 ebcdic - 18 euc-jp - 38 euc-kr - 56 gb1988 - 2025 gb2312 - 57 gb2312-raw - 39 iso2022-jp - 37 iso2022-kr - 4 iso8859-1 - 5 iso8859-2 - 6 iso8859-3 - 7 iso8859-4 - 8 iso8859-5 - 9 iso8859-6 - 10 iso8859-7 - 11 iso8859-8 - 12 iso8859-9 - 13 iso8859-10 - 109 iso8859-13 - 110 iso8859-14 - 111 iso8859-15 - 112 iso8859-16 - 15 jis0201 - 63 jis0208 - 98 jis0212 - 2084 koi8-r - 2088 koi8-u - 36 ksc5601 - 17 shiftjis - 2259 tis-620 - 106 utf-8 - } - incr mibenum 0; # Convert to canonical decimal string - if {[dict exists $encoding_map $mibenum]} { - return [dict get $encoding_map $mibenum] - } - return $mibenum -} - -proc iocp::AddressFamilyName {af} { - if {$af == 2} { - return IPv4 - } elseif {$af == 23} { - return IPv6 - } elseif {$af == 32} { - return Bluetooth - } elseif {$af == 1} { - return Unix - } elseif {$af == 6} { - return IPX - } elseif {$af == 17} { - return Netbios - } else { - return AF$af - } -} - -proc DeviceClass {class} { - if {[expr {$class & 3}] != 0} { - # Not format type that we understand - return {} - } - - # https://www.bluetooth.com/specifications/assigned-numbers/baseband/ - - set minor_class [expr {($class >> 2) & 0x3f}] - set major_class [expr {($class >> 8) & 0x1f}] - - if {$major_class == 0} { - set major_name "Miscellaneous" - set minor_name "Miscellaneous" - } elseif {$major_class == 1} { - set major_name Computer - set minor_name [lindex { - Uncategorized Desktop Server Laptop "Handheld PC/PDA" "Palm-size PC/PDA" Wearable Tablet - } $minor_class] - } elseif {$major_class == 2} { - set major_name Phone - set minor_name [lindex { - Uncategorized Cellular Cordless Smartphone "Wired modem" ISDN - } $minor_class] - } elseif {$major_class == 3} { - set major_name "LAN/Network Access point" - set minor_name [lindex { - 0% 1-17% 17-33% 33-50% 50-67% 67-83% 83-99% 100% - } $minor_class] - } elseif {$major_class == 4} { - set major_name "Audio/Video" - set minor_name [lindex { - Uncategorized "Wearable Headset Device" "Hands-free Device" Reserved - Microphone Loudspeaker Headphones "Portable Audio" "Car Audio" - "Set-top box" "HiFi Audio Device" VCR Camcorder "Video Monitor" - "Video Display and Loudspeaker" "Video Conferencing" Reserved - "Gaming/Toy" - } $minor_class] - } elseif {$major_class == 5} { - set major_name Peripheral - set bits [expr {$minor_class >> 4}] - set minor_name [lindex { - Other Keyboard "Pointing device" "Combo keyboard/pointing device" - } $bits] - set bits [expr {$minor_class & 0xf}] - if {$bits < 10} { - append minor_name /[lindex { - Uncategorized Joystick Gamepad "Remote control" "Sensing device" - "Digitizer tabler" "Card reader" "Digital pen" "Handheld scanner" - "Handheld gesture input device" - }] - } - } elseif {$major_class == 6} { - set major_name Imaging - set minor_name {} - if {$minor_class & 4} {lappend minor_name Display} - if {$minor_class & 8} {lappend minor_name Camera} - if {$minor_class & 16} {lappend minor_name Scanner} - if {$minor_class & 32} {lappend minor_name Printer} - set minor_name [join $minor_name /] - } elseif {$major_class == 7} { - set major_name Wearable - set minor_name [lindex { - Wristwatch Pager Jacket Helmet Glasses - } $minor_class] - } elseif {$major_class == 8} { - set major_name Toy - set minor_name [lindex { - Robot Vehicle Doll Controller Game - } $minor_class] - } elseif {$major_class == 9} { - set major_name Health - set minor_name [lindex { - Undefined "Blood Pressure Monitor" Thermometer "Weighing Scale" - "Glucose Meter" "Pulse Oximeter" "Heart/Pulse Rate Monitor" - "Health Data Display" "Step Counter" "Body Composition Analyzer" - "Peak Flow Monitor" "Medication Monitor" "Knee Prosthesis" - "Andle Prosthesis" "Generic Health Manager" "Personal Mobility Device" - } $minor_class] - } elseif {$major_class == 31} { - set major_name Uncategorized - set minor_name "" - } else { - set major_name Reserved - set minor_name "" - } - - set service_class {} - if {$class & (1<<13)} {lappend service_class "Limited Discoverable Mode"} - if {$class & (1<<16)} {lappend service_class "Positioning (Location identification)"} - if {$class & (1<<17)} {lappend service_class "Networking"} - if {$class & (1<<18)} {lappend service_class "Rendering"} - if {$class & (1<<19)} {lappend service_class "Capturing"} - if {$class & (1<<20)} {lappend service_class "Object Transfer"} - if {$class & (1<<21)} {lappend service_class "Audio"} - if {$class & (1<<22)} {lappend service_class "Telephony"} - if {$class & (1<<23)} {lappend service_class "Information"} - - return [dict create MajorClassName $major_name \ - MinorClassName $minor_name \ - DeviceClasses $service_class] -} - -proc iocp::bt::ResolveDeviceUnique {device} { - if {[IsAddress $device]} { - return $device - } - set addrs [device address $device] - if {[llength $addrs] == 0} { - error "Could not resolve Bluetooth device name \"$device\"." - } elseif {[llength $addrs] > 1} { - error "Device \"$device\" resolves to multiple addresses." - } - return [lindex $addrs 0] -} - -package provide iocp_bt $iocp::bt::version +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +package require iocp +iocp::bt_init + +namespace eval iocp::bt { + variable script_dir [file dirname [info script]] + variable version + + namespace eval radio { + namespace path [namespace parent] + namespace export configure devices info + namespace ensemble create + } + namespace eval device { + namespace path [namespace parent] + namespace export address addresses port print printn remove service_references services + namespace ensemble create + } +} + +set iocp::bt::version [package require iocp] +source [file join $iocp::bt::script_dir btsdr.tcl] +source [file join $iocp::bt::script_dir btnames.tcl] + +proc iocp::bt::radios {{detailed false}} { + # Enumerate Bluetooth radios on the local system. + # detailed - If true, detailed information about each radio is returned. + # If false (default), only the radio addresses are returned. + # + # When $detailed is passed as a boolean false value, a list of radio + # addresses is returned. + # + # When $detailed is passed as a boolean true value, + # each element of the returned list contains the following keys: + # Address - The Bluetooth address of the radio. + # Name - Name assigned to the local system as advertised by the radio. + # Class - Device class as a numeric value. + # DeviceClasses - Human readable list of general device class categories + # Subversion - Integer value whose interpretation is manufacturer-specific. + # Manufacturer - Integer identifier assigned to the manufacturer. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # + # Returns a list of radio addresses or radio information elements. + + set pair [FindFirstRadio] + if {[llength $pair] == 0} { + return {} + } + lassign $pair finder hradio + set radios {} + try { + while {1} { + set radio [GetRadioInfo $hradio] + if {$detailed} { + lappend radios [dict merge $radio [DeviceClass [dict get $radio Class]]] + } else { + lappend radios [dict get $radio Address] + } + CloseHandle $hradio + set hradio [FindNextRadio $finder] + } + } finally { + FindFirstRadioClose $finder + } + return $radios +} + +proc iocp::bt::radio::Open {{radio {}}} { + # Get a handle to a radio. + # radio - The address or name associated with a radio on the system. + # If unspecified or the empty string, a handle to the first + # radio found is returned. + + # The command will raise an error if no matching radio is present. + # + # The returned handle must be closed with the [close] command. + # + # Returns a handle to a Bluetooth radio. + + set pair [FindFirstRadio] + if {[llength $pair] == 0} { + error "Radio not found." + } + lassign $pair finder hradio + try { + while {1} { + set info [GetRadioInfo $hradio] + if {$radio eq "" || + [string equal -nocase $radio [dict get $info Name]] || + [string equal -nocase $radio [dict get $info Address]]} { + return $hradio + } + CloseHandle $hradio + set hradio [FindNextRadio $finder] + } + } finally { + FindFirstRadioClose $finder + } + error "Radio not found." +} + +proc iocp::bt::radio::Close {hradio} { + # Closes a radio handle. + # hradio - The radio handle to close. + CloseHandle $hradio +} + +proc iocp::bt::radio::info {{radio {}}} { + # Get detailed information about a radio on the system + # radio - The address or name associated with a radio on the system. + # If unspecified or the empty string, information about + # the first radio found is returned. + # + # The returned dictionary has the following keys: + # + # Address - The Bluetooth address of the radio. + # Name - Name assigned to the local system as advertised by the radio. + # Class - Device class as a numeric value. + # DeviceClasses - Human readable list of general device class categories + # Subversion - Integer value whose interpretation is manufacturer-specific. + # Manufacturer - Integer identifier assigned to the manufacturer. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # + # Returns a dictionary containing information about the radio. + + set hradio [Open $radio] + try { + set radio [GetRadioInfo $hradio] + set radio [dict merge $radio [DeviceClass [dict get $radio Class]]] + } finally { + Close $hradio + } +} + +proc iocp::bt::radio::configure {radio args} { + # Gets or modifies a radio configuration. + # radio - The address or name associated with a radio on the system. + # args - See below. + # + # Returns an option value, a dictionary of options and values, or an empty + # string. + # + # If no arguments are given to the command, it returns the current + # values of all options in the form of a dictionary. + # + # If exactly one argument is given, it must be the name of an option + # and the command returns the value of the option. + # + # Otherwise, the arguments must be a list of option name and + # values. The radio's options are set accordingly. The command returns + # an empty string for this case. Note that in case of raised exceptions + # the state of the radio options is indeterminate. + # + # Supported option names are `-discoverable` and `-connectable` both + # of which take boolean values. They control whether the Bluetooth radio + # is discoverable and connectable from other devices. + # + # Known bug: the command does not work reliably on all Windows systems. + # + + set hradio [Open $radio] + try { + if {[llength $args] == 0} { + return [list \ + -discoverable [IsDiscoverable $hradio] \ + -connectable [IsConnectable $hradio]] + } elseif {[llength $args] == 1} { + return [switch -exact -- [lindex $args 0] { + -discoverable {IsDiscoverable $hradio} + -connectable {IsConnectable $hradio} + default { error "Unknown option \"[lindex $args 0]\"."} + }] + } else { + set unchanged {} + foreach {opt val} $args { + switch -exact -- $opt { + -discoverable { + set changed [EnableDiscovery $val] + } + -connectable { + set changed [EnableIncoming $val] + } + default { error "Unknown option \"$opt\"."} + } + if {! $changed} { + lappend unchanged $opt + } + } + if {[llength $unchanged]} { + error "Option(s) [join $unchanged {, }] could not be modified." + } + } + } finally { + Close $hradio + } +} + +proc iocp::bt::radio::devices {radio args} { + # Discover devices accessible through the specified radio. + # radio - Name or address of Bluetooth radio + # args - Passed on to [::iocp::bt::devices] + # This command has the same functionality as the [::iocp::bt::devices] + # command except that it restricts discovery only to those devices + # accessible through the specified radio. + # + # Returns a list of device information dictionaries. See + # [::iocp::bt::devices] for the dictionary format. + + set hradio [Open $radio] + try { + return [::iocp::bt::devices {*}$args -hradio $hradio] + } finally { + Close $hradio + } +} + +proc iocp::bt::devices {args} { + # Discover Bluetooth devices. + # -authenticated - filter for authenticated devices + # -connected - filter for connected devices + # -inquire - issue a new inquiry. Without this option, devices that are + # not already known to the system will not be discovered. + # -remembered - filter for remembered devices + # -timeout MS - timeout for the inquiry in milliseconds. Defaults to 10240ms. + # Ignored if `-inquire` is not specified. + # -unknown - filter for unknown devices + # + # Each device information element is returned as a dictionary with + # the following keys: + # Authenticated - Boolean value indicating whether the device has + # been authenticated + # Address - Bluetooth address of the devicec + # Class - Device class as a numeric value + # Connected - Boolean value indicating whether the device is connected + # DeviceClasses - Human readable list of general device class categories + # LastSeen - Time when device was last seen. The format is a list of + # year, month, day, hour, minutes, seconds and milliseconds. + # LastUsed - Time when device was last used. The format is a list of + # year, month, day, hour, minutes, seconds and milliseconds. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # Name - Human readable name of the device + # Remembered - Boolean value indicating whether the device is connected + # + # The filtering options may be specified to limit the devices + # returned. If none are specified, all devices are returned. + # + # Returns a list of device information dictionaries. + + set pair [FindFirstDevice {*}$args] + if {[llength $pair] == 0} { + # No devices found + return {} + } + lassign $pair finder device + set device [dict merge $device [DeviceClass [dict get $device Class]]] + set devices [list $device] + try { + while {1} { + set device [FindNextDevice $finder] + set device [dict merge $device [DeviceClass [dict get $device Class]]] + lappend devices $device + } + } finally { + FindFirstDeviceClose $finder + } + return $devices +} + +proc iocp::bt::device::address {name args} { + # Returns a Bluetooth address for a given name. + # name - name of device of interest + # args - Options to control device enquiry. See [devices]. + # If a device has multiple addresses, the command may return any one + # them. If no addresses are found for the device, an error is raised. + set addrs [addresses $name {*}$args] + if {[llength $addrs]} { + return [lindex $addrs 0] + } + error "Could not find address for device \"$name\"." +} + +proc iocp::bt::device::addresses {name args} { + # Returns a list of Bluetooth addresses for a given name. + # name - name of device of interest + # args - Options to control device enquiry. See [devices]. + + set addresses [lmap device [devices {*}$args] { + if {[string compare -nocase $name [dict get $device Name]]} { + continue + } + dict get $device Address + }] + # Also resolve local system radios + foreach radio [radios 1] { + if {[string equal -nocase $name [dict get $radio Name]]} { + lappend addresses [dict get $radio Address] + } + } + return $addresses +} + +proc iocp::bt::device::port {device service_class} { + # Resolve the port for a Bluetooth service running over RFCOMM. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service_class - UUID or name of service class of interest. Note the + # service **name** cannot be used for lookup. + # + # In case multiple services of the same service class are available on the + # device, the port for the first one discovered is returned. + # + # Returns the port number for the service or raises an error if it + # cannot be resolved. + + #TBD - maybe use device::services and loop through so we can match service_class + #against service name as well + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::service_class_uuid $service_class]] + try { + while {1} { + # 0x100 -> LUP_RETURN_ADDR + set rec [LookupServiceNext $h 0x100] + if {[dict exists $rec RemoteAddress] && + [dict get $rec RemoteAddress AddressFamily] == 32} { + # 32 -> AF_BTH (Bluetooth) + # Further we are looking for RFCOMM (protocol 3) + if {[dict exists $rec Protocol] && + [dict get $rec Protocol] == 3} { + return [dict get $rec RemoteAddress Port] + } + } + } + } finally { + LookupServiceEnd $h + } + error "Could not resolve service \"$service_class\" to a port on device \"$device\"." +} + +proc iocp::bt::device::remove {device} { + # Removes cached authentication information for a device from the system cache. + # device - bluetooth address or name of a device. if specified as a name, + # it must resolve to a single address. + # The command will raise an error if $device is a name that cannot be + # resolved. + + RemoveDevice [ResolveDeviceUnique $device] +} + +proc iocp::bt::device::service_references {device service} { + # Retrieve service discovery records that refer to a specified service. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service - the UUID of a service or service class or its mnemonic. + # The command will return all service discovery records that contain + # an attribute referring to the specified service. + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service discovery records. + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::to_uuid $service]] + set recs {} + try { + while {1} { + # 0x0200 -> LUP_RETURN_BLOB. Returns {Blob BINDATA} + lappend recs [lindex [LookupServiceNext $h 0x200] 1] + } + } finally { + LookupServiceEnd $h + } + return $recs +} + +proc iocp::bt::device::services {device} { + # Retrieve the service discovery records for top level services + # advertised by a device. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # + # The command will return all service discovery records that reference + # the `PublicBrowseRoot` service class. This is not necessarily all the + # services on the device, only those the device advertises as the + # top-level services. + # + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service dscovery records. + + # TBD - add a browse group parameter + # TBD - perhaps check that the sdr acually refernces browse group in + # the appropriate attribute + return [service_references $device 00001002-0000-1000-8000-00805f9b34fb] +} + +# TBD - is this needed? Less functional version of services +proc iocp::bt::device::enumerate_services {device} { + # Get installed services on a device. + # device - Bluetooth address or name of device. If specified as a name, + # it must resolve to a single address. + # Returns a list of service UUID's. + + return [EnumerateInstalledServices [ResolveDeviceUnique $device]] +} + + +proc iocp::bt::device::print {devinfo} { + # Prints device information in human-readable form to stdout. + # devinfo - A device information record as returned by + # the [devices] command. + dict with devinfo { + puts "Device $Name" + puts "Address: $Address" + puts "Class: $Class ($MajorClassName:$MinorClassName)" + puts "Device categories: ([join $DeviceClasses {, }])" + puts "Authenticated: $Authenticated" + puts "Remembered: $Remembered" + puts "Connected: $Connected" + puts "Last seen: $LastSeen" + puts "Last used: $LastUsed" + } +} + +proc iocp::bt::device::printn {dinfolist {detailed false}} { + # Prints device information in human-readable form to stdout. + # dinfolist - A list of device information records as returned by + # the [devices] command. + # detailed - If a true value, detailed information about the device + # is printed. If false (default), only the address and + # name are printed in compact form. + set sep "" + foreach dinfo $dinfolist { + if {$detailed} { + puts -nonewline $sep + set sep "----------------------------------------------\n" + print $dinfo + } else { + dict with dinfo { + puts "$Address $Name" + } + } + } +} + +proc iocp::bt::IsAddress {addr} { + # Returns boolean indicating whether addr is a valid Bluetooth address + # addr - value to be checked + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){5}$} $addr] + +} + +proc iocp::bt::Uuid16 {uuid16} { + if {![regexp {^[[:xdigit:]]{4}$} $uuid16]} { + error "\"$uuid16\" is not a valid 16 bit UUID." + } + return 0000${uuid16}-0000-1000-8000-00805f9b34fb +} + +proc iocp::bt::IsUuid {uuid} { + + # Check passed parameter for UUID syntax + + # Only checks syntax. Not RFC 4122 validity + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid] +} + +proc iocp::bt::IsBluetoothUuid {uuid} { + + # Check passed parameter for UUID syntax + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-0000-1000-8000-00805f9b34fb$} $uuid] +} + + +proc iocp::bt::BinToHex {bin {start 0} {end end}} { + regexp -inline -all .. [binary encode hex [string range $bin $start $end]] +} + +proc iocp::bt::EncodingTclName {mibenum} { + set encoding_map { + 3 ascii + 2026 big5 + 437 cp437 + 2087 cp775 + 2009 cp850 + 2010 cp852 + 2046 cp855 + 2047 cp857 + 2048 cp860 + 2049 cp861 + 2013 cp862 + 2050 cp863 + 2051 cp864 + 2052 cp865 + 2086 cp866 + 2054 cp869 + 2109 cp874 + 113 cp936 + 2250 cp1250 + 2251 cp1251 + 2252 cp1252 + 2253 cp1253 + 2254 cp1254 + 2255 cp1255 + 2256 cp1256 + 2257 cp1257 + 2258 cp1258 + 2088 ebcdic + 18 euc-jp + 38 euc-kr + 56 gb1988 + 2025 gb2312 + 57 gb2312-raw + 39 iso2022-jp + 37 iso2022-kr + 4 iso8859-1 + 5 iso8859-2 + 6 iso8859-3 + 7 iso8859-4 + 8 iso8859-5 + 9 iso8859-6 + 10 iso8859-7 + 11 iso8859-8 + 12 iso8859-9 + 13 iso8859-10 + 109 iso8859-13 + 110 iso8859-14 + 111 iso8859-15 + 112 iso8859-16 + 15 jis0201 + 63 jis0208 + 98 jis0212 + 2084 koi8-r + 2088 koi8-u + 36 ksc5601 + 17 shiftjis + 2259 tis-620 + 106 utf-8 + } + incr mibenum 0; # Convert to canonical decimal string + if {[dict exists $encoding_map $mibenum]} { + return [dict get $encoding_map $mibenum] + } + return $mibenum +} + +proc iocp::AddressFamilyName {af} { + if {$af == 2} { + return IPv4 + } elseif {$af == 23} { + return IPv6 + } elseif {$af == 32} { + return Bluetooth + } elseif {$af == 1} { + return Unix + } elseif {$af == 6} { + return IPX + } elseif {$af == 17} { + return Netbios + } else { + return AF$af + } +} + +proc DeviceClass {class} { + if {[expr {$class & 3}] != 0} { + # Not format type that we understand + return {} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/baseband/ + + set minor_class [expr {($class >> 2) & 0x3f}] + set major_class [expr {($class >> 8) & 0x1f}] + + if {$major_class == 0} { + set major_name "Miscellaneous" + set minor_name "Miscellaneous" + } elseif {$major_class == 1} { + set major_name Computer + set minor_name [lindex { + Uncategorized Desktop Server Laptop "Handheld PC/PDA" "Palm-size PC/PDA" Wearable Tablet + } $minor_class] + } elseif {$major_class == 2} { + set major_name Phone + set minor_name [lindex { + Uncategorized Cellular Cordless Smartphone "Wired modem" ISDN + } $minor_class] + } elseif {$major_class == 3} { + set major_name "LAN/Network Access point" + set minor_name [lindex { + 0% 1-17% 17-33% 33-50% 50-67% 67-83% 83-99% 100% + } $minor_class] + } elseif {$major_class == 4} { + set major_name "Audio/Video" + set minor_name [lindex { + Uncategorized "Wearable Headset Device" "Hands-free Device" Reserved + Microphone Loudspeaker Headphones "Portable Audio" "Car Audio" + "Set-top box" "HiFi Audio Device" VCR Camcorder "Video Monitor" + "Video Display and Loudspeaker" "Video Conferencing" Reserved + "Gaming/Toy" + } $minor_class] + } elseif {$major_class == 5} { + set major_name Peripheral + set bits [expr {$minor_class >> 4}] + set minor_name [lindex { + Other Keyboard "Pointing device" "Combo keyboard/pointing device" + } $bits] + set bits [expr {$minor_class & 0xf}] + if {$bits < 10} { + append minor_name /[lindex { + Uncategorized Joystick Gamepad "Remote control" "Sensing device" + "Digitizer tabler" "Card reader" "Digital pen" "Handheld scanner" + "Handheld gesture input device" + }] + } + } elseif {$major_class == 6} { + set major_name Imaging + set minor_name {} + if {$minor_class & 4} {lappend minor_name Display} + if {$minor_class & 8} {lappend minor_name Camera} + if {$minor_class & 16} {lappend minor_name Scanner} + if {$minor_class & 32} {lappend minor_name Printer} + set minor_name [join $minor_name /] + } elseif {$major_class == 7} { + set major_name Wearable + set minor_name [lindex { + Wristwatch Pager Jacket Helmet Glasses + } $minor_class] + } elseif {$major_class == 8} { + set major_name Toy + set minor_name [lindex { + Robot Vehicle Doll Controller Game + } $minor_class] + } elseif {$major_class == 9} { + set major_name Health + set minor_name [lindex { + Undefined "Blood Pressure Monitor" Thermometer "Weighing Scale" + "Glucose Meter" "Pulse Oximeter" "Heart/Pulse Rate Monitor" + "Health Data Display" "Step Counter" "Body Composition Analyzer" + "Peak Flow Monitor" "Medication Monitor" "Knee Prosthesis" + "Andle Prosthesis" "Generic Health Manager" "Personal Mobility Device" + } $minor_class] + } elseif {$major_class == 31} { + set major_name Uncategorized + set minor_name "" + } else { + set major_name Reserved + set minor_name "" + } + + set service_class {} + if {$class & (1<<13)} {lappend service_class "Limited Discoverable Mode"} + if {$class & (1<<16)} {lappend service_class "Positioning (Location identification)"} + if {$class & (1<<17)} {lappend service_class "Networking"} + if {$class & (1<<18)} {lappend service_class "Rendering"} + if {$class & (1<<19)} {lappend service_class "Capturing"} + if {$class & (1<<20)} {lappend service_class "Object Transfer"} + if {$class & (1<<21)} {lappend service_class "Audio"} + if {$class & (1<<22)} {lappend service_class "Telephony"} + if {$class & (1<<23)} {lappend service_class "Information"} + + return [dict create MajorClassName $major_name \ + MinorClassName $minor_name \ + DeviceClasses $service_class] +} + +proc iocp::bt::ResolveDeviceUnique {device} { + if {[IsAddress $device]} { + return $device + } + set addrs [device address $device] + if {[llength $addrs] == 0} { + error "Could not resolve Bluetooth device name \"$device\"." + } elseif {[llength $addrs] > 1} { + error "Device \"$device\" resolves to multiple addresses." + } + return [lindex $addrs 0] +} + +package provide iocp_bt $iocp::bt::version diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/btnames.tcl b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl similarity index 96% rename from src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/btnames.tcl rename to src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl index c441c4fa..8b4c96c3 100644 --- a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/btnames.tcl +++ b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl @@ -1,383 +1,383 @@ -# -# Copyright (c) 2020, Ashok P. Nadkarni -# All rights reserved. -# -# See the file LICENSE for license - -namespace eval iocp::bt::names { - namespace path [namespace parent] - # Maps UUID to Service class name - # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ - # UUID may be either 16-bit Bluetooth universal UUID as or the full UUID - # Uses lower case hex. - variable service_class_names { - 1000 ServiceDiscoveryServerServiceClassID - 1001 BrowseGroupDescriptorServiceClassID - 1002 PublicBrowseRoot - 1101 SerialPort - 1102 LANAccessUsingPPP - 1103 DialupNetworking - 1104 IrMCSync - 1105 OBEXObjectPush - 1106 OBEXFileTransfer - 1107 IrMCSyncCommand - 1108 Headset - 1109 CordlessTelephony - 110a AudioSource - 110b AudioSink - 110c A/V_RemoteControlTarget - 110d AdvancedAudioDistribution - 110e A/V_RemoteControl - 110f A/V_RemoteControlController - 1110 Intercom - 1111 Fax - 1112 {Headset - Audio Gateway (AG)} - 1115 PANU - 1116 NAP - 1117 GN - 1118 DirectPrinting - 1119 ReferencePrinting - 111a {Basic Imaging Profile} - 111b ImagingResponder - 111c ImagingAutomaticArchive - 111d ImagingReferencedObjects - 111e Handsfree - 111f HandsfreeAudioGateway - 1120 DirectPrintingReferenceObjectsService - 1121 ReflectedUI - 1122 BasicPrinting - 1123 PrintingStatus - 1124 HumanInterfaceDeviceService - 1125 HardcopyCableReplacement - 1126 HCR_Print - 1127 HCR_Scan - 1128 Common_ISDN_Access - 112d SIM_Access - 112e {Phonebook Access - PCE} - 112f {Phonebook Access - PSE} - 1130 {Phonebook Access} - 1131 {Headset - HS} - 1132 {Message Access Server} - 1133 {Message Notification Server} - 1134 {Message Access Profile} - 1135 GNSS - 1136 GNSS_Server - 1137 {3D Display} - 1138 {3D Glasses} - 1139 {3D Synchronization} - 113a {MPS Profile UUID} - 113b {MPS SC UUID} - 113c {CTN Access Service} - 113d {CTN Notification Service} - 113e {CTN Profile} - 1200 PnPInformation - 1201 GenericNetworking - 1202 GenericFileTransfer - 1203 GenericAudio - 1204 GenericTelephony - 1303 VideoSource - 1304 VideoSink - 1305 VideoDistribution - 1400 HDP - 1401 {HDP Source} - 1402 {HDP Sink} - 02030302-1d19-415f-86f2-22a2106a0a77 {Wireless iAP v2} - } - - # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ - variable protocol_names { - 0001 SDP - 0002 UDP - 0003 RFCOMM - 0004 TCP - 0005 TCS-BIN - 0006 TCS-AT - 0007 ATT - 0008 OBEX - 0009 IP - 000a FTP - 000c HTTP - 000e WSP - 000f BNEP - 0010 UPNP - 0011 HIDP - 0012 HardcopyControlChannel - 0014 HardcopyDataChannel - 0016 HardcopyNotification - 0017 AVCTP - 0019 AVDTP - 001b CMTP - 001e MCAPControlChannel - 001f MCAPDataChannel - 0100 L2CAP - } - - # Universal attribute names - variable attribute_names - set attribute_names { - ServiceRecordHandle 0 - ServiceClassIDList 1 - ServiceRecordState 2 - ServiceID 3 - ProtocolDescriptorList 4 - BrowseGroupList 5 - LanguageBaseAttributeIDList 6 - ServiceInfoTimeToLive 7 - ServiceAvailability 8 - BluetoothProfileDescriptorList 9 - DocumentationURL 10 - ClientExecutableURL 11 - IconURL 12 - AdditionalProtocolDescriptorList 13 - ServiceName 256 - ServiceDescription 257 - ProviderName 258 - } - -} - -proc iocp::bt::names::is_uuid {uuid} { - regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid -} - -proc iocp::bt::names::to_name {uuid} { - # Map a UUID to a name. - # uuid - Bluetooth UUID - # The command attempts to map the UUID as a service name or protocol - # name in that order. - # - # Returns the mapped name or the uuid if no mapping is possible. - - set name [service_class_name $uuid] - if {$name ne $uuid && ![is_uuid $name]} { - return $name - } - return [protocol_name $uuid] -} - -proc iocp::bt::names::to_uuid {name_or_uuid} { - # Map a name to a UUID. - # name_or_uuid - Bluetooth UUID or name - # The command attempts to map the name as a service class name or protocol - # name in that order. - # - # Returns the mapped uuid raises an error if no mapping is possible. - variable service_class_names - if {[MapNameToUuidV $name_or_uuid service_class_names name]} { - return $name - } - return [protocol_uuid $name_or_uuid] -} - - -proc iocp::bt::names::service_class_name {uuid} { - # Maps a UUID to a service class name. - # uuid - UUID to be mapped - # Returns a human-readable name or the UUID itself - # if no mapping could be performed. - variable service_class_names - set name [MapUuidToName $uuid service_class_names] - if {$name ne $uuid} { - return $name - } - # Well known private uuids - if {[string equal -nocase "02030302-1d19-415f-86f2-22a2106a0a77" $uuid]} { - return "Wireless iAP v2"; # iPod Accessory Protocol - } - return $uuid -} - -proc iocp::bt::names::service_class_uuid {name} { - # Maps a service class name to a UUID. - # name - Service class name to be mapped. - # The command raises an error if the name could not be mapped to - # a UUID. - # - # Returns the UUID corresponding to the name. - variable service_class_names - return [MapNameToUuid $name service_class_names] -} - -proc iocp::bt::names::profile_name {uuid} { - # Maps a UUID to a Bluetooth profile name. - # uuid - UUID to be mapped - # Returns a human-readable name or the UUID itself - # if no mapping could be performed. - - # Profiles seem to come from same service class UUID space. - return [service_class_name $uuid] -} - -proc iocp::bt::names::protocol_name {uuid} { - # Maps a UUID to a protocol name. - # uuid - UUID to be mapped - # Returns a human-readable name or the UUID itself - # if no mapping could be performed. - variable protocol_names - return [MapUuidToName $uuid protocol_names] -} - -proc iocp::bt::names::protocol_uuid {name} { - # Maps a protocol name to a UUID. - # name - Protocol name to be mapped. - # The command raises an error if the name could not be mapped to - # a UUID. - # - # Returns the UUID corresponding to the name. - variable protocol_names - return [MapNameToUuid $name protocol_names] -} - -proc iocp::bt::names::attribute_name {attr_id} { - # Maps a attribute id (numeric or name) to a attribute name. - # attr_id - numeric attribute identifier - # Returns a human-readable name for the attribute or - # the numeric id itself if no mapping could be performed. - variable attribute_names - dict for {name id} $attribute_names { - if {$attr_id == $id || [string equal -nocase $name $attr_id]} { - return $name - } - } - if {[string is integer -strict $attr_id]} { - return $attr_id - } - error "Unknown attribute \"$attr_id\"." -} - -proc iocp::bt::names::attribute_id {name} { - # Maps a attribute name to it numeric attribute identifier. - # name - Attribute name to be mapped - # The command will raise an error if the name is unknown. - # Returns the numeric attribute identifier corresponding to - # the passed name. - variable attribute_names - if {[string is integer -strict $name]} { - return $name - } - return [dict get $attribute_names $name] -} - -proc iocp::bt::names::print {} { - # Prints known UUID's and their mapped mnemonics. - variable service_class_names - dict for {uuid16 name} $service_class_names { - puts "[Uuid16 $uuid16] $name" - } -} - -proc iocp::bt::names::MapUuidToName {uuid dictvar} { - upvar 1 $dictvar names - - # uuid may be a full UUID or a 16-bit Bluetooth UUID - if {[IsUuid $uuid]} { - # See if the exact match exists - set uuid [string tolower $uuid] - if {[dict exists $names $uuid]} { - return [dict get $names $uuid] - } - # Nope. Now see if it is a 16-bit Bluetooth UUID - if {[IsBluetoothUuid $uuid] && [string range $uuid 0 3] eq "0000"} { - # 16-bit UUID - set uuid16 [string tolower [string range $uuid 4 7]] - if {[dict exists $names $uuid16]} { - return [dict get $names $uuid16] - } - } - # Nope. Cannot be mapped. Return the UUID. - return $uuid - } - - # Not a full UUID. Retry as a 16-but UUID. tailcall so upvar works right. - tailcall MapUuidToName [Uuid16 $uuid] $dictvar -} - -proc iocp::bt::names::MapNameToUuidV {name dictvar resultvar} { - upvar 1 $dictvar names - upvar 1 $resultvar result - - if {[IsUuid $name]} { - set result $name - return 1 - } - dict for {uuid mapped_name} $names { - if {[string equal -nocase $name $mapped_name]} { - if {[string length $uuid] == 4} { - # 16-bit UUID -> full UUID - set result "0000${uuid}-0000-1000-8000-00805f9b34fb" - } else { - set result $uuid - } - return 1 - } - } - return 0 -} - -proc iocp::bt::names::MapNameToUuid {name dictvar} { - upvar 1 $dictvar names - if {[MapNameToUuidV $name names result]} { - return $result - } - error "Name \"$name\" could not be mapped to a UUID" -} - -proc iocp::bt::names::Asciify {svar} { - # Replaces non-ascii or non-printable chars with Tcl sequences - # - # Transformed string is stored back in svar. - - upvar 1 $svar s - set ascii "" - set was_escaped 0 - # Split can be expensive for large strings so string index - set len [string length $s] - for {set i 0} {$i < $len} {incr i} { - set char [string index $s $i] - scan $char %c codepoint - if {$codepoint < 32 || $codepoint >= 126} { - append ascii "\\u[format %.4x $codepoint]" - set was_escaped 1 - } else { - append ascii $char - } - } - set s $ascii - return $was_escaped -} - -proc iocp::bt::names::ClipSpec {{use_list 0}} { - package require twapi - - set csv [twapi::read_clipboard_text] - if {$use_list} { - set tcl "\[list \n" - } else { - set tcl "\{ \n" - } - foreach line [split $csv \n] { - lassign [split $line \t] name uuid description - set name [string trim $name] - set uuid [string trim $uuid] - if {![regexp {^0x[[:xdigit:]]{4}} $uuid]} continue - if {$use_list} { - # Unicode chars, needs escaping - Asciify name - append tcl " " [list [string range $uuid 2 end]] " " "\"$name\"" " \\" \n - } else { - if {[Asciify name]} { - # Need to use the list command. Just restart - return [ScrapeBTSpec 1] - } - append tcl " " [string range $uuid 2 end] " " "{$name}" \n - } - } - if {$use_list} { - append tcl "\]\n" - } else { - append tcl "\}" - } - - twapi::write_clipboard_text $tcl -} - +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::names { + namespace path [namespace parent] + # Maps UUID to Service class name + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + # UUID may be either 16-bit Bluetooth universal UUID as or the full UUID + # Uses lower case hex. + variable service_class_names { + 1000 ServiceDiscoveryServerServiceClassID + 1001 BrowseGroupDescriptorServiceClassID + 1002 PublicBrowseRoot + 1101 SerialPort + 1102 LANAccessUsingPPP + 1103 DialupNetworking + 1104 IrMCSync + 1105 OBEXObjectPush + 1106 OBEXFileTransfer + 1107 IrMCSyncCommand + 1108 Headset + 1109 CordlessTelephony + 110a AudioSource + 110b AudioSink + 110c A/V_RemoteControlTarget + 110d AdvancedAudioDistribution + 110e A/V_RemoteControl + 110f A/V_RemoteControlController + 1110 Intercom + 1111 Fax + 1112 {Headset - Audio Gateway (AG)} + 1115 PANU + 1116 NAP + 1117 GN + 1118 DirectPrinting + 1119 ReferencePrinting + 111a {Basic Imaging Profile} + 111b ImagingResponder + 111c ImagingAutomaticArchive + 111d ImagingReferencedObjects + 111e Handsfree + 111f HandsfreeAudioGateway + 1120 DirectPrintingReferenceObjectsService + 1121 ReflectedUI + 1122 BasicPrinting + 1123 PrintingStatus + 1124 HumanInterfaceDeviceService + 1125 HardcopyCableReplacement + 1126 HCR_Print + 1127 HCR_Scan + 1128 Common_ISDN_Access + 112d SIM_Access + 112e {Phonebook Access - PCE} + 112f {Phonebook Access - PSE} + 1130 {Phonebook Access} + 1131 {Headset - HS} + 1132 {Message Access Server} + 1133 {Message Notification Server} + 1134 {Message Access Profile} + 1135 GNSS + 1136 GNSS_Server + 1137 {3D Display} + 1138 {3D Glasses} + 1139 {3D Synchronization} + 113a {MPS Profile UUID} + 113b {MPS SC UUID} + 113c {CTN Access Service} + 113d {CTN Notification Service} + 113e {CTN Profile} + 1200 PnPInformation + 1201 GenericNetworking + 1202 GenericFileTransfer + 1203 GenericAudio + 1204 GenericTelephony + 1303 VideoSource + 1304 VideoSink + 1305 VideoDistribution + 1400 HDP + 1401 {HDP Source} + 1402 {HDP Sink} + 02030302-1d19-415f-86f2-22a2106a0a77 {Wireless iAP v2} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + variable protocol_names { + 0001 SDP + 0002 UDP + 0003 RFCOMM + 0004 TCP + 0005 TCS-BIN + 0006 TCS-AT + 0007 ATT + 0008 OBEX + 0009 IP + 000a FTP + 000c HTTP + 000e WSP + 000f BNEP + 0010 UPNP + 0011 HIDP + 0012 HardcopyControlChannel + 0014 HardcopyDataChannel + 0016 HardcopyNotification + 0017 AVCTP + 0019 AVDTP + 001b CMTP + 001e MCAPControlChannel + 001f MCAPDataChannel + 0100 L2CAP + } + + # Universal attribute names + variable attribute_names + set attribute_names { + ServiceRecordHandle 0 + ServiceClassIDList 1 + ServiceRecordState 2 + ServiceID 3 + ProtocolDescriptorList 4 + BrowseGroupList 5 + LanguageBaseAttributeIDList 6 + ServiceInfoTimeToLive 7 + ServiceAvailability 8 + BluetoothProfileDescriptorList 9 + DocumentationURL 10 + ClientExecutableURL 11 + IconURL 12 + AdditionalProtocolDescriptorList 13 + ServiceName 256 + ServiceDescription 257 + ProviderName 258 + } + +} + +proc iocp::bt::names::is_uuid {uuid} { + regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid +} + +proc iocp::bt::names::to_name {uuid} { + # Map a UUID to a name. + # uuid - Bluetooth UUID + # The command attempts to map the UUID as a service name or protocol + # name in that order. + # + # Returns the mapped name or the uuid if no mapping is possible. + + set name [service_class_name $uuid] + if {$name ne $uuid && ![is_uuid $name]} { + return $name + } + return [protocol_name $uuid] +} + +proc iocp::bt::names::to_uuid {name_or_uuid} { + # Map a name to a UUID. + # name_or_uuid - Bluetooth UUID or name + # The command attempts to map the name as a service class name or protocol + # name in that order. + # + # Returns the mapped uuid raises an error if no mapping is possible. + variable service_class_names + if {[MapNameToUuidV $name_or_uuid service_class_names name]} { + return $name + } + return [protocol_uuid $name_or_uuid] +} + + +proc iocp::bt::names::service_class_name {uuid} { + # Maps a UUID to a service class name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable service_class_names + set name [MapUuidToName $uuid service_class_names] + if {$name ne $uuid} { + return $name + } + # Well known private uuids + if {[string equal -nocase "02030302-1d19-415f-86f2-22a2106a0a77" $uuid]} { + return "Wireless iAP v2"; # iPod Accessory Protocol + } + return $uuid +} + +proc iocp::bt::names::service_class_uuid {name} { + # Maps a service class name to a UUID. + # name - Service class name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable service_class_names + return [MapNameToUuid $name service_class_names] +} + +proc iocp::bt::names::profile_name {uuid} { + # Maps a UUID to a Bluetooth profile name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + + # Profiles seem to come from same service class UUID space. + return [service_class_name $uuid] +} + +proc iocp::bt::names::protocol_name {uuid} { + # Maps a UUID to a protocol name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable protocol_names + return [MapUuidToName $uuid protocol_names] +} + +proc iocp::bt::names::protocol_uuid {name} { + # Maps a protocol name to a UUID. + # name - Protocol name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable protocol_names + return [MapNameToUuid $name protocol_names] +} + +proc iocp::bt::names::attribute_name {attr_id} { + # Maps a attribute id (numeric or name) to a attribute name. + # attr_id - numeric attribute identifier + # Returns a human-readable name for the attribute or + # the numeric id itself if no mapping could be performed. + variable attribute_names + dict for {name id} $attribute_names { + if {$attr_id == $id || [string equal -nocase $name $attr_id]} { + return $name + } + } + if {[string is integer -strict $attr_id]} { + return $attr_id + } + error "Unknown attribute \"$attr_id\"." +} + +proc iocp::bt::names::attribute_id {name} { + # Maps a attribute name to it numeric attribute identifier. + # name - Attribute name to be mapped + # The command will raise an error if the name is unknown. + # Returns the numeric attribute identifier corresponding to + # the passed name. + variable attribute_names + if {[string is integer -strict $name]} { + return $name + } + return [dict get $attribute_names $name] +} + +proc iocp::bt::names::print {} { + # Prints known UUID's and their mapped mnemonics. + variable service_class_names + dict for {uuid16 name} $service_class_names { + puts "[Uuid16 $uuid16] $name" + } +} + +proc iocp::bt::names::MapUuidToName {uuid dictvar} { + upvar 1 $dictvar names + + # uuid may be a full UUID or a 16-bit Bluetooth UUID + if {[IsUuid $uuid]} { + # See if the exact match exists + set uuid [string tolower $uuid] + if {[dict exists $names $uuid]} { + return [dict get $names $uuid] + } + # Nope. Now see if it is a 16-bit Bluetooth UUID + if {[IsBluetoothUuid $uuid] && [string range $uuid 0 3] eq "0000"} { + # 16-bit UUID + set uuid16 [string tolower [string range $uuid 4 7]] + if {[dict exists $names $uuid16]} { + return [dict get $names $uuid16] + } + } + # Nope. Cannot be mapped. Return the UUID. + return $uuid + } + + # Not a full UUID. Retry as a 16-but UUID. tailcall so upvar works right. + tailcall MapUuidToName [Uuid16 $uuid] $dictvar +} + +proc iocp::bt::names::MapNameToUuidV {name dictvar resultvar} { + upvar 1 $dictvar names + upvar 1 $resultvar result + + if {[IsUuid $name]} { + set result $name + return 1 + } + dict for {uuid mapped_name} $names { + if {[string equal -nocase $name $mapped_name]} { + if {[string length $uuid] == 4} { + # 16-bit UUID -> full UUID + set result "0000${uuid}-0000-1000-8000-00805f9b34fb" + } else { + set result $uuid + } + return 1 + } + } + return 0 +} + +proc iocp::bt::names::MapNameToUuid {name dictvar} { + upvar 1 $dictvar names + if {[MapNameToUuidV $name names result]} { + return $result + } + error "Name \"$name\" could not be mapped to a UUID" +} + +proc iocp::bt::names::Asciify {svar} { + # Replaces non-ascii or non-printable chars with Tcl sequences + # + # Transformed string is stored back in svar. + + upvar 1 $svar s + set ascii "" + set was_escaped 0 + # Split can be expensive for large strings so string index + set len [string length $s] + for {set i 0} {$i < $len} {incr i} { + set char [string index $s $i] + scan $char %c codepoint + if {$codepoint < 32 || $codepoint >= 126} { + append ascii "\\u[format %.4x $codepoint]" + set was_escaped 1 + } else { + append ascii $char + } + } + set s $ascii + return $was_escaped +} + +proc iocp::bt::names::ClipSpec {{use_list 0}} { + package require twapi + + set csv [twapi::read_clipboard_text] + if {$use_list} { + set tcl "\[list \n" + } else { + set tcl "\{ \n" + } + foreach line [split $csv \n] { + lassign [split $line \t] name uuid description + set name [string trim $name] + set uuid [string trim $uuid] + if {![regexp {^0x[[:xdigit:]]{4}} $uuid]} continue + if {$use_list} { + # Unicode chars, needs escaping + Asciify name + append tcl " " [list [string range $uuid 2 end]] " " "\"$name\"" " \\" \n + } else { + if {[Asciify name]} { + # Need to use the list command. Just restart + return [ScrapeBTSpec 1] + } + append tcl " " [string range $uuid 2 end] " " "{$name}" \n + } + } + if {$use_list} { + append tcl "\]\n" + } else { + append tcl "\}" + } + + twapi::write_clipboard_text $tcl +} + diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/btsdr.tcl b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl similarity index 97% rename from src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/btsdr.tcl rename to src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl index e42cc462..cd014a1b 100644 --- a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/btsdr.tcl +++ b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl @@ -1,962 +1,962 @@ -# -# Copyright (c) 2020, Ashok P. Nadkarni -# All rights reserved. -# -# See the file LICENSE for license - -namespace eval iocp::bt::sdr { - namespace path [namespace parent] - namespace export attribute attributes decode print printn - - namespace eval attribute { - namespace path [list [namespace parent] [namespace parent [namespace parent]]] - namespace export exists get raw text - namespace ensemble create - } -} - -proc iocp::bt::sdr::decode {binsdr} { - # Decodes a binary service discovery record - # binsdr - a raw Bluetooth service discovery record in binary - # form. - # - # The returned value should be treated as opaque. The attributes - # stored in the record should be accessed with the commands in - # the `sdr` namespace. - # - # Returns a container of attributes stored in the record. - - # A SDR record is a single data element which is a sequence - # containing nested data elements. - # {{sequence {nested data elements}}} - set rec [lindex [DecodeElements $binsdr] 0 1] - - # $rec contains alternating attribute value pairs. The result - # is built as a list as its faster to do that and shimmer - # to a dict on access than to build a dictionary to begin with. - set sdr {} - foreach {attr val} $rec { - lappend sdr [lindex $attr 1] $val - } - return $sdr -} - -proc iocp::bt::sdr::attribute::exists {sdr attr_id {varname {}}} { - # Checks if an attribute exists in a service discovery record - # sdr - a decoded service discovery record in the form returned by - # [decode]. - # attr_id - attribute integer id or Bluetooth universal attribute name - # varname - If not the empty string, the attribute - # value is stored in a variable of this name in the - # caller's context. - # - # The value returned in $varname is the attribute type-independent data - # value. To get the value formatted for the attribute type, use the - # [attribute get] command. - # - # Returns 1 if the attribute exists, and 0 otherwise. - - if {[string is integer -strict $attr_id]} { - set key [expr {$attr_id + 0}]; # Force decimal rep. Faster than format - } else { - set key [names::attribute_id $attr_id]; # name -> id - } - - if {[dict exists $sdr $key]} { - if {$varname ne ""} { - upvar 1 $varname value - set value [dict get $sdr $key] - } - return 1 - } - - return 0 -} - -proc iocp::bt::sdr::attributes {sdr} { - # Get the list of attributes in a service discovery record - # sdr - a decoded service discovery record in the form returned by - # [decode]. - # Returns a list of numeric attribute ids. - return [dict keys $sdr] -} - -proc iocp::bt::sdr::attribute::raw {sdr attr_id} { - # Get an attribute value from an service discovery record. - # sdr - a decoded service discovery record in the form returned by - # sdr_decode. - # attr_id - attribute integer id - # - # The command will raise an error if the attribute does not exist - # in the sdr. - # - # Returns the attribute value as a pair consisting of the type and - # the raw data element value. - - if {[exists $sdr $attr_id value]} { - return $value - } else { - error "Attribute with id \"$attr_id\" not found." - } -} - -proc iocp::bt::sdr::attribute::get {sdr attr_id {varname {}}} { - # Get the value of an universal attribute from a service - # discovery record. - # sdr - Decoded service discovery record. - # attr_id - Universal attribute name or numeric identifier. - # varname - Optional name of variable in caller's context. - # - # If $varname is not the empty string, it should be the name of - # a variable in the caller's context. - # - If the attribute is present, the command returns `1` and - # stores the attribute value in the variable. - # - If the attribute is not present, the command returns 0. - # - # If $varname is an empty string, - # - If the attribute is present, the command returns its value. - # - If the attribute is not present, the command raises an error. - # - # The attribute value is decoded into a attribute type-dependent format. - # - # The $attr_id argument must be one of those defined in - # the *Universal Attributes* section in the - # [Bluetooth Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/) - # specification. Otherwise an error is raised. The recognized attributes - # are listed below. Refer to the Bluetooth specification for their - # exact semantics. - # - # AdditionalProtocolDescriptorList - List of protocol stacks. This - # supplements the `ProtocolDescriptorList` attribute. - # BluetoothProfileDescriptorList - List of Bluetooth profile descriptors - # to which the service conforms. Each element is a dictionary - # with keys `Uuid`, `Name`, `MajorVersion` and `MinorVersion` - # corresponding to the referenced profile. - # BrowseGroupList - List of browse group descriptors representing - # the browse groups to which the service record belongs. Each - # descriptor is dictionary with keys `Uuid` and `Name` identifying - # the a browse group. - # ClientExecutableURL - URL pointing to the client application that - # may be used to utilize the service. - # DocumentationURL - URL for the service documentation. - # IconURL - URL for icon that may be used to represent the service. - # LanguageBaseAttributeIDList - Nested dictionary of language-specific - # attribute offsets (see below). - # ProtocolDescriptorList - List of protocol stacks (see below) that may - # be used to access the service described by the service record. - # ProviderName - Name of entity providing the service in the primary - # language of the service record. - # ServiceAvailability - A value in the range 0-255 that indicates - # relative availability of the service in terms of the number of - # clients it can accept. Note this is a scaled estimate. - # ServiceClassIDList - List with each element being a dictionary - # corresponding to a service class that the record conforms to. - # The keys of the dictionary are `Name` and `Uuid`. - # ServiceDescription - A description of the service in the primary - # language of the record. - # ServiceID - UUID that uniquely identifies a service instance. - # ServiceInfoTimeToLive - The number of seconds from the time it was - # generated that the service record information is expected to be valid. - # ServiceName - The human-readable name of the service in the primary - # language of the record. - # ServiceRecordHandle - A numeric value that uniquely identifies a service - # record within a SDP server. - # ServiceRecordState - Numeric value that is changed any time the service - # record is modified by the SDP server. - # - # In the case of ProtocolDescriptorList and AdditionalProtocolDescriptorList, - # the attribute value is in the form of a list each element of - # which describes a protocol stack that may be used to access the - # service. Each such element is itself a list whose elements correspond - # to layers in that protocol stack starting with the lowest layer first. - # Each layer is described as a dictionary with the following keys: - # Uuid - the UUID of the protocol for the layer - # Name - the name of the protocol - # Params - the protocol parameters. This is a list of raw data elements - # of the form `{type value}` the interpretation of which is - # protocol dependent. - # - # In the case of LanguageBaseAttributeIDList, the attribute value is - # a dictionary indexed by language - # identifiers as defined in ISO639, e.g. `en`, `fr` etc. - # The corresponding value is itself a dictionary with keys `BaseOffset` - # and `Encoding`. See the Bluetooth specification for the former. - # The latter is either a Tcl encoding name or in case the encoding - # is not supported by Tcl, an integer value that identifies an encoding - # as per the Bluetooth specification. - # - # Returns a boolean or the decoded attribute value. - - set attr_name [names::attribute_name $attr_id] - set attr_id [names::attribute_id $attr_id] - switch -exact -- $attr_name { - BrowseGroupList - - ServiceClassIDList { - tailcall Uuids $sdr $attr_id $varname - } - BluetoothProfileDescriptorList - - ProtocolDescriptorList - - AdditionalProtocolDescriptorList - - LanguageBaseAttributeIDList { - tailcall $attr_name $sdr $varname - } - DocumentationURL - - ClientExecutableURL - - IconURL - - ServiceAvailability - - ServiceInfoTimeToLive - - ServiceID - - ServiceRecordState - - ServiceRecordHandle { - tailcall AttributeValue $sdr $attr_id $varname - } - ServiceName - - ServiceDescription - - ProviderName { - # Special case because they need a language argument - tailcall text $sdr $attr_name primary $varname - } - default { - if {[llength [info commands [namespace current]::universal::$attr_name]]} { - tailcall [namespace current]::universal::$attr_name $sdr $varname - } - } - } - error "Unknown universal attribute \"$attr_id\"." -} - -proc iocp::bt::sdr::attribute::text {sdr attr_id lang {varname {}}} { - # Get the value of an text attribute in the specified language - # from a service discovery record. - # sdr - Decoded service discovery record. - # attr_id - Universal attribute name or numeric identifier. - # lang - language identifier as specified in iso639, e.g. `en` for - # english, `fr` for french etc. or the keyword `primary`. - # varname - Name of variable in caller's context. - # - # If $varname is not the empty string, it should be the name of - # a variable in the caller's context. - # - If the attribute is present, the command returns `1` and - # stores the attribute value in the variable. - # - If the attribute is not present, the command returns 0. - # - # If $varname is an empty string, - # - If the attribute is present, the command returns its value. - # - If the attribute is not present, the command raises an error. - # - # If the record does not contain a value for the specified language, - # the value for the primary language will be retrieved. - # - # Returns a boolean or the attribute text value. - # - set lang_offset [names::attribute_id $attr_id] - if {$lang_offset < 256 || $lang_offset >= 512} { - error "Invalid text attribute id \"$attr_id\"." - } - incr lang_offset -256 - tailcall TextAttribute $sdr $lang_offset $lang $varname -} - - -proc iocp::bt::sdr::attribute::LanguageBaseAttributeIDList {sdr {varname {}}} { - # Retrieve the language base attributes in a service discovery record. - # sdr - decoded service discovery record - # varname - optional name of variable in caller's context. - # - # The retrieved value is a nested dictionary - # corresponding to the `LanguageBaseAttributeIDList` - # universal attribute in the Bluetooth Core specification. - # - # If $varname is not the empty string, it should be the name of - # a variable in the caller's context. - # - If the attribute is present, the command returns `1` and - # stores the attribute value in the variable. - # - If the attribute is not present, the command returns 0. - # - # If $varname is an empty string, - # - If the attribute is present, the command returns its value. - # - If the attribute is not present, the command raises an error. - # - # The dictionary value returned or stored is indexed by language - # identifiers as defined in ISO639, e.g. `en`, `fr` etc. - # The corresponding value is itself a dictionary with keys `BaseOffset` - # and `Encoding`. See the Bluetooth specification for the former. - # The latter is either a Tcl encoding name or in case the encoding - # is not supported by Tcl, an integer value that identifies an encoding - # as per the Bluetooth specification. fs - # - # Returns a boolean or a dictionary of language attributes. - if {![AttributeValue $sdr 6 sequence]} { - if {$varname eq ""} { - error "Attribute not found." - } else { - return 0 - } - } - - # attr is {sequence {langid encoding baseoffset ...}} - set langs [dict create] - foreach {langid enc offset} $sequence { - # langid is ISO 639 language code - set langid [lindex $langid 1] - set langid "[format %c [expr {$langid >> 8}]][format %c [expr {$langid &0xff}]]" - dict set langs $langid Encoding [::iocp::bt::EncodingTclName [lindex $enc 1]] - dict set langs $langid BaseOffset [lindex $offset 1] - } - if {$varname eq ""} { - return $langs - } else { - upvar 1 $varname var - set var $langs - return 1 - } -} - - -proc iocp::bt::sdr::attribute::ProtocolDescriptorList {sdr {varname {}}} { - # Retrieve the protocol information from a service discovery record. - # sdr - decoded service discovery record - # varname - optional name of variable in caller's context. - # - # Information about the protocols that may be used to access the - # service described by SDR is stored in the ProtocolDescriptorList - # universal attribute as described in the Bluetooth Core specification. - # - # If $varname is not the empty string, it should be the name of - # a variable in the caller's context. - # - If the attribute is present, the command returns `1` and - # stores the attribute value in the variable. - # - If the attribute is not present, the command returns 0. - # - # If $varname is an empty string, - # - If the attribute is present, the command returns its value. - # - If the attribute is not present, the command raises an error. - # - # The returned information is in the form of a list each element of - # which describes a protocol stack that may be used to access the - # service. Each such element is itself a list whose elements correspond - # to layers in that protocol stack starting with the lowest layer first. - # Each layer is described as a dictionary with the following keys: - # Uuid - the UUID of the protocol for the layer - # Name - the name of the protocol - # Params - the protocol parameters. This is a list of raw data elements - # of the form `{type value}` the interpretation of which is - # protocol dependent. - # - # Returns a boolean or the list of protocols. - - if {! [attribute exists $sdr 4 attrval]} { - if {$varname eq ""} { - error "Attribute not found." - } else { - return 0 - } - } - - if {$varname eq ""} { - return [Protocols $attrval] - } else { - upvar 1 $varname var - set var [Protocols $attrval] - return 1 - } -} - -proc iocp::bt::sdr::attribute::BluetoothProfileDescriptorList {sdr {varname {}}} { - # Retrieve the service classes in a service discovery record. - # sdr - decoded service discovery record - # varname - optional name of variable in caller's context. - # - # The retrieved value is a list of profile descriptors - # corresponding to the BluetoothProfileDescriptorList - # universal attribute in the Bluetooth Core specification. - # Each descriptor is a dictionary with the following keys: - # `Uuid` - the UUID of the profile - # `Name` - the mnemonic name of the profile or its UUID if the - # UUID cannot be mapped to a name - # `MajorVersion` - the profile's major version - # `MinorVersion` - the profile's minor version - # - # If $varname is not the empty string, it should be the name of - # a variable in the caller's context. - # - If the attribute is present, the command returns `1` and - # stores the attribute value in the variable. - # - If the attribute is not present, the command returns 0. - # - # If $varname is an empty string, - # - If the attribute is present, the command returns its value. - # - If the attribute is not present, the command raises an error. - # - # Returns a boolean or the list of service class descriptors. - - if {! [attribute exists $sdr 9 attr]} { - if {$varname eq ""} { - error "Attribute not found." - } else { - return 0 - } - } - - # Sequence of profile descriptors. Each profile descriptor - # is sequence of profile uuid and version. - set profiles {} - foreach profile [lindex $attr 1] { - set uuid [lindex $profile 1 0 1] - set version [lindex $profile 1 1 1] - lappend profiles [dict create \ - Uuid $uuid \ - Name [iocp::bt::names::to_name $uuid] \ - MajorVersion [expr {$version >> 8}] \ - MinorVersion [expr {$version & 0xff}] - ] - } - - if {$varname eq ""} { - return $profiles - } else { - upvar 1 $varname var - set var $profiles - return 1 - } - -} - -proc iocp::bt::sdr::attribute::AdditionalProtocolDescriptorList {sdr {varname {}}} { - # Returns protocol information from the additional protocols field - # in a service discovery record. - # sdr - decoded service discovery record - # varname - optional name of variable in caller's context. - # This command is identical to [protocols] except it extracts - # protocol information from the `AdditionalProtocolDescriptorList` - # attribute instead of `ProtocolDescriptorList`. - - if {! [attribute exists $sdr 13 attrval]} { - if {$varname eq ""} { - error "Attribute not found." - } else { - return 0 - } - } - - # AdditionalProtocolDescriptorList is a sequence each element of which - # is formatted like a ProtocolDescriptorList - # assert [lindex $attrval 0] eq "sequence" - set protocols [lmap elem [lindex $attrval 1] { - Protocols $elem - }] - - if {$varname eq ""} { - return $protocols - } else { - upvar 1 $varname var - set var $protocols - return 1 - } -} - - -proc iocp::bt::sdr::Uuids {sdr attr_id {varname {}}} { - if {! [attribute exists $sdr $attr_id attrval]} { - if {$varname eq ""} { - error "Attribute not found." - } else { - return 0 - } - } - - set uuids [lmap elem [lindex $attrval 1] { - set uuid [lindex $elem 1] - dict create Uuid $uuid Name [names::to_name $uuid] - }] - - if {$varname eq ""} { - return $uuids - } else { - upvar 1 $varname var - set var $uuids - return 1 - } -} - -proc iocp::bt::sdr::Protocols {attrval} { - # The attribute value may either be a single protocol stack encoded - # as a sequence, or multiple stacks encoded as a selection of sequences. - if {[lindex $attrval 0] eq "sequence"} { - # Single protocol stack - set stacks [list [lindex $attrval 1]] - } else { - # selection - Multiple stacks - set stacks [lindex $attrval 1] - } - - set protocols [lmap stack $stacks { - # stack -> {protocol_descriptor protocol_descriptor ...} - # protocol_descriptor -> {sequence {data_element ...}} - lmap protocol_descriptor $stack { - # Each descriptor is a sequence, the first element identifying the - # protocol and the remaining being (optional) parameters. - set elements [lindex $protocol_descriptor 1] - set protocol_uuid [lindex $elements 0 1] - dict create \ - Protocol $protocol_uuid \ - ProtocolName [iocp::bt::names::protocol_name $protocol_uuid] \ - ProtocolParams [lrange $elements 1 end] - } - }] - - return $protocols -} - -proc iocp::bt::sdr::AttributeValue {sdr attr_id {varname {}}} { - if {$varname eq ""} { - return [lindex [attribute raw $sdr $attr_id] 1] - } else { - if {[attribute exists $sdr $attr_id attrval]} { - upvar 1 $varname value - set value [lindex $attrval 1] - return 1 - } else { - return 0 - } - } -} - -proc iocp::bt::sdr::TextAttribute {sdr attr_offset lang {varname {}}} { - - # Assume that if sdr does not explicitly specify encoding, it is UTF8 - set enc utf-8 - set base_offset 256 - - # Primary language's ServiceName attribute index always 256 - # Otherwise get the offset from the base index 256 - if {$lang ne "primary"} { - # See if the specified language has an entry in the languages table - if {[attribute get $sdr LanguageBaseAttributeIDList base_offsets] && - [dict exists $base_offsets $lang]} { - # Yes, so the language-specific text will be at an offset - # from the base attribute index - set base_offset [dict get $base_offsets $lang BaseOffset] - set enc [dict get $base_offsets $lang Encoding] - } - } - set attr_index [expr {$base_offset + $attr_offset}] - if {! [attribute exists $sdr $attr_index name]} { - # This language does not exist. Return primary language if possible. - # If base_offset was 256, that's already primary so don't retry - if {$base_offset == 256 || - ![attribute exists $sdr [expr {256 + $attr_offset}] name] } { - if {$varname ne ""} { - return 0 - } else { - error "Attribute not found in SDR." - } - } - } - - set name [lindex $name 1] - - # $name is encoded. If Tcl supports the encoding, well and good. - # Else return a hex form - if {[string is integer $enc]} { - # SDR specified an encoding but not one that Tcl knows about - set text [binary encode hex $name] - } else { - # The string may be null terminated. TBD - always? - if {[string index $name end] eq "\0"} { - set name [string range $name 0 end-1] - } - set text [encoding convertfrom $enc $name] - } - - if {$varname ne ""} { - upvar 1 $varname var - set var $text - return 1 - } else { - return $text - } -} - -proc iocp::bt::sdr::DecodeElements {delem} { - # Decodes a Data Element as defined in the Bluetooth specification. - # delem - Data element in binary format - - set decoded {} - while {[string length $delem]} { - lassign [ExtractFirstElement $delem] type value delem - switch -exact -- $type { - sequence { lappend decoded [list sequence [DecodeElements $value]]} - selection { lappend decoded [list selection [DecodeElements $value]]} - default { - lappend decoded [list $type $value] - } - } - } - return $decoded -} - -proc iocp::bt::sdr::ExtractFirstElement {bin} { - # Extracts leading element from an SDP record. - # bin - tail of SDP record - # Returns a pair consisting of the extracted element and - # the remaining part of $bin. - - set binlen [string length $bin] - if {$binlen == 0} { - return {} - } - binary scan $bin c header - set type [expr {($header >> 3) & 0xff}] - set lenbits [expr {$header & 0x7}] - - # The nil type is formatted slightly differently. Treat as a special case. - if {$type == 0} { - if {$lenbits != 0} { - error "Invalid non-0 length for nil type." - } - return [list nil "" [string range $bin 1 end]] - } - - if {$lenbits < 5} { - set len [expr {1 << $lenbits}] - set dataoffset 1 - } else { - if {$lenbits == 5} { - set dataoffset 2 - set fmt c - set mask 0xff - } elseif {$lenbits == 6} { - set dataoffset 3 - set fmt S - set mask 0xffff - } else { - # lenbits == 7 - set dataoffset 5 - set fmt I - set mask 0xffffffff - } - # Ensure input length enough for at least length field - if {$binlen < $dataoffset} { - error "Truncated binary header." - } - binary scan $bin @1$fmt len - set len [expr {$len & $mask}]; # Since binary scan is signed - } - if {$binlen < ($dataoffset+$len)} { - error "Truncated binary data." - } - set elem [string range $bin $dataoffset [expr {$dataoffset+$len-1}]] - switch -exact $type { - 1 { - set type uint - if {$len == 1} { - binary scan $elem cu elem - } elseif {$len == 2} { - binary scan $elem Su elem - } elseif {$len == 4} { - binary scan $elem Iu elem - } elseif {$len == 8} { - binary scan $elem Wu elem - } elseif {$len == 16} { - binary scan $elem WuWu hi lo - # TBD - check this - # (2**64)*hi + lo - set elem [expr {(18446744073709551616 * $hi) + $lo}] - } else { - error "Invalid integer width." - } - } - 2 { - set type int - if {$len == 1} { - binary scan $elem c elem - } elseif {$len == 2} { - binary scan $elem S elem - } elseif {$len == 4} { - binary scan $elem I elem - } elseif {$len == 8} { - binary scan $elem W elem - } elseif {$len == 16} { - # Note hi is signed, lo unsigned - binary scan $elem WWu hi lo - # TBD - check this - # (2**64)*hi + lo - set elem [expr {(18446744073709551616 * $hi) + $lo}] - } else { - error "Invalid integer width." - } - } - 3 { - set type uuid - if {$len == 2} { - set elem [binary encode hex $elem] - set elem "0000$elem-0000-1000-8000-00805f9b34fb" - } elseif {$len == 4} { - set elem [binary encode hex $elem] - set elem "$elem-0000-1000-8000-00805f9b34fb" - } elseif {$len == 16} { - set hex [binary encode hex $elem] - set elem [string range $hex 0 7] - append elem - [string range $hex 8 11] - append elem - [string range $hex 12 15] - append elem - [string range $hex 16 19] - append elem - [string range $hex 20 end] - } else { - error "Invalid length $len for UUID." - } - } - 4 { - set type text - # TBD - keep elem as binary as we do not know encoding - } - 5 { - set type boolean - if {$len != 1} { - error "Invalid length for boolean type." - } - binary scan $elem c elem - set elem [expr {!!$elem}] ; # Convert to 0/1 - } - 6 { - set type sequence - # elem remains the same. Caller will have to recurse to parse - } - 7 { - set type selection - # elem remains the same. Caller will have to recurse to parse - } - 8 { - set type url - # elem remains same. Encoding unknown. TBD - } - default { - set type unknown - # elem remains same - } - } - - return [list $type $elem [string range $bin $dataoffset+$len end]] -} - -proc iocp::bt::sdr::printn {recs {attrfilter *}} { - # Prints a SDP record to a more human readable form. - # recs - a list of binary SDP records in the form returned by - # [::iocp::bt::device services] or [::iocp::bt::device service_references]. - # attrfilter - If specified, only attribute names matching the filter - # using `string match` are printed. - # - set sep "" - foreach rec $recs { - puts $sep - set sep "--------------------------------------------" - print $rec $attrfilter - } -} - -proc iocp::bt::sdr::print {rec {attrfilter *}} { - # Prints a SDP record to a more human readable form. - # rec - a binary SDP record in the form returned by [::iocp::bt::device services] or - # [::iocp::bt::device service_references]. - # attrfilter - If specified, only attribute names matching the filter - # using `string match` are printed. - # - - # Alternating attribute value pairs - set rec [decode $rec] - foreach attr [attributes $rec] { - set attrname [names::attribute_name $attr] - if {[string is integer -strict $attrname]} { - # For easier matching with Bluetooth specs - set attrname [format 0x%x $attrname] - } - if {![string match -nocase $attrfilter $attrname]} { - continue - } - switch -exact -- $attrname { - ServiceID - - ServiceRecordState - - ServiceInfoTimeToLive - - ServiceAvailability - - DocumentationURL - - ClientExecutableURL - - IconURL - - ServiceName - - ServiceDescription - - ProviderName - - ServiceRecordHandle { - puts "$attrname: [attribute get $rec $attrname]" - } - ServiceClassIDList { - puts "$attrname: sequence" - foreach elem [attribute get $rec $attrname] { - dict with elem { - # We use this for printing service-specific attributes - lappend service_class_uuids $Uuid - if {$Name eq $Uuid} { - puts " $Uuid" - } else { - puts " $Uuid $Name" - } - } - } - } - ProtocolDescriptorList { - puts "$attrname:" - foreach protocol [attribute get $rec $attrname] { - puts " ProtocolStack:" - foreach layer $protocol { - set name [dict get $layer ProtocolName] - set uuid [dict get $layer Protocol] - set params [lmap param [dict get $layer ProtocolParams] { - PrintableElement $param - }] - if {$name eq ""} { - puts " $uuid ([join $params {, }])" - } else { - puts " $uuid $name ([join $params {, }])" - } - } - } - } - AdditionalProtocolDescriptorList { - # Like ProtocolDescriptorList but an additional level - # of nesting. - puts "$attrname:" - foreach additional_protocol [attribute get $rec $attrname] { - foreach protocol $additional_protocol { - puts " ProtocolStack:" - foreach layer $protocol { - set name [dict get $layer ProtocolName] - set uuid [dict get $layer Protocol] - set params [lmap param [dict get $layer ProtocolParams] { - PrintableElement $param - }] - if {$name eq ""} { - puts " $uuid ([join $params {, }])" - } else { - puts " $uuid $name ([join $params {, }])" - } - } - } - } - } - BrowseGroupList { - puts "$attrname: sequence" - foreach elem [attribute get $rec $attrname] { - dict with elem { - if {$Name eq $Uuid} { - puts " $Uuid" - } else { - puts " $Uuid $Name" - } - } - } - } - LanguageBaseAttributeIDList { - puts "$attrname: sequence" - dict for {lang val} [attribute get $rec $attrname] { - puts " $lang: [dict get $val Encoding], [dict get $val BaseOffset]" - } - } - BluetoothProfileDescriptorList { - puts "$attrname: sequence" - foreach profile [attribute get $rec $attrname] { - # Sequence of profiles. Each profile is a sequence of - # profile uuid and version. - # puts profile:$profile - dict with profile { - if {$Uuid eq $Name} { - puts " $Uuid v$MajorVersion.$MinorVersion" - } else { - puts " $Uuid $Name v$MajorVersion.$MinorVersion" - } - } - } - } - default { - if {$attr >= 0x100 && $attr <= 0x1ff} { - # TBD - special handling for 0x100-0x1ff attributes by - # looking up languages table - puts "$attrname: [PrintableElement [attribute raw $rec $attr]]" - } else { - lassign [PrintableServiceSpecificAttribute \ - $service_class_uuids $rec $attr] attrname attrval - puts "$attrname: $attrval" - } - } - } - } -} - -proc iocp::bt::sdr::PrintableServiceSpecificAttribute {service_class_uuids rec attr} { - # Returns pair of human readable attribute name and value - set attrname $attr - set attrval [PrintableElement [attribute raw $rec $attr]] - - # Maybe we know about attributes for one of these services - foreach uuid $service_class_uuids { - switch -exact -- $uuid { - 00001105-0000-1000-8000-00805f9b34fb { - # ObjectPush - if {$attr == 0x200} { - set attrname GoepL2CapPsm - break - } elseif {$attr == 0x303} { - set attrname SupportedFormats - set attrval [lmap val [lindex [attribute raw $rec $attr] 1] { - # Val is a sequence of {type value} where type is int - set val [lindex $val 1] - if {$val == 255} { - set val any - } else { - set formats {unknown vcard-2.1 vcard-3.0 vcal-1.0 ical-2.0 vnote vmessage} - if {$val < [llength $formats]} { - set val [lindex $formats $val] - } - } - set val - }] - break - } - } - } - } - return [list $attrname $attrval] -} - -proc iocp::bt::sdr::PrintableElement {delem {parent_indent {}}} { - lassign $delem type val - switch -exact -- $type { - nil - - uint - - int { return $val } - uuid { - set name [names::to_name $val] - if {$name eq $val} { - return $val - } else { - return "$val $name" - } - } - text - - url { - # Assume UTF-8 binary form - # Strip terminating null. TBD - is null alwys present? - set printable [encoding convertfrom utf-8 $val] - } - boolean { return [expr {$val ? "true" : "false"}] } - selection - - sequence { - set printable [list $type] - set new_indent "${parent_indent} " - foreach elem $val { - lappend printable "${new_indent}[PrintableElement $elem $new_indent]" - } - set printable [join $printable \n] - } - unknown { - set printable [BinToHex $val] - } - } - return $printable -} +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::sdr { + namespace path [namespace parent] + namespace export attribute attributes decode print printn + + namespace eval attribute { + namespace path [list [namespace parent] [namespace parent [namespace parent]]] + namespace export exists get raw text + namespace ensemble create + } +} + +proc iocp::bt::sdr::decode {binsdr} { + # Decodes a binary service discovery record + # binsdr - a raw Bluetooth service discovery record in binary + # form. + # + # The returned value should be treated as opaque. The attributes + # stored in the record should be accessed with the commands in + # the `sdr` namespace. + # + # Returns a container of attributes stored in the record. + + # A SDR record is a single data element which is a sequence + # containing nested data elements. + # {{sequence {nested data elements}}} + set rec [lindex [DecodeElements $binsdr] 0 1] + + # $rec contains alternating attribute value pairs. The result + # is built as a list as its faster to do that and shimmer + # to a dict on access than to build a dictionary to begin with. + set sdr {} + foreach {attr val} $rec { + lappend sdr [lindex $attr 1] $val + } + return $sdr +} + +proc iocp::bt::sdr::attribute::exists {sdr attr_id {varname {}}} { + # Checks if an attribute exists in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # attr_id - attribute integer id or Bluetooth universal attribute name + # varname - If not the empty string, the attribute + # value is stored in a variable of this name in the + # caller's context. + # + # The value returned in $varname is the attribute type-independent data + # value. To get the value formatted for the attribute type, use the + # [attribute get] command. + # + # Returns 1 if the attribute exists, and 0 otherwise. + + if {[string is integer -strict $attr_id]} { + set key [expr {$attr_id + 0}]; # Force decimal rep. Faster than format + } else { + set key [names::attribute_id $attr_id]; # name -> id + } + + if {[dict exists $sdr $key]} { + if {$varname ne ""} { + upvar 1 $varname value + set value [dict get $sdr $key] + } + return 1 + } + + return 0 +} + +proc iocp::bt::sdr::attributes {sdr} { + # Get the list of attributes in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # Returns a list of numeric attribute ids. + return [dict keys $sdr] +} + +proc iocp::bt::sdr::attribute::raw {sdr attr_id} { + # Get an attribute value from an service discovery record. + # sdr - a decoded service discovery record in the form returned by + # sdr_decode. + # attr_id - attribute integer id + # + # The command will raise an error if the attribute does not exist + # in the sdr. + # + # Returns the attribute value as a pair consisting of the type and + # the raw data element value. + + if {[exists $sdr $attr_id value]} { + return $value + } else { + error "Attribute with id \"$attr_id\" not found." + } +} + +proc iocp::bt::sdr::attribute::get {sdr attr_id {varname {}}} { + # Get the value of an universal attribute from a service + # discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # varname - Optional name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The attribute value is decoded into a attribute type-dependent format. + # + # The $attr_id argument must be one of those defined in + # the *Universal Attributes* section in the + # [Bluetooth Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/) + # specification. Otherwise an error is raised. The recognized attributes + # are listed below. Refer to the Bluetooth specification for their + # exact semantics. + # + # AdditionalProtocolDescriptorList - List of protocol stacks. This + # supplements the `ProtocolDescriptorList` attribute. + # BluetoothProfileDescriptorList - List of Bluetooth profile descriptors + # to which the service conforms. Each element is a dictionary + # with keys `Uuid`, `Name`, `MajorVersion` and `MinorVersion` + # corresponding to the referenced profile. + # BrowseGroupList - List of browse group descriptors representing + # the browse groups to which the service record belongs. Each + # descriptor is dictionary with keys `Uuid` and `Name` identifying + # the a browse group. + # ClientExecutableURL - URL pointing to the client application that + # may be used to utilize the service. + # DocumentationURL - URL for the service documentation. + # IconURL - URL for icon that may be used to represent the service. + # LanguageBaseAttributeIDList - Nested dictionary of language-specific + # attribute offsets (see below). + # ProtocolDescriptorList - List of protocol stacks (see below) that may + # be used to access the service described by the service record. + # ProviderName - Name of entity providing the service in the primary + # language of the service record. + # ServiceAvailability - A value in the range 0-255 that indicates + # relative availability of the service in terms of the number of + # clients it can accept. Note this is a scaled estimate. + # ServiceClassIDList - List with each element being a dictionary + # corresponding to a service class that the record conforms to. + # The keys of the dictionary are `Name` and `Uuid`. + # ServiceDescription - A description of the service in the primary + # language of the record. + # ServiceID - UUID that uniquely identifies a service instance. + # ServiceInfoTimeToLive - The number of seconds from the time it was + # generated that the service record information is expected to be valid. + # ServiceName - The human-readable name of the service in the primary + # language of the record. + # ServiceRecordHandle - A numeric value that uniquely identifies a service + # record within a SDP server. + # ServiceRecordState - Numeric value that is changed any time the service + # record is modified by the SDP server. + # + # In the case of ProtocolDescriptorList and AdditionalProtocolDescriptorList, + # the attribute value is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # In the case of LanguageBaseAttributeIDList, the attribute value is + # a dictionary indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. + # + # Returns a boolean or the decoded attribute value. + + set attr_name [names::attribute_name $attr_id] + set attr_id [names::attribute_id $attr_id] + switch -exact -- $attr_name { + BrowseGroupList - + ServiceClassIDList { + tailcall Uuids $sdr $attr_id $varname + } + BluetoothProfileDescriptorList - + ProtocolDescriptorList - + AdditionalProtocolDescriptorList - + LanguageBaseAttributeIDList { + tailcall $attr_name $sdr $varname + } + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceAvailability - + ServiceInfoTimeToLive - + ServiceID - + ServiceRecordState - + ServiceRecordHandle { + tailcall AttributeValue $sdr $attr_id $varname + } + ServiceName - + ServiceDescription - + ProviderName { + # Special case because they need a language argument + tailcall text $sdr $attr_name primary $varname + } + default { + if {[llength [info commands [namespace current]::universal::$attr_name]]} { + tailcall [namespace current]::universal::$attr_name $sdr $varname + } + } + } + error "Unknown universal attribute \"$attr_id\"." +} + +proc iocp::bt::sdr::attribute::text {sdr attr_id lang {varname {}}} { + # Get the value of an text attribute in the specified language + # from a service discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # lang - language identifier as specified in iso639, e.g. `en` for + # english, `fr` for french etc. or the keyword `primary`. + # varname - Name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # If the record does not contain a value for the specified language, + # the value for the primary language will be retrieved. + # + # Returns a boolean or the attribute text value. + # + set lang_offset [names::attribute_id $attr_id] + if {$lang_offset < 256 || $lang_offset >= 512} { + error "Invalid text attribute id \"$attr_id\"." + } + incr lang_offset -256 + tailcall TextAttribute $sdr $lang_offset $lang $varname +} + + +proc iocp::bt::sdr::attribute::LanguageBaseAttributeIDList {sdr {varname {}}} { + # Retrieve the language base attributes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a nested dictionary + # corresponding to the `LanguageBaseAttributeIDList` + # universal attribute in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The dictionary value returned or stored is indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. fs + # + # Returns a boolean or a dictionary of language attributes. + if {![AttributeValue $sdr 6 sequence]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # attr is {sequence {langid encoding baseoffset ...}} + set langs [dict create] + foreach {langid enc offset} $sequence { + # langid is ISO 639 language code + set langid [lindex $langid 1] + set langid "[format %c [expr {$langid >> 8}]][format %c [expr {$langid &0xff}]]" + dict set langs $langid Encoding [::iocp::bt::EncodingTclName [lindex $enc 1]] + dict set langs $langid BaseOffset [lindex $offset 1] + } + if {$varname eq ""} { + return $langs + } else { + upvar 1 $varname var + set var $langs + return 1 + } +} + + +proc iocp::bt::sdr::attribute::ProtocolDescriptorList {sdr {varname {}}} { + # Retrieve the protocol information from a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # Information about the protocols that may be used to access the + # service described by SDR is stored in the ProtocolDescriptorList + # universal attribute as described in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The returned information is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # Returns a boolean or the list of protocols. + + if {! [attribute exists $sdr 4 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + if {$varname eq ""} { + return [Protocols $attrval] + } else { + upvar 1 $varname var + set var [Protocols $attrval] + return 1 + } +} + +proc iocp::bt::sdr::attribute::BluetoothProfileDescriptorList {sdr {varname {}}} { + # Retrieve the service classes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a list of profile descriptors + # corresponding to the BluetoothProfileDescriptorList + # universal attribute in the Bluetooth Core specification. + # Each descriptor is a dictionary with the following keys: + # `Uuid` - the UUID of the profile + # `Name` - the mnemonic name of the profile or its UUID if the + # UUID cannot be mapped to a name + # `MajorVersion` - the profile's major version + # `MinorVersion` - the profile's minor version + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # Returns a boolean or the list of service class descriptors. + + if {! [attribute exists $sdr 9 attr]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # Sequence of profile descriptors. Each profile descriptor + # is sequence of profile uuid and version. + set profiles {} + foreach profile [lindex $attr 1] { + set uuid [lindex $profile 1 0 1] + set version [lindex $profile 1 1 1] + lappend profiles [dict create \ + Uuid $uuid \ + Name [iocp::bt::names::to_name $uuid] \ + MajorVersion [expr {$version >> 8}] \ + MinorVersion [expr {$version & 0xff}] + ] + } + + if {$varname eq ""} { + return $profiles + } else { + upvar 1 $varname var + set var $profiles + return 1 + } + +} + +proc iocp::bt::sdr::attribute::AdditionalProtocolDescriptorList {sdr {varname {}}} { + # Returns protocol information from the additional protocols field + # in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # This command is identical to [protocols] except it extracts + # protocol information from the `AdditionalProtocolDescriptorList` + # attribute instead of `ProtocolDescriptorList`. + + if {! [attribute exists $sdr 13 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # AdditionalProtocolDescriptorList is a sequence each element of which + # is formatted like a ProtocolDescriptorList + # assert [lindex $attrval 0] eq "sequence" + set protocols [lmap elem [lindex $attrval 1] { + Protocols $elem + }] + + if {$varname eq ""} { + return $protocols + } else { + upvar 1 $varname var + set var $protocols + return 1 + } +} + + +proc iocp::bt::sdr::Uuids {sdr attr_id {varname {}}} { + if {! [attribute exists $sdr $attr_id attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + set uuids [lmap elem [lindex $attrval 1] { + set uuid [lindex $elem 1] + dict create Uuid $uuid Name [names::to_name $uuid] + }] + + if {$varname eq ""} { + return $uuids + } else { + upvar 1 $varname var + set var $uuids + return 1 + } +} + +proc iocp::bt::sdr::Protocols {attrval} { + # The attribute value may either be a single protocol stack encoded + # as a sequence, or multiple stacks encoded as a selection of sequences. + if {[lindex $attrval 0] eq "sequence"} { + # Single protocol stack + set stacks [list [lindex $attrval 1]] + } else { + # selection - Multiple stacks + set stacks [lindex $attrval 1] + } + + set protocols [lmap stack $stacks { + # stack -> {protocol_descriptor protocol_descriptor ...} + # protocol_descriptor -> {sequence {data_element ...}} + lmap protocol_descriptor $stack { + # Each descriptor is a sequence, the first element identifying the + # protocol and the remaining being (optional) parameters. + set elements [lindex $protocol_descriptor 1] + set protocol_uuid [lindex $elements 0 1] + dict create \ + Protocol $protocol_uuid \ + ProtocolName [iocp::bt::names::protocol_name $protocol_uuid] \ + ProtocolParams [lrange $elements 1 end] + } + }] + + return $protocols +} + +proc iocp::bt::sdr::AttributeValue {sdr attr_id {varname {}}} { + if {$varname eq ""} { + return [lindex [attribute raw $sdr $attr_id] 1] + } else { + if {[attribute exists $sdr $attr_id attrval]} { + upvar 1 $varname value + set value [lindex $attrval 1] + return 1 + } else { + return 0 + } + } +} + +proc iocp::bt::sdr::TextAttribute {sdr attr_offset lang {varname {}}} { + + # Assume that if sdr does not explicitly specify encoding, it is UTF8 + set enc utf-8 + set base_offset 256 + + # Primary language's ServiceName attribute index always 256 + # Otherwise get the offset from the base index 256 + if {$lang ne "primary"} { + # See if the specified language has an entry in the languages table + if {[attribute get $sdr LanguageBaseAttributeIDList base_offsets] && + [dict exists $base_offsets $lang]} { + # Yes, so the language-specific text will be at an offset + # from the base attribute index + set base_offset [dict get $base_offsets $lang BaseOffset] + set enc [dict get $base_offsets $lang Encoding] + } + } + set attr_index [expr {$base_offset + $attr_offset}] + if {! [attribute exists $sdr $attr_index name]} { + # This language does not exist. Return primary language if possible. + # If base_offset was 256, that's already primary so don't retry + if {$base_offset == 256 || + ![attribute exists $sdr [expr {256 + $attr_offset}] name] } { + if {$varname ne ""} { + return 0 + } else { + error "Attribute not found in SDR." + } + } + } + + set name [lindex $name 1] + + # $name is encoded. If Tcl supports the encoding, well and good. + # Else return a hex form + if {[string is integer $enc]} { + # SDR specified an encoding but not one that Tcl knows about + set text [binary encode hex $name] + } else { + # The string may be null terminated. TBD - always? + if {[string index $name end] eq "\0"} { + set name [string range $name 0 end-1] + } + set text [encoding convertfrom $enc $name] + } + + if {$varname ne ""} { + upvar 1 $varname var + set var $text + return 1 + } else { + return $text + } +} + +proc iocp::bt::sdr::DecodeElements {delem} { + # Decodes a Data Element as defined in the Bluetooth specification. + # delem - Data element in binary format + + set decoded {} + while {[string length $delem]} { + lassign [ExtractFirstElement $delem] type value delem + switch -exact -- $type { + sequence { lappend decoded [list sequence [DecodeElements $value]]} + selection { lappend decoded [list selection [DecodeElements $value]]} + default { + lappend decoded [list $type $value] + } + } + } + return $decoded +} + +proc iocp::bt::sdr::ExtractFirstElement {bin} { + # Extracts leading element from an SDP record. + # bin - tail of SDP record + # Returns a pair consisting of the extracted element and + # the remaining part of $bin. + + set binlen [string length $bin] + if {$binlen == 0} { + return {} + } + binary scan $bin c header + set type [expr {($header >> 3) & 0xff}] + set lenbits [expr {$header & 0x7}] + + # The nil type is formatted slightly differently. Treat as a special case. + if {$type == 0} { + if {$lenbits != 0} { + error "Invalid non-0 length for nil type." + } + return [list nil "" [string range $bin 1 end]] + } + + if {$lenbits < 5} { + set len [expr {1 << $lenbits}] + set dataoffset 1 + } else { + if {$lenbits == 5} { + set dataoffset 2 + set fmt c + set mask 0xff + } elseif {$lenbits == 6} { + set dataoffset 3 + set fmt S + set mask 0xffff + } else { + # lenbits == 7 + set dataoffset 5 + set fmt I + set mask 0xffffffff + } + # Ensure input length enough for at least length field + if {$binlen < $dataoffset} { + error "Truncated binary header." + } + binary scan $bin @1$fmt len + set len [expr {$len & $mask}]; # Since binary scan is signed + } + if {$binlen < ($dataoffset+$len)} { + error "Truncated binary data." + } + set elem [string range $bin $dataoffset [expr {$dataoffset+$len-1}]] + switch -exact $type { + 1 { + set type uint + if {$len == 1} { + binary scan $elem cu elem + } elseif {$len == 2} { + binary scan $elem Su elem + } elseif {$len == 4} { + binary scan $elem Iu elem + } elseif {$len == 8} { + binary scan $elem Wu elem + } elseif {$len == 16} { + binary scan $elem WuWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 2 { + set type int + if {$len == 1} { + binary scan $elem c elem + } elseif {$len == 2} { + binary scan $elem S elem + } elseif {$len == 4} { + binary scan $elem I elem + } elseif {$len == 8} { + binary scan $elem W elem + } elseif {$len == 16} { + # Note hi is signed, lo unsigned + binary scan $elem WWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 3 { + set type uuid + if {$len == 2} { + set elem [binary encode hex $elem] + set elem "0000$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 4} { + set elem [binary encode hex $elem] + set elem "$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 16} { + set hex [binary encode hex $elem] + set elem [string range $hex 0 7] + append elem - [string range $hex 8 11] + append elem - [string range $hex 12 15] + append elem - [string range $hex 16 19] + append elem - [string range $hex 20 end] + } else { + error "Invalid length $len for UUID." + } + } + 4 { + set type text + # TBD - keep elem as binary as we do not know encoding + } + 5 { + set type boolean + if {$len != 1} { + error "Invalid length for boolean type." + } + binary scan $elem c elem + set elem [expr {!!$elem}] ; # Convert to 0/1 + } + 6 { + set type sequence + # elem remains the same. Caller will have to recurse to parse + } + 7 { + set type selection + # elem remains the same. Caller will have to recurse to parse + } + 8 { + set type url + # elem remains same. Encoding unknown. TBD + } + default { + set type unknown + # elem remains same + } + } + + return [list $type $elem [string range $bin $dataoffset+$len end]] +} + +proc iocp::bt::sdr::printn {recs {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # recs - a list of binary SDP records in the form returned by + # [::iocp::bt::device services] or [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + set sep "" + foreach rec $recs { + puts $sep + set sep "--------------------------------------------" + print $rec $attrfilter + } +} + +proc iocp::bt::sdr::print {rec {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # rec - a binary SDP record in the form returned by [::iocp::bt::device services] or + # [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + + # Alternating attribute value pairs + set rec [decode $rec] + foreach attr [attributes $rec] { + set attrname [names::attribute_name $attr] + if {[string is integer -strict $attrname]} { + # For easier matching with Bluetooth specs + set attrname [format 0x%x $attrname] + } + if {![string match -nocase $attrfilter $attrname]} { + continue + } + switch -exact -- $attrname { + ServiceID - + ServiceRecordState - + ServiceInfoTimeToLive - + ServiceAvailability - + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceName - + ServiceDescription - + ProviderName - + ServiceRecordHandle { + puts "$attrname: [attribute get $rec $attrname]" + } + ServiceClassIDList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + # We use this for printing service-specific attributes + lappend service_class_uuids $Uuid + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + ProtocolDescriptorList { + puts "$attrname:" + foreach protocol [attribute get $rec $attrname] { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + AdditionalProtocolDescriptorList { + # Like ProtocolDescriptorList but an additional level + # of nesting. + puts "$attrname:" + foreach additional_protocol [attribute get $rec $attrname] { + foreach protocol $additional_protocol { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + } + BrowseGroupList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + LanguageBaseAttributeIDList { + puts "$attrname: sequence" + dict for {lang val} [attribute get $rec $attrname] { + puts " $lang: [dict get $val Encoding], [dict get $val BaseOffset]" + } + } + BluetoothProfileDescriptorList { + puts "$attrname: sequence" + foreach profile [attribute get $rec $attrname] { + # Sequence of profiles. Each profile is a sequence of + # profile uuid and version. + # puts profile:$profile + dict with profile { + if {$Uuid eq $Name} { + puts " $Uuid v$MajorVersion.$MinorVersion" + } else { + puts " $Uuid $Name v$MajorVersion.$MinorVersion" + } + } + } + } + default { + if {$attr >= 0x100 && $attr <= 0x1ff} { + # TBD - special handling for 0x100-0x1ff attributes by + # looking up languages table + puts "$attrname: [PrintableElement [attribute raw $rec $attr]]" + } else { + lassign [PrintableServiceSpecificAttribute \ + $service_class_uuids $rec $attr] attrname attrval + puts "$attrname: $attrval" + } + } + } + } +} + +proc iocp::bt::sdr::PrintableServiceSpecificAttribute {service_class_uuids rec attr} { + # Returns pair of human readable attribute name and value + set attrname $attr + set attrval [PrintableElement [attribute raw $rec $attr]] + + # Maybe we know about attributes for one of these services + foreach uuid $service_class_uuids { + switch -exact -- $uuid { + 00001105-0000-1000-8000-00805f9b34fb { + # ObjectPush + if {$attr == 0x200} { + set attrname GoepL2CapPsm + break + } elseif {$attr == 0x303} { + set attrname SupportedFormats + set attrval [lmap val [lindex [attribute raw $rec $attr] 1] { + # Val is a sequence of {type value} where type is int + set val [lindex $val 1] + if {$val == 255} { + set val any + } else { + set formats {unknown vcard-2.1 vcard-3.0 vcal-1.0 ical-2.0 vnote vmessage} + if {$val < [llength $formats]} { + set val [lindex $formats $val] + } + } + set val + }] + break + } + } + } + } + return [list $attrname $attrval] +} + +proc iocp::bt::sdr::PrintableElement {delem {parent_indent {}}} { + lassign $delem type val + switch -exact -- $type { + nil - + uint - + int { return $val } + uuid { + set name [names::to_name $val] + if {$name eq $val} { + return $val + } else { + return "$val $name" + } + } + text - + url { + # Assume UTF-8 binary form + # Strip terminating null. TBD - is null alwys present? + set printable [encoding convertfrom utf-8 $val] + } + boolean { return [expr {$val ? "true" : "false"}] } + selection - + sequence { + set printable [list $type] + set new_indent "${parent_indent} " + foreach elem $val { + lappend printable "${new_indent}[PrintableElement $elem $new_indent]" + } + set printable [join $printable \n] + } + unknown { + set printable [BinToHex $val] + } + } + return $printable +} diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/pkgIndex.tcl b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl similarity index 82% rename from src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/pkgIndex.tcl rename to src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl index 0f56138b..2452b04a 100644 --- a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp2.0b1/pkgIndex.tcl +++ b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl @@ -1,36 +1,36 @@ -# Core iocp package -package ifneeded iocp 2.0b1 \ - [list apply [list {dir} { - package require platform - set initName [string totitle iocp] - if {[package vsatisfies [package require Tcl] 9]} { - set fileName "tcl9iocp20b1.dll" - } else { - set fileName "iocp20b1.dll" - } - set platformId [platform::identify] - set searchPaths [list [file join $dir $platformId] \ - {*}[lmap platformId [platform::patterns $platformId] { - file join $dir $platformId - }] \ - $dir] - foreach path $searchPaths { - set lib [file join $path $fileName] - if {[file exists $lib]} { - uplevel #0 [list load $lib $initName] - return - } - } - error "Could not locate $fileName in directories [join $searchPaths {, }]" - }] $dir] - -# iocp_inet doesn't need anything other than core iocp -package ifneeded iocp_inet 2.0b1 \ - "package require iocp" - -if {1} { - # iocp_bt needs supporting script files - package ifneeded iocp_bt 2.0b1 \ - "[list source [file join $dir bt.tcl]]" -} - +# Core iocp package +package ifneeded iocp 2.0.2 \ + [list apply [list {dir} { + package require platform + set initName [string totitle iocp] + if {[package vsatisfies [package require Tcl] 9]} { + set fileName "tcl9iocp202.dll" + } else { + set fileName "iocp202.dll" + } + set platformId [platform::identify] + set searchPaths [list [file join $dir $platformId] \ + {*}[lmap platformId [platform::patterns $platformId] { + file join $dir $platformId + }] \ + $dir] + foreach path $searchPaths { + set lib [file join $path $fileName] + if {[file exists $lib]} { + uplevel #0 [list load $lib $initName] + return + } + } + error "Could not locate $fileName in directories [join $searchPaths {, }]" + }] $dir] + +# iocp_inet doesn't need anything other than core iocp +package ifneeded iocp_inet 2.0.2 \ + "package require iocp" + +if {1} { + # iocp_bt needs supporting script files + package ifneeded iocp_bt 2.0.2 \ + "[list source [file join $dir bt.tcl]]" +} + diff --git a/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..09d3cf0aa9f0d82e775357fbf226283e817d89d7 GIT binary patch literal 125251 zcmd?Sdwf*Yxj#G;CNRk09wpIKqmJ!pLmM@zu}w7CfyqEr&`|=?ii%ZJTd7i+5n8z< z?wQK&W^`)t98aq)RL?oqOH0o|iT6Mv1FfwQQL%Uw6?Kn;6>lMmb$;LPv-aLIA?WLQ zFMqv!K4j1Awbrwq`}3^p%=m8BGs@%f`0&42^muyk&C_ea@hYt3v8~fK| zd%V;CdThrPKUf%AaLv`8_V6VsXFZf^M@vNPI20{D} z;k#$urSchdi8tTyDgT1%<*6rCfUXFNLaH4bus#0Cqu1+c=a&JmXU;f&_^f{wo+aZy ztIXr6o$4ieBmRBzwAXVh5&MjPCnhdmoIqJ>JSq_((&`eS8qM=~=AC%iC5cP$H4CzY zcGfv`IN*rK(|h7Is$$|RC~rV{7|+8-)Qgter`tA2%-;Higr{&aZ#2zoj32dbX%8a(#|P;beIdc7w`&+9;mr=EXy z+VEB!pk89W;OA$(J$AjHAE4efm(RcYQgkIU1ab$X{>l;crVz~kAOGpQYjeJ^XZ8J; zTHT|(9=*S|FSG7=aw78p|DHd4roKC9wuQs#-sClF0;t%yHQl@HeBJVzlP=qeuY-5+ zZQ+ni;&@LMz%2Na;`91$-$P`xv1gC=r|PQv?!VON1BjQ`)Zn8&RH3DwMk%TvgNOC{ z7_UdF>-|Av;DU=T-Wc!*JblQUeB-h;G%?kkImg#HSg5s;%(nDtskI7_+plza6uU%+ zFE#G4yYyO>v%m$uHs*$XeAQ&X zdZPU006E&OupdA7{@&$hIh(ueF8}X)tjJ z3#!<}7jC!&eLRN`tE<*sYF$KTUjBj70kH$gKOQ5~3%wK=@Y8qKtPx1PiLnlZfi*+{ zI0g(~xL94I@Y>RsooU9~jHjW^ue9s0HRvDt6DKro(f0)tqm9iM^mS$*q!JHq>`Lyz z>sNh=ycv(Q#9NoXqHpmPUhK@W=t#5OpWc$Fe~_kxw#oj&HdlWM>;i4ohD;zMo?&yJ z=#_Mm{B_|ThxtkdS-Q_Eb8a}4Sy)9C*;eWGn9(*cSYHwjdlScW?fTS+wz23>C~E9w zNq^M^7xgK>(DkIZ@UjCF{Ma8}itt9E5#1F;VS6e*eZ_bk;2M3_`KV-UFF{tg3EuSo zZDEO`l+uDD*NK$CT+4fj(i5k5kRE6K?HSbhbm7T& zNQnQceQYwFO?C(AfI|S3QOa2XUQ#)W)zhD0xu3y8OS8*0Yyp^6Hf+h>P&dGSdQS$v zXlM7sN3q(^e&W(k4g1-Sw`&5hnev*4p<(45RHE?~vrI+0`$?Ly{em)oWTwK*YS7Z9 zDNYslg#}_Udl#aeIP?o;`Aa5Q45{e}JE>-p|P?fBIy9CCu z(w0hD`Mq}ePf-3j-50Ctz~X#6P48L{)U#bEfjZ9dCK{kdbKzDxKyY9H0Le1YFA>lP zgp1ia@LKP0C#O6Y^krF2;lePVM)hLWVrBTFHBF$n#HO8i?bJnam6C@q{Bn7tnl;Uz z*QnNMw7a}fXie+qHBxc{dLNZHs#_EJyhim-PnC^jG&QVg`@BX?PNT!h8#S+KAJHh& z5e|Y0t03&ZEQ?tgc-LsV)Wp1-#7I zj$z3P(T5r!C;EzfA~)Ok8}fsA573zu zq)-64O{&0)L{zGnN73W3^ce-Xk_~|zeUH2z=8cp)(EDqsV4Yb3!7lE5$?jsl>Z0&F zi5C)oM1jmx5_Uh&5itC9;s^vF>pjSGWUx^>2c%dTRSzHb!ssjXEB&yJM4nf8)%I?k zS@5TBur1u=Pgd&vbr~$X;mliCUHcRQ}P7269nJJYZ}Xk0c^wLNq{a_;{Zn;^^Li{pVWP zr`{iCT4xI>iIo@G-vwBt0^M8DUjW=_4>vtqbh8*q7+4iKiEAeeW^7Q;bwDg zxFJ2LrMfBQz51J@_2OtP^%s20Tq(lz6Y>N-TIW@QXmST;u=Df$MsDvWrpO7OepXvV zf5RsqLQrgB9#@`4TTrB|XL`74nDrnx zDOpdhWIaw_%T~cQtr=)fa&NTB%|d^gz^pIxNqE3l@{~Hn@RB|;$pbQvABNHkjG?ml zK>rO!h0s21E4C3u;1ypEz{iw;^uW@MQFwQx-tWfC93T_6xL?F~+vZgb+8cD+M0`vf z1|zTo+`Jk`iLUjy`lQs#9&aRo(A;pHIW=G&*0NnYdwY6o;s~NRYBopger6j$%zADR z?3@~yxZQh^-I2llj)eQ)vnP?T%Qob^eGe_ea@T~-Ch)$P(U~K;m7?HW; zll|pQ{4g<9X4=aG#sG9Rf9OZWVrSMi^I3aE@9};=0BnyG1*7J!H(ndp5e^&iwoJTA z?`59hCM0kwMO$Zz)mH#Pr~_e1e6GQGf$A1E&uudXj@_OdSEk>{1U3Zs4jemROm5=% zp?U>cs|1PmO*>GUyPyAo?*Uy+X0(D8+1epP2l$*1DLS?=x zNY1^T_&!=17yId)jA$q`mAr*}@>yri0^{KcqNwe7S``gjo=6<4{OWvk-sPzjo2LVU zYA7VZPsb&x7g>TD`mh8FeKd$diRUWrbjRzUCTu2PlhL;H#$?TLHE@k%_arCUgMA17 z-mS-O6h&zhWgxdm&|u6N2a(x56x76NPG*zvd`biHA_fZ$_J2qD@P7 zBYZFzIz8bj?g39Re`_-WA~Qi?Wuwc?Ptv_CJWUsRDm*+)zv|7o;fOgu+-4_br+)y| zSy%_2Wu`)3C?tL7o5n`{&;GqnFotQdjr!|8Ow>pWgO{{^`8HFA zL&)#`1c>@jF!3dQ$fu>(;u|J&=L#q6W*9kb)lz>zi5bBpOyV`Nd*Ji9%lJGJ8wK-P z&SiD&dadipAb0~O#vldt1xdqw2mKuVh8eA=>i~+KL^yp391B2!b@{E3VrP~Bpn{8` zf>zfoTZ+&_*c7n+l3)wAGlEd<@*x1A9dw`7)`jm3EO4#)uiLM)<|v_4OYLQvOIm?K zLASZlnRW6VgnxafKS{5qgrwNZ{Gh>{3-_$s8zu`in%KT^rN%z;x6W4m$vOA2B!l9 z#|hH-k$n}jK^PAUCo1$5ixoQ*zq)sGKEb>#9O7dgM#pYS_~9Oqq!PlxXnn1geiA|f z!trK!?uF*!dibRIOe*{NK7|=j^!4sz`0KGYyaPrJ6%&ih3FgXhJxh>~Ih8OdnH~;M zJ&Nbm%kap}>}E1)T!@}!5Y*jI?356%MLMCS#^SpJ26F*WPCuu%O74R50mDEpzYlDC z`rTqt0h7@d(hx9OD5EAPp5R7P2XBl3Z#V%u3y_pO4`ubXB7%MKtF{zJ)b z9%MH?+)BZLaG!__q@C9=jJ?aIa*VhEt8wqFV5rD15yJF_TDp-znCb~Cc^?FQ(FLyy#MV&js?O;_qD^p0Y z!Nj3JYP!=p=UGNx#AFZ?lVLFDmkrBW1N}wKT7-gsRP(C=_p$^|!=shfr^s|V<}*?cru-%bTXuE7-r za9=Y0Vmu6a-C`gE`p~-PEZgLZ*_DWF$QoSIj4|fa`uss9?O@&wPSpT9yj}hnlr(M~ zjK#c$rLEd%bs@tf2Zwx#v2f{H`f>QDGR`bt2(FmW4B?+=9B}Lbh+_WlsAn_4_z=|J zbe}Y=&)>)b$s44JEimK5oHQnO^I1dbNv-i=VWZ8zo36zBdttXC#~ zf&{tkm_xw7OpK#rC~B=0LG|I90M+oAv#^k)zY+l#L0T1UX$F>swA3Z|n2AxpC4~Tp z8q994JDyOp^f&*84{0ol+{HF+s5Qr*-l3&l;n#)!OmsV%RH0(}Wxk1iVxG09C1-5X z&fbevr^7~_HAK&VZYgK$SF>$~79z)?|x6re2*G^Ya#0+eSGl~zmr z2PrDq#_}3yfU#RzBea&;1;Et;JL{3RM7QvDcsUFvc@G!k{?zq`H+g-*8S5Q^fjf2sUi&f_al#`Jb@b_)PX3!zCM7ts1rqOm)n*K` zq;wtXi{T!F4~0V|&X)C`Oq#K#(_+m_3SeY@A7Q~>o^YWQdKlN2goA{gE-rtAN&}LA za>f@BmyvAf{&ebBJ)Y!`O47>z3k5NwDZOR+O4xW1jw)ru8VGdTwBcsmQIeKXLHsUY z(^*aFt;+`T1N=Npp7d$?C|<*D6#mQR2?=(_&mdpfWhpjy>O;(Nh6Hgb)cyLcB$G?h z^>j*)(1bYbpBd)99xY|^1?Kl=HYg8iefdqQYlE;RaTxU9jBv&*Aam9*#Q_6_*R}c< zrOaq6venf<*-;?q0qY~eVJd@lb5WTY!e?s=!X=sfT?B}H3~qSLu)>Jh(RBeANH#MsL5+L)KX^?t7x5aA!57Zv0c&bhWNM;F%}`DgKdbNg!p4X z%nonC}sQoRO&3I?r@+AMFEAj+9D^fRLxMYvm0%qi4 zucp+_r8#%OA|ox1NLnMXrhf>yoLtimNq>MUcyMVM1BW?)>zW2zlAQGH{HTF8au&hfsh9`1A=r;T7U_VvL?)x898O4uj!n!*2?@ zQ~^`a1>j=!JlUiW#D1}3Gq=b_A62U=Y*x$i%FPyls-_H7+=z)D=9r^_<9Ur+8@Dv> zXhcZC`sS9zTw_YDu~Avc5&B|ZMe;3X55EIem~!D-7wjkb22&5_CC0(IDUzBJWpgyE zoSwOSkBl!dNqBMjEFs3z8;uQxJ4*JL6Cwb{V@nN#s!ZlZ(2dzSijP`DCQ@6hKX7J6 zN)-o<3}u%;AAf9+SbWaqBeWBxk!<>cl8lwzNttMAMlHp2ZYIs&P-+eQppZRD@aleQ zmoXZYH1@h=U_Tw^n9+u5hv+NwIkmDZiiOdqWM;R8YbjWzatt33ABN|*p)ELmuGEzk zWUd9zFDa_s!q(4K;Tk}p>R^7nSUn$7HgeLIJ5u1kWJr}RCR;Ev3$m)+6Pnn1u6j&f z(T#m#9?$XywXm#Al=Ily`e}3|#p*5q*VgyKts0{WFSz!ssFE!KWF^Sv){33rsJpd- z!$RpCV4hL%OOp#K-<@xcry8PS7dT+93a{W}T{y+Zz2TL7%<+NBX4M$ zV}C(GVk^IqTi_fAM8kB0?5LiPoqVb~`O0t{cLCsy%U$=axuS-yQpP5&*j4}|MD*c^ zF9e)qu*~rzzSoR98QjO_Hhd1sXWPHXXUdRN*)~iw?|q-HiFMKrlZS;WnFMu8aKV zzg(mlJ>e1I168Ku|6KfEtfm*Z-mTV?qV1dunIO8-0bR*O1r+o}rC18BtjzyL%(>wX zvn@QwoY}5_Tuij;J4bU=HboyAowyvEZIfRc)QJN2xqh8?0*>LE2ksLS@&GQYn(#Si z3q=u8R}~-Jc9_GIcBnC|U15`oBiPR3N)ggS5TaDYQQ=v|{vars8Ds{9cmA%Bay2mV z&4Nmkpn}1wmR@DoVRxwO6Rs)qwyr=5#I#IHF>N*GRB^@$m4<}~@z~LJ@(&7zNk>BE zY#~x0bYtWuy$+aKYCPWPSEBt?m6>4TYct>%j<)x!^hbIQLod(;VTjY39(pT0mZ`d^ z9xkH%AL!#4U+wzva^GmR71QkEz+3N+0H(XLU^@WLH~P%D+FnIb`tc%Q1i4$p&7V0Z zAgPd<2*=4B;3vKhjJ3|%#d&Ehbq86q0Gorj8Zi~CAP^ANL{MEO&+)YxGHRksC@Xj} zTAIs35i@a_O3Ozc3}|#)Z6YEDq-uR?lNb9H``b#{H1id5S9qIvNxuqwwKG21R8Vit z2YMi>zL0bH+txD;i49?pU#>pKF>;z?Mk6iJ)+JoJoSP#K+#o58bMki}=Vpb1b1T;O zUvu>1F|+{va4{W7g571$mtVl^sWry3uyGNmtXs(mStO0@$m8f`in_iL2|ovE$+2qM-o*>4yHk-0jI_Jl&0xFD7x71E z_0SB65L(x^vD`V4{{O8%Lw#;MXai8_81QYz3L)S?Ds`Y)^wZW^&VtfBBFr|;2=%ni z7F0c-2Bp&=(y2n67|%h41J2gd!f#bRF&8F;`RoOFXzM3*>K|7mj>q4_6QQ1OgED*! zX{jGWzq{f0~~4&Qp!>l!qYtAG712b07Tx4nlccB3Nd^= zi)@{4Nz^nIUZRS*R4MVy-pQ5)F)`&m2n>-?S`KWZmW!smlL$vQj&p5(E39PCLRfk)7j1fka*T1*D?>#DpQXjSOF ze(5XG~c#H507>4t@%k zX&u;Oo$`Iq|5>0vyC8Z5QV&WZcrv7-!jTME@Xkm0#p-*vq6EFj7#riO>wGS6@-LS% zS2G$kXI7nDxm}q5qAW7a5umY1+EP&V-}rTDpe-?LnYT zXdt6}h8hGy^(1ve=TD+8?FX=g5hdMw5sK8<+ucv(CHgt~-b?TirYBnJe-O#gRMz6~ zY(No69IK^Pfe>tSbw};U~KA z#VmnK%A3ECjuGyGde)0FlMA6fEfvLkPD!Uu#Rs=c;XDyIn~z90je{6OO)2&1hChI! z|4`immsa3X%&I{q#GyUVn`t7hs?YZH4$6x>MkBNO3dll>RWHB;vhstNx-J3)pNm`N zK#Z1p4eYg(1KGIs3n~oeOI-SCzS4v|n04}jc#!9!riu$TF8V$Y? z{3jgAfl)5bicd!Zfbi*1d~nO)dbN}XZ`k!uovzJWsS@T|f>(6XONDM^+b-^1mgrO5azCqHys2 z^Ab>YkIJH^HN(2+POLtYkdZ850xG?#e3f=D{c)|0 z{iVQI#_HGb1ZV01LLB00=rg_5?Qqpbd=%Z*9^hNmB^)LN1c2&Xlie>Q4ZIU6)~g~1 zFi833NeA=@4?yUWFYS+)Juz_(d4L_o`o|L%O%QK!Gc{paq_H>u75GZXsa`3S`75y% zbP2yZKwG(4Yz1R?qWg@JtMZWLjyZoaPym?Y%$@@qn+@Rk12i3S_1u`&42;AnD*Y+3 z?iOJk75EFVF5gRrsC@u&#sZn28%Eonk+kKHLsc2kgI&H{qywE{&>rW*8F?uB)p|w( zmN*x?)i99a^o2RQgS%{oP+RrKari0POP|ua%`hXbV%un<+<52Wx4poO#e&ZDulD_yp|+ZvHVZ>PYWc_EmFge8X#% znWjn#Y^} zRVMu|;ATr-hH<{)bBZn}BHz3UZck3bI_H)E;~gd7GFMqg8bO5Ed*LbSDe4_3^=#LS zRZjq&&JLApb~+rXE!f1X&clH9A%L~IFm0}zE5HV?8ioKq{FU+C%nff;>bs|pS`arc zs%g2XR=e>iSR<^g9!v3utNdYVO=gbQxTxkd6lXpqE3vzuE?_E;>R=qKEOMoIf`E~e z^oR4YFiJNT0IEcg>Uqc_c~lc+e21rxeg>rZkic30vCZ-OkFdpJYEHd@RhK9S<*nS4 zfFM4^cQbHDGL(5YydH1Z5P5Mla$sFmMI+G_}-i#8BGUTX@o@ z5J)U~7zota2*@}!@eb4tnRa50Ku`cyc&aQPOl5ygrcW|IFVoMz!F&MUM1@mEQ}#~ zwqajV%fb%r#=8LnTVWWap~M?@R*s6sO>(cEB!U$VFzKUa%vsGS^)?}CBsV3(Ue6_Z8C4$F!@VDw+V4cc`0>R5933a zvbe?a1k&!L_)Pv51~Td=0Ln~~8W3KK{krz~qj=nmz$BKPXilR2HSSRH#MlmIn7#^5 z{_+S?@p*H0oRhwN-qSAXSh7>;0_JVwHMyx0k8)ExKmc+fIr{^r1L~{HcpU3yl~x9t z!i**0L@kxS)taZUQ)F=snTvV!Nil%_fkSb&ln=81ntV zoaSqp>Px;`E_c^?J3hXTY`Dst#sGj+um%jEHWjPmu4>M+hj34br3p`$b7v^}yG`|f zZ7p}+$gv!tl8eaxRAOpe(LnYMy8yxUuTSJ>lA#z6O87w(D)m0c={fREo7>9|HX{Eg$x_65FC^xR28;+k@tXah$M&Ckb=m>18WZsc-G(1iVHqJn`y`r= zTcDi0j1Z9TJ3|9FJ}diIcnSNo`be&`vdeWX1=vgn<{gW@EjOT{+Ms(MyI#<4GnwM$ z`X`>g>2U2t>NGGEQDqwUb^+fy>-r~P{Lnb?^I|aBxR?UA5^6#=UCUR%t?5N{vB^XO z0-0*J8BCn7913P8fDF3A$eznus3_w5E8anT>B&%n?tV(R+`6Hk9P>B{M1BlvIdO@5 zN-S&XAoM8KP7QbqE)ihcAh`C-BjKyj3+R&BQpz!tWV+?BA0;w>il>@26|}(RtDc0dcv)GwKjhnZyZA2$WS+oN{yJ#J zd9H0%I;!wkP8^GIj+$~ zd@YlG$v44CKjn+3I?xY>;M$G=&JS=RFQvpWqKbGEGd9P#5R4AMg287$mQ+UiLM>eb z7%;PT%-K#yHzB*bT0Sv=msFMnKrCXmL3|dpU;{*p>wr)ek#m#CS(XAJP>M;Eh#RLr z)5M&%Wmd*KpIzV~l!I-~E&=PrusHvVEmFBD{?W>A#(;Je4aLDuS?*Ku_@nx z7iO%(JcIHyX2iKi<~VuRkc&wiK`8z;b_${&djqu79(c+c9Cnqau7sNACRdRZjslE8 z*m`3#^ppn$P+#^JZOUJYZ;x>$PUcjVO8tv{z6+5T%S4QZpRpdz^Bd1z;B&SV!2kUquba;T{J1)x29j8g^<| z&De!Mv7MO+PWOFp3;y?ZYV$W>UB1}>KzPjBULcO4klG+9`*5~f7sFbCNcwyLVaKSq z`*CqE)VX1v8!ov!1Qc_2y(d*YE+cbYiEpoym5qB;0sY3>V*v~IUme99*>=z3qm+*p zdvC=3IeUyDcgGC$t4`{evtAr_vqT*6!yZyQGdN~!CvviY*c)>)5NF1A;nm&cuL_dL zFs5YT%%O4f&FKH*Tn^BF_pFj4H{Ow5D;eNy33tM06pkQ)W_6a+f9gGhxAif~h)qQf zI{Z&G7CtcPtEZv^5(F*AKA#ZXbEA^Tk3|)`e&V6>y7!EzTlkgqkDl#hIv@FSgE7Bbt8er|DuYOzR4nJF2I$zmL-^$11L5htTwN0Hjy{4CL0$Jwpjvt=!&x-$9g;YxrM`*pNGs7S#2)aJ zp6p%^xS3>U`DPL(C2kj5`b}CG2AFNle)^5=_)6q#&?+3Vi9KtBAulDD-jWP})6KN- z6M)cX#vP%kEa-5H4-PpizSbyrf3twjV%tW}kQ+w_lxVQkG7>-PqoT^dip4a3#bCd zaONqram?J!>|oMj>mX-h?jrY7;ZHy_j?wY@2^<4LrXQ`xN9;jtN1+Fxjp%f`r9&Pj zHwgqM_>atU+OMQfxZkDaN(}JtWc{~rKj59MKR#BRfI0H!lK*@f5l^5Fm<_b5f@8%31 z&0xNO8zgMsiZ7kUTGLE&h*Nyume9RBN*dMS7NS_cj{RG>?x(qXdn0r#7Q_|iWLXI+(E@k*aTvkmOdLDD?e3U%rQs45k+=B zGE9E?(}=9fsMkn1a3(%WPIK2`Bb5ePobLm#Vb8+DEy)narA#E#?*L@Jw8>0vvc6- zp~Q7==~IA`spZ(xlW96eri-Pys@m0jf|kA$AffzC=$spB*`5wQc!CmW0LY5Z{G=Jv zMx(_<+QOHW__7%l$k*2a2kdB)8$O=`B<;q@kg7g}%NBVxuiSbkoogwX)O9GBVp9O5 zT_S)0=WK{fz0i3RryL}H!w^COh7L?t^b=Tj!JQXQ8_v2k5Si)WD>6SWw){g&^O8BU z>W?mXP9dPWUzDAOLyFYWo3-j07!3CqrY9&MAr8A32H;NX;ZCiM{|$~U_6ia(1ztnI zK`OLbF66R)igGE04KUQOLJf`wv6C44szY5BB$O`<47A{C8wU?00;_QjOHB)WoBgsO zp`&CFE>d-j8B~zqTYVjx0*UMb@JnjaL|mK&$M4`M18Z8ZJ+J!pB8&nXM0KEXp%l2L?tuH%aEdW178AR7m`Q<1g) zf>JzvPqP+}Fx6pCS6^vAAXd|d-N)jbi&0nSIM*$e-w*}DF-M-|H_r>=3YQ98QGI#d(SemKgY!Xwv>~xqKXg*f?uSg)LZi#VtSIh>WfXL)`sW)s= zq7C18uUPpFQZj!elS3h7HVvF9_JubL9YdWG5Ljm&qdYm*G8sUA3%VUDC>qEfQvmb` zXv1?uT(nVb!6l%J*+9vyxQZpyb0uW&Wh?4;l`++?_j(<;HQ&0mGmk9Qq1nct2~dZcKbFCqv9!%vDzvPX|KWCgB6Z z+_Bq$r@Gw_h@&kKe*}NQ%`xyK4Hb!L7#N=-U79_NDWW}x$akl_5360STIL0nCel$+&3-PQ4yc8S<>Pt|VMsM>kBNPyjQc8X21?b?Ye9+T=Uh zV=VIFmLK3-tPZODS$B5l*!C#PZJaDueS()%4@&qFcAlLFHJ6*}#_Rp|?NjeVu*4%=#N*fT$nj#p@^%5hVYEi3#Rn9w3#B|KYnu!M_cx8=&YKFm+BhxqPEM zvNW2)E7;#L*?8CZ7$ZUMODHti+KBfP6|#4&1D7OqLJ!c1)gl(zYGfn)Ecz&!prg); zp9j$I#z6!bbyqCZut_-+Fcz2lsjCmyf_kD4myfl;I^<2(hrb7ZudtIjM#d^=uB=T% z;UJUyP!9Vtw%Z2hsJ%L`5JIle1Y>ILzD~>t3?ulzTc_`nlqbz|D&S*f*Xf>QHOiXJ znL!@vu)QAFA+Dg_{`9{J^%#!Bj&JL14NHS7tz*+|-OWHAYbH_cLDOp;&L9KaqG71Yhex zZ`eEra-7}0Oi?`^Zs+}vkob&QjuLjNn{2Ho+ zJ2%77ER0|17M%!Nj8j!Q;Y@3B)Z;`^_HxEGovI?5&9ku0GqsNq$TS=6^hK=zawzu3h1zcv{_~6RbF}!i1Mrg~g1aX_f2XYM|1EyL z|KVrz^Q(7$20tHDjYjZO&?G+r3!E71*+ka|4b6*&^O=d}0TyKEriz z3|5Y@1@T#pTS|i#2}$m-rELApU45%Fz2;AR=8yeGY*+Vkg-U7_KtZtxQJ^#8VTRTT zkurru*TTfD)=Auw#~K$0bJGL_Fc{T{qxw9t?XEsyy}NW5XSwfL3@?DSr4m`x$C0Pj zq3;(53wJu|kA#B$&%G?(rXNs@O~vXemGHsHzM)8vQA2r#M&20qKxFFz%|TNSqEeIuqW)jYO`0 ztK}Nr>$Vlk597!W)SQf<$SfSi70KGbcJ^5SKx{{NZUgu0QPpGK%<&!kRfH}U?ic#bLlzM1R$LB=7*Y>UiQilJ zQZFiCUdiJ*&fZ5;#V?gb+}nhdQ&J4RaPq_ID%nCLTLOQE_wXR~+WP5JKJC;$Iw&P(l&cQLqdtsnptz?F5=cUggeus3+K4&SgV2m_#rx zWJEKo<+DIS{j+O4ysdkl-&RK(K9C{KkZK*EsXB37GG&th1dCa&LLn@jNbHq%JF_^< zXm{ z;ExL&Zo~muVbhr4b!k=c0;#-D^uN$c&n_kG`87qGK>>Xygh@y-&_}7%||MH zCmxumk|F5;v4(bIL;>U^DZ32C?jB;K@9F?6Ljx)(z4C=aSbW8`2_T}opU_KrBIg^r zci)D0Sjk$)M=FdpBOaJ_dn6(3hS!SERXrmXsZKc+aqDlu94r~8!kp7V6*VwBVE?FZ zY_hIWEuj8~%dzf-zV3eF;7n#i4^v@A2k`WD`=dQ{VvH%DBnvzXo)E+5#gouxhQm^N z;MgbSrDUq?C-AOn?%zk&f^CQc>;VVpAUpWF#7T;YpoX3tJ^B5o9k{AWAe3e}`VQvS zVQ7>|;U^MAjx<_7-8|$I1ZfR;O_AE-q-!r$_t1~lhBu|c$KX3&bcinpmk6wMCo}X9 z@O=tCABw19@bQ}L@!-i$09L^#aJ&QX2!O*PUB5ye90D3KZa{~q<8~M z-45C)Ol^I%qGajz+bX=}@ejevty_b)oGlyvwia&>8u2!Sw}BCF!+3k}h_`ik>l^+S z3?uh}U_Cf3Lma!;7x7w`1NXLxKz#0elfHW{ny6pE!9?Mik{K}T#0t_mFvTW}K0#xDA?UT7G*9Z(vy2fkaOZyeRN?^NxU4PE9f|3}H}h7Ro~D|VPfvqP<{(^L3v|&xs3hWg znh}=i<~nI7u90!#*kBZJ@osvnG{TF^_doS>r`A`@gH3KPi zrmX|#k%lUkUu%wEyBc!Cmbn7w$EZp(892WHOj!3R z)+Nu7SSX;@vAEBds~~eB7x$KOkji@~i`68q5r?x8X0=;3<}X4e`Wv|H2Ec2+3-Iu4 zT28u%T7DzOmdH1p_GCv{`=jmlYW<(*WqD3E4z6}!&R}k4+NWH}zX1~gVCB6wEBBID z>+l6sGOtd(fft~tUMPui83~_!wtcNYA!n|W9eQyTaI3?2X5qD3is#q3F#b0@^^d&? z{GKDyNI*S+1k@=_R|2XFE_<}93(eL1kOI&YmY+J~+$cH>cw1FyL`+0LqbDzGr4$VO zA@?(H)oU~P8Rp-8`Eh7jzJJvoKO_Djr!s%yH@|~1bRaYBpAg)&+&2)Y0{PHt zS0ABhwrC_|UcN_0h~H63hx~Q}@{ZZQOvb`8G$zFNs2Qi{aN!(ZP+O<+kSDRUPF5$T z9?Ya8#LxBRd%$e_^1Wg2?2GflsLYG=)T2s0xRF>d`jW>0YjFwcG^35|05+-`>3Y=3 zNxN1^(?%&BkheAX%e<)6yetT)<{?zcq<7kMWCV~@M>~;wvy8O^h`MvtUMZCNo5?}S z1aoxPaQS=F{^VK*AXnaD*K29sq2#|Z>0Q8qu)nKd2TX;P&DMK)7drNJB#$?xrp<1f z67}X9`0sJ+1mFu-WYv@ zH_&z~5-qfCw5^x=h@KP<_`$^6k9S>oN{SCG-IFGOlQrk^CxoNUx9H;IUVZ*o#pW*Kb1wcf!#6nv9?@kaUzTP+p;32lYxP3qP4km>z z6_=d-=Jal}!N_uM(%&k7S&v2AcJ7yIgTtJjQ1d%LH86jFlVhBuwMqQbog0Rcr={pq z(FuN|2%Csk@*5l?`Hv|NPG+cGq;QG&&Ywc!5BsX0os|1uk(6))lz$a_oNrGhW2NXy zOJ)qe3P%)_5m}jP2=mw#!btvy%AcrsZV4G&-sV%cB)eZp z)g%Yns{Kk%a7%98uO#M{+_qoIMQ+LJ{YsK<$({R^taMB6+OK4-TfzudPR|y%ggHrh z$u75qQM9~d%;`3z(BV5I6+r@p8HB82I28xqRg&mTUC`WQJ}C@qYp#H$H9u|CWU!CpVACh3@zIU92&$l+_SMioTdmIUQl_&IKeElMTH zu@W05WvRom3I8}^&oF?skt?xZ7(WO2J6pdHUq2)Nnu3aIJ*Ip2y>PzfQDiZ|RB@SX z6;%(PjpxjBea2InmEpyBgXFtg49XB|?>fV-+pW)NST!JNd4 z91;(x-G%`mKmg=6UVDs}T{vnh4sX1#hvR7jxLV^`<9+2rpv<^U1O?N-pjKMO-7_NUAk!Rt0 zQ=o45X5IHXy=C&d_+FRiE&rg)Z~G!2eGz)L7BeP85~wt+6Y8e}jO`*y$*p#~zMPR| zLv;<5jC*9hjMuKTVTu$($jo;?#ZP)K!a_4n4nKv|z|kw4V!#btLAW^9hR?&s6$i2x zz^?=E#8TLnS%#QT;y7U9PP8gZfjX22C*G$gYql7}U=t6th16LsxB?J7dKoCIp-8K{ zEb*68st<4Bx~FxD*B8w4BkYuic=3WDVj72I-U3h`UOb;-8LQ8^@ucjKbD-76Hyu^_noj> zS-f;kP*{6*3Zq(uQNxBplDF~rPxiC@#i8g=#%szGIoaGG6IWZEp>S&aJD$E@93mEh z(OoNtz1^UNOxXkqctfC!%EW9OKlwTSv0UypCnvvRbMiS?^6VDwksREBu6#SmEI7sg zpdt4a?!)}pXZpi$pibu(^ygnhA5_YeIGdQ0>;ve+atK6pq2Hyz&VES2A4x&gw-p7u z-Vh3Y{n_|mgBl9|CT4$(M{NK*{Xp@k4NwUkisb=+9DQyo#Uo%%A4=~{p#`Z=cm~*# za|pCQTMiQ)Djq|3YMsee4pQ-ZwDL{2sW@6uaf_ocm~X&(gZq1d7!2U{^jp!TIoW5# zZZjwQd2xDdwK3yPz|wAfp4h}z5-FuKu`EC5Vw^O`gH5pq8F?F{5+{Q6rMF@mbPllq zl}Ijpe9MKawHpb5_n}_2n5q5K4jWrbfUl&Z|3?D zuK?hhZ`o-@+^@KFl~*;@Qiq~DhrDm#GsrXB8hpT~c(crf#cm}CBepUxR?GN++_O1? z`PvS+#Qe^ZEt^YkHDjyoD?&apHtl_3;!FC5ik81=={dkQGh-b+zBvOubbnGJym<3! z^g!nG?tkpay4YO0l6mnq9k}CyAZLh=pzf?g$Of^MCItDob5_5xu(Q*MPB&w{#?14K=-Fm0izH!;x(=y=lLPp5HvIPFY;e;$Cyh4ARZPTi zZ1N>!LpWCjU=*8-V<|qe8hsMJJBBsXfx!ELPG;6-bvJZHVlqDGVw*)a*qeyPY9Kr7 zPQt_MF2X|_H50%~&CFSMVqIT`#O&TsRZ&AUMBe=x+x3wZ=xJ0$k!PGiPq zJmu;^4uJuU#*9S9J!MBG@~)sv>=AU%<{VhpWMVz|Dsz>19{3<(`Dt|fcIUFQgvSD? zIje_}@-Q+EK83*X6vdZAf}&4$)3S4cPpSIkh>(e|B2_%BRX=BJ!&QYSv~RQ~z6R7I z_6R;zWunv3hpbMO_vy?4*w^^Z>cQqD!l|`!vkxKr--MAsV{QAkF5d+u;_;T$;p`AO z#ebW4gHUrCGB!AA#vm;<8c>FLggB%q`fJLzaeL4z>>qKK1X3w(6;_B(!L2w&Y#UOo z_dhMM3%OKTzEu$S!OpeKT=67?t8SaGT%1 z@^6aa&)Z4qW-q zdLR5#zDuqM$=3@&ULTM@6gAX1gxi(%-Az&tZ4N~nZY|oRJunK~4QmfXxt~3RX9b?M zc=CM>o<2OQ@Z>MD<0GeN0=q#5o_>HQc|HLrSM=J5?4mULN|dhU_k}+0`0*ct&w77T zX)T%w^Jm0s{U`oWYJxWrW5>bC;9sINB!DW|J#yrmOEcopj7|Aez6Huj4Pq(6u_qxh zvFoP$f5yo&1l5N#v6UdRY|7W}U^Ph)u&8xQF18{7$C6pP0^H;$RZIN}G43ZIYY&Np za~{sx;&+$nAn?M6|Mi8lo%@6)9sO0jPG1Z?0C#N=HD+vu%Q*qKMLWb#c(c`wOs{>nYGA)uR z-dOCK3_@gj`&FvpBY?wS!b@?mZq62!zZ@S4ZanGqgsGbD7GSk^j3nGEY4tm@4h zUe5u4S7tJoU9BJv_8YN#*+7D=FMRcZ8>}@a^CB#pQ`?18>oT!hp)Yhi3^aDxi};v1 z2PX9BBZ_1KXE~}Rh66{PUa$zetMQ{g!=c-Vwq<(2_`AArP2br;i0{nIIo=Xfn-@z* zK<&|U73`KqstzGp$75(S9B3$UhB@1h`JpMpfF77}Ko!#T11{P2!gKop8|<&ZuTM_J z!F-HP%2BkrnjVQFY6F_dvlAens%A5A0K6(;DlaoLdQ6!IF>Vnh;*KItGn3cw0~CTO zC5Ub%W6Hk^zmMPZfioV&Ze!&lnDb0p2HQ(K3l8~L0x>pcV9{TIGKiTM9&~&L{FAuP zIf4gyTBd+*cnoHue%Y_j3M3gI-7S)jqYG8j5eZ6bGSLV*dDF|vjbe1bM(T_NG^O8d zgtypgMBhum%-3u_@gy1FVj~@!s@G`xXSXR%4xj*s{1BjUVY%Cq>dTe~sh0JMHeYyK zspVNUXvu4hw;Zq~O&%58pz?C545N$>*PTPaf6b{ES zt4|!<-OsAB_KNd4=J|evtUAo5Zbbhw*nSCMLf~kK)0jZTX96tvU99H3w5+Tq?X8Tm zw#(|)ope^JuObcrmAbGIMHz+UnSv2U^Ygr~N~*}@jC(o*<5HxT7@gORLZ4wQpw=f3 zGsm-AxQkXDzd9d)6JVTmY{xb96 z`7)g7>fX%@upD-BLI!-0wSJqOr#x*lYX+Hv&1NNrEs^QYVl^lJ!7L7JPaQE^br|sR zpGv%eGk9ZddzJw4+XRkm?BDAVeKFM;rXlNeSiZyMXNOl_cRVP^?=*vdli0OKI0(^_ zi-~sqql(1IV4JhdBt3_LQKmG~@!{f=*O@!w}=NF@?n{CFg2Ak?YMGt#Vx zZ#T}tw@Jl!cz3~t_^8NgQ28zTmU&mZi<@d0^ll;b<8Gmijk|>m8F$NS{1yIbspkPB za|>CPxxEK}b7}q+=cqgmF0`iTbMN~>Y)unM?LPVRq}oi&-uKj00{ z_|doT55Y{ao}0cG%>lYt_o^rz#5RdM&S)6vRd}zAO~#JML-$)KEk_F%pWH$f$nzdckymb-T6UNw zs}45)W57O#dHMI?GL#=^+$vMnTIx4w4N3U!gl?5c{t{jF#(XylaR{PvEtJ-_gM2mK zm5$&a2Pq(8%t326c$Zj*;q6nXUPcqU`ig4Pg*JshL!P#~qce8gUHR4~+ucol5-4#_ zC@nuujP+i?YWJg&NXv;2TpyMAprv`?it1Wo1iL;^t`S0~aqFwtn!y@&vf&2*111}F zXfg+eYaNR)!d7;+;RgOvw!u=+OC%f26y|*K(ajs2jKj_w+~~inoSoPynF{k@eW{(3 zZ*W=$fZf>tPIa2w7?VdTRo=@(o7Ajf<8m@d{cMij-;A`yT^w}JSID{#<%~`n(A*dKIX1-N?o-fh z{TixE#nq83o7G)p#a3-ak|Whk!h3X$HX|m!vNx+uwV^1jYlWlEpH+K>s{PF`RJ->k zRr{PTRD0kTs`mH4Q0)c3SGCh$sP>*aRPEcoQ0<_NNzWImy}*Y3{1>X-@H>Uco-b5; zo~~;9+dfY^3=5F0->LoO&#et>mD<7U73|YKw>F1fgzePs_}tp4EVT!2RIulNq1v96 zs`f9wQ0>SsRqeaJQ0-t^)qd~`)o!pg_PH-qd(W>F?04PTIu>I0l<&8dt&8NJ%^BY( z;yyW(=+@y>f3L&^F>sXtIytj}}0T$j%3xDv3Sf=17nCjM~X4^xdjti>d{zrK(Cm-g4j0i&Mvk;rmc<5hg@KK-eO z?)XGYKZnn7=v)z$*?NGdFYCZ}-yu`g=z(MW*|-ju*$l33@}L}-=;Ayz>S+(u8CI$n zXSwj(GmTpim$k=waql8_rRzguwSVQoPA|^7Rit{8uk&{_@_ zBUj1R=+Kj^WLthJJcjWk2*B~&e4$F^XW9d2dPzvHH?<}4jb?x1AH*8GXxu@W2mml` zf7HRL{58xMwa2(MpG>%c`D9@Og;Mr!P2|fUf-~F4Vetm@kpuE3L$Z8EHA^vZnAdso zopH>K9*PWg|ISehjsoHPiiwFh;&zDCD^}euFALt}2T-(mYu8{aw( zHoDhqR;5`Pr{eb%9(AIj>R?R}%mH>|?0xgOO1>bug%rt9`W?poWeGXLEnBFd$QuLd zet9Z7c*^}IAp}dm0YmU3lPa&LdQ$jse~Lb7I>?q3AaPKxQof)=Zl15<|rUtfjx_TKJ_;F_5oVSeup}0xg$Wq6F6Rmg`op9$l^YY zGU;W7|KKluFLptNt@?|VqdXEXKsy)voRz{R7?L%=6XmMDi!qVSf7TQ5V`eqWMoq(j>WF~7!^K~V zQX&Sq;WwG#V4$V+04CmpSNN%)uOW?PNG!2lf%7B~&Qg9cpM!@D>|a7GCL_Qx5o%NFJ7=A;H7gca@a_Mk`E zTdtC?sU6lUP>GDME8|O3xEn0|EQimk$An)_XvRCw#C~|uujo7X+(GJk5;O0=^aTFV zhsG?s%dA!($7wem4S)~x8`V@x^BOBTz^JAFBp*5JqNUG)R_T3#?tQGO>6d{{W9gH| zONEO{^y?q{@$>1Zs6B9l$FuIz?pJmKk5unZD#v?Jf8uuY!foC@T-7?hdtVVH%a`f< zx|aL|S2bH}FaTWfTQ<@+1-d^~oyh*WWj^z4e@5VG4?Jze25|KFR#r5-_!n0`cYPGp zQY!$DzxMUkP)dI1Ss6g|CyqcJX%{{)^%rE=m!Hz^w)In z^K@a-W93hQEO-HCXg6IYh!22%>zC-d(-3ypytw`YKsT2@S-6n?1Poii*(<4`I!?eJ z3m%SyHiDzLuA2_nT<9}T)BF6R9wWyxKjUBPBm$5Zs>L-gm`k^r)neIOwHtLbH;YhAE@W_#V&rt~Y~NA2>o*nczvyxcpwPnytqfr`GxKr=D4 zhD+dc`ryB(h&PtGRO}LQp%TY6MA2{%HKJ4@MFIQ7@hJOu!1c|=!k}X=c7K8c##$lZ zp~1=X9v??PT`+DZ7?y+l#(?s_ zEL&VN&a183h*#YIhUgK1@PebxYt`0I!THW>MrYb8YC55dM>K9R;{gQ3F{9#$hSvln z64oISTIy~db>FPI-wmf@xpC=8(60~q7hTcWJ&8sNJT5e=Tk)wIJ3ZH3N^~;U^HVa` z+5J!rK8*7wPu14nT!oL?`YT3f<_6LG+li*e!NNiK0Vln$uH_$zHxs@<9G?C!Dg&x8M6 z!hbSeTmNIIrMHav$H=&7n0_8=>nHiYReotOYsPVQa_`m+Zym#4kl$=M-$e+z54}&0 zKx?g_HU0u)bLpb65?+?s)djTSRbJB8&xXA$8=dJm3>sTZ{Izk=JTuU;8TN9fACE(+ zy&LOV_5zEy=CzO9A1FC}L~zQ65iApLWi6-jJx5-x(_VhxtQp zocLqzP6K9Kl6DcaQ`H@8Fus=j5h?nvJ^Jo>JAUMUp?3%Fc6{OWU%$K)TQm~K4EFo9 z)QxD5*A0)2!Ut_Vj+5Ymn!~8qc#8w(STRJsyo3$gQ%g$}W_6Z9HRaV9YwtH@GRn$RBEn zhL;7+QC)9@l3y}shE@cEc*%P?@mEHhAXCmMaL?4-?=aOJR;*0P?t|->&&@Rcf}f*A zu2O^5xCW%uavv}dCcbjZdh$Gi3AAi7Hfm=-m+r-Hy=!N@*Rn^u>G!CCZK1iSY>EO5 z{=V3a8lorB&*}imk!6jn8pBg*>m#2c4lpsYjsaBU<2S-Xc{255j86*_NRaOk7h^c~ zZ<#ZFu#%)_Z`^m8b0kn}P7btebT~ZO;qVr4CpTHxo4E?VWUFqWW(xRe z;TUjgMU@v^Qv>{sUXVC5iJ;Y%uwMmkr&`?@>PWJ0<$oHRK|m4Cj($PwErz|r!5Z%seoudB zJVmN$!T3R-p{upjJMcgF1(n7f`02!SuMyjY8_l;=c zDC^W0*Lo7;dhWUV?z{KC_ViocXV{?KZ)|UQ!I;sn&Dfy#9%+xkd0%DY78!dViLtl7 zbl__KD$yQXaxr!&6Zq>GfYj0(@t6BSpEQ^HyLRn0u%aK^Olxn1Q^qJ6d)=U&lu81d zvfrn-X+RU}?;R*!x~*lOmO2PEjqMYMT4Md$O*L#5^Ru^HM65wee}Z?9(W)M&Bw8-q zrd|IvdY+O$cB%LashD0y#j9AxeNaP>6Z>s8!10kDp=IolmWlwT_5eoX7j9cIW2e3| zv?BI0zV9?H+>T}UWA|j@4SMgl4vM{OJl*wii#8P#t}}M6h!(Z`K5guM=AFR}AuYC7 ziwzlj#ttPn*)*w5_QHt8K$Qd1S({1Z#Elq(-&6&PNFsjTokCpN-!i1(B3NLFUuyCj zi?P`e;=>%of3tYTW^7Sf;YFLwqT=4zlbov7(!4|8*lQ-muHIArY#n5#zvEr_jGluE4&vz@Af?Ms6nAAUV4|3Dq$;biT6t)3cK>{St@pILLlGapZ!D7m34H&+fh;`oQy?sUDprP`5>CywgML>+mcCy4ABUP?tP0XJ5E4G>g2k)%t*Z{j23GRn;=>Z z|J;4Xr&PpdExX*J)wkWTHorIYFa`8sWy=Y^{w?jK{8Rl8hBfY{LuZJ5k5ixfw%Fkv zo+*2LomZaOo$Pg0`&OpX0&26<%0zun_N)4X9iHjCqh>bmW9HLjg3~Zc`_F7|<&?O& zu(g}!T>ArBu=PJz>Gxn)yZzJO9=3<`gAgB9Q|^Aj|KR>n;^XTrdti|0$KCt0FL*N` z8fX1_EU{tmN9yM?x<-+!RVFru@46(DNiC^}|87y+t+Elu{7jyH*#4pKfbHy?`hMSA z7dwLvJN;+I0sXW-z3Ya;uIB{wnz_enPs>g1e*W#V9|HOyL#<9YRS}vF5A&cLRJ( ze?z}pv@0ZY8kYT-zQcd_3;JBB6uOcphd5pz;yDy9)nz>aw; z?9MLS&tYb~w#4u%v8}_?K1-}?VF%wszR3^n2lr>3&)Cn01&A1Rc6$f-JnhDJ@^;Xr ztzAmQncWOL)tcb9E6C)Z~`xWD->{Rj=wqgg^tyWFn)-6CcFiZt5j zCXQsmQXBp*F-6MGUgCF)ySv|VciW~zU^G|DUiK&VjG({0-LR*(YqDV(7w&dV-zGjQ zw7<4;Is4$x>=b{{FyJTrLtFo>dH2Rz)vpU?mA&UB#)HZscoT=A zuQo3!zUjMhpFC}{6KR)CLl{Fl1hHuZupg=_{z759u9Kv9#cKXiV*D!;IZjI|Ow)S> zWMI>`l&GfPU8Ed|kbDHnpcAf%&(GFyJe8Xz+9+B#`_VeK9>u_{`U~0}@CQ+nC8VtAn0J#^47Hi6@v$=cW`!7IkFthz+9K3yiUq;0xpo?YYXVpEX`E-23ca z##aM|3H0mMFyU3JW50YFq*$YZ>|VxKRj*r(O1tB~kD;Vs35QxmqN$%nWDDdf8iQ|5 zMH%s4>2ngXP&U7;+c^V;CqyL|7Yo%Z$9kTfd!kxr2X}sQ=ya63%omvDuwyg~1;*hx zl;e4Jc1q6ySzp>|enCK1O4;AzpOz>C((&kF_8(T2`SX)9KBUur_&?F`j7}EBq@jmm z0h#BLr!y;)7$7El+PeT?|NP4hr3HZhHc7 zq5orPMycmBS8mPXzU?*dP_)DI;x^9qe&Yk2yZ7zhaZ;@JSheo~eEC1t?h@U-WiG#^ zdjk2X`eC&hGB}J9d!DuIr+mX>)tzL-SrBnk<6BN4G=^=|-}A8S+k}9<49I_z%CFjngVf;3bCewp z6gi$(CiX3T&w+Pk!pZlpJ!3C>+LP=pSBD-1PSG#;57;}YgQxAs5CE4>hGeU(NjHsy z7<$^qfiv{$XuR1{sw^dxZC@wRq~bbmL$W?+B2jXPm?8@AEADz8wgH0w-17U7yhT#I z-qUWQ9{y=b2!{R%iDwVPJVSh*2O{(;|Mbq#4lYLQZNQ7)dI#6gKa;~M%$Wa>9+O?* z%frSPHWOozs-Hu1b34ynhvk-+8dA0{s;;g+`$1>;I{iTBr;dzIy{Rur6#U-9cec)c zpzi^?^SnbpkkQ%F<9{IC7Y?^1?ar68(u}vgvOmKTc&2}*?+M)D$k?BK;CW&Hb5goz z#y0&l$^>(nsglDvgHMZdnV*hWp|^CP-P-ECqWZo)g|dKtBv^TpUCIv#e6oI+c~Cw) zV;o|1fn)fr8ltbpc7ilDbGvL7M)xQu^yAaXe3-l|lB5pMKAMvjyuODbC34s5Pi5tl za|Re2Z=-~fK^dxvQ9r@?0lwG?)b)Ik9znJ}F9V34$Ki*{pdSv-MyDjpzeTe^4(mCx zy}_VoIiGwoatn$9gvn4u2-A5BybodbMF|U>7d|C2Xx#Euz!|5Q<2f~RWb`qH|AF$o zw({HlX{P^S%txJIRM+4C_*XhEun1QCk4SxdD3sLaDDUr?ERgz``!zJhV9#1PUw|F% zLiu2Pex5h;{d{c!K8Mu`bz2wUG_yKp+!y|A#drL(ld|7fHAk$4ykwPeUY{-J$VyXi zhUe+v4;f7N82f%KGd@R{6rHzP^%tS_8;aIG=eaUfG+vTYsHI_QEvzDTqIHsb=1Wq^ z*omVH)@ z0;l`VyFF_Pi156`%rdMW`CPJayiO^ACTd=gmZqN@7WB03bgHIc8hV0C$8*zrNlxZm zb#(AAG7@JtOg?RxDVWyl=X~8BwZm;H@(K}1*-%WVwAg7Hu>BgMvzfDRa`~HOAKuz+ zL@?%9>j*U2J)1vpcE0Dtc(y!4J0xcFdGdP|2{I&35#u2>lwN1MkPJB#!DQVzIP?;A z3o!Z4@ok=&ovYtc_TjwU*L9ve>H1F7XzAyorpeRx4X`WIWhzWu`=tk~ZvX`&QhCKP zMI#&>QEZ!GfcJD2G_fTcap87DnQEc^1Do7%ku z6K!MP8+#_>KK7WE>Eh(jNtz{+WJu>>1B5KREavIEe-CXP(0MEipytK${wvY7fn( zGZW!{=vl~BguSQ;3y<*TxT`Z;Pea#&CbZfR%R;A*oq=}0C(qI5%vsySt%YS=Df$B>lG&@9ks z4~>a-y6SzW(SJ|Ft3*CixU<#zNX(A=2e1zoOQs@KkBR2x3 z=B9*R)33~w@_!`U+oC#5{{teEkH{Xmka&pXYi}aouAy1DujIpvTzIoZ;b zD~mYFP8Kn!W=y4)?Lr?!YiI1Y8ymh|zCrf(snsVtAm4v?O7K$F+^h91X-~?t97DH} z8(|r@o6K7Jt8m>^5QG`ti4ri+bwh zd1jJ5=O%{ksAA5Ip7xLM%v$evjPta8B3l>PR;>_Db%0GjuM7sq0UE-9oUnO0hrP>J z^V*Lb0Qk^l+=%zw1{$?eOf}M&!n+QSEX{AHvTKJ@k~{^Rt$~94{kodJ6EgJkGLZcI<1u-WK@TDx6xU+DPA!CM|?zCtXiU(+|+Vr-Cy?gCC>wT5UA5*GLDyGN?>4J{P zPsyga4b}zQscC3A*_2qALgs0^jHXsn!?4@Db$DH>#W@t582{ZP{d_h>wfBY41Q4{J zX}OFxO3ya9vN6{7$V0`S<;AKWwZELW%4(t&eL`&2t`^UE|OkorPp}c z9tDS_W6>h%qZFS!?N@R&+bGvi_|R}O=^~skl}vnQGxa2~W|M6lB?vDBe{>u|Ut$<1 zR%RFJIP)U|8`D}zM)Kt%KMNQAVoNn2sI*r4&?Sy_k*d)j{m7ieotKw6+!npw7MuYJs9qdtA4ex5?p zgrJZ{exdnGTrKKgl8N16rhZJ#tW*ol`Ow?M`g&2|BNQk(3_E;2^oq%dONh6!nIQW4 z&@V}iikApgyKKMob;yn&F%`!Vq*#HGBV%ZI1SXg9Xs+j3_8PG1ykRjOx|Bwj9qRL8<0Tf6!lf&Yr`F;PBX^o?q@i_^$FSw|NQ= z+CL2Z#)8- zT!MR!T882w@|z~+Ny+DF|2cd=ZVSvy1lTfW=Va)v49gt@;BScoKToDE6#Ul)z#pUu z2H=mtpFImbRwevdTi4P@BO?W;AaWBzBhpj`_e5M}pq9sIfUe^hDeL2%^@Uz82T!mnCID!v9E>JNy#rsKNz~8i0B9{<;n+0 z^s7t-u&a1+0`O}#f!HjAJ6Q`$Q+w1xN2ws*$#{-Yj+&U+QhwlJFaPtcH`rJs+V(;$)C&Q z7feahFYuHqPh4e;DAyFZqEqsT*9nZ#6&Lkgemm0;BlS0!h)88o zoFV;K>20~sICa(e8$fss6UoFK@^v~%PVv2C*YhrdH97lu^V8HXi&+izc^*p0k$0I| z3Ih2TvkTJFn#b!$ZkUr+7AWfVy?a&XyG(HhQl1vVG7jp8BO8j>``)u>@AO{cPyGc= zaT?Of^cs2fv{Fo>dZY(mbiH9J*Gj$Wc!9{9mNxcf_SeMJi(|Wnj(zQh$rpV`Y#Dp5 zI^x^qWMlmSz38IvO?&od%_jg(hTOD3%CG%b^TxxCfk_v$cQ=nxR2GIUD)sX{!)4r~ z=t}gL{Od&=f)AsFsfBS`gP3${4`y^buxkI+C-*@pCjM=?Y)SiAE-$2=lFNp)ZZ1@A z0_j1ymG1~`NxNvoW)geSh)wrr>74-P!g-DC=Op?R?UUo{ewDaXJ|K9?*L?{aC0K$% zm-Vy0zeqC`c-rM$N*Q;`2@d{=YR8po;9_K^*caATUS{b7`m>E?v{h&Il(gWVt^&($ zYE~S$BZUpr++}uN7!pW&)C6Zd%q*bOx6P^FdojDSS@@F4#=$`9k43T3tn5yunx08r zofjNE!`91L^SMG?7kjn*bw5erMTs8I@8kL`q-rtco_Xns&%3h&rz0r zN{xhE5>9j;XCL-yPbRU4-~W^7XsPF+vy2MGEO&ONFjFl;+?BB>P%NkG)KRli-=|l2 z9=exG9C!YH6Ij48PpCTl@}E9D-eu&+#;&9G@tf}N7^Zd-W_-AzI3$DZ+>bGCdJW~A<>Gsngww`3C0lO7Fbi}3BX1S9$XA|X~y2>uNYss#@Z_ZkV!V>d6!FU9Sf6Vmc6-%vV12LqybfZiwfR88Vfy z^D`Jbwqx~YA`YH^=+Ji>fKE@_LK$=d*f!jycrFG_e(LS7Qk9tCX_v!ns8cHmv}u;2 z)4qq&HxYx5v2}}g?>#g3vn`F+hBv6%2Y*0!O?(Z7<*?6I=E4io8og<1LC-UVd*3@U zE@)&!cUvJvD8CWqlN3I`<<{Sa*e~zmn{ZeTd865+!3BV){c<8sdyro==-;jD07 zey2T`0fU`PMw%NB2mkb;=b;ao7{~A7xvQgV$b6CZjmALO7)Vo{^<<@tja-;B%}pRm zTy!chPmI?a@JQ|{wIr>P;+`N;e!xA-g;LiU6Nbj~aRcLV=gvnd4aCA-LG()|y1H(u zlt^z{4NBW)WHV+H#9N?NvIH~q#G9yi*|E(k{qW02KV0&i<6WU*M9;F9pp{zF3Q!?; z*?T;Jc~E+I$Q%m5m>zri#NNX%quOQ4Jd_D?!72qz3ZDoGTF*nL+39<@vukuInIz`; z;U!Oqn}}}~7Ao}vJzo+z&pz#GdmUCvyncW^9y!_G9t6Aw3f4NHMwo(#7HB3_w0_8g z0Gb4oBq=s1C_Z*+F{NQGvlL})7EaKSAEO?@SvYb)X9+Yk*Kpq`_b^@CN2z( z1#+HTX&|ihvSrYm12wHAf2$~ERSmsu7A+xGj$IkLhbA!F^^pYbJ?(3tvFp+@AqeJ} z7SPv3tcgarKqD)~nd*%HWLP)qU4GET^+O+tYp zd&Y-WV;%4W?nNsB(E>%E`MTNQ^d+A4V8(vN-$c*z{A3%xSNj=sT$s2T`U!*ybhHTbZAgI9)hY68~%dD|9fYjN*zW6b`u_7e`w@Mks-4~iZ6gFRyR?EGR|;ZElZwNO zI{PlRAIWlGFABKeXA0W0PkD#7E(>RQKcc)E=}cY{z!gXk&v>_~ zyP)V>M5ObsoI}5aXmu{sFuuYfeWca9iQkWYmHsJoyz74Uyj)<&=c}VL4t>a-DW^tH zQbsm$2`%9Xy>9kr3LgCu%PPOn4C(r0{@pqDJ?NvOaeBY1`i?)qW>YBzlD@+$uTX2K zjxDrnhjpBpB6XVpVrl{HC1izO0Ni*LjQEI}QBnDFtf*fBwvSDe0Oy3% zc675P47C$8NQ%q}JTBTN>ae~2sFIm7#yCo5YDZ9{sD!v_UYdr_jm|ghftZOh(k2*t z5_eFRjzLv1QAp;K@9bz*N>hizd%co4`m?KZ1`Y6S<9ifuTkW z%rn%1cOM{WUbZ38Y)2-6cBWbp>0>#=7QC5)`bX-!^~|*FPdsfCk-y*wAavscEo89i zI{{ZGwgxX?6_gf;(op3I(XN5Aiz$x!IfHMr50RmZ_$+~WS-@oxT^bAsZ$!7YD_|=9 z3NgG)iM!h_?`cV914mu3Op>q`#Fgo|>&b>eDkDx~qPM+K^p9`)_=o9xu`Ff2x9qMt z(s#cg?hKv;1eEzc&GxkQ$STfJ&%=4r#o{(fLu=6Q>l_Omvelsss>;?T@=1q?{1)qw zw~@XGkTNv0?ODpholjx_mk6_ht&%2v*Zj1a&?=@Vkl)}@BK4j6uFzqKVEhonll7P` z@aTJ(lc0{KlJ{>2ab$J|HwYTBO`9N-O#h6h!jm$Un4Vr5RGFLhX6{T(@Jae4$-(VB zLBf#mc`aW?3>x*d)G>{>h1u zdfPf{ZJx}T3(?ucJwrQTt^YAcw1m5ul zi>jg+S%ZU8JzUYl9bKpK&@^hmoEcowpk52);hdUM-L+Rlk=DeOd>_{_bo}uBUp5(D zy}soN@gkKLBI%(aW^=A5vs9UcdAudt_Dveo;2X%`p()UVmp&=nwKrPe1CRFnDqNd9U$0s(vsqi;2#Bbv0%+7D3XGMZ&YXgL0|(ykR)I@R>dwwW4s$`hz2HjaMX zA!bqZ3m1droCsmNY-2+|lo@E@n(1x*I*EE33;H5&!Rs*BVtsb%9|SL_FUv5riIr-2 z5c;F6(JC|M6k&o;k)O<9=u#fktd8WB&ge?%I7miPAL+!_(5C{(JejBc-+A<{N>*fH zYHXOMM{GjYy_ZZOOqE>>k37KOM*2YP$_f1yD!~LcJ*Nzs zB^p~|i4;gCtW69Z!H|)Ep43xb_`;{(1Ow^FAO1E@o}q0h`x8XU$m#e7Yw-FcF_@&y zT)HluhY2#q%hKc`;r+(V#;Da&tbxPl(0g=B=I0{Zn4v&1hxW;L2a=fjY!_Yyiju%9 za{*$4kPIn;I*|a6b4|p{h?~&OtS#QD~}% zzb#TFjWb=!H{6*<4V<_$hO~8|5JH$mm6B{o!MzFHg2Z=q9Q|8J?xw3?!R zTYT!WD%HX$Y$g zMU15FR*Rl3NReihD3%cJ5fBkPX<`AnM?i?<-U3rt#JM=K%a$E_jc%r8*qKg@-?J1U zeAdVtlEb|EmDeLPd5sWQU}6l}z#eNZJ&(Kq8##VzRf-Lt&T{B}E=ytRXRHs&LWC@6 zvCG(hPjctEA>d3DLzKOJ)hKYM?ameYMP|9hO-3GeB*z|HASr=T8Jjx;ncw4*7#ROO z-(RltR;$^5{nNlbPWDifa;PDo6ok&-e#4U~NUqK;AYGS%D2+mRCeHj)}IJTyemu*!9(|5`cEipz+u^6|a}1JIy+4$yaD*s5>&oj=~B zD&}40Pxjp+m2~JSDz0TFqAAD2sfvy;4|JoUQ~Am-f}kH(>pf~K;KA;L@9?^?uUq6U z_yb-xfZ5kt_~tqF#zgOLcIRTbF;Rwq`;3=I7=ImptcK%j)LD@S-|}s{M(F*Hewl<=#sbqbz>+uX)d#mUwRYs?=jH2QTGjOLEA5HqJg3iz6Z30wWE-8$Ey=wn_vgg-cf6CcPe~SII{oG~m>IMFkvG37u zqF20gv#N`!{S(82-mAconw_7x@;bS}6ItO)k)9zx*|#GzcK%5!L_B=InpTxh*LjcA z^9x1Mem_6j@5?+HeieS#iQpb`DD)BJkz<137j+9hgPD8m2}49iM@3|WM0xN2$g^_D z{;ABaGvLwXLs^Gde&GCJkq_0X`nJ{YN?UfGPhjbA^?7N>C55n7gM-0ZNP*nn3z|Kk zF&joLuD3issQBs14h2dzQvy2Fv$g&{pY?ZoJ^usAEWu~@u3&**KdbNJ*gHlh;4Fh- zfy|q;?@N48_S_5O-lUSGq>Fo-0fm8L*5`l0D(5$-SUPOnjd_zcXU&9zmwdqyymZcK zpb|&E#NbodY%C{V6H4JZE8yJ9MUHbW^L3;I#y>?DmCE4D_itoC4l>09mF0Q;bjA_i z_7T2!N3c$wed4!*Fp%>1a`EIJVJ?U7Df4=dVGqFp53^4-m-(5%=L41Ala!)SZWgb$ z%lds96NaqoO9`C6+9^{9BhQv!Oi18OXpuE6J`iu{EYyG4^e5_;2IzR8G(Be=w+8;E zO`sHqDX0CtRc?+$Hsqakac`o(;1?HZv5&};ZYg4gml(} z^-sKW{I|3s3G6X%J3yrV$(il;7VB*-?D~NV-m(6hsnt#TS(3aTsPPYzOJ6a?+Bi&0 z&rH)l33_O;0~cCe_3cR2gQ|Bl&I=35)Q7^NnZm@*Yr{V4XTz2e>-mYy`^fN$soxzK z1^p7GH)D507;mrb5P|YL+gHf!Ld@JBQV^64KLYG&ABBY;`(EU|U)4_p*WgoF74=?k z(%@-#18dR|p&}$l1`&zC*@E{#x4fwBy@p<2zRrS~l!V8HXC_&{;gj_|35k?z2c>0^ zAUAT}w+vk8r_J$CO4d*Kr#d@(hwxG=P0ZOX{z-}1J6C!Pd9rt4bh3A{{?B$iG6j!9 zI)JI71v@CC{R|RP%3hU~*LJ?+fK|v=qW!F;3&Gzqx)caX3N)nzlGd=kV7wA)I3hSY z8C^(uCP>o|r!;0i=CkYMf|RE3^?%#*j40D~j)rm`{EN;$E(FtGB#y|TrZ@Ei3O7g? zgMlRDV4!J)uV+MOk0blT=aixtTMs;w4(rbA=$f5?X$WR2f~hk}+9OTDqi;t}tpD`W z>_0UBk$+8Rdj1g0KhD_WXgWkU*?(s;qo$1QEn^ur3AcTn*n`VBJ1?c@QZq~Sdd$OR z%+dSab^6C;XL(Oub=3EYUCwbb&&!UrM@%$fkFzHyd)j{~_QgOOm}#KBJf&y2;Pd#N zB+Vz82X0r{1Lv@6t5&$|zWOwR&RKs}wj$Aj8;CY7jHmqgaQ85hqPSa|73OdEe z*gNfA$`;DXy?m8Ei#{;TIV$0Z{WYJ_&6l&@cEaF>2 z*?3;3%lOpy=0!Wl&a|Wwn*;rsIH@QMujcuac&prd1{`9m(@D?dU~IXy_V-{XR2z8o zWhiTC75_{DknbytYy_WRzzjij9^Rd%(wdHYEF$)oy=?CJYGu2Xjk3nW1p99NT&N69 z=KYx8X`oXsg{)tRT~7-%s`g4<_y8^7EzxZU-$j{H(oBMf?bSoZ?wEt!)Z7`EeJ=Y@bESXw z$NHxS-(@u5Ogrx6Whgn-k&{Xd<1c>qITz>Ma_**yhp~e*%|Ik*V)Ll}ihU4!We<)C zE--OXKgS5)rvK3d%ji7#-s;~=K7^RwipmY~-*@iddx7E+`u>C6ym~eWeTKeeGQpS& zF0q}=rj85_VQ!43SxLjEqTD0(gPrfDtY#Cb2hVf$%QnuYVb>5dyq?0tbUx){`E=0> zy|Chami{h#PFSfQ&-nDsnwO$*cwdn0fejPoa3TGQ=f6qG z8RrFueq|)P*&1T+kq^3K+V3{I=b2%$snL76$aTMcj*&5u93h&BdSm2R{C&CQOFbh5 ze7*QU+cJ)1?`FX*iql9{9vH?ZINE?u2(}FCNd_)^n|D~XFDK8|a+;OIzQZl~ z9zXz_gDQylrgIhY)U+d4O~+{6$;>4^wm}k+oTEM@VaO8^yk|Qr`AU zt-d8}TUrklrEBe~e?>1NBFQYqZUSr)Cro_r`8>0Bte$0nEUR1UC1uj()ifb4kvQo+ zY$yj`@1|_uN#>GpZP;JOp*uegKETx3Zrj%Z*@|K^Zly>Vr8HB6iLyXC9&FdJj`l2D zy1RmJB0l==;9j}#YA5}qock+<1;45=#J)dm*$+crWzRxsaJ_-xALU|HWsZNOY@quK zR3McVRA%1T_oQe3k&qtLk8^Tas%k&`F8lV@m0_Ew?Vm{U?R0!=e%icdweJgJWUh}R zX$x(-`9Z9|$Aq!1>}7hz-%0hpqX$LLu}^0Lb!j(5+`aYoU9BseU$=Rea?%vVr$&(_ z==fmb3(32J;QC|Fzl%TBrT3@}Pjh59Z;t7fMePST=IF=qB6mZk$U#34BF4UFQX>`P znH{rc1T!e)chXK*?#?x4x)he(xvDKWQe3rFL^iDUN~XOS5qjF5035R*S%d#d9`%)o z`5bb>tI3G<^?^xF4jQM7G~HpreIGbRL2CMC`+9$Z{zk@b-I=EMcJ?|t-${^W4$4k* zsLIh30x1)4v^XnZi{W31oRKJ{YEtSP9`pqHK%Li3QGwQ0Vgc#UlI3l@F8T{eqS~;r6h?(d!R?M`549nVjWic03bj%!j-P8#H}{fxt89IN$hy|NUZ9!8(my z`7gxuIiKB_KNAfpR!tM^K?3w$LD}!re)M@wT0{ymM54e`aD-ct=Uk*iHHS2&PB-`i zhLDCR#%v#7A0G86qQ0K%M)SJKygq4OpE9p6nAZ;Ty2HHgF|Yf~>wfe4x_Rv~ugA=5 zw|RZryq+?zADh>6=Jld^wH-9*bDGygb(OQa0MP% zA5tH0^xp7zD^r4M`ZnBcc0F5jui^ty1LGy{@M&O4O$#4-T!=Styt*}u3_VYWM@gP& z3q7`hBB`Gh6S@KFt?rHf#+?6nY%RGE66GbJ4r(IhmaMCpF1uDu`4EMij7KQ4sHS*@ zkV<~4IuddsRaJ}<`oxe*b)VNjVtk~sVB6zT3`_A_)Lf+G|6eKJVg}dns31hj^`v>7 zZ(c7Rka&|mSTi1LA?%p|dvbUmV+58%-dM5t5t2`J%KPl%NvXvV(NsD2jQys`+1tI5 zL&2opy9%xiOTzOeH?0OT$`gx}OL+w{@AS7KcTD+w8Kn=*x5<&e!9SbxIW0H#?R>63%Nv{^6Der)2Dw6Du4P{|yn4S+N3Q z$nTXfP3Y7fUl*ebOZd>-HAu``5qiR-Bt6=IeBLS8D(n2%VxzN6vUFMT1#;w#_+;kS zQ-XI0AnuHBN22-=1MLvZCvLY*>O$;Rxf%lrvxxAhElM6NdlCB4ncmp%fuQ=~GQLGd zKDA0N*Z<8Q&HlN+)^Sf)3<09-+AS5`V+fgB6zw*ySzq5$aq6pEDxMm#r6O6|Qc=b= z7FoxrzR$5~*8OE;j;zXB(~!Su>?b$5Acm6vA}`D^Kk8^n*7&z4?__FZ=tww$=s3kNx1oe$UTc@-L%4I$l=vaA?V&*LjD2 zJ2tP}YtyLXZCfh#6I$>1{Q8v6*iw-TyyV-YJzX)6YexxunYE>&<6B022lr3ix}`$9 zi}+mbZ`o3@{@VnuW%=;5n0w+S|2p!|{?^kKS}lC9;S(u!29CRlYvlRVec&Zc${XzW zMfEF%uJr_qzTAgQ`k>*zLSHMgEHW)W>HTsE+g}QYJ8m_$)C*l~Jh930NY-UfSCkQ2 zQ=YEqxE!q{w9*SDWAUZyXibgZhJxd-zyD`AP*=NfQANdq<^`2Y$BiC4rn0WiwqQY1 z)uP&FZ&lNR>ZbC=RSRnCs~d=|tcn-kkF5NLmbyx zW#SN3NP7L(yjn$5l_XX)EN-l;^42!gYmE&}-iewmilMHdyi!|QTi@JJao-o@_11u{ zlH9iZn)3Sks=7QWz1eyyYO1PgwoPiOuC8h_Q;Y{Ap`hGb-X}2{VF1`{LPHf)*EKX( zjcaUbsL(2?U{$5Iu(qKm5UYZV45nn9dau7I>rl>#%Rp`uYp z&2NBOl`z{|)i_aWsjphrNCcRhy}}^CCueM_knkVo^-1$;#rJ=<;;ri!rCV@=T@O*- zgfTZ!ukwX;RhqX!t8CC3>Z?Y3YZq71w6)&a^19ml%T+5@H#BLrRBnt-{!g5^(2M-k zdd(-PQ?qT{n5;45RE+vp5#fPw#&~bfx+Z^fOU0h?*eF7o1fF5Z#EIUDx``88>YG~{ zMQ*ApCr&h)D6hy?TVF+a{JSYe-WnRK>NNvoq7pT&zGd;kswS$_pw-kas(OASiR|!vX-%6h$ zA&Ybgf_o9Q0%MB^kJixQ)f%d`rt-%}8Bsb!}DU z7+Z53nny1#M=t9tYwH(@rZsCZ5lfqDy;aSln;V)cmVj2{uDQOFJNUVY{wy|hP1#Z* zLE`qCA(v+f5!=wGt!$$ktAt&Pt85^yxX-v#th0@_M5lg{EruXPVQhp!VQfr!MMYI3 zq8Oz$LceYFeN|PBw8#=ltS?_wS4BPkEAsFKypJK!DjhrU-;{+flsRO#qFf9tlBZdm zn`p^aZL_k?CflkiMMP~je4FG_+F&NWL`>+2xj%((QwH4U!FVegqxQW|8;)wGlxP}; z(&!rQFpPVCU2RppH=5qq(CpQu5wS86osH=jtiHpruhKO{^EOpAH`F2DQbL_pURg;* zb5-+14H_Z>79n6xMPrF{b@f$EG5A&aW*dQhbX7w&w!Fz^Xr}O2Iylrs8(rSKyuLy+ zRaR_Yb5#@iZ&KZCt8dWCn-;Y!rheKYB&uE$pwWw$3uR_Lp?^>~v2I@sXO?Ob2tF#d zV79U+GZ)^QzZkQ#P}mu3TCn3LgoZLuuajbHQSapHPkQCz9JQaugx{8 z%U@WfU2ht&TdnFz9dFg-Q6V%g5}~+ut5zfJEJ@dEFvZF~+N=z&!Q%4zN|j%6%Rr~# z&F(~uyk+=fdKG#cdH|yolwR>#u96jvS1kmOj!t;G;w{3vgwq6R2l>f(=WDe46w@v@ zHq?ri3XKho<=@4w8rJn|z|Azw>*A`#4Nc4GvYRlFwbje{ug#r1rNHO~OAQ2br28a~ z`KXO*6wB;g-iQHi)=YL=K9(5b7+3|hxVCw5xwoQbjF#Wh)FdKhX3j)g43~{w%8VOh z`&yoTX|vH^Y84HYFs!ap$v~d6 zR%o{J7U-&I;ycx&y>yDrb;@mt>NsXXe6v|5Kl|d( zud6DrZ^2EgEMHu{2wodTg6dZ^G{Je*z;i0ajpgFd zi!59Ifo;;vnI(mJGqjkp3JPzZl3%FJE}SLT$!QUMAsR)sP0ij}lDlvGjH+evq7`JmIPy{O$yEmA6dCG7NQxjY zsFEu7O%Uwy;*Ge0_>K6!1T^&B-e$m=)7#O^RTxQt?9# zw<6=crYejdzONRcMD>O6P1@AJ6qT~*C{GQk24<3Q7uNWxIb_M!wiG2JJ%^ zMmn@;KN%raY1;L$AqJ{{cWLV5^=5ZYef|gh6{F8I{RdFep8sw4B4X)g73I&IKE2N% zY*ghKZF&pd0B*#>D(})N9Hp_E^!_(yseWmo>-OLB9T$3`dDOTu#fQ&thLR&7L_lqWg5VWHS^`yF^rrdJpvv`X;Xu@p`xJ< zy;>}eAqv{C5c;AzF}N-0{I`4C2qqjDZ@FDwmTw4+3WtZ@RNl zsv?RI)78KQeq}#n>_Ry8Q|3wu`z1WVxQ4jdEemBx4##ZM>AtI(h-qoU;g8N|MCLGJ ztrs0_bP{9YL}k&7l{GTGPf@~17cooqKh~Uwkr3^G`o>2)EQK@%U7vm^PNo8*BeKP0 ziBMpq^~Wg5F7!vw*Inf+_dy{DV=X7vUpgx^rG};(s z#FSp5Ito+KCgHc$50+S1Z#a!ID>ZRqpM1h1Mhtz_Wi-8=rAVV&j7h!7lmp5$y0-qi zElmF5Ng85p8(pIWR|L}hU0V2bMJgekFqV)*C?w1v2#7UEF$`>93M9Q1xQ7UjnDDY* z6B`63<~LwDM>*|{*b{ud7RJHYR7{A>y5jfBY+kdsWnr_nxJo7~8D21OtP^~Vzd@f) zJze1>q%(IvvO79Hnoj?_boA1j#%oP2*cRphWoiqRnmF9Ddos4c=V!7(s})a~PD(kf zHch*|w#nO4UUypyUW@51sxdKhRSTChkEE3|cWipV`11TBdBkvn$I4|)$H{0QYuTt} z0%y&-{ll?{h!qEd#EE>%Hc^&%9bV{_k8LGjTNir-A?9xxPjC(%Z&! z=P~0dxb;6p@B5cE$%K!MZ#3%`8`uASQn!I~-H9y~e*u0bWsD#k<#`zOmbe2ZOo;1a zq;(Jkx7~~voZkUHpAcK7buaO-TevQr!E)6E%%!n{r^Y+w`3)66f9SPa|6@;?KmGf= zZoRPlC#9Dc9K6+e{=pw#F?vJJxVt9Lm1i`Tpyja=mTR+h6E+cS3J2E#KbdRb6=mCl z15#omPMP`Ba*fzKjce{B#&zv><9hEob!}}ML-{t_{x4OiYug(?RM)EC|6Z=zf=!M| zT!TxBZ;GoQ3x~Ll|LQ0ACFR>%yKi2;Julz3sjK~t@k{b; zduA+J-1%?$wp;$T?Cp7P=I5?^dfBzV9#K&G!CQykzIs~05B@lP=&Pq%3&5*>FX&xA z{P)*3=G^%6C;#}BGgGo}`T55MpLu>d?#Dk|_iEzv-x-zPu(>jQ=pIqU_&>A!E9-|m z{q)wqEBfTqTSEymCrlrAZPus7OTTwl&P&a!j?Wy?Vkfu5#31+9Z?Ub}w!~IpYqEKn z7N}GjW@9mHL0!cHnMi2FI4zLjG;uLew)pv$G&eHAtx<}>5NELhT9VL%BDlk@W@ zYU!*AjLXy}wba((;V##v&%Sf^I5A20V$CwNu{VxkMdYurf91np<>9Y-@mKrsS3CLF z{^I}7{ifxc{J-I<&fb6!`~9cpk z2|?mzW3w&Ko%oO=&z-a;AG>$@u6vA~@| zavND?FKFv;dc*QQsNiWAJP#%0yOY;|C&f3U(4AH1PRn;^m8$=<)c@}c2f=oCGKiBv zoS3)M?VRSiTcyoW|Bd?@>b^z&FIWHXQU47XylVU5!_#PQ(p?RmTMcIWMc!INDIwgfQk zKYGL8h=spT01Ub$x_>!Bqm(m}e5(b3a(s^Y!?(Mg`7TG1TxZFvhSEj_gjV2~npSXY zgug!cOG~B}xwX0O)IztGKO8=8hmX755GMJgT^=Q$0;VJTc zg{#QR0{AtD+kJ#R#Q)xGXP=|U-I1`|{Zv9R*Sp;SnecCU9p+W;|~>T)c0r%-xwevELc z_L9#dYyqdVx$tKBaJRF-H5m?32f``R-zNRH+}m!GHU+5KoeX8Sxkqw;hdXtaTPt*@ zPIqflOupp1ZdYk!$t8?u093}SHjB9L+is;w^=1rc?_$?fD}^FNhL?e39XNy!7N4@F zQl2m;&$Y~*i8|&Rj}riS68O!)*O~C9EkKKuEpQ-dRACD08e4d3`L(6p9hZVo0 z);tBE^r{P78=vnD%K;pN(3n;0&YVKt+&B`J2U(ezMU-{`$ptP-BkciI+R}l7m_?EE z5V(@x_J-$kZ?&%`IwS2n*=0#q0fM2Tk^x&so@M0uvgF|>`m~9-4a5z!Ef2Vp&|YOu zfRi%!0lO90=ealPj9n~Vli+hYmMW_#Ca=KNAO=!y#lFcVi~`Pe6`73CSrwH#jr2LB zKPKq~yrwoKd5ELMmhVYu@eWNM>S$S!oZx6#=Jw@cHE3L*8+c7rDP3 zU7QT_q}$3zBc=RSVBQ8sZ3rjci%uJ0r-7%r;x)TSs>s^Zff7MkIfRm-v>#<#Z61+dZ9$C8 zPIb*z9GKb1t{0Gh9r+(6|KFLk_v`PCYzqwbp{=o+Uf^1y;uYUt2hS<+=q4VY18GGB zj8>iG68g>oGylcj@HP`B4xe&oyK{=%xevrqkD2fyC*PeLk55G|smnaxCfx`Ao9#TP zc7uj;lZ0ZIW14x3IJz}j<2Ih9Gn(w09Z3;ew+TEuUg`~h&BPOj&a9F+Z8*g>%bht5 zzlJpFC}O>sB56s?t7DaF(7A@yurX_eT}!mt+B$l}dj>17$i2yNr+cGgGFL}4H-+33 z#i5|UtgIc0O7mYfgcgz>RMBa%w zMj18Rdczk7s|zi7hdXDgJJ&HIP6diw6ipwL>sX{3+wm)RPM$j#oZy}?%w|j4(Hs6I zc6Jc@bITR(g!%Tk$V(K`$#K&h<*Hy%rxTu_tnOXC;i=q5Y(PUqOiB&8Hu~oMz&dvW zYt}iUgS0kEXj#l+JRKzhkH$m_<5Hvye*OKyV&cSJoddUe#dwf@uU=|ZTJJ6?a+fCj z&_1wrz%f`E5-(*u@p5nY2CiZ=7hxx*EyojHApYOz%at9TrtEM5cDQk9@+D%2F~-Zp z7<&_5un&)zV?1Fo$hb_;42Yz{WuRM6NZ$83{dp07n4{Xg)-jcv6>&taz%|{y4sR1M z1{D4EtrYwp6ZTQY^1)@X)t*ooPgJCnAJ7R8#zWzsgzn$>hQGztkj`tOI(>W0TF^f7R?FVQ4y3j%Op#uxOMxhnCt0T`T@-~=di_e_ZDBfX~_%Y(q z6uPjYxM*fiOdkyF6XZ)jfQ^XBCxapQ9MM^U+Xq}OaG%#6S-^b^TrqHsG5B(^TqXr8 zgx|TA4_)N4i9e8zzO<2NzUWOnoh@>oO1v+Q5t{5;>3%&ipH#<7=1plFuqm!(ar9x5 zOLX=L%B(rq8{X?Q%8r#EMrL+LrF*lTA(`EQXVi!NlUM1Dp8vW}SZ z8Qd%RnJF5!~(d=`GT4$y&GI7WH z2x@+#$X(_rcF)OomsP6&j(X9_vgPji{Ezhj^UZmVe3#h!w+W9P>kVH!SRYs9-fwq2 z7KdYbu7rQKyLZrTG9DEj68)?DQ*XG8d#OXe@!P#>qM)*`6bO%N$hU!fvUW*-kUZBM z>$eiWk@y;kk0;ltDB~Oc-vfPgdwo2hDK7Ye-u8)9eQg(bw}AH!6R#Q{&vB>TCORws zCmVb(Bi)JY1%VY>3>u1LUa5}{<0y%l62eB{>ea>}XsT z-Hny*$|>%~JJh{nj*N~Qm%GdO@2C^$Z4|BU-*yGq-@57U#+7k+J4yUOv9nvC;n6?$ zhK&vA=G;f5-xKfKn9Di_+$P}m39T~b`#fKxYM`$nUjq?4AjdVN2YbVu%c*`J^M3+y z&Ak-mjo$K@D_7*+-^czIgS21i*f1v z_j<#>qW*E~jU`HW-VukWDK3pHjpX0=Vbp%8KG-qO7&MVCwnBdW{Z_G=5>AmmyN-$6z9P;b9d;a}Ec`eIo=q`$*5H2`^UI53`bv7~l4#E<@{of5o+DrESR#w~OyZM2 z=?&k*z0@g|zs2rNb|w@1cq78wGV`B`>+L^Pl6M3 zqkbY^dx(32ILp_J<(X#^-^>PVKuhtBrz z@g}*L0T`gk)WDbrNXA}n1n>U|ZF9h}=YN5nghTfJBxD=)HmgJ9Pq zCNzN>Q5NAZNn8o}mXWXEd~f(iLX* zH;N5c`0)24Yo56duegc1jz)J*p_+c|BVqZj#eKEL(C<{{5L++whQ+7vqYD~y==25F z9J<(oVql*DcG>{gm3?5P9lgNr0X9)!`3WD_61Ri6?@2@-KFSt85bn3k~lS|ZpK|CZar~Azx*Vx%wujQ&eCB?lk*C<5_h)% zL7VEM9oB$Z#sq34t@`s)U{3;D7z?{H+MhR)p88pDxYkTJ?F+M`;$)o4@n!m-b) zidq-WCFv9}|6uCWfWDhSMZ$EMdh72PO?KTGrx~U|*c@;^N|?aG;Uy;Cm_FvA7+Ig> zk|jG!&k9@*s`>2LWxUBuAIgdv_`E_ZA*wibE*0A9pKIx5)K!d zb?Pe{OqDZ-m&;lL#xeJ9^)OeaT5@OhlaK<}o$m3)0wXi?{mp2Bt5Wd}e|=~;{ENZ) zm&<(Q0A>`pyzU8LeL$5wMg4dDa~wd{Fj^_+QTSAINjSX4EJx|wbhRQ^m3Ecz0$xp$aWq=fkd z)3+4ZAh3xpISMTbE7edIXOfErOR7MU;7Y-7ssq-Nr>O4rtvB;t>Ex2`yU^_Ernnps z5aEMTK80BLSqI+Aq`_iQ*YneojkU?Il+es<^ z(s1}^-1liuk=U5x=?FwUlrOE}7j=_oIQ`%k>21QOfhsENKEnSR@I09k4*z}Mvc;hK zV+k!FeFLzmBY=h8FDlbfJizZ#{*Ff0W8m>#6^^Vi$Xab#!gF@ZfX@fUhEJqj6Atf^ zdt~$>#wYkhc>6wQk-OU|jy6M;$l5|aTfn;5+12f2eAp+Yn2jvS$Uy|urh&2y{OiFl zwwF0ZjTuqr6fvdFBA4vN65q28n3KS46_|c{qQ+&+fjDWDAmyHNHlrr}Cs&GG4BPrg zs`9(RU78vWuMpgn?*a&u3xq4PU1{}kv)@8 zM@Y)#3wvB6`}Vx<)ER1B!q{G^`bh{AzHB7S2Y#)wu`B_HX zEnKA>Q?Hj>Bhpf$=MXjkBlU^PN11<&w_|V>sT;-<`x(PMJHFHH5uvIm(54Mm|85d*RDRZzpfU(A>(sm<>J4IYT z@Wu#$i!M;mk8Eq`(s^YtBU2PPIXm8 z9|n~%;j`e|3cf#^_>?Z^oBN{wE=GBo^+^Y&gs?0t)>cktmo8#W(RBSRF362xjduO^%FGn3tgkq2d)W!*LHM!uR!euM7($lmwJ{364s0c2Bd7Fim}?Y5Q`TMg z<%GjCxJL3=ZTH)l90jh@m?!1C%puE2#xKWi35OpyeS~)L5o7oGBUK5v#A_cGKX9!F z)E}pK0(cw2J3cQQ{v-DWU26TcATsYS?#gcmW|ubGOx`Z?`pvwieUp}soeP`7$R;A~ zBA2pXIcril{K~+5ol!_*k6SYJeH~aCy9<4><7z2rW53>SiVH*Dr$1z?3;jZ$D7Y#z zTLWhc?or4(WV}^y-n^D1P+A%V5Ty@S-e&_BSFW zt|i3wA3YSfz8eR2nk!+$&^XZS2pXKeIVOEi)`!Di8NA+UEpZA&f_@t(TBgaAzbQO! zZ3r9dub)FhsY$~IVE1q@Jc_RMEmSVc^q8KZm`P;uM2y+^`_w&yrid=dAXD_Ik&xUN z4y%{j?=F%tWsKNmfh$q~22b+rmq4C{W*#*UgrjWs zBjN)KdgXOdrjjx76Oz8m7qiVgroD=tH<+B z$6^yEH1Yjyu5sl}*4(}E<+zwEPm*t0GdeYR{w#+`V1K!^=4u>1<<$x;vUhE>7a15F zS82kCc=owit^dnAC!1Tskv*`cEp_05Dj!PZcb?c=V8(DAw7$>9D}#ykyf_j}AFu*c zPl4~LrP!Vb?fv%Ph=FO3lYg4b{0`9N$*$NfSJta##SBXZfZ_*IzU-Z8Tow+$&HW(t z%60r>oRSJyG>wBZ88mI+Oj;g}teFgEx5QMX5|V)xA4|&jIqp?{(O`K5zvL18Uy(fg z27{G&3;zuQ2rp&N?_ls2ZlT95@cagY)kqWEAH%ziT){1q;EZ1#jJtT?#jz}+a|w;$ z%i}8b>)Ur^v4D;GMyHHy8vyeX){$p)L?-&#av9gls{;sVpSQGldyYBn0V*?1)kbZ_h?&L zw(CFYoE(2Bs`{o>%J`VF=9u(F`ny~i^dWcV;_Mf-?fwV*j?bj-`-saGyy(;lbV_Wx zgpY}DT{AfHU9zK@#a%xLj)Y_GvY6jr8Rv2+uZ5Q*BQ7(CNyaIKgBgzeJjB) zay_3iYPkxZ~00 zqjJKAN#ta%k`rD;i^yo9TDujQ_?UcOBoo3r;ZYZ`ijVp3H@j>}h%FDI?m_g)I@CU(T$q|o#{rxX;;GG+$Et2BLb=YlUTCI@I zb0#5+(8$Eux49Z8501+kr#=-jg_DvOOp(t!{Pp+$O%7DbivZ>?>xkWUkwYCW#>8zU zZo_MK+cI_7g8|q@T=GG?ZGB8!$x*v4ow#_v4aEP7@B-lw;T=LR;fmMEL%4}hM7WDk zPiQ4uhU-5No*?{+@H}A~VK1SJ@D|||!S;sTmPkk@Tur#1@O8p?!X!co;dVk9;agnuAxBK#}i8A1m^e*3tdAeRCaolkOs**TmDLEb|wICO8y57#lSvjDZPBtinxkm>~FjMkRMjCn~$b+--!~i-8 zsV3Fb5l6D|xdV|sma++(Nr18AqVJP4&7J9_l_9r+PSoR+pw1k(a1bpm=Gj6?WH)Zc=S=(A@s3oS5}RMq>OOSMB;Y?Ziga6>v* zLFce=-2LS;%&K^Msgmx7H@L~j{9jx z=zVg4LD@N>vTa-npEFGy_v!!xy_f~vGIH&1dd8m&GCJE$eC{A*WVCN^+y%aGl*Msu zNxX@2a?2(0d`Kc|-aetyu=w}A# z8}vDiMBUsr`qZ8P!AB)?TAaL+M-HBw$eH&~rBC@xTNCykOl2zuTLTDXaM)KgBMnXJoZwr1FcilRG|s(E}7N?Dqxb7v43l=xa*kWYwdOPJIqp6# zZJsfNnZ~u}Gp#x9VGg=LtupD>a=F&dJWe>lc(3E^#?k^!e%OE6-tqMmivz; zjzIhVU;MS@<|I1u<|c$2gEkcY)zyx#YY1BRb=?2I@i)C*G(5@B0RAt#O068o)N0EA zT}C;;zrVhh!1ogP|5pNdAU77v%zpgoe3Y-|XYw8V{roomU4A#u35N)>P%r#UXcc}g zEER4M?hqaq{vf<094w9$^WwU|`+)<4hXh9l#{|a(rC>Bz8~kZ770d>^f_DcW3O*Wq zHh8dfxTHu?DJ{*Gekt84Jt@5|ZI#}WK9|0c0%0xulWPzbXQTM8n7S(2Jmuq)v_i0aRFKMr9f75nopJ{{i z;reJ@(sezh&(~klkByuZnHbp}nH2q1^ucIvba-rZ?2Onsv3P80Y<-L?LrgL>VWdzd zB!%AylDJ0PB<>c!6vqa}2QCX-9|#0z2OkN36D$o)3f&$WDvgkimwKh{@HlxE`Vdf7 zDudL2tN&3ewAI=(TCcWDU#b5t@>%4y*x%_TIB)%S9xgY?kIDa(M=2*L zXDD6DFO|ELt;z`XM`~0ZuTE2EsW+>4sSl}})IX~`)o;`TwN=_cUDX%p*XVn65&dt9 zWFmaDE?OCDifxbWjnRv^O=#XwKE!wO+xfkGTv!G@*epyEGvZ?LdGQ_b-{SB;6xX%}i&X+K1J zf2z0Xm+QaQ@74Lp^vKpoNmPv96ul$*Nc3;fe?$+B9UYTn=f|#%t%|)Adnfh{$+y;u zBeW33v3B|9vB>~2%Qyb4=oJc z6B;O0OFhy-;ZS%(_;|TOUL-G**UGQU=R?NVE3ZQDLh8TNg!Z8JC+$6Lh<>zQuE+JC z>nrp(^&R?HGcbp));4Ox^yBqe`ds}YeYyU; z{(=6beqX7b|9+RGv-heInS{fQI4Tr-Up+%Z}zI+Kr<^lN?dAs}rWNzvK90Ecx8f8r?e`UD-S78 zDLa)C^;Gr87`r+(fw?|gy;xnOUZ>uqE?4hSA6D0^f53RYs{U2|r}`mm(Ex2fZMb%{ zR;Hb%g)~*G)~0F=nxUn%cCAahOk1q|O1n*4q1~@NqCKuX2g&_iJ5oPMKSR%8_N~@m z&?iNH7FiIvE3!EZ(@<ot4XNBY8o#9iU;T`go^2735@=)be zMN?)dElP*-E9DL4EoGOoSK(n*WJjL@0o{HGz55ia!g#%1&*|&+C-jf? z-TFb1A3`@KN9rOMMJ|in7I`%Cm&o?Wmk}Wm#V|`uA_wxVbKjw!EBZW$#6SjUU*3>HTJh554T6|jE zD-H>a3RDC-1HTO16nH&wOz^_sHNh3Z&B0fKKgE1F37YYNR2u$axDvW?Y52zQ!{Lv@ zUxyEoB{?Fe<(#}!zE6J0UN1|O(aKb1k#Z~6pBEHKov3!go?eIEKdL^Zz5q?2HS#d+ z7;TI;UOQ7e8}qzNTcq8t-3^Jn2z$0&`w~+5fqsfEV6B|48~XVe{cH4P`osETm_2Xn z?_upYIC2DLQ6y3wsg2Bv|21NrS>e!*VzZ&AK+c zG<;L|cG%K~!hZ_y4u28;CLE9zxlW!ZUm@QmKPqpOUxY3FROXcZl<~?WiOWPY)FIi!B4`Wy8T^%?b3_yL2pL$zZu>#AT|8?*%GT&K1U9?CP?YuYww;x}3e zq<(^ax?ZVYq2Hk2qOXFypV0?I_{f?lw*j#{5Lpccwf9<|@1>W_+pXUwObMPP&62K^ zZkPV7y{&zu?bQy|e*%sE5!Uw~$JWK3CwcFJ?ZQ8&@s0dTd`vKeRm#Uo1?)pWJ5M_^ zIvcAMy%EfUW>erzczg#3j|`Rt$3nv6gZ07lu>RZ_d?2_EHf(2b52QRaba1FDG%Ivr z=+4lap&eL_21!Gu1Hwmyj}4CrSA-{pGvUtgVn}mUxL(-^jeH$mk*-cq8`N{uE7coR zt`zS(P+v-TSEveI8Hz}^Nv}$$V@A)HFO`2TUoYP*uaNJBe*X>%n%x}wzmpdAur4m76=RB zfh-oT7M8%nUM6%4D}~j<8mu4d;cIUYII%<=B9@9H;Bk)<%fu7KG2&RfehrE;R?Jp0 zEoR|~&l4Aj3&ln7#jYk@;0JJP@Q=l*$L003ut|79=oMZUwg_(u+l3v%PT@oN7kh-y zg|FVfpeL|4us-m3U_;>Pz^1?pfyQ8Sur-*5$Da?*3oZyQ z3@!>T4wgtmqywc=c)ug1QBoP!>@m_pC>oU&2lTOXjaZ+W-q`FWRbjBzFJ-atGZ0SRqmEo%B$ryau58!_44EL z2JB2W!PfS|8{8tl3BO~9yi@*A-X-smKbPH}VqW4MBKmqDKawBCVl2eKs|9*kzuiR~&UCbs&ADqP z-a*X#5o%~knh^)YhZL48vH4)N?CYXPqi%7jm_-9R=l2t^xH zsEk*88IW=YDF?EIL8by}Jj`E;G7OfD_9K+fFzX_Y#EwICg%YC|LV(R{F-mPX!RCX{ zg_pzUMBH%s)MnCM&?3LebW_=r{Owr}q-TsD*#xBb6bErQ2VKroH)WV&uU$ZD5s+$1 ztmv*qI|vH8#Gx^vBYVUV8^%{V_zC()elN3I7_hPmwVc^F6| zgSGu#2)7fK*}cj z5d%m$gXDme&Twd)x|;f^Yg#^qH#ydI6-sSDvM#T#-3+1uS%^k^?Wrdk2AK(D1A|-&girWOtpHNm>_;92vXDXk z1f&u3#;fZCqG6D|KzbSEfWc@_i@($;AZ4(H9-mWyY+#TW5DxDqJ(?LnY8j*xNH>FA z2c(BV9t2W;zMs!iK(Y+-DwSf8zXRFDAbWt6&GPdpL0DlqgPa1Sn?a@l*~K8UfHXpz zynbB_q_)kE+z4a?gRBNphBswiskK0Q7~~Bg3-MmhqH**Y!YG+^JGySyJ~wB+CxXs7mqUXl+cY!dDI;gU z53X=$cPBWPFSj}OVf`FFxjy+!clj&=pX_(wLy@?awQ0fIe5bY6zfN6qzXJ_kiGM4f z3%&!*#V!q2{Cb>Ri*O8Y{5bAc=+}nt(67r~J~I=Ews@-D&jj~Y&p;`( z(9nsJ#E0YVC>-}IL2Jnn;TJi2L+w@O$nX~~O+KB=buwNIZgHDXSJvfB$X|ezt|+YQ z8r-q!N>H3ca$d5yuIpSHa>nKwt;uv2r(>JC;k4&^m&Q8m7AGMia>3(8*}k^lSy=mx zZYlI4-`dga6~G;&hK4Uzd$&`HkmG^$GDszm#?=mu(;o84>{TzFYoiyx-sEv;ZgFXd z^K|g>E{6tH*?h7H_DT5Jn8dTF_IvEw`wxq!$SNS&d!1eo(I0^9 zVh}n7fxpjBvkOQMgA5poQVen&kcHUXv6c;h-7mcLHd?X*?-LO2Riae+{r&d~MJW0y z@6xzFc8|*$n*s!kbab(tYp6Z@;Ot0#t)EXHGwx^Lv*bJQS?}_(wATsk0c>99gHQK& z;PV)$Ese7HWUYuQ*Fs(hKDB==l=DWnRC8M@=bI2#<=nL>RsKf*awf9npjqh9nB{&0 zgpxaWE68I&co%VG_$7+)Y)0+1pY+d%8vO?K zVz<*~hx1mKbDxa4cTuW$k5h{H>;}^FsUP8naa`GFe&k>v+`s+EQ9ybac7k(Ov zX!iP%Dj>VQ^dnP&lz-(%&IOYF#*efRpa1xg*+9B6TI?MBtJ`<1+wGVuzVV@@oST4eA4GN)^06UqfL;4O_`K`#aStu_wTR;`LNEC5z~?=ePaKAqV(!fIlHHkd z75FrM2RI)Gpd(cfb2k%b7jfhRrL(m+(@u{Zj68*dddya#)RIB{ z%ZE5W37QQK4FTLXAPcb_XUFt&+_9}h6WTo2KjRUwXhPZjoKnQ+U*Oa0B1A)BdZj}g z8mAXfOq+B6o)r}HTWO$}HVsa%wxSEIn<5ckJarn4v3WV{k{X(H{rTKwNnM}VSViZcHco;csXM+ z+~wn)-R);|Hu!8ux_MSN4tGmo;WXn@{be|=38i*9rTVCyTGI}i(t`>)KL9_#(z=BA z#EgO*aT!)HD!1O%D6%aSAG*s$h;BYOmpgrjcWMRD&U0xDJ3MxYhgjjRT#pa20$w+G zh!rHub-M`Fbpz@u!{K^$-dJvvLGsr1G7BWaV5IWJ)>%}x6biwVg z`_&9&1pWrQ$OZUDeDI%5^J^eF!>0!bUEWx129jrJ{sx5dBY3qB_(5T*ql=JW5mFC? z9l@DE9(OsDRMSR+8wutlS=_*t2MuLj@M^yaNI5K#*X9R-&?^ZKc^=5sC`CLhK5qgU zgKr+6PpK62#Vd92!HDW$>N*|>9)J00B0%sM+tp3WHu01pO*q*+q0qwc^Jr8riGh`295T{eLEqTbzTei z6!F;~Z)qACK1UJajyrB~rvkZ^>6Z*-0n>|#K+JqJNNEtKAtw7eJl-K43KxlBsJCkWz-`z{9P& zJkG}eS;F+hay7#_1>{4f&2xc3@qE2l3WP4N_IrWw%xd*4 zkOqeHUy4fY1;UQbP;z4!&L;v10`au>bRZ>+d}2kV>VPajFTB2Y6wzE#gj@%NatnH; z?kb{r0!Sm%uh)RQi6Y)8zYXL>2Kf@mE(RG2>&P?xIvxmB?MXEVq=zXr9!M2~#DQ#K zkPMIw3~~Vw)*o8}gf6eH)kVmPL=mzD$aaSF zKZ?kLDN~@n}vhLV`dhGMpy^ zA#L(>;~b)4+MEVbj#8esTuG%|D?yr>GUl6JuBVyHKr@Envj)f>hR@?b=(5^um_O<) zCxhhc=~mF>nKpk0r4C<2wh(7Ulbw3kA*+a@F@dAYUj0w2Qm*d_|Mjjvxo+l z$GH*6f$r$gy|yBn1x3g&s1#HCy+t$|fz&YV`7;pGD6fSd7L^)y9JRsU!Xt~2lZ%jY zAUhe(X91xV#H%X-g!1}%$UGpd%G7>15W2ilqsvUdhtvScGJKkfXyyYM#f zAgrxh31lrJjo%jWc@{`1L$d=&FN1snWQ~iA!wc~|=Us!i?~(7NK@#|06TY7p)=c={ zpb6B{!N*(eg+Gi;DD>IEFj2%EBoanPawes?=x|G#XmhdHf#Efri<_r_5 z%u}WxKU5XZ$16KqT9O@f$0GBv35aeg#3pCvB$02&f~}%cl5_oEDJhEe9qDrF#ayJe@$wQe_b9(jMGN{|c)t#bMY_&Hh9ic2N@w#-C;5)qX}eBJ21F%t z7PXa4&mwS>I$1e;u&^5qiL{l?r5?#cl6fbM65)jW7}=F5n~hPCNF=lQ@yI;W6i>{e zj7yvsBag6QN-~1im4^Lsd^&~L9!t^;M1lr82CH|Hm9Q}Cwz3lykUJgh1D=kwO&Ae| zdqu9RJ%J=f?Fi3GLWKR^oDjz^7YYfg5S7zU*@ljIdoGQiDO8&|b{xhv(A!i?mn%u8 zKJ;m7)=abHkXw?f_i$5xr)HDwh{CpR(kNw#ZL%H8LHTE_!E#2}Cay;knPi7aY*?wY zNd8u~D|4V*joM6#(sLom(Ha_za;q`DBc8Qy*kse%=k6P8h|lBy_Yi=i-ZONIg1#f0 z84J`U+h8Dl1eq+@Am5o+B$KU3r>*bah_IH#$pK{+`WB+8?WXcu4DEb$FkhDGwb041 zq3AY)p}&qfPPW96M9Q8wPM$W`#v8Tq_7rRw3CU5zeY6%s38^NKn9Ur`$_Ar26&3E7 zixO^H@~TSCNhK(!nFCk1ceW)vDCu51VtEk~*-Y~oH12{{Zztf&v{lwjt8SP&wPB)B zQB_q}U0=@`U|c-cnOJYm@PD0d>Z!RXP`EQhftMs1V`e(j6i*w70_-S&5-!x$ZXgWT z%$H^_)cG`KC6xh}Y%&#)C8MJgIl$VISidQD$hu`1bETkRhD9QX*Fah z?{=7K#SmBll?^rZ348hB{6WM&N<%wo=`oOCF4atuY(t8{N~F94hV`bow%SQgSn$2F zYl`KTSdBAaooz~FpK}d306}yn0@e8Ud=>YJUqR<)&m{Pg7=8*`)UI zogE#tG*TNuZu3q;3yf47&2uAdVOv zq7u+C$AAzs38PAcu{MnMLV&`7C>WG(&hE@$I2_WBTx+T&?|jR5V7Z*{e5+)bRuU8E zRvTs<4Y^%mN^EjI(jDh>HZwQm|DI+lL%fSa*qmxKnz_X z)11s=u&pRMU|wEAEAElBrU>N_Ip*dbq}w@P(m}%uD?`~_GH*v?!FBdN_mlRKoIbS9 z%J4^Jtr$O!Vuq5IBjcq+gxl9=-cVxVOQ>ka37Y8(sclYjM8i!mNDU(wk#ySBRyGM_ zxgdd|TM&5^(dD{nQyL~st~P4xtEW{>HR`Hsrj9piCe=4!7h>26`^rVxUJk5P0Pb2g zo=>!*JZ2O28{{onUZmM(n1?ueiroe=A^+XZziHkzsv4%&xm8#k3Ns8=<6SlN-eh0n zB<%Dc=Vi7{(N{msqOcQgy2EnbVQ7<^nJ}$cZ4&zuXqKbHhG~f1ZeSK8b?2O<)v?a3 z6`y7|0K|PLp~xXePB(OhNgv{)mrVcYu)Fr|*Br;DoUPKHvRF~H_8>CErufh?YA0nDHF@vLZdOmJBEF z7}?tjnk`gfZ-FFXz{{t#%i6e4vAi-j)v%YI*%Y2N2`rfwrqQ;nm+kQ(sjZRk%90Og zcJ*5fFIq$qy*6+9P4zVj#{??o zC8#wA169FhW#E%|#;VZJTd8=RwSyd>Cb;CDUV|QkSD4_}n%VSIIC{*n7yQbMo+kQ}X?xSKaEUi%y)UoEBq*8_))@IcvPl&Uu30n7K+FZ69VEX` zAI4(py#2L@#qai!@>yN3#}iK9cEK5BY{{H4w%WkDP_$h$%`;56>*3mYz&#Q5nOEC) zyTCqs3wV*=fx66Anr&fIM-#tdJu=-fTPsuT5?N>?-j3%v#`t1Stj{hKYMt$#!C|VP zZI0<#kx{iPDay|7xn6xF9XEB|TEh|!2W!y0$zux2+7~w`=Q$J9Qf|hBKR?le^s@g zdD#mq<;!6Wo!v+~-Mn2xUuOuIVm8}o|G31aHGfnCb7Xp!td;f@x4`NN~e74DkM zDx9?QhC!y!)!9C_KoG{h+%}pln2ye1K)g;ebZ4=^#;ri06=qjm4fX4@qod%%Izncv zoxKpVv!llo?vO&0@l>Vg%nGV-UrUrFcI$UYHX$?J?Lj0p1@kua*4bE?9#qku0Ci2G ztoG#r+6r5yA@7M9QujU8GAl=i{fh3}Lb$sXIH9K3P;85><`oLULg_IZ-tXAT)UT)G z>^kQp5@|f%!lROS4(CT04eZGfmJzj_6ZpM2(ZfjPC@|ZUuh(avujg-BQ4&pO#>~SF8yzA#7kSAS7iZt&Ed}4ck zrf6LKjuccj4(HWqf#Yp$N!d+}XP&{_of6Jg$C(aLq1IWs{46{ zYD#Y(PmLH`Y)xRx5A1vIYTYaDomq_&SY+#%_ngR*+&&(=VP%J_Y(0U1LC3&)t3|;A z1yY!if`=9TK6Ti)f$eJ z0#M`P-J01^TaIMw&5HVc>cMV^a-A91S9Yu`V@bTnkEW;E$C}jNGGWhl>$k18l>A4> Qz?}EdISggrdsOa!0Bc?jNB{r; literal 0 HcmV?d00001 diff --git a/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe296717af838bb5537a33cc4a6e8a58c4ab5296 GIT binary patch literal 125285 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

3!Vv?_d(G$T( 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

*0c68LP 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 cVDSg~O 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

3!Vv?_d(G$T( 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

*0c68LP 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 cVDSg~O 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

3!Vv?_d(G$T( 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

*0c68LP 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 cVDSg~O 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

3!Vv?_d(G$T( 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

*0c68LP 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 cVDSg~O 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 T0^7`neV1@w;)30ZGqwK*)4n6q literal 0 HcmV?d00001 diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..e3c902838daa88a79277fd3fd37f93dccf7aaf44 GIT binary patch literal 109672 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

)NL2%iQB%mbvXZu};JSO_i?}rmKvf-x}&& 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%0A(h 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�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 z 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 sFC6 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̾GG+OQS%=%K=MbqeWxiN}5 z*8|KpwR~5wo9^=db4ALdakcJ~T#UyT37?-@cGoH~^nP1J@Y&j&0D4$+F86cy^_4CasENC^hAwMr$1)DDBAy6JwxY}MtX462OLni`7~(2 z==&7+-cYAFrR~~DTd!C({@SUfc>lnR?$;iQXUD$z618pCmw%742aX3-PM)kfByOH8R{F#|3v4~KQ*-e5O|{y>GjxC5i}8rx zqayWZ(b7M&KBC`fc_PdFC-fy5%UXXway_+dRMwG}Rr~vY zIOWP0#_XJPZrRUof!9J7i!M23Jbrh@iOWze>zF^|wd~0FfkDnjVHS->8xDTGbyaQ6 zXZH;kKD|=d(0cT-v#agvqqPy=KYaSHi~c{|{IB)@zt;c%H0%HEVx|8y_5bU`|26;r zCz}7l>X-i4_5UAe|7e#K{7*gp$+vd?|N8eU?w|TU^!qoP`~H9W`Py?Q{`d3SJI}Uv z{x9VN1m5BQS^JOI|0AS!^pze8j}qvG1l?TF)dgK#(7y|we%;C|63@ zY-{G>p@|;l8Z3l{NgQj4Ga-|OvE-(w0JHOp$z5LzG5gC&I7k&<%|B^a&{=57J;$S; zCh$Ewon`-&BF|fg$=-J<4wZ#s!!~)kz9SN+EtgPdQ#M;N`~mX2PVx@JC)DPuOH*h8 z8K$N4qeb`NF6Ye$4=tx|ja9V%O&)y+>?c_@2g)qF&m}B&!Myk?6WVW&A$N3n(mZFX zd>}!ipWH&omF;|E`5oGKIFfyxc?&)wovggQ4Jks^PB&I5;S+c4V zS>-E9BDfFAYfLD6y&E<(-ollSMJ%^4jxHLQP)frn3SMK+Osad3AF~gtRS}4s(aj^H zcT$?@0C#XMg_zJyeC>>-@!?TyTgWitSC?_4zD8KcYhbB%G(DBs$$Slb>6ylQ61f*d z%Kfjge@!%wHM_#wA{16jU*ez1Rg5Qhxb|}xtW-sF)eJOwWUvcMXW)ca2g`KIqjht7 zN%H_F&CDAZB#{Dxb3TEBRqBH*Y*s%>C zka_PS@4qEROI^oMU4{-B4DDuy!p7KeWjCL9(-M2Ms`<#f()41cBXx!uQQT>7x^|mE zb9xKT>rFz?UTgN)+z{vXtHLgP6v;HoLA~-bTvxX7oJ%7pUCoxnM#|x6ZXY+y(ZTU4 zX58CR7v)`vEbfZ|N@7=IidHzbu98E?l`<-OR?L>GuES{k>1^(MQ@lPF!f)-Bq0~tl zJW2Ttt^R!g#kH59D0&y>aMsX8o6Y5?j@CCn|;4G-=;V%S#=HJecUnm!-iBj56$ zw+~`w?L@w=Z8Z9#^I7|mHae?U3t!7}ns-WwFCG1q#NMVc#oS8F)_l*DdvD9hy`y|?p5R+2c{ca~Ma=V0chH6l3+>2+Ea>%7To9~YZhxB#} zXs-B!&o9sNw=vtO$S#Zh+9`?VUU`zfU2Q5>-(#y#~s9Sm)n-=u&ni3y+Hnf+AD`q0M{RZ}0 zzenkBe|Fiwm?B$-Vex8Z9PV>q@=|uBx2cc)@n40!MG5@Cu(f#9?#fHwJg0>Y16)a{ zfDUfDM82jIuwzFO|6Z||KF%>jXO}ddht-mDLmgV_K6hX1PYa4u*{N24T)nHxp9|)+ z#qTCIe#cYP)}`>UZ!;0zD9fvM?ZBD!B?$A)rZRmS?iNr_YxCU@Ra=ADZA!@7Hw8PB z$`K34e(J`qOQaQ?@RlSgJe`Ac`p~ER7t_%u?55m9aJcD+Wo|D;TKRH?zIg90<8c#a)t8qf&Hw0znVcMTXwqFLgarf(L$DTdON-@#-YJcM>Q5q?_@!JL^Y7$o0? zAv%j-v04ePH}50cD2HBNjU#Q5WUQJr0{V*vnsJeA2EI##t{5v>!1* zvGPNj+#yVN&Tq$-u^;e4{1R7P*Gg`aLs(1y3K|#Sz&2;*(ye?Kc#Ky;@v2)eJ}?wnm#yo_s>)+xNg$*aU5LCG_~tJEnWJj7D~s zBiZ*ftatX{*_FST;U4Dn&`(0dC=& z2`LeEt|xYmu9Aou z05xu|8%5`h6ES&5C-T3}#sR4z^vF?^9{QR?>25Qdyu%pPF#-6caupN9Z266u=TOjT z%quPmLB&9c9j{2ihC3TbF7FDdw+8YZ>1*-t=v{Ux?J$1z4@0|58csSM$FvF4p&=hZ zUb~dh{B$MG&bWn{$41l4k<#>?BH6V)4&?GchIZEM#=H;5uysTkMTDQH4xwwXixk16 z-DZN?NjspoXEOCi-WNn4<&yHU^X%G|UodRi#!igyf@!@hRy!Y}KM7vQJo+1hLcej- z;m5H|djxwR5=*8h{b|ar;S~I1J>6}optl#}sIy`!scyW&MPe-|d2%ZZW^Si(bEot6 zts7~&XCl)Q@Lg^J(-0XThnB)kT=BTFV0^u~(ieYnAM8pKMtUK_b_KHtc~3W=*6R}d@1=!@TA1N0C3>v( znD%yzp{W|Ow6!9ZHkQg$T&NHo6;@=sKRMHLlQgW@b{{`xtKp993_*>!e{Al)XEbfz zCRFUcOT*_6Mg3P36x$c@9$9b9%(3QgL{brWR|+mhW%OR>E*r8$4q|7^xXQdD(p}<# zoH4Q38YhT?9qo=QrFA@dXeI6YWy0KaJaPB=23{t97pvbF@-kl?eEmC?+0Kigb`qyN z%XMTO=8rqku4vnuND&7ta4ycAwoJ&U$+N|2t49C{wdbJk_Yn%sl!r#M8||*FrdYFh zGL}u`k%vrae555GdPD-D`jgn~wttY*l*BIcmlXS=8Lofg@R?U3tN0DY{jTGrcmqS8 z{9r!!j4yJNtxtlO#^Do5InN#maZ?~| zF_gt+IKx&c0Bt=@=szTd9_~VeXWKB*PGK?&xWvoh50^{Xpa<-?2(kv}8 zpkxA>d{r`8wHE_9AF ziWBmWnvzd(B*pT_TvzM|6sleLgi3w98hLb{_QokzUK@&!Cct>j^&-oucqv6cg$ zs5n}my;@_BDbFAA3yUV>RF^(go;yzSm1psn9wpeF8c)%6Lr`<%Aiv=hO>1lZuDl3QttElcQ7bGE)8>|1!|0z# zEW`N{8s@v6sXiEo9VepsktfA8S#d8as%5BjcsbuD#%Q9zen4Vl(;gg!feHG9E4xjbt@y3d^h84PIzYqEEgyG8U4v5Cb zvz7J_NMnU848&*SRKqox3_e3*9gi@)a1K3umcf!H-l3-^we0t;YRD#Q@Ji9IcynYa zJ>0AYpPLVvzB1eDLqV19pD9JK8TPQE8VNLcfJpQSXKQykWB$ z)vf8}w>&Lr#mit!7%f6UCzg@*%FU<^Rp(kqKBLJ?8mgiX@NTgdJI2Q&{AwwVEnbcH zvPnSmDLnn>%y<00iN({VF_jzLq_eV-d;Q3w7n{?_@AL)=ww%vG^M&ZmpR4>_OfZHg ze}=lITSo2xq$5FsiS+&+6+G-om^|Fu9?z#lF^79=ue|`yFw<;R=Gn$n@c?*T-0o>E@871CU;`8h` z;giaCev)lRl$9(x4=<(x;jb7vcQma%a0h`?=8|}ZH@>)}ki-23Qu+Rqe%}7f|42?k z($ZMu8o4byU8hM^NV|H*&R&2!Jm+f|zG9(sTi>xvO1EKind<1r~x zjAXkmBlz7Owrsy1jh^PjgD$@$n@^u9DoGi!sj_^Q`(P5QlV-))X5{YK%0`xLBhPuB ze4GF$(Aa81r^_djo&FM%88IGrHmqigf7>EDP>}Ot-VX|WYfEy%lgM7voLwptU|Wyo zl5Ev*s4wZ|l3~U)Qq~wMrsYr>qRhsfI!9rd(vXZhj$i8@agY3+bfn4_^WEBL&^X8viuj# zKD&>W>gPenSdSk1-{#hb+({?ZjM@xu)4QXosQIZ0xt{`Ds%{mGRwV%m^#=BoMY=tFPGd+Vk6F-uVUxfA%BT4RAW@G+%&^?kzPP>8N zKXEAid!|NGhLvn^IcV{=5cbJik-jazM}0RxqakG&vy0J&qe~K7mn=m7r_(6l*BRJZ zc~i!l$Mk8HG(Fqw2=V;=R2lIHNogL`7%&HS=jO2fzjIJF%z;gQ>4~eNKm9Nnu$)RUqJmE8YS=ooy>qaar`!y<4ws6JH-Zw{5##>zs^IlJ_wz5Q)zp0F;u+|(ijGu9Zk1!NLsEhzp%U4H$t|HufB}v_a z^Tsh5l&s>d=Kdqj(}=BSg4>{%%L%%jiSVtJAOLON-9hh@8;Ctdq$svP-?`cDkQ zI{q226}(xO=3i1c=0}5qG^nB?m3X@f?WXw`0`bgkH4nBfq_g^A7$a#< zTW0=bj!##CuNUFv&Ub0~Z7Cl8;w62zI)|KOS9%qBf;U>#;-vZt7FSzA#aTkMt9>4+ zDEP7*YKLm2E`lsF>GRrlGBXUPGuflq$N&-e99AQ<#aEE@;}pIt_Tuo_9A0`j1Y4#P z+jx8xhLkEmLSZ$g&brNI&Q#JCQDORGE)07eKdvV~2_pMHuo%0eH1v@)KQKOl{se`S z{m(9Bxcgz>pKrA5_A3_cWkfbgaV$kYggUx!@q+w3G#Ul*S-pGcxz<(qXWoVRz-4Zk zRZF*)58#5fHeQ7N<$^&bYE=JxzJMmtAyJr1)?X?AsVf*PbuNh{Foh zI&>Sqeb62Ptb;#YFozOaqp5!4R;s#k1L2oqQ91Gg%X76v*!b(bP+A_lj(?%K%{JtJ z`z>vL7)2K62Ke?#R=8$27!Rr1O?kKO{4&QKIycY|j9AkuOh#+T3kLJ>I) zIBs)-wmmJzM5iXopK+f#l(=Kxl1n6QAxD;%H?ta#({yfMAr0*Br^)Y)5#;YfB57W1 z<4QY-H*Mx!OO6TlvoAcvV<(+2Bj%gAjH(aL1(UjsSr;#`Kjy}G^EHv3ahi|ceadWB z&l-~dx&yXrmqGgVUYJc4@I?<-)AP1TwDikq{_*_*EF1TYcAhguT(dPV54?iQnLYf; z=wqa`=MJv9bVJBFj49oyLi_|NmZrKK)-xvZKUv?gJu(=tt#hgS)ISp6YfroNV|k=$ zHZ8xC!+WQxk(0%6USPML{=JnX*B%2nM?dF9k{K`?ql+gyZ<1HM9WU`egZVut`SiVh z*c-0R_Lc?H!{lyK7JfmGhkBCg@Lv4db&!b zAC$jf>&3%x0^k~Fp{UGh)=d^LTE*xdl z@vE-}V{hMt`|w{xsAj2fk0P&^kw7@^2`BSo>a@ zHhLS9lhwKA+!gfq@guU1tf%{Z^{iRodmAs9%hU88;ov)CJnsBQ3&pGOV#$1Jo0!B@ zs~b_YyG@Xrx0P8M&QO$|9A+F27UbQX`+y&&hN7=~0pBdLYCj zb951ZbO==3J)ItPz~Oxg^o!Y0)fGb}6U#DF7t2AFf9Uk0>R zeJZONR1Z7fRIJjm#KH-Y>`hV)ZC-r}=O;a&M44bV@8Uh2(p^JUQ|$yeofGN)F~;rY z*-Y3xoks17;U`PY=>DSsHnlE}YIpr*oi5&3G}I1@c7CA?+9P14TTkms2Vh!}fIqb_ z+5STeO35CW7?Vj0oYW~)WegniXRrk;HsZ(|Gx7}-VC)L1s6F)@gX=1AY-tNpH>7c| z@cVe=rOcMaN}%uW1Ew!EQgc2*j z$g~*xEx;wOH*aS8>@k&U++e{Wi|J}l6~@GQqwLgSw!~;7-ds&%;hSe5Vy_TQ9o$Kk z)K7O88PSSgt62XXNt&$wg?d&0(q%<8<{h<=9F05BBua9O z4i(rTxt(PATN)irM=4d^1qt?l>5$PM{17?Md%s+w>cmw3_tXs<5&M|prX|s(YvcHj zJ)5Xb#)V0ix4R;MEKg@ss=*QN_}BpIRSFoma}`fan_0*7dX%<_uoD8_ zJKoKlmgd?(_4sRaMOYwifgz8sS0SB>C3G-1n3V1x;)SzC=uXKe0p_`sXl*d-Klv4Q z8~oX*HHJ8T>>3VOJ78AuDMTbkk=2Y%FuJ6QsiRz|@`N4L zNY4%wljG_S>{Z!A8am=IuY03Qlbm1jzm9HbY#72kw1-k{&N}YmKMq;1Uel?-i)fp7 zmlkU1BVudHMc9+d)gSQquamJkZ#)0j z_>Knayk;u)s;Hi#NQt@1u<^=aS7v;|7GrvZs+|GX=VxwJ_2a_$>RLat-BBT>hAgZL*Tk?XRG($G_2z zlrwCU*k%MPIP;F~S#+tgli4NAroQ!~*!UDZTEOR`uH^{gXSs37EL&#sH}gDb>D%nhYJ-dU}mE5H=r&E}!JRHVjF2dN)5bst+@-zNp zV6-=xe^$|XDm&TOr}gye)oJQ$n}VKbViikCxku3&1~{Zvghh$PgpyaN&z#AY$E=0u-LoWRl0y@8ZZeq}moYv& z8^KR}(faKTwP6p<)xQQ^i&i@H?;;=gdW`a&r!(_aM)Yc|4tM<5Lt{o1(szR_SmhV6 zhGZXsy;6Yr&BwtuJj9-C75rFwhwW*+j900LnPT)qN>y7=R#l1gCt@`UxHE*cX5fsy zJ}o*w7Rwy1(Hi8$obN595585br+70IttwgiSw}jsO~5_%*V65`!qB}Ki{;5PsndB4 zeK)zpub=KiSZfpf6{gXhxPx%jEQfF7Aij9=4>*1KNp~Dy(Yi~^n1v>|)23c#c=R_FteVS2hKtfA z+jvx#-==_PgBS{dtUFHZbbx~ZU;n|Ug<*oT`OGldJd zhopcVbA3dQKApm})Pp?T(@KE9-No-!&oQpe11%^govHHh-g=%cE?vrk>;B-9X9+p` zL{QVDau!&dC%E2&Fyic6GMoMc`%V4GX7F)Dq-2s;@Gv^^>>dr@k&iIpS*Tv0$(kk8 z$T&R$gYzHJQI~pVcfA>Wt1@X{%R{(9HFONm(qorC92Bf!Pru_xFxo<{cc-z|$G1^v z?Z%cy?WXNQp@BO>=cVKzrjqe7vBt?C+uvL0EcY?Od^8P;6=!)X#ASrp)Y1JR)YHLl0-i_U6Nd!eWo>)qVQ5gsr`vkskc|qZJMi#pBUNKCR}4-rj^ z-$}A7<^qmXyO001bS8~q7F=`2N7(n|vtQw^ND}Cu@)TZDlbJG`t}`0{b~YozT8w&o zLU?(ipl;Xp-8}kcJ1z_MtBYR?Xq;3(8)W29DI*rK^AGc}(_=sR{rwGz(wn5`d7OSb zWFhiX1kUD7LZr@2x@g_Sd}>LE>9 zoA_}d#Fb4&z(5F}GRKS}1J>f*%?`4eb)Sw;yAHnBh0R@AiF?hp?3>L~lK!E_3eLZw zsqQM|7W@w!b(ry219KDG*qWt9xbWr&KR+}O!W;rodUs6^yrVmOrY?_=U5*Fw2 z!F8Q9$h?BzzqSk7`=2A@fFm{it|nji92DGn$l|XX1$4-Z&K~1L5v!Bscj}Z@C6qh8NP)Bj)UW>@E5=UxuZ{eW1ak3Yh%@cM3jc$tP*b z;78ad`llrZ#l$6CUEYH3jZ>lELoeh6*xei6_1TPU z#t$R&oGyBsbBt`nH=t@-6f|>+fu2-sfBYL)_Z)z7pnwNS2}6wmqYb*PkhPsc4Tob` zMP>qBjZ($@q!_ezZDWVWib7L#49$`4pi22x?wxo6BU`uQg6I_7unT65$EKrB{0DJK z2?*6g%%y&X1r!!&Nsp!Xv%#@Z zBTYA zz~cCoFk3#F3QLBe&G91HOtvC^Z2_f_e?`7fKu5m!1m6GOa zR>gF&el1%UAdl`Bs@UP$MylHm@o8TD6gaDgi}oeZ>D|ZZY-|!VZoedpx-;}@%T=6t z7E43j0B}9CpTCsRrBOuJ7xhE8xD$sJr64w`nGd?=3dhkt)cDR8 zCrj?|`=#f|(*81=Y;g)VV_cZNa~`^U>S!&e zi^cCSzRMj(Cg!Yt>n5o5A7i$X0?l@vB3#Sg(BXrR$UsU7Zx{C=#PSpZ^a9z=uiNls zoj_mP=ZhkNwz<%6741*?hNM9!Ddl_yoA)b_E^m0i9JG6B&r4le`!;|o)SKDqre{=_ z^Owv{h||5W7?Pekfrf5b$)B3lq1()goYY^_&ks?kI~_*))$@5)uN9@~L@~8$E!a=4 zz<7yFN=cN#X4C7`{^~gkJ==z+hM_DkK@8tC=dgbscaSHXKm~3|h#m7D`_@~L`1E1) zTuPvypZAf+gdBjz`%+pQGJ&pMJ;oev_))U7FL}=qtoIyACZ|4}9^84&0$+=fgH#x* z?MW=}%YU79WNo^H3)2170HO$vs{&yn_rp{kT@! zLa4b)bKwmhbTI7~Hu}CsPjE2`_N~W-vDdL9R}Hf2Nffli59!Mf^9wS5_|SA26CJweF$ znLN=-9Uf`jOw7}VhIYSXSq2)Ee|8(s`VmOMGBSLn-A1_oz0AJP72x>viFP&KI z%EDV0psQ-XKs!8+7W*z|1Bv5k+VNq`>U9nc9O_~j@26u%z8%gxJjC0NYk2ZSee^ZG z;5$8+)2f}e-0!{!c6u&AO+gcF@B4raVy%?Ex)_}eac~?tkvx`5P{5L}Fy{~Ou-%2J zEj~uwAAgX3&S$*ucHj;H@$|hQ5A#NvVWQV|n%0;K_Xi5BB|3l(W)?u;JPPs}XHop? zizxJvV{;z~^lCkCxxYa(CMzqjM=onHQS%0W`ePonmxu7koA1bBX9VT(Lb@&W8y&+6 z&>7wX8=a}tHDNmq+kBat)>YGW-!&+*dds@1@6grl=h>Vz6Vlq9z|8BM@Ih%0+_mRJ zWWrc{kA8_;#ZfqEG#h6(OhkUqAe5?|V4IUO&>X&t**b>!Tmp&k*JbFVyi* zauYkZ{16s~Ol6C9%Fv@5F8s(hd#ow5Cr{mtwD$Bb_Q~TmiRTBf_qv^EY1>SbI;7}^ z>lt?UpfaLo#PiNgyWq6&Ig=f-RG`b!MBH&lntVW&Cu|Ye#ii0PzM_qjE{6Q#uFKT^ zIttRIm+8dXFZ{DkEo}*V1~Ydpnlz(=iG{8t%PWVtn4~E}i%zldDm}VlA;L-?+0*D0 zPugzYgc|(=>|*~A+I*&lCjT~|w+;7rxLG)irnOVhB0UJ3cG1Jf58$pb4vW4#gXdOl zrcobB5)GetP5mkoo9W4dkEY;{07ts0^N>RACg9@R2lzL|2MsfxEmFh>k&nP0vyN911F_~^$kHPzQ z+wfa8W_6EfMhxGH;bjov~D*|?OTa%FAa1?_@lOa z7ed4BQFi?h?@>RAuT%QC%ENJZbg-E!C&p6I-$r&)?--db7Uec-u8;`Q;5jrFza{h7 zl6gBZ>fIJnnwlLnHbDOYPO&fR&yGX?(qht+p!+|V3^mhB`f0dBv> z(}0bglmRw$E0=#i3{7o9m?K|;AdwnwH18&i8#XZa>?hzk+gMUxGTB^x$gKzMrn}ZA zw0x^6tyt{KdsX$ZwcCgHhHs~tbBegm_BLEji6e`iiP)>mKZm9&|X@83%`?p{}O{ev?%B6+vOwn;|YtJ$5f0y88_#g<>>VHlI!ZR)+OT|O7Q{XDxZN5v{O%=3t$^+6I6GXCEmZYwBgGbEzja$3xu%%*}1p29;@cp0$r|2TM=oDkms*il~n8(z+bw1rYB}I=X zXwW)KDXi$Xrv{Z*^sMq7m5jQHt;e^p8O;m8hrgk%PZrU^oWFd{uDNJ^ki{Mvx=>Nl z4Vt~@K6#qoKuC-hT^>G!!oOHi$VofC@auH4QJF`(Lw#Pud4&q9U5wxm)7@+pkFQiD}2c~}01q(nWA8hG$$vZAC>`wr@*%kY)r1o0-K zs1?K!CD;EXoB8iq&%HsUuqlo;s|Hb4a}5^EUPya~MWQz;W-GOqQoCx{6USS<}6vjo4ihp#82HeocT6{EveGQGuM^uz>h-G zPmY9{{1sYbJc&!T?4dJGW4O26ak@K7hc3R>#{0zAC_3~R(~`yL;zApmzu*c5>z=1s zyThpCd?XdGSK!Y^lwhm9HIEn-j)uoI2>v!xU?-2^Msf-?aMF4qD?elx`w(KNckI`QhlLhS!tLh7}F0)Iq=DXNOXOkcq933Sxn2UKar z>5){jIhlt_8=J33!@-}80@|X07UBRG}H|W5S7W7mwNGVRB8P`pr z6V}WA8r;RJNrP#P^BKy$F2Ne-8<6Mh^ZfXbI4rr74B3DCu+eTK>#tb{+fA=%ink7= z^xbh?tDC+j3)9`FL`>h0f68vgwtp=YqPPeB6IL;GNWs%8l8xOkk*dn{1@S&U(6vq^ z5ubEOWQg&9Cwm3_q#8Hr0R7p#jJDRF!Xr^)L*t6@?pQwCSpE`zmra*VMuYtz8aPn3V#FIq%lhnOatiDwalihC7&4DCZ9{isB_AA2Jw4K#7ji%`Z zw`r?M2j!2gr``Fvq!et++OO)6qr*lxe;$M{ZFRV8eUMhT#L<#XgJ{h8NwmFOVD~rg z;IX~&u>O63->e@`HG>wgr1$IK*(gEdJPzXE@KAPtb_4Bv^_brnv=6IRmtxlCRFZjd zh_8)Ght;TTHX<|+TRJ0X_%|bL2suc}0p-v=_5#-%e4*gy!MD89#IBYS(iXiyGZJda z>-!nxf6(Bnzjh$*&pJdbE28^usaR(E7(Z4_MA(%=tGXz1O$;z6ktT$1z*3u0 z*gVvk+q;EP->PQ%H|#XZy*6`2_iwONTfoN7iN~XJ3k4df=P3WPkn1@2;?Ni~9zVDd zj*I2Uur+{sYUXg&n)PrE`%QNDc2KsDK;vATjxAcg?AE#WbokX)jggbkHFlJDAm8ctGVR z7Vx_JC#le4E{&h`mzI=kv63POnBB6$`Dt@dzqf@ej{ixYU*_@-iDpz*#_`0cT2!gV zu$P6gw0nOH6SK`EcU==ax)y=r79*|_mq8hqGns1jc>DaF!a1i(pD>dg4Bbfgn+2a{b{j_73n|kgN`Qe#cFi#LK&>?RJ*T_QHU-H3Zr)uuDFPTj4 z6Yp7Yk7_3;^Z7CAWImY4wKj{M$Tsp(7M5f*KA(@32%{f|I5`z;CDHd{tY-ElIGvoz z?9`uAm}VZ&&>l`sGg{dzVKI8(ug_~6lgLxAg?(weN0YNk`OnNgWRG0K{45zh`QL_U zT|OE3tia8W))bkL&kO`f6qjySWyw~T@$u+Nik_y4s}kdwSC=~43+__5*E3Si$;OuS zg@_IHBD$eT78KQ6cP|{))cuw!;FwnQ(g~&SrHVKm@Cys2fkv zFMf>a&h#ezn!mJC;}Paq9i^5v&*_A(4;v$yNwc&C;~o0H(e@@_Ic{&m_qjxAqDUoE zG>D{`q6rm>BvCX^Dw;G3QHC;cHVwM8ucMdSBIvw(otX5U$Gh_1FmhcsUKuwV zBVRbd(Yoz$qUaP>KRZVFZq~vTQ4>V5o8VWMM2hMxQJ4KT<}X{13KwG`LS!eZ^kh;2 z?Sn8oG?xyQkHI3Z5F&lK8RMPqK+Iq&D1`_y_Dm}v>d{tuJYfuWhL^$alZVhW_7UaK zPK6(5Qn1Tl0Sap_M9FuS@aYy0mX%h*#MxizbiGyxahXOky#RcCy~%#|A9yS%i8wC{ z10#tB>}+Vl(&s@md8a+*_ zJa-B}vg3M`@3N!uAuqrkQ|au7&mlr`4f%c{08ZJ&qma^Tj24W5c_&g(YR@frxVjyE zgDUY-NDDsqD}?sXVlbq{fj*n%a8hX;W*)qbLZjHoiM<=~2mcwOt0n>wTlwL}T{Tq6 zT8wVX7hvIpAUwg?Io3^z#lwPGI1v97I%LXm&G9e9LAL@N4bs6O;xfh_{6y{*9E5F# z0VH;G7@Ue$#)sa0(B-+3PBh}jT>&3ZO6eZVtxUjE`%@re%v_>nE`{RjS7Sz#FC1EG zO}!czyQR-MG{)T!Qqw!I(Pb;-wOd6nmDR34X8LTJ$Abcj$<#1!FiY-{hF9J zRfBn<8r3*ZgvE0fLqMl1-ZW z9mHpSfm8lDur5BG#xec{x7W3ix<&ptO~@3)0`}mgF(>Fbp1pYL;WS9HJ_sCQ(@3(! zAh6A6Vo}uQf@*pLDQh1Es&@|3oq3#C?6U`Nibp{ z5-KD{tr0h^j3*_R?|@;9ELt9FMe%!!@qNWj)O%4w*?lwdBx4uLtR#ce=@lfjF%GjD z_^9=JbMz9pMA-)npkMzADsN;*xZw*c7{7R}v1KIvKpo_09Kh$?w(w|1Ii#ibqG0Py z68$O%zFo8-DM!Ac?DiAHv0o9?bf(hDnGazX$1A#M>srWekH;T+;^3RR48p9G@ttJ~ zov*G8WsAK~wqpSG#)y)jxK7+6+5rA{)RFCcH(hq=GERG$fkMSUV92cpiUL%@HQySQ zygEVBzXyv|WhYM~__Yf(ylaG5r2T?2NfjUO3HxF7*^TitP&smRzUTO9dGp=o&hMdk(@Gf0{Vi z6O#>#;B*W(#049WmkWM?oZ|s-e)9;1F07`?nL;4UtTW+!Yuu_9LT#UZN6qhkxP5UI zdfr$_&DMW{6V|on${x5~`<5!vmcN*du$7%$jEasvndXqh2ZeYRUMpH3fp2 zK|O+XC7!(60cR$ygd1KD>Av?{08@kE%vcZHu3doF9u}gn+y%6_NWi0LGtm>KFfnX2 zJ*uGz`!es~>iARWx8WRIDv!kS195o#)FMpY-bR1aoPuuNxnQZV4>B)#Kn! z!uQ}=sZ1Z2`oXwwdPH+D2$ng;kdVEGux8^z^l3SbldZ#{=GYj}>E2FG^jM+iTmjg( zVid$F3*br(S9rKN7UuAj;tok}GQf2Vo&_9-uf6YZj_7*gQL`J8zi%MR3-{n!&Pl{} z_Gm0!E<^O)55b4Utsp9Ag{3n*$+U;eJh(fRTDTqsrwAGN$a52wJkG%?`UKO*C4>6M zD3sR9q3r>+&|{iP_2kDweCbly^?WBtzV#)M9v$FmdmUmnJ_eIV9*}b)3FcX+K!2tH z=DvPOr8Ib<=V%x-Hshxk>VO1!V|N!wsxb;2OM`+(gVf640OMEFgYPZ&;ho@z z`10f>5b#Z)p^SaTh6HAuQ8fpnh&|ory$Y-LxDdzja&Vt-Gtp7B!mWYK`ej->Y}yq; zwj^c2)m0s&DC{zxWqgX-Zz$pCp>deJjiO}NVX8F$JajwEr2B5igL}CK?y^k88J8(| zq^n@A-4tAQcn|^>1(NvKbj;xShChrG!06F9dbwyFO3a>5wmeG(;k;R}FCzlVYDPm+ zLnW+GbVeJQJ7|zyhu5YJ!kWYl=q-@}MyqRxk#QJfM_)~@nm)!p%WveeuPdIzB+sG~%D!94ibkbsv4pJUEy zPO#DY0?BK)X#Y8D&3T$RG zqZeL&gyyz`;KZy`&A-qIv+pef{jY6D7tpVT$rDjL+5$0L*$DSMC6(iXiqgE9SSEgGjM>G z-M@$D%@0v!sV=A_rS!rTF$n9CA>sQK(~k20|8zA&*8 z>Bf}gJ0R{E3Qv}fK?#*8h~;MdrPsINKu9&oVeEo8z0pUV{zy=8R{(QCL+BOvqG#=m zaPob2vPWGUhL%mIOFAxL68{UTc{LOk?imehJlerEQ3}G>ao`dWMkjkQ6b&r%5Tfp4 z)biVO%j!MYe!886n0&%=-VSogRUB{Fzk(ZQC%|1dPQtIblNo#7z?7XkF(P*@UH;hx zCwp*X<10l>Rrr9OYg}-~L@&Jc*$x^_!f{1yKW@HgMS@P8fX)Lvq_I>C%DqI$=CL2Z z!!(>kd>;p&6E0JG4?idfJ4vNf)-mgs^I@*?DonT+2WE^t(G^}kGG;{smeLbonEedw zId@QBhcx_A0yOd20#I4;ov1MDMO<22shNWkzQ5T|j^KW1ImQp|Dnj5#y0PYB4CBAW zNpr^E!z+%iB%OB5=4G6c{CDJv_G?Ja7RT_-hwwBr4-Js9g^w2RsftG;>U#H(liwF&&}@Fv zZP*JYI+KWH(@Hd0x&#XvTN!}Ab>iIjrxCcL0m zv|X^VX$?xgt%0+R(R9Y$36Pk{O}I8$;+u3+I9t_>j}I!rUaPkln%hc`Yzsu6v<@o& zMgSKJq+ye5E(p9Ar4RVhp^+XWA+wz!dUY9Ab~eDm>ix7!br)PJu)yzp-%vP41G!?j zV9k*X-2UYWh`g==4aV;(f?2C>nYj+v%Dg1?8vO9QHh`J$GS9=cEp(s7VRY3H#|(c) zH}*pp-tUS+r*pbd{hY?`KFMeq7-;g zI~tYpB5>!aL~62k0UB&q$8joiVDg**G`Y|R&r@`v`XW2Nt}37_EY9JhnT{yUyhHcB zzYM+3Q$bt%J>w_Ii+fe2=#kYbU>vOs3#?8<1n+ff<;D133i)F1`EKOC9Re9!O3;S! z1(Y6=fpf8=;fQY+SQBCT@njWVVB%FNbX^0_Ic;!%K$*E2-RZ#{bI`_R8N7%U!qAFp zn45PNp3GIiEnD*Nu68Z9FYPaT4Bf`cSj zJvxsi-BF-Xehd$M9fVOA#{uuRY&gn6LA*!^O`q0*S&$M=x;-7jn<~k-5B=DpGlO2c z9fF-tmB^7b(!hVMmWuW)gD>vwFi(sV8$H*O(aU*I()k{6_$c7oHGH6{y#!B-yW#3? zHf)d?Bu6Kg!S>?z2$+x)ME-Pr)+>U%0SB z1s{%4f)$s);BLXw7|Fz|N%oIr{6DPV0WG3N(aGQ^eTR#IE=RZ)OP5ahqB(w_1y+Ief+S!W)2GcV&E^MTve^KKOOlL(ZounC)OhK1!^{ zq2P(o_H_rSZOMcO+&tjA^dkw_wiwi<&cRiu&oD~%4l3}3LBWxm7&71lHsw?2`i>RT54es(tMe)NXxOZ9a*UVU9*NtkQZ0m#>!7t9lY}cuEyL9#8;W8`r{x5(kv7sURQw7Ql3| z2ISnW3k?C5G~P!5Rr@_ii<}moGe1x2pSPg+_HbA@R+^bJ_K_{NDWJD?3soMy05@es z6SnO;v3JZy;yIv>^rR74J)R#_J2w%lx)$hAlBJHv&!PV_aWa7~2lWN?;10I~%$Ey7 z!LCYN-4TTDt4mRRg$+Kg9mKgWO5wnYyA#$Y{btx3UtTmNrS>?(IfzpLw!v%;Uqr7`}h%Y(_m*NT{B7DnaItN=0JKJ>WV zh>H)+fJ5`nf~TVz9a|F($z0p8z-a}%+2@TJYs{f!poUu9&%?bqi&p2xFtM)$P{4t? z-^$KIbLUEo(PP64CNJPr?g2a>v=TnAIe-~eYq4a-S$h9M3XD7Vm`=x1ShB*5oY&io zMROTnm2Hi|LH9<=8;K?r}rS)J3rW^HCU+ zV2VL26VP&FB(yAugt0PLU=1_x^}78HjJ5W`vic!>mZt@AQ`Zt1#)sXc`mg!(N(`F$ zkti)S#KH7aI66}TKAf|l7FUO$P{#!8x6B8-X_cUn>xuI|jD?F=zoC%pA#Bc?4f!dS zw9lay?wMM{LrXi{(#cJgcQf&6hy%`#8U@9>c3~LLE8soP*dYxv(U}Hz;g;i(n8o`9 zrmCkxZR}Y3X0HquwIErz!~`ww*-+2HaUkk!PDGxCf$%e4I!QqeZy)!@D(UwiF)Nv# ztDKA4Le`k|d@T0)9Kxj!6hNP69W`X^+c{IjNt0V6bn|Z_la;E0y^aTu^r!+4qfh*L z>mJtJ)4`VUM}a(eh=>p&=6A2%7JvXzmmF$uT7?8TL< z+u?NQ5wdtmJLnF+By+M)qG*W+L`DsPiGwSd$HbfKW!4cbW7}}C1>++T^@h+eYM)d5IcVrhR!$4y-NR21~6W$&W zoZdvPZ;QmBOD*)r$sj!TUK(^;$KVd<-E_m(L^mH1Vb2!nfaPMx`07 zi)^4ek+;F3(uwj-z5=bcB_VG|C=T*^ptG9=46e8Y`9gIl%*NRJTQtGRsK?axk|6Xm z@34$rJeWZ@(}km#K)~}GaLYOhWPF6^{(Ijc{8|Y;cxwShez*>i4jXXZ&TulbA{O#D z&7teY2Vl)*BY1H!0{D1mfoO$28c+C69`Scz?d=wjPrQcizBc&D=_9OJY)*B)bEDX0 zJu=Z(8{X|o!5d4(a6|f6kk)g@-4PqG>4O=3sS&0+^FHF68)7iFp943`n&aoEn{iu9 z8kvz(j0InHaR>h$2-ba!C)lFVN7E6OIPJvh1rrH-m?^9a?!kk~j&O0x4Y)2Z4DJh3 z;eq-KoIzZO+FV`8VN1q>6U;i<9!YxFPZHJr#uDqK*EqN2ExG>X9QHBm8Qu?>eV{F# z(!f2L5b)_Mo=C1m6A1wt+L;fWoc9?|tp=DT$pvsw6Ps65QpsR*GHQ9p z^mf91#wNsfu_dbWNR!Cxr(u57J>X-a-Eb?;AZm+j(Y;_bi8JekP^k!Vg=-K>Dio;8 zUIS>lu?f;S24F_I8q7(X4&HV~=zISN-nmjpU%9J5&d!(cJwXYSZdhP3rx~iqoTvLw zmEfW_H&CkDh(nM}I8AQBv4iu7<&HjBpPNRa%nO-~xSU|RRXV(FQ6k^IeZ)iQ$|yFe z6qh$XCv8T|JYBAl@nO%#!oeTR_<~t0aoC3|F5H3rApv;U$p$;KFJO<8F^0Tsz#|c# zAoox-F;oe~4Hr|fz+f`I;lF|Wy!V+{C)F5M9|Q8eSDABg6(`!o5nRI9cU3OLFTHFy zFzy5-nAYOyXTfx^Z7R%0dXxivQWmhuf7tJ6(&wD_4 z`8&Emsts4&a;FjFOz`=dgZT1cHSC`fO81Q!f?X~Xaq0_wSoLlpZO_sK?X-JT$^9L2 zIj=^;OCKRz*c4@2{o!~}JyHK^3`6r@W7n#gAb)%UZIRG{Wx;~<=qXE>ak`4u%$<&- zo4(N>*LazD4>M?YFEVjUxaiQk@9@rZKcFl-qo2qm#+EU-vBHQpD_z8z4j|7}nlQvK z20e=J!@5Le${w>6CL}ZSRvRjDaz6AwIgYAUg7D@2S!n;(hRf3?qGPW- z8arB`wtp`%s^^6{YozgeSt`Cznnd$Onen8%8aU`lqM(^Eoi>#h3tL~%-nb&h*6R~g z?H>nLT4l(u@eSMe`4bDF)$p`biLT8%2sd6ICgBq-p;S7Q=3adb?zZ6=qnV6V3d`xl zNM(qWSw}OBI+5>yF*X!3_o9seZub>KPlZ-YZjQ$)5gzcI?S*C4!SJXy5xs*f>Ejo# zQNQjx8S=h~Z7-a#Y42Ce@!7z{E*=YA)6~f)lLHtKd5D%?sAJZh`heLS3EosE;Z_}C zJoM@&b{iMs-W^pKHufam{mWKjV+J6f9Nl5;mF5fid;`a!6!eS@T*9|lwB?)?Zra08mfj!DlBUg~aq8H^w6)BeRCIQC@*;k;!9om0lrOHbdxgXR#hUG)hY%tVOM z>)H7J?qgD0nT#Zhglk)z!#3Gmb1@ zYzS8i7GoF_uhu@X7h1|yVDV&qYFJPJ6WEGq;u$a4AO4NFTd3lqPuwKq2%) z-(}($lu^U16aZ;}+QyKAKr$HKNb? zc+t9ZCP_Hnf_s8AP!TIpBdmj-UdY&4&hCZ$+yTaaRvB!K*W#5DbsD9x5KBMa1Eoj3 zU@Bcjr`+>{+}1c)x;6>S?|<0P;LcuXS@a6($I%P zAG4tJJ>v5tl{m_4EKcKb2c>udLz4zkB4q~U*;@r$CMeUnc5yJSR-L}&Hpikz9yChw zF^K=zPPblM%^b@ae5St{&R%^8^T=n|e)}5bwOxguc6DQ0WG)t2*F))yIZ$523zObk zm5Vxu$r&X zCJ{?*=GjBBgaWDVRfY7lTlihi1J1`!hj(vnu*YI09dxkAm$ep158q+p?kM5nlIvJx zsRUl0378V=M0VRXpzrBkvbBg0?r&-ae|A%d7qy@P-BnCHrqh@bHwm-r1#v()7PqaQ z0pHS?__fzdQR&bzEXeYN`VS|7%iWO_2aSf#qXwYFE`@G2DOBy*5NN#60c#sTd6_4$ z-Cqzt9+4x3GP5vCVG7w-X@>I`1j5yw&P@Dr1G=oY3;Py0QUl2^ILe&y>q|GmBbpH) z>@o#sZJt183C6 zocpMG;beNk4DNNa7gt8vHN@p1zVb^{0$>iHTaMAA)mSh~k5UDJ@%=-kFWE^K={z$_Pk*z!4Vbh=o0NO*Vf3y@ z^+fdWTV^!%4^@LGUB)ItDhZQv8C!)VA^64X2WZGXfg4O{kG%^&K??tES5qh;-B>}qL2Z?7}-+*T`4d^L)^3@F6VCxMjP zN*fn)TF~^9>{xo)8gvrfphi-jPBE+j-vCe0)m?_;kFLahJ94l`K!NW4Bn6dcLdm0; zC5*rFQh2a?6YzC*5Z^nSkxlpj-qC3Rl}F~JM#mBj4>yxCjuv?5-a^dU{g87b2N`8c z;JDraRQ!4ao34A1MR_h*Q1XdRs(1iVTw-Ly=>{fV+!3n6p^qAyyy2$WML3*XP1mN* zfTx>0aHdBitc&p>*XvhedPFU@l&(R+)r>!*!3U^U-Ay^=!{AF)3USH`#nk;gWT*BX zcyBVB47!Yk!u#RGpNT7aSx^@gw7t>csucD+x5N1o4sd()3SLkVdZA$mW`tVgv-&C!sqVmYHhfUHI1hDb+~DJ9|ohE z(Kb(*>6gmzgV9&Gw`z*wu4SJqc5}bCH}~UE7%LbhyM`g|xIb{!tWkU2$auxFBY62~n zhxNwypgaEyp4@$md`M2l7MqtuD`Yp^JzhtYKpLmc&Ldg}JK^fX9I$Pu!98bsNyAk& zNNhh!ZZLN1ID6o9qvwQDb2C?VYIU)q)nOg&1D*1~cO);^d_} z;CW&#j*`*9Eo&mF;vqXUv2SI30h#?!vI(Yh1~B$`NkrT;4CNSK$37+=$(Z!FRMYbm zhAd1ZhBHE7_dq7;cqIb@QyII$sFm&3y6;LPk}cu!>F-oH@D4te&!86P=AyLUM`C*G6TYe>xRuU@7A`wXh6Z^3 z$&865T>|9_RJN*N#=) zAHaPeAJ;R!cMgjbaNm`9>L;U!zU_SU%ckdGdFC}?WBY)%tIVn4f)*Il)Jm0w>u{Tj zFA>;JFs-r!RJs%Jw%0@)+nf$^^MH7Io=5hT4fu3pFC1#rC;eAcKyUk3>c_^7YquBT z!5y|3+5MKd78qb%w=Q{JbP;Xt%A%}eI@+19puX)l@V;0(6=@d5cRvc5x!iJm%#68% zH{S*KP8-@H)dG!AwP+E~dpJM&9c4*27AagqrJ6o^p~434Enf(;m{`MY5sOHUULM>&dV*@{ z?7^gxD7+SP4-U4L(6d>GAZhm?Z5Le%+Rv84^r;eH;b~2%`8S-GrUOs>xG~pPf#AA> zxZFqtbQ?#3=tqBYb50yA4{m0xal~Ox>==mCT#2)?c7gxfXcQ@}A#*M>W1`cM=(<+` zmTxV>p3Qm8I_NPv-_!@jmNmjXw;^;56rjQD74Z<$KfjzMi2L};(f`CH@SG?_&Ku6a zZJ&2QTjl_0yWOCbm;9j4HNBH@B8!@C+l2agf2FC-0$tMHI*}u?-(+mC;YH5?~9H zZYZxE9mn%yfek{2%tc!G{uIoX^TCiUUSK166QX68;JUrd^s8hLn0LidGrK*I5noFabxKj|{T*0TW(gZ2WTAOO zA7~!COC-k32G>CmI282(lKqcigm)P}YRkb}cSCU{55*$_U+~hVF0@&C2m5Y@(07au zzHf{oEs-^VScjLeQYZkZ^-S`yawSSvm=ca4SJ>gyL@#|QfiI;Y_+)B3E@9sTXCjt^ zeW(=}MDxPnR$r2}C;*A13x?%Y;Mh|EWZPLY^k-rO26wsOD%pNG+tda8?p$DcLLXmz z?1I!Y!YCW#f?F$cp}p0PPG#14(!~y=o3J*H(zyv+Hygp`PaI$_{t4eEuOkbeuw%%F z5ELGG2TLq3(7ygkOm!)ynB)#m$5J@R>kanT_M`NR-FW@LUQAeXoY9L&5T3G0s4>fp zd~&*qhWp*|X!;5;ZoWaCGebeEe>}Dy=7YXvJLxO`YSgrPMQRQl!tg93l$**8y`EvD z;_50qeKQ(wH?!fn)9$1-?+Exkxr?v1XrcB;O=>N28Slt)lNS>hA8zA~%)V2BV7ZN% zmjsog&G>bcb80Shah?OUr_8$ZSzFAW!1$zO9Rb(H+i`UMAY82cj1rg4>G7&Q%v_=b zL%d#)lU_zVb5AhwLa%@{6Vu<+Wg94ot%9YKkK^0X92nE>iFYg7z*gZg#6;KN1U+%0 zEX4s6LzbY(WgSTWv>xoN<^Y=tAIwf<<}X`b(Hw1KG`p=%mxYHRZ|`}u^DKnNZfEg6 z6Q^-k*H_}EEeNAN&L`DsTfup*Ga1(s3mjwiL6}n<*08a|zNTc%w4X{^cdo~>adl+K zx&}P=fA<4;P<$DnhxYjA0l6L zIbl;NV(XJMKuL!C{X#J1pr`5yoawkg*Njyat}+H0{{dL4>MwUV?tN)wkFA1BY)2($!m zBzp{YVErkAMogTMjQDcAJy{;#u~m}h=)1U{@gUCr!`B@1JBgh8NC7A>7U5l4GWLm}}{to-0j zl@&D6Kg|gfUt5BS(Ky;VcMPN)*+Vxl`%LXsTS`l&%i#QnJIHtCWIPz|NMBTXLMuli zy%;+e-DSSQ(S0GfZ+R(-H*fMb$z=5Vd&nNu1dB|s!;X|@{IRYW>r<`C_3Q-v;+#qP z3qQkwr6q)l%Yuxs7y4;Xc=-M{-5JL0QLugvHn`t{n${R*pCk_KJbWCMM$N<-U&F8> z%p09NW`JqZWmxy3h040efQ+OP*)LNHhXY+{yH7Co+`U4(J3fQao(?KimxOLv?{Q~( z7leE{MieXJAezmK)}H%;3*&W|b$Z0Ml`=$NVl`SnT1>46XM*J8l|<|EH}vsV!6yC* zpmErdd~H65y}reCgXkG_opp$;ueuFf_wUmk1_zw*Rft<9 zBZ$G(r}%u#0F^bJhUa8UQFOTmqLo z(t&%l8R+y*AwK)eVP@=A3_ZCO2ScQ($>Gsh_#uk=#~L#>M5oAU&dIQ13qN%^37FS> z8}5(2hUq6NpmXmuXn($&s5fi|)w82Xp}Qi=tT7;7m0b9Wb226zDa0zHJJj&NIyfEF zLV6Aaqtrn@SiN-(-db-?u9rT6v&*9}R_88UR?H;>m6x&V%QmucqX=p(b*J93b0KN= z6kK3PVM~PQV!TFJ& zfn7fk=A15~CjK=*wspW5nE{C4F@hP4KhXOwKTPGw!#hp;(T%YowwzZ=l9e8TL2fry z6$pdgNLf;5uLuEKkJBj+RzvL2Cc+UOfr=6{U>2j7I? zZFXl#poRz7ZHa=j54d3F(=2#CE**m;TTwZy4s7S1!Ed}$kXw13uulR+_2cxAOb~7q z@us^aJz-Gc3-u2U#M1AnG=*6&cyhf54uomI=e>7{+f@V*h$Iq~{>U+VFB}~(#;~Gi zz-i@#+`Z`#+_oE1$xh05{{*(Rcaa)h5B%YsNWZ*$gHP8t0!Pjw@cpJqyjL@^5BrL! z<^&1cs@z5u-c~VoRvdJcr8Hz|WYI)}A`A(=1Rt}T;HvUrYPe`M#ukqvnh$5enCaP& zb}bJ47ngw~W2bf5a5aVC7`Q#Nn_3Rl;XKbD;Idm4i=`OdWAhZ8-6cd~{1x$JbU$!? zNJrnVqiIo}B<*i9H(}}M zM<7+htn*s$N7KVMXj9}a%>Md~*)w(n4sEWa)L$G9d9k6I@Jk4uFbx(r*ub@;%YmaJ z3G$2;>8XJgP-FZNW!}|8=kaj7G*=l7QhneyNMXQrN7Q-0l-VoIl;~e(hl?_egil-- z6-II5k^6!Weyf$f(ouj8l_XNLZ!R1@Hjf@`mO^<=F;qX+hYpOrc3bK;h;@=A{yDF} z|40?}WBlWm|9o&@Ho7J!&WE57M>0iM>& zcx-n8zMVUSlkH-_W~h$ndC4RD)fKq-S_$mB^_?^|*+CG`GkPKa0hV6lqp=^-V6_Aj zi*kDi9xZ4jGoCMknmYm5HeU_zde5aX852=a;}b1q_U0;RQ-jO1PGcTlHg#mP!mxI6 zI%hIBPI_=2eEVczS4!S!Y?ws^?Xm6twY>#asKd?Nq_TlbJXOdON>N{>ionmC%WZ9INi)`|Z8o2a|5gQuvCsJy&_sI9ZNsE4PstAprf zX9pL1SSlmGK#}=YQ2hH_<*#ogIaTIcd7(V$|4sG(rpz}(_5Y^&e^Wylc~u24lu=Mn z`s-VH0rRb>^4I^B73BYGJIy$Mp06s_CfhbF`2|aMWXZ2svMWn|!;-yN@;jF7&yp)x z@*bA_oFyM%$+ax`2ups=l8>=uCN%KhpBR>Wl69d^v*cnHKg*KOu=qumT*Bg4S#l{$ z{_Fhz`>FqVoqkKFpz@DPG-A31a(_rS(sds(G3!40t$x*i-XbH_&s_2m>Ni-q&RF)J z2S0M+5$k7eqW^ih%)K^3Vy4_bw1c@T|C{uts`D2pjL>@K9v?Xu^P2iA%m3g6j*w14 zQT3mt{;3HgpOulWi@dzz-^JQGyV-d9v!3xk_6LeT&&Tk@BVHHz5tIJsJdb$OjnopR z@R7@3FjDIOl>gf~9x?IA<*NR-<&My9rXw8T4F28hzt7|Fru@2$!%yKK+RgNzBNzRL z^h`hef0dr;u}4ln!u`ke`u|KjQo4mdAIM<`I8r**5$YZxor2uY4({K|8}Y=BRNlg$ zPv^gtrzrQculUz=$|Icf5$aW3@IN~arI8+-kzUOsTn-p1-AJ9$2B$GUq^P0%~lr&f7V?urud(+gtGEK?w|QwoCfaRqFz4E-Zr)_%m~BXQ&eY} z_PQkuSnK29XE`UGRtMs3GRsMgd*GPAnsPY1)M;<<0 z8u1+d)z&|x|Kp2do=~&%3sdniR*^v_h9i~^ECL4$Fg|kul5-Y z&tJ~szvhkkYlriG7O!UIhu^r=xS#23ezo6lc>Z7f*Syhs&2VnW%KvMg8fY*wfBEb9 zR}be0S^0m>LlajG=hs;L*St4Te`Nk^-m0ZHoY%AR|C%S)uNayCn&*lxAI|y4|CE2t ztF?4S=9gK=7pOg)Yq9cQVe$NBBlC0?hd=PEEN;DYcz!2V`PW!HamjEV!Q$6hyk2W$ zp26axf8aNN)vq}`e>SW9EEW&c7|!ci{1%HRs*lXGSv+5DWPaxtUp$I z-;RF;tNc$ap07N-eh(J!WO32ocpHnyu@*!Fm4@fHp72vX#p3mfBlCC`*ZPe|u=1Z~ z@kE8;`G4_*U**dW=lQJsXINbH4?OW#{c^+ee`e)RVsUHPk$LhjK7TkDo%mBe`-{tr z%+Im7=)B?Fkd;4$#p~ye%+Is9)|}xykd;4`#S>?b%rCHbzT|LziIso(ulgm1^Gp_B z!Q${6FK6+UEG{ZOJbx#P>#?|&*l^Ax@Ke_RL-}$nzUmM8O+Vx>a)T z9e5MyqAztuBicgd`V!ZfdydR@zao+qZ}{y<=Zv#_ZiAsEj~@vT!T;x$|I9c_Bz*TP zl~bIn=5b74u>8XD@oEzmJNs-en0VK5{yEIexM-sY5`*Aw}o%(5;=d)jR`7PC8EDoAO~?p^}U7!Zi|kzqkMQ z>x2KR&(P!lRdyVD*j8X@@##nK+xvOk(wiN-Unylvc>7Mh*5;o#cIptftwX2UwTK?x z**2?gm}v36ig+75|9kbv&gC|S@)6rNZF_9}`|%4|Mt&)RnE+tPS`%0a;fW5bp* z%c)V9yVLIb1=WsD3rUOA7 zY14}=w=OBV^lZOd@b-wcRsONxaz@Gcocb3Y`+NDr<-kqD<$p>`I+{(EZ4(JqySH%P z)3N!Tt5-kNs{;elPL&;|u&f!7!QU zq3+WjS>yii(Lc4c+h?kTxPh86cufoNWsux#V^&4JnK zQ#CU3J(?fs6dwp<>lpOb?|&h*ZTFAg>L*qnJlK6+ytyexa7yW6W^?)9KEEH|e}8)n z#rz9D{#)_Ci|2p;;WwFt9c$WF9~=K_cAeoS9ZhLpckqa&$?V3pKxZn!0+|D z*zkFr+*AQN`=@Z;vdND6tvvYs_yT`VFiajjOt&};{S-K??)AoAiVAI2&90v{c(qGw zgQnXSCC?vo*RJAef7P&f_Xv6Z>rbP`He5(-S-ZSC&rXnSoM1)#f#N+}@yU)8q@&Xq zcbf2`!2g<>d87>KVd2Oa8{JtP#$y{IOfZ-Q%E+0tcZsy0`0 z(~6$Bw*BZ?_tGt%?O!-_Dr-J5mbxg%t9RQ+Pdk5nT?bEG)Agi4IiF9_J9pmF%*w2O zu;ISyM|olO{p;R1zSpX}cYMZB^~S>I>)-$B__P@CSO0S9?dj~-?GKM=R=1=p z7XRQVkqgVrEUp~)+%Eo!$N%B(P2i#W-uUr5X2#e@DNCZUQ^_t%i=9Gt?HG)G-}j}m zw$M%#X%P{MNDFC`QrZ)VHc5qOQR08@%)QJ^sXjk_fB)C(cV6y&&Uwx`@8>zsdCs}l zy>rf0VGE2k&w+BTdgz^KJ~;RFp@i46iDL@#8?GkF7CYIm(lQyGV*UP@v*(5lW`C>p z6U=<+vFYs#u`gVlHwnE$fYjqocJ+n4wk3zNi^B^a z?$UDoa_FPubAQ$<;bQ4M-!j#nthF(Hu=`UPv7m9sLZ3|~U1wJ)3RUx~)=yKKnR^0z ztEY9R*rp48V&0ELK0bMtd{jo!@Ak0o+07+gS*Jz82Byk?Xn%m+l6AUfMbbjL$z-AU z5?*7GihGG{>z_QWO4+j^w+Qn5U-wUh;vuEYPTFbDl>-e+B+Rbty76@D;$eQ?W2(AM zZ?jy^$-iH}=9rY$L5>9j*EjELtcsr|P&n`7wKLrpOU%PZb~XK%vh8e3+_Q8ns~u`z zJ(XoY?~=g(MI!OU%kz&Hw7RYlQ(lm3=Cw2FeO^FSEC0nxZRZa|E$rLIwC6-TZ@ItK z@{ULJ&NH!&F}{|@I$jPQo7KeG&z<=y9^0X@-rhy|UH#j-qT3PTKR&X5fh3Pa&&r3c ztWBMhIPGM6$Fk*)CJt+xOdqGqxSY7tG*97Jzb$J-vnWyPG24#+RsSd=<7dkm(G7<* zd}WF2{o^cO>pq*Q5Xk=C*1mFj#S@|U>S+=s_X_1QMP>!27(;wYgm*UovAkux)q&E& z^T&iAOSc_0-cHNhkB?m*6B6F#$zOB=2(-ZGPGQ4kx)Eqqyi!-axv7AIv=__kB} zYPSy)(~l%Bx751di#vAhsrW5fX_Ri(tg_|rH;7TFj_o3}-#xk@mFLKkUgrlfm6|NO z+ph1@s!rbX&C+a|$ZXeskK2Z01xK|Kt1m9~PBlK%9XDVBwr^@08Ht5&Pn@Q= z0jn#$$Y$ohvY#yCfA<`WN$Te;s=F3NINZf_Qid~bAN@SU-Dk1m%)5x4?_@{rPp=p_ zwLs=C%Sje~B6n)^iau~Ub;r0Zg2cyaCiF4xZj5_E@t63TBgVI?Do2OJRk1cZ*7khI zlwJL9z9e>8CWe@(%66RB$AbMvTxW#GO+NoIm6e|*3xwZWyVPn$$(Ssua|>+ii#n^= z`+;r6s{HcsQoSSjS^Gu1g@tqK4IRtxUiyW8M^0x&BbC_kEXY*Ax`0@qOxzeBvg=Q=T+dG_+$dv?w;2r_!2jwpVqp zKVlZ%;b4n9-JK#jm-k5aHQutV+?vl>r_K1$VrTencH?oKos#k4G7bd&vG>ygH@q#n z77<=t;&ttwwc#Dl1>3s*Yx_w^{}+8<*>>o_V-KZ+aJ9yxm=4W^e*>7 zGqzJ_T?f*gQnz|lyfC;WHKwsgZ$Z-as@r&zd$!kk$5}>gW(@LLzGvOxKWFOOaqiHO!ku|&KcVCAZ}j&#@_K&JEuYzP zS=gf&Z8)t~2+n<6j+?u>>299BRKn*)w_oaNq!9QjiX=Q=ED=+?>(+KENW({mlRHzc zbbj2rAyqbslgDDOZaHA~LcmPa@5hJf^|7mp?Y{)o2l-20tvRrwxkOUU^gwpgq0jfi zX0SB>7yaSihr;*2mso~GcFt9AE<9wUxbx1|>Wo{)zFo(h_fEp!#rMGf+q(!4QIFw{ zN^P|Z4c|R%7s#YVM{4!F+OpyEB1PE+bGpwCEiIgH;PrWkoG8a0H{ag9L0l_yZdH@P zerQwmt5Y2@H!;`g@D&am*zUdd%>c{3%Ow|4`~FEkuk8XlVHKTSx!@*uG&$*FQ>V_W ztNUd8jKC2id-G6xkOI^!*a*qfYt3z&#sm3KJ}Py_e^Kv zFL+#0cHX`xY}Q6wy`OI$TO7r?&TcTFBD+F6N44*wW}ItnFTSLKGA0pG`Y>>FOpFk@ zKU;W{q<*-V_4Uru3B1+Y?_0|09B>+8NBxnRqW1fpeyTjt&kGWIL!MtC*FFhZzxU0T zOOWWQeM!6ZRHyS<6w9A~x4ZGNyThsr56bx8F7>i--nqp{sdvqn^XJ+w@w?wJj+(XY zlE$#Hu3PS27yc(MV*f?Iy|B}P0QWEassMRi0JGpgqMm84iA4W~i1tiU3KsLLzztT2 zjEDEbF$QgUlBC*^LI+eo*naS%Ui5d^Fo+I(`di%yDnC6N0d=FP2n;7oBTF$Ft_@yG z6>dq+IOfApgf4;VM+Cf|?8kx_7cI}6#JEL99_Bo}_tk@jfCyFrXvlQ=z`F()9=3>e zy*J@SGt}^-ZhM*UTkvh9Uj>aA`P=8)%!FvUdycd33d>_pgN1pcvYj>NR6QotNefOV)~{dWUbMf9e0PXl zT70xAK=|b+qVcN073nEwKOSAxw?(Y`N?#8}eU6V@c720!dD|nd*VUw!vNUXw|5nF> z2D^~;iwuPo=kGLXt$vx*eeUHP@6~gbWOzNjQGU)$VOLaa*RdSu^9CfI9Df zkYcxH{q+3s@O$yRIZ}R+`Ohou_Hu9KPF~w;9C0P=p#n#>Rc`a#S(#ohxS^5h0{4|U zG7aa4ajbgZtl0RJHD{5kp~k{PD;{uFEfeT!m?ppJj_Z)Y*DuD&I4_7d>3OJ4V*AY( z7ww|H@iz3=H*Bysx^il{rrEh~n=g8ccXy}t$XpQfXX*OR=j8}RFR3`+UhDke@kxA+ z^~${?!5ikU#^*X~8IEb_-+J>k=1$U@C&xtR5-e+oPb9#K1`L#GC0|q)RK9UpYx?^8 z*UD`cG+EukKEFfMj=B=+YcVQ^58H7$2_%Vq-x2B zdtQg7*v}MLwf3Nj{M7`&|5HHMS77Yj-MuKLq)-brzao-AtC4+-k-MG!z;63|BmOG) zyAnq_?d`wYX7U%g*&kUc8T7nGC}x@Z(mCCqPPY-(Tc6$)%eQ{ZVZ(&=n&(vQrP=nR zgv@i1>t;=m5K3JnwQUd=?%w^rbKpmD56^Yd{SZW9*ge410h41pZZ& zUI0;1X9w%ILB0ZVJl9sVm{=eY(j+k8tk6N*NM>iw!N?Hj&Njd(=VSL zx$Vf4qY)XR1(7R8J_WkfT-4EY61Ce9taAYpiY9Dw$a&uC)kbt&6hD9LeAJSf)1#Nx z#XpRfe)?H~+NbiH^T5~Mk8^V)@X z)Pn|MFc0ik&OKg|FcD#qU$_0Kz|xE=@)M72egl2H2+a;?`z1gUd==|=;}02{oY40C z?fu%0waZW59LinsRGe5Y8z`@LA#~n+9d`5D=BniHBq-3m@A|xxFK)%87mulS&CRt7 zz&^UB_-xspFgw9mWnbIJm;-uJt@HHu>nBPHOYe-13r`i}iTYYg=56Q6D8ivuP*p7|-| z1~u~YW4{>|M`x$=M`wsS?-pb1Z{`#_endeA*Z~(hK=3#B^mF$JC0m7diRL{GA13K0 zFjqp;@Swq9BvL}$yxhZl-NL>^ybe3inQ&EUf6K{=)vxd)vGXipjju}u_AZJf;Bv4 zehz&2Gm!bMbv%zoS0A!x8`N0X$p4r#a)``zlOdEWn|`}@A)XnKUW}qi&fxdmL zfHn7Mmcp~c==`u=+FeE_HUTO76QQK8P%|5 zy4!!!*ITkxCM7gt`pSrsut*%+#}7{ica%?m-*-w=L2*bqhU?JaoJ$)oix+R)Xx+F&umD{a72|_vO{6i}qdKaWTo*B&8jQSAG^~SoE&0V&0py{F8OJg(>9iPI0Ve*UlY2tp3fL8exZ6qJKzsC8s z*n(+Jst4wM+hm~mZDj7!J1!XqptK*~Umow;`E2*NX-#$D1QCxUocz1Vq z{EfiTS;Nw|H$|rP^=++O{n=$#D^a<0>C86^*PA`J=+HMv>qs%`7D&cevcLAzD3X3^ zyGB!hyl3V5tkv1-eqQ&?ItybSO^;^1K<{B>E>0nj>2;@N)0Z5gSBGAI*uU5U`xN>n zuCEt5Ti4A@psz3Blr0no2-S|T`;y~i_>o4kf1@!fkoqcBvw)G1*wTfvi znLQZwPCwQyoir{Bf-Eh!)Z|fQB|E(n8SZ51^@s1DSMW92${3*bw^NYWw`EfT9nM6Q z1taTb6YcJ9?c(L}iDKI&ts~>@dfBR2WtZCU3y(hr*SkHA% zX#Epojtj0cI<{R@IU(C~_R+{Yj?S@<&{qD2WcRGo&~X3e%42Vw2IaPNE?=1i`5(5) zl`nnzu(panmRzac)jF@j%YHt5joD!ny!YMV&ofqLKb*%Qbc=OVx|F6RYTub43ES85 z#z5=3qVXwcz)Ss5&BPd`)d6P=fpA3n>f$9*1@`#_&pcb>7js|QSMgxKunJ`LWWNB zv=83`zRin-(ODm65T|jB?ctmBKWCaQ%=+6Tn&G*}{bLo^;pcfjeyKl(?bLzr%N1-F zS`**%r14CPv6|f<#Ohwst5LSXWQ$IS%Ge2u*@^rb&q$D@wBL#L zoH_O-(YWG-bsiFuXZbaE3!1@%iC2ZC;F?K^56-A7BH`g zjT>fPGe1#w5oP_FqWYVFY}kM1=CAsq#iMfE#G6RQ@m|r2=ab<5h5o+=`z;qI_1o;f z&&03{@x+OXecat47bs9Y_)uY;*WR%kTlxl?y^q~mzE7}p)7bIz+rF(-OgyESW@xZ+30;^pjuqOe#$)g6|*?VY?3Hu__* z-V>J`-)KpLZtS_gk2wES*;#Yla^^T?IHVE!f`pUcX zWl3(qUTFG*`%aPxCYVjL591{uctU3GZn@WZ@FvBm6Wrp@Mh&SIQ@Ba5m_ z0`&}^5U&_d|4jq~i5+QZ{E*kX&U9a>cC@i~{6oD31A|Z9jmh7#AC|~879HGsGH8Q> z&x0ttVhhU?O3{3_j+pf(PA#cVPc8H2a&irIb~2dVv3ww0#JqgY=vviRd$H|OYm(Ea zC0N;GPaJoOTw!}Qa!(;duDE^N`_`n*5VgwXoyp@)S#K!*=Xr<58Fk!v^8Q*Ie1m&rBVYeB!wZSi!537RLtm!m z9;cC=>6!d%adTt(N>5S$AwvBNZvWrSzkk)%-oA4@6P*b~g2dNf7m*vUK6m(3b(M$e_#2`Q?zhbgh)1{T=nv|(oRDyDU-@s^wn z9Z{OQ{%SSFroV~A2)ib7^lHrMo;kBcjzKRY82%q%EJ(Y)!p}e9_QC!g@*uB2-dlGu z&p}gFQ&K@2F2@pGg2_-7wgol(zReiO%{8vWM`eEe7IulVtHiWbv`Ia;^b$v>ImTvW zmO|gIKf6(~k?*XS=D^ud$3DtA%{iOZzkclv4*X%a83TE-9Tvi>u|~W~lD$xu&e`|X zy|76=&_53ImDid{Fkeqdw?ew7I95a&d%N=ysvdLy{c3*v%eJ79zor;WkaBI3R$TC` z0lAL~Lo;@}92RQu{={Ln->#9-nUyVY&_`S@vui6ZX zf~T7pY3+H)}%8(uwDGESX=8A?3oNc_4yq}1n`f5%WA zInvoC=yClO@WH$n*&j0(PQN+$_JG!`JMrbGNWxxUZVBV*^@pwvE)UNz(7L&Ur*_$d z{%{Qa|Mgebg6p)RjAslBhm#PY=DJPOjKp4ED}{)V<;wUU*#6#w&dCK2;_|bndEC!7 zTKGnFi8}OP&N-jJe0Gb1<_Crqv)6G&21^c%J1xYrV>G;*Yx-dcGZjV=a0>PR-|9y= zojSj`@0i7I+p$fu=Tn~>-Hn+0oWMtxZ*H&TIKIpKiCok6FIvN^%386Wd-v=;uAdg3 zX78S6A0H7F5q8uT(syXn>nl5%wcl!Vd>688|f}ECdH@I8gdJHWpe%x_4~Qn zBzJ%qmQlWXo9J`XN{R`Oc6O}rby&CS!yA?~0JKz-J^}9q$3a;z zYz3V+xwF3>=^N*bN6_RByT%vyV|wDvwa#MX#aYVCc&)aMv(?5ksGLXVn8>U13m z4FZ(lleijbFQY5=D@%gSUSwc0#CBlq+aG@%wp1#m$V==NKIL|d)ui7yIa23{y6*w$ z^*z0kL{vYFr)_}>E~hACTqTAGvwq`ahpOlkSik2#=|?yy%wFhiFJ_>dF*vqGDxii% zpx&N0)zsz8)oXJ65ySU%p~TwZInT0GE)#PF`Se{jW)rsf#U0uL%?WPgGZkJj++~UC z$D$;$TWgnVl{tf2;|WwhBH&Qxr=OoMtY;YC;GMKbwqV=4xHa4lpVb%JU4wS_6urxl zi@sl{-nm-P0&^tFD_XkA;gdph4ZrbT=v3+T6?aD6%~)UWuB{b8Mk|IbB1dvh+0KdU z+qnF0iQ_ZS?z11Q%eb+uh*`d3sub7J?rWp2xo=jHzCdp-yu9~ZnqVNfxwGe*1hhYx z?5{R-dn)5{>bVOEBCUVt>O?Q+W?g=KNvqkI8cj=n1{3m`NkN*CL`Br4yP{5;^ z@>qm_7$Bre>k+H9NwM}Qlk0n>avz4Lj;%H?&x znBP$S;QswdzaQNw#xE(8HWL!5rBMwq@7~(Ydc1IwUyPvgNuUl4@RNBa7@$8#6sXYu z=O;qs1-H22_Om5+`OdpF=|h((>koe);hDyE z?1w<5$#(IoU00V`HMD*3>&~C-O9uuT z_sW;K@?WR%&_TnsD(3y!&lE>w4|*PU?|B&zT11Sa@SLtbybg+E%X=SRtE#u5>HEVk zFND6-m!Dt#yvel9&X;_IvDYx|Z|=S$Z))l<3JlAu_%bp6(BFsAsfK_*VwW_p& z*fxs+p8AzNC#1_`SrV#;R7H;5sd(Gz6Pu%;;G1hNTewNrrDf)YWL2FF#i^|h3j0xE zfR0Lk91$RT8hM3u{MZWH7ry^S$Jd#h?DFhW5}h*ahtXNeirJ}v!X?4KVg^8!U8?uK zukPJ^e5K4XoSXTYPuSF2$hy5;RZeSK+#HJmVZ|>(`;`oC`9h;*qaR-=ecaO-_>Frw zYyN2A{iO@&Y^f&NoqSAi@Zdr3-&=-Re_osBm-ELbSR3uZuPTPjs*c42EBuN%0?pog zVZ`=))MiS_*Sy60_e-x(z8ONx_f&ao`xGo|>0c1j+*Vpq3c zpN^BTS{QZZyYhton4!jDnG^S#Ld949S%0_$YJX<6Xtw@6#zZaw(l*nW|03$(j>DJ( zVT$TQpPrh>orQL88CFxuXn*hBup!w-K;=^ZB8v}ny4HN-e|CId6aVQ`6G&(bwJ-dt zm3qk?zras_+^?!M@>)9;d{uPp2RO#VL#$BNol|E=>sH^)sT)4g!ZDnJa$+qxAFCN<-+FPi#jr=w|k|TYkdf09Zhh&WNNS!AjN~@0wfv4%2&nk zJ`dFkrS$qyo5aICt|3>wmO*ffdL)2i(2prS@+LI$B5&l-;>VKNi{^iDxW_-|i}mKT z9CJ@5 zv-04yaSd(w>m4}nx5dn@bAI9AmJoKKl(d;F(_n@lOy?;6ypE@TzE#IS;`*xhAtRT} zk{+>}9^$Bav!k-hb$`o_Wc|`+qfe;|78-X9`tA1HtNx6m>~YPb)iDS0q!v?DA$s{# z`+VYL*)}d;4i^-$w1QDSLn@VW;A<^TlmA0U+O&Xq;_N0tn%Qd{y1ng&8~U@3<-RIQ zk*qIR!bAKL3L*8vWS^B#$XA%cqUbOkhv=_YM;t z2@)`mKKv$sWX)Bz$HiJZyHE9=w+qWkK1UH@Ex&0~{#pChq9rnSolLz2?tJW;uV?)A z-0N2^hjl)NV`CHLdsXs>4r&@c&T80W@j~zU=&bmhZQZ@gV~7X%=iO3S>8<2&>&9sv zkA-zspI|+J{BfPmz5Z5OC~#oY;F+d|5f>>dIB&9m+8=(t0XHxI$g7jzgiT^fN(apAE=mz#-q2(biDGbZ~tQ3LP z+L_0Fm$-YJ*d};($L-tDx?TGUuC|)kw7hmY`_;DMx}{^!8h*m-v!pT^Xg^}c>hF$YHh~M@sYCo3fb5o_pb>917 z{H1%VRxVyD^ZmT`qBF&1vAu6z_He8};iqHUWW$+nWP8d(@Rqo@=E#j`mVpyYS{-oM zm$yDM@cSKW>D0{qoXqV*?@um!dKjp6oQk-7dR@_K(YY&K@cR~wF$Qo+`ei`*BjVE+ zH)yWGHx=qFK5_H9u~XrFXDROop8TXByyHW4hbh`WsQyYt3!by&-Qms@ zp0*^Lz}tU!*45MWI3SMd$;2M#kK7c0`0UI&Z>g8L+%B$%UuAJ=Y8CK%wAy?qsO|1r z7NPgCBi!?L&-JwWpi)nN6YO$I$Q=>S)9<3(=he(*Tm123`KH*OfRi0F=>&{ARuoFM zC+H@dJbtS5EB*BV6P_Ph4x~bIvukYj+;kVvRNhpa|HQSH`5!Yn+`o)Y8DRvn+P&lq zdE4xLb9U?#%Ril@^P%8L(Aw-%GY4xb)3$FLVDGXQ(zJRPw=0mls43?au}`}mw=UhWa(=9EYt|=&Wh-+G zJOs+l3&<>So4@x%P`1O6w9XaFGnl2N1hU$qfaHRp2A$LkyTbDBxLN+u4&SVjQ#RHq z?D3Aji81-Yexyaa>UvFDuJMVp{?($}HC%uCH3sT`ib($*QrqEmNT?u61WNOA4?esh zs<|Zi51mByN7N_&P!`T+E|^NL(B+V)>j)V}ERi}Qi1 z#@P?9jkFt9{*8JHuJ2dwmc5rq zdfO!iJ_g1Z=-WV_gYrjQ#qdI}aud3Vg+0fEBo&v9)^xnPXmqARaO*Lia>dNgF?FO~ z{e!8{&=|*$4P(612}1LBk2OAtaN(Wy-dnQyLGW<+LD5K)Y4qUP9beUFcweumyerEv zf6?~Gr|qv#-!3$cl3P||HAeVs;I5UPme>$KANmUoc+8%X(P8ck@UQRo>$ z8Pyw4={!g3y`fRw_XtJgS)%E78eg%WMs)^g)bcTn1|T#6q2mIONI%kEof=wsG(R6r zpF!{t8i2G5Eyqj|`}=tY|K9#2q#gMPg%JDIBXT-XdH;{wKjAFRzE0;D?aypiH2s(M zpBNBmMT8>#6YLw{gNzu)0_gadw0{sk%5SCV`~Uj*fRORtdFx;G5BS0f#fAA3C}8h! zlV4816c?mCGUb7EFc|;@JNgF)!(E7eM1{Kh_|Fg%<%2!l{eJH9iTto2H;}DsW(HZ? z=t5vCa|l)nKEX*i0$J;>Fo5)zS(+GH+du}(j16sUZ6Hf<<)EG%^{rQ1+FH=!x;876 zAn;upPd|zRAdYTics>XY30f@0aDPEp#M967=Q)Eee`3<7b6JLb>YU^0N-u&gKj2qa zMI=AO*^wOV=S#gP(xHRv39Shr-^DY)gA&ASL3q5-hl%p8p6*~9@IRGjuK%a>1b%=w zauKH&&-l;=-m62A;mZpC82q-qW5G zL~#oZrno|(e%^lm;eHV46j>=(Imj*4&xINRfWiKL)F^=Q57LrS)bLrQB^6Bj1HYKp zg?!0AKK?EwH?pS>fC3H?;_pxLCHqB^s8tvogrRj1RIf+7AIR55?iY+`)X9)WNd`0u zA>}%k)6)G&emRogzl_G$L2!a2gF`63B+wj4Zv~ZrQ9z|&V8UEOdrMlboe`B<=q;yG zOG9%i1*3#YSE+$dS-=(G5kQUTTdbf}p^XnJ&rqLA;IAK1MJ}_TQcF{Pn}yUE3}de{ z&IG%z!{y)sNR1fS&?*?NFkZI8kjk>qx1nihYeP-@0q>)@l7L$)(aZ`tlaZvqvlr-2 zk_YH}N)VL|djPd0lH~$=2%p%gzC3PdeS`|cFU*E86c9OB{u zfk-g5G*zU3Fa!oY*@wQjLxU(tf5HM_?MSfWP(3TsKa>>g;UDVbO7a7rR-kGSKn@1Z z21@yb`uLCn$U$V_^FU?byObb5vJcWQfcZ0ChzB`@6iyB%xq1e}pJD(o0M*|&063j| z2>2`%{-MB{lwc?*m^_g%@N@&;r;<0J z`1pV!Fu@1&2l%)IXrf;j#RrI+$nmB4`hzdtIET8qfsb8{qat1B=}Pen@dS``S+J9i zmjf4&i9`hKt}c`yxP}WkgiZ?0ar|8TUEw|*uMN!Q=II_9M5cC{8!#q)o-erZhOOX2 z{m8*Y4rrWbK!~Rc^4W=TG976UDnBdW7)`>KAJdMK}fzO0Y#x#3D@MI_D6N*lUt{?)hlAH(zhBm9@_4V{2I0Cs+f+OG<%7Gcw zw4s5a3d&T0xhl{y14CUZ3ywg*3&@bQzA}{qC=}$P4CxtBlX^ywvm4~>;~(zk8SDY+ z>)P0=Q8VBe7)ik)RGz$xvkNsvjpza`l$1575wy5o>20|s;f0}%KN8IGfT`1@;!jaI+ zv>+U}F;}Gm0SX2CszT-lY7n@UQkM61rAAb!5gQX-C77)uAM8O^f^UiBsVp=iuM9j3 z;h_|NA{68qK=E~jf_z=!6hokd1t`$pDZpupBd`*< zLfc~p$6Q$_KZfvwV;bO?tjsQucNkz?28SGOqTGEX{*q;IYy;2W*g@jtMBjpM=gt@Zsmr z{|o#X2tM!s2%p5o3qwOXJ7{*uN9~b7OV@8W}kpW8m*6~(PNX?zdnkVkJ=rjli{~)`ZxF+5dG2d`>*w%4F6y0 zACKsdj^BT+|77_8O8;~^|AE_p*P-=%OE9f}6`A~kZIbyP^%rJ38GdKzzroJ{{tT-` z0tc)=RtKCwK=tp8Wc2I*s{ds8sQy#M-|`3jGb5&^|77^6{!_&-1;%Hvza^vnb*8BQ zWcaB5Q^hY~(jUj9KQu-CC&NeepDKPa&>!}D63y&j!*)jdX9i5oe{`tr}gqRQdulQ+n*DqNAe?5OrhL7q$)%kfFGXKEWUs!*v z4mg4EujbFm@KODzI)85dgZ@*U-zLLH^`9z!5%7QTN8R6&YJVpGpX&TJ89u82RPm27 z>Hn|i&&lvn{iiyA9tHZtc8C2R4*z=ooD3h;f2#9mA)WqkJO1_jIT=2xfBqEb&s4hm zXITG#J%3JykLo|w`Ex5W|G>J#2UZ81K=@bl=VbV({!^Vl)Bm9VROh$J@KODziVv^< z0e@86Q|)g_HUGH%r#in)hL7q$Rs6$D`v2?sb25BX|EbQO1wen)_hHNb>-lpsd{qCb z&Yy?q^oQH=ujkLn@KOEqr#OGE13JUcyEJfWEJ{@es9m^nh@2YhGD zr=d)o9Y7QvU$7kb850~({PH5k`u`PwJ;0*JM}HWP;`2@se=WdA?E_Qv?Hh_uLhJ;x zsNuirPkVoZzHegw^#=s~K;~BxK!;5aE_c+>Sw^#43qlb#BnTS=gum^d5$5|}T6GA; zub(3RcjoH@6FzB*_}~6uAKoeA5C6eF&=m2%{=q($Cm6@af9>BPfDii(39&ok9^?J1 zGevyZAz@4yAC`kI>qq^kymV^%fB8fI)|X5Te}K+Du>K%P4XFOm6!8ZU{PF(}nD7;+ zX#X+9KIs47%=XWpqWwQJ;WN)a*)$z8R?QuuKe-{^b{^hA$6e(#gOv;dj-?$fAY>gfh=BllAXTq<`V-E4}~JU!mD0p^3>p$OVQOKmP_}IYbR;#~<)B zuQS$5_>cHvh~DG(ABcTW{duQ|57vcJ{Rg!V>ObT7jZ@Pf-be^;GK!D-4~h?6n;QOn zI{VQ34~k!Zd20A11RuVBzyTesR7!w@m^$E5AIQ|svzlaGRwLgkKRr}8a z_^{nk{n7D@;!oB7wCj_*e{hJhhLY++4b6#56OanNIm2{)G(g6eQwyzsNOu_fe?HQY zoi}Ox{#!JfiBKvi-ASGaU!Ms-0g;1N`LFoJ03Ws=2?5Y$!tcL5HT@+4F4{k^2m~JL zzfZ$WKqw*}dW=Mv=RY)`nJxef7!&P(98v(qFTcmA|9{1oV8RFHq6QS7cZ&Gp01dSd zOp)nz+&-3fr=~xwENXK&j)wXNG~5=1qWu3#Y2yR@*qdowpg7JT0=El~(B()5=`l^d z6A> ziavo4O%Y#)3Ez|n-wEjlgbn>y`%8g}u-(z|_pj|w2H3EDVE=&wEDH{uZ8W>rBb3Gk z`RD|IGX6keT-5$>(SMD951GGV`@jd<4(J6<|CX1G{*wU8{Fl%S@KJ2I9tKXp0l`mr zJ~jN?O#0)P@LLc$G@%fbfJXk3wlLvinea(d#J@#n9~hd28UNMP>|+TqVV^e_g+#u7zG5C07HaOOT9_ABkBEZTH zl4lqxm=Y2ifG&W9d0=fS6BAxU2e0{qSM7ndIxgNM+Il-G!<9l^VMUF@RnaUz63Fp) zrJyVR{73k>|ko98l%07lNu3(8EwWe>VPlzX|O!9YQY%W8C zT)|>FN)W)ODjX6S0M6N zHv6ZN6q=B6?Woq9=n7anPqL3^6m5YXt=~QU0zyM*9fW3s4)XL10|i~d0!etK@_2F_ zd7MM%VQ8K4E`@+M!~Wz$4sxeJpd?s&M|Es?t*0*~IGF5CK_rkogQ*KS0UO?Dgh_F9 z^(b8@`uQU!hld6PQmzy?GVl{rW0*tLeLNpnk+D9kI#PLDWt0K0h@V)0iY{yfh5@)? zlps&H$Y17C{dxincIk2RlVEoX@c<14f?@ZB7mC6Q5&eF$K3EJ2cM9kMaxgq8z+D6d zc)J8^kdVdIBU#TTz{~F)8B7A7akF0A9w#C&kzq^zy(GCi!8x9TVxF;7|389q@Qar zEf2|Ls)2CF1y5KK=#9*3u)(SDNMV`@sP=%Dq@yc{e<~B~8AV}W1_ZfLJC6yE-h#hY zFPIFRj@k|3LF54Hq(HSPypBB_I81nuKUgE{<_RxJW|*sK8FW2C1)(hjcSKh)qdLJ9 zy`F%XpL#t()5zupwyP}->9;qauS$*Ufirx-d&t7qzD!)07a&jqLIV&A^XNnUqZn#A z#m6)n0y1HoKyVl#1$6-RK;n}TxE@>wmEVHMt4C=0|5$!LxOs#RR6Z}B)_w?~{gcS= z=cU~jv`nMXEchmrDd>RopW3eg?r-3L-u7l9auX05fa*gl_Mgu1eT^9}t%~9#IC6-5 z5-0}i(1Mh!M`-yZ^=PQSKa)UqE+T)jk{DpOE#N?l@viT@7~$Z6{T~jN1e*PI5UPk!TJFU8Z`=PT|1SWO&H-@f0BHEPLA-1<`TdC8&MC-G z0C=6?_+2;%WbX%uA}9pQhyE|i&j+$|!9hagw;=5;M`->O+OGpJBf#;ya0tlG0|&f6 zAKd=>|I6}qfJ|L*!2Soz&qU-WAT(f#?f;Yi!PiS)M{s8cZs4dFP@n^SIDyQMI)Dk! z6*}BB|5QXM38D0L{x+@y3i5)v?{}e;X@2HKX4&eVuej|{79ULT3 zmfAmv+ysOMp!$G(`tY~&LkNAPU&JNp>%;p`0)QXj&9W5wkFcF z-M1#vG#!vz>~SSE5c@#qY5Odq?xczE6V5U6wb1fRd~LM+S;q1eXn7|7LbUv$1nPAd zRY(mje}Qq|M|HG3q9yEo!{Ue>tn6HzENnb1cvf}{7ZJw^adTh^cy1yqI|~;FCxH!v!}34`BA$Z{!^#b@ zV|h4nEL>b{EEsMak(B^(;ITaHoSaxZ8w-(xn+HR{aj~;PtZXb;b`Crjh6hLB2{^!JA@XpsVmP_kv1}adtZdvoEI=xr z6NljfGC488COB9g2Sns%;|9v&F)Zv@E*@4Q2Vg@OJU0g$Cks1)2egij1rOoaiNJDP z+^jqpPVl8I9S+OiQITDJZKv* zKhTRCSb`1Yv9RK}c{sT+1R@8-j%Oio5!vw^oERKvGaC=+Ac&ij6~lt%0A0qz&c?$9 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 +# +namespace eval tls { + variable logcmd tclLog + variable debug 0 + + # Default flags passed to tls::import + variable defaults {} + + # Maps UID to Server Socket + variable srvmap + variable srvuid 0 + + # Over-ride this if you are using a different socket command + variable socketCmd + if {![info exists socketCmd]} { + set socketCmd [info command ::socket] + } + + # This is the possible arguments to tls::socket and tls::init + # The format of this is a list of lists + ## Each inner list contains the following elements + ### Server (matched against "string match" for 0/1) + ### Option name + ### Variable to add the option to: + #### sopts: [socket] option + #### iopts: [tls::import] option + ### How many arguments the following the option to consume + variable socketOptionRules { + {0 -async sopts 0} + {* -myaddr sopts 1} + {0 -myport sopts 1} + {* -type sopts 1} + {* -cadir iopts 1} + {* -cafile iopts 1} + {* -cert iopts 1} + {* -certfile iopts 1} + {* -cipher iopts 1} + {* -command iopts 1} + {* -dhparams iopts 1} + {* -key iopts 1} + {* -keyfile iopts 1} + {* -password iopts 1} + {* -request iopts 1} + {* -require iopts 1} + {* -autoservername discardOpts 1} + {* -servername iopts 1} + {* -ssl2 iopts 1} + {* -ssl3 iopts 1} + {* -tls1 iopts 1} + {* -tls1.1 iopts 1} + {* -tls1.2 iopts 1} + {* -tls1.3 iopts 1} + } + + # tls::socket and tls::init options as a humane readable string + variable socketOptionsNoServer + variable socketOptionsServer + + # Internal [switch] body to validate options + variable socketOptionsSwitchBody +} + +proc tls::_initsocketoptions {} { + variable socketOptionRules + variable socketOptionsNoServer + variable socketOptionsServer + variable socketOptionsSwitchBody + + # Do not re-run if we have already been initialized + if {[info exists socketOptionsSwitchBody]} { + return + } + + # Create several structures from our list of options + ## 1. options: a text representation of the valid options for the current + ## server type + ## 2. argSwitchBody: Switch body for processing arguments + set options(0) [list] + set options(1) [list] + set argSwitchBody [list] + foreach optionRule $socketOptionRules { + set ruleServer [lindex $optionRule 0] + set ruleOption [lindex $optionRule 1] + set ruleVarToUpdate [lindex $optionRule 2] + set ruleVarArgsToConsume [lindex $optionRule 3] + + foreach server [list 0 1] { + if {![string match $ruleServer $server]} { + continue + } + + lappend options($server) $ruleOption + } + + switch -- $ruleVarArgsToConsume { + 0 { + set argToExecute { + lappend @VAR@ $arg + set argsArray($arg) true + } + } + 1 { + set argToExecute { + incr idx + if {$idx >= [llength $args]} { + return -code error "\"$arg\" option must be followed by value" + } + set argValue [lindex $args $idx] + lappend @VAR@ $arg $argValue + set argsArray($arg) $argValue + } + } + default { + return -code error "Internal argument construction error" + } + } + + lappend argSwitchBody $ruleServer,$ruleOption [string map [list @VAR@ $ruleVarToUpdate] $argToExecute] + } + + # Add in the final options + lappend argSwitchBody {*,-*} {return -code error "bad option \"$arg\": must be one of $options"} + lappend argSwitchBody default break + + # Set the final variables + set socketOptionsNoServer [join $options(0) {, }] + set socketOptionsServer [join $options(1) {, }] + set socketOptionsSwitchBody $argSwitchBody +} + +proc tls::initlib {dir dll} { + # Package index cd's into the package directory for loading. + # Irrelevant to unixoids, but for Windows this enables the OS to find + # the dependent DLL's in the CWD, where they may be. + set cwd [pwd] + catch {cd $dir} + if {[string equal $::tcl_platform(platform) "windows"] && + ![string equal [lindex [file system $dir] 0] "native"]} { + # If it is a wrapped executable running on windows, the openssl + # dlls must be copied out of the virtual filesystem to the disk + # where Windows will find them when resolving the dependency in + # the tls dll. We choose to make them siblings of the executable. + package require starkit + set dst [file nativename [file dirname $starkit::topdir]] + foreach sdll [glob -nocomplain -directory $dir -tails *eay32.dll] { + catch {file delete -force $dst/$sdll} + catch {file copy -force $dir/$sdll $dst/$sdll} + } + } + set res [catch {uplevel #0 [list load [file join [pwd] $dll]]} err] + catch {cd $cwd} + if {$res} { + namespace eval [namespace parent] {namespace delete tls} + return -code $res $err + } + rename tls::initlib {} +} + + +# +# Backwards compatibility, also used to set the default +# context options +# +proc tls::init {args} { + variable defaults + variable socketOptionsNoServer + variable socketOptionsServer + variable socketOptionsSwitchBody + + tls::_initsocketoptions + + # Technically a third option should be used here: Options that are valid + # only a both servers and non-servers + set server -1 + set options $socketOptionsServer + + # Validate arguments passed + set initialArgs $args + set argc [llength $args] + + array set argsArray [list] + for {set idx 0} {$idx < $argc} {incr idx} { + set arg [lindex $args $idx] + switch -glob -- $server,$arg $socketOptionsSwitchBody + } + + set defaults $initialArgs +} +# +# Helper function - behaves exactly as the native socket command. +# +proc tls::socket {args} { + variable socketCmd + variable defaults + variable socketOptionsNoServer + variable socketOptionsServer + variable socketOptionsSwitchBody + + tls::_initsocketoptions + + set idx [lsearch $args -server] + if {$idx != -1} { + set server 1 + set callback [lindex $args [expr {$idx+1}]] + set args [lreplace $args $idx [expr {$idx+1}]] + + set usage "wrong # args: should be \"tls::socket -server command ?options? port\"" + set options $socketOptionsServer + } else { + set server 0 + + set usage "wrong # args: should be \"tls::socket ?options? host port\"" + set options $socketOptionsNoServer + } + + # Combine defaults with current options + set args [concat $defaults $args] + + set argc [llength $args] + set sopts {} + set iopts [list -server $server] + + array set argsArray [list] + for {set idx 0} {$idx < $argc} {incr idx} { + set arg [lindex $args $idx] + switch -glob -- $server,$arg $socketOptionsSwitchBody + } + + if {$server} { + if {($idx + 1) != $argc} { + return -code error $usage + } + set uid [incr ::tls::srvuid] + + set port [lindex $args [expr {$argc-1}]] + lappend sopts $port + #set sopts [linsert $sopts 0 -server $callback] + set sopts [linsert $sopts 0 -server [list tls::_accept $iopts $callback]] + #set sopts [linsert $sopts 0 -server [list tls::_accept $uid $callback]] + } else { + if {($idx + 2) != $argc} { + return -code error $usage + } + + set host [lindex $args [expr {$argc-2}]] + set port [lindex $args [expr {$argc-1}]] + + # If an "-autoservername" option is found, honor it + if {[info exists argsArray(-autoservername)] && $argsArray(-autoservername)} { + if {![info exists argsArray(-servername)]} { + set argsArray(-servername) $host + lappend iopts -servername $host + } + } + + lappend sopts $host $port + } + # + # Create TCP/IP socket + # + set chan [eval $socketCmd $sopts] + if {!$server && [catch { + # + # Push SSL layer onto socket + # + eval [list tls::import] $chan $iopts + } err]} { + set info ${::errorInfo} + catch {close $chan} + return -code error -errorinfo $info $err + } + return $chan +} + +# tls::_accept -- +# +# This is the actual accept that TLS sockets use, which then calls +# the callback registered by tls::socket. +# +# Arguments: +# iopts tls::import opts +# callback server callback to invoke +# chan socket channel to accept/deny +# ipaddr calling IP address +# port calling port +# +# Results: +# Returns an error if the callback throws one. +# +proc tls::_accept { iopts callback chan ipaddr port } { + log 2 [list tls::_accept $iopts $callback $chan $ipaddr $port] + + set chan [eval [list tls::import $chan] $iopts] + + lappend callback $chan $ipaddr $port + if {[catch { + uplevel #0 $callback + } err]} { + log 1 "tls::_accept error: ${::errorInfo}" + close $chan + error $err $::errorInfo $::errorCode + } else { + log 2 "tls::_accept - called \"$callback\" succeeded" + } +} +# +# Sample callback for hooking: - +# +# error +# verify +# info +# +proc tls::callback {option args} { + variable debug + + #log 2 [concat $option $args] + + switch -- $option { + "error" { + foreach {chan msg} $args break + + log 0 "TLS/$chan: error: $msg" + } + "verify" { + # poor man's lassign + foreach {chan depth cert rc err} $args break + + array set c $cert + + if {$rc != "1"} { + log 1 "TLS/$chan: verify/$depth: Bad Cert: $err (rc = $rc)" + } else { + log 2 "TLS/$chan: verify/$depth: $c(subject)" + } + if {$debug > 0} { + return 1; # FORCE OK + } else { + return $rc + } + } + "info" { + # poor man's lassign + foreach {chan major minor state msg} $args break + + if {$msg != ""} { + append state ": $msg" + } + # For tracing + upvar #0 tls::$chan cb + set cb($major) $minor + + log 2 "TLS/$chan: $major/$minor: $state" + } + default { + return -code error "bad option \"$option\":\ + must be one of error, info, or verify" + } + } +} + +proc tls::xhandshake {chan} { + upvar #0 tls::$chan cb + + if {[info exists cb(handshake)] && \ + $cb(handshake) == "done"} { + return 1 + } + while {1} { + vwait tls::${chan}(handshake) + if {![info exists cb(handshake)]} { + return 0 + } + if {$cb(handshake) == "done"} { + return 1 + } + } +} + +proc tls::password {} { + log 0 "TLS/Password: did you forget to set your passwd!" + # Return the worlds best kept secret password. + return "secret" +} + +proc tls::log {level msg} { + variable debug + variable logcmd + + if {$level > $debug || $logcmd == ""} { + return + } + set cmd $logcmd + lappend cmd $msg + uplevel #0 $cmd +} + diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/LICENSE b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/LICENSE new file mode 100644 index 00000000..a0dfecff --- /dev/null +++ b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2019, apnadkarni +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. diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/README.md b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/README.md new file mode 100644 index 00000000..b07b558b --- /dev/null +++ b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/README.md @@ -0,0 +1,28 @@ +# Packages iocp, iocp\_inet, iocp\_bt + +Tcl extension implementing TCP and Bluetooth channels based on +I/O completion ports. The iocp_inet package is API-compatible +with Tcl sockets while offering much higher performance. + +Requires Tcl 8.6 or Tcl 9 and Windows 7 or later. + +Binary downloads are at https://sourceforge.net/projects/magicsplat/files/iocp/. +The single package supports all combinations of Tcl 8.6/9 and +32/64-bit Intel platforms. + +Documentation at https://iocp.magicsplat.com. + +## Changes in 2.0.2 + +- Support for Tcl 9 +- Added `-authenticate` option to `::iocp::bt::socket` + +No functional or API changes. + +## Changes in 1.1.0 + +- enable lazy loading of Bluetooth so TCP sockets can still be used on servers +without Bluetooth libraries installed +- fixed crash in Bluetooth asynchronous connect +- fixed reversed sense of -nagle option to TCP sockets + diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/bt.tcl b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/bt.tcl new file mode 100644 index 00000000..3ec58d30 --- /dev/null +++ b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/bt.tcl @@ -0,0 +1,697 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +package require iocp +iocp::bt_init + +namespace eval iocp::bt { + variable script_dir [file dirname [info script]] + variable version + + namespace eval radio { + namespace path [namespace parent] + namespace export configure devices info + namespace ensemble create + } + namespace eval device { + namespace path [namespace parent] + namespace export address addresses port print printn remove service_references services + namespace ensemble create + } +} + +set iocp::bt::version [package require iocp] +source [file join $iocp::bt::script_dir btsdr.tcl] +source [file join $iocp::bt::script_dir btnames.tcl] + +proc iocp::bt::radios {{detailed false}} { + # Enumerate Bluetooth radios on the local system. + # detailed - If true, detailed information about each radio is returned. + # If false (default), only the radio addresses are returned. + # + # When $detailed is passed as a boolean false value, a list of radio + # addresses is returned. + # + # When $detailed is passed as a boolean true value, + # each element of the returned list contains the following keys: + # Address - The Bluetooth address of the radio. + # Name - Name assigned to the local system as advertised by the radio. + # Class - Device class as a numeric value. + # DeviceClasses - Human readable list of general device class categories + # Subversion - Integer value whose interpretation is manufacturer-specific. + # Manufacturer - Integer identifier assigned to the manufacturer. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # + # Returns a list of radio addresses or radio information elements. + + set pair [FindFirstRadio] + if {[llength $pair] == 0} { + return {} + } + lassign $pair finder hradio + set radios {} + try { + while {1} { + set radio [GetRadioInfo $hradio] + if {$detailed} { + lappend radios [dict merge $radio [DeviceClass [dict get $radio Class]]] + } else { + lappend radios [dict get $radio Address] + } + CloseHandle $hradio + set hradio [FindNextRadio $finder] + } + } finally { + FindFirstRadioClose $finder + } + return $radios +} + +proc iocp::bt::radio::Open {{radio {}}} { + # Get a handle to a radio. + # radio - The address or name associated with a radio on the system. + # If unspecified or the empty string, a handle to the first + # radio found is returned. + + # The command will raise an error if no matching radio is present. + # + # The returned handle must be closed with the [close] command. + # + # Returns a handle to a Bluetooth radio. + + set pair [FindFirstRadio] + if {[llength $pair] == 0} { + error "Radio not found." + } + lassign $pair finder hradio + try { + while {1} { + set info [GetRadioInfo $hradio] + if {$radio eq "" || + [string equal -nocase $radio [dict get $info Name]] || + [string equal -nocase $radio [dict get $info Address]]} { + return $hradio + } + CloseHandle $hradio + set hradio [FindNextRadio $finder] + } + } finally { + FindFirstRadioClose $finder + } + error "Radio not found." +} + +proc iocp::bt::radio::Close {hradio} { + # Closes a radio handle. + # hradio - The radio handle to close. + CloseHandle $hradio +} + +proc iocp::bt::radio::info {{radio {}}} { + # Get detailed information about a radio on the system + # radio - The address or name associated with a radio on the system. + # If unspecified or the empty string, information about + # the first radio found is returned. + # + # The returned dictionary has the following keys: + # + # Address - The Bluetooth address of the radio. + # Name - Name assigned to the local system as advertised by the radio. + # Class - Device class as a numeric value. + # DeviceClasses - Human readable list of general device class categories + # Subversion - Integer value whose interpretation is manufacturer-specific. + # Manufacturer - Integer identifier assigned to the manufacturer. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # + # Returns a dictionary containing information about the radio. + + set hradio [Open $radio] + try { + set radio [GetRadioInfo $hradio] + set radio [dict merge $radio [DeviceClass [dict get $radio Class]]] + } finally { + Close $hradio + } +} + +proc iocp::bt::radio::configure {radio args} { + # Gets or modifies a radio configuration. + # radio - The address or name associated with a radio on the system. + # args - See below. + # + # Returns an option value, a dictionary of options and values, or an empty + # string. + # + # If no arguments are given to the command, it returns the current + # values of all options in the form of a dictionary. + # + # If exactly one argument is given, it must be the name of an option + # and the command returns the value of the option. + # + # Otherwise, the arguments must be a list of option name and + # values. The radio's options are set accordingly. The command returns + # an empty string for this case. Note that in case of raised exceptions + # the state of the radio options is indeterminate. + # + # Supported option names are `-discoverable` and `-connectable` both + # of which take boolean values. They control whether the Bluetooth radio + # is discoverable and connectable from other devices. + # + # Known bug: the command does not work reliably on all Windows systems. + # + + set hradio [Open $radio] + try { + if {[llength $args] == 0} { + return [list \ + -discoverable [IsDiscoverable $hradio] \ + -connectable [IsConnectable $hradio]] + } elseif {[llength $args] == 1} { + return [switch -exact -- [lindex $args 0] { + -discoverable {IsDiscoverable $hradio} + -connectable {IsConnectable $hradio} + default { error "Unknown option \"[lindex $args 0]\"."} + }] + } else { + set unchanged {} + foreach {opt val} $args { + switch -exact -- $opt { + -discoverable { + set changed [EnableDiscovery $val] + } + -connectable { + set changed [EnableIncoming $val] + } + default { error "Unknown option \"$opt\"."} + } + if {! $changed} { + lappend unchanged $opt + } + } + if {[llength $unchanged]} { + error "Option(s) [join $unchanged {, }] could not be modified." + } + } + } finally { + Close $hradio + } +} + +proc iocp::bt::radio::devices {radio args} { + # Discover devices accessible through the specified radio. + # radio - Name or address of Bluetooth radio + # args - Passed on to [::iocp::bt::devices] + # This command has the same functionality as the [::iocp::bt::devices] + # command except that it restricts discovery only to those devices + # accessible through the specified radio. + # + # Returns a list of device information dictionaries. See + # [::iocp::bt::devices] for the dictionary format. + + set hradio [Open $radio] + try { + return [::iocp::bt::devices {*}$args -hradio $hradio] + } finally { + Close $hradio + } +} + +proc iocp::bt::devices {args} { + # Discover Bluetooth devices. + # -authenticated - filter for authenticated devices + # -connected - filter for connected devices + # -inquire - issue a new inquiry. Without this option, devices that are + # not already known to the system will not be discovered. + # -remembered - filter for remembered devices + # -timeout MS - timeout for the inquiry in milliseconds. Defaults to 10240ms. + # Ignored if `-inquire` is not specified. + # -unknown - filter for unknown devices + # + # Each device information element is returned as a dictionary with + # the following keys: + # Authenticated - Boolean value indicating whether the device has + # been authenticated + # Address - Bluetooth address of the devicec + # Class - Device class as a numeric value + # Connected - Boolean value indicating whether the device is connected + # DeviceClasses - Human readable list of general device class categories + # LastSeen - Time when device was last seen. The format is a list of + # year, month, day, hour, minutes, seconds and milliseconds. + # LastUsed - Time when device was last used. The format is a list of + # year, month, day, hour, minutes, seconds and milliseconds. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # Name - Human readable name of the device + # Remembered - Boolean value indicating whether the device is connected + # + # The filtering options may be specified to limit the devices + # returned. If none are specified, all devices are returned. + # + # Returns a list of device information dictionaries. + + set pair [FindFirstDevice {*}$args] + if {[llength $pair] == 0} { + # No devices found + return {} + } + lassign $pair finder device + set device [dict merge $device [DeviceClass [dict get $device Class]]] + set devices [list $device] + try { + while {1} { + set device [FindNextDevice $finder] + set device [dict merge $device [DeviceClass [dict get $device Class]]] + lappend devices $device + } + } finally { + FindFirstDeviceClose $finder + } + return $devices +} + +proc iocp::bt::device::address {name args} { + # Returns a Bluetooth address for a given name. + # name - name of device of interest + # args - Options to control device enquiry. See [devices]. + # If a device has multiple addresses, the command may return any one + # them. If no addresses are found for the device, an error is raised. + set addrs [addresses $name {*}$args] + if {[llength $addrs]} { + return [lindex $addrs 0] + } + error "Could not find address for device \"$name\"." +} + +proc iocp::bt::device::addresses {name args} { + # Returns a list of Bluetooth addresses for a given name. + # name - name of device of interest + # args - Options to control device enquiry. See [devices]. + + set addresses [lmap device [devices {*}$args] { + if {[string compare -nocase $name [dict get $device Name]]} { + continue + } + dict get $device Address + }] + # Also resolve local system radios + foreach radio [radios 1] { + if {[string equal -nocase $name [dict get $radio Name]]} { + lappend addresses [dict get $radio Address] + } + } + return $addresses +} + +proc iocp::bt::device::port {device service_class} { + # Resolve the port for a Bluetooth service running over RFCOMM. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service_class - UUID or name of service class of interest. Note the + # service **name** cannot be used for lookup. + # + # In case multiple services of the same service class are available on the + # device, the port for the first one discovered is returned. + # + # Returns the port number for the service or raises an error if it + # cannot be resolved. + + #TBD - maybe use device::services and loop through so we can match service_class + #against service name as well + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::service_class_uuid $service_class]] + try { + while {1} { + # 0x100 -> LUP_RETURN_ADDR + set rec [LookupServiceNext $h 0x100] + if {[dict exists $rec RemoteAddress] && + [dict get $rec RemoteAddress AddressFamily] == 32} { + # 32 -> AF_BTH (Bluetooth) + # Further we are looking for RFCOMM (protocol 3) + if {[dict exists $rec Protocol] && + [dict get $rec Protocol] == 3} { + return [dict get $rec RemoteAddress Port] + } + } + } + } finally { + LookupServiceEnd $h + } + error "Could not resolve service \"$service_class\" to a port on device \"$device\"." +} + +proc iocp::bt::device::remove {device} { + # Removes cached authentication information for a device from the system cache. + # device - bluetooth address or name of a device. if specified as a name, + # it must resolve to a single address. + # The command will raise an error if $device is a name that cannot be + # resolved. + + RemoveDevice [ResolveDeviceUnique $device] +} + +proc iocp::bt::device::service_references {device service} { + # Retrieve service discovery records that refer to a specified service. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service - the UUID of a service or service class or its mnemonic. + # The command will return all service discovery records that contain + # an attribute referring to the specified service. + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service discovery records. + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::to_uuid $service]] + set recs {} + try { + while {1} { + # 0x0200 -> LUP_RETURN_BLOB. Returns {Blob BINDATA} + lappend recs [lindex [LookupServiceNext $h 0x200] 1] + } + } finally { + LookupServiceEnd $h + } + return $recs +} + +proc iocp::bt::device::services {device} { + # Retrieve the service discovery records for top level services + # advertised by a device. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # + # The command will return all service discovery records that reference + # the `PublicBrowseRoot` service class. This is not necessarily all the + # services on the device, only those the device advertises as the + # top-level services. + # + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service dscovery records. + + # TBD - add a browse group parameter + # TBD - perhaps check that the sdr acually refernces browse group in + # the appropriate attribute + return [service_references $device 00001002-0000-1000-8000-00805f9b34fb] +} + +# TBD - is this needed? Less functional version of services +proc iocp::bt::device::enumerate_services {device} { + # Get installed services on a device. + # device - Bluetooth address or name of device. If specified as a name, + # it must resolve to a single address. + # Returns a list of service UUID's. + + return [EnumerateInstalledServices [ResolveDeviceUnique $device]] +} + + +proc iocp::bt::device::print {devinfo} { + # Prints device information in human-readable form to stdout. + # devinfo - A device information record as returned by + # the [devices] command. + dict with devinfo { + puts "Device $Name" + puts "Address: $Address" + puts "Class: $Class ($MajorClassName:$MinorClassName)" + puts "Device categories: ([join $DeviceClasses {, }])" + puts "Authenticated: $Authenticated" + puts "Remembered: $Remembered" + puts "Connected: $Connected" + puts "Last seen: $LastSeen" + puts "Last used: $LastUsed" + } +} + +proc iocp::bt::device::printn {dinfolist {detailed false}} { + # Prints device information in human-readable form to stdout. + # dinfolist - A list of device information records as returned by + # the [devices] command. + # detailed - If a true value, detailed information about the device + # is printed. If false (default), only the address and + # name are printed in compact form. + set sep "" + foreach dinfo $dinfolist { + if {$detailed} { + puts -nonewline $sep + set sep "----------------------------------------------\n" + print $dinfo + } else { + dict with dinfo { + puts "$Address $Name" + } + } + } +} + +proc iocp::bt::IsAddress {addr} { + # Returns boolean indicating whether addr is a valid Bluetooth address + # addr - value to be checked + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){5}$} $addr] + +} + +proc iocp::bt::Uuid16 {uuid16} { + if {![regexp {^[[:xdigit:]]{4}$} $uuid16]} { + error "\"$uuid16\" is not a valid 16 bit UUID." + } + return 0000${uuid16}-0000-1000-8000-00805f9b34fb +} + +proc iocp::bt::IsUuid {uuid} { + + # Check passed parameter for UUID syntax + + # Only checks syntax. Not RFC 4122 validity + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid] +} + +proc iocp::bt::IsBluetoothUuid {uuid} { + + # Check passed parameter for UUID syntax + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-0000-1000-8000-00805f9b34fb$} $uuid] +} + + +proc iocp::bt::BinToHex {bin {start 0} {end end}} { + regexp -inline -all .. [binary encode hex [string range $bin $start $end]] +} + +proc iocp::bt::EncodingTclName {mibenum} { + set encoding_map { + 3 ascii + 2026 big5 + 437 cp437 + 2087 cp775 + 2009 cp850 + 2010 cp852 + 2046 cp855 + 2047 cp857 + 2048 cp860 + 2049 cp861 + 2013 cp862 + 2050 cp863 + 2051 cp864 + 2052 cp865 + 2086 cp866 + 2054 cp869 + 2109 cp874 + 113 cp936 + 2250 cp1250 + 2251 cp1251 + 2252 cp1252 + 2253 cp1253 + 2254 cp1254 + 2255 cp1255 + 2256 cp1256 + 2257 cp1257 + 2258 cp1258 + 2088 ebcdic + 18 euc-jp + 38 euc-kr + 56 gb1988 + 2025 gb2312 + 57 gb2312-raw + 39 iso2022-jp + 37 iso2022-kr + 4 iso8859-1 + 5 iso8859-2 + 6 iso8859-3 + 7 iso8859-4 + 8 iso8859-5 + 9 iso8859-6 + 10 iso8859-7 + 11 iso8859-8 + 12 iso8859-9 + 13 iso8859-10 + 109 iso8859-13 + 110 iso8859-14 + 111 iso8859-15 + 112 iso8859-16 + 15 jis0201 + 63 jis0208 + 98 jis0212 + 2084 koi8-r + 2088 koi8-u + 36 ksc5601 + 17 shiftjis + 2259 tis-620 + 106 utf-8 + } + incr mibenum 0; # Convert to canonical decimal string + if {[dict exists $encoding_map $mibenum]} { + return [dict get $encoding_map $mibenum] + } + return $mibenum +} + +proc iocp::AddressFamilyName {af} { + if {$af == 2} { + return IPv4 + } elseif {$af == 23} { + return IPv6 + } elseif {$af == 32} { + return Bluetooth + } elseif {$af == 1} { + return Unix + } elseif {$af == 6} { + return IPX + } elseif {$af == 17} { + return Netbios + } else { + return AF$af + } +} + +proc DeviceClass {class} { + if {[expr {$class & 3}] != 0} { + # Not format type that we understand + return {} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/baseband/ + + set minor_class [expr {($class >> 2) & 0x3f}] + set major_class [expr {($class >> 8) & 0x1f}] + + if {$major_class == 0} { + set major_name "Miscellaneous" + set minor_name "Miscellaneous" + } elseif {$major_class == 1} { + set major_name Computer + set minor_name [lindex { + Uncategorized Desktop Server Laptop "Handheld PC/PDA" "Palm-size PC/PDA" Wearable Tablet + } $minor_class] + } elseif {$major_class == 2} { + set major_name Phone + set minor_name [lindex { + Uncategorized Cellular Cordless Smartphone "Wired modem" ISDN + } $minor_class] + } elseif {$major_class == 3} { + set major_name "LAN/Network Access point" + set minor_name [lindex { + 0% 1-17% 17-33% 33-50% 50-67% 67-83% 83-99% 100% + } $minor_class] + } elseif {$major_class == 4} { + set major_name "Audio/Video" + set minor_name [lindex { + Uncategorized "Wearable Headset Device" "Hands-free Device" Reserved + Microphone Loudspeaker Headphones "Portable Audio" "Car Audio" + "Set-top box" "HiFi Audio Device" VCR Camcorder "Video Monitor" + "Video Display and Loudspeaker" "Video Conferencing" Reserved + "Gaming/Toy" + } $minor_class] + } elseif {$major_class == 5} { + set major_name Peripheral + set bits [expr {$minor_class >> 4}] + set minor_name [lindex { + Other Keyboard "Pointing device" "Combo keyboard/pointing device" + } $bits] + set bits [expr {$minor_class & 0xf}] + if {$bits < 10} { + append minor_name /[lindex { + Uncategorized Joystick Gamepad "Remote control" "Sensing device" + "Digitizer tabler" "Card reader" "Digital pen" "Handheld scanner" + "Handheld gesture input device" + }] + } + } elseif {$major_class == 6} { + set major_name Imaging + set minor_name {} + if {$minor_class & 4} {lappend minor_name Display} + if {$minor_class & 8} {lappend minor_name Camera} + if {$minor_class & 16} {lappend minor_name Scanner} + if {$minor_class & 32} {lappend minor_name Printer} + set minor_name [join $minor_name /] + } elseif {$major_class == 7} { + set major_name Wearable + set minor_name [lindex { + Wristwatch Pager Jacket Helmet Glasses + } $minor_class] + } elseif {$major_class == 8} { + set major_name Toy + set minor_name [lindex { + Robot Vehicle Doll Controller Game + } $minor_class] + } elseif {$major_class == 9} { + set major_name Health + set minor_name [lindex { + Undefined "Blood Pressure Monitor" Thermometer "Weighing Scale" + "Glucose Meter" "Pulse Oximeter" "Heart/Pulse Rate Monitor" + "Health Data Display" "Step Counter" "Body Composition Analyzer" + "Peak Flow Monitor" "Medication Monitor" "Knee Prosthesis" + "Andle Prosthesis" "Generic Health Manager" "Personal Mobility Device" + } $minor_class] + } elseif {$major_class == 31} { + set major_name Uncategorized + set minor_name "" + } else { + set major_name Reserved + set minor_name "" + } + + set service_class {} + if {$class & (1<<13)} {lappend service_class "Limited Discoverable Mode"} + if {$class & (1<<16)} {lappend service_class "Positioning (Location identification)"} + if {$class & (1<<17)} {lappend service_class "Networking"} + if {$class & (1<<18)} {lappend service_class "Rendering"} + if {$class & (1<<19)} {lappend service_class "Capturing"} + if {$class & (1<<20)} {lappend service_class "Object Transfer"} + if {$class & (1<<21)} {lappend service_class "Audio"} + if {$class & (1<<22)} {lappend service_class "Telephony"} + if {$class & (1<<23)} {lappend service_class "Information"} + + return [dict create MajorClassName $major_name \ + MinorClassName $minor_name \ + DeviceClasses $service_class] +} + +proc iocp::bt::ResolveDeviceUnique {device} { + if {[IsAddress $device]} { + return $device + } + set addrs [device address $device] + if {[llength $addrs] == 0} { + error "Could not resolve Bluetooth device name \"$device\"." + } elseif {[llength $addrs] > 1} { + error "Device \"$device\" resolves to multiple addresses." + } + return [lindex $addrs 0] +} + +package provide iocp_bt $iocp::bt::version diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl new file mode 100644 index 00000000..8b4c96c3 --- /dev/null +++ b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl @@ -0,0 +1,383 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::names { + namespace path [namespace parent] + # Maps UUID to Service class name + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + # UUID may be either 16-bit Bluetooth universal UUID as or the full UUID + # Uses lower case hex. + variable service_class_names { + 1000 ServiceDiscoveryServerServiceClassID + 1001 BrowseGroupDescriptorServiceClassID + 1002 PublicBrowseRoot + 1101 SerialPort + 1102 LANAccessUsingPPP + 1103 DialupNetworking + 1104 IrMCSync + 1105 OBEXObjectPush + 1106 OBEXFileTransfer + 1107 IrMCSyncCommand + 1108 Headset + 1109 CordlessTelephony + 110a AudioSource + 110b AudioSink + 110c A/V_RemoteControlTarget + 110d AdvancedAudioDistribution + 110e A/V_RemoteControl + 110f A/V_RemoteControlController + 1110 Intercom + 1111 Fax + 1112 {Headset - Audio Gateway (AG)} + 1115 PANU + 1116 NAP + 1117 GN + 1118 DirectPrinting + 1119 ReferencePrinting + 111a {Basic Imaging Profile} + 111b ImagingResponder + 111c ImagingAutomaticArchive + 111d ImagingReferencedObjects + 111e Handsfree + 111f HandsfreeAudioGateway + 1120 DirectPrintingReferenceObjectsService + 1121 ReflectedUI + 1122 BasicPrinting + 1123 PrintingStatus + 1124 HumanInterfaceDeviceService + 1125 HardcopyCableReplacement + 1126 HCR_Print + 1127 HCR_Scan + 1128 Common_ISDN_Access + 112d SIM_Access + 112e {Phonebook Access - PCE} + 112f {Phonebook Access - PSE} + 1130 {Phonebook Access} + 1131 {Headset - HS} + 1132 {Message Access Server} + 1133 {Message Notification Server} + 1134 {Message Access Profile} + 1135 GNSS + 1136 GNSS_Server + 1137 {3D Display} + 1138 {3D Glasses} + 1139 {3D Synchronization} + 113a {MPS Profile UUID} + 113b {MPS SC UUID} + 113c {CTN Access Service} + 113d {CTN Notification Service} + 113e {CTN Profile} + 1200 PnPInformation + 1201 GenericNetworking + 1202 GenericFileTransfer + 1203 GenericAudio + 1204 GenericTelephony + 1303 VideoSource + 1304 VideoSink + 1305 VideoDistribution + 1400 HDP + 1401 {HDP Source} + 1402 {HDP Sink} + 02030302-1d19-415f-86f2-22a2106a0a77 {Wireless iAP v2} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + variable protocol_names { + 0001 SDP + 0002 UDP + 0003 RFCOMM + 0004 TCP + 0005 TCS-BIN + 0006 TCS-AT + 0007 ATT + 0008 OBEX + 0009 IP + 000a FTP + 000c HTTP + 000e WSP + 000f BNEP + 0010 UPNP + 0011 HIDP + 0012 HardcopyControlChannel + 0014 HardcopyDataChannel + 0016 HardcopyNotification + 0017 AVCTP + 0019 AVDTP + 001b CMTP + 001e MCAPControlChannel + 001f MCAPDataChannel + 0100 L2CAP + } + + # Universal attribute names + variable attribute_names + set attribute_names { + ServiceRecordHandle 0 + ServiceClassIDList 1 + ServiceRecordState 2 + ServiceID 3 + ProtocolDescriptorList 4 + BrowseGroupList 5 + LanguageBaseAttributeIDList 6 + ServiceInfoTimeToLive 7 + ServiceAvailability 8 + BluetoothProfileDescriptorList 9 + DocumentationURL 10 + ClientExecutableURL 11 + IconURL 12 + AdditionalProtocolDescriptorList 13 + ServiceName 256 + ServiceDescription 257 + ProviderName 258 + } + +} + +proc iocp::bt::names::is_uuid {uuid} { + regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid +} + +proc iocp::bt::names::to_name {uuid} { + # Map a UUID to a name. + # uuid - Bluetooth UUID + # The command attempts to map the UUID as a service name or protocol + # name in that order. + # + # Returns the mapped name or the uuid if no mapping is possible. + + set name [service_class_name $uuid] + if {$name ne $uuid && ![is_uuid $name]} { + return $name + } + return [protocol_name $uuid] +} + +proc iocp::bt::names::to_uuid {name_or_uuid} { + # Map a name to a UUID. + # name_or_uuid - Bluetooth UUID or name + # The command attempts to map the name as a service class name or protocol + # name in that order. + # + # Returns the mapped uuid raises an error if no mapping is possible. + variable service_class_names + if {[MapNameToUuidV $name_or_uuid service_class_names name]} { + return $name + } + return [protocol_uuid $name_or_uuid] +} + + +proc iocp::bt::names::service_class_name {uuid} { + # Maps a UUID to a service class name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable service_class_names + set name [MapUuidToName $uuid service_class_names] + if {$name ne $uuid} { + return $name + } + # Well known private uuids + if {[string equal -nocase "02030302-1d19-415f-86f2-22a2106a0a77" $uuid]} { + return "Wireless iAP v2"; # iPod Accessory Protocol + } + return $uuid +} + +proc iocp::bt::names::service_class_uuid {name} { + # Maps a service class name to a UUID. + # name - Service class name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable service_class_names + return [MapNameToUuid $name service_class_names] +} + +proc iocp::bt::names::profile_name {uuid} { + # Maps a UUID to a Bluetooth profile name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + + # Profiles seem to come from same service class UUID space. + return [service_class_name $uuid] +} + +proc iocp::bt::names::protocol_name {uuid} { + # Maps a UUID to a protocol name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable protocol_names + return [MapUuidToName $uuid protocol_names] +} + +proc iocp::bt::names::protocol_uuid {name} { + # Maps a protocol name to a UUID. + # name - Protocol name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable protocol_names + return [MapNameToUuid $name protocol_names] +} + +proc iocp::bt::names::attribute_name {attr_id} { + # Maps a attribute id (numeric or name) to a attribute name. + # attr_id - numeric attribute identifier + # Returns a human-readable name for the attribute or + # the numeric id itself if no mapping could be performed. + variable attribute_names + dict for {name id} $attribute_names { + if {$attr_id == $id || [string equal -nocase $name $attr_id]} { + return $name + } + } + if {[string is integer -strict $attr_id]} { + return $attr_id + } + error "Unknown attribute \"$attr_id\"." +} + +proc iocp::bt::names::attribute_id {name} { + # Maps a attribute name to it numeric attribute identifier. + # name - Attribute name to be mapped + # The command will raise an error if the name is unknown. + # Returns the numeric attribute identifier corresponding to + # the passed name. + variable attribute_names + if {[string is integer -strict $name]} { + return $name + } + return [dict get $attribute_names $name] +} + +proc iocp::bt::names::print {} { + # Prints known UUID's and their mapped mnemonics. + variable service_class_names + dict for {uuid16 name} $service_class_names { + puts "[Uuid16 $uuid16] $name" + } +} + +proc iocp::bt::names::MapUuidToName {uuid dictvar} { + upvar 1 $dictvar names + + # uuid may be a full UUID or a 16-bit Bluetooth UUID + if {[IsUuid $uuid]} { + # See if the exact match exists + set uuid [string tolower $uuid] + if {[dict exists $names $uuid]} { + return [dict get $names $uuid] + } + # Nope. Now see if it is a 16-bit Bluetooth UUID + if {[IsBluetoothUuid $uuid] && [string range $uuid 0 3] eq "0000"} { + # 16-bit UUID + set uuid16 [string tolower [string range $uuid 4 7]] + if {[dict exists $names $uuid16]} { + return [dict get $names $uuid16] + } + } + # Nope. Cannot be mapped. Return the UUID. + return $uuid + } + + # Not a full UUID. Retry as a 16-but UUID. tailcall so upvar works right. + tailcall MapUuidToName [Uuid16 $uuid] $dictvar +} + +proc iocp::bt::names::MapNameToUuidV {name dictvar resultvar} { + upvar 1 $dictvar names + upvar 1 $resultvar result + + if {[IsUuid $name]} { + set result $name + return 1 + } + dict for {uuid mapped_name} $names { + if {[string equal -nocase $name $mapped_name]} { + if {[string length $uuid] == 4} { + # 16-bit UUID -> full UUID + set result "0000${uuid}-0000-1000-8000-00805f9b34fb" + } else { + set result $uuid + } + return 1 + } + } + return 0 +} + +proc iocp::bt::names::MapNameToUuid {name dictvar} { + upvar 1 $dictvar names + if {[MapNameToUuidV $name names result]} { + return $result + } + error "Name \"$name\" could not be mapped to a UUID" +} + +proc iocp::bt::names::Asciify {svar} { + # Replaces non-ascii or non-printable chars with Tcl sequences + # + # Transformed string is stored back in svar. + + upvar 1 $svar s + set ascii "" + set was_escaped 0 + # Split can be expensive for large strings so string index + set len [string length $s] + for {set i 0} {$i < $len} {incr i} { + set char [string index $s $i] + scan $char %c codepoint + if {$codepoint < 32 || $codepoint >= 126} { + append ascii "\\u[format %.4x $codepoint]" + set was_escaped 1 + } else { + append ascii $char + } + } + set s $ascii + return $was_escaped +} + +proc iocp::bt::names::ClipSpec {{use_list 0}} { + package require twapi + + set csv [twapi::read_clipboard_text] + if {$use_list} { + set tcl "\[list \n" + } else { + set tcl "\{ \n" + } + foreach line [split $csv \n] { + lassign [split $line \t] name uuid description + set name [string trim $name] + set uuid [string trim $uuid] + if {![regexp {^0x[[:xdigit:]]{4}} $uuid]} continue + if {$use_list} { + # Unicode chars, needs escaping + Asciify name + append tcl " " [list [string range $uuid 2 end]] " " "\"$name\"" " \\" \n + } else { + if {[Asciify name]} { + # Need to use the list command. Just restart + return [ScrapeBTSpec 1] + } + append tcl " " [string range $uuid 2 end] " " "{$name}" \n + } + } + if {$use_list} { + append tcl "\]\n" + } else { + append tcl "\}" + } + + twapi::write_clipboard_text $tcl +} + diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl new file mode 100644 index 00000000..cd014a1b --- /dev/null +++ b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl @@ -0,0 +1,962 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::sdr { + namespace path [namespace parent] + namespace export attribute attributes decode print printn + + namespace eval attribute { + namespace path [list [namespace parent] [namespace parent [namespace parent]]] + namespace export exists get raw text + namespace ensemble create + } +} + +proc iocp::bt::sdr::decode {binsdr} { + # Decodes a binary service discovery record + # binsdr - a raw Bluetooth service discovery record in binary + # form. + # + # The returned value should be treated as opaque. The attributes + # stored in the record should be accessed with the commands in + # the `sdr` namespace. + # + # Returns a container of attributes stored in the record. + + # A SDR record is a single data element which is a sequence + # containing nested data elements. + # {{sequence {nested data elements}}} + set rec [lindex [DecodeElements $binsdr] 0 1] + + # $rec contains alternating attribute value pairs. The result + # is built as a list as its faster to do that and shimmer + # to a dict on access than to build a dictionary to begin with. + set sdr {} + foreach {attr val} $rec { + lappend sdr [lindex $attr 1] $val + } + return $sdr +} + +proc iocp::bt::sdr::attribute::exists {sdr attr_id {varname {}}} { + # Checks if an attribute exists in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # attr_id - attribute integer id or Bluetooth universal attribute name + # varname - If not the empty string, the attribute + # value is stored in a variable of this name in the + # caller's context. + # + # The value returned in $varname is the attribute type-independent data + # value. To get the value formatted for the attribute type, use the + # [attribute get] command. + # + # Returns 1 if the attribute exists, and 0 otherwise. + + if {[string is integer -strict $attr_id]} { + set key [expr {$attr_id + 0}]; # Force decimal rep. Faster than format + } else { + set key [names::attribute_id $attr_id]; # name -> id + } + + if {[dict exists $sdr $key]} { + if {$varname ne ""} { + upvar 1 $varname value + set value [dict get $sdr $key] + } + return 1 + } + + return 0 +} + +proc iocp::bt::sdr::attributes {sdr} { + # Get the list of attributes in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # Returns a list of numeric attribute ids. + return [dict keys $sdr] +} + +proc iocp::bt::sdr::attribute::raw {sdr attr_id} { + # Get an attribute value from an service discovery record. + # sdr - a decoded service discovery record in the form returned by + # sdr_decode. + # attr_id - attribute integer id + # + # The command will raise an error if the attribute does not exist + # in the sdr. + # + # Returns the attribute value as a pair consisting of the type and + # the raw data element value. + + if {[exists $sdr $attr_id value]} { + return $value + } else { + error "Attribute with id \"$attr_id\" not found." + } +} + +proc iocp::bt::sdr::attribute::get {sdr attr_id {varname {}}} { + # Get the value of an universal attribute from a service + # discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # varname - Optional name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The attribute value is decoded into a attribute type-dependent format. + # + # The $attr_id argument must be one of those defined in + # the *Universal Attributes* section in the + # [Bluetooth Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/) + # specification. Otherwise an error is raised. The recognized attributes + # are listed below. Refer to the Bluetooth specification for their + # exact semantics. + # + # AdditionalProtocolDescriptorList - List of protocol stacks. This + # supplements the `ProtocolDescriptorList` attribute. + # BluetoothProfileDescriptorList - List of Bluetooth profile descriptors + # to which the service conforms. Each element is a dictionary + # with keys `Uuid`, `Name`, `MajorVersion` and `MinorVersion` + # corresponding to the referenced profile. + # BrowseGroupList - List of browse group descriptors representing + # the browse groups to which the service record belongs. Each + # descriptor is dictionary with keys `Uuid` and `Name` identifying + # the a browse group. + # ClientExecutableURL - URL pointing to the client application that + # may be used to utilize the service. + # DocumentationURL - URL for the service documentation. + # IconURL - URL for icon that may be used to represent the service. + # LanguageBaseAttributeIDList - Nested dictionary of language-specific + # attribute offsets (see below). + # ProtocolDescriptorList - List of protocol stacks (see below) that may + # be used to access the service described by the service record. + # ProviderName - Name of entity providing the service in the primary + # language of the service record. + # ServiceAvailability - A value in the range 0-255 that indicates + # relative availability of the service in terms of the number of + # clients it can accept. Note this is a scaled estimate. + # ServiceClassIDList - List with each element being a dictionary + # corresponding to a service class that the record conforms to. + # The keys of the dictionary are `Name` and `Uuid`. + # ServiceDescription - A description of the service in the primary + # language of the record. + # ServiceID - UUID that uniquely identifies a service instance. + # ServiceInfoTimeToLive - The number of seconds from the time it was + # generated that the service record information is expected to be valid. + # ServiceName - The human-readable name of the service in the primary + # language of the record. + # ServiceRecordHandle - A numeric value that uniquely identifies a service + # record within a SDP server. + # ServiceRecordState - Numeric value that is changed any time the service + # record is modified by the SDP server. + # + # In the case of ProtocolDescriptorList and AdditionalProtocolDescriptorList, + # the attribute value is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # In the case of LanguageBaseAttributeIDList, the attribute value is + # a dictionary indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. + # + # Returns a boolean or the decoded attribute value. + + set attr_name [names::attribute_name $attr_id] + set attr_id [names::attribute_id $attr_id] + switch -exact -- $attr_name { + BrowseGroupList - + ServiceClassIDList { + tailcall Uuids $sdr $attr_id $varname + } + BluetoothProfileDescriptorList - + ProtocolDescriptorList - + AdditionalProtocolDescriptorList - + LanguageBaseAttributeIDList { + tailcall $attr_name $sdr $varname + } + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceAvailability - + ServiceInfoTimeToLive - + ServiceID - + ServiceRecordState - + ServiceRecordHandle { + tailcall AttributeValue $sdr $attr_id $varname + } + ServiceName - + ServiceDescription - + ProviderName { + # Special case because they need a language argument + tailcall text $sdr $attr_name primary $varname + } + default { + if {[llength [info commands [namespace current]::universal::$attr_name]]} { + tailcall [namespace current]::universal::$attr_name $sdr $varname + } + } + } + error "Unknown universal attribute \"$attr_id\"." +} + +proc iocp::bt::sdr::attribute::text {sdr attr_id lang {varname {}}} { + # Get the value of an text attribute in the specified language + # from a service discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # lang - language identifier as specified in iso639, e.g. `en` for + # english, `fr` for french etc. or the keyword `primary`. + # varname - Name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # If the record does not contain a value for the specified language, + # the value for the primary language will be retrieved. + # + # Returns a boolean or the attribute text value. + # + set lang_offset [names::attribute_id $attr_id] + if {$lang_offset < 256 || $lang_offset >= 512} { + error "Invalid text attribute id \"$attr_id\"." + } + incr lang_offset -256 + tailcall TextAttribute $sdr $lang_offset $lang $varname +} + + +proc iocp::bt::sdr::attribute::LanguageBaseAttributeIDList {sdr {varname {}}} { + # Retrieve the language base attributes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a nested dictionary + # corresponding to the `LanguageBaseAttributeIDList` + # universal attribute in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The dictionary value returned or stored is indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. fs + # + # Returns a boolean or a dictionary of language attributes. + if {![AttributeValue $sdr 6 sequence]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # attr is {sequence {langid encoding baseoffset ...}} + set langs [dict create] + foreach {langid enc offset} $sequence { + # langid is ISO 639 language code + set langid [lindex $langid 1] + set langid "[format %c [expr {$langid >> 8}]][format %c [expr {$langid &0xff}]]" + dict set langs $langid Encoding [::iocp::bt::EncodingTclName [lindex $enc 1]] + dict set langs $langid BaseOffset [lindex $offset 1] + } + if {$varname eq ""} { + return $langs + } else { + upvar 1 $varname var + set var $langs + return 1 + } +} + + +proc iocp::bt::sdr::attribute::ProtocolDescriptorList {sdr {varname {}}} { + # Retrieve the protocol information from a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # Information about the protocols that may be used to access the + # service described by SDR is stored in the ProtocolDescriptorList + # universal attribute as described in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The returned information is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # Returns a boolean or the list of protocols. + + if {! [attribute exists $sdr 4 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + if {$varname eq ""} { + return [Protocols $attrval] + } else { + upvar 1 $varname var + set var [Protocols $attrval] + return 1 + } +} + +proc iocp::bt::sdr::attribute::BluetoothProfileDescriptorList {sdr {varname {}}} { + # Retrieve the service classes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a list of profile descriptors + # corresponding to the BluetoothProfileDescriptorList + # universal attribute in the Bluetooth Core specification. + # Each descriptor is a dictionary with the following keys: + # `Uuid` - the UUID of the profile + # `Name` - the mnemonic name of the profile or its UUID if the + # UUID cannot be mapped to a name + # `MajorVersion` - the profile's major version + # `MinorVersion` - the profile's minor version + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # Returns a boolean or the list of service class descriptors. + + if {! [attribute exists $sdr 9 attr]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # Sequence of profile descriptors. Each profile descriptor + # is sequence of profile uuid and version. + set profiles {} + foreach profile [lindex $attr 1] { + set uuid [lindex $profile 1 0 1] + set version [lindex $profile 1 1 1] + lappend profiles [dict create \ + Uuid $uuid \ + Name [iocp::bt::names::to_name $uuid] \ + MajorVersion [expr {$version >> 8}] \ + MinorVersion [expr {$version & 0xff}] + ] + } + + if {$varname eq ""} { + return $profiles + } else { + upvar 1 $varname var + set var $profiles + return 1 + } + +} + +proc iocp::bt::sdr::attribute::AdditionalProtocolDescriptorList {sdr {varname {}}} { + # Returns protocol information from the additional protocols field + # in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # This command is identical to [protocols] except it extracts + # protocol information from the `AdditionalProtocolDescriptorList` + # attribute instead of `ProtocolDescriptorList`. + + if {! [attribute exists $sdr 13 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # AdditionalProtocolDescriptorList is a sequence each element of which + # is formatted like a ProtocolDescriptorList + # assert [lindex $attrval 0] eq "sequence" + set protocols [lmap elem [lindex $attrval 1] { + Protocols $elem + }] + + if {$varname eq ""} { + return $protocols + } else { + upvar 1 $varname var + set var $protocols + return 1 + } +} + + +proc iocp::bt::sdr::Uuids {sdr attr_id {varname {}}} { + if {! [attribute exists $sdr $attr_id attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + set uuids [lmap elem [lindex $attrval 1] { + set uuid [lindex $elem 1] + dict create Uuid $uuid Name [names::to_name $uuid] + }] + + if {$varname eq ""} { + return $uuids + } else { + upvar 1 $varname var + set var $uuids + return 1 + } +} + +proc iocp::bt::sdr::Protocols {attrval} { + # The attribute value may either be a single protocol stack encoded + # as a sequence, or multiple stacks encoded as a selection of sequences. + if {[lindex $attrval 0] eq "sequence"} { + # Single protocol stack + set stacks [list [lindex $attrval 1]] + } else { + # selection - Multiple stacks + set stacks [lindex $attrval 1] + } + + set protocols [lmap stack $stacks { + # stack -> {protocol_descriptor protocol_descriptor ...} + # protocol_descriptor -> {sequence {data_element ...}} + lmap protocol_descriptor $stack { + # Each descriptor is a sequence, the first element identifying the + # protocol and the remaining being (optional) parameters. + set elements [lindex $protocol_descriptor 1] + set protocol_uuid [lindex $elements 0 1] + dict create \ + Protocol $protocol_uuid \ + ProtocolName [iocp::bt::names::protocol_name $protocol_uuid] \ + ProtocolParams [lrange $elements 1 end] + } + }] + + return $protocols +} + +proc iocp::bt::sdr::AttributeValue {sdr attr_id {varname {}}} { + if {$varname eq ""} { + return [lindex [attribute raw $sdr $attr_id] 1] + } else { + if {[attribute exists $sdr $attr_id attrval]} { + upvar 1 $varname value + set value [lindex $attrval 1] + return 1 + } else { + return 0 + } + } +} + +proc iocp::bt::sdr::TextAttribute {sdr attr_offset lang {varname {}}} { + + # Assume that if sdr does not explicitly specify encoding, it is UTF8 + set enc utf-8 + set base_offset 256 + + # Primary language's ServiceName attribute index always 256 + # Otherwise get the offset from the base index 256 + if {$lang ne "primary"} { + # See if the specified language has an entry in the languages table + if {[attribute get $sdr LanguageBaseAttributeIDList base_offsets] && + [dict exists $base_offsets $lang]} { + # Yes, so the language-specific text will be at an offset + # from the base attribute index + set base_offset [dict get $base_offsets $lang BaseOffset] + set enc [dict get $base_offsets $lang Encoding] + } + } + set attr_index [expr {$base_offset + $attr_offset}] + if {! [attribute exists $sdr $attr_index name]} { + # This language does not exist. Return primary language if possible. + # If base_offset was 256, that's already primary so don't retry + if {$base_offset == 256 || + ![attribute exists $sdr [expr {256 + $attr_offset}] name] } { + if {$varname ne ""} { + return 0 + } else { + error "Attribute not found in SDR." + } + } + } + + set name [lindex $name 1] + + # $name is encoded. If Tcl supports the encoding, well and good. + # Else return a hex form + if {[string is integer $enc]} { + # SDR specified an encoding but not one that Tcl knows about + set text [binary encode hex $name] + } else { + # The string may be null terminated. TBD - always? + if {[string index $name end] eq "\0"} { + set name [string range $name 0 end-1] + } + set text [encoding convertfrom $enc $name] + } + + if {$varname ne ""} { + upvar 1 $varname var + set var $text + return 1 + } else { + return $text + } +} + +proc iocp::bt::sdr::DecodeElements {delem} { + # Decodes a Data Element as defined in the Bluetooth specification. + # delem - Data element in binary format + + set decoded {} + while {[string length $delem]} { + lassign [ExtractFirstElement $delem] type value delem + switch -exact -- $type { + sequence { lappend decoded [list sequence [DecodeElements $value]]} + selection { lappend decoded [list selection [DecodeElements $value]]} + default { + lappend decoded [list $type $value] + } + } + } + return $decoded +} + +proc iocp::bt::sdr::ExtractFirstElement {bin} { + # Extracts leading element from an SDP record. + # bin - tail of SDP record + # Returns a pair consisting of the extracted element and + # the remaining part of $bin. + + set binlen [string length $bin] + if {$binlen == 0} { + return {} + } + binary scan $bin c header + set type [expr {($header >> 3) & 0xff}] + set lenbits [expr {$header & 0x7}] + + # The nil type is formatted slightly differently. Treat as a special case. + if {$type == 0} { + if {$lenbits != 0} { + error "Invalid non-0 length for nil type." + } + return [list nil "" [string range $bin 1 end]] + } + + if {$lenbits < 5} { + set len [expr {1 << $lenbits}] + set dataoffset 1 + } else { + if {$lenbits == 5} { + set dataoffset 2 + set fmt c + set mask 0xff + } elseif {$lenbits == 6} { + set dataoffset 3 + set fmt S + set mask 0xffff + } else { + # lenbits == 7 + set dataoffset 5 + set fmt I + set mask 0xffffffff + } + # Ensure input length enough for at least length field + if {$binlen < $dataoffset} { + error "Truncated binary header." + } + binary scan $bin @1$fmt len + set len [expr {$len & $mask}]; # Since binary scan is signed + } + if {$binlen < ($dataoffset+$len)} { + error "Truncated binary data." + } + set elem [string range $bin $dataoffset [expr {$dataoffset+$len-1}]] + switch -exact $type { + 1 { + set type uint + if {$len == 1} { + binary scan $elem cu elem + } elseif {$len == 2} { + binary scan $elem Su elem + } elseif {$len == 4} { + binary scan $elem Iu elem + } elseif {$len == 8} { + binary scan $elem Wu elem + } elseif {$len == 16} { + binary scan $elem WuWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 2 { + set type int + if {$len == 1} { + binary scan $elem c elem + } elseif {$len == 2} { + binary scan $elem S elem + } elseif {$len == 4} { + binary scan $elem I elem + } elseif {$len == 8} { + binary scan $elem W elem + } elseif {$len == 16} { + # Note hi is signed, lo unsigned + binary scan $elem WWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 3 { + set type uuid + if {$len == 2} { + set elem [binary encode hex $elem] + set elem "0000$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 4} { + set elem [binary encode hex $elem] + set elem "$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 16} { + set hex [binary encode hex $elem] + set elem [string range $hex 0 7] + append elem - [string range $hex 8 11] + append elem - [string range $hex 12 15] + append elem - [string range $hex 16 19] + append elem - [string range $hex 20 end] + } else { + error "Invalid length $len for UUID." + } + } + 4 { + set type text + # TBD - keep elem as binary as we do not know encoding + } + 5 { + set type boolean + if {$len != 1} { + error "Invalid length for boolean type." + } + binary scan $elem c elem + set elem [expr {!!$elem}] ; # Convert to 0/1 + } + 6 { + set type sequence + # elem remains the same. Caller will have to recurse to parse + } + 7 { + set type selection + # elem remains the same. Caller will have to recurse to parse + } + 8 { + set type url + # elem remains same. Encoding unknown. TBD + } + default { + set type unknown + # elem remains same + } + } + + return [list $type $elem [string range $bin $dataoffset+$len end]] +} + +proc iocp::bt::sdr::printn {recs {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # recs - a list of binary SDP records in the form returned by + # [::iocp::bt::device services] or [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + set sep "" + foreach rec $recs { + puts $sep + set sep "--------------------------------------------" + print $rec $attrfilter + } +} + +proc iocp::bt::sdr::print {rec {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # rec - a binary SDP record in the form returned by [::iocp::bt::device services] or + # [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + + # Alternating attribute value pairs + set rec [decode $rec] + foreach attr [attributes $rec] { + set attrname [names::attribute_name $attr] + if {[string is integer -strict $attrname]} { + # For easier matching with Bluetooth specs + set attrname [format 0x%x $attrname] + } + if {![string match -nocase $attrfilter $attrname]} { + continue + } + switch -exact -- $attrname { + ServiceID - + ServiceRecordState - + ServiceInfoTimeToLive - + ServiceAvailability - + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceName - + ServiceDescription - + ProviderName - + ServiceRecordHandle { + puts "$attrname: [attribute get $rec $attrname]" + } + ServiceClassIDList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + # We use this for printing service-specific attributes + lappend service_class_uuids $Uuid + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + ProtocolDescriptorList { + puts "$attrname:" + foreach protocol [attribute get $rec $attrname] { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + AdditionalProtocolDescriptorList { + # Like ProtocolDescriptorList but an additional level + # of nesting. + puts "$attrname:" + foreach additional_protocol [attribute get $rec $attrname] { + foreach protocol $additional_protocol { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + } + BrowseGroupList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + LanguageBaseAttributeIDList { + puts "$attrname: sequence" + dict for {lang val} [attribute get $rec $attrname] { + puts " $lang: [dict get $val Encoding], [dict get $val BaseOffset]" + } + } + BluetoothProfileDescriptorList { + puts "$attrname: sequence" + foreach profile [attribute get $rec $attrname] { + # Sequence of profiles. Each profile is a sequence of + # profile uuid and version. + # puts profile:$profile + dict with profile { + if {$Uuid eq $Name} { + puts " $Uuid v$MajorVersion.$MinorVersion" + } else { + puts " $Uuid $Name v$MajorVersion.$MinorVersion" + } + } + } + } + default { + if {$attr >= 0x100 && $attr <= 0x1ff} { + # TBD - special handling for 0x100-0x1ff attributes by + # looking up languages table + puts "$attrname: [PrintableElement [attribute raw $rec $attr]]" + } else { + lassign [PrintableServiceSpecificAttribute \ + $service_class_uuids $rec $attr] attrname attrval + puts "$attrname: $attrval" + } + } + } + } +} + +proc iocp::bt::sdr::PrintableServiceSpecificAttribute {service_class_uuids rec attr} { + # Returns pair of human readable attribute name and value + set attrname $attr + set attrval [PrintableElement [attribute raw $rec $attr]] + + # Maybe we know about attributes for one of these services + foreach uuid $service_class_uuids { + switch -exact -- $uuid { + 00001105-0000-1000-8000-00805f9b34fb { + # ObjectPush + if {$attr == 0x200} { + set attrname GoepL2CapPsm + break + } elseif {$attr == 0x303} { + set attrname SupportedFormats + set attrval [lmap val [lindex [attribute raw $rec $attr] 1] { + # Val is a sequence of {type value} where type is int + set val [lindex $val 1] + if {$val == 255} { + set val any + } else { + set formats {unknown vcard-2.1 vcard-3.0 vcal-1.0 ical-2.0 vnote vmessage} + if {$val < [llength $formats]} { + set val [lindex $formats $val] + } + } + set val + }] + break + } + } + } + } + return [list $attrname $attrval] +} + +proc iocp::bt::sdr::PrintableElement {delem {parent_indent {}}} { + lassign $delem type val + switch -exact -- $type { + nil - + uint - + int { return $val } + uuid { + set name [names::to_name $val] + if {$name eq $val} { + return $val + } else { + return "$val $name" + } + } + text - + url { + # Assume UTF-8 binary form + # Strip terminating null. TBD - is null alwys present? + set printable [encoding convertfrom utf-8 $val] + } + boolean { return [expr {$val ? "true" : "false"}] } + selection - + sequence { + set printable [list $type] + set new_indent "${parent_indent} " + foreach elem $val { + lappend printable "${new_indent}[PrintableElement $elem $new_indent]" + } + set printable [join $printable \n] + } + unknown { + set printable [BinToHex $val] + } + } + return $printable +} diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl new file mode 100644 index 00000000..2452b04a --- /dev/null +++ b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl @@ -0,0 +1,36 @@ +# Core iocp package +package ifneeded iocp 2.0.2 \ + [list apply [list {dir} { + package require platform + set initName [string totitle iocp] + if {[package vsatisfies [package require Tcl] 9]} { + set fileName "tcl9iocp202.dll" + } else { + set fileName "iocp202.dll" + } + set platformId [platform::identify] + set searchPaths [list [file join $dir $platformId] \ + {*}[lmap platformId [platform::patterns $platformId] { + file join $dir $platformId + }] \ + $dir] + foreach path $searchPaths { + set lib [file join $path $fileName] + if {[file exists $lib]} { + uplevel #0 [list load $lib $initName] + return + } + } + error "Could not locate $fileName in directories [join $searchPaths {, }]" + }] $dir] + +# iocp_inet doesn't need anything other than core iocp +package ifneeded iocp_inet 2.0.2 \ + "package require iocp" + +if {1} { + # iocp_bt needs supporting script files + package ifneeded iocp_bt 2.0.2 \ + "[list source [file join $dir bt.tcl]]" +} + diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/iocp202.dll b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..09d3cf0aa9f0d82e775357fbf226283e817d89d7 GIT binary patch literal 125251 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 Qz?}EdISggrdsOa!0Bc?jNB{r; literal 0 HcmV?d00001 diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/tcl9iocp202.dll b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe296717af838bb5537a33cc4a6e8a58c4ab5296 GIT binary patch literal 125285 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

3!Vv?_d(G$T( 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

*0c68LP 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 cVDSg~O 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

3!Vv?_d(G$T( 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

*0c68LP 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 cVDSg~O zUi^Y#hv+M>31xmiu!mo6Fc$fEeCLIM6J8{_Y-YU`(^J0Op*|&wCYqttQcM7XM~`?IBnFD*d$HK7K#ZrSXrd zAxEpC{3wi>ab`RE79GfB4v0b|A;H4E>70^^FTSb*?|j;zD=tF6Omo7Y?+W8*WDSq| zD0aPe!joq_zDm1&3E;5kD!WK_xIzlNoX-9AT1K2ucpm~>Plj&TXEwG5@jkUCdJEK|tM`dB`riUMUU<_wSj4bM<2d(T6AjMp{El zb5NK)URz9Pm`7u_iya794G(1^CNDL_kN0iidqtBO&#O(L6U@w$`#9e`A8VftZRCy> zX_JKi4*rQj$ltG5mSwDC5QQy0A25{_p0>}x%a~Vvh7m)8g=@r-XrU6WeCo+3pLAk( zFL##tUs`0@D1R>%FzGV8Tg@2j)bA4LC*Zd;=?stM|H>QrKZmkKptu+a48 zuRxy36=EEI@?*77lxNTQxsW|NNv<<%JU}0Np6(s#bWp_qxKX(r*pz4DP zgz(zvj?wq#rLhbGJzpsr0v|me6xWF<9YOgS=B-S~r9&u3|K=xztlBMGD>}nmM_vv9)3ttyNQjVIMWt<=f4+Z{KRk@ zF%f|H@_#RgdRM{wZNVqY3)+z{woa*&Uo+sAe}1W}jFlW;_W7ko+X21z^Gg+MPk&Ng z3X%x5kSgFc#Nu^Jd8u8oZ+y>S?-om$)VE+;O%kS#uE9z*X>*z^5Iz?_S8CEsX{^3d z`SO(B!od@3$g=J#9DHPOJutZL8C(Yh*AHU0+@|2VHn=ViuJ;DlUj)~ig6lQG^|Ii4 zesDb{xK0eNM+VmegX^BbbwF_aV2_~v;JP-rE)TBvnro2XGV)&=5D@#r9t6|T@S+Fn zyFZ+*TL~DlE({q|K8gAFI||An+-dO4a*8|zD4ux-;F%8 z^p~H+I$z{LJDJ#SIN_g2U04MvE*+I1l3h4$1iwsZEVvaEBr7v>9%;yfCUta^Ny1*J z*u&iot4r(cq%e~mT%q6dxmDZuDwdJq+|k|oR(3m~CTtj}B3)qiL$XW1G@pwtWzKQ1 zPKgCY*S`*G!NC$_AK+}qA2hGnAfw|;-T1Hd=th6Yh5Q!AA!#kgVtkSf?;FImJ&kA~ zl_iS1y03J8j?)vpfXe1uPxO@Fw(k!iJq6YMpl_e>nZ8dX7lRWCv)vUU!XZs#Bbof? zbgJ)q+0U_uBalX9xo$tqufC^)AvXSz7ma^MJvBLW2O52f;p{~VSMoMzI$K3&H|r61 z_BEqOt-WW~m+QzO`0y8kFu+}=l)rH-FeONR&R00OeV03jJ;pf=%eM)S!d_0cugV$s zVe7w9dka8Cz*xF&ywK?1hHeX2hkUrpj?a!?4q>}nH;4P`yy)?-Nh@XsLE20UB-Qw`Db8%41_eCsMgDI@TPz7_Ny33w=6fKL;_Md6)}_X6X?=m2=0}(U;X>t`6aeI-uX4%l zd!MuHwSoKyv{t}+_ySUU5z6w;7@ArvVddn866%ZIYD%*$f^AYGMik;-uVM(~FMXFX z%vqB6Kgn+TI8&8t>CSF?q;=11^jxZdMYw379D9Xl5++9^wKz$|#g+VNiZyjR14oWl z3+@S3>?m>EQ(3-ycCZ!^Tkk8x_v8+iJ?O^@QTVfr;mLSy%^x5p_M$sB3gIe>Ja#+M zz-t;Y!vC5qAhYx47T)<^;jydVfJaUX0|u7xj$%s5Chy>10Zqb=_OF3}B)?yUf@MjpgG*2JMC$0Da~RG)dH~VY zxKX0ZcMd~~Y#Y3w%VUCW+dX!J++pH=_%s&|;I6S!x8Kn1sa5)cn9iHHhu4Ru$Mwas zr$Yl)Gd@Lrryk+H(075I$Jy;wu@1JyMY7v#SY4-1?EW$Bo4R>38`g54q#39EY1B}` zIc!zB;%N>+*wbnMG}2ZmTzvsI=0|%u5`Md{D|;4Ya?i#$%lm^}2KG{JYJO9`55hY=t=J zkD%yvCMMiQF&64#opUG&0o>)gpIXJ4cp6i1$iJe|jNKRXCfPx6Si&~FfdIwc#MB8O zExgsqe%f~RjS5{+-M@)Ol8eTOeAkSof~A*d&eWyV>j$bGZ}h2A`N>Z1yPt%9&W-p# z%#|ijKl-Bs!Psd^7@VIBD?Fddu7ulH)HNM@@N9}*-J5?9su7p+0yI0fUYTirKt0na zrH8yy#B<-YP6&KotuE%%LVb6}t%_a$vR*;|S6S+G$K-PH~d4q{%RrqRk)i;aW2o= zq)QImsF%#E35RnLe%|ip2P8O}opg_CBmfzgI=E|#6yw?2+S~fF7J+4XnSCBk;oz~< zwbMAk@16tO9GpYl-GZc9)-BDCxflz?KW{p%+YNI^@k0Vgj?wSEOeHK2^0#juAQYny z^2UTYM&4`g8j(r8eCqpS_OU|W?*jcr-kWMZL0fy6w$P|nKLYee7DOSQG$5-l)H4eD z(1x{ryR%(Vdh37C2VTXv7uYyl)7qzPx%;tkxW<19wPMhjPTexdyo??I9G0j{ydb(6 zdllsw>mb@3xuPCOX)pTeOU9G!c$jxR(RX-aq7>DzGKNF_i0*EE%Zol`DrElQk7tDY zo-eh*?_`)yt+T!8UEC2CZM~nNyoGxW<%mF!$+Bad{Gn%?_pR^47+orZy~RcUdZIIf zJN8Ae*Cjs}j`xp68-$u0#QX=~#lD-1_yMMwT7~$RRimC7{nT|A_DlP?DQC6DoS7$& zcPiGjk87Dkw4%5lTHmi6vHJk_oiad44)jVF=DnR_K~Ta13M3O z$##6KE~F2q{}1J$0DiUWvZ{fseT{qkGSXAK>(b*MYkdvlA>cnPWSl8YpWRk<1^NB) zS;)19mu9yg18!$JGbhq$Z~Iu`)_VNx_W5AcIx%*C%W!^isAdE=gJv^w6LS;6pc1`u z^@_T_YJ)Gr#ToZ;=1}`XP}ru@>|4*&__l5eeG#)j-zFj>x&r^iyo06V8(5|h8H5Ty z;xTbNMnlTtQliuBltt=&7IBlqox|2N=b9oTCh;)HQ0HiVs0`eO-vo7f>q`7#>9agN zZW;8!avitkvOkv7hbeWY+Fw1peXJd(mJ%s4L)^!LI88v4_ha`vgY3%=bq9?a{i4Mm zlm(!9X3O|=#q#!J`j7MM_V0+Mn`8G+)WA;Ezz(njdw7UCi&wo2Y@6V2O&*#ctD9;0 zkhU%gWrts8^LIX<3;chCd}~-+#NQGB1Z+0MaeM8&w_N7Ln((B0%kq(-|Cy@)|C;`g z9;!Qo7>AbO&AF*H>cr~I`cy7ucmw(`???ZM@MpKBKSu*F4nuKQ;Lpbme|i&ZKk8lP z?+=!y-)V2;>K_`jk`X^v95H2P$R_Q&t|p{xKgT`n8aG;m^7dR#ZgD+o4^Tidz`>sHO7FVht-^zFvg zj>T};s**|BI{5!#?_J=ds;H(*=O&y*Is+=wby>0Xn76|8rlPz_s{ zQcbpJYka_mly>vUSaq9l?CL?z^rP9`k-cm)JMuZYhUW|SI_LCx{kwZIKO+;N=EmM4 zxHu;oUnf3)@FQ!MZ^tWa1Tj$Eu&LFi`s?L(gX^cO!*+$GN#~eYE zTi6rncFuXWwKH0rUefnuX1J0FY&=>s1w?ivoB&Y>c9|?4r?K?t@+sL=-EI*Z$=s2moJcpt z0!3!%E2u{aN!G_O>>OKOq1Vk~ww?i_5-a&<)}IcQ1xm%vdH3O%1)&1?d*=O$}VX==yv*g!kwoc+9BHCJXgL= z^c5ebcNxJkZVps!`}MDW^{X=HZ@PS4@pC+m&S+B(N+5L9S=PfiKiuh5>b`c``fs%o@s<T!qPaWuVU@&gVvj!&>eY2F(u?OgPP)lKla zX*$gFhTnF8x(La(`kt58y#yr^snYo;E(< z31kASGoz=S_t?UIsJy{iWx~^ysiquDi#W zctH zFyC6DWW=4{DcdAB+(mOJ^B1AACGz9=(KK zYJSZX>@_W$uLvv5P znfvRpY{IL@aM#Nd&X#A$%lXjP$1zRhY`I%9_7{)wM&3x8C&RcjNVUrn4l|S-CI(|* z?#{;8h=0Lt4oIFN5D}oyAMTJPPLH5TACDkM!7no&QTDTKK&I_yH5_43+8zJgr9-5T zLCR5YNxSMCU{E#|=D_yuWv%JL=AnOS~~M zI1l)Q(gNj3uDh^v?RD|i+%yk zhZGhx2ibht`W*4Y`-(WQWXaZF4@jAsi6e4I)?gz_mS#1TOKA6^0YzLVMJb? z502O-s*M%v5nLtUaQ?9ubnzINXd=M^DNOKrQ|Iof>5(RQ@j`wp*|vJQiwh6cvS$1d z;>!X8`R4L%v>KAJB=abU6<>1A!GyZ>+N^KUuHz__u#_mUbmL|!OLwEP8DXrnh17mD z$|ysC`1>3iOPrl-Co;=7ZakaLS53#!d<~v(2hTw2KLk*Ub z`MD&&^HhGx@%Y-;6Jy{5E#UcWGMtJJ<9%XEQjSeAW4)XAir&AoUnfnUB+iob)$Z_W z7oE%wRE8`Y4gOxL&Vv> z+BXyHQ+lWQ_2JG|`8*PYBqoDb%bO*yDlgRnS{%eug5zvC2nQ3_6FVv2N{A7W#}#j# z8}yEf{q&<*qgCM+iAo4eW^&oA;sXXU<-k6DJET6K*Z`$x~*Szp0|F64F4&Myit*UvUUyg)bAw-E+1;zw)w|=*!Z_&&-eyCRjJy|_S3ICucg)H~klD&0 zcK*PR+s#Bw9CXLuMM5Q!96z)Y9f!WvCns?TCL5t>N@ED2XB~&oTFH|Pp*3E{G9prM z`(A2leunU4?3hdbxgE2jNYtv`jeS+Z5ani!c-4xp5q&JlNIjY~3TC$1z8)=Uw=fB4=wCo@DprVL!Cs=K9Q6 zT;ZLI;$M^iWt@B-rJWIUnLS!E!D;4uYkqny7cJH1P4N~UYWzatCi2qq?tG}&RgF%Y zQcVkK?coJ8y3UqwN(`A0$9M@JnZkXY1cZ-F3UUe6^YMq+O*`;c)1C$4=Suhh&}VdA zIX9dG-O2u`XZVy-rYjP&kvU<77Qur|_gKs@H9`Tplmut;gY~>taV+V%_E0QgE|F zF;@~2QM4*@EA~|ZL!!jRs&TV3&a@~QYQ>jH))UO|(#W%k@1c_{{=0vV-1CL^`8kOY z9ev7P+QUZq#CN3cSA3LK*rXZ)Yly1(C$d1(E)6M>Pzal0iz(Ct^va3%2sG_07vb@+ z;G3MpC;C`EzDk2mOtI0U>|WuUCZ=vE#KRg-D1ksI!W66(bX%@bw0S;=O1%Rsbk;ZKCPcI zK(^>6Rx&JtF&*e)7A5^}cW5mDAw8^omg8j31$k6j-LJ#Kwf%iHch z(t8<+#SpfR@Ao$I;EVkSosH!Eb@K4}%T)fs&zSkXg~jD{{t;XMqb$~YdgXX-gD>(o zZ)6~z@g{j)1?g}pG^&sqakfN>FJHzLtcs{@t0WII!R^v4CrYz~rkTso9OMYE+@@`9 z(zc!V^!fCy1M!Nl@LqCcGgG`t+S7h%LE!iLP3cn{X^E4oN$)hxzu+jIDbv?@6Uj*Y z2#Hjg)^h){6iG6LOVq1k$Q+BPFFRXS@qXqPovpWLF|W8LW16$&ACf}I*fxPA@n5|H zA+mQ-=?Os_hL>FSZL!yoB5-C(J|!IPbx~8RewpcP`3NGSSFzE2BsPb~`RE>W9JYY# z(%6}&y4Qhpa4mZtUT}}ZkneRL+FLTR*UiR?l97Aee~=A_CHdRkT?35icR_P?>kH1d zQz5Zhmh7D^f0r<|f<98d9NQ9x=IV9(2QNvS%Nx{sh6s~Xi$35;b|P~Q=XXcK2-(e0 zp(>1}vI^=;Rf*KWy)48hnnW>yLrNq#%!DssG}r~=>Aw@zOH{M)3+;vf8kxLszGPFR zl3>^t#Sbvm>kB_lCqIzLJ$|~sz_er1?M3eo^c*Lb6~~J`N@Cxtn(|^Yu_~;H3QH^` zK)or^Z=_ksEUUbnP8hlX7L6uE(wX8S@6NOmo(U7=l}ad_Xa-owK-zH77AUHG)K?Jb3aJsf{y+y$3hvaxw0_UY>3In5$U z3=iGw`X&AZ3S-Dmh zT<}j49F+ub(7Nn7bn|?o9DdEbU*bWDkZhtNx0x>y{0Bxls(Py-S%$L}rR+Un1ImKD zi&TYS)~gLObR`wEZ6p53H0e)e3k|WawI&J6v*VruG1*908U+JWWGAyDBkx+PH>B5; z4pJmRB;aE*rnhjm$S3UGhvfgDvsLzVn9Xn@yKiJKc3Xt%mPEeDUr1Ln^@T*YMe}`l zl9EMA!TSsAgeh?+)%(EVj0$loWUuk+7Oc0lja z1r59?!Y(~TV&-Ahfapvgf&XgSiT0GCE+Aun{(;1=iC_umkI9G4O)rXsy+H$#qQegg zrj_(PL}rM#HKUW4C4C7WJe9YqbJKBJiDNdC#NWwM3%3m3B8Qv*BnLcKC9Z)@GRxh1 zRK&2gbDP+8&XT7S@5;0D(VdCU2y-9mO#Dc4r2AL|IpGA?9I486M@~wd#T$8a|B?C= z=IfkzpfkQLuNjy)%mTe=%tQ~dvtiPaUdEk0iB@ok zC?<%*^r{jnIh#(Gpm%`+VqIYl*AF{n>;Dy^T;=o z%IEOkpkNpovL8+DFP{2B_{ithj8nr|Jx+6UV)@0&PcPhc!v~o9C1+BF<5-NyRMT0h z{l$8Ij$J;?y)z~GxitZWhP|<>f41)xE>r4I5`nvXITo#*on$NL6I-(?+5%V%P(0NGihN$mPIx=@(v?}74_1^ycf<{<{jTsn zhqv(MrWd?&S%s(7h-R~DF~uD#k#pFS%=CERE87>-?c!)IgXbLKL^z)5EquK3ZAs?l zPRlWpc3_8=6WAg9I3=bpyg#$F@Gp&Ts#u!j7hOWKGa}kz;AYMb*!j=>&lC=|CaTgM z%?EPT+Bb%WTC`VMgO=^Fku$ zEG9jByi|DCCm92y)%pYMlrFL5s}K|Qj`48h65dfSua)@^My?pswJ&Ry^AXR3xGWL;&dX@@6pZOO9m86CKy{~9z)$gUl zR-#>#^~qAhAi9Dcr;o5jSsxMRUYGb1CFijVnO!qha^)zgi94YZEuu9N=y6W!t9D-4 z%lBSngcaGzF4(SD#&{gPw1}>Lmv>}0JBp-1nAd7}VvEF1{*3d+S&&m2J^4EHn9sjU zh}usUxtTjb_qg-Mfe{RZ8IeM>zzTsZ7&*;SOno80i~X<_?P7Z@D{<^0N`pj6Oa?9E zz^U8=ni;BA{kJmt+^U;oFYPeM-@#Y}70!LwB(XP@#8K%C&_lxW+;Q+;3;>Nl_$Qlj zs|A5P#GfR#oY-R`_*h)5se;Fr9tKJJ)hfLZU@gUXKsb90Z;ixDm^PA5w*I{vpF$u_ zLxe!Y#j)MZmIj0+@vMp_>GXXn7K=(+Us!!gI*(+YF!BuDp(oPk$zPCHacHl}wW+&l zdkqk1o_qa1(ic9Q<=ogdTqL0LV|qQd<*hq>h^lb@XD7mXn-HSnGXVQ3(IkG$XHQae ztdauAjkhfk^=6o}b+!ofHC(I39Vw2zu|*DW+)6C93e)-mMe#c4ZkN5-=KZ7|rd4j@ zy@%&Epl%=W&(no_qGp&UYR2Jc5It^=m*95Em@TlLyamBLwGQO;;Sax-EkThkXUl~U zq6C1YBYAT}p7#_tW%8VB7K)R?&K8-X5K2!X2IdSkOfM|FmtT|%Qr~mtYLo4);Hf-J zc)Lovq6_<-H*RINx7s=*d?jpVAuoxXadiE4iH61yQR+;r(%>w8`9)9|HM%&PknK7N*{FUi$=1nf~#e z-L(EbCdS)_5y007rB&wUsw5LUDoO5H0te=%=2x_I(M4-s7sj2j>!1p?fK~9$Zt1!4 zlgFe^3;(RaR0hA{KqUo#h7eH%yH;%WMqYM4cp2?t&%o$*FS~vMJaCB)7;#`Go}fg+x7Km!gli1;QLi>9nLnG|^+CwwC%+q5^%r;jUqP zZBA+|{sGn!Y0D+8B{@7RM%yHapjFb6sc4Z)IyFh<6$ou8QKIOBdxPe zvSyW(`={h^%wYN^b{4>&dNf_#_0;ULaCDZ8`8Xm{<%yy40Wm-x_zLm)89fXkP z2B9It?fm)UXn{HW$tpWU?VhKr&PvZMoLxf3KgFOCVRVICr>LM33f+;>-Fz`+EY`Nrdj`)gVX=%XpU z*AI8LBpBJqGDxCdMn>(#u29l8^)0Ahl=!3W@1Re0^(yLxGGa6L&(JWX(~%#RmK2zN zJ$Rn#cQ$3F^*h0_ASr;Ih8nu_pqe&0mMbZNKXZH057N+?JwgNH$`j3GrkbNf6gHtT zVerd`_L5t9;t4uVnQw=wT0`Mg+F1HaYJ8E$v5)E=4>Ltd7MXQ^CyhmF1)*0$@whMY zmJ-4b(GaD~w>PLhkk6Hnm83&(uHlOIyU1MCL*(grJE~Pp97+ReYox7vPO8c)2#s|;!M``4B@;?cwTEK1dZ-L+4c1@n>FK?l4^)0!yF6vT*XcRfbe*tRi0m2d5 z(n93!Y{VxX;T_99sn?IkR}dthJ~7diM&5uGViZC2ySk0X8=36jQZz!`k)GJ1g7|M? zf!rm)x7hc)Bag-3mFGw|CrPLY_1!_@$~06({JT18g~&wwR#P!8q67saxpRIk_3Vz0 z2=_ZqQa$rqR0&77guPq?2G@LYVLdEmyjsYxfD#JFHgw^U3UyK9lGyC2C2bkr$U9}R zC63a_TUx%PJxnA&b??gkWF+I$lf8xSNb5KfKUrQ$1^qqo6w?~j?eXhW2-W#`Lg?V) zElMGEUZH9+{s**=qD(5jf|5qgCLu*xmSQN^2O@n+P&~1Q!T9NBfmca7FLeQ*MMz2f zU!CrA9v9u!Nwtb49r|a zYe`k|vD{=u%E_kqFW5)s;rKuaX9YZkrarvk4;-PNDtF{9fTEUf!z>0XYIQ{z$M&VQ z=SOFcWAu1lYiC*H1LqGrSzY1#kkD6@kn>EmBVXZ5&Xz}Mq(mr-tr&DWA7q3!voQEN zbXb+;ZX2HPoAEsia3Wozc^l$=NTJf-qI6j{>2hu;0uw7#>`xkl0qDK8#Q8|ox^eFC z2jdgxVVleTaVDHwN8`s>%oLF&A9lILKB3u%QAQ3~+clR=68lqnNB0viqST~PNZ7!E zPR>8vWS)O`#-0Bd>N_%0dSK8H!E3V?*^(&Q?RO-%^ibk`rsuH=o>${9CKpPkzN%Bu z>Xcn~Hz$$@%y|&3qhl%eF&xA!Xu&I0vE_(wV#Lp5293+3W&U%>Ib$b1yvBZM~D+Q7uRtA`K?353r45D0QRWrh# za#f;JWIJB0p+J2{?71>F1vuiDQEDbe?kbDyL#cpJy2JvZaXLHKsa(3muS1?yQ*?oD zS`S?y{bAxLG9z}mSIteHjWx#Z2i5t!<=o)skw%o|yu#E^0!3J5ctm)w2!FSXc0XB| zYzfC3?ItF$3KcEm)1dU&@txS--q^xiW_ELZk;lCLk};*sw7~@<{lJwmI9BZP>hlDLw75Egfe;$C?MDze-8y>Ezgwl3s<^FDM|sXVIT zwX_Z-T#SPSiSN^gYYV}{Jg)?L6L+zCD++jOr?QFRq-b5n>dv2ZI!n5AZw-zGqG81V zewh?X!90<;L@?6X3Gt9Iuk70$~894JbBgH(1E<=j|^ zz)$TAuNh}xU0J489)WTff&BD+KK`r5ME>H7?CSbBt8ifLXWctyVPPYGJ7&?qtSL`* zk3+0-oY#-2oq3xtcG11zedB{wp6Dr$x`-u^Ob#?dWr*WnCzWR}<=EwhPZPSS2~FNs`6aNX-7W%}PSZC-y(tgF-*-N2IJ&t5#N?k||B7Zc{TB@2 zbInX=Zl&CqUNdEb{`uUcv*q`x`5M)V2KR9PGx3QKd|M@HB}r;|i+)7e<2PoVMJxyuV<4PN--@gT}5jb!c$&GRDPoNYY$EXic-c*zmTpEyvcKXtn2^%#egZ>1Jn zzaZu311yo9%$*L4H?g~2rXEfYU;GrAWJDHaM;da1|IHU+L~RP!&AU5VsOV~r7d_$gAK;1-`+8*56CI~+GUr5$c;$1{n8z$c7CyChpO=jT zoR}F~`0boE6ZkTWI?VpJNH9Du@@??N$<{o5!ywV*m)zLkb;M=2C&rpEdav};>hxMn zK0T^C>JMNxVk=ui5)yX^?&Nt^;a7nYk4-2uMiYB}LPF3^;jCEr;v5*npnU8jga&1P zb>>{^Y!T()(zftpeBo2eEh%65JSq83`u8PgF3sGfYAf8GrK@X2@iywoh<{y3BZo&p z_u3q(fV;p>F6G&wpIL%;CqaW_?|P7^O7L2&d}4eOC^9xgUIUL(pK0rxd@G^UUpdC> z|0YY!fA+CJUijt>r$pGn{gluDCVNgw{r&Ff-0WGgYbJ0~ZE?-Y{7 zmc}Tce>?g%>??e~={z|P9eECIm4BGk*)v$s%Md!aL4LCN2E9G&V`4Rx9_V&RKC;t2FD^$I>#1n=UJfj+Rwy z=A?oHrZXNNCdc`M=)x(8Xi3*X?_=KB$bpIGxK zOqCpo%RwkMF-UWxl#}(RqXxFO%Pd4}?B_3+X^Bqnt-@1El|!OWg_xlOLI<-S6+d!ghkg#m0%qvr!+rss7?-6fvgjlPovQ zBggT6>^eu`<7-B>tM&E5$JZW;XOO+DhD#59Cz!plRpU5oJ;vMhI#qXM>TbD0*l{Sn zo6(<-H54w#3g(R~jpcmT9e!=Rvt>PL+`o76pG!5^WSqZW?11QS4_{4IEAl;=dzg!V znu|%;NfEu`=$`$hg-TE z)QyqR#4PA7RUgdNHxj!4!+q^`?2ad!o|pfP9}Mi4^Jd&f7WIvB=XZs^BCQbm(9T`s z*bKmifCU-j21cs+SvCrsuJ%%}OTrU9)l--o94I-1T?ji_rjaYM%=TvE?nJ>YT}R~E zLiZSF>s4a#$HtgB#C(`6nWNcN^qFx3r={jQ*pT21Un((W-v)U_XB18kCe*#(+-w&) zFcleiNzDE^+3|WtH9l0MRLSem9a)9qp3UfL4$5Q6xNYMIlX^k*%v>ibolR3=ZaMNW zFEZ(*Xyz&2d@eeL-nC&a`X^lEHhKxwe=i}3eNC<^U9R_sO+%7c8RWJ$PA` zS`eLoSPbL&o_rRH_C}s1nkTYTguZLf+xSKv*t5?YyZkT%`~z$(?05MJcQtVi=<*MU zftAfpSEiX{ny(Xs&@A`V-QmHLLRWz=naUogT?VHHZ_!TqNA|>%k%XWTo@nu5_nvrc z?pROc`91qQBfITsm7I_i%BJq#)1ORgk7L~zr8E>f$Yh3q0~9^+XVmPCDVnBI#&*~% z>*DMg=}L@I`*)q(6w~Wo*J~KLY^;^pj}m7|uRF4fg93F>{JzKeFy}eD_QY8`431Uo z^+cXv1LmH;6U@%;J&DLHrhkt(ls*jaWzWN9XEKbSn+sj;^@BV<4f%9FQu@6yUHw^} z$cXT} zfwFy)X2&`uMV;L-$ zy<%s|MtNi9Rc*P7Wkg(*>fdFXWT{88kqw&5%uPtL=reulZU(`nFfVoGz8Gh&!Lc2i z)p#qL;dv>{{_ptd5VONLpQu}Dd>kqx2W@Yd z=R-8DBUr2MNN4=3g5XwAs81=hr}cjYZ1&^p)L<%QqH>*J=!vffltm}yv5Ac}Q?Fd2 zdNr#>Ay-1l@vUCRcAuk%luqjwAZ1^0{Hs6qW0pE2I{As8Y@ueo}Jp*^NmqjM! z4cw8EKi`hbJmImAO=$zs>UIUgB5CFi#syDhYNZpo+u_cYsLs)j?TXJ(WPZ;~eM60Y zi_3l)dHf?;@jWi{1_^U>+#>+W!E&$HuPR?MSH#RFJWxo3p@38!Y1K#N3Y(7J)c%WW7 zM~{dgap53SVM zf^y^JD;>0K`DQmMn!&w8x*!ALpV>l;+wto!Cp%WGl#@?)6CzD@6{{D`%Fq1%;Ml$Q zsCKeI9r==(v{gQ4Ak5oq)ECL!>PmZ#^3l>{@1?AMNP6>YnPWRudS}Zm1gTw9)R6ZZ z(ve_?wWydh?=sGZRio!5eKG=OM0I4eDzBtS$y?ikyo=3jrM=IzGM3c`*FbZ1yA_}* zfi_Dea5h8JHBr$Oip9svL}{ws4}2d|+-;7ni2L9GBQ_5lT%bn~MG%7qXx%9%E50V0 z;ARSwMLT3+LrphDlZ)FKK!{>P1>*)4IPV*Ih+%$zFh7fz0ex?4Wsw6a zBN^^|+05wqfnShnpu;vpVcpXy+?Orma`;;+AkGMga>r)^D7>bhp;0udBpsWwlxlK% zo@Zrm(U7sR z3F)yA?ZUp`Xtj(cQ-I8I$y{8!>cJTyj`^BYyXro_{|i#a#*QO|Wuw8d5%c+G>2y#M zOm94g0u!a-g8zsZw_0R9GnUv%w5j%-q~~`MLD6&7ze7*U#YuV`X8vu2D0+_Ei6=HT zp>^5QNtw~&eL#~hSM}WSHn{^%+T;X$PhgHG~BIWw}$s=xJkpC zHC(S@qlQZ~oTuR|4X0^%u7;;-n4#f&cj){z+^u1^hWBZ>NyD2pT(4oHhD#K*^piE8 z!Zo=lBP?(BfQWT?y1B=LDpnJI2@ma3+I_qj58E71m(xdu&j-g_F~o!;Qjh1XL1Tc; zq+0*=c*gSle6mIcCtRk>W5UF*sn_IN_^W?ga+k>kb;+LkaXso&$$y#*q>*@iZLcEI$OWj&sMsjaLajSyrJPkpGQx5 zer>^b>x(<4e)#3P-tM|Gcu~3>MD_CzHT=1TcWHQ^hTAp#i-vIx-_!7@h9ka|j{m*y zeEETUpLlBY4|f^Oi@mqs{O*wFZ6h{Kc>iw|vo|DGymnJ?I65=^*FM`Ur>jhtSHp^V z|6cjiv8A3meP;SC8o%|X-(}J-V+;8ec+Gh2I^Xu`>G}5S@Pe6Uc+o-=_Uq^J8RoO= zt0pYa&+V7!c*Q2ny;wi%{5w_dhVh%Pn)LMf&G+pZ_E($FI@$@rHl5z3(>3dK4ORbU z`hQVBF3pb$4Ttild7YV$#h(t%m;YJ(>Himc&93u=NkzErj}8uYa>tI&54tLAYwP`$!GLQ) z>3mmBT~%P!6pG-N_jO+gy8Mj+dFiiT-cTC|*3{Rz8tNN^Gh9XrLv4L!m1|{9T~odP zI!PThN&@jUlstc@YAb=d2g2FJ8{5RKQi94TbDg~acp%p&# z*$TJdzj|-QI}yB^{H9O&vLbz9UFDM6fGb$l*YNiEtS^U+6=f&XdBy!s+y=sC<>P!NjW0wpQh zLDiJ1(e!`sOgHV@gBpIK;qby^?Otn)Y{&2gEtx*8{~PLSq-T(_)i+dr3*BvM@Gs~@ zb4@kAJg~gJaWy7HBZ|1DdNu!D3m5u4W{+HMB3U3^xlX5pToW3k7YMFyK-o9Bbagcb zmWpsUiqanvU&UNr)3m%Y=wCL)RUB$;6w%ZOCeZ?|q*mAz;|r5qNh9YASWL+WmFk!f zm8Uxx3@oREM93_ZbkMc6RJCHrP`aSJvR0b^>STOH{cP323YuiwT~^t&%oS8-4Wu`& zrf)RmQy3=Ai?U6DOv~`mn!{wLf%>VcU)f~#(=LB~6^yH`QnKV$eMd1FEvfWh*JMm+ zn$TcOvFV3&9po}9L(t`KtgSXC2kD5LYLz*a(i^BAu^Z=Uo~Gk3t_@Vyg)kwjDwkI- z#Tqdc55@P_H&&SiG#2=4mn~ixs;M$`g*xST-BjE)8JJDgB9>xm^Z~`muHR5tpTX-i8;x^lj| zbne{C7nbYvDxG~)qDD0-M2(eIHT5Vd!#k^VVabBIa~F6QD?Tiq?VIBidifJk5u$a17=KLUG)$o3b`jBMNCDPTm}2_ z!p*Q_@tfi48O%eStO#f<>3l80Ob#?+{`%@L>}6;)-()mnFH2%dRNL{?H2J|o153W} zHCcg`S`{Tub`%D)FoZZjWc4t#vgjqFq&rCo)WwU#eak11LxA za91)*)!)KDsaq3UO6pSS)KKu)L>LP7*M%Bf*b`N?0Wg$^;qIyqpch5^qUEqhr1H}x z#}s5!bkgV>nvYoyOMH{rs8ZR>H8h>d^O$5__Z&}Y$-hoFG@ldp(@T<_W~whvlB`?5;(w%I+i7W|(wXOh}*-t6MJWo2mt#R*kYTxB7w z56qS&f#Avj#@SStSQ}p~NcNk@nvVZN_Un?MuI~xcrc7HkzTqzA$BDQ~O~*8A&gX;{m}C^cr) z)-Oqu;}f?thOcJt%21X@q3BS^L8fL!7+XMBK!)LX-J#$za;On?TxFD2HU;OyV&z%b zM2PyO+Nq3PQcc@z1?d@@osml`j^nl0l1Gr<&UWblzPQ<8Hsb`3}8oSKxG zF+*v8uhLlB`jrMcq_48(aLgB}&MG}T)wwC8Hgsuy)^QSf%uY*6Y}k?|sW9IU#X6*G zOW`FuGllBHQsF~8p`mZAuEs{fknSm!BE9EHr~99Dh$#eV9cn7r9PHRAC{>+`Zg{h> z`|3{gvZT&5I%T$P#*DOdQe})>(wfg?dPl2aO%9opmdR-n8q?&Ox^IP;;KUL&MckOY zObM~7rPRNBO3ZQ0Rt+E1@Xs3V)$kP!_iJcjum)77f=;JW;uOs`@n5RpTn(-CvPNNd zR`>~^wWN`Jtp*Pb=qPN?5aWCFHEKy_sj?RY%xasxoWxkC5$!Kl*Mx>iuGKYxTA7bzF5Vg~PiU|ZPn1DqKJ!;b zoe?r>`Qw4EXbwHS%u830&-H`GR&F6JZpFO<%rW41v;(bBmd;r)>{O?h=YjAlu z4|z-27L8{c&sDhfIKh1y(86;iii>V~VAWZ_8S5#3XWz50e)e+D&3`Ey{?vhH z4|vt@;mF<}-M9O;nP0s7mw!3$ps(=ayFc)J?EJ;FpZ(~jr?P+ljS0o|_f}0D-fJ~m zD_IKa{7a=@5=R8&wR9L?ce5(4LL}Wp;L+9`Ykt_ zj1`98Xf%RMjZ~=?Xc#M+8ko2WRxdUu5v|C;{x7HNTbJGsU`45zn*M&viuC8s^V0Qh z=aBVr!$^;xdqsNuq0h#p>F>8_{ch27|B(FJhrGXa$g^>IIzD4aKaoA;Id{mjYshn- zD;?jKA^m50bNYL0e}_eX#Sr>d4SBw_xOj$ZBI{YxCb?#XYHAt%tag>nziR$8QUBkh z=g4`%H|V#{ZN*pUT6h?d4Pb@Torhw7>ZOUjJy>4WGjQpKu`8FmiB4gTs)! zoS)*Fkxcx3z`WDg*Mq+RI14use>re5ZW8_qU@fiye*^GaxQp;N12e`L#&rC-z=gP( z5+C?;oELuw@Il-n{GGtdIF51+{&HYHH_k4@FEB8Ui#zZ)054}C*DP^>KgQjHzXRy` ztYO?H;lR6bcjDgy{0O%NzwtTlTET6_-wb>Y*DK*?vLEa$=4tU40E^Bxj5qNMtQya# z6@LTp&$#Sw!4u%SxLo{(i?tWr82shH;pY&9KNmO)S0LfQe%ws_0v+d4cKo@(FXBq^ z7XjDe%JDY~JP)4W7x)X@GW=VBSLDG5{1w3GaLxGpfg2_mMjQTi;3J=hC-^&oCvjED zZTNG64=^RNMZ$r1pHJEGZvn2KWEgw#w*w!)kWn}OKHz!S`q_=}1bFpS%8S1O*z`qM zhQAqj5AGuTTYxWeM0qCue&F-$%Ud9R;Af{pKYkbRTHFozD}dj{HREpwPMiV#_zQp~ zxLfd-1JC@bVcd$}1w5ZaUUy0y;4QfO@pl0C;2y@`4;+56VLXOE7ubON6Mli~E}?$$ zw*#LorhX+paKmh5zKL+)UASEQTYv>#>J-1g%Wy9I<-n~zXvNz zGmNuvRrp=N2(A`?JMcN&4fy+k)pPjPk_zaQvd3SNm1`~Y_weq$Nm zdBxp}zX14s-2M1FfZwX2zVJ5#!?SE6$6*6L@wVZ4kc;cwRka#9si6;u`R` zi@$*~iXXTMcMJY4z^rc}pZIfuPvY*x-v=yhBoF-Mz`x@j!+!wi3Q~{w1zv;e$6o>b z74A*^TY!JW9md}Y^oFQc{N=#4xNN?a)(pHKmxI3(_!ce~{{i4RE8r3S0^ls%IQ-?n zyK!gY-vay)Hxa+F(lGu5cM<*y;OAGtAN&Qt7;X{%cHl?2Dv7_^Fn)?_z~2F^xE^`N zFYsHqoA5US3%||m7XBjOt{ad6{C&W)*B}G~!DfK@*+j9=n!0Dk4C)Q7|Y zZpJ-^e+zK&ZIlat1@OE?^9ovx>3+_v6OkKLGUHMcv_d z-A&(zn~1+0_a6$YoU<9`ce>?EKd*K6qV>2?d1wP=f0QTX2iC^F)zoky`7XiC)k4ZSN`#$1{ zA9xhE7r*g4=)%2-U*KD~e*6c34foR)@C&>fcL4tu;CTD1PHX z&Jp5_)#L@-gUiJ4dI;IZW#caZei@g8zX*66E*F0X@TRTs8Gk$Q4O|}n1Hd)EN3QTU z10VYX^?|<+c>2SX0ly1)AMP6boxnsF@`3*V@W+qSM(}q4-|j(gNI0-(7ktCt2mG%m z;G4t&Uh*gMz+VI$|7ZAy-vzw&Nq8l3fb;jD5Ajz3C%;75@fQG>;>KJLPk?XY&cuHJ zc>2r841O2zzxpW){tn>8!;}So0r1=-&@X=AH*vN28-Tu#s1N+*z?*S5;O_uN|A9Q< zZwJ1QYsYUK<-!Kst@t~E-}#s_;%^52^%HCl{QbZy4TsT-zXDk2a2R{>HvqTd-o)Pt z{Lf*~j=ux=n@oq1`E6tX_+S=tioX-M4|gX11Hf^^9fk|P3s{27!(R?uj+=r#Otq@HYdqoX7(Hf?S93BixJlw*Vi& zy@~(-?w{n~$Qfbe%*rkpQ8BzBt30zaqmQsr!;MkXh8JX2WHt;d&*)U~B(B|!$TUVw z8&;4};b?GVXBpYEG7E-PWHdO+b-a<8#z@Ckjf?_Eg+uvAoBrX$jN!8+lz1nPFiv*3 zj8sm9NxjbkW@Q*z(;Nj3dwdC-N7&H(?6mMTg532v1t(REY8Y9b-8sB3vp?ei_#{rF zrgv!hth}Ur_WXv%v%j}6?VLO89mY%4ZlVejI79R7*RgF zGqW$FADV~eCp=hk93Gq~+$z7F4vD+zI5?z?OUP5va{jTjII|6>;~c{-QI1oxjZ=1~ zQ>|plp6`9S{8k<8(=sAu+ntgSB^M$CR$4o3B>c2d#%Yd?#!$*lSsRxwYl6GchFkKs z^T5I_yyzZ^k+>?aGmg#6UdNV943$|s?pY_o{lKxfEqub$`-bq;!YO%e8qyvWt>+wz zPs+CMIQffwFVS^i;kC=W-EX~@a$9XoXjpHfLF9RpT`DxMr`g)%JX>81<(DOQqq2-q zjxmOr3LHaaU+QpZ+M)Gqr~P>COv087q1EEM;9g=Ym*i*VX%9ahj-hFf7srx8i!aBc z&n{nfc@_NjdbgL;o@QtsRvGNjp4RRk%74|~$ELS;d;IKSMz$l{;V6)L5r3PZ{8A_O z`j)&UUALAwDT`fxG=aIeqV@H*Y9hU69J4f_o^$$DXC_ko+OB$gLwB*7}0>JL*f6X^BpmD%03e69~67 ze=6KL;!Dn9$(GTn>?vsrv#c5i=g>Si+2qjjOB_qCq`eH)y-F^|9xE4CdF^=!&!qfT zDeQLDiR5~CR(fA6IP5sBeC#~8!iJVt!llg#4Hn1jGz^U|edDwteWT=mJY70$SgJ3v z(`T0xt1d12y#GI?c4$Cr86S{r&IBfn{5y zPR-v%k1&?~9qzZOu!jlz<^7CR?@NWfNLa@Mj8ko4<$V08kFXQ{e)*vJu3N)C4fkvK zk%p%}WX3yR!R4ZDu~FYM7_tMH*hF;Svokey!5a8#KIA!}~NeKc~XHs(yY}!SUquuj8O8m`yyCJp~n!%Z6AtKowhc5C=2 z4XtwT)zAAiJgi~vA58ks)zFGRO+OcDI8Vb`4OeM+vxdLauv5cb8a|`pJ`MM4cv!=X zhc*8+bZIz6!8M_ z{pvu={CQWFa*sjDDt~RLNv`hrpp9S-Zz@s;-Ha@U`DTH-c8HaK zhbO>IMuB2(HQ`KJ?R!Pk=LYRru@c zuBdFRsg&cjH{0J1q58+RcbY}U=3yLwnpHf%lwI4V-I$}s+0!U9V|OSwFzoKxU( zRVyrlZ5yl6@O1aUgv}Q?TW#ei$dpa`Vd}Xa%uVmz9mW67AHG+0Q{gpLUfnvEB zV@@Xb7c_)8z(o%8)#U}GyE?O+OC3&7WTPi@9`#V!6gZ}4%d_SOf+i>BESQ|*m-BMQ zomum1xw*x-%Y?@;3Pi>OSqlPhFk1eP@m+PQDiJ$ zytvlCSS}oBK%5rKk$a<9LYFUD><=|A<{}<5&{#ldP5l!6bc3*?7Y|#!qRDT*V2_PF>1f#Z zwkO%Ye|Bc&5;-Y)L1tyJzQ!;v%tQth7w2VGQ`dgu%FJpJ38N~rT3x`yie@GeDr>p9 zpl$_gUzy8wirX?dwB~PE&3cshxHW|Z!Au&gLindl?z`|WXC(Br_$XI>BddAhujQbq z@vQjjB*Kx*71b8?r)915H>vmMi7!~cdM{As8B)agJphWnT|ax}2`m{2(tc%^ZfgwG53R~U1}H=nDU zjH{K8LsY>Kyu8y`!Yu;qCNlhT7lA&aco&!8s5838-Ghb#>G4tS3&*vIogn7ObypGX7xHs(W_Wtz^^% z>z6ed+YQ7(s8aDBGn$r#g51bKeLrq!Vx&*c686Yk3S(-x;VWA@#j{{P>Q1G(wvsH_rL=Vi

o9VluD64KF7~bn=qTuz-ci(1-m$2oqGMS{L&vI)TRZwX z`a9ws2RaUS7`JENo_qVa+g-O$yuIM|=G)tDZ@>N4+xOnyfBS*kjZN8`ayPj)P25zl zY5Jz3P2NrAn-*=V*tBd@!=~mQI zcPzW3;f_^zG~dy7NBbQecO1CGxHI=o*PRpZEV#4iPNRVRKSJ9*6YnXwXZk%xkwdn@ zvUxZAuH3uE-Q~J#;#~!IRou1gu7328W+k9{Py&d;%xwrG)zI*%c zJ#eqFId`*bbHV1K&E=abHaBc;-rT;qWAm2HotyhM_isM18Pi$iGfwe9GWh@YZ%Q>6 zmQutl+$V$VC39d>FttC0QIh?a$%F8p9ZyDvdT|Q>3x-)CjI-2uq&yj7VID<1jd}G_ zxeIDCIV=gN6$e4&!c|zQF!GM~`x1c-Rvg2^{5e8R%t3!G*YIFkH`wVe<$d{Zi$cNs z9}+Us9)iJZG&t=c>j~*g3;7`-QutKa*%)b*r-f{jD5LFhdL>R;$X^NRPYZcNQk`l~ zl>rBe(n8K5q&Y3*LPF#mMv9)uXmdMRt+7p0sjmAw|#k3F)6;59yGQ&s!lD=8KY;C2tlZhWvIA;+lA3 z3MKk;#Hq02sPu0V(w-KQfe;s*Z{;qL&LE^bE#yK%To>5m%$8J>>>=_ogpRb3^@Q}N zh1^C+?qqu^`5IC~TF8@xv|nhC^P1rKq7`ECX9koeIj?1&yFI6KZ}{7TZG_329FYiAhL>Jx#6H350A(3;7x$ooOM92`LZQ@zfF0oEEZ%kj}J_7$FDJLjH@8f@(XS&4iSv zg*;8jmb8$ggcxZ3R7pnwR zEO{o%G4ecXL8{029;vp_A*3+hMM!_h9`Z0DayB9rr(Z&Dwuc-g{qbF&kZ)2aGnhdbr!`!~wfn#8lz?_XY7 zQ)frQyBXkV{_%760)3-+@B83E5;jBWBcV6p1`U=3X_%B#$+5Z##Sy~7N~9DCORb> zRvd&^<1sI6;Nw@nQH*4qBv-d2F*7J&lDu(s3Nx>a9_W$tNlG6c z2eU=t{dPP(o3Z6VMHzZCL~}h-=ck zv?^F%nN|^t7ppL%qwmDITXpe73eRy%F`3*8;wQ!e&B( zRCfNcd0y2Wf7*hS0R^wnEUjoUgSC;MB}*uTl|h`w5ThhEiIax$$7AR9Yv*nJw( zaoYW1S`gVJDzAcKtw{C8FIU>Y6$k~2ivOXDs6a-Vv`|*?HnFA~Kg_z8bSSXTx##{m z?++JVDXpLHbM86!o_p`P=RWt|b3-cOGYB&O-#Ib_Qv5VWCP7vVG7B>Ivz+EWkhP!Z z$O9ll>pAi;Nd1>N@(9R^LB0yI_*_o&ILPw<=Eylo^?X2rR-cY#XWX8ouVnS(kzzfh z+MuWWTjaxPRudnO6x}a)=^OC*N#qlQab-E8r%;N4-+|AwkYK^7#zN*p84r}7>@p-9pF>yS$L^uR~X#;Jb@ja!*qzfiMgRFvVjYwF{pN)v5dJ394%}!fX z*nQ9)X>^wWr^q8h4qY!e8xi62Kk!+M`UyJ|c14=7!hbFzy2}9R5h=^rN_jVR0YAFE z|1x}Uj&iDCz@{v!Y`HL+aRkZhd%~cH08SL zHMIHIS|t4qAV*DG{WFk7L-Q9POGf$$kS^0N?g3ednitLsAnQi@;~??{N=s47lx+WC zu|ld;?@vNAXHq=_vdfg~Igt6tM`T@2MXZ`UzY%2TrMxfS1hU2Cc_+w_DOWGZx~ahr zfyfVWzIMhyx=rnT1f*{A^9c~!ca}lU8d*<(*uL{45Ya}eljp}kB-Ru~ z^E!M&_?S{W1+p#X5W?bFkiGmr*|A@PY%-ePd=>54l;TYwYbMY8K^7woI~sdrQ{7%8 z>qF33Sv8RPSQllQVy@Wn&?BO zPOk$wAKN@4ia!TgFf@Z8@=MZ3LAp(Axf5i~=;uC=#m#wsJ_)jFWIY06^?VlOtfBcX zNMXwLGmx#OPOn7HtZ5rtLFAXblv_cTO)Vb*SulC7gRGif`B9K$?HB0GH0X@2@?5;^ajYPLGAA{ehy@-L0(0<=SYP(-x96|S^0FX!<#_5Onz<#4CaNYv4=C!37dmG3Z)8lRhS&luWlusglc0)q)ypQ*8 ze%^PWZ?N#|kj&RPj^_@~u&P$6l;OhKa#U_NWvO4l5$?3pt8qOnyAMk_$JfvsmNhuS z?io4JmmzXb>TY)@yNjD3sS?DcXf>BELHoIKlS6e5)pQ4{r?MwG-Q*rQfX9KJ@$l9^ z?%^ua{B_SMj;^J1hxscxzSc7;M<3(KN%#7o+`{`o&M)S~2TqQRg_Cts1}{zy`U$;W zbgQ8u-0R5FpT--kjO$UXL!A7n&VQPhARjw@Oa_FEuBa5>1>!pSU=9r_0U3=}XS0~v%=r3Q-Zb5fvYPD{BU3yZv z%Z_^rMHuy6RUbVvA>v@ojI;1sL|u)?bE~caAC0n4o{xi5E>jF<0{0Arf2eW z|LBn(718=8T!(yMislz?tX5q)d0u#UXL5RYGW28vAE{2GTLaOJI}n>UF0aiA+p|+N zO8}!jc3D*GeGPZbb5vh?($c4Adb~W4(sj^s&s0CRg6iM!~^{IF}shr|Nvma*s*<)-;6kn3CirX!sC&IBsYCN5c{n$YYn?sZ`*wgCh zFo!v{PlSWi!9JG*+%Z^jOhQZRoK30?HC8w_Ej7n|wymc@d2!psh`h`1n@niut13Os zA%6gNY9d{AEw?Q99y%mkTRO1Wby(z2w@Z6=_FT5zH5dd(rphtyiX9#=*^Dx6kxD>L zPr9WDXMjuoq^AjWQP!)q9XsDq`b!ZRV&%ZFdsCL&mRP!7IqV@TIf+`HsGfF0kZNCY z2`0m1{$6+U4DLH53$Ai)dy(&8yGcG~FkqA%^{$Obm;kwKaW{w0i&8Em9q0{F0PN%L z)9!LxHua_XI}B3{O~-b{91WwuI9Ef^iAG&a(bG* zR*pb+7CNC-F6u686Vi3daXph?%CXyq_$|G%Gs_vdNFP_^BjWcs@jQK3=~lYSJ==SD zoNRiQg;v>N#H%|`PN8H^H{Cd18=dhFGt*pTdAEPqowM65>HUidI*U}H3aV{Cmf`-- z`qZgggv>vV=R#Q7_V7q;bkzTOYq<}02 zKz7tVMF8j&Qz1L|Pf!80+enA6Wfbw}<7<=epw{Splc&k4(B+56$W2L}80_m%^P zdJpgEFAw(a>%Xns*LP?T|GI`{RnsEwusF=Zz^Va2w@nZQp>~W8&wo!;r*U9+Y%hl? zhkDwQa7=UP5XoXXijNV(n`WOh-a2y0)}F!sgCQGUY^$FA3-orPXo-*^67u8!?2jED zNqN*W*LeSg$Lt*IzqCW0GJ zJY5~F&rIJ;?Ec8h2lF_Z}wVWzA%oE7#WD4eVX$@s+G z)c8WUnw5srvfS-jPI0;84EB`>784Wf!V>jMZ6$Xwgdy?8m5?3y#WtpigzAVTH8~7s zZ^Fe>DQgYM4zyOd>}@0gk{8;nk;8#CA}+*G+=TN{RKU1QHc(4NX{9{?t4 z_O_PRq#J4SD|yLM>6puYeQk&GRWwc@UDHniI})|dMVp$}bRAxAk4?+(uF>444n$av z)D{ZV)*-e^D9P{P6@?_N24Kv@dI)r5I!BPnfph+Rlk%0CSrnAgRKKDbH8(B6dII;2 z?To5LhIFEXge13Cg}V~27J^U;-a8( z(-$T4e&-dqaou`xb<6TtBlT648H2xM0dw??elDbGbhfI!2lm7+)?%V!r@&TWscnaB z(z-emGXCMkj&Q?s$ujaZ}2f({;&~$#CQ3RLn26-tZoQ)P@}znqv5bZs*LSA1owM+rUT0a;KcV zROQe?WQ_9mUX4QCe{xdox`~JX!Z1@AXW}$4K0MVX-YVks*oYzpro+Aci1)?idA zQ>?cRN0o=ICMwNs#KT}}Tq_*D+Sud-Zl!BSe$c?C%4U4rp5X?em7G=K8g;%~G&O1; zokltPO^Etn9G||PL0@7mUsg6zx8HHOQA3w;Bzgq-XzP7~HUXxx1bVcp$$_AsR$VNi z=0a%*oy-IB4uv<(I-a$J%hF=|&5>oCqCjZCS1bFcYfIl0&I7%twdOrG+=+pY!&#=~v=- zsEpiB3H>v-s$rlRpPH(>`C1g4M@^&uw(7abiAiN1$)=rUoCtc^;bTv5_uCi;H9F(OPxZvlx9XcDhw4LU2S;?CHVR7bcyKgh2sdbpVhSp`jIk~Uq z?HZ$&QMVbj9S0fEvglq)-V{~M&I*HX?%a^vHCKx;lQh?E?!!DW3Q+)+aX#~RJ-mGcLPoU>3?d5capCurXS>jb`Udmi4lFR- zG`TIVL`@%CLbs~P7q-lwY0rP0K$F$!auf$r=9tYED70**z-f?VZhE3IbE2GR$f8i@ zX*5lU-H?|!bntwf{&dHU7B{QL)M4$MHvi-!I`D^1G<{0cVj_IIU{Gs`0O?gt`yn=> z9d>A4AnGEfo{~Jp@UB_o$}$|V(5E(D)G=pxa!78j@Mw&* zGBuNmjE8I{l&J#KX+F7|32^(lyRvILfoU^GlR9hm38SO?Qtp1prfqxeSgM;DN{T=7 zKAC0dbHztt)F(3YeirDcQxiE-RN$vs{%eq%X=$3=PJ}$Uol$oqA{gG_XG-=+5Uu70 T0^7`neV1@w;)30ZGqwK*)4n6q literal 0 HcmV?d00001 diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..e3c902838daa88a79277fd3fd37f93dccf7aaf44 GIT binary patch literal 109672 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 S-M{ys=m(nc@#il{;C}

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 T0^7`neV1@w;)30ZGqwK*)4n6q literal 0 HcmV?d00001 diff --git a/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll b/src/vfs/punk9cook.vfs/lib_tcl9/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..e3c902838daa88a79277fd3fd37f93dccf7aaf44 GIT binary patch literal 109672 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 LUP_RETURN_ADDR + set rec [LookupServiceNext $h 0x100] + if {[dict exists $rec RemoteAddress] && + [dict get $rec RemoteAddress AddressFamily] == 32} { + # 32 -> AF_BTH (Bluetooth) + # Further we are looking for RFCOMM (protocol 3) + if {[dict exists $rec Protocol] && + [dict get $rec Protocol] == 3} { + return [dict get $rec RemoteAddress Port] + } + } + } + } finally { + LookupServiceEnd $h + } + error "Could not resolve service \"$service_class\" to a port on device \"$device\"." +} + +proc iocp::bt::device::remove {device} { + # Removes cached authentication information for a device from the system cache. + # device - bluetooth address or name of a device. if specified as a name, + # it must resolve to a single address. + # The command will raise an error if $device is a name that cannot be + # resolved. + + RemoveDevice [ResolveDeviceUnique $device] +} + +proc iocp::bt::device::service_references {device service} { + # Retrieve service discovery records that refer to a specified service. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service - the UUID of a service or service class or its mnemonic. + # The command will return all service discovery records that contain + # an attribute referring to the specified service. + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service discovery records. + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::to_uuid $service]] + set recs {} + try { + while {1} { + # 0x0200 -> LUP_RETURN_BLOB. Returns {Blob BINDATA} + lappend recs [lindex [LookupServiceNext $h 0x200] 1] + } + } finally { + LookupServiceEnd $h + } + return $recs +} + +proc iocp::bt::device::services {device} { + # Retrieve the service discovery records for top level services + # advertised by a device. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # + # The command will return all service discovery records that reference + # the `PublicBrowseRoot` service class. This is not necessarily all the + # services on the device, only those the device advertises as the + # top-level services. + # + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service dscovery records. + + # TBD - add a browse group parameter + # TBD - perhaps check that the sdr acually refernces browse group in + # the appropriate attribute + return [service_references $device 00001002-0000-1000-8000-00805f9b34fb] +} + +# TBD - is this needed? Less functional version of services +proc iocp::bt::device::enumerate_services {device} { + # Get installed services on a device. + # device - Bluetooth address or name of device. If specified as a name, + # it must resolve to a single address. + # Returns a list of service UUID's. + + return [EnumerateInstalledServices [ResolveDeviceUnique $device]] +} + + +proc iocp::bt::device::print {devinfo} { + # Prints device information in human-readable form to stdout. + # devinfo - A device information record as returned by + # the [devices] command. + dict with devinfo { + puts "Device $Name" + puts "Address: $Address" + puts "Class: $Class ($MajorClassName:$MinorClassName)" + puts "Device categories: ([join $DeviceClasses {, }])" + puts "Authenticated: $Authenticated" + puts "Remembered: $Remembered" + puts "Connected: $Connected" + puts "Last seen: $LastSeen" + puts "Last used: $LastUsed" + } +} + +proc iocp::bt::device::printn {dinfolist {detailed false}} { + # Prints device information in human-readable form to stdout. + # dinfolist - A list of device information records as returned by + # the [devices] command. + # detailed - If a true value, detailed information about the device + # is printed. If false (default), only the address and + # name are printed in compact form. + set sep "" + foreach dinfo $dinfolist { + if {$detailed} { + puts -nonewline $sep + set sep "----------------------------------------------\n" + print $dinfo + } else { + dict with dinfo { + puts "$Address $Name" + } + } + } +} + +proc iocp::bt::IsAddress {addr} { + # Returns boolean indicating whether addr is a valid Bluetooth address + # addr - value to be checked + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){5}$} $addr] + +} + +proc iocp::bt::Uuid16 {uuid16} { + if {![regexp {^[[:xdigit:]]{4}$} $uuid16]} { + error "\"$uuid16\" is not a valid 16 bit UUID." + } + return 0000${uuid16}-0000-1000-8000-00805f9b34fb +} + +proc iocp::bt::IsUuid {uuid} { + + # Check passed parameter for UUID syntax + + # Only checks syntax. Not RFC 4122 validity + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid] +} + +proc iocp::bt::IsBluetoothUuid {uuid} { + + # Check passed parameter for UUID syntax + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-0000-1000-8000-00805f9b34fb$} $uuid] +} + + +proc iocp::bt::BinToHex {bin {start 0} {end end}} { + regexp -inline -all .. [binary encode hex [string range $bin $start $end]] +} + +proc iocp::bt::EncodingTclName {mibenum} { + set encoding_map { + 3 ascii + 2026 big5 + 437 cp437 + 2087 cp775 + 2009 cp850 + 2010 cp852 + 2046 cp855 + 2047 cp857 + 2048 cp860 + 2049 cp861 + 2013 cp862 + 2050 cp863 + 2051 cp864 + 2052 cp865 + 2086 cp866 + 2054 cp869 + 2109 cp874 + 113 cp936 + 2250 cp1250 + 2251 cp1251 + 2252 cp1252 + 2253 cp1253 + 2254 cp1254 + 2255 cp1255 + 2256 cp1256 + 2257 cp1257 + 2258 cp1258 + 2088 ebcdic + 18 euc-jp + 38 euc-kr + 56 gb1988 + 2025 gb2312 + 57 gb2312-raw + 39 iso2022-jp + 37 iso2022-kr + 4 iso8859-1 + 5 iso8859-2 + 6 iso8859-3 + 7 iso8859-4 + 8 iso8859-5 + 9 iso8859-6 + 10 iso8859-7 + 11 iso8859-8 + 12 iso8859-9 + 13 iso8859-10 + 109 iso8859-13 + 110 iso8859-14 + 111 iso8859-15 + 112 iso8859-16 + 15 jis0201 + 63 jis0208 + 98 jis0212 + 2084 koi8-r + 2088 koi8-u + 36 ksc5601 + 17 shiftjis + 2259 tis-620 + 106 utf-8 + } + incr mibenum 0; # Convert to canonical decimal string + if {[dict exists $encoding_map $mibenum]} { + return [dict get $encoding_map $mibenum] + } + return $mibenum +} + +proc iocp::AddressFamilyName {af} { + if {$af == 2} { + return IPv4 + } elseif {$af == 23} { + return IPv6 + } elseif {$af == 32} { + return Bluetooth + } elseif {$af == 1} { + return Unix + } elseif {$af == 6} { + return IPX + } elseif {$af == 17} { + return Netbios + } else { + return AF$af + } +} + +proc DeviceClass {class} { + if {[expr {$class & 3}] != 0} { + # Not format type that we understand + return {} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/baseband/ + + set minor_class [expr {($class >> 2) & 0x3f}] + set major_class [expr {($class >> 8) & 0x1f}] + + if {$major_class == 0} { + set major_name "Miscellaneous" + set minor_name "Miscellaneous" + } elseif {$major_class == 1} { + set major_name Computer + set minor_name [lindex { + Uncategorized Desktop Server Laptop "Handheld PC/PDA" "Palm-size PC/PDA" Wearable Tablet + } $minor_class] + } elseif {$major_class == 2} { + set major_name Phone + set minor_name [lindex { + Uncategorized Cellular Cordless Smartphone "Wired modem" ISDN + } $minor_class] + } elseif {$major_class == 3} { + set major_name "LAN/Network Access point" + set minor_name [lindex { + 0% 1-17% 17-33% 33-50% 50-67% 67-83% 83-99% 100% + } $minor_class] + } elseif {$major_class == 4} { + set major_name "Audio/Video" + set minor_name [lindex { + Uncategorized "Wearable Headset Device" "Hands-free Device" Reserved + Microphone Loudspeaker Headphones "Portable Audio" "Car Audio" + "Set-top box" "HiFi Audio Device" VCR Camcorder "Video Monitor" + "Video Display and Loudspeaker" "Video Conferencing" Reserved + "Gaming/Toy" + } $minor_class] + } elseif {$major_class == 5} { + set major_name Peripheral + set bits [expr {$minor_class >> 4}] + set minor_name [lindex { + Other Keyboard "Pointing device" "Combo keyboard/pointing device" + } $bits] + set bits [expr {$minor_class & 0xf}] + if {$bits < 10} { + append minor_name /[lindex { + Uncategorized Joystick Gamepad "Remote control" "Sensing device" + "Digitizer tabler" "Card reader" "Digital pen" "Handheld scanner" + "Handheld gesture input device" + }] + } + } elseif {$major_class == 6} { + set major_name Imaging + set minor_name {} + if {$minor_class & 4} {lappend minor_name Display} + if {$minor_class & 8} {lappend minor_name Camera} + if {$minor_class & 16} {lappend minor_name Scanner} + if {$minor_class & 32} {lappend minor_name Printer} + set minor_name [join $minor_name /] + } elseif {$major_class == 7} { + set major_name Wearable + set minor_name [lindex { + Wristwatch Pager Jacket Helmet Glasses + } $minor_class] + } elseif {$major_class == 8} { + set major_name Toy + set minor_name [lindex { + Robot Vehicle Doll Controller Game + } $minor_class] + } elseif {$major_class == 9} { + set major_name Health + set minor_name [lindex { + Undefined "Blood Pressure Monitor" Thermometer "Weighing Scale" + "Glucose Meter" "Pulse Oximeter" "Heart/Pulse Rate Monitor" + "Health Data Display" "Step Counter" "Body Composition Analyzer" + "Peak Flow Monitor" "Medication Monitor" "Knee Prosthesis" + "Andle Prosthesis" "Generic Health Manager" "Personal Mobility Device" + } $minor_class] + } elseif {$major_class == 31} { + set major_name Uncategorized + set minor_name "" + } else { + set major_name Reserved + set minor_name "" + } + + set service_class {} + if {$class & (1<<13)} {lappend service_class "Limited Discoverable Mode"} + if {$class & (1<<16)} {lappend service_class "Positioning (Location identification)"} + if {$class & (1<<17)} {lappend service_class "Networking"} + if {$class & (1<<18)} {lappend service_class "Rendering"} + if {$class & (1<<19)} {lappend service_class "Capturing"} + if {$class & (1<<20)} {lappend service_class "Object Transfer"} + if {$class & (1<<21)} {lappend service_class "Audio"} + if {$class & (1<<22)} {lappend service_class "Telephony"} + if {$class & (1<<23)} {lappend service_class "Information"} + + return [dict create MajorClassName $major_name \ + MinorClassName $minor_name \ + DeviceClasses $service_class] +} + +proc iocp::bt::ResolveDeviceUnique {device} { + if {[IsAddress $device]} { + return $device + } + set addrs [device address $device] + if {[llength $addrs] == 0} { + error "Could not resolve Bluetooth device name \"$device\"." + } elseif {[llength $addrs] > 1} { + error "Device \"$device\" resolves to multiple addresses." + } + return [lindex $addrs 0] +} + +package provide iocp_bt $iocp::bt::version diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl new file mode 100644 index 00000000..8b4c96c3 --- /dev/null +++ b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btnames.tcl @@ -0,0 +1,383 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::names { + namespace path [namespace parent] + # Maps UUID to Service class name + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + # UUID may be either 16-bit Bluetooth universal UUID as or the full UUID + # Uses lower case hex. + variable service_class_names { + 1000 ServiceDiscoveryServerServiceClassID + 1001 BrowseGroupDescriptorServiceClassID + 1002 PublicBrowseRoot + 1101 SerialPort + 1102 LANAccessUsingPPP + 1103 DialupNetworking + 1104 IrMCSync + 1105 OBEXObjectPush + 1106 OBEXFileTransfer + 1107 IrMCSyncCommand + 1108 Headset + 1109 CordlessTelephony + 110a AudioSource + 110b AudioSink + 110c A/V_RemoteControlTarget + 110d AdvancedAudioDistribution + 110e A/V_RemoteControl + 110f A/V_RemoteControlController + 1110 Intercom + 1111 Fax + 1112 {Headset - Audio Gateway (AG)} + 1115 PANU + 1116 NAP + 1117 GN + 1118 DirectPrinting + 1119 ReferencePrinting + 111a {Basic Imaging Profile} + 111b ImagingResponder + 111c ImagingAutomaticArchive + 111d ImagingReferencedObjects + 111e Handsfree + 111f HandsfreeAudioGateway + 1120 DirectPrintingReferenceObjectsService + 1121 ReflectedUI + 1122 BasicPrinting + 1123 PrintingStatus + 1124 HumanInterfaceDeviceService + 1125 HardcopyCableReplacement + 1126 HCR_Print + 1127 HCR_Scan + 1128 Common_ISDN_Access + 112d SIM_Access + 112e {Phonebook Access - PCE} + 112f {Phonebook Access - PSE} + 1130 {Phonebook Access} + 1131 {Headset - HS} + 1132 {Message Access Server} + 1133 {Message Notification Server} + 1134 {Message Access Profile} + 1135 GNSS + 1136 GNSS_Server + 1137 {3D Display} + 1138 {3D Glasses} + 1139 {3D Synchronization} + 113a {MPS Profile UUID} + 113b {MPS SC UUID} + 113c {CTN Access Service} + 113d {CTN Notification Service} + 113e {CTN Profile} + 1200 PnPInformation + 1201 GenericNetworking + 1202 GenericFileTransfer + 1203 GenericAudio + 1204 GenericTelephony + 1303 VideoSource + 1304 VideoSink + 1305 VideoDistribution + 1400 HDP + 1401 {HDP Source} + 1402 {HDP Sink} + 02030302-1d19-415f-86f2-22a2106a0a77 {Wireless iAP v2} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + variable protocol_names { + 0001 SDP + 0002 UDP + 0003 RFCOMM + 0004 TCP + 0005 TCS-BIN + 0006 TCS-AT + 0007 ATT + 0008 OBEX + 0009 IP + 000a FTP + 000c HTTP + 000e WSP + 000f BNEP + 0010 UPNP + 0011 HIDP + 0012 HardcopyControlChannel + 0014 HardcopyDataChannel + 0016 HardcopyNotification + 0017 AVCTP + 0019 AVDTP + 001b CMTP + 001e MCAPControlChannel + 001f MCAPDataChannel + 0100 L2CAP + } + + # Universal attribute names + variable attribute_names + set attribute_names { + ServiceRecordHandle 0 + ServiceClassIDList 1 + ServiceRecordState 2 + ServiceID 3 + ProtocolDescriptorList 4 + BrowseGroupList 5 + LanguageBaseAttributeIDList 6 + ServiceInfoTimeToLive 7 + ServiceAvailability 8 + BluetoothProfileDescriptorList 9 + DocumentationURL 10 + ClientExecutableURL 11 + IconURL 12 + AdditionalProtocolDescriptorList 13 + ServiceName 256 + ServiceDescription 257 + ProviderName 258 + } + +} + +proc iocp::bt::names::is_uuid {uuid} { + regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid +} + +proc iocp::bt::names::to_name {uuid} { + # Map a UUID to a name. + # uuid - Bluetooth UUID + # The command attempts to map the UUID as a service name or protocol + # name in that order. + # + # Returns the mapped name or the uuid if no mapping is possible. + + set name [service_class_name $uuid] + if {$name ne $uuid && ![is_uuid $name]} { + return $name + } + return [protocol_name $uuid] +} + +proc iocp::bt::names::to_uuid {name_or_uuid} { + # Map a name to a UUID. + # name_or_uuid - Bluetooth UUID or name + # The command attempts to map the name as a service class name or protocol + # name in that order. + # + # Returns the mapped uuid raises an error if no mapping is possible. + variable service_class_names + if {[MapNameToUuidV $name_or_uuid service_class_names name]} { + return $name + } + return [protocol_uuid $name_or_uuid] +} + + +proc iocp::bt::names::service_class_name {uuid} { + # Maps a UUID to a service class name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable service_class_names + set name [MapUuidToName $uuid service_class_names] + if {$name ne $uuid} { + return $name + } + # Well known private uuids + if {[string equal -nocase "02030302-1d19-415f-86f2-22a2106a0a77" $uuid]} { + return "Wireless iAP v2"; # iPod Accessory Protocol + } + return $uuid +} + +proc iocp::bt::names::service_class_uuid {name} { + # Maps a service class name to a UUID. + # name - Service class name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable service_class_names + return [MapNameToUuid $name service_class_names] +} + +proc iocp::bt::names::profile_name {uuid} { + # Maps a UUID to a Bluetooth profile name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + + # Profiles seem to come from same service class UUID space. + return [service_class_name $uuid] +} + +proc iocp::bt::names::protocol_name {uuid} { + # Maps a UUID to a protocol name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable protocol_names + return [MapUuidToName $uuid protocol_names] +} + +proc iocp::bt::names::protocol_uuid {name} { + # Maps a protocol name to a UUID. + # name - Protocol name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable protocol_names + return [MapNameToUuid $name protocol_names] +} + +proc iocp::bt::names::attribute_name {attr_id} { + # Maps a attribute id (numeric or name) to a attribute name. + # attr_id - numeric attribute identifier + # Returns a human-readable name for the attribute or + # the numeric id itself if no mapping could be performed. + variable attribute_names + dict for {name id} $attribute_names { + if {$attr_id == $id || [string equal -nocase $name $attr_id]} { + return $name + } + } + if {[string is integer -strict $attr_id]} { + return $attr_id + } + error "Unknown attribute \"$attr_id\"." +} + +proc iocp::bt::names::attribute_id {name} { + # Maps a attribute name to it numeric attribute identifier. + # name - Attribute name to be mapped + # The command will raise an error if the name is unknown. + # Returns the numeric attribute identifier corresponding to + # the passed name. + variable attribute_names + if {[string is integer -strict $name]} { + return $name + } + return [dict get $attribute_names $name] +} + +proc iocp::bt::names::print {} { + # Prints known UUID's and their mapped mnemonics. + variable service_class_names + dict for {uuid16 name} $service_class_names { + puts "[Uuid16 $uuid16] $name" + } +} + +proc iocp::bt::names::MapUuidToName {uuid dictvar} { + upvar 1 $dictvar names + + # uuid may be a full UUID or a 16-bit Bluetooth UUID + if {[IsUuid $uuid]} { + # See if the exact match exists + set uuid [string tolower $uuid] + if {[dict exists $names $uuid]} { + return [dict get $names $uuid] + } + # Nope. Now see if it is a 16-bit Bluetooth UUID + if {[IsBluetoothUuid $uuid] && [string range $uuid 0 3] eq "0000"} { + # 16-bit UUID + set uuid16 [string tolower [string range $uuid 4 7]] + if {[dict exists $names $uuid16]} { + return [dict get $names $uuid16] + } + } + # Nope. Cannot be mapped. Return the UUID. + return $uuid + } + + # Not a full UUID. Retry as a 16-but UUID. tailcall so upvar works right. + tailcall MapUuidToName [Uuid16 $uuid] $dictvar +} + +proc iocp::bt::names::MapNameToUuidV {name dictvar resultvar} { + upvar 1 $dictvar names + upvar 1 $resultvar result + + if {[IsUuid $name]} { + set result $name + return 1 + } + dict for {uuid mapped_name} $names { + if {[string equal -nocase $name $mapped_name]} { + if {[string length $uuid] == 4} { + # 16-bit UUID -> full UUID + set result "0000${uuid}-0000-1000-8000-00805f9b34fb" + } else { + set result $uuid + } + return 1 + } + } + return 0 +} + +proc iocp::bt::names::MapNameToUuid {name dictvar} { + upvar 1 $dictvar names + if {[MapNameToUuidV $name names result]} { + return $result + } + error "Name \"$name\" could not be mapped to a UUID" +} + +proc iocp::bt::names::Asciify {svar} { + # Replaces non-ascii or non-printable chars with Tcl sequences + # + # Transformed string is stored back in svar. + + upvar 1 $svar s + set ascii "" + set was_escaped 0 + # Split can be expensive for large strings so string index + set len [string length $s] + for {set i 0} {$i < $len} {incr i} { + set char [string index $s $i] + scan $char %c codepoint + if {$codepoint < 32 || $codepoint >= 126} { + append ascii "\\u[format %.4x $codepoint]" + set was_escaped 1 + } else { + append ascii $char + } + } + set s $ascii + return $was_escaped +} + +proc iocp::bt::names::ClipSpec {{use_list 0}} { + package require twapi + + set csv [twapi::read_clipboard_text] + if {$use_list} { + set tcl "\[list \n" + } else { + set tcl "\{ \n" + } + foreach line [split $csv \n] { + lassign [split $line \t] name uuid description + set name [string trim $name] + set uuid [string trim $uuid] + if {![regexp {^0x[[:xdigit:]]{4}} $uuid]} continue + if {$use_list} { + # Unicode chars, needs escaping + Asciify name + append tcl " " [list [string range $uuid 2 end]] " " "\"$name\"" " \\" \n + } else { + if {[Asciify name]} { + # Need to use the list command. Just restart + return [ScrapeBTSpec 1] + } + append tcl " " [string range $uuid 2 end] " " "{$name}" \n + } + } + if {$use_list} { + append tcl "\]\n" + } else { + append tcl "\}" + } + + twapi::write_clipboard_text $tcl +} + diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl new file mode 100644 index 00000000..cd014a1b --- /dev/null +++ b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/btsdr.tcl @@ -0,0 +1,962 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::sdr { + namespace path [namespace parent] + namespace export attribute attributes decode print printn + + namespace eval attribute { + namespace path [list [namespace parent] [namespace parent [namespace parent]]] + namespace export exists get raw text + namespace ensemble create + } +} + +proc iocp::bt::sdr::decode {binsdr} { + # Decodes a binary service discovery record + # binsdr - a raw Bluetooth service discovery record in binary + # form. + # + # The returned value should be treated as opaque. The attributes + # stored in the record should be accessed with the commands in + # the `sdr` namespace. + # + # Returns a container of attributes stored in the record. + + # A SDR record is a single data element which is a sequence + # containing nested data elements. + # {{sequence {nested data elements}}} + set rec [lindex [DecodeElements $binsdr] 0 1] + + # $rec contains alternating attribute value pairs. The result + # is built as a list as its faster to do that and shimmer + # to a dict on access than to build a dictionary to begin with. + set sdr {} + foreach {attr val} $rec { + lappend sdr [lindex $attr 1] $val + } + return $sdr +} + +proc iocp::bt::sdr::attribute::exists {sdr attr_id {varname {}}} { + # Checks if an attribute exists in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # attr_id - attribute integer id or Bluetooth universal attribute name + # varname - If not the empty string, the attribute + # value is stored in a variable of this name in the + # caller's context. + # + # The value returned in $varname is the attribute type-independent data + # value. To get the value formatted for the attribute type, use the + # [attribute get] command. + # + # Returns 1 if the attribute exists, and 0 otherwise. + + if {[string is integer -strict $attr_id]} { + set key [expr {$attr_id + 0}]; # Force decimal rep. Faster than format + } else { + set key [names::attribute_id $attr_id]; # name -> id + } + + if {[dict exists $sdr $key]} { + if {$varname ne ""} { + upvar 1 $varname value + set value [dict get $sdr $key] + } + return 1 + } + + return 0 +} + +proc iocp::bt::sdr::attributes {sdr} { + # Get the list of attributes in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # Returns a list of numeric attribute ids. + return [dict keys $sdr] +} + +proc iocp::bt::sdr::attribute::raw {sdr attr_id} { + # Get an attribute value from an service discovery record. + # sdr - a decoded service discovery record in the form returned by + # sdr_decode. + # attr_id - attribute integer id + # + # The command will raise an error if the attribute does not exist + # in the sdr. + # + # Returns the attribute value as a pair consisting of the type and + # the raw data element value. + + if {[exists $sdr $attr_id value]} { + return $value + } else { + error "Attribute with id \"$attr_id\" not found." + } +} + +proc iocp::bt::sdr::attribute::get {sdr attr_id {varname {}}} { + # Get the value of an universal attribute from a service + # discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # varname - Optional name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The attribute value is decoded into a attribute type-dependent format. + # + # The $attr_id argument must be one of those defined in + # the *Universal Attributes* section in the + # [Bluetooth Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/) + # specification. Otherwise an error is raised. The recognized attributes + # are listed below. Refer to the Bluetooth specification for their + # exact semantics. + # + # AdditionalProtocolDescriptorList - List of protocol stacks. This + # supplements the `ProtocolDescriptorList` attribute. + # BluetoothProfileDescriptorList - List of Bluetooth profile descriptors + # to which the service conforms. Each element is a dictionary + # with keys `Uuid`, `Name`, `MajorVersion` and `MinorVersion` + # corresponding to the referenced profile. + # BrowseGroupList - List of browse group descriptors representing + # the browse groups to which the service record belongs. Each + # descriptor is dictionary with keys `Uuid` and `Name` identifying + # the a browse group. + # ClientExecutableURL - URL pointing to the client application that + # may be used to utilize the service. + # DocumentationURL - URL for the service documentation. + # IconURL - URL for icon that may be used to represent the service. + # LanguageBaseAttributeIDList - Nested dictionary of language-specific + # attribute offsets (see below). + # ProtocolDescriptorList - List of protocol stacks (see below) that may + # be used to access the service described by the service record. + # ProviderName - Name of entity providing the service in the primary + # language of the service record. + # ServiceAvailability - A value in the range 0-255 that indicates + # relative availability of the service in terms of the number of + # clients it can accept. Note this is a scaled estimate. + # ServiceClassIDList - List with each element being a dictionary + # corresponding to a service class that the record conforms to. + # The keys of the dictionary are `Name` and `Uuid`. + # ServiceDescription - A description of the service in the primary + # language of the record. + # ServiceID - UUID that uniquely identifies a service instance. + # ServiceInfoTimeToLive - The number of seconds from the time it was + # generated that the service record information is expected to be valid. + # ServiceName - The human-readable name of the service in the primary + # language of the record. + # ServiceRecordHandle - A numeric value that uniquely identifies a service + # record within a SDP server. + # ServiceRecordState - Numeric value that is changed any time the service + # record is modified by the SDP server. + # + # In the case of ProtocolDescriptorList and AdditionalProtocolDescriptorList, + # the attribute value is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # In the case of LanguageBaseAttributeIDList, the attribute value is + # a dictionary indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. + # + # Returns a boolean or the decoded attribute value. + + set attr_name [names::attribute_name $attr_id] + set attr_id [names::attribute_id $attr_id] + switch -exact -- $attr_name { + BrowseGroupList - + ServiceClassIDList { + tailcall Uuids $sdr $attr_id $varname + } + BluetoothProfileDescriptorList - + ProtocolDescriptorList - + AdditionalProtocolDescriptorList - + LanguageBaseAttributeIDList { + tailcall $attr_name $sdr $varname + } + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceAvailability - + ServiceInfoTimeToLive - + ServiceID - + ServiceRecordState - + ServiceRecordHandle { + tailcall AttributeValue $sdr $attr_id $varname + } + ServiceName - + ServiceDescription - + ProviderName { + # Special case because they need a language argument + tailcall text $sdr $attr_name primary $varname + } + default { + if {[llength [info commands [namespace current]::universal::$attr_name]]} { + tailcall [namespace current]::universal::$attr_name $sdr $varname + } + } + } + error "Unknown universal attribute \"$attr_id\"." +} + +proc iocp::bt::sdr::attribute::text {sdr attr_id lang {varname {}}} { + # Get the value of an text attribute in the specified language + # from a service discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # lang - language identifier as specified in iso639, e.g. `en` for + # english, `fr` for french etc. or the keyword `primary`. + # varname - Name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # If the record does not contain a value for the specified language, + # the value for the primary language will be retrieved. + # + # Returns a boolean or the attribute text value. + # + set lang_offset [names::attribute_id $attr_id] + if {$lang_offset < 256 || $lang_offset >= 512} { + error "Invalid text attribute id \"$attr_id\"." + } + incr lang_offset -256 + tailcall TextAttribute $sdr $lang_offset $lang $varname +} + + +proc iocp::bt::sdr::attribute::LanguageBaseAttributeIDList {sdr {varname {}}} { + # Retrieve the language base attributes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a nested dictionary + # corresponding to the `LanguageBaseAttributeIDList` + # universal attribute in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The dictionary value returned or stored is indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. fs + # + # Returns a boolean or a dictionary of language attributes. + if {![AttributeValue $sdr 6 sequence]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # attr is {sequence {langid encoding baseoffset ...}} + set langs [dict create] + foreach {langid enc offset} $sequence { + # langid is ISO 639 language code + set langid [lindex $langid 1] + set langid "[format %c [expr {$langid >> 8}]][format %c [expr {$langid &0xff}]]" + dict set langs $langid Encoding [::iocp::bt::EncodingTclName [lindex $enc 1]] + dict set langs $langid BaseOffset [lindex $offset 1] + } + if {$varname eq ""} { + return $langs + } else { + upvar 1 $varname var + set var $langs + return 1 + } +} + + +proc iocp::bt::sdr::attribute::ProtocolDescriptorList {sdr {varname {}}} { + # Retrieve the protocol information from a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # Information about the protocols that may be used to access the + # service described by SDR is stored in the ProtocolDescriptorList + # universal attribute as described in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The returned information is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # Returns a boolean or the list of protocols. + + if {! [attribute exists $sdr 4 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + if {$varname eq ""} { + return [Protocols $attrval] + } else { + upvar 1 $varname var + set var [Protocols $attrval] + return 1 + } +} + +proc iocp::bt::sdr::attribute::BluetoothProfileDescriptorList {sdr {varname {}}} { + # Retrieve the service classes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a list of profile descriptors + # corresponding to the BluetoothProfileDescriptorList + # universal attribute in the Bluetooth Core specification. + # Each descriptor is a dictionary with the following keys: + # `Uuid` - the UUID of the profile + # `Name` - the mnemonic name of the profile or its UUID if the + # UUID cannot be mapped to a name + # `MajorVersion` - the profile's major version + # `MinorVersion` - the profile's minor version + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # Returns a boolean or the list of service class descriptors. + + if {! [attribute exists $sdr 9 attr]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # Sequence of profile descriptors. Each profile descriptor + # is sequence of profile uuid and version. + set profiles {} + foreach profile [lindex $attr 1] { + set uuid [lindex $profile 1 0 1] + set version [lindex $profile 1 1 1] + lappend profiles [dict create \ + Uuid $uuid \ + Name [iocp::bt::names::to_name $uuid] \ + MajorVersion [expr {$version >> 8}] \ + MinorVersion [expr {$version & 0xff}] + ] + } + + if {$varname eq ""} { + return $profiles + } else { + upvar 1 $varname var + set var $profiles + return 1 + } + +} + +proc iocp::bt::sdr::attribute::AdditionalProtocolDescriptorList {sdr {varname {}}} { + # Returns protocol information from the additional protocols field + # in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # This command is identical to [protocols] except it extracts + # protocol information from the `AdditionalProtocolDescriptorList` + # attribute instead of `ProtocolDescriptorList`. + + if {! [attribute exists $sdr 13 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # AdditionalProtocolDescriptorList is a sequence each element of which + # is formatted like a ProtocolDescriptorList + # assert [lindex $attrval 0] eq "sequence" + set protocols [lmap elem [lindex $attrval 1] { + Protocols $elem + }] + + if {$varname eq ""} { + return $protocols + } else { + upvar 1 $varname var + set var $protocols + return 1 + } +} + + +proc iocp::bt::sdr::Uuids {sdr attr_id {varname {}}} { + if {! [attribute exists $sdr $attr_id attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + set uuids [lmap elem [lindex $attrval 1] { + set uuid [lindex $elem 1] + dict create Uuid $uuid Name [names::to_name $uuid] + }] + + if {$varname eq ""} { + return $uuids + } else { + upvar 1 $varname var + set var $uuids + return 1 + } +} + +proc iocp::bt::sdr::Protocols {attrval} { + # The attribute value may either be a single protocol stack encoded + # as a sequence, or multiple stacks encoded as a selection of sequences. + if {[lindex $attrval 0] eq "sequence"} { + # Single protocol stack + set stacks [list [lindex $attrval 1]] + } else { + # selection - Multiple stacks + set stacks [lindex $attrval 1] + } + + set protocols [lmap stack $stacks { + # stack -> {protocol_descriptor protocol_descriptor ...} + # protocol_descriptor -> {sequence {data_element ...}} + lmap protocol_descriptor $stack { + # Each descriptor is a sequence, the first element identifying the + # protocol and the remaining being (optional) parameters. + set elements [lindex $protocol_descriptor 1] + set protocol_uuid [lindex $elements 0 1] + dict create \ + Protocol $protocol_uuid \ + ProtocolName [iocp::bt::names::protocol_name $protocol_uuid] \ + ProtocolParams [lrange $elements 1 end] + } + }] + + return $protocols +} + +proc iocp::bt::sdr::AttributeValue {sdr attr_id {varname {}}} { + if {$varname eq ""} { + return [lindex [attribute raw $sdr $attr_id] 1] + } else { + if {[attribute exists $sdr $attr_id attrval]} { + upvar 1 $varname value + set value [lindex $attrval 1] + return 1 + } else { + return 0 + } + } +} + +proc iocp::bt::sdr::TextAttribute {sdr attr_offset lang {varname {}}} { + + # Assume that if sdr does not explicitly specify encoding, it is UTF8 + set enc utf-8 + set base_offset 256 + + # Primary language's ServiceName attribute index always 256 + # Otherwise get the offset from the base index 256 + if {$lang ne "primary"} { + # See if the specified language has an entry in the languages table + if {[attribute get $sdr LanguageBaseAttributeIDList base_offsets] && + [dict exists $base_offsets $lang]} { + # Yes, so the language-specific text will be at an offset + # from the base attribute index + set base_offset [dict get $base_offsets $lang BaseOffset] + set enc [dict get $base_offsets $lang Encoding] + } + } + set attr_index [expr {$base_offset + $attr_offset}] + if {! [attribute exists $sdr $attr_index name]} { + # This language does not exist. Return primary language if possible. + # If base_offset was 256, that's already primary so don't retry + if {$base_offset == 256 || + ![attribute exists $sdr [expr {256 + $attr_offset}] name] } { + if {$varname ne ""} { + return 0 + } else { + error "Attribute not found in SDR." + } + } + } + + set name [lindex $name 1] + + # $name is encoded. If Tcl supports the encoding, well and good. + # Else return a hex form + if {[string is integer $enc]} { + # SDR specified an encoding but not one that Tcl knows about + set text [binary encode hex $name] + } else { + # The string may be null terminated. TBD - always? + if {[string index $name end] eq "\0"} { + set name [string range $name 0 end-1] + } + set text [encoding convertfrom $enc $name] + } + + if {$varname ne ""} { + upvar 1 $varname var + set var $text + return 1 + } else { + return $text + } +} + +proc iocp::bt::sdr::DecodeElements {delem} { + # Decodes a Data Element as defined in the Bluetooth specification. + # delem - Data element in binary format + + set decoded {} + while {[string length $delem]} { + lassign [ExtractFirstElement $delem] type value delem + switch -exact -- $type { + sequence { lappend decoded [list sequence [DecodeElements $value]]} + selection { lappend decoded [list selection [DecodeElements $value]]} + default { + lappend decoded [list $type $value] + } + } + } + return $decoded +} + +proc iocp::bt::sdr::ExtractFirstElement {bin} { + # Extracts leading element from an SDP record. + # bin - tail of SDP record + # Returns a pair consisting of the extracted element and + # the remaining part of $bin. + + set binlen [string length $bin] + if {$binlen == 0} { + return {} + } + binary scan $bin c header + set type [expr {($header >> 3) & 0xff}] + set lenbits [expr {$header & 0x7}] + + # The nil type is formatted slightly differently. Treat as a special case. + if {$type == 0} { + if {$lenbits != 0} { + error "Invalid non-0 length for nil type." + } + return [list nil "" [string range $bin 1 end]] + } + + if {$lenbits < 5} { + set len [expr {1 << $lenbits}] + set dataoffset 1 + } else { + if {$lenbits == 5} { + set dataoffset 2 + set fmt c + set mask 0xff + } elseif {$lenbits == 6} { + set dataoffset 3 + set fmt S + set mask 0xffff + } else { + # lenbits == 7 + set dataoffset 5 + set fmt I + set mask 0xffffffff + } + # Ensure input length enough for at least length field + if {$binlen < $dataoffset} { + error "Truncated binary header." + } + binary scan $bin @1$fmt len + set len [expr {$len & $mask}]; # Since binary scan is signed + } + if {$binlen < ($dataoffset+$len)} { + error "Truncated binary data." + } + set elem [string range $bin $dataoffset [expr {$dataoffset+$len-1}]] + switch -exact $type { + 1 { + set type uint + if {$len == 1} { + binary scan $elem cu elem + } elseif {$len == 2} { + binary scan $elem Su elem + } elseif {$len == 4} { + binary scan $elem Iu elem + } elseif {$len == 8} { + binary scan $elem Wu elem + } elseif {$len == 16} { + binary scan $elem WuWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 2 { + set type int + if {$len == 1} { + binary scan $elem c elem + } elseif {$len == 2} { + binary scan $elem S elem + } elseif {$len == 4} { + binary scan $elem I elem + } elseif {$len == 8} { + binary scan $elem W elem + } elseif {$len == 16} { + # Note hi is signed, lo unsigned + binary scan $elem WWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 3 { + set type uuid + if {$len == 2} { + set elem [binary encode hex $elem] + set elem "0000$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 4} { + set elem [binary encode hex $elem] + set elem "$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 16} { + set hex [binary encode hex $elem] + set elem [string range $hex 0 7] + append elem - [string range $hex 8 11] + append elem - [string range $hex 12 15] + append elem - [string range $hex 16 19] + append elem - [string range $hex 20 end] + } else { + error "Invalid length $len for UUID." + } + } + 4 { + set type text + # TBD - keep elem as binary as we do not know encoding + } + 5 { + set type boolean + if {$len != 1} { + error "Invalid length for boolean type." + } + binary scan $elem c elem + set elem [expr {!!$elem}] ; # Convert to 0/1 + } + 6 { + set type sequence + # elem remains the same. Caller will have to recurse to parse + } + 7 { + set type selection + # elem remains the same. Caller will have to recurse to parse + } + 8 { + set type url + # elem remains same. Encoding unknown. TBD + } + default { + set type unknown + # elem remains same + } + } + + return [list $type $elem [string range $bin $dataoffset+$len end]] +} + +proc iocp::bt::sdr::printn {recs {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # recs - a list of binary SDP records in the form returned by + # [::iocp::bt::device services] or [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + set sep "" + foreach rec $recs { + puts $sep + set sep "--------------------------------------------" + print $rec $attrfilter + } +} + +proc iocp::bt::sdr::print {rec {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # rec - a binary SDP record in the form returned by [::iocp::bt::device services] or + # [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + + # Alternating attribute value pairs + set rec [decode $rec] + foreach attr [attributes $rec] { + set attrname [names::attribute_name $attr] + if {[string is integer -strict $attrname]} { + # For easier matching with Bluetooth specs + set attrname [format 0x%x $attrname] + } + if {![string match -nocase $attrfilter $attrname]} { + continue + } + switch -exact -- $attrname { + ServiceID - + ServiceRecordState - + ServiceInfoTimeToLive - + ServiceAvailability - + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceName - + ServiceDescription - + ProviderName - + ServiceRecordHandle { + puts "$attrname: [attribute get $rec $attrname]" + } + ServiceClassIDList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + # We use this for printing service-specific attributes + lappend service_class_uuids $Uuid + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + ProtocolDescriptorList { + puts "$attrname:" + foreach protocol [attribute get $rec $attrname] { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + AdditionalProtocolDescriptorList { + # Like ProtocolDescriptorList but an additional level + # of nesting. + puts "$attrname:" + foreach additional_protocol [attribute get $rec $attrname] { + foreach protocol $additional_protocol { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + } + BrowseGroupList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + LanguageBaseAttributeIDList { + puts "$attrname: sequence" + dict for {lang val} [attribute get $rec $attrname] { + puts " $lang: [dict get $val Encoding], [dict get $val BaseOffset]" + } + } + BluetoothProfileDescriptorList { + puts "$attrname: sequence" + foreach profile [attribute get $rec $attrname] { + # Sequence of profiles. Each profile is a sequence of + # profile uuid and version. + # puts profile:$profile + dict with profile { + if {$Uuid eq $Name} { + puts " $Uuid v$MajorVersion.$MinorVersion" + } else { + puts " $Uuid $Name v$MajorVersion.$MinorVersion" + } + } + } + } + default { + if {$attr >= 0x100 && $attr <= 0x1ff} { + # TBD - special handling for 0x100-0x1ff attributes by + # looking up languages table + puts "$attrname: [PrintableElement [attribute raw $rec $attr]]" + } else { + lassign [PrintableServiceSpecificAttribute \ + $service_class_uuids $rec $attr] attrname attrval + puts "$attrname: $attrval" + } + } + } + } +} + +proc iocp::bt::sdr::PrintableServiceSpecificAttribute {service_class_uuids rec attr} { + # Returns pair of human readable attribute name and value + set attrname $attr + set attrval [PrintableElement [attribute raw $rec $attr]] + + # Maybe we know about attributes for one of these services + foreach uuid $service_class_uuids { + switch -exact -- $uuid { + 00001105-0000-1000-8000-00805f9b34fb { + # ObjectPush + if {$attr == 0x200} { + set attrname GoepL2CapPsm + break + } elseif {$attr == 0x303} { + set attrname SupportedFormats + set attrval [lmap val [lindex [attribute raw $rec $attr] 1] { + # Val is a sequence of {type value} where type is int + set val [lindex $val 1] + if {$val == 255} { + set val any + } else { + set formats {unknown vcard-2.1 vcard-3.0 vcal-1.0 ical-2.0 vnote vmessage} + if {$val < [llength $formats]} { + set val [lindex $formats $val] + } + } + set val + }] + break + } + } + } + } + return [list $attrname $attrval] +} + +proc iocp::bt::sdr::PrintableElement {delem {parent_indent {}}} { + lassign $delem type val + switch -exact -- $type { + nil - + uint - + int { return $val } + uuid { + set name [names::to_name $val] + if {$name eq $val} { + return $val + } else { + return "$val $name" + } + } + text - + url { + # Assume UTF-8 binary form + # Strip terminating null. TBD - is null alwys present? + set printable [encoding convertfrom utf-8 $val] + } + boolean { return [expr {$val ? "true" : "false"}] } + selection - + sequence { + set printable [list $type] + set new_indent "${parent_indent} " + foreach elem $val { + lappend printable "${new_indent}[PrintableElement $elem $new_indent]" + } + set printable [join $printable \n] + } + unknown { + set printable [BinToHex $val] + } + } + return $printable +} diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl new file mode 100644 index 00000000..2452b04a --- /dev/null +++ b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/pkgIndex.tcl @@ -0,0 +1,36 @@ +# Core iocp package +package ifneeded iocp 2.0.2 \ + [list apply [list {dir} { + package require platform + set initName [string totitle iocp] + if {[package vsatisfies [package require Tcl] 9]} { + set fileName "tcl9iocp202.dll" + } else { + set fileName "iocp202.dll" + } + set platformId [platform::identify] + set searchPaths [list [file join $dir $platformId] \ + {*}[lmap platformId [platform::patterns $platformId] { + file join $dir $platformId + }] \ + $dir] + foreach path $searchPaths { + set lib [file join $path $fileName] + if {[file exists $lib]} { + uplevel #0 [list load $lib $initName] + return + } + } + error "Could not locate $fileName in directories [join $searchPaths {, }]" + }] $dir] + +# iocp_inet doesn't need anything other than core iocp +package ifneeded iocp_inet 2.0.2 \ + "package require iocp" + +if {1} { + # iocp_bt needs supporting script files + package ifneeded iocp_bt 2.0.2 \ + "[list source [file join $dir bt.tcl]]" +} + diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/iocp202.dll b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..09d3cf0aa9f0d82e775357fbf226283e817d89d7 GIT binary patch literal 125251 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 Qz?}EdISggrdsOa!0Bc?jNB{r; literal 0 HcmV?d00001 diff --git a/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/tcl9iocp202.dll b/src/vfs/punk9magicsplat.vfs/lib_tcl9/iocp-2.0.2/win32-ix86/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe296717af838bb5537a33cc4a6e8a58c4ab5296 GIT binary patch literal 125285 zcmd?Sdw5jkxi>rm6BuN0jWW?xV;${iLmO>U)0$}L9-K^w7Bosgs;Jm%O1D%|W`tG_ ziEE~^T#QcLxa+%XTd00}+cWKEL$n?Oi43%M8vzxITTxNhI9Tyeh)SLJ_q(69)=Uog z@?PKf-^X>4HEXTsxu5%d-_LQ)ydvuv>+yJe_+Kn~JUzJbUsV2n`kyYmKK9f{j`eJq z@R!qiyfglCTE~L#EDkTa<)&}na_x7+-@5k38*fU4zj=N5mgJ4$@7x$}xnx%OyEk2T z{n?Wy9TTxZx5qu68Qx<&|Nh0Vf7|nKUQc$6$FtWn?sF5pW2Sh5UXSNr@xRLBSw9&S zg7_cCy(hFlexoe$<`Z7!Ur;?N5qcs@9f0&OA~mv>sS;bLZsCtLRFgY@ytK_x@!~H;&uRJ z3GFNs8VWe-@${a3iz=A-9NyRAeFWF45#^#^YQS^+v!>$De7ye}uFWIMo&C+li&?Jj zMEqGh^gW;RUl-fGUbQQ{>_s`^`Te2gqfn=_zoW_tufcOq0OgjADA#*-?6MBL@znCq zUK`%3QOYG23Vwc;J7AZ)bChzoT)*(9Z+UDP0=Z*Re)WiQ7ZA+1HXKm#I z>lvTdqxV<$Wj35iPGmOn@8xr5>HC9bOC*x+P2RFDfP(cq(!DD#*DbF(^|~FnJ@^2h z7LLm#&h%sf%z{5DKCkchJwi4c2M%bzudMn_0T}ML+1um{nqjz~?eCE}w(8yyip?!!hX zQXJ6t2lNV(>=lgj7Z!^}8=X$lFU(AUnYjwmit3&_1e^RxrILRnZ}Q*d1L3nOQRrri z!M&9W0LRaZfM8kcbidb=pTP>kXO#1)avoePAKt3M#5ezCx17!3UE@*v*i!or|7Krq zJUz$QI?LDzO)x4~oetu6B)=zu1{d0`i4TDMWf4CS17wfU5V8K_*B%eJz{keCh>wp_ z_M<1uA3pJqViDyag=3ud>fg>)un}NVgt8eHWPO#Za<_V{AO9Sx7b}*5fq0g*S&JQ5 ziAT%cSpn59P>t&!dlL)z&XpSQIt;lI)h7#g2;Z!N^FaT2td_nBXyhA8Y{>$MmcASh z96%1u?;jRg^5?TK@SOa!5?@5m(b(M%Y^YZ{GzyR#?E=I2IeqlUU3Qay`ovYRENID9 z)Y)of&WKb&EkiKRp!H>x()$B69NU1YCy*-MhHbSpAVljkUS;Q5{+n&QE+#Hspbm1W z48x{jyx{0!qmT9EX&auJQ)|?_2ZC#%nt(MK?~LjQt=(^I<&!C8Pag1vp-l%9$MHpo zReX`UYthDY_^~z=T3}sGW?uh+(gCpp$v++^(+j;681U2gSFICBy@?49gn@NL0XPN> zU!+)BrSRI`mz`y{w;0brn_p?uU#rtU^e4`$->!cgOpG(OUD?-}eV9soa7$P603N^U zOXSV=Xj6Oh@>lfjzQT*0S-v{iZ1bnLCu$$2DWPt%zp%^IUjn;QTeCS6$cSgy)+c%; zjU<0u_?5$aC4(&8XULov31=3EXeSZap&4rdgY{*Rh&OR+*S=4TSPNfy@v6R;Z~8-5 zUfrksLf2E?!pja!@MAcRE6K^#i!@>cW$INQnO` zeXKHrRrUwzfWrWkQOY?6yrgm#D`)KFdq0DPmZl=CV-3Iz*{~&hL)ie^S$_`jMLoM6 zUW%1|w$m={RI#1ic)BhKn<+1O0xDL_MIjn*G0Rk>yPu>PyRR(sM^ewcZv$c)Ym*M> zU^{IhYoR8QHd7I1KGejW;@3-nvVy(ns0UanEGkG5>cTPkMu5HtpbHP!67S5~u-Wz^ znij(Ahn+BvAK4|!-v8Qu{{r3*Z8lflVzIIYEY8mYCGcZV&o;pd>NwY%sDm2KgIno9 zgMk46WW(eT^~wlDirE_QTJLWor#x5oWy_fI42ru<39f4ZkwACz;Hl)1v{B4HG`_?~ zJPs*__#!Wt*R5LDcocQ3ox1zW>xS1gA4T1WQ}@I2x;5*fM^U%dsasKBMsMA^mZPZK z;M6^#yl&&Vwj+0g%Jy`qnS&fXT7CPJytlsOz z1uqCr3Kp~MW|*TN}~ zb15U`0fTJo1E4G~Qg{KmO&x;=iI@~IPoZbx+nvhQR$ey9*ql8X~sq$6L<-VbL_ z%oah@(7M2}8^~VHWSKo8j{;c%l`IZquJ#ws5PCC9cbm}zXlo)^r_`ri86Z@u*j`1r zRac4z@5;1>P{^8qW~3z90%+}3=ma4p5_!*-?5r6ib@=W63nQ6rn=O$l>)(*~8GV^3 zWo5m6g`f^Cs)3%>TEF>3NXS4X3!f)~W@`<4ZboYj0^{AkqX8YTYQQ2v0LT%==7^Of z9ExbAx!8{sZg1ws5R~7#9`I~00c>-vVd{wqG|q3WPCjscYb5bgrR=tYY7fB(s(?QH|4ClXOIAr_!C?o`ke+DGzd>tQc?e}rk7ojk>a z-^;(@@Vg59uC}HEm_VW|t0S32qzW7@ob2kSF|*@%nxE- z)O$re-h-PnXZZopyg+1k0c5vglfETngd)~cc#Bts-`XuGeW^ARkw$Y~q%M6>OLbGq zhx9kc>BVtc>d&~#+#tfdkB}$pu^KOd!)V-&73}UZzmYq%l__q*r+>XAs=wirA7L{l zSj2$E%HxdP)?4hQpeHvda-w`10oND=OgvV?yVYZ-zh|&XNmpk?8XzYBI3mYBByx*( ze=b(b0VE@bMsjvoqM%6e8an^~`B;&Amt&7a8_YUO7&;=Eswoe%ibVQ3;Pu1_X!uHF zu*`Lf{nEdJabduX*otn%5E#YRqM=huO4?j#Ay~arH~7ncaU#0HJx28nZQEp3t;gyLfkcN8%)+ICc(u;C|)= zK+Kvm2==xHrtJ1!?KfHj=tv-&E$J-UM}G@n4Fx8iD%7R zKS*@Y-A9~jaBN-J7(=3-d^s1ZW);|l!l=CpRqFTHl_Xzq<-;1II9ZA=3I|$sWpOVB znwb&tf!yNFhlTP>*&nVqTbo@mwueY+{SNK8XqD0TzIIbZ_RC0nqbje7su&v;|CEc8 zSsn@3Myxgy3mUWPwBuq8;064H$2bR2icAzwOm_LFq0$mMeteP7318}P;7Eb!0$IH#dZiY$(nW3WBnzKGU z1ZBQ4NY1^T_%>=9*ZAqIj956+O5Va<`K*&?f${JReJCwjgYqgmHa(d*O`aGF(Ri1q zf_5nm396xx1V0@Yr5r`Vl$ z2mYZQr)?2MX%S^0w@A>z#PdiqBd}d$d+6=wcX5m-ai%lgN;Ss#fbBKkIVjRbZd;=l zy~#PYJvrtAl%G^3oW$W)*QBAiMy%RO9)!SSRlvdeH&qnsAW995v@S>Eg&xH#@DyI5 zgO0O32z@a6kWn$)2|<4+i2lMJ(&eW)mPOjbDC0L{jZLwJW&066 z7z~{jaTULU>yX6D8bDxVh6XH^{YxfRo_=9`HCEe&mfdj{74K@zd6B5OFw&B_EyVQY z@xMcL7T18&nO24mj=1c7)7YZ_$$#ic#xyOyMStCgfftEs@RC;F+e{&n=NgM^(g#<< zgVfUPTys_a7_iRKY4|Nbp9|;!jUp<#L2S|w23-2CBhq&Ok#Zz`2z8l7MGQ|W|EBmI zt!8N`d~2e8GX>FrF(IQ!P)D|If9l)XhZ(D-3jwM(67lpm;9vj>EX+SbsVZ}z-v=P* z*HY-06@}@#?M3O&KO@*f42&pL&wLPoXcygIZ|TB40}Whl{$%@c)*LHzYpFMx29tK6 zl3&W#!cQk3LYkoO^(X1&l+YALS+fQ9J=bp=+7I5horrl`b;*z964O}BjtS-Y&u5tb zyf68W_`s4r(2vvs8lV(j0o}mL4v6|-cu75S&|81SU;kc=q#pz`U`o5KB3M;eLy|CN zo-l=+LQ3)yy|?QZD?yc(z8Qbb8{iYQR40C-n!kWcj(&AE9z4W6?OQD$6NBHrS8|H< zmdMVE*&w?&c=^BZ66oc_sN~u&`PIFj{eScRNSK!m=*_w*Fnq86h=7F*1M16ug5y>02nn`Oe`@co2w(We1ingDTJZR8Ib_h zqj*`Zv9*lLD`qjNeC=J7N7~}+?k9GN)3+kM&{CK?a@#`R9ZKrw&{fHeMA=q3$d$hV z+v?vd78Nk#fGaqW!xETA{!Fk1{ag07(0!_f;X7jqpw>Jly=wze~r>@G=1wyDQFiqxc+W0DQQ7tM|!DzbaO=C>cvnSOOy9z z#}^PulR7*gaLG?D2{h~IKjZJhNQ3Pke|lh;82=w2>4d71K~aq8A!XVZU}os|vH(ci5bE_Czx)rfTFvJz>=c(b)Oe@sa`=#RmvRhSl~BRCar>USKB z$GwK7t=VENLdHoB4*C)k;M%qH6Y#NRoLRXTTq!^^gnyoiz_AA)iuu1XnNbeb_&YiH z8>v{Ezl$%#FH03$V8-8bQkmE-WC^7w)yCfoTWtQ_eFL87JMf1HKM!_XS}<6yO#TE3 za@%si0}d2LlQYNs*j@eu2@pwwJ{dXJWRg}00Snb>Yr387&6Wj=`=HotzLDQ9feE$!N0wjZxYXDm^Hmi+Ah7T&?Eh$u)QhOEK z`!<*lE01>1Z0RC}@!G<48}V|v-gju2T;By>0gg7ncw4l^f$DT%lB`Xo(rT%HCq*UO zSXl)P$nTfh2(4xL5WrOfJF9lT=oW5=zK6jif5pkR-*>$MlURXP3n9kIxQE)$1X4@W zIhygtrg+1$H_f<8NPI@Tg}@!V0k3@-^g!W}h&tMIO(*|V&}Gw4wm{76}q|y14QUDh)_(ntl}G zGLm)OpR|77<4JzEB(40vQV=s6(%V}$X9k*%HONaSq0p`z243FQ>|2sDW6j83*8*inp~Qa8C?Xz)I#@OrlP(^9Tc^LHocDPM z82LER6?@Z{*ZPt#u)RSqRQ)r(`rbhdRv7VGu)`1BM8I4jM;I#*g3s~H{hly%^W{V_&sH|#ZEQhtDsLktr#Gntx0Q=Ry+u*e;YN?BfRjfw26tU&U zE&vEc1MzVoqHjV-e)~0}iYG&aOV5)wzD7Gh3{zhQN$1>E50* z;T^P=f&A6PRD~aoS2Sbd1^v=3J`Xhl<2_ITVb%r+0;mU|0%0KQ6v``f4k-|(ADgXrd)$C)?#%nJiXx*rWwXV|sLsP!zFZFpwf`xO78 zDe^=#4@nCQkn9m#C}x{6+G0xST$*DWd}XA?F-dI%+VnpIE(hPV*x3bOE?nIbRkf1hGEsw9LJ-ut${&Ma)W>j=9GIP*s(I z>jd1Pg}LT9;CNa6j{5EOd+HHlFh99HG0(W5+SsD3c6O6#yC(?-|Lcr@g0momv#Ha->I2lrYDY3EqTM*a>{ReegAFSGU1(0C8lqqqIAE@ctm0)uB*n|GBdd9t>jRaI zv}frDHKL1F{9!F3sGuM*mUrY9IM+5@H^`34Ut{H-Do!RaEVsJ=@Wkb=`_x=jMOP_( zm1ZmpfDt14um>0hPSSa1e-igq6VCznv6v0NgYw(!TLxT^JH3CQ8U#>!Z7AmDQo>5&il1G);-A^Q4XxTWihEULw&OD$n(?|B z_hUj~VJ^x!%3`jI^5DN*v=J@i66FO&X5jxk{9lS$5&CB*A2VZZ94nbDy3+yO$;AW| z^hc#&3Qf#Mqec|by~AvY%r$4V=^qsn&HCPP?6qB>4~|P*kHxmhFC5f~0@k^Hk#+;6 z!kq{16a)AGF0-QWK^Nnt-X9h9Rrt8|FqS3tP+~~E!d4Ymut>#~Dx`^^L@A5o!ZpP9 z;8bM^{IRd{*Vd;6~30 zXa%|<3~_4HWAA_mgPcYYQU15|d6cJJ9$s%pldY&`7c+PO226J*!gdH8FYuY|YIzk! z>BobB5#;I*H@|Kn^T-%85osrL@Kbyt7;BxpkK@={>H$167GbduXD?bYYXSjb4h6;O z=_!jw$f$ubp{(H9Xn8IRMa;ydm7btMs?d$Kh=>@VD)rU|FV-vex0GfWnZJ;`!rR14 z`i6wm1$4<{n`rMA5)m$?cZHhH7<5cIo9C6?RNnxBb z%>?A!EKzW7#`=oa9Q{z^;#?!!fuz`-CjHVL@UFGWSP?O<=FoOCIU!R^;DD{Uic1OQ zkJz%)63Y81^<`ihInw5GWHQk-8ueJSH{SuZq5_ojWA9Ks1E5-~7PuOLcOi|ajpaZ~8TylaMZg=rQD(@7ek3kw< z?q;xj=4kv8YCSR&B81kpY$Ec$8dEJsOc z9#Qs8%qaD=#uij9uLh-4C(;QaZcJh)!vSaOY2jy#pWXZC!Gtj0y$Ba=)8tP5qcMpy z@%O|;xaXfh8GeSf)XmVZ?tT_kfkN!jOhpoSg8tZ-&>y=5I8>p>N@WR!aW&7njsn0S z0g?Bjq;$-nLJVimB73J>5;aXlmZ{fe*~MNoi58aP1S7csv%D{xN5 zK)iCnIhDKccfx>{`WD`F>bLnlJ${r1z z&~FQQ+3^~q_xi!pDw`(K01%|bRkhJ6NPI@Ac{z8K0(1T9=wyK>Ol;((n@y2k1;oyv zvSex|HU>EODO@}GvR?nTFlKbn4%i`yny@l<5>gRLB6tp@qQa4MVDQXG_(*Yf;0@-LS%Su++iXNAtG*e%R|QJCLD$zv=FrSco>03hnM9?zj(+i=l8K1RFa zT11{=}m~Lv;$FdXmba@uyIh_5)bL2pElIQ&?IGS=9G-_Y--EevZEP z5`2{DiI(~wL^4#BxjJ0y@CqbO&{7YA5G-DGd-2HZg1E24mAyuVHx0Cecd7<;fKoqT zu*iJMi2c{|?7)filA*YmU4w#>0_U%?-K?yW#ygV}r#>we!*dQ6EVG!WNbX|pZ8hw>tiQOT@a09k19 z%0;+9R(=4e=^{Yzxwu0%#AvAk*lQ;TvheGNR2Yn(xb)L}r3!g4+esdV@F0JPmCEI! ze+=Te6v2!}q2JO$zY4hL53^?nKkDMF_;fM=2%nC}5BD8hua@%Q37bBt-pO0;qe__T z{QzIgGKaxy?B#GY2W$caQ7Am$X+z7YMML@D69Ic&!o!n+k|`Qym(rKk`g$tyRBPD` zyd6MZ4dpXaoX)6AP?c zG1H>f|J{kHM~92>kR`eX!H4`yCA?ru4C&gMUI469!w~6S;jCh0e6x8E1(yFcl|;E? z>?epe=3gTe+dn%tN^Ls|8%@t6erT|eQT$Q;&t@cR?Z`$u5W43g+nT4ZY%3;C7M_d8 z3s7ewKbhZvu|nZ1R>0V!xSzsIbQ{4>uGecW^>}T$gnqIX-3M@x8Sva100#OUW>*v> zqdY@&ac^f9UD+DE;2?k>zTX)l(9(>P6a(~Q6-R``>jG5J2TsuAq8}{YmMSJ*7P@Fo zZ-YdNmCUM;Q!yr|P@GArwAjb-HTrEop@^Sm+iW+B;K1?)ya0!407@KX3~i@(xDBt_ z!rFM|(Khm_Y8sA|f}-;*Zy($t{;6efax5Dn7tn!u9{IX*UF9A#|6f!D;3xdx++m3y z$;-)$OnXuPX!4Si#f$tKDVQFu@6DeI@Q%JfZna9;%zqPGLbLEm1GIp9#FiYPDLIXT z#@RpvV9qdmMmI(q!1JS2ec#o8V|pVnOOqm{PbC)KD=ef!9|a5ZJp`fF5wtT_$avlm z+767QEq?}zN{1h8^W~x)XbheDc3zyGiNZ_Gha_O3^RW629W4%bm~%R~YG)9oReS8G ze?_~S^AP%Ag?0ydZ~D@r;VN)9-tZO37Tpx{cBSE1i!A!hc{WZZLTi{(-oqg<4j*gj zj|gN2rl+UD#ZT7m*v$K@JJNereBNwr-~3udreUo9S{NhyZPj{jxamMb!}uJ3M~$|_ zO$UzIZZV9yNeciTSHKlExp;F1#zgPv*rp-lQ+z|EGvGx4J6a-!8eYv6Wew=72Q zmAv*O-Vp&Sm#j2hW^rl=Ozo|5fMKz}+4Q;Vtn6Y0*g=ssrV zdX1~A&d2M_CuA*F^V3DNa>EY#&B{_&i>C+}IZ2l|o(oc`o08(t&c&eTBkSbWPTTg$ z4tfxf=0n0~weE7_!=tRRl$um)VAdwaPJA=hI3TE1qv0x44SCQ2u0i`MS&fO+e+(!@ zaU9#6HrG7ROCfGASM(huD)l0frF8gMq{``@d-#R*;tJHSB;I3^m7w0 zIh3$Iz)&fyXU@X45Av;}LJ79C@N5Gj33xA0fghWPYWjyZX^w?6Hzs3NZDn_9Ln-Nfo4Mt`-c< z3%OW`z1_S1hH_XjwJcJH;9Og1RC2KiX=cc0e#A{9eq3cOlrDJm0yVl4?ScyAH=*!Wv<~?C#B4c z3k$H{DZtjLfU;YQGn=yqz-PGFv(9AJg9+a0?GVKAn1)pQMDh`;APfjklrbrYW#dgV zP~jxPtel6YbFmp@OD-lf(EEskC@-LbaBMEip#U6WSjU3&{4Xq5+dH$fAN%kN_Nf+J zC|_qTYUh%kEb1sdu3e8YFjMvb9{Us*>Iyd zodE!;BKLYxyNZ?Vt}4#AyL?ZIWeHDLa-}KSyI-|`Yc*H$$TlFLl8efES7K@`s3ZG^ z9GKwx*C(C*yej-43YA))>$IM{eFXZnZ^j(lnw**mziqY!&8yn6qYj=U!ilRV@bLVZ9a?51-*N%|qQK@?E4(|A9JlBb#_5$rkc~n#je!q*S#-cWx$rLZ=PpO?V;L3^AufR}5k?CA32DmlW z&$o(ske}08`x*+^+7}VNr}vQ-;I?lMn%HWh0)ceeb)cC$P=Kt*!mk0Wj5p(L^s7@T z{}!yj$MVX|qmi44EIb(p#K`nalDUz?E4-1xQC!uKD2L{0NE|E0kSMz_A3TX>X12jJ z2$Bk5gd*10`T!55k=BwJS(9SMXRz`!thE)j=o@Th`rT=YsQ-jx?e3?(xq%x+gtFDD zhPX-KqtXdRIW^aWF&~T%3pmlm-{?7y^SgeEOOITFakHvdP16OKAHLr+E20~eaH|JS)2pndaMC8z&{hLhmc z3~08*TTh3BJBjU@m01o0s^<*W*7r{WV^IGOTcOi`M`hs$le}7}w4!}@r*EMUZ;_=A z@%c|s73;MQCjOx6$Y>Q1!HArUQ~YW~o#zX8*z@<2Oy{E?6pKzC?eJgqcd7UAjGV~l zvLY&Y$V^2(z)w7C!G(_kcr>UUwb_sS^rxg2uMV&jG-S$d#VF3rA#8=2Ayho+DD*{$ zdL}AywP)g3jQ>{aTfC6cK_pH>7~(3nT$)_+6O$C$h_!Qb(J`n9WE$CWDc{G=Uo`K> z`WC?=P8}^}z*{ADgfSFg-qr!r=h$qR0AQ|k_u~3DzgdBwvLXb%Py7tf@p}jH%YOjE z)O%?z;{DM^P$H}A{s~k|w=&$t+P#AkC$-d9agVeT)uc6CRgh7wj=K0{Z~5XA=2~tO zTKY{|AUc#Sjeh!--MA%kB!rlW#8|~1wLuq`l1pz-vM<$04{#O`I$~TBipqjg8~GJi zy@q&|qk&GZL0CFlp+MbRDb<%rJj7WBa2Snv`M$QIBG^>9VC-$}#<{qe=`$0N#!l_I zOW*|*OVq0R8C`?` zba3A;h7jwhfNaJ)BN1X1r4fh=kt6_=tnXKOp51P)aF4n-XL73t^9h_-A+q&xq`1?V zHYU&X)$t$Nrk2QM0-XyAL6^4aKw({ppBP1>U%)V_J-&sW(GWkst>ov2xDia)**O(= z???-jKU&CdC*dkdmHc(?Bf`;glaSs9I6M_AK8+`7G&pihIuhrguyF&%8hNIUbqemp z>);;s1D}I92`56aE_9@}Vl*x_oaTlUlk9v-8Gd&DGDM#FFO~|dlW>8! zN$Hg-Q48`R#ek9$L8}$-5oaJgi&I~#6?>6#|HVOU#HkZ+yOAM(9}3xgP0xoI^WQ4T zQsR;tUzve3gBEzrWs!xTvKjL8Xm|C}ouik+yT`yx-qi92*NA-$^IDjJaTu~Z=dXoFJfWxUN)|AvK}!kLCsWw=MNcI z%KcK>T^x_r2XQ!K4K0_aBBA3=>j^e&JYdxHMZbrtKq9*c ze3Ozi5m#F6V7B05K4bwE+o7;LSM(A9_$yB1*)jw;aXZ7=TWk9df#M2rF7-CjGu~NLfD*qMdRsK~p>ckJOUsRf;<~5o6 zQ8Ln_1`z!8cYRB3nd$+ja9|u{<}~3HWT`*nEv~+2Sc+SwYOrRk&zavdVn7^=4qt18D%H7N7Rswqq=W9lXZC6K|w z>oqx1`2VX5+`-kOcuBVbe$6nIm;y@ge7D-%Z1)CpSSw; zUatd}e4W9EzRaawc7WOG!%OQ5td1<3^*Xi$stXP2{b3Bf^r`*)#q2&j%5%GH_xA&P zArq_^gP0MF?W2%q03j~7V4A=c!F9O$1tJhfT_8RW2!fmA;bH2=BrZosajFzBdl-92 zKFf&;krq5~b-fCSIsvj!HGhNq&%;;tprSoQsickZ6J^RY=~_&1sm5I{7PAkI7c9C_ zt^tvyvHl=mN@+O}PEfK#J-OKc4f*^~b_x?Y)Y||&_zIX=f!~6gu>eX3oP+LQ4A{>n z?5giIZU`ESL%_Lw_6B(f7pg1VRbI>drhX5G!vMbyYSdu12h9bcT&ut_<^*xJPYuIi zAJUFb3x~JiJyvV$!{&?BaeN%yR?-3PWIH+gHE_0=oewpWlk6tx{q|{C3D~(KOLlLe zQY=sAOX#(E_9AM^PjR!dZqy(Aj&e;3b_xdNLRD0&d`{LwadwjdmlHzlB!?xP_Csen z5Or#iMnJ);tv~&m&1RxQG#oF`D)rjfN;R~Wq10C2XFbLeAC5r+!-|zbm7eSN(j$kW zc9PxhY*+{9smh?wpHN!IK5CfsUPpQvPuod_y+07*GWxprXYmfhwzcSx1kru~n&E(O zs#@z{KGLYYkVlG?+q^yf^?Y|f!O{FM|8}|Yyqx!mV}+`%uOTvs`hh<@9wQM&=iy*t zvbmHSSf$7CW1Ni*mi-g3uESLWrq1pvSB_*ymc~wHz?;q$@STav>wzGeS`qa zCDtI%vY!1V_`ASP{unPq%*2!>QYIO^hkY5lZG&^vUhR(vn_NtU#I^f2F(WXH;Qwl! z{jsD*=@n=Ze5~mDnkQL_ca7$(AUBcNUJ!O_-WCEb`=mt-p?(GJf&IG(2Um684g59W z(3V2tv1}WIWMsJhMd}k$Rw~3>!bCbgn@_^O!rhE-eqbANFxAjP5SXTLga^dQMbvmw zWd!wpKjSwpiuLw>7l}VX@U<57M$A(o$2r|A6xEa9hTgNzQ;@#eb-pKYq6DG@vtsG+ zU;PVFo|DWuDmOwa-~u(#Ml)+(+wPU77I z5#bQe_rV5qNa!}4kXUSoELBv7!>Pvpk0xcW!-qc`&ZDOk;+)}6Xvz@6fLx=|Mw*&| z>+$dl!r*_fPrMA-=Xg}GIV*WXnPZ@SfMo8_4~}T)d6_Df(savw_cGPO|0nc*bp(Hh(t9gr4o0O{@FBhZSRoC6F@j#ozk8|V)mRFRb@`5E+xjsq zKewGTPrxp7&W`#8(VWWLOG&%9U^i_qo6mA*vuaGQ`8}U`v)_pC>t3l)Nul%3Y>g0A zLn_A*dN8u-ga*lSX|YTYs@()(ig8UaH(fvhld<*KBF~-D!)J5g$B|#!@Kxt)L^o|| z$>eBC@qKJ+=aDv$!yg6^}Vr59>ACStIZC)~~F3+GC_>9Ft zmxaicoPB- zq8^R{z||fnmS8xwS*DO;+*a$8W%aG&SkXlB9`W926W}7uNpau_*9XHdF!1=zV{(Lz zNeU$N7x|1fpW!tw@b%f9L}LtZ2p^7J${z1Hj=0@{&C5K@{Y0Z3kRAU>_y*^n@@U0Z_R20H__A8a^cy&2EipbfO*&Fd7x)rm z@V$WxWF1lhJy>WNHE^o*WflpD#FuP$Nl7jp#&3K1qs5Uqf(`|@ba|9$>8Znrl?5%tM z8dlm6S<6c*@*png=SvFTL#&ZjBLf3+5)`k)Yj=(2L%6|96=zVuv8pg8yRgk2M0EEP zdTGZl`zzi1@5eLD9MEZ@#U5OEzzhiRvqciZW_WD$Jk_$%4D;n_0PMQ8{T$4}+;A#_ znI6u-t3-Cx_EFy0YJFDKfcl>(+in-*R6pY23>HEUQxSR(@bq>2r=4oj2eI4Yn#^N1 zhEL$CY-T7d+jP`r==cr@3%sDJ`(MMA;)kDL3+@a=;!AEz2kfHn9-*iTKuR+leFt-E z{9s)QN79QN+4b@>9fAOZAgvCMDNd4kqEROrvXHHcnL$@d-SCSu%!PztIZC5As}6hA@eCLoZ!E@{)hY$E*NGp$CIo0qT(ymTut;Lq@GnC5_8RL+5~Mk_G`bO}_q0X@xH9BPF5^TM^TcipPnyw|5a zeuX!)c))nK>%$34Zq^HR6L{ugowo6cG5VIVT_2yP-MhK#mD{`-LCa=z`z0j|h~%5lE>$Y*<&#$=bW`1pelv z^=lD3FthoA-1XRZZ{QASRO5nKe9Y`-_~R4&5WakZ$=z4*1aNssB;pb3A%jTwDqB`t zNYoZHQbRB-caNBm#15D;Q+AoJ0Nd1fE_@Y=2Yy06!17rYyhAK!C_`b_^099LO|$IN zEK43D!Bv2*hh?vcmYM;P6z+HOQU_HM3{>;nEC~J}9@GI&<3oUhYr{&khn8@n`_IwG zRslhcGw!BHN@E+!CYo3}?~!e9C=)r7BvwrWDMJT@VSvI(G4 z)`XD`13)JIqRo$tG(-N%36WIRMj2D!#8rn{V}-Z=MoNM*$5`JngFt@kaJE^auuYy} z6>VwU1@m#~y=a>-uTU@prozT{9`oKJbO`3Nrj)eBWz#^DQ3tx)wTQ7t<@V}(M>2j# zkK&iE<1&>7QhT6^VO-qBma_EkZSu13;2o=Q9g}FXeF4wSrD>|2D9}X0!?P}2rArE| z+?^5B^`YT`BbcQ_YEb!V6Rr&D$hIAs7-FVEIG$6yd_w0u97^QHwY*I zB94JrNR9uy9Y%>S)*l4$Fl%4aHVnRlNuf){GiSXz4R{VLH_tB*IN=L{LCwe2Vv@Fv z>#JJeU}q%Mcok3$EF5m4?5i+ZNT|@A6OGZ&NYMwQ5qy9MgD|Uk2ggbNBPD0DtyY;r zUvbY*Gx0;g>NA#dzATa)E|BuCVT%jxp=ZnlebJJk;Ln>77nC*trFT_An8&6VaQ+#D z$$YTv)U@Lk2jqiD4k$Eh@0;QhmWi^dfk4hbLUG`soT+wIwzw19Ku-Zg#lNY0L2_b0 z@mu7W3UGTF3z*dzBgz>w-YJc2v;A?4^2fDE@vq9SM{$=#4r&5KGswfi<-yEFMZ?b! zb2m??2a&c#bOE!x2ajx-aXbYG{}Nc>Xf0UA1J^0~$@`ViDD#vWJVi6UO(>JA=)!AJ zH1KWb1C-J_RYTnF)1*e;tQz*_4fhQhTwdoB_f2-#o6uAT+L~c+&T`+}H|$N^eRKb? zH&?rF)((4P=#5MUVLdo5}9Y==eS(9D1{_(Ol+Q-r4Bzr1w4b;(~V(f z^JK1l>ciIv-F)H*`PzH$(!SJq*stMy%~Qx?fWf$}mRdvA!*Am`^HQJjbY^vADW2p~ z5iXdkgMC@26FcAqMNM;?k&Y}kbruWRDOY_je$|HPF_1KJbwK7(sb2Vs=`Y-?l2!PziV~_E& z3rBrClqc+S=Dh{rs*Puj_my9jrm%_FT;Ic5_~Jw^X#EanQPZPPps2=G7scn!$Jpd8 z114*~B@f{O@j~I6WyFKDMxm*yyPs^P=WsPh=&u2;rF;#?rvi1mHS3hu#l>BL`EZVmDnW|WmZwDRLBIwa{Y$@nghYiYrd zKDwfrD?Y&|!k1to89R%gMk?Uwl}#~mUqHCn7Kh(M`X8g&3*gs)cVa25%dA4oCvgTa zaVG|x{BcNd;`w|s<;#o-t9|SgWCD&9L;~_MP*%OGW_Oz9&!v=TsbeEZi4GwPF8ogV z=hEN%KiV&Ym_r?mY*hQD!)1uXif@KUF0lzn@UyX1S-ACeU@J!5D@I+CHBKfKeU|Sv zHmW%FMxQr+w>RIF@whEWN`bjJ(9#`H09e%X2B_L#3lvPaQ{OCS)@g?^U;JNqF8zas^qe^L~Dz;7P)6@GkV{7-lB zZ(#Pvc+>*0Ge(O?Er3esP%IDl|fr8Ce3L+BYwX*&Ci3`<7*c#9BcP_q0U0Bsqs6pPws1yZx`ttj*9QsYf0u&;-@bOJot<~;g4Kw~=rr{^7 zLnL|4G}oEq4cD8qmnO^qC!uO4z8=Mftp4Al_~@?wUyE}QQ6Ip}`28g?+W;FL>QXbl zo-FwmhltWwz}#~2Y#B~>FT^8zbXN|WB35|Bl_X2il0E6^LND&*24@dC)+0AQ>dV)7 z?ZKtgNod|7n~8@*EXX%o>U_Yncn>Mf#qT3LBfdKS9Icx115GzZF&~jaD9rbkY~5Ub zpBZ1PTEKYY)j88Sf7cjnc$0zqpb-UAo*wcM z2-x0l-Neo+KOC0k*AUF)hDES~O?(8;d(T zvA@fV_ZqVQU=8?JKHA-l+c1{QN{ zqU>xBZ2JY|O#D$a&g$$)S7qWoxRr5GTu0rA7zUGxuk3cWPm?a&i+DVMnzMTtNyn3M z@HqsItMs#FlcDI7UA1gq;R~ufIU;1@mKjQ(aPfKmaOZxz&0|LDHP*2egDT@A@vOLZ1)C&7Y>^*^0iaUfA;%jiA&U?-f?1)t{`{+KLmFaeq zsPHH%sI%Wdv*T)5HlH2sO%O!pZ^auDbOG`Yn$rE?0PlZ(pW^TvFfB@J8#w}~$`e{T zGFcXzcqbf5%TPqg1_){)YLp#N=}>hd3adPhjOW)C>_L$@wNmY36n)1TInH^`+y@E9 z!$dz@wqu%cOQ2fu{{a@?1tfe2hN@N%sK!8HXVy&KPn&%JLy<}j!`BWzBuH8$TE#!G zF^SSPPFwOz@LS!$``L%C??B=VRm1wtX(1kpZQu-o(^rXi%6}fk$uB?@eXuoFc`7TiLS>N9vrBU;E0O8`=t=h)1V17i~7~}f(Fs@^8t;Utlt8itn zCWI@$=#HNp(h2Mbam0lr;OY}_azwC=&ORbJY&Ds&*vA!5{^RgY?{6rrwO{}_LVVZ1 z=O3#EkP~q>9GnJaNtC(}&=k8x30Ua_5TaR|@(U4f#L}>Mig4^fN(>yk-GfK4GY!G_ zkD2&tkXbgQY&Wo$BneoQx;GbJ6@Vx*%U6M${H02%uO{O>1hV$HXe8(1STDX4O^1O8 zKK!pOT4Utu~pWvErS@&dDG|s(-vp z#e5WS_@%zo55Qw{j#$KP_(^b+6r|>1kXjz=Fp2qnYgUjT?2FtqdWH4oG#;?EdGJ(dif;&78#3|x_z+mpQtfvy;%DX(Skz;W zDh{v>zDKdd&^Az}7aYUlYkYHP)JANyTQWTm$3tB>4DjM0gnCit5^o8(&6(vRfcNOR zF>D=TD5|YXR`@0SR4liFh7%W>bNm=(x?l*<(b))$A!9$_l8F!Cc#0vw4)%}1H#1wY z{U3dtaujVIr$?fQ(tu`ihXuGm1+@7%0G5@2%frmf9#ckBjC)13&`&^~i1P(e6yAZd zFy#$mV9CVtdk`$>cNi*O!YbUHST=K`sViWUiJjn)e>G4e0LG&Nn54|VunGMJai?;jObY3n&n8mKH0|6)2BDRhOfhGb7YB>a`c-w`5r_oF-Y(0no zO;C2p76;pd5=#qCb* zgV=P-u6g3v?tT`Pxn1n%F)#Ncq}5<__H-%^r@m@se+b66B8_)LfvTq{;`oErnm zC}ST^AZMzuxV45(%uXP%VW`xEGrp#F7kMr~k7RHU&(D$~GT7tp=77E;9Yt@yW-Qu_ zU}Cp6d4f5K&BAds>;2XF0NerX!I2{H4AcTB`|#v1gX)k$HhW(b-qqOSj1d`U1k!}} zC~zsyKQMlbErj@@L>W6dzym(W8Uvf1ay}xNHIvN2V#^Z4mdWsGv62%BU=};b=ZzTs zIsy3j&m-Qz8GN%EfCn~i^|M`(jr~0#(Gyecc^b6Nh6ixC{Or)e8_opfo-Fw{l}&qu zgAgsbm}t{K9FsVwoM}STr!dWd2Oe3a9F4(nR6ZvDs^oGxc;V*+`;x3 zXfDAg=~7S=6GOu3F8o->Sr|GthIe+`^vC$vn@it^OBm!}j{AQ6#N{Dewx-wPuV1_C zb(B{N5e{Y2tMCk1$vB2R`T?fW@`J;NGv;$R!}l}MX+gzdNHMF1G?4v6fiSk9KSr?Q zmMk>|K0cGU4Z?zciaeTrLOiv*ZU!Ei%xe6W!GVr*xhTPBW=JJcVtn)xG_X(6$~+{^ zg1EbDF7Bolu^2?V>ni+IWYwwhYP2om6m}DLS2O(GOX?@y`=I-%P`B;`^pnjqOIAUuEJFF}J4BUDCbP6BY!C1@( zJV6>Csr&XI%#`1w`!%Wqjz?x{vpJEB++PS(aZfDezDeB^Q@Q89sjR~~egGwt-k?w^ zIjk12kRHd8jW`g|n0Hyeq69p0YJmw^;nCWr9-%(M7_kR2@Pd)Y2 zp*J?abnnBHy4cjF~Ck5sIO z(%N>Auf(%bU-%3l2OQ_3wj0b#tV2KUX%sJ`iA{Y@)#*Z=!k-{d+uhL_e@gV|q*B1$ z;p_F38^usL4kS zs?3GqTF32Dwe0M}O#n*S2VnvvVb+-`j3MLai0E(U4{r3|=d!b`I#z_HBHW-qvOb4% z(108J-x;lbl=UwjS>G42n{NSCVXD3`%}xuCGM+vJM*JV*=@`h!ji(fo6HlG*ia2zE zW@U|^5*lEJM~njtflGNj#rzm2Orj>f38ighri?y(yBAmV;oHaHiavb%SX|MEZ>Rr6 zA3j>4GGcD}q^1q&$CF9eyTC;^{f!8f?&PD)_sRShsxZ&;pwT+~_GRzHu5X;Z3c9Tu zpvqKa9XYgF*+o|D&{iecv)wAZN7JY?V&H7=J!bftf(V5g6&boN12`u?LTJ^zQQbk9+h4%)DvKdRCLKUc61998KqU6uB? ze42C^Ca4=u>C->8G^|xhhi_M~&;QiY?2-|-Q@Z0*OQWzVeYb+W@Tf|+uU4ghcvPjc z|DsAibX2A5(yH{sM^(DZ#{0RWD!u5(3ii8hX&n>2*x_cM=P}|N5{F$MsvH4U3lI4c zxU*VczMl*y5r+G+PP2K+a}?<$VDY`Q_zQ2qvGUQI2=gx81I;K1Kn?Wir7Yi zl$EL8m!W-JiE=!yC|0|d$>`XomdHXa{Ryfd05s#d%nqP=`o8Ok2CmL>-uVVlnH5k2 z@l4P3*S(3GSy0*}{V9vI)ReY_XvMPk*xJ~%=95jWU+u}5S~syF#M zzXy_f6i^6&48Tuf#w91hfYDn{T(2lv>4ST1UgyF7NoI=$0>U8dDg2NvA8_3OhDIv6 zdD6s=!l>V<^yBvfKK#*#A1sBo=I1;Xcw%n>tUp-zEAkU;>)1zb?GtX{*Ft`jKK~># z8_By!FP0eQ3n7&cY8x-|lHgu%YJ1|AMt|aO<dxVf3QfGdpk%Bm=-E|C@zpfcK_1pE{fEjGsdCRf* z4B6Pr{Y^Wy+jTN(GxR;w^ltK1cuVV-;I=w7?hxKd%}n*i&99B|?lzWUV4nZ1C*hLJ zN|Ye>L7hZXCs1d49D4}dqcT}{($sC0fgT^sp9p&svdoq$fr9}BR7f}#zJUsyy2P^%ir~T$0IUx* zUY8}>Gt8+CKnOGTlk85KvbS6XA5-70SD+FZUsuMLrf@e~_)LkOh1PH4t6&y;?6 z?a%3Z4?IBXdJ?lL7o5dE`r!B#51Ezf=S1!9&jR2dc}F$X(maw(HXLi|-^)*q5^3p6 zpjCQbp!;K%)b#5>r?LDgitC+-9xKnnU zSMBol;V9cl-5(?Pcvh~^Kki!gJsfpytwV=()z4T--x}!tL^UGo3s?Bei~Sjar)_-3 zh!0?cAeNql7MNZ9izBnUJ`8H9Re;AYsirw4h;y%}>>gW&1E$gbgmWcPCf&RACpN}e z2qMTMvSAv)K{N}JhiK}iLo~m|c$V@5NW#-=QFq-^(Ei6R{4q99oV1LLNaY|v;a>hv z6s6?(+NQFmJJUix7*L|Ws{3P47Y1ooe;>$#7hr~V_l<)10O+^=1xE*;g)Q>grz3EPawrK2i3&T>aK;)!RP{}{TMZ5>=&zGT&Tox9Z@tKL=EhsbSPk(Slw)Y zS6$y$EF5&q#coe)>l)HW>`mzftt>u)C-kfI)g4_fJc zHBEm@oQcTqM^sh&wGoFOxtg9Z`4WtwY(O7(^?Sw8OhRGE_mkB416e#l>miH`I^ zPiObTHcD^J(KdAxr&-#j-;cu;Ou|7$W2(B5iiZ|_8h`$~#Gmr^j~~v5gtxE6f)BJ! zx3d`+8S%sQy|WPI{pCcQB1HVrCE{O%h;gtv*kfn+A8q{K(KhLdr9UQECDT2LQ|foH z+`;JQ!-?@QwrzS|CG<5-e@*7gOjqgUQR6SdMKa=|zu`=(JoU}dgA>;Y<~=Uv2zX$B zfV!zlcV;`gdu;sk;D49!pN!Ww-3+z#mNEZSbi)uw%JuVjTR+MF9r9(ytl5seriXTH ze(O}Wg8XLtf9>l;K z?n>`jX_;*SjBIyrW^rR%DM#M(TIn~<{?hiT+}ejZ#=tF=-Fg1jCwR`{RbDJ9OxcVF zbH9OV;I5nk=s}wAxD4qxl-)b~nYqcj4z5AI#|c``&CD%w*_Gth_MwOHMHI2M^c0V$ zsZYCPHxFRc%g;!a_0#;Yog9)#3uXnUUKI6IaR(dp_a%QsioWlFzJLCn@A_Zp-GguE zzVP~wU*3x)Cy7%J_WQKdU4Vkeb&qpYNZW+%G&peR1nM=OVk19hI8iQnS^YsQp-M#B zFjCNkln`H8`WjAcoPj9L?_IUw%EY`KMR+b8KUY(QXMQ`XXVGi2Zu=s*tEL&KKL`7x zi7!6R?nzJI6Y$K=(enSZ8`QPIhodK0U&J>*ltO2XK^a)-Jfpa>^Q^B$?~_Gj_DhM^ z#!h21W7cK9gIlAF{NbipWJS;%+x12``5ALocvT>Xhdg%_f8|sikSY5#xGHSkH<;=U zd97Nu+u(fbbF+*;<8z&$s!F~ph;9acyW=nP+R4iZCeXCi*rHwh9Ck9t{Mv=@H676I z{sl606!JXVR zVQ=O}eEC;Pt;G|jfS=%a>{xhJ$P2Ei4*nJ|NSqb+4tlLb3yP3&{ZyM-;ig%U6@S$p zp997n><=flqUT56S?662V6(!7$3tT+b%%c1>`Ihl~2vXkC0 zu(I~gQP4Kq^aDMfi1+c8|z=HnJ zt_aq6ukZ`{L*r>uO$){cl?HFpQt!b3;0rVLd+=$=bgvQLrd_xj_vpIlea9Q|rw;a` zH~A_6y%e#{m+Wf}urWcHv{o2gUdhf}0AN(Pl zC0M^*`rap_@2xK%xQSnNI)KAMCJbf*e;E&uT6zoqV!S8*l)2pBweOID6$tTdwDx*9 zW%QDcIM`)QasHM2^ zK--Ak_*J`B&D^W+4X=v7jQhRDRlBiF;Ispo_By@y++*S|8_#ro)TFgyV0Y%eRk5P> zn@{R{cfNCQb6ATX(&B^0feC}jtu{>spldT?DNtpHboMq9Ib{p_;CF{W5lPficT$MU z`F;e`YdZVB$=yK~s&_sSP#J zz)g@Up|J+?LHLe*NVF&k`5@7dq{)PjiV!*payks=wccBMOIv!?*51;V_O*(ZYQncj zD~QDaLZcV$jPU~y0Rc1bZ|!r=%p{rUz5n-pp8xy*Kb<@~=bZhq_S$Q&wf5R;@3Rk| zgjrvu*M~2f7mvPbh#ZP$mpu_RV-8UtT3z3jLxRxy(JKEfHQ-SXY=V8@7EWnVgOa;v zC;Eei#AJ7NTd;4W1;5(u@>L&K+b+SJ4MV2ziT1naPB5^4L$xw^4I-fGH>l9ODpK_-whNoI z3NFFbA{iBD$)W~SpfZEA9(q04DJ_BSAKBoAN1h^wNl!(};AWxJY%Ab*#bs*ywS(#B zgLVP>jG8C-8$-cA<3RBhbdl~au?4&237>tzzThm%MmOst@%!{^mbX|n%V>wIHHDTT zulsl98a?Xmu&piX4W`D_??{?eTyx!n?D(FMX=Hor<6`hoQ zs{KXR#@!6)Op))iYjaPC9^T=ayvN&l_1WEt9!HgTWePo@HfODj*Y;$;sy)=o=f)bcLU+olox?NvRfbYz*IZSq4rmX^8J$5!)@Y zamV<~pmx}L%6q_a?k#P<_w7ra0h^WaGyQ;eTASW=b3xbhlJ(lTZ`Pcao0|Rn+h;vQ z?t@IVI=$Ui>&O1Fk7~`YzsMBFa_-%@f-YZkw|^efn@rcH!`8KzZIQjGzK2d6=*d3G z9FUew@A~q2?cO(=uJPSV<~OxBw0lLmg5uLK^~dxbzI(T7bHP@0;rdNVr|yR&N~+U$ zi{_amVjjg0q>3x10ZYJ&dMfD7F4)iEgS?i-^eVo!!__uRv}-{J-;2K02keLTr(a0l z&xaI<70$ zBYNB>Q`0YZDSNk2slP%ueRL~F*r2I()x4LT)jcB^ zZ|~Ce>0NHwYK;kZms{H=HY>QlwsJZ9`p@nZdr(*4$9+Rv-f?^OMr+lZ1hOjL^Ahtx zWe~iDL2w{AN95t5ZW`ih`L2nplmPleHAN@8rcS2o zikAGPg4W7-jwO=@)Ab%AVsw4;G}ZOH3zZ=el#gWTctSO?`R}5qUF&a?XuWF97)NW_ zDixJy)!&ispevG3oa~5W!490-Nq+Bu>=zt+?2pDYZGH!HneWf*XLFO z&!022=gUU>%=vnRzZx)2U|ct+39p(1`;}8c#hevn_cFh#eBJC+`W^dyG&KdL zaSB8@n)aDUHiNFhG4S>jgc0kNF()1`gvBT8c6NWkaS_SIMS}IpEZ1{$k5_54lQ{A} zcsfE|`~^lm>~IZ%L2$V-K)Ifqoz!zc)|Yk~Ur3OZQuZJDrpC*JbS!e1{g+jxzPyC= zQw-Yc{}TyM?_@ztI(jG?5I>hZon4v01ToRo)2fYLL-N`OIaibb#uAnW+S(fl9P~W`C!Aw>hlxV&Z=_g{VMHwgsId zX7%VFX)H}V!`p!hSJ(QdAPHc%F$Uv<-c78PX?=pu-;h;G1a`=22D_}yANBlP=@mQy zOxx+})>$BYF3&DRcC{7o;0@a7cvH^J`M;_!YW^R*g?20clZjrSg7n~|2xls?#jA8x zfBt~-x2p20Z(ObKLX+pIJLE64y`WU=+uEK3?}@|7`<^v@FMIG4tj*U1A0nT^U*I3G zcG3n{>yJSID!m_+t-S``bPjCjY8?a2;BO=GMo+1_6jQd|M52Mk_1p$!ea--)_z*UQ z6+Tqdb*;CM1^c=A&%v|Au)m_sLOXm@6JQMO6B5rIhIl&ryg-iNtNhbCgFCqBv3HVR z?ACc)gFm5!S()ztkP*`|&%ZpR&tWrA1}WNkBsaJ7{Pk#VdHEt~%c82Ps&gN8hOXBR zbbe|}|J0N6l0-r8J$zs6+(+6Ta(7;^X$R6fn|pi@rg=l5=7imOX|S&T_FDF5Xad*t z&$K=MdA9WZ*#}+_@;@)NyJl?DP9sbZ7f+QOSsVC+7?*izuoZI40NU+dy;nrvyQe@F z(2oQv-e*_!gOWc{JB%Nc59{cMI9>cC(n91_-%gOKJh#hcp>&Vda(0dG4@;qG3F<)f zqqx{^=%Gq{R@$!BpUTQ9=PWQc-bM|{0hy|aQa_#q3egVK_T9M&FtX(ZnLzYB0XA)`r9A>!N3I+fr|YRZjTp%lJ*?seMEx<(jH^K zhFcNXwe}NmW=DRk<_IT9b6yC7G9QG1&LOoz-P%P8-K>rQ_J%%N@h#u%gzPs~%@J+! zCTZyxG&X&uq)JzCHo@t@4?abs>HB_6H9kj(6zwk*T>n|&+UGoGTt#CgDT!9M9w`%2LCQIalB#jsmBa~g#X;z?NU4GrogWt0hbL$g-VZ|rUToqB|00Q zeWSzIlz<*uyNw9u9BXa?qIXXX%IdY(DRPzd}K##7V+3(n2v2whPLj zLlrpd&O@P>XqzABJIB|#W_GTATj_@jR&Un@%A{#K4W*@>kEkYB>pufssV+lcV(Kp~ zP<1mPn32jmqDjIb*#X!3G%!yOoRgh`YVf*FS`zJ!*2iHtmia*RUmRJdq!Ms|3t;vy{^`6l7dekBgT7vw5#VJJpJENpW-t@xe)u`iRfxT)5DR5>q#uBPq$4bV*XpY zb7?vzPoA?{S&L#iP4&_yqkdI)Ks!&1l7|JP;^sEC4X!qNlYwL$D9SJtuewtlRl{}> zJDQ|yre^*IYjAXA&}|o;_BiNjr*(VD%TCEPAE0)`lL@Z{PjC+(sV@5xyq!UH!*#im z^{rh)NBi%x2LD}hMfdw0j$?>6;r=cd>WotyxJ?oPZq19=4zHeS7fIpS4fs@!PL>6N z;CA%8@NZWj$P_Cm29i`@7_GGC)<7{}g5uDUe20Q}fP?UzvO8ZtL0VpdV(|qX zM*btpK_A#8`L2!R6HWIdeUtUF*A6MC)X9?bebzm^0(rLma27d(Q z^j7c#?jyI5P5Gt-U&F4C>A2++8QahYiK6sYjBa+A+?;< zc3y7s>}mt8TaYl3qwYjugDU1!YS}LMQKWS;A9`nAR_HGI#@dOKtKaXSr0?FOz~!vD zS830rJ}u934BrNBgk;=qG;V>M!ne|$MwJv_#jxyW zRMC}J`nIa-u^3!!t$rl0k~aU)1gY4#2xwKD}q@e9rg}rTU*_bqV`K zf88{2<{R)u9c^+wJJFhRD^qtwG3ORn+s9aDE#I+?akYLTTNl|%tpHB30ZqH06b8p3 z>dXM2uz5L$z023|T9O=q`0y3ni1pkG7`0MNGg5KkU5`bU>f=K&uC+s{NuK=n7Jq); zeoguB1P$$ixUyswBLd&2cZoSgZ(seSqIU?^^A#j($C-F!aPHy=V9G7Wx#QJ(LTV^T zmI$lQ;%Z$AGV(BlL)f^@dd=J03_V+fQtMP7X^*8!524vf*Ah~-19b4ozzmV#pD}o? z*1K6ss2YLJvK4vx7R~(B3~xcQUEA|R??s#EpExei)#@^9Y<9X{e3>MpMeSyN7X#nL z0t=pnO{M83oec^H%=~&(;CjyH(ZSs(;B>k0?r}eDH@HtVdlXn4 zP4kx|P)x*2jMS0LBJ`4}pHp%>)%P!XEnZ{2di7qi zn^aUpkr~oO4PVXVY`R+~U7(GY2A5Mzv4z28uGTB)YQ;57yNz3$$Dw-M)!Ivp@7`n| zpE*+feK9x=0BvWRub_|8vUO^?TDw72I!R|Nb|=w?CQEHu2E793!(=`uTJkYN{Um$$ zy5U37MQ69c#d8_=6}8l6k*blFf~wms@YZ|KU+p3(<*OR0Y}|Tz`S6y#Ek@Dk%<%hJ z=`1#!fMPNHF$aa0QMLN5&?*is1<_&qIm(Ia^R^BMlE|IOjo4Kdla7@JeRFDEt(OB( zxE18W=>=wbHG?%UBprNGaISz6KizYmXQMR zV&F%|K=dW1aiV2*k&ZE+?B9^uLNc5$7x|g67#9QX+bGGNE6~zCc&x&R3ok!)rSQcs z$I@W{!A`tfD7vyrTc-~W`k-OpQYw9g+G2yoL+4~6D&%VW4OF17Q30v`BI#z?uD$kW z1{t;K$=U@fO%;TKZpsVJU*c+F2aycuHY4>6Ei+S1Fc*R+i1qd&z(=T1O6Yp{Lhu!X z5OP$QS5_u@3P@TowK_^&}05iUp$?q&@CRHe-b1&!ZgB7rBF}URc5~H8H^1osqyK< zBT!BGrS$rL0X#Jf%VPW=Gl9TIIJSr=12gz-uoLDnS<)2mHN+FA26{8Dk+`bW!O$HB zT{OG66EXsu#2Ndj(FX{srDdPZ7#xB_p$**PYPAW}Glp1MiLD@MZc`x;myypdp zH}dGo?;{K99An0=BC_>gUmi>*6D9B;pj?t;LsajaeR)k< znNK1Vc3tfhUGfI(2jAv{iJgSKd@hmeA$_O`wn?>;-+cd*S13WY)YriGiL;v$UF-HE zGXSe#qnLNQ9R8W=UzX&z{~CH&{7IKtyL`!LflQyg0-u3ZQDo(jPi>w%#Z~70Y1yy1 zzWX-0A$_uxf+(rlMV?aRiL1;J<(ec{WJ*4XJB~Se4l}k4qfxvM2Nv#g<(ITwJ}YjB zWbF+c5h*N+Go{ax(U$x4lV84YGXSr}kxbknZ>OE)B=5UcZNep>CT1UN`UCCD#H*n` zZ$u3_@-9AB38@=LnnG^mwo)#j_Nf@!J72)^hBT~)4?cGdC%kta1Z>t*)WM74`^$A)ITcJri5 z-XoUuJzqZJ-DPKE{Q<4;lJ_lZ_GeAU$t>TUPW31K)^`nWJlx`+cqw~#(@2G7LCC~X zyU;US<~<6pc;B@DxP(D)DuS3=7^gRgNi+9gMz#YR=gsc}QB?d1xjd74MlM@ZPs-)7 z)NU?x=Qz>>ax43mpGm!>$7T@wz8;(A%hWo_m<#3I6hAM~C+VLYZ}+RjrSQSeliu#j z*eJmg46>}9^Zs4BDc{vLlt-C&$_W#`@oLAF>fj=HrpOy|ue`#P2jpiP%V?|4X-Rw^ z;>$qUP0Nb>^OD#=&0R+4#UcKL$BpdFhw%d1z1!^C{g<*kn}jYIY#j8b{8$7V$;$3j zqUoC0)p^m@Gpt?SaLyIvy4b7btNm#bFG}>d{xs*ON!p~u&h8=J)7P_+VY`U#6>v59 zjbpgkDs7#Xx{NlosRL7Ia#9@gJY>jkq#9dx_uXKz9Gl*4!p#4-DyHv$D1C4$xAm;ZL^SeITN9lMU!%MVdrAy?Mcl9di3qE;f(x6z&z zO5c#ZRigMw4OVKxx^3YxcrO*TXPxz)w>Qf>;VHWr%+c}iEro>jWJH75!llG?bNMnz zt==Krt9)Rq7!i%0mk!z}9i#vOS?SqtL%cij^cqS6k;tyL5ceQXDkH7zUYfU zVqXM$8AWybXZIwf-GT=hGL;`rEGBT;gt9|sgkgN_{>T5FzFjyc__Ac=d?KBk3gJ1a zwPBLJw*1N%B&AKo@SOH@F$=g)rw1rf40PW70@W67SYceJh)xc-_$wd&0SxXvKg9LO z3~?pw{0zcob*%nO*unL$9okMkqutfIP$r#ZY#r`UG#7;?KlS!ku}b7qYI67vZE7Kb zKFw5kTK7=M^jwp< z_r5Fhf(ABpw-!)^vKtXTN#S#4PVGaG{mL%Bt%v52H<}F`9At2{T}i}gS0dbsOH4Qv z$_mxxciMFYdC-%IaC5`qz~4@}9zKO*9J`0-u8yuD^M%_t=o4XmB29DJDM}q1xZpF* zjU!4-bQ(}1%IghiB=^)hEwzE_o+MFz5TR>D~mf7HE|$!3;hA79w8y<|dVX_~oOgmVE12SFi}yGxa54rPcHT zSjb)W9+$raOb-v4Lj@?)H(x%!_wdVzcBxVir2<@_N(B=`$Abda_3&wS`X27=8dXdo zi8*$7$&+FxVw;5oOYK0y?J>NwV^FD6=rM}j?>^Dy&ZvB7;@y!l-%H4&3%L1LwF&iND#Lh zxiB@BoF#IlgV55;mVt8)*tC%Rjl!f=)r_{8^n_?Rc4cTTy1?kyN8-45wXFfiuFFdW zA&6sWKyM?l1{k3NwG;tS8dxe8H%jI^go(k5rPy?JxYy+sy{2FY&Ae()Tm}Cc4D+({0#ZZD)~j>0Z}QJ6gAS z;7cAMsoYBFosT^EH%GElYs^lzl+k@0!t> zPS*5J^tVoQ(Nw7NT0}~~Awg+a2&zGB{1b1&2SPYq$D{6mY!CeKDyrCRR`@Nw!Xhve z)z$XO5XgkZz}Ez1Rql`11DCW~5LuDF-P>dL{^#}9BiaVlI;{)bMhIYbX$3m27Qoad z4Tl!B)?I8rlI6Z4tQ5P3CH7;Q6d|U zo{)Qr)1UFwA^mCxUyFeLJqy7`6QzbgBclzF*4IlJowL9RzE>>3dpyPv^uwH7+;~0G145}#O_DFivJco*7Y6syj*0;=dEQh4xZx9 z;8T4hDW#a0geLO@UpK}xl^^*M%__g(3>o^w|8CFv0Q?we?4GZwvEwh0*$_&}$=KnM z(+%B-jwQH*Vo?%HuV*TohjpBp!gU+TglhrmC1eJ-l3jlljQFsa(NOvFvxr|lx{pni zWX=hy?dV2N=wc^okQDI=JR#C2;;_B#sN$JY<~WLHYDQ3{h=iDF9=e9lw$9h}fvAa6 z(#Gj~5_eFSX>ZVPnbfT#f7G@1cZjKjxn=X^z{xlphb!wRBFXbp$1#)Cj$osSBe#Js zPgIM7d6qWt?gJ>5Wa}Kwc4QLhXPOn}K9(~qf!nC4f26is%Sg@s#ML?;{tJvCho*m^ zh5{CCC)w3Wvw@3f1tkR{G&Ff!WN4u7BC4Z(_P`1DAu@Flo5f#}Nxn>?ivxb4jmXw^ zm6=ArLJTicV(zxddsnF$%i`gh0?FN5uXBK$KR)j}&ru)dd;$}=c!(8fku({*=BHK%SK*?7e*;H})OKpSf`>tZ`9l;< z+EHD=(e~h1qK$@-_wNWXWOfD~6ELEiHi9OFeuAaKlTwwao?bdsshhSY?hHuKNya28 z!EK4Mm!Xwd0Y!?hXlIFbu;)hbr2hUD2i#_z(zVj9bd>NKU22S(PP}C@uIGiaHz}>T z;yKL3BUv9%MT1C1t#rwxMD?lMr?{78o=0ZLAUBCO!dz{6++xlh

0ZNt| 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 a`aevWsFTgO0Ez19IA2h{fBrvP;C}-S-Vsj# diff --git a/src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/pkgIndex.tcl b/src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/pkgIndex.tcl new file mode 100644 index 00000000..9ac666db --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/pkgIndex.tcl @@ -0,0 +1,16 @@ +if {[package vsatisfies [package present Tcl] 8.5]} { + package ifneeded tls 1.7.22 [list apply {{dir} { + if {{shared} eq "static"} { + load {} Tls + } else { + load [file join $dir tcltls.dll] Tls + } + + set tlsTclInitScript [file join $dir tls.tcl] + if {[file exists $tlsTclInitScript]} { + source $tlsTclInitScript + } + }} $dir] +} elseif {[package vsatisfies [package present Tcl] 8.4]} { + package ifneeded tls 1.7.22 [list load [file join $dir tcltls.dll] Tls] +} diff --git a/src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/tcltls.dll b/src/vfs/punk8win.vfs/lib_tcl8/tcltls1.7.22/tcltls.dll new file mode 100644 index 0000000000000000000000000000000000000000..eedad634f5ce9a4d8cec9e27c4dcc902fecbebcd GIT binary patch literal 1584128 zcmdSCd3+RQ@;}}=2Eq|~5F$}f2Mih&G4WUv1)Y%$c4VSah@yZ-0mTc0&LHjzB+d-j z+YZJ9MMcG3by-hX6+^rx1PH8GJQu_xc=kA`VO2~(&G%jP^wV=BAp80K_2Z@Ke(I^J zr=EK1si&T+r{bb|B}-A1Z2aHVr6>(}(myx<-S?kbWDhv3ae(rCpErj$Wcc13K55pK zH@Ir9zwV0bFTK_^{nFX9uk*Vuo9VhfFxz$IY**Pi6J6I{H)H1L+}yqe7VAo{qRd#- zFQuS%W%P)CnVE+uqn(OUn6D^jDaxurKtWPbYOZ2rde0PcQ`$iZ`qv74RL1xzpX;S4 zE(Qx)dSxc|!-lx?l?qB!<|`jlS@NIzK&9+^nZUC?UwMCQQabI`RhzFArokxbtjbrG zk&^fFZ?u2rJU{ZPM?pRkk?Nj?U#-+skDhU<|59YvK8rtY;H<*)cp^yrb1T)+(bo&a zEoBZxX-EEeJU{P-=N?@n@VJhstI`fWg?J|Yb1Oye(enhJyAc^hLlos@JoE8P`sY^M z?$MXsa07vAHT-E$&6lXPB;QQIk97{+bMw8QiqGXyZf^tKn}mVpep!nhGra+4Q|C< zuM#so??z;5#<45C`AWR^S`(bRKG5`zc1>M)Up$(p7`=iWnQG`05Uo87 zf6P}QG7AJL3a4Cr$u!SY&$Q;??hG~jIPzy|M)t@eyr96lKwsRY88ta5=`<>7uzHwV z(MmU}p&W?OL$O~F{z+F^1!t(C(}8F73P;t@&tOW@0N5A6aF-f-m&g8=FoG|6yd0*Z}6Y#GmiZg?8}V` z#mpkO-l7G6$k9rFyK#_a^rkX@=xVhtOlf}RCmh1w07Drop(NkNT z)xGT3s7YekU0q%3VP)LFi?d<{NTP|6h9SftLQ5|~(~R6VkT8EiA+hg%2Tncm|H&H2 zy_0f2K~76A(m`ws=Zkjhbp!}r2B6-y78g|g+28V%@VlD+w*H=epbv&K$#6{hWQc-# zonU6dCd+lR82O!MxSA9CN0gYAl1{2&Abb~_DH}+$*#Ol= z#&x0f=DNU9FgXECf_t(+ANY6tQQMyF{KJ{4|4AG0-%C^o27LdN;GT@Y8gsReWol&2 zK&+OGxAKc-F40Vr?r8_mfs-yT_^T(Olut#s37M$Ov1o}3RY_YrX z$HT9UCnMRJSJN0v`ywS$eR_#x>0cEvwjGb-9Oqsr6_Z(F&( zSuLQV;JMs#_jc2ytlAuoP3 zL@_E%ZTe=dX?vEI@o!Ckr>iv%DINW7+y;Sb^oWaoV`~=r08m<+9Ms)Zt^p?j!0f&x3 zSt#2Dm7)V|0;KCbP`xUtB%7xtU=^_e#&QX$ZRaMU7CC?$M3ezWAMTz1#gba`uaJOR z!M|b?v)Px72TKykqmKkg9tCP7zCo>#RcF!f}Ltf8NB9(}}yluJ@Y;Lg;OV|^UWt`kGyq|xSq`5QzM6(GAh#p zZsYVOq@%V&>+>D7sC%ipOtck5>;y&f2~Iw5c$Z_Kal%}{cg?1d6t%062Fe1_=W z!TAk}zZVK`i1&v7KKwnW?NmI{<@F@jk&>_Mp+K^%91Yv2d#=?(VKmO*CMVl(t6u3u z$y;Qn-NK#r&E=N1YOPKS&fICY$W9B3I~s*NY9rPl`i43aBD8pyqXQ|i2k-}&f3`8* zSRzQl)F_@a9FuK~$uy`$s-8mN)J6(`ljJyUqTzii99`f8@-a5Dn@1oj_$-$%;$05b zAcA}&ftGEf?M*(aJW6QCvnTJ<%#H%`)~0zM>Uz##+Yd=vblhVjTMh}bvr}~3+&xD& z75Xg#rw8sKjgvz<9&zHarz3ExTTdQHx}>@tm89XwQw6o8P`zR$9McL^@lvKN()1TL zK@*6gg7a4>{xPV}3U!SViu#Lz^ecNWszy<7*8WPXL05Rzf*mFbty(1SiB{_5@v$Iy zT@ozg#xNX>5>RWkA2?XXCS4q21>FmgyvTxRdS$hE7r|S25y@>h=Gcs^PyyJ&4{#3z zVZ<9P*WV%M63H71k*J5>bs(OG<3yXdGPxd+s;Gzk9*rgr)kd-!Jt1m+tPk6%BYvN> zaLX|gjvYd-0+-DaL-yJR2}p5__q?5OjNrkJaaEFIyhTz;HghCEI>u@VkdDz-h;Y7% z(4r^)BP6xtKTrZ>?64vwcIb)6r^}==tf3xA2}cs`o@+UhRnlv$Vy|(pjc9oduwk#U zD#>dc3f0cG5xX@IEM}FaBf5e&l)*qlA{aQ)rnI^v!Z6Xp4z_cwzK8xHa+K~lNwphs z7uB8njtB-;pGv_%-FoV7-1&Iovc5KH;^L#;XE?sOlSp&qtO#aL^9+OjhZ%^Wp5BXS z?h>vQ=70UYQrCqX4^^~M56r~;RRrUC6W%0)LJ**#h%y_yR(egY6&w~q1Z_IlBOZdT zgvFUB4x(-XZ-y>Yg7?@>@CnR0b{+dk$A8&f07*s!U~wM22kDBZTaH#6!?Fg#Kz)QZ z|J6NLlHtl!zDJRWe&*&vUhGnohYv4*sv{O4kK+! z%N*LInEmV=%N#bzoSjQ$1K2sTdL&W63yI#eanjXp+G;nG)c(pqR=Y`RZ^C=rAzBmj z|9UD-PKc{YX+pfC7E$Izt!~H4RF95vsHznc<2PVmQAn|vY4bJx!F(VVZ$l7h!iW?+ z+v(Zj{gNKR`EO(_=!L&o3$X0s7--90nL`e@3alzxeUV({b*$0pt-Ns5)hfcok42dH z$=y;X^&&`*YQc3mP$+eYg>MJ;6og;by~;%ND|6U&Wd1uoB86he^QWx{OzeFf)R zz!@@SvP|=B{!X?+$P`@X1b+lp=GpowV~Pv+eNt9!wXX*n!Lk-CMz80OveuH_0UdjH|(D`C$L`uwi*2j zT+py4_98UIBJnEo6$|u%1f5|vTA+0jbR2{V@4c8MNyVIxVzB6frKmtDsLqj8%Sbpg zI@+P2`0^tiD4ymV+=*DGuJ?qp^SD-GJ?foS7L%5?+J!ZDv7bU^1-}}>uk(nW_<=oJ z@sk+T1@E=OZ!}6KwFKrNpCbo$8y$xCOL(lB7H=E16CyD1VJCFg^DIpcq{&g#&{Ep*yVZBdZgU08yPtg>yfugUYb9H7-mH*P)iB>(tx-L~F7%Wfkgt{|yLj zHBk=qkHUxBWz7;H5tKz83XYcY^E|haaO^^~6Angj*?J-em*jk=SdQpcLQRC0@Re0j zIHH%~h$s~L-5?flMAJGFUZ}dZ*J`F ze!SEcSpSeT%QlGxz6uGboy-PPwMzJ-zHE17a?}qH^HI@gK#n}yU}L-{Ok%~p6_%D| zbE@f2Wx+D7p{@-De|5X`dx|3^x7qxt41m&R{}iCTD$%$D0r0d0$dFBPWwA6kN*76n zT7R~IYYi{-E#6pE(XGxB@=donwTXo@$qDTpp|uikyNGPbOlORTH}rlSod2O`+io`Qv1|!!CrZL)!kQgt zNPsoh{#Z1hP&Bo?wDSvfs?;>hpvcJM#cbqc*wV;NQgFh^b715OjQm#^Ih5l#i0Nsx zoeV^>^Dt4}TN@ADi60H>S%O)Y@ zMSB4(57G)v3@PLq8@sL8EFN7Rqlkb<1-dVB3QY3tqIK$NrvLx)Gtyi(5*jrnV z6%t+{Bs@7u!soI32{q2K$@fdb@Cs7npK)|4(bfl8Le6%O8UwS_j8U~q=t+gkUt<-% zg$Go44<1s5gCHPmT3d#Adjm>Ei5NCAT4}>b57(0%|LvPB$6dq&%KAEcuN@%MHx zDP)--Wa*nE%i_VTtTLN?|CB{8C03ClWXG#QNRK6C4>tGd;(ZBuH5i~{tRg;)-uDJh{_Nch_# zS;761FdQW{_DT`530$eIcW3r!a}U@h^dw-R5O6;6h~$k(67b;z_GNR+NR996 zlH+=oLuIo}xBq?0vixOM)*d{_{_&8?8U$I;oULK0ESjwDW%!_(YZ;A=G;>u$O^B8n zc+7fhV6wCiQ7O>c`V7&u)QQrhz0@wQC(V8T5=(o&kk&*>N;{Jaa*VLa|3F$TA?<;- zw5}9sJHei$J#g>RJ{OY8=}B6*kajikiR7J3V#$gxNleBj>vTpkcX>UVH>b8%Nc4JUhcK^B!#b&X$c_tx+Yy?+QX>%7H zlUZp+XF0LnO zzd#?tIP_=Y6UqAoDJktmT##dkP5uYcmL#M-*p@agMcQGOwEgxj?aErIoSvjTNl5!X z9+399Bx#@GfXi{EP5uYcVk=AP_Bw?5Lbq3>L~V1a6zF!^uY1+)?uGl3cJUh4?Pwuw z9a2)azjNf}c*!RJ18Lm}Y1d;w5Ynzq3Ex`5o}_(tkB~OleNgb(#r34ytuL^&w-TR7 z-g`(%X)A@axi)$J-iNThSYRmR<-6+G!9YvImnT^Q4zL8Q?JnSx3-+bX5+UGsctF78 zlLUO0V>8D!Hu)b2Sil0-#BavHe6mfHtdtT6N2T0Hh2cnI5#D`Vfk`-lXe@1|Y`|TZ z#a=4pnkM9;hg94U$W5C^-J5QFTqUk9Vr!Edo~W2AZ#8mw;qv=!p!4 zqf3F7Fck4m3UmxZF+`<6-3)c5K(QiLt@v?h*IO-CZs;IMHBd0so?jz!Y7#j$R?aMu zGb@oZ%gUK2a^@v+=82qFR?yRb5R8c4M1DUkXi$p&VQ#;Yo{U0o2^5A4lvOqgy(Lf> zE>Ku!%!S?(C=4eQ!!h7y%Q)SIYe{mlq3{;Y z6)dOHW#>)OE8#jtUL@}?tI6Oht5LIhuv7fyDEh9y$bK=Ix>TfUm}W#MyQ|gk4nq?a zNCw7I0)qjJ)GE$hXgGuGGO(E%d}W!<(%lnF%I<+Bfx!}kr({`_U>PNrYKT^fAZhH+ zQc_xUML3IN6-x<4I523emJ)JFhFX?Y2!(k^NXOR3Yk!UhZC-m>p+R{!**IdYHF*`M z*}VEVl!fKiYk_%=#FWder%8ad+?q^E3xO$%E!VD+E3WhKhW4c+jJD{19WF&5DMbJL zCaDIo0Gq_yaBI-Qt@x_tjvF; zXRd%(lbL_DGdtM4!c1uh^4eQR(PhqjdBc68*Ou zr2;HrxQ^-QXGrw7Z1j9megY3e$dN#T;C&?S-}ZrpLjYhj?s-{jMA7lF4pfNB}CQ@&wo1}i|x_OS9$X%FM5tqg}EORc5a z_>8cnD^VzI@mh^>l9cnUxl&ygh1D3{C_109YPLL`!fNeq6t7B(tI{c~Ch11;ltm#O z8v10Dx#Kyqw1hq?-L^v9ZqmqksCaK#gxL3-H<0&HGx|CNsaHwfkGKrQq&tgghe%a+ zP@f3CvI#As_-m5OGL{k$=as~AcH_xNk%U*>L5IKta2gaA(2=Tn0jX6PzE~^Zk-XfL zI)eI(VY1`}RI=^OQO%{KA=<~sgG;Jkmx5tbO@e*?=>weX2BRB{ULXaTjniF!cA`Q%7pf>wRH09GmsYN-~E=#mx<$Ij-PIjZ6FSx zu`(0q&v-Dg_uCSCOTWz~GTs;x+e^0%;q4{bQWD!s!5y=b_LerKZJ@rw^t6GxrFfgY zReFUjOxjlI-~QQsvt8BR5+{NI>l>4z1-@sXS6gRZ80^ZAI|cYHzY^CY2eE zkFJ-Ru{KIE_=qz>dr4mh!+#v~&8>Nzt#Y)~IS+i0O z+f8`mp4fmYD{(AaIL6v++Tb{`hXa`^qPXnVkrC)E&_pba{qA$GrHV}3A{y$8JTz2r z{Vcf&xQvFVq)kA*D!WYgVqC@%9V1NJNE#mo4Nn%YCZN^gwap06!W%YVjW8Wl;?q*$ zc_I@6g=@v@CcNG%UZZ%e7q1Pd#+qwLG}>a!0T5e^hBwDLD^`TSyad(7Ze2b-f;eCv zK;LqlF-NwGN-}uD}J?kseG+AFPE=X<}ubQ z*D)Ki_U3wvDO7eJZ$4+_q-8i|hBGZA2O0HHzl%JRdLI{Ff^Jb_-zmF-NB*iJAeAJ0 z+dFYDh+tyu@HEhrtM=1>^jreE4aZIT@KrISsWSYCPJaJe4wo^P`I23G;^C zcuR=kdW<6c9?A3nNBHeI4-%*fh9gkwC{0Jm)zDbsBh>3;6Si@PV5iMwki7P!uK4?{%XPw-T5H`e$tV)|3Ja$K7^L zH*1~4Rwt#Na1RDJ)Ln`F&QYt^Efq>YdqDDk-HXaD1eu#;`SNe<*N?S}NfjVCzfcK` zgM|HUgl=*SOp&8`sgz?b+N(O)rCa|^Vi z1-`*6Amq3p#Uge^r04X8z->cJ1RXPNgrtUO7Y%`NIPMUNAai3cM}?M&Wj>Lw!8!Kzw@;I;%Jho{u+R()3AK+P!y6 zU7ZE1hdH#N#!HfGT+zmN@knnN0x@^lA!NN(=wmMXKhj6KtW#xpEbI9gY{XHY zqU2ER;m6Vh`~5B{*lIMB`SxJGHUm9!9g}o7^IP()!>Frdat%6^fQpLX;2rg+b~Jql zk{v)ekopq6rsZ(EJ$<$8VM*y6#FX3rW1)TQ>BlY}o~POKI6x?vx6ofenWF##OR{DbmFdu8~$u zC}=fa4Z5<*_hm~w`erg4eH~9*%wv8KwMB0semD2b4>_0FIeV+ETVjaXu6A&3Px+l{ zOJPKcZLIryTGc*>2NjNHDSJm@%EN9U@dBqnfbCbxs&;b~_kbBGT+l4DIZY6L_8&H7 zYWFrAk+htpMMPma;a0_eB+9+g#t}9YW*iaPuo*SS9SE6O->A&uq(yGrq((V%UXR6vq3TU_O60ABZdKKcfrx3TT z0RL%BUL>v&dl)}z;LAv$t5xIC47@NuS~>1)&UbntP<a;<4n5%3~=?5Rp;QXgD0|$bkiQw2x zcASf77!}zppS>Yr$?qmRZPcTb~lgWQKQ&yj3 zV#kWZv|^!{t^hjTkO=Zb?@JPpa`~tEVRjJgbjeR7U;NP}g=8^L0#>bJ6HHWZG%6xS zdI&ji7N!>Rx-?C6lDQ^mc0WxuY%khsy!nqB#Q?nK;AUATW+Ey=1_>4cAY7`ZjK8i7s+dTYES5W}ijN`iYA} zJ1XI?O!zB&;KBPb{1utk(@d8mZw^`hi^^#wd$g|Q*F7kjfX?nZPDvL9K)M|%MY)Js z%8MpDdhxWEzT!X^P3X%rNwZ#z4Jj8*4rSVWEKkg%<%u`MS+9k*AFomI>cZ;_;M}pR?9yw-HU>DJs?~;Je}>z|BHz)n(+pkhR)`~WknM=VW>r0 zldJx!pgG;9vBo2!$xp0Wd8@(r;GU3NNl3FGy+d6xghF@25wDiA&r8Sw7kWUs9>aui zN4h8{Kot9HB(zTOgw4Q;+gj~)i6N-I9KrVVChh%7N{rQuk*UU}w5o$J^4Fw~nAo>L zy*2P;HSlD+?-!nI!N)w_(haR+Y$lc-C6i!NhWr`|UoS!|ER?E-`lEa?2F-+27Vp1X zyhkK>zkP|+%^&doDZzUc+FJ1Lli+=(#e3s6TkaeF54?-M$?O7K1z zIvHZ~PE%}(PUNy)PV851dFJ-G#cf!E+h%qOP?KC(>`}v#n=|%Ig4+~}+gl%Ty-Bve z{7>BSW7P?6s>Q7~!R^@piJL2irEb~Y&C7(|j!$s=Y?^HE{n1-tY(0$GvYvSsw=cJH zO)0>;>9t_RHyT13~YvndsA{-e1+* zX5gDp=GSXzzHP1as38?N2qjV$W-^ovx{T(#yf-i@=)>xKYa{JcQK%7q(IKZ*f1N6o z+D(p&Sq`Elnx>Z=R?m4G{nI?)Sz2vsGXDy10a+%)MOFiKGOxs7iAHI>#wslTURn#P za#Jovo<)H3PH-W;$m93P(i7ixKVwCSDDb}M{w&3ua}yUN<~2Mce0$1XMPGjp56PD3 zy=>73vupEe{2ex8Z4Av(jA?KXm@pyP|F_->eE_V!Dw%HF!3d16ZY02CeN-EJ3=UC+iSrI}r=y z{cEY{2bothU+4bP-E7hQ`zofxFtmT=ooUhCDd=4LOE=J>yGYPsh}ysMw%sl2H$u=A z?JwQq7Tvd3a{Ws7mu`+l_b)+L2|8}4TMo`q%xRCwMK?FWt<|yKfgI{B<0a}ZbIG7Z zuXwTn6uAF=;a*MgViYKQGZrxwbIX%t$W3NtgbkW{&MdI&q33*h7Zp^cQ7%zj{7a%w zT+h8q$bNZ}KnFu0{CR~r})$xgfn5(?Rd76kS9{_HXO&x;&H8>mY!%)r5^B$m=;T4{28+ydj{}l z%#yyehBCGgJ>%ub(3?LlmV+H*{+!7N7s$GaScN9p=K+^bN&vORjWCGBV6sXg!}%U7 z0U1xahg*;kMNBI4Xh+&cL}lR*uwu!CEpB%bM=cpqBgDfO+K5^bB7F|cPlf{l@p%ot zbmukY4S6$hLq5x{5ga#)v@~Xw&7RWYLSHGcSr&mMYhJQU`XJM)^QDq3mS~=Yg4hdF zgKR#R&}4&5*)1A`W+V+x?0T4`129E7YJJkQqybcSF%?W;>8(d$iwyU+yis_oml2n& zeGL$+>pcH1i1FD2JLbIV4$<8Q{Q~xE@q!LA5&;)9Nn++DX0f%L*&|GGZYvfOICp@_}Mq%KPPMz7F2yu-*= zbb}Vh6G+m(7RO5a?HTJ0*j?wz7H^>Wb0l{O@?hme68jo|3`YnM@XkRv7<(TOD$l0k z&rL>hCo@%xV-lXIfMWim8GCkO@xfcbjO2dv6oSw@h-)Nw!P9u3iFdON$E2{)@9zV>Qn~Si%!&(8{Nx|Ala}tx zQsRa9*B0BV>?0_PPD)}*n|D$LX+kz;%g@}Ha7p6(r1ZT}tkvjMUxH6xMuqRUK6!Z+ z*g*YixJi`r1Uo8l!x>ZZ^|TV}@DdW%Yw_?j5Y%4P`|(Y3SdIeZv?znfsxe<9>_}cs zgj`o8A~%OaKwhIF_ympP$3=|8x7qc|eBf*skYmXi@!?d*+X8ZVN%>QuliE2q_M8O#hp*BAQ1At zn2@Z|z)xx2qnbxuYqbf*JkRL1R(bd;N+@X%$x8!|V?w30*;)Ph=;a$2OIp!(?ey&! zYM`TR429K+y*?U-bZTfve>*cDH72MDfwwebm5Qsm1Jw-9-+`T&;QTL@g|sHTl{-4> zRZCaj*U~JbtJV6%N_+^R1qLz?^daW4iFth473h!5uD~d$v|r!|Mqvd(n@fR$Zk0+` z4@@@`~wOL>-LR2*zDp~GkC5*z8zI^zRjmDpD#vfQGVjHLcOu5D zCTrKL-p5LBD^_|pp@?Rtu-l9sShqIY;}mUXc;ClI9MsTA*biP7j>Q(18mdLT@v?9b zIl<}tY4T?Qij^={A2o!{TOjmNZ-0?ra{Pf9oj?e7%<@mfpM^t$9sSi1t$_qPa@EkQ zM7#%o#@6DGKYwhP9s`3NSNe-$U*NsP+ls$NBk>M$CH9thr^#e&UDEp+yi>Id z_b_z8*gs^l3f~tT7F)(i$BjPOr_+rKiy-_3W;htfxi;1&a#pm5PyO1OP=I^fXGs>P zlO7{%baE8rAncfM=&ckCgroE9BVr^sap(&U_$)PjSw##C!_G`@*k^v|(P#|E&*KR# zVK63~A?ij|Hxcs}$#=QaVTS6m#Nq~edjk{8!RIVdM#6Z6VPgF#rHo`hJ&KBfhGG2V ziIWSr3mlYG;F&1}Zv9^RW_)Gans~vrJBFu}hw^JN63{^R9cqL1?_0Tkjz)Gqs3|Tf zRQ<#OzwVd`T%^Ed3f^cXh&6y-l-0uHT^TWp%F--CgYU(7kP*m36Y&RHHf8Wa zSvv-FI{#{YwdHpk$Z;47UB;}WeH<+cO}7u>xUOYXE8HYgpi+r7Ik0{dRO9=f?A%bB z+9Dnw=_4n3?|bslC1i<5q?FjOLmG%x&FZwl`(@d z=o~PC;ex>)f!zm%4FzV&qC@+a1y4WuhH z00!s(q%1&O%-EI5D}Ie=TC{eP8OQg?fGIhW(E?h9ePw9Q@+ZV*;Z<-%z5NH^Z;l@W zdwZrIpH^$n3fR#ne(gb&(@1Lp#O2xfJRsQ^<3QjD(2?&jyA^5!F=EmudPbr&@}+U= z!*)%k;A|BgSir3@&gI~Qg9%o020CQuGq^0!K!H>%p|p#^s$Kx`0SsEM#VDbMn(>a8 zg}FpG1URvFhB*8fMS7{>&us>WAk&=oc{cQ5va>s#A6aZAn_SFh3wnvDSqPct`t1;` z$*lND9`@Nn3g7VrD_twUSt&*hm-1rk?ygJ7i>_vux0d`-`&aXpgxv1 zI;8XyjQdfP`#O^(MheKyJv1tM3Ca*3Z=!>#)~^^WLrY+|12ui~iOQ$C)?i^CLl)V? zO~>H-W;BEWBPtY4k)_2Fc|?QnhQ-dNHtq8%bVe@0a90`?96npY%)-A=Uud&3-TX}~ zd|&R*)ECGGdZ~+P*OS~z;=_BN)+T*;?;Rwp5ASgSl81wMG?@==ka7F5$2blk>P#G(e*)miiJF6YK`J&aQ4y2?h2rnJC0+wh00mIid!zKJ1x%f129=3 zE(yrk_^Fn965Qj37QO&cmx`0eNiCeNe?p}&g3i;DjllS3Ka5}_lkA@n4DBY3=mve* zD@vXIwt;AkLr~OmmF8w)IIU`*|{ma(MpE*QGK1@4bN`x zTnx1dteuN!^3Fw+!eEjE)Z*s?u7U{PuzP}7wbo1vG~$e7IJ%HsA&nhm8NXq~ptZ*n zLABbZa*^IJV9~kXNZt)l>ss;@v=YW$0#SAjf4kE0N+~zvChdfL{M^DhpNqYK$@1z{ zay!zG!;saYq{U2?XiMmqEY@+HlwcXg8Ay<1!mFumdG@{%AL@l*LRWcXYyl3&#{u8k z&UVD*i-;bzkeZ_iGWKg0@=L`sydzYLkJ%8}`BJk<3p!fX@ zDrCV%q5)I&)o7%$Rc~!-%gkuah_W-ldSo<1v8ZkfSl1*4h! zpv_95ZNu2#U%mYf$jwo}lEu{TsaC;g_CB~cQn*l%NEE&HLD4&f0`|ZZIs2f%cY;X+ zRxP=Uql~_QjyZ0@B^CotmM*NclKn)kCplTV5OXaqjga@IQ=&|ihEKy{6z+oqj8_Nn z6>|DDkt^_m!{TL;Tdu*fHe!AHVZ@~GVMg*+#AvBz@Ia&DOO3uvP=-GxhWBGSsR=X2 zqY&X39xm&SMB!f9T}3PgPm zLUw$Ey2>rsY$EEPuBC8z6S^zc29U=HjUn2yjb#pZcjj_QBOlDcie*DF~KQqtKdLQ0y+l2UwNk^+AG zb|0esDM_@mY|+G+FIVROg=S6F;Pf=nsBQNu+Iux5TGa|E&&2n8Vd%5Y(MQ;XU^LhV zqMJ{c{hY2S$9P}Xo4YZTL*gx9sDFf--PcJC`G(;jms!19hPO2y=AMP1dE-7R(~B!36x(=ZLgyFnV(Tz(g8&qpp6v^v4I)o!a!KTPHc32{e2hDcGipxY5GQcsFtuMhuA? z^hPntL{*%Z2he<%n0s4Q`@M!>!2eH!TT`5kFQ1tBhmfcGdi>(sC zEHwINdm$=AzNHBkl1~4GM3*AmCjluUjNXR`S4axU;(Q5^BDfoAxo2-894XNy{ecpY zBEoam?Oh9B9-pSj_awj)AwoTSpHe&`DWnwj5|ARriTjY^Vo4!cR7ij=1?+X7QVf+8 zl0{z$NRgsp_TF`|r8rF&EfQc$f&Q>hDV9nKDMd&EQlvO_A5xqrDI^Pz1lUr*q3u%& zRZ>V6zZY3GOOfK`Yxk~;Hzb8*@sb4CQo!NuOA1=r&f9Pd70d&;IQ}>3eU1TW=*Q&G z{<+tB`W57F=H#dloL<{aEo! z(8WK)z;7C^CO|N=@*Yaxrl-#^qyHk!zD& zb2UPNarxxUTE@BP5nq3KeR_GC{uUMlH2ppEjVIwp>7edTj~*qVr(7J%zak4ia~mRJ zg%)Ru8eMpUc=U@JirEAyXiiEnvHTB!wsOVJd_RtsdDCh_2JBRpWTaVyquV{4jm6ZCKx35wsUI4HRLTL0kU?{ow~>oxizYso*j zd!f4cADGYkB6+`F?NspdThp`u>Z9LQ;a}=CuFHvU_SAJyEMr{Pf59td`kDRXFWSFf z>zsj^{+rfEH2j%VsPwc__T7COr;hYL|?@!GCQuMuM^>o zv-GVDE&k4QqoNGg^xm3rKC(SU#(DkqO6*a{l#@@($kbq7Vi76K%Mn9X<`b0`EK%I* z(hadaw3DLmY=k;tS@;qc#k@u4htp}yoRH7EMpR@;8Gg^W$b4oN;QCH|gXu>g6nllK zx0+X!(UC%ZgBpI4cV;Fyy~Ufo#ZeLdSsISb0dakqK7N>}do55euQz57i_OKG)Q_+9 zEj2VAu2vvTVSeYAXyJGL-)W~#bo#&Hk#;L*o3jyxXvWzkd;^P2p%&VPDg5awCoClo zmC}MunmOcenM%yb<)Lws>iO20L?5JSC0CHEsu}bpx^X#}>=$aMRu-zEQpkfhmyj%# z#gxm12ubwe%>i_v2~IRAJ~D?UX7OS_LV$YKG*oEba2cs`Ys>fo6t0*jP6r!(tG*7p z-E=AKS>eFLv#3q5qqiFRE5Br_;R!f?(b$LT1s0WH5i6ww8J3Nh18$O*qi4h#sXSP_ zzX-@lmEMZLn#kv1Lxm!|pYha;1x1N=rbhDVn|-=!-gX+wBAagZm%z%nAuyY6GEbne zO24>7i%igjs_jOgtvEEo>RITI%U5J7E#vb6v07B;(k3_?VdP5eGf3|VcAVNk>t)7u zgUi&jqs5!irv`WB_1T0iX#LFm*!dul_Q6(3%QLY@L3(*T={a^EUM>G%8rRE`4ihEi zYo!yMaDIQ|IB+~_5cMCfMKnIVdK&~|=PjxLsrOs~slR?0!MNpx*wT=2qEIFFZz!)3 zE5Y0VZ&yPH#`qG$U7(LIh&@AjBK+NHK8L*6a#r5ML**T ziQj*>f3Lj=6F>T`b~xJqP$wPbztU%1TEzBLZj350N1#9|W0a=vreC6ecN_^BgGDu) zb1xx7%F$cY#c`57lIyw>zHOa0J^KzVV^dd~?Z0H*UW5Nq%nKQvwoPH>9m@0Ws-t=9$XrTE6|cO6vmfh|9F9f?6X?R9aeJoC*kavg>1e@`+3z7UkZ0rq-GRHzl zeghl$bn{pidK-K@*qxvm6N@lxBkZJT1iv{=S4%LAz3U%f9zIr4kQa`6@Cn~C8QV1j zlt@6oAPfwSAC=IrQ4Zdl{X;Y(qppNx2w)`;5r~l!t8^<$Qs~P;FvY*-$;a^w+dS+h zhhiRmvPc|2i8sfKgo6^U3X$lTheVc@6Q{&@EAc5Mj^YobG@0*E;u`U77;`-(p2dz6 zHQkGp_^?i`U=iUbbtpEd)(c$`to9^y}UH$S+Dbf!L}JcjPk3mH!srv5xuG|v2pe+wFA<$j_!Iizb)ot7GoXg&r`tlBo)vhS`v)PS zA7EfAG=Cb4W3zZck4mKv?m&o>_tj=$1| zGvI&NkMces8t_Tbn{D@GDu(0!_vnV~ydynpm)lsF)KfEu39FH@{J>(qp0!KT(Jm&m zyMX3dd%K-SK_OV2L3uYai(*mGv7WW}+gKEx0Baawoy%DLWI@7T*;jf*1Yf)vmG8V1 z*v+}Pfa&-I*Nx)OB+dU&U#)Q!xQe;l&ksRtG)8_)?MeV?2*ExJ0#GL^qcvY%-_!h! zFcEnzMlrPRUSgiSmQGFJ4O1<9{N++GE{K9|69s7)QbXHOjAr!0L?BGV%FeOoR2IWu1ky~Bj#F&Wli8Sr@Kv5Prd4fWxy3lOoK zfyn@)xCrGyv#&&XIf3E=itD8!)T8)2Uu5Jf)l`!+UB>uA{Y;SeKa@xh*Yy5}Rp{F> zB^l;vp!kYrEMMX&#*{frX$9DKDE3x2`;WqB$PUX9*g2jCYB$3E*2W$O?BH)%H)5;A zcPLFY{HpozMwmHSc-di2e;~LaLkl+N2Y+*@;dvNDS{B<)qT}OUc0fmbGf@{5=4qhA z(TUl$`Sgp~O8h~>cpD`PsT0{~w0AFoZ7c?s6(4BEIr&=T5_~%1St?D7oOCpLmRa=~ z^os5ZdCY?VK-k`Hwl095FeY?h05!9zhn9X4z{)`b)-#vfL+M`IRj&3~*GSxfaG++$cZ zTmnr%Q1AyAQ#*H>FXF@|evTbSH^_NlF(I)&!bTan82UJN_!ua{_h~{AP5@XDE%T)! zzBTteMT*!%ir6Xn#l}CNDx!ai@f_|;|4=gq!b#HTj02QsInhyHI~64~5(5MGj%u9J zBT_hGwT~4KSTl+D{0ES8y@H)W9QLcCIN^VJy+<<+BF=Ew<6x;#L>3qwZh4a2w)i~&wnm8P1|#y)^xBik!fjM&bvrQ1!A|ci*Q1)Th|0s-iPP-pOV_#K$w#T7r>Nj>I_S!MJ1{X^KhYc4 z`^V!mGkI&LaQ6vEF$rj(ceSk5G!CHa<`=uEU%f`}*mTf8GT-N0{)=d(24!eu^;m}J zG=CjP=(6ltngMr>W}kx9E{r;zxbJp%fk{Lt8v^O~tRR7U4S)K~r9k5Rjb z4ZEEfDtBIlM)}i`Se*cVtew`QP;4Hyh~kgJ@~rs^Mc{9l3m-?T=q>RR686PQq2?Mg zW!U^fC!sYU9=tmO`A&1eN)BBP4mM{{T)z-#rCS!hE<7JkS)is1iHN=8XFyNNOPa43 z_X*7y{DC9*2K=}f$jqmi;yH#U%{mhDS^mJd5UNWe{^~>!*K{MM^%f5>Yl)%guSOk} zT-R_b{^Iwi6u-;Lrg~<;eCvc=xNR~cqp2eY>sxd&022YAVg1srfy|i=uEo*js zT(i0+Dl+!T&*Z9;`a4#~XK7Yu6q$Cs;HF5XgoGj|8Ogm1ndaG#as!!bu)|30F}i}< z11-8i4NU@n2&0B3;01PV4R&q#*%+(X=FpwIe4QgLxMnMVbUxZ0M{c~E{_+bq` zA3!J{5>a@U|59>SC1&qN6ie`hf|+^G32Xc5fKS#SQx>yJ(5)e|gLZp3;i zIH$E-IR1a<0xLJ;s?+j4li^Y`EbAji4TPH@S zfCj|i%LTP~WEn3Pkmx=GE7_p}a!Wp=NDXa;HhucfKE1_Efs3nZVKRXeELMFq0(7F!>feFAsbb znUK+l5mmt_<;+%?#oti8 zEj9{UNMnP}d;%f{VPnfFg|w{SBWUDrmzi1izW=CWQ0eV~+J}V~!D@&Kolv z`iBf-+-m4hdYsezxDe@rrEbBRgp6Z$dHT~zfQ|gDxQrA=|IN%)*wtaTgk3rKj=AgM|APl^R%0pR!p}cbaVGPZ@J;)*aOIr@wefny`;f#STm@X(haWRP4LAt{Ev7HFC9*x zyjhe-vl7mmZ{u@M9oTKYabA)hIA;k7DVQ@NAXhy7E9Y+&x1$mZm(ki?a^N2 zw5^MYWg7(0jN7=K%=32hcDi?J!Oc4UkN-`??_&M<5EM-d#@*JBM>*i5uxup9Kil8C zG~=3r8jbD`lM%d)H4UG>vAl(nMjW9+R)EK)G94voM`ypr}%Zv?u1@g%tN_Kf%ZvXK{`LMc91s&Oc zKAuiIN8^d^8o*vB?c99)4T|TT8$DAlXO<+duQJx$Tqq$KE51dR&-st1bBQ-t`C`3L z_OkcP<@iwA$aXBqS$%|D&%#539Yy}hu!6ve=Fmy3Zz&6w6S?#Fj~GkcDKN;qYDzkS zWurV_F(pM(8BI25;fcT3mAoZ`)%1{ZiVeba|7 z#poZJcf5e5w{R4baWym=bj9C!i@U8_1Ea$8 z@&iX|)8B-v$2W={zKrdfo}smPU;r?W7EeAsTRi086;H4wqYTTNFND8aaCtcd=S<)9 z4dqR3j^J+@=R|zjZT?frBlxMn;BT4bXmF>0M5F?7e;YB^GTy-!lv~r?CAfa#;-%cu zO@a7?NLe<7_vDvby6nL0}Z^Gsfm!~$imC8bG1|-wo7^t_z z!5_!epb^T$BI#fO;s?U8j0VZ>RG#c;{g_m`T7rbEjIH$-2Z{cV&=h`^<5E%2~rqb z3kH$%PonlAIPP1S7p=D3#yT}L4K<|Tw&s`r(2skz#IMRpkF?1AE;HjMtpA~)xuegS7ez*|#JXSA}E30bg17v6h^?9su4UQjc zkAIpm1>S9&e<%)&qU?6f7(}aKkJW=#WCgyWm9-E!QpPosX|o!d!%VF(-^ekm?;s)> z_TzZ(0JDNX`qpy&P^^EtN5EdMj$r*0InA8Y-qZZj>g1wBo8QtWEYrI>`GWtUt3M7N zF(<;|`U)I~#;IxjBOm-MZrSJFcm{&%$I%g?!rrI&FbL1`pIsi@&>Nv>n3iBjpNkf# z(Cc=rINyx7SQ#QCvM~ht)_QPay%TZz7K{Vt3HM=QzbkeSP{A!HzDl$=%17Z4fy<{A z%A-*>7ppf@QA1Gxrn}*>C^~MC>iYmfrTv7`;`L|*i1G97SbwGC%YI2S zzm=gR?LVk5ZjLPjNRDUL{_tdoc@M-aGcGJBGtM1sc!n8Qj`Hkk&P1GErf)RPb(I;O z{J=oq`GJQV#HH=j;E}Nl2@u?!<3CZPw!SaHk@^fYzx_hkH$LsVeUZw?->F8-Jq zSddJ=i{j^SmH=iG+EH9T|HKXYa$!<@)uk-vrWK~CZzJ($SvW^y;xb3EnK1keo_(x-D8 z;uj$u8zL%tA%NB_km7%!UBM7)=}>P%{2qU3|JN~x=yzjwMfC9{fZPN~Yy{_nP@&ZO zcKsa7kfxCTz}lMzWdWO1gZ8@iG(1%AI)uf^`*{(^EDJvy+k-jCEyG-LwA4RhX|T$r z{a=USIZ@BV7Gm;#q2Y0+?C;0o|4!-8#<{~pH&a88pflX)Voff>>)bL}aRW^|@rzlz z-3?FTEikgVZa0~Z|AM)v;0?yo5XOM#H<5syG_+7+8@4Q^e8Jrr4Tn)L-iRh^7=^dM zcH#kM4LhMs|1(J7*)STEfsM@#3-L^v&+y@u4ouCn2ipnF7PNOI16Ufb=GleE>i04v<6?-SpYTEgPA|AhJutK=Q00LXllp+2sSRkEUGTqingumbQ6eTF8|XgpAvm2 zv7(_1lUq`52uVU*&D>68{--hjHildVG1Vp4u>3n2Qi*r<_TLz?oj5{j2znMgNL^<; z0Y(52co0>12U%E8Viu<`Qw4cy=vao(nhq4}CP*9=P;aMBRo9sToCb}V{tRzY*O?8S zG*l2EOfduOxvq0Cvo0e>x6fwQ0|99`g2`S5nYv^Tb31^*M>6;y%xw?>s+b!sgu*1; zDAdVydml&ih3J4cq}_n-T|DK$$&Zn;Pby=dM+`o>s$q~)zBA^LA`w}GrR+kh7u-v zll<(G$Dzc!&J_eMW$+q+>pCAHz$whjLA=nDV4YL|W_sXR&&0W0yGsd7=i1cJO-x)v zfB{Ur2>`1<)^!dirbjViCDZTW!bcGJXa>`jq`J-`0vyBiw2s3lqXa*l5$l=0 zg3E6ru$#d*F?}lmJWRg{0K5JxNUj4Jv6AU&Hy*ks@DK)Z=0ZH;- z4TqpGi=?ht0wj&){r$#;0K>ue?Pp$w)ATI+6Xd^Ar}islzl z0dW#!Bt!bA(+?zim^|p)v1E_(?;r?W7F5HZ3dp$x!7LMyHwcm#pXxdki?yyCR<`Dtl7 zX`Cz%g)qtKGt$yS)6y7SlJRN%mydbjgqLw^eFkQ*|HNrP-kPB!7A}1I%tK$NGS4u? zpj(nMpc|esDe)ZQyJOD4edyw7a5n-q_2DSKm=Gybr2W$P?OE%_Sl39B;sD=Abk;q9 z(Sw&^DG+cc#S?_jKD^B6TUOdf+}=s5FJ^HiWv8A|68HwI31e_{dyHBB1RZY_2Up{* zY4IsGVwRy?iaQ)wzsRBEY-#6BOE4uk2SQ;gG>fJx`oQwKXa^BR&ds4?M}^-ajt2wm z^x}L|9UVAYAb!RJVMHz=gEV5?9w`dUi!KZ}k-UKlAF*Gt?K$GJn5(oL-jkd;Bdc-tsgIM4P7yD<-YE z9un4<5SBG*WP+3KDLTU_%lwsC{TdIZ>95eP4`$)zQ~y8Jr?43{$PSdJ)@SNI>a&*4 z?=(`SU|S2SQH6srx>&Y_`^-ZAsY&wRl`OwFZwb#U;d_Ivh@>+WaZ>Feii7v-VEn zO=%pT-6D#37BsR5w^`h+2#hM=FMdU{81~cQ!Un_K{#b*e<34olrXGi~BYqtCgGrTP zzjLw{IeQGAE;YCaWstJfa0A{*;ZA8lYk5pEBTg*bP=lv3dA`g1U)-6*-{VfpcgOBc%7;II|Jg7N*1>_O zSo5xHdK8r-PjWm44D;*DaJdAZPiF7R-CoWY18AS&p$KfjQMmn+GWFAZa{Vz^U$8q)`@7*T(TO95#(e!iaFj)!d z8}-ePKb07Nk#?rm&y$~$M*SKU*aH;fM1APcPnS6!TUz8%^L$ut8U>y5?Q@dm<0JXr z*C?x!O8nJs#V0f(0&m1iw|vfL7CD}=h7 zgvv+}%C>*|;}BP(4(8M&vXs&eHB19E6~T+$t>#i$MErr&_DHEuzO2vV!xQzn-z$`S z-eH!KyVs`$h5E=Q>E`ZMvrrZ$%@KYmwLbUIC4k~zl9imOPrlfrVUsd%8<|oc6oJM2 zR&xU)bXHkFG*e>mN5z}FYF>G%5(rus^oed_pTt!~{L)HBy#PWuRS zrWiZVn9uE#zBPrOrtfAbjozrc70r-X4`650@?DZYsiVOGK3C(&$?z1W#xvyK?oGz0 z`vLA*K&}NHc9o<09aIFJF=SRg9jxs22QKgAuhPrY>(_&Z;uk?=)LQA-F)ya9=lws@ z-UPnN;`;wjAYgFe#vS)qV+*cO`|Xz$vG+p28x0!QVzn(5i&$-odZV~hFWf|Ue7q31 zN^Nbi+Dcp3)@l-$CV)h%R@_Bf1-H&MYQZW3F8tn~GtYDHP1OE=|JR?F=03BXIdkUB znKNf*&ZzyP#nhZ9nUkfAhB@AM7y0j(c-6HfuSyKlX1E?z6W87|TxTwVN%zS?ngp`~6$ zU$h>S{-~OHx>g;VHPH%MUoo#^c2$wvXS~^jL3Scg#uoNtgQ_I2Qyq5!t-t<4ekOm5&%dqm?>-=ZjNiAw4-$1}E&;KoP<)qYT?nWcxt*H0NF@9& zzoH+0oq5qwl_Ixp=lHy9Hq^)t7)qNxj*20zRVHcccHmmiKjiae1pXUWJzH zPmI_61m33yGeSdPKZXA#A=0;f|76AJ zc7!#CKUXp2w}dU?{Hc7l0wrsF#bN9Kof-{OV;N)#yZK3E-u+d8dX`+`uYUB~nidr9 zBc+Uf<+NZjm&myVQbzM(Lonx7@HW~O=1|fyeEE{f=)7cZQ=(b}8GM^qZGwS^6U_$k> zvT#)}p=wzw-!6KL>1M|VlAQmtDxed02bv*mCBnHio|yU}CN-1Gd-?>vzPmj-4dR1i zW`=VWhV&Ngb7&R=;V4}p(k;@LDC?{UzG4I)coV9PRjN%tP`Jv%RTftLuY2fLzrw2r zg#R%h{Puvb8xVeGKv+Wv?2GVM4hTOyApF>X@bZ4)Xfaz@?Kp4;{TO`HJgsN67xwSp z-{KGa0xDB-s?StdL|4b-_y1}i3-M1rEIg)W|K^&{v7xnVuCD<;H*C8zgO*YMJK_c! z+X~L~C+XtPtbOC{COl>&Q$KT5_D-T??xI&q^MAkl^p7T5uTwdW>S2m%s5{8rwtc0k zLw<_K zPr7}^zO>>tkM$?UTh$W0~PI9Oe#w)^N>mgo^j!sgcFQvdlJi-O<)O zsOYJVPUzQPA^Q<4S?Rw`Oxho1qI-ho05bCDG=_UqEP(AQG1bPlr2KK|ru-c1Gmc-~ z^-Njq@56r7z9KW6@rTo5-DnlfQk|DlS(k8tNN7?QsnVSa+@grH^yB~HT|}=3WmQhB z^nBy=7ozj&A7O=EqQR3`aJwaQ1M#=|fG>Y!Xe3&%CA?w_8hPMXj^c*rdTuKAjnjPU zx%@=3nblyU)Lqvbay+Jf*?Gqj*QBei8{OSe)HiiyCs=O$Poes?mp;&?9YW>V@GaJs z^zq;o3UBZ&{@!<@yOw_ZPgqg)m5RQ{iu2tV&Xx_#S#DoPMLK{nkX04o9JSJ0im0Ya zk|G_>?I7|C$3t9}@lz-E>J*5I()dzbmmqMsx%w%rg8}}anJ5sG>q+Q>C2x;A2>+fRJqA* zI??V_J?BRziWU0u{dHMtP3Mo{X4=k)dwAUH3d3vuwseZgCv}S5o235756_>rHD{!6 z_PEYF9vRidL8_5jFF!Z(zkuNfFF$!~lxlVTBy`e zoHRoCS(YM-vudLlJfw_pU;VH6c{;UEvqovw&d%hsOyBO;ALPcE(QQV7G*iXHRl=1S zTT7UvVpbB93QFUp(6?VdeK59%&{?d}^NnWuI0AEF7fgX~oULgcqVl1`ky;h(e6zKr z^D(l0MEcB;&F=K45H;$QT*qQjB{ST6{CMSD3M+n2_gbPcQu^!Mw;U&%13e#E<$GU} zB6DPoJFJaaBRsO&waw6n<=8+;*HQ{=spQ%ix0a5-nfC;2zIGJcyFakMxXRz zoHTJJ58IOFR{q&cG4nrUfrM|>k?8;re5BrcY}JeaC5#v!96BRth6!j=cHm8RL&N`Up#048gEl7Pn}!oF+E;?+P8EpLW7#$)jO zf6{;Kl^xm=dM9dyz*2sMe)tzb2dU!s$g=Oz@prE+94gu?Js4ZJ#c5zPwd{LZ>3gDw z)tj8@w~Ay;i{xxdM|8<%_02zPRDS+Q;qd5e9j%=>(?iM6AFIfF21KS6d1Dlbuy6Np zVz`U?2Q@17PhfNDs;b!BisdoYp~JZSwYi7c#Di{M;&{G0@JxAzNKv?dobUVUg{yqw9ur!Xzwasq;iAzT2(gc`73}xwQ0GNyMd_oNsGg>9`s^`JO=){Qvkenk?!GMLiC@a{ zAa|efaAsTkHU>^6u3gdd<}g+PA7;|ICo0i<60N_aMDB^|6zW{=;mTnTrxKIa1|Q@e zuR_s1B$<688=P4aT(ouXM2%JOEW4YUCBpW-mja;g0YBer?@u$c?jck(R#)Wy-jFu6 zO}QKQX~27pF0f8|w|JlUf?7;$TT>)Vjij`9$ZC$dVNK}v`1FYqJk zTnL$%XBtQIIa2gaH*sbKY*T16lfvW{QBgGWJ|&d(r&-N)SVq`pW7#mTq(9Jy$_DKX;{n3ROeRJjPAn#;1^W;eUoAE z&`Yv9^xfMY*Cz$&>}FGfw6(I12B}W>5BBbmD;^t<2aDqMOOq}8u3uW28KZu_WVCKR zrt?+X@$^c%XZ7H|;YIq`anp3ZVJPGX=50Ti!}_juK`K1UzQ#oLG1Jd(57U z*7qg;3@0$79@D%8mHmk+!Sbfy@y!3E!*8=G@lxj7wx5f?uUWv7daxpMEZaMcK~-Hk zXaW2SWt_4k$&uyo`mE~Zw$bub=j0L|8(DHzg9p;?#uqU~ET;-KXkKc$q3`)rXOl9{ zG*T+#E8{KWY-Rj~=eHs~o7p|!PJt$qGW9a*M=lrKOv0SmqcxX4s1xuV6{wDhhPCcO zHcVm_=S?!X3%(jfOsvUl(`8Bt%lDi92QS2rVBzG5;-%yG{z4ICxVhS(VpC zdCU>H+o;`fP%WHHMe%w>?iu~6msL}E#8`7V^9x#r6{6dIQZvI2eFdNF{Xu4SlV4*@ zLgyzAQPc8+4}1NMl2yu{n@zKMiZhobl(KWVCdnnmucga4!lm)+c41FbGB(qqoQl+sdF|ABdv_N%I>WbREY6UmvGafy4fkCAKqX8y3u z#vjTva`ioP=FY$jyNSg)R)^ZndWHdfOzdVV8rKE^o z&aF1&`tGu@jW=$?LCX7iKdI zM{*rx2Vnc|n$h0lMj%Erd-ysRw}-X~h(L>?j0X-uJ;>6j(_-wvm&HC_`aKgGNM?h!{<4y2=*2qu}=}o&wK>0(b!7b zLn%z7qijYfyn)(cXL3#T(LKr5Ee4~M4ALv@gA;AzloKwmu+0{`>0mBQX0+S+hp6Xm zLlkbvOhTg3ui8Rq6Ut8xA7)u%R8$~FF=-F zJr?1A5C+QJ!<+kFE{>l_ziJ`1q@kudCQwL2^Kc-znbV4U#O>OjOm!Y5z{&&QZwTXtn!wj%mKMdmi+&LeVviNv+aD!IzQK_u5 z>Yoi&5XdUfk3WRQ{I$fo9~c^ONk!>j3@e3sGC;uN}NU@$BDfIJ`PtRR|@+9T=JMge`r<%xY`znE`*52F| zz5-U@F;U|rJ0D<`N24#=yT)M^vAx;(QAtLM4H_O!9USDA5L<*G;2J%}h*m+J75nGpk8v@krB;+FZ7+R|;dS1V(ePqe!+lw@xUU{fHN7^U609iv zi4IWtTL^UB&mYF-uT;3ctZ+N1u>Ijz4w_&RZ711u6KdK`*KeC>ZB#fq&y%xQO~7m$5VAexdIMy6x9T5UBIYiJhQ2U>Dm=`=;{S{9Yx@rq~E-ua`I zviO)1hWQ-_?T7Qv&>KB2D@zHz$>Zs7md0;>$7la(V6>=nCs}5C6GduE%`$Os8xOb( zb$UO>68rGwWq=}+_W1@Olz!pPRI-ix(I*$aOTZPFETKa!95bRY-MF&EACW!MIE0t7 zc%}jh*Ru+qhmPL08i?N7c$UDnb*E*BT%;t;em?1#ILXMCXvKpb^__^_amyJ&x;k<1 z5rg>MGCZOxNN*h;Q5~df!pmzY9?aJsd^)_`IO){Iv{N-q#8~wMW*fnX8XjJyoc<7B zSv2Dp#f#?vN;!#|M$}l8uplQ%6iz;XbLGLrsfl@OSxcaADSJAmXmI2yM2$@c$7Kvk zaO7!(lj-0{RFq?90*b6fJcyb_RMI_rUt~j@m`t=v=O9yaI)BOTV(+Pc6<<&&*ywE} zQDqFW8%8rXUNyoRa}C&`EY37FCp5cSkh_&gjn6&E;_mn#^_-D4(uMvk0RWs6fD^&C zt%s>Z$5}B!PAd!cW*dN29+B%Hgs9luo2~7igoGXWnHceUS#k1=?H|*dO%h=M!CU~` z2VWEBlOe&MyD6K_b<9_p%nunMS-yO8!uyP6YsQOZ=QR@_cS|_mLNaT9+oAy#whKm6 zXh)UYR)=almG#V3f}0N7({cV>$09zpxbqiQ@^kZ9F9ROnM# zyX7$Bz?8~vcg?tg)mnz2eX56~{?Q_b7SB@ciMAK{+nZa=U;UF~Oh#w9@XlP+o0C{p z_9$5|#^r}(A7w<;PpI`*gMx`8-C-NB>_oDF%@UP}1@WJe-2H5ott8r{a{;_!7fkT3 zvByCO`KfqrEBpuPsPzy6^97v!iRVrO9=x^jK=D?J!X*t9^T2y2pp)2|)y{ZE`cOE3 zHl@-LRTq3iR6QKSMMTmxo=+diTu-=2n6O=hX$Wl(&o!t-c(qTB3LQH$yh>1m=Gwwi zfQYedV1;iS512e)qV@SmU5W_ zYD_-tN1!RMn+M%?oyxuOfzS1kfji2$e!KLzqwxM`K2fCs*%n!U9Ha*a?fN?ayJPEu z=DPlIP!cX{9;uma;>iAKp$Wgh6!C#6?N*2W4ouMm6tw$ks6Srfgga4Yv4xDxSaMI1AIZ zo0PW0O7%Jpv(X(+h>0Dy{_X(uKa1pz3imz5WAFHzj=v0&W@y_+^WSHD#MV80#?UG; zM56T*7A>$17d#@ix&1%JHf^ty(6`j~EBx`OAuDa*wjGX??)&{`)_2}A)ted#_Eh?4 zZ`qp6Z|$>(rnG4yFeWWuz*s0ewpMK9{=}+BQ&CO}#CsuWN2fAKvLp{~9 z1E~}yYpsk~IM6ql_xoNc+Aqq>gC8jTSNxhk4?kmbw_Q1YZ3OU_|)#vP*TeAU{m=r8caZmQ>aQAIf^$#Bv?7rPO zZ!cR?g?Gb8G9Pj*Ct0_?XHdiZL7BHEaWrw*r!yLW@#!+FsAwNy!=H5{X#JhKWZX$CF13)3?nimvU**m~ zuP3UjMVa)@h7#N%c*mg=zuEQ<(q)7>q;AtH2ANNundmx)#riT-H8yE-}@by4b%pYzm#}!cqR*&1e06beVW!iyTYi`O^ z(bTA-!Xbm9jqGSMYhKAvUc|o?T%M_ zegABSzh?&RBOpnU-)Dw)s3H!Vk=suLcHY^-VOG;ZDeNbMQkb1FpB`2Mrwm^tjito& z@b>C-y+5^T+j|(x86-1Id->-zQYAQh5YD&R_B&S*=7Lq(-ZDzq>HDw~5ln8DUZ(rR z?q?&_$OWp&eVYxmbnq*itW4;#=TPd8boldba`jyq>wDZkfp z?Y@Bdq@ix4UtOPkAWda2L`6(q1up=Qdt0Jc;hzT$(y5{nzo3Mcr{_6e)Ia^d)Q3`2 z;rj3Z)UQ5!4`(e&g;`Vvz4Shgg+~M223xuPcfftxXn#cRY4|hVa^6>8mgjYe|^7LT?!u3e+^7JzxQ+fEL z0pS38m#25u_74~KA{_f@E6Nx5)oxx(PZ)>d)3L{$`r4r4>(s7=Kygu`^%iU+298dZ zl;r2Y^WBU`$5zg`?G+@H(NG3f>Q|TFwU(^{|HecM}U<2Xa$*;FR>v6;0ErAhr3~ zUeYO%yR@bv(R#N&qxMaIdQz~usq($ls29?eZ~5DpqIY9>bd5XtRq18H(!_$rL{%

o9VluD64KF7~bn=qTuz-ci(1-m$2oqGMS{L&vI)TRZwX z`a9ws2RaUS7`JENo_qVa+g-O$yuIM|=G)tDZ@>N4+xOnyfBS*kjZN8`ayPj)P25zl zY5Jz3P2NrAn-*=V*tBd@!=~mQI zcPzW3;f_^zG~dy7NBbQecO1CGxHI=o*PRpZEV#4iPNRVRKSJ9*6YnXwXZk%xkwdn@ zvUxZAuH3uE-Q~J#;#~!IRou1gu7328W+k9{Py&d;%xwrG)zI*%c zJ#eqFId`*bbHV1K&E=abHaBc;-rT;qWAm2HotyhM_isM18Pi$iGfwe9GWh@YZ%Q>6 zmQutl+$V$VC39d>FttC0QIh?a$%F8p9ZyDvdT|Q>3x-)CjI-2uq&yj7VID<1jd}G_ zxeIDCIV=gN6$e4&!c|zQF!GM~`x1c-Rvg2^{5e8R%t3!G*YIFkH`wVe<$d{Zi$cNs z9}+Us9)iJZG&t=c>j~*g3;7`-QutKa*%)b*r-f{jD5LFhdL>R;$X^NRPYZcNQk`l~ zl>rBe(n8K5q&Y3*LPF#mMv9)uXmdMRt+7p0sjmAw|#k3F)6;59yGQ&s!lD=8KY;C2tlZhWvIA;+lA3 z3MKk;#Hq02sPu0V(w-KQfe;s*Z{;qL&LE^bE#yK%To>5m%$8J>>>=_ogpRb3^@Q}N zh1^C+?qqu^`5IC~TF8@xv|nhC^P1rKq7`ECX9koeIj?1&yFI6KZ}{7TZG_329FYiAhL>Jx#6H350A(3;7x$ooOM92`LZQ@zfF0oEEZ%kj}J_7$FDJLjH@8f@(XS&4iSv zg*;8jmb8$ggcxZ3R7pnwR zEO{o%G4ecXL8{029;vp_A*3+hMM!_h9`Z0DayB9rr(Z&Dwuc-g{qbF&kZ)2aGnhdbr!`!~wfn#8lz?_XY7 zQ)frQyBXkV{_%760)3-+@B83E5;jBWBcV6p1`U=3X_%B#$+5Z##Sy~7N~9DCORb> zRvd&^<1sI6;Nw@nQH*4qBv-d2F*7J&lDu(s3Nx>a9_W$tNlG6c z2eU=t{dPP(o3Z6VMHzZCL~}h-=ck zv?^F%nN|^t7ppL%qwmDITXpe73eRy%F`3*8;wQ!e&B( zRCfNcd0y2Wf7*hS0R^wnEUjoUgSC;MB}*uTl|h`w5ThhEiIax$$7AR9Yv*nJw( zaoYW1S`gVJDzAcKtw{C8FIU>Y6$k~2ivOXDs6a-Vv`|*?HnFA~Kg_z8bSSXTx##{m z?++JVDXpLHbM86!o_p`P=RWt|b3-cOGYB&O-#Ib_Qv5VWCP7vVG7B>Ivz+EWkhP!Z z$O9ll>pAi;Nd1>N@(9R^LB0yI_*_o&ILPw<=Eylo^?X2rR-cY#XWX8ouVnS(kzzfh z+MuWWTjaxPRudnO6x}a)=^OC*N#qlQab-E8r%;N4-+|AwkYK^7#zN*p84r}7>@p-9pF>yS$L^uR~X#;Jb@ja!*qzfiMgRFvVjYwF{pN)v5dJ394%}!fX z*nQ9)X>^wWr^q8h4qY!e8xi62Kk!+M`UyJ|c14=7!hbFzy2}9R5h=^rN_jVR0YAFE z|1x}Uj&iDCz@{v!Y`HL+aRkZhd%~cH08SL zHMIHIS|t4qAV*DG{WFk7L-Q9POGf$$kS^0N?g3ednitLsAnQi@;~??{N=s47lx+WC zu|ld;?@vNAXHq=_vdfg~Igt6tM`T@2MXZ`UzY%2TrMxfS1hU2Cc_+w_DOWGZx~ahr zfyfVWzIMhyx=rnT1f*{A^9c~!ca}lU8d*<(*uL{45Ya}eljp}kB-Ru~ z^E!M&_?S{W1+p#X5W?bFkiGmr*|A@PY%-ePd=>54l;TYwYbMY8K^7woI~sdrQ{7%8 z>qF33Sv8RPSQllQVy@Wn&?BO zPOk$wAKN@4ia!TgFf@Z8@=MZ3LAp(Axf5i~=;uC=#m#wsJ_)jFWIY06^?VlOtfBcX zNMXwLGmx#OPOn7HtZ5rtLFAXblv_cTO)Vb*SulC7gRGif`B9K$?HB0GH0X@2@?5;^ajYPLGAA{ehy@-L0(0<=SYP(-x96|S^0FX!<#_5Onz<#4CaNYv4=C!37dmG3Z)8lRhS&luWlusglc0)q)ypQ*8 ze%^PWZ?N#|kj&RPj^_@~u&P$6l;OhKa#U_NWvO4l5$?3pt8qOnyAMk_$JfvsmNhuS z?io4JmmzXb>TY)@yNjD3sS?DcXf>BELHoIKlS6e5)pQ4{r?MwG-Q*rQfX9KJ@$l9^ z?%^ua{B_SMj;^J1hxscxzSc7;M<3(KN%#7o+`{`o&M)S~2TqQRg_Cts1}{zy`U$;W zbgQ8u-0R5FpT--kjO$UXL!A7n&VQPhARjw@Oa_FEuBa5>1>!pSU=9r_0U3=}XS0~v%=r3Q-Zb5fvYPD{BU3yZv z%Z_^rMHuy6RUbVvA>v@ojI;1sL|u)?bE~caAC0n4o{xi5E>jF<0{0Arf2eW z|LBn(718=8T!(yMislz?tX5q)d0u#UXL5RYGW28vAE{2GTLaOJI}n>UF0aiA+p|+N zO8}!jc3D*GeGPZbb5vh?($c4Adb~W4(sj^s&s0CRg6iM!~^{IF}shr|Nvma*s*<)-;6kn3CirX!sC&IBsYCN5c{n$YYn?sZ`*wgCh zFo!v{PlSWi!9JG*+%Z^jOhQZRoK30?HC8w_Ej7n|wymc@d2!psh`h`1n@niut13Os zA%6gNY9d{AEw?Q99y%mkTRO1Wby(z2w@Z6=_FT5zH5dd(rphtyiX9#=*^Dx6kxD>L zPr9WDXMjuoq^AjWQP!)q9XsDq`b!ZRV&%ZFdsCL&mRP!7IqV@TIf+`HsGfF0kZNCY z2`0m1{$6+U4DLH53$Ai)dy(&8yGcG~FkqA%^{$Obm;kwKaW{w0i&8Em9q0{F0PN%L z)9!LxHua_XI}B3{O~-b{91WwuI9Ef^iAG&a(bG* zR*pb+7CNC-F6u686Vi3daXph?%CXyq_$|G%Gs_vdNFP_^BjWcs@jQK3=~lYSJ==SD zoNRiQg;v>N#H%|`PN8H^H{Cd18=dhFGt*pTdAEPqowM65>HUidI*U}H3aV{Cmf`-- z`qZgggv>vV=R#Q7_V7q;bkzTOYq<}02 zKz7tVMF8j&Qz1L|Pf!80+enA6Wfbw}<7<=epw{Splc&k4(B+56$W2L}80_m%^P zdJpgEFAw(a>%Xns*LP?T|GI`{RnsEwusF=Zz^Va2w@nZQp>~W8&wo!;r*U9+Y%hl? zhkDwQa7=UP5XoXXijNV(n`WOh-a2y0)}F!sgCQGUY^$FA3-orPXo-*^67u8!?2jED zNqN*W*LeSg$Lt*IzqCW0GJ zJY5~F&rIJ;?Ec8h2lF_Z}wVWzA%oE7#WD4eVX$@s+G z)c8WUnw5srvfS-jPI0;84EB`>784Wf!V>jMZ6$Xwgdy?8m5?3y#WtpigzAVTH8~7s zZ^Fe>DQgYM4zyOd>}@0gk{8;nk;8#CA}+*G+=TN{RKU1QHc(4NX{9{?t4 z_O_PRq#J4SD|yLM>6puYeQk&GRWwc@UDHniI})|dMVp$}bRAxAk4?+(uF>444n$av z)D{ZV)*-e^D9P{P6@?_N24Kv@dI)r5I!BPnfph+Rlk%0CSrnAgRKKDbH8(B6dII;2 z?To5LhIFEXge13Cg}V~27J^U;-a8( z(-$T4e&-dqaou`xb<6TtBlT648H2xM0dw??elDbGbhfI!2lm7+)?%V!r@&TWscnaB z(z-emGXCMkj&Q?s$ujaZ}2f({;&~$#CQ3RLn26-tZoQ)P@}znqv5bZs*LSA1owM+rUT0a;KcV zROQe?WQ_9mUX4QCe{xdox`~JX!Z1@AXW}$4K0MVX-YVks*oYzpro+Aci1)?idA zQ>?cRN0o=ICMwNs#KT}}Tq_*D+Sud-Zl!BSe$c?C%4U4rp5X?em7G=K8g;%~G&O1; zokltPO^Etn9G||PL0@7mUsg6zx8HHOQA3w;Bzgq-XzP7~HUXxx1bVcp$$_AsR$VNi z=0a%*oy-IB4uv<(I-a$J%hF=|&5>oCqCjZCS1bFcYfIl0&I7%twdOrG+=+pY!&#=~v=- zsEpiB3H>v-s$rlRpPH(>`C1g4M@^&uw(7abiAiN1$)=rUoCtc^;bTv5_uCi;H9F(OPxZvlx9XcDhw4LU2S;?CHVR7bcyKgh2sdbpVhSp`jIk~Uq z?HZ$&QMVbj9S0fEvglq)-V{~M&I*HX?%a^vHCKx;lQh?E?!!DW3Q+)+aX#~RJ-mGcLPoU>3?d5capCurXS>jb`Udmi4lFR- zG`TIVL`@%CLbs~P7q-lwY0rP0K$F$!auf$r=9tYED70**z-f?VZhE3IbE2GR$f8i@ zX*5lU-H?|!bntwf{&dHU7B{QL)M4$MHvi-!I`D^1G<{0cVj_IIU{Gs`0O?gt`yn=> z9d>A4AnGEfo{~Jp@UB_o$}$|V(5E(D)G=pxa!78j@Mw&* zGBuNmjE8I{l&J#KX+F7|32^(lyRvILfoU^GlR9hm38SO?Qtp1prfqxeSgM;DN{T=7 zKAC0dbHztt)F(3YeirDcQxiE-RN$vs{%eq%X=$3=PJ}$Uol$oqA{gG_XG-=+5Uu70 T0^7`neV1@w;)30ZGqwK*)4n6q literal 0 HcmV?d00001 diff --git a/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..e3c902838daa88a79277fd3fd37f93dccf7aaf44 GIT binary patch literal 109672 zcmdSCdwf*Y)&D;U83;<8piE1x)Un1It@tD=l_<0`B*2Ld8Y?PRs#uWHib{2&_(&yj z67}?SH2T=qwmz*^Yi(<*t*rvyLO>#3FoHLv-fw4&R!~a;Tj%%w?0wG61hL=W_s=gc z%{gbEz1LoQ?X}l!ud`>yH+mw&B9TZH{}&38$TF__mo$I>^Pdi0@3HTSJtBV@`S^az zD$*vE=Vjo|GaOXf95%fxo4hx?s=KSx6VqO-+pf5JLe`^jz2MR&Uy3BI<%%{ zx4Hmy+tf&8-sQVTuK4n?-;T_yh&Yvz$mYnGzPNj3I!fDi$-|Rzs=OA@TuXIj7yCZrr5UCtpQTJ6iaTbh@|y!XGLW89=Zws0phyd z!w0S}S40v({j`6H=@pUsUA(;VF4`Hyckz;0$e;vZ_EDO)j{6^|&`qD$|Ct<@wzhmo}Ibm)L zZI1gyz1~BczdjQPky-lF8^Ak#7xgj=4St3T27-EXc2V#AvlgB=H)7}d<)GfTK2dLq zy8D0mpOdTEeqKc+|KYYsB(`rdk~%3h^Q2QwKGDg(Q}4J>%y1v-Uf=%TUi2L*b@yI+ zYSyoCym4D6RYV%!eNg0ZqfhcG?H#b==8DK*^>QC3PZgeWTz|6@`_s{myTrXrVjBlo9r@px=wr`gqYj#IP`%7mz zUey5&Jk#|hKykc9)s64c=jG}%F1($lHLD<|I4-@`_ZuW%k$sRm^QM%$#>0UEb_b%8M}mSpKl(UjU4$$N7hb)5m;7H#RX*!Yz`&>f<4Ap^xJ} z0!!SwkR`S9ACX8842G(|OGcvmaMv5*@F9 z>^3h{>3Vn$Zni8?od1r;Dh6LH16hpRx9w;JbW)-^1zgg}LO6=1zZ0 zozdrMuO>m0#`TSBDN67vlFmhE?4u^9Sdi5r$d}a6i}p-Zeegb#e*2?azKi5AZz6@B zZS=)BJ68+$(@rnak7lRR%<1nFva~%7{3MFJ>lYwi3O&+U@U!Fg6*k-}aOF@WFKRBP zpF;eu|5dx_s3E}G1gt-LA~p2y)8G!?T9Qw>`1a4yh!u1b>TvLf!MbEp_7gD5UCQaU z(sVmX-7?ni*d7Y;vze0oiHyarTQgr*n!d*^I#P;bFUNf*?LG|SR%9Am-QIMrW{@6N z2ms(yg=^cYu}jTk=V~>DTqXR9gt~`DR5tXPTQd^`9G8iL$qGGfF<=#I{@e_Rc{T5X z1_uTW8g|j(qM*UV@&-E=*dCI*XmDcCz$tI=c+jA27Yz;#8q6whaAVM5&Mq445j0p( z-r$0u!J<+F7(20CHl2vEnDQbciJ~DRvB*lKEiEqOL0}1Z6IM;Ah%DDiaJ=D#HrDbg z$6Z;7-~Mx`;0>Q2d#|^@d8{TWHoCdG(6b1s=eW;%H6MLjWNJ`O|^xucY1QNeHD^lJ-w#d$=j&c{;GNF=F^@S(A`n!>0pGmkg5YzU1pYd z=jtJtv}nw-yPgae1l$;J1tq&KHhe}4W>2#;hO7$+r6pn7uddHecV*0(amkQNc+rdJ zOX;_d4g_{-{XlcCW;$=JoGipY|1-@}%_NFedBt zio0{eg0JSHf0Xq{KR7FRh@S&{gne54yqZ^dyW$To3iLAo8~k0_^!8YO^J-R837hR9 zDilyct7M8HDK`4MZ`Hg98e&zCSMxJ!Qbq5*nw))bR}F5k^ze_yEEs%~hc1Jk=+TgU zwYq&c+|_W}D&Lcs%`UD6=&H<@keJUOhI$W#zLQHcfuX!dTo!sJB89=jw9c>l5M4B6 zy%hDG2m2+f;Z0#ySr}k29YA(+->)6~QmKjKIlhyd)$4Sx%?vk7izy#2j%sTV+a7{L z1BCWxo!pFi|8)8@uk!$J2(_eENI1UKDXEjoY(kUuX)yiG#uIb;rzJH%&e7*i zY`>YmPUe4sw0taOM%tX~&?N2Fx5|82RO`^6QtcC`~a5vkKt zXP{U+1Xvv=Xp^5URaBzCwRe(!7iI@_z6HdmUv5I^oGxD3r>!7?ErQc=gr19CUI z&13z6_l3iR-!_OhspQSzU(5Ul_ff}vw#8jDRaTN#PmB9d_Ko4$e-Dp!J;Y-=cP8`r zihr>IISg*iHn&xzTpzh#XsdPmlUvtRW~#EsE=*?MtTHzVui13lOt(w-VL1L{7}@SN zXp|T#j|F(q7Ob10?^r+ILq2ZttBRe{TWXo2AoSVaJX3)Lx1WBPH0%Z@4K`JOm-eXL zKENEjsx*HDVuOA9|ix$F~(V9~J8)9^AH%8Zkp8@C)w9442$H-gv1Xj?aPJc>@ zH$o2w(ZglxVeTVFh%Bnn91W9b_<|XGkslD2{YH4&;+|j-VI@!+smUA*z(xTC$-k{}N?+$9OaoiVzp`=@z$t`65P(L@|Ri5__@i0;fNj5FKKp`7c>( zOoe2dO56nt+{FvsMGaIr!d6KpySKLQ&9h7mXe2>jVvVF#qI1`Uy)@TLI6fyN`kNbU z6N%2oO+%X&l}inme|>5TB+y z)B^kxm2@4jquXhAG~Z?Gdg<=@bGL_fx0ZN*UuJkG{y^=gj4gsA+VT2pBL|$^379!6 z6K*Q)6b=|q$7L<>FR)LjipQGTZ=ij2i`y$MDcKYMT(8Fa^|>dc3-SM=b;?-oVZAc` zalF(-O@O4!P1%iYNI3wUlniSWU*SgRhV1;!*(*F6*SH>cRT1KvHKOkH~p}hX*z&FXN-+rMd6n7bXTTlRnu%TEN~us`I)$b&`8 zfCNxyqW_bvQg$?OSNgkaaPG#&_5PVsT?{dR^8TPk9ewm5+~M~z)n6TD!L%iM$~eN< zQ;soqm>|t&W_V$MH8~&oJ2Sz&4vsc@56?qJO_EpDUh|x$=GjXIQ4LiXplG$w#0m{B z|0$!^qBH2-S|y?F1qaG*3&%$%9qQySfu6VYYH-Tq7tb-07Vp@$R4ye|pisukZ@GQh zN2;ZvVpIE2Pt1qWP-DQx|MuTayzB55_t>_n-D{WPVpxL@;(i|5nK+LK)gW@OeVbJk z;?uZ3971tSezhG-XZ_A&K})sdoWkHwO7h##(;e_mmYBTZdj;dJq zEj&TOS(Xa&_eX01{hDIOo01UyUoQ-N3Q?D$Y?;z+2kwkWk*LUzg^~b>1t-^#xligMw>(0o+2&j{PkAW8B12r3WqZ`pEe-W!OEcd1~2+7YH zCK=82Re^ebJYdXv>pXU02L+VNGs+(xmOJuXlNw*FOR-1#EDV52{z4wg__56Pt#=WV z>#{Fbx9e*9KFhJOI+k&N?!PD-Q7qeWz$F~HMiSNvJ{GlR{Rt_CWuJv~Mf2S7&m zeZq3T8=*U-HBwKm_Yhc1?-gZEd>_MeGhvUC2cG(7N`bfzpeb%grE?VmZh#iW^_}*{ zFRzmum8nbMQ?wdhu^|E&lo4l-iewfv78DJi;{uLTxxfCG?>%L$<%d#tlNkW+xpMY5!H@*8Kj$t|ULo>~|Y+HPJPNTxYnC z`Rm>+pQzgm*(};j)J!I7P9!sfm@Hb)Z0`6uBw;W=GY#VJ&p>r*EOU&JU7Vk01$7^M zV`s}4`4n~c*JSKqTJ{>QyDg<@k)0lbSbD!Gejf}m#JC( zYI!c;gfu>c`h^+p6aE0`NqE7jx51~__^8qL8LFZwXEkPGss0g{avwGWzi`pVt{xD* zz;P@%64^NfsWQ=eE#e&GYa&3#Z@<#I*@vWa7bFBUnXjQM%MWS?7i3UETUWD%@i$;1 zUyEj#`mN39$iZdHz&{W3Q3Tsol^Kc1 zk99weiBLw8rL7{z$oJrp{t?<3HkM@mb!vtBFn9r!kfizR1jLXbcJ&S1=da|C*r($) z*7aY@m>GO_p5K)9j#}L(VC<>xLryM*6Z*rt>&@J(RFt#ce+xh6}|N^FS|Fzx-dl| z-ic_9vwOSO$GZ2_WAir0JJC-yJ(R*vjve=p8FSy1%!}hzeK^B?S%}Owx)+ZUJFp%V8o*UuTJ&$=x zbsoR40~|HR!Rba$v!And`Z*OuLo{)B|C36*EX~})s94r%t{~ks>5_J7q-GMQ(z`X1u6HGS)Dtk9Vj}^g5d6x{yxnD z$QFc_rsMskGy(2#Gth48!J4ySsjDj)6$I?wp)z;+ELQm6rkE8zRfu?z!Fy8P@w2Qsdd3&G7EtIFp<$W_b~x*%F$96`@%9_Vfq#w0G{=l}doB;eUhrrj6b zN#4x@xyH)`U|gTzxa-ong>{aixJie*Ni(F_Uq)*nYLKuA02K<)roBv^Gee^6c9t`& zIVt6yJh5@T2=L#O%gY6~z_vrffx3THxJ+hC#^L{071{%HxZl7z;2Xa)sdufU4Pe;ve#A7z)$tZIK= z{n^OC3T50D~!eVKSF zd;G#GBCsnW)wb(-G!+W(sOx806M@`GWzU;k5w?qU9S2gaxs#I;0DdBA+8Qub3@sRm z(f5LNqhNUk7CIwnH2Af!QRDhzi#N@{xBKU+#i?46p*}3ckLf^;^mX?x`;z2D%Rv6T z^7m%5WY!R?Vo1UO@t27()zcr7HltWl+D+HZO0!KdOLz7Ds4jREyCk$@huH55FSU5L z)ZNLIFFZSXTa(z9nf?i75QJ~&_D#LT|g#Z{HMm5+RQEBMJl4eq1lImRn!PrLRp^-oB~2~Hp!=yf z$GPa2L4nyclUs<_n0O>A*beo^u*v(4YYRQar!8sk#s&l#JLAY9fTRo%Wc+riqoigG zZb-YCx`}Bl-Ui2ASO?o7|4F@LM!t~thKbR(r!YMB3ed7$ZXOXns>|XNI1gB4%vf}< zpwOa_=EgHd-V-+M0U+y#xgPSKU?`Hxgd_V?i(schf9En`%D?{9944E~0$pOGQ;TDx zJFhgiowD3{x{V9#BeYkD%i~LLFJ$&8#5=yv6!bS6nDi<0q?nLp?TI*mhrGX5B0a%40?0cpM;Z|KvAl9qXD% zN%m|-z_+lUnowW>w;?msQz55BNWlMP*V)V_iQL#*jktSGE@^EprSpK-D%B$rpIj8ldoOb;{!$P0(!7yg?-n zO9c@x11Ub50buvVv(2+B%(O2q%fBkb`!J9UK{GGzL`<5e{d>U-?=?NR#>ksx)6wG4 z)51W0gxRx;NYyiOGIqw+5PKnQZ^_<)_iB0#mR7)T0T;lD5k&uwWhhIrp_hLr^gX9O z%`Tn$wtvZbJx;IJM7bAf^CMv>B3(V(Lb03zNf`8;G7>DQtA`#5;4$rcF-)isOSwnX zWt-nW2w~XRnYT zXX;YRY8PoPwClF0gh~Wd+&(!W?X{4S=za-aA$}P6n87yBD#Y7)@S10ZycyIkRo|uj zND4wAKWMyS1}H+=meAKJHQ%-TiIlac$071Z<4bx|+7Lcab0n{m+MK`+VYO?R44aXf zh0k@8h%XanQoxL}7i$_awM13IwjHWdh#$q9!8?Zd1D)?IMjyCO$L<*o5!rD~yX)p> zcT{E$;_qiO`!D+%BjYv^>uQ0MP@Ae>65)iX z=9|-mrHG=eJqzf3+5t79;^x0+3?dU{wb+-(SW%5z>@(KU zL*?>Li5PQI%LJSakxBw^LB8YoKkvct6ptgGsSN|sL3g&yK6}l3uTWER8nYyygr>B+ z-AbY}+C`zEO+cWWe6ZMNyumcTD zw3_RE_A{3oZ74kIM+CYmFEgV=A1|dK8sQvD6$0%#o|_VKkHhMu#4CI7FcKnu0n9*S zoBkdk+eEnuRKdSD5uoGP6zNUHezbaO#p~Uh0*V<~5$(;GwKuVY5jwG{=2^KkY^TP$ zZl<1k7ab~(hSXx%j^*p{a=mdMS!(7=u-b z%Kg)3Y1;C80wJ)Uyy*7;k-d0k74vfTW?ZTL=M-wy=C ziwZ0bO&aE+nT zpV_an^ksJEfZFLrDdLg+SmW#^6 zc{aJ-4AbaD^G#4-C$-KckQn{a-)JYnJBHMs%e2FX)JcgL(!Zv@u|boR&qEKpThMM~ z%uWP9jdVY*PH}+;fBJg$mBDPr&c!-&V#LZA~ck1^v6K2{@Y3y!I`6Tq!B33s|u?zl3 zqeN~&Cx1Uuq@*p%SGJ98!FRGLG|u86+mjk#ZVIb-uZvF2epHodaZg!*cKMnF7_*VE ztKvjF(pGPU=r8QenlKaa?9P3i4L9|;r_2GyN8ASM7)T^xqr-AF3ygCcERQls8wUSE z@|CLA+}G=m%T?``%Dz&Y*~8t9=UM5dz6=R95qCE-&*!+u3{cSi ze$Y4mb1K)iop0u1mn*WVz}UWany)i-8zT8^)LdD%(t>3waPZF3`qysO+sMMz(eics zrpbtcBD)B~58i9#=%&mqz&4*ly=1EIqVvJd(BJo05i-pZ{>MdUG8 z*H+=4Tz53Jb8Ae?eZ1qwws_MNJ5hSEi3vQ#8x-ma_=0&}$yTggq~?39<9T`b4k9^a%JNwM;PpX^xiI)Lxrzw_u3n zZ`W%f{wg$KOCN#_4!|#F;SFvH*XIy=cfRv5-G*x=NCzh21Hm-i=-9h?4YhwWtYr3V zM_ipUXl)Q{^JG7+j$JWJ>m^t7Bi=Z3$Nb5uc%<;Egy>R*4R5R8^xn8ig zgw+e3tA*N-0UEymY(57lY1X2RkQX#X=&Z3h*ay8p>Y~4bQ2kJZBHpo>I+^Dc-&*gV zH(qE=co@+wn;4c`r697F^nx)Oxiqw~ASkxr6#yio!EyIeiqFz(!uo99Fgy7cW;Mj~ zdjjiaI~a$t!@~IQIR@R0E%^L}_}`whq%@XuDXwZqlK)3h4jUu1*UMfBw7|M_y!L9v zJreNORZ%)-L7ekl%@o^PIDlFhlLp6IzR(^KsW&oOh(BDu#U#K@YbEcbrRiP)ja}78 zXUFEMmcqG{g!ByMBfRgde3><9N8weoPGF8dv)}d|YkTKtgy8JfW*Q&(Nho@iCe4M= zq%0pgK12~N0@Xc`JO4!*Fh82W5Cat*Er$$Y` z5RI0yhZ+5u*{6F_rYn2*{g(@({`DI)>y^mRSV!NFC{|iocpB#P$8L~d+Tw3MfWEL;HaXb`O$Qmm z21;8rxp4UkI_e3Z=*v_IZnB`wK(l31dw?xcy+-d&|{ZEBaS~#~TFF8(&RF@My|F4eW(h?-$aVCILfhi1qy) z-VfG>`xoA6?AdI}9Pb@l)pUGS`+K3#7Ae+9mS^S>fJn5E*>2>x!sFj!UCoyiBrj@9 zA)WxBW8t(fE5cn0T*4ZcK&4dwG?R|l;%=MjUaH*7Vm4uz_ag=2_UF%`wprhfcM5$f zuY?Zz>5O@BL)=VZ-@KC0#FC93W_HMG`A&`(nzpYL;$PraU{3<9U8m5)PQa#E_hz%U z0z5bHV7{{yvI$7WGH-P+T$EQTU`d}3nUBtQZo?-{dlT;dHzdxb!1i%zDyLLm`5lgb z>;9So?4%0Bc-s3z2k^bc2{E2YH@uMIqwg1Dv7G#MXD*9l(O7-llv*w==j6tGo$LVa z(z#peYGrM$e6^~z;%R+{d`?{j8S!Jm6TzgYEhv!4MF3NG%=Ag>G#tC#j{nsK;Mw0DPcn@aCf$(mhHaC>RwR6pYA?OYigta#uJ5t zb=+I|)K7DE$+yfb#JavNWOFqe27v!D)np=w6i2DtU9?DCWxhW_@qPR{z>|jjGjiwE z`yn)2`J$1JS(EzpBnU3$jDB!+hL}Qto+oji14PP`b}wc%vXw z-hW?7+u83gip09U&0H%FV3Ct+uV|g}`zK}ov-JR-h+yy%M__}Z%8u@y0bp**8p=BUhi~h)v{k}PAuTvfMr%YBdF;=rP z-YJgut`Uc!6TSsF!&nG0FKghq7r@$MpaV!V~rZHsF7S_f`wMcaAN382;-rBtzV<7+}+_}M0uGMgiQJmKP zB^W1w5D9j;x>j-@4qrR-uAzjPz&Rz}3*rKW_-^2zVtbZQ9J|nPQ7Z*u*gtLUto&K5 z93!9M@Suz~`Qs?I^40PF=SupWVLCMPmUa#5;)HH($+LqHWT&g-JcL$@>$kXPOD*gQ zuk+yz`Uv*ulx7*GiLfoK9TtB91Pd%fMAE^hoBAg8J$1j?P8m-l%W|9~Qd%qGvod|n z7MtBKZ4)U)Dh0TreP0;wf010x zeMbu#|s*|G^=PrHAqYonyq-J0@Fu1B~Ugjnx8MVn8l< zAd>tir=ptPZ68S@Sgd<*gH3c8TUy~(3wjZiw00K6(d<-1m{#xP1T?eu_mQUsnI0?? z1$7HcLAVfK92QR(t-WdZvG*GKxktKxciib?$xJ@1IQ zGp_*#%FpsYi&crNB4toqF=`Va$9zH4kQRThl^-eF`9`Wq)d@2M$4w7mK5cdI`iG`t z1ul@a{|fQTm@EeA_T%?x-teJbf+p_Y8w(;@_j(=zwuK)Sih2r`^4ZySb$W1eRmmYi z1!tfT|EnPNW>P8l!^T4XYCr^ZFU0rcmD|rZbVfdF-djX1hR1n8!bKyU?@p=AuaOG8 z=ZAHd(Tl}1cF*R)+f6&>&dBkv@poXp z{EH>LM87*RrDOo-;%`1@Nq8=~d7;|-7g(s)QFp2Af@6PFY+Q^8o-+Nv|-!mc~1I3Gs}NrmxCHUtoU%DxF> z;tL%w;x8E^49fTbHZ1?HuW@ZY@sTl>?0hpD`$JZfYwTN$1h1qXdNlMhB1%2v?>03< z3ibe?@O0kg(lbP!XWv;Y7J*O<|LLjv-c0-26|t*sLvG6AoyYsMdGC$ekA73;vn4I8 zHdD&YEZuN*Oac6#96Y8pe#who7+g@hf5}aX6+{qkYSYKDt2Rl(dg&vpNf$E_KS@P$ zq5CV$>j5CnnEMvB8`mF`JK_y>kj@=)WUDuWiBg2HW8(N&_r+R2M(umUQp>ANrRXh8 zX)F6?AXYz;B8TiWyKPvk`)wXE z*VDo}*7e(92nNCC68=Kk;v`bggkGSHs$H$Tu~2@%`VN33k>u+uz`U^70zJm}u;8-? zucqdv!G92HTdeyT0M3~E1Z40=Jz=}iM%My(ckpR-rEOCG#-|Yj{c69J`cMQ}6YJUs zSjtzIH1Z$%1GRIDG!_ZUHu7#alkAHtMB1^xRByduM%nRB@z*vGUI2WK24TH?m5{vh zOWg%lf%z}+Axya7zNwCsxRUoTWXA(Q!52DS{KrIrIq8QArepep_(Re$#kNI3psdW0 z!vkm8?gJaskW?^xh++BpBjs#`632Qlj8&r5LTL5iQ^2*f$~`K_#0|6U1@sY)Gb~sFGCZ`XUwr~ZHY7EmmbVCy`{)Y$P;!h znO(!+s?Mx-9c)(VuyCZgn$Oa6kqCw&2HC=f!TM6AM>OG~$Skj9*<&D0>^tLaLr zp{>DY=Qh{F9pv0{uuazNw3WnRaQ+hbZOE$;n;l@W*vJuWe2|0LV8frm{(5+xM)YP= zANIxM-Vm-(2W`wD7?m9T;cx~RJNKg=04#^+SRku{d_6BZFKn*dz=VmMHz$N zwg!d7lPqB(;w_Od{>~Dbywax`-^ZBr#YAwZoOf)!)=Yz5NSa+Il&x#-UnO}bA!|L-LaU`!ql6v z<}GWGprdS}f!Xc?W2ndxXoM0fGbiC$XV^5+F>Yd%JCh7Hly5a3t_M&q{tsPj7JC9x z;NkQZo)I}T&&H=Vr@xjMQa)FMmI-EN1sPPM5K5|fa` zYOR9(M5_`t9s5ZFApt|odZZ?aietq&d%!T&x(Ux&IeTo{JsSqQq~5HyG8_$m1p7yD z_Mc4Xq;ZH0^_%4@PD2IlG(0+6_Si|#PS7@ zn{I=>ZMHON#+K$X^jxghsEwreW=Xu&+L$W$DvA(2pDqv8NrgQu)HgTI_l# ze-6#=pC9r^J>GApx-i+U`tr;QB(bR}g8ztGg2E*URt-oUC^YZBnWEfc7-lN3_*|H^ zvQ3^4A0V8scdV6MbmB7INf5J!%lOp9heCQMsTeaA+2Yb^$O?TnO&Oc1@we?TwpMos z6Q-`C9bb*Ke+G$=aNH4w&dHF@Jsc2%pZ|J>b1$$YIfo1^kQVEe0on{5Z+KN0PmZp` zAl6=b9;@^*9*}(-bb+cSUW%4)pgW#WUwrS1tTgh(kazHhMg0}%*Ie|(dsG?3*BGov z`cn+n22Vyl+~CPN58~nBG?|jp6qU<+BEIAQeYchffij5l44-#7-oA>|o$NH7J2tb2 zB~R!$I-<|)PR6cSpBaV7@JIa0Fy!YQl4n}os=5$|;u+pa2Fy-ZwjYtY zsr`5C6`CZ#i(X+Y#Q@^tY)_PPn`}nxD3dr^HjDshneZ|4OzVB8Zy>dk7_4#DI{WKr zfT+JG!Qvt~mVcMXALdVUD5SMq3}bL+BmU1dW_q^xNHZd+WGN4t^h3-Xxq~r! z2-`lKiy6K7zve>WM85^I0EFhO0@g=0RJxz!6Tx@h^wkk^EW6dV>d+W`34=u=EM# zyXXQ9;Iv0sIR39qw}Fu11Xu^BdQNiW~x*W>iLeP*ti91Kc* z9OpIHTeQqHyzO6Lm(L%B({121c(WP*r;PWnW(1!$UWis2?A{je(*itDKG>b$GtN&P z@5z?DyB=?=0!HQIoybjDe+N)Pc zD=W3yN^|!V1X3x1Bg`vC!&slqoWoNTPIs==sDl7txi?!BC3Y2?pDbt*rr$~o=1KxL zv=6<>m$7AK1^^Kul1zVn6}!L1_DGc)8+~WG=AzHtDWY7!U_$+7s2Io%0@-gho9O6$ znx0(Eo}g@(IQZE98xSLWTBlXHIrgXBac0s}Dk2Z)nZQ@(VB$&{r5mThQAy$;n3Cq) z7^<|tX&M)vbvFaxa6FzvFw76phm|iBMHCI7;mQKX3}j-;P>b+W2Y+Cxg2yrx9FL8? zgX_yAxXv$&o=W^#^1m>rK0Hq%LGhHJSORB_`=m&QW;zetC+dw1935Xf+&B>$Ihq?g zzeB3ja6VRo%|{B7ay!6)j?AVUXzt{tY17Fur!$DSc()dBSS28~m4{BYDU#V6`@Y)g z*N{MD3XFxFeL}UyZ};aa9e1=TVSL&f;=IoODb=<*?{vWKXlg^3p#FR(c5dJN*uvh} z=!){?fxQ8Euae#8%$Qae^Mq^KO{%XB3(xF5y_0rRrvIS7&&gK;2Q)Q^23`kYMy$=sHg!rx1ctxI&uJ_^x1V@k0CBV%qn?XmDBxQpJ;#vi!+O8m;l>>O zqA0f!CRgAQeK;O$Lu3x1)NEB0R%=|(Q+F$O z#R^ub1hWji)$4~V4 zB;eH_FJYBclZ?#OndF7|i#$B0o_TkgoYWsFQi?T_tJ$)OJGuF?xsv7frZ@gPYNc}r zET)QCN^)}K8==XDc=5;3NnnA#UpR3V8bZRCRfUkbK%unIKt!IhX#_aGN-JjQORFzn ziBARq)NwHaAfDmF&{vtttJpY{-(^Ao=BPzuABAT_O!#9Ev&@+T^Fdwtr{R(LoQ1(M z!ri2OVs}wN5k#VGaqdf7h~*NfXb;Xp%%5O@k3jSR7mH9LJKV;}#joxIO04CPp<+Dy zZ%&cfOs}7VTR4Z8`sjkrSCn~3<9enp(z&uAY{WzYXxy+u{P*PZk@dS&F*sDg{Opnw zKHe=e$C*1|Zfjy)E^YmP8AlyHw1GQfj^@ZChGw$9Z76P()KM6UGXO1o>ztYpQYgP0 z*K^QT=C;9~gz}uPPZ&ThUa9ekT=mBJR`kU-wCkHzpq5|AG?z)clK#{}eWnn9p^42u z)F#U`Uhx$B!R(`K)^8@*FMh~z*LgPd!;lNlhT7j~JljuE9upG*P6FOuzQJx^Frh}5 z`c4T%F_tI4Q&DNtegPrGEUl~riQO~O22W%69E+aOG)R`~H##VRN91Odl_2DprSg*9 zR6}NC#IsFgVvTHe9o4!VIur^o6*yj#kAjkWYQ4uEsBFo#4Wv4DjJ)WQtlvPk zd`s+suUAaXeS<>23%f#WmCC-|&@ytMC3g4M$u933M)jj&m;Zz}bf~U4FY@{!@zlCr z-sR%^>VjVhA1A2I%Iz(MXdLN;K;MJJgFxj|rXNtvppf;C1e+ovmtO?(JW=fx%DCZ$|9qCu5hL!QGXqsFTfCv^EWJuAU%taW>vGZV*vNIsLU=zqEMmNt>*H3Ml2DhmgT~03qHBq4y~Sp;JI6uVm*k{+y`q zT@1%_EDbj4(ui97^?KvwTk_xbEce6xN7FZhkAST~9e zq7#SbJG^1a9*D0UQ1Ba<|3@p$%K4dbw68V4DHFK#W>k2ltFZT?KczmJQ^~VIh}Fb- zdrCiJW4u^v|FwE2R0obVxfnG!(8ynGzP^Zc>DW8-Q#>t2lxBcZegWm7y@ZfXsiUPr z8*B{d{4_-myl4zePzY^Q2vksc{AXe+m;Bf|Ti8NGfwTvH#qR|uJ9$&3q2532(+l>A zT>!aa%M>95@9?unbW^$x7fWBk*CX7`fwS$LcN^0AV>YH){t@?WeO)VtM^4AOeQxl} z>H>0KOefkBFVgzY<%Bj8>KT4}%nmV#28rGWfllf^`7$Or2sSlW+#k|jR+|XFbHk8+ zW&P(XpzS4wwn^DMdGd6#~!w-x?V|0Gt)1#v~{N~PxB)=%Msv;S1G>vf%AK{;y2Acu4+Hjgm3xC z4x>8U=wiQShC;b4DZ@5KBX`x^sqQEt>k+ZPBkj(s(?KC~7E|8eRd*$qTK*pb1^c!f ziq@{CqQRUj=V0AkuNLu2vb8XozFLUCe1yIVjNWuFeep{keg0<&k4T|wFc>QXcIdV_W|` znKJHjFuRW~PZ*51fu{@&^FiV&ineB#@wYN_C?vt+)C_Uzf^phAAOfWc8K=fSD#U+F zbAA8vStoZQC+vKIH!@1%O_W230}AnH4_E4xDcpxbz7?>?Zo8Mvg5i^}(JJiHP>69} zJ=kMTncXI2T6F27nk_bIj%$>e(VJ;(EC3=wPnz2c7L@@Cqc(H>)T0zI?Z%a+3kQiO}kAv9z{MU&)Jbosxjp~$V|4kB(D z&nSrJ`3vRhL^-=DD`}>mSg`5I*P3#CIwt&)<iMym`@wo6p3n#I zGd)dy8LTsJf!Qc9v-!Qp5Dm=hh;+kuyxNc6%H)`#NxqI+vQNxr3gRUC9<}_xgOKCJ z6htuQEb(1*!quM#8aVb6STZEm4s(7uh2eQQxtcXJFT@}CFO5E`->$Fqf5#h|j^W7^4}Mf$91V+~vcbPqZe3txm|2l*(<-_K4)F{KA{nDZO zQDTy%J3**my2w>t73;bPWvH{iB7R@s?xVAJZx;W8^cuk$Eza z0+VSuuP({s5k!$;*2&Kj4AR%JN2J|1rSt@N@DQ_}THQjr>CMc@z{BGp;B?&)Is}!Z zZ-pp%ME0*CSBcMK)9%}a_z$)-{8f|5!HHy6D*0^-cbv3zKAots%=>==!LD`E+%TMv zfPxLSKRF!R^<#F~{@S#6=OT^i+ni7fY}^87FI9Nv=-G8goc}a1Y@t;=LbH}tyBqz; zGE@wrL2(0oZ;^3lBcThlH2M=|&x~PDMM{*hMbo5AIukS;8qzsEeTWIw{n zmrnxD--j(LIYYe^dyV3Qy#E^tH7EI5@U6Mg=kHceYQIjwxjsz!+?0=zKNsx|AJ*sJ zXAj84i<>afL2+-uS@|E@c%jti=V*%fDIcn#jQjr!?%%fBustw(K^^YjiSa`?97NU= z+D#xOqw8uK`2OpL9xnuT>z@uW`jnp^y@ERi+5=-#9mC=Q45}d~np=O^DMRQhAoOhz z0{B9J{~wkk!rS@{EP~BjnJ>FPvJ;m33(LbesLrfQ3>!=$sWuFd`)CABGe;KUD-U7| z07i&R#;J&8ehKE)U~ZRc6_^utmk3awD6x<~%Iu%No821n%U?AFG5g=xDWL&mbcJl7 ze3I$4dXP@8q18}*l>adnbEqeco%D}-Z1LS_W#R`D8STLut?~0Vz!{mPa^4Puy}wL{ zp&4MMSgz)qaIfiHrm-1=e6wIZ75y#E=i$`!$G#R&>ZxExAN+?U(rWb5mjw%5eKSY- zBD?++$X&*<7Gt$+9CVRiN2N0Hg`5lUFNt5x_eFhXzL!9V|HFJ2@rDA`U=_&U(A8iB zM*_q@nF%m+{YWr^7e2HjSiuNxvP|y3@~#=dZwwv4f8;0e-(%PK@650FtTgy9{=ebZ zT(tKNiDpZF*~dlykWq{`IAXv0gRcZbc~C={Sct#GvPV~29XC-nm) zYIUEQs`QS>ZL0WHQ6>=KzJOP7#kd{4xLEhY@hrJaU4}RF^oHx4;5u7Z#iW(}HaOnU zOM*MeBe`y~$+LaB%C-=gRQ4hA!QYErbrlb-bN}rqADk0PWM0z8ya|kY6z4oCP-GIq zYiUI#(nOwQZ?x4LQx~L#>wFN$`=RNV36c!~T0wsJ66J~5gz)y0kmDHeZr7^y%Cni5S-ir4ImoT5Z>zWd(dFCie} z>@tyD9|C?wZ%c{fXjsRU+D^N3swr3csZv8pKyS#u36e;(e>0Vs7>r1pHg5zTU#z>I z*WSXq#kr}ZwZ~bZij0mL+Jflt*!?v8$1YQ-S;M_y0*yDxxwmEo^4X+%4IM(c z{qc|Yw_hZt-SZYvup&Xz*yyf04aa(%^I1mf3ExI9e!(r@eIFo=8S6d^c-k)G7vGNO zgB$DLYpo`!MiZPa`&pU!jEohr4#0XpQSZ6huh`(QL?OO+0JK&>v-g=;0qRj-F`y=% zx=*Ovvvlc8PU}y`6DMj#Tr%%Bvm;GrKQaQ45p3R%th!w3R~>#5cIsT6TZ2+aEHIP1@m>!*tbcwaw2}L+cVR? z-JkZqK}pwrEH9|{Yd~q;o%5D$5@&+)X^St0aIU7V97W|T7Rm4>YVmy*lWh*J@A@tf zIsb!aIM#ir5Z@0b_D8*B#MLVTvV3qB5i0u#5f7xC5VPD|+UCx1qc zaVnZ_VlHoM=hb}Qb%Fs?*jIxJe&Wa4XV%|q!-?4w%#D}w=yJ$`@lW`nrW?e9jm{M+ zMdrosmZW11IkCH&E4U`PR&t%lbr{zMu2o#?xkkCN)^1I(bmX$^DiKJ#e(X&1%f6%4 zLw0qW0!e|K-^rgdp%@uQ>rmFiGAStj4I&vsBb^`Ku8{s z!J?FX_tI|=oZI;B2>*q&dLo_PgzX4H=@0Vx$RT0ZM@j_V=;eopz2VCpY(|}^P%rtS z2T{&DU;{(uq?B7U^4#K4d{7`^3#J?x_SQBY7wPe!Z8>LgWADoRFe0G*_R(U>IKHC3 zmh0f@*n@a$lfQ5vQnGuBQJKRPag@3lE+btYWNvXau@k}V7eokH0*~{5(Dv-g#k+)0 zgo#XE%;y>I_8G2E`X58OS{`3x;kMe$RDSV@V=%r-S1a}c?t@4dX{tZ|o+0)~5VU&7 zGi`lv8_UvaCVv!`r5>)NvOwuat4vG>Xmb#+ZIWL=tg*n_i@<<-KKReVYZ*!WW?4@ELPC zx7BR);5{Xan}sOuaI=s&yaCgfn_l5uAC8mvE8v`fuFxPiy)xThRjMFDICC9Bhruw9 zal2mcpoM6BTSul1@+KmgRsjkKa2ox9TZO^N49hz{Vac2AhmMxQ!Tk9%UoI4ez)WOX zfmv&Q#C>>3tG9pqNtm^tfa(8}Gv~^IZCzcNIUZ<>A{k9bNYIr#L;D0IhP_B1aRi@f z+{l!NaDn6GuB#IboViaF(^+MpXq}pLR_fU0lK%*y9@^zypJVp#XEjA8U&_kLD4IDL zRIDR=wET&O23H622Y)Qc)bYk`{tFJAp{|U*b+E+x+lq5$eo$5p$?p)g;RrtmSn&pN z9=;>8On6+fxMq-*{-zhHj>{nqdk=7`4uc2Pz2hLSVQ&h=SSv=_s*eO!p=VH4$=CY6 zaoQLXATS4hv@&S1n{Dx{papy$0zY}IZ8V1>GMI}kg2j1B&|*Qc1*abFW1GP3d?Tnm z&Y+iS+sjEo8(2Q*#mOB9+Xdob9BgJdozWg{ss=_}S#3nIH7x!2UP73#k`gu#lH%*t zi|U$r4l|j`;x|FQ$pVBG;{5wJiJ+V<^>qS#ng(~Be+*wr>U`bI7!*(EYSolh2=m;` z{+tP9agZtorGGFetA6_c0=_}AhF|6(}6T{W~uDSl$(Tbv9kh1X8<&g5BiD!H5`$fYeq>nC~96TqOA zBU*Abdja{=LS3bJMAMC7JVKC~!2-O7(y!%ajkb(V;1Z+Uzgz`;YQB@vub6+hm3M7ioLiQu67cbkg@b zeEKx`2)eb=o+lOUfmW5~GnL7}f*?zN_2~o&;!GSoDl#ZvCx}0O?w95C*&l?ZkB7#W zpEK+b{o`vwnco-e=a(Cdo&FRr=ie2b41IHCw9};|a*Xm}Mmad_0?<_j=;j>Jt5C@o(kV9&$B*Vw^TMK$L)J)TD>h zlcSAF;;Cl6nGE?YIzY)B=p=Cxh6!TbI_D;pT~mSAK5O_zXQNzZ6@R`fjJHiCT=37> zRp&53vZNswm_K798jyGT|!u{i0a+1y{HZO6%l_aMMkDQ<%?^I>VYpG|L~w_sj< zq$~;gUJT07Z@fyQu7hc$qsng7IMBRtXX+-Di@pQqwTYy&4H8@ zX_JKi{&9mr$Y1w_EX!Dz*$Xr4_FmIi;cQD^;AKqFpkc(2U=fqT0{=w|RdD6wk3II7 z6T73&S?M2qrDdc1pJ{+em)YGaMKV(;&@albWziWP%il$b9H;}?zb6KqiLyI5Otza-g9CHf-bZ2trw_`K0ufq?$G;T7XVty4%9p~czggqtk zuAqjyDu1p@;msBMLt(i0PQ4D@RHH2In%|Ir#?&++s1*m;%BN8|P^n?9JE_&UK0It% z(;4Jrb-r^wbIv@R^KIz#c|eW)&p;hd7uZv&FGqS3z)@05*j zqh6Ih^55wTf5o;Ej8Nje@+H&afE%?gYNbfL;2ePRTDP;y1KhFyxz>dWo8;oV{pVWx z=}>A_{^wecY%{e!K%4#tyjs{Z1lGI%xz-*Q)|>yi*2Cl~0BhrauJuKP5VamHuO-hR zB&q%S3&L)H&Q!eb(Z(F(BUl=@pq3RlLYnyrI0Jo%v$#)Y?yBzg+y<|6J=> z%u{Mz^Pg))?Sx+TpKJZnlH{E7T98Djg;W8rjdJmG@p?sPPf`DVf-+E7;>h7V~N(N9$6fGR^e2vBPk>I)_xc)A< z{>)rM`Cj~3d^ah*K-G_F4VbHeH*@d*>UG0(tcRE+DC6SA7my%||J*u5Dn2 zhn_p&+X&-S)5=))o0KRCeAsUBL6uywQDVw zzPlq8dthbb+8$D-$w02q@BRElpC?wVB%`{kxBac`b^=ik&=kx&M!0Q4J0%7gLISWWPI~+sDk77F10nR-tVcI0$0QBnS$n*O zIjzF*Ki^S`+54NJiE0i*Weh&8->=BLW{7e!uT#sP;AfKaFB>h`p^Ad#?ik!Vq(3A$ zhFlL$6y%r>(c<)$iPf3B9U((CBpfox!5^Fc`=h^pO#p>M^xEJye~htWTTJ0O6!KWe zSDV5vQ#tlP1QLt9SI8(E{BenO%$@Payy!nJ1_TQkH~Wi5e`Bb7(U(-3GbDnD@c}*J zSic5!$iXo6%>H*hd)_DhN)RgDl}br$Vw)>Ls&<|{vwixUgC6GGg}!YXM`14~J6P>B zebD|7)a<2TDqw7_-@L%+`^H`iwuUX>Qk3`Z<6?*8bl zJj5G7yv}{N=l?Svb|%A z^km~C-L&d_jg}U{rrY-Gk8Eo_;ES+$>)JP8UJk?2V zpSzuvuMOizr?n5)Pg9WE3s2WOWmIaJgdxXn)`GwYQ=9k_pZSgD$6)Q`95{< zVENeKU)fhb%2emtdb6)S*uG~r`ozm{2P_usQIKiCuM^0p#`WTNeUdaZKkm~KYw2}{ zjr%p#IclQX9FlS(n}|oy1uTDeA%6aFO?yrDX9|h<^UM-wzP9G~6BB#U@6y!c+hcxn zf+~2~W5@bBugZQV_Tf(%(|_^UGi1UeCxrnDOL#{ytYnLKz!;!$4v3O_&#J~kmmpHQ zcNq8TkN*87%lH04a5RzUTvZ4EKSazC1N!C7{$&u5Wd2RaYO*3;=h7d2fHwN)97M1I zJ%hlzi=s69EeD~cwvD*7$76wR+dX!*+;5_cga^LEdhnX7bbGpPPpsAt&~#tRJ-j|D z-82}>o(v7xJvm*9;X&>TgJ;=woZVg>>*7m<3HgVv-g*6~IEo=)e-k&Z%P$ywa!_uiIrWc+sjOyYC%T|QU|`g{@?-Z0SdE~!da zY>wUiNZPATSNt=(JrU~~1>6fS%WhA`x;Lo{??^syG97fjqR!c$QE~Uy#dnw^I^0JP zzRcYbnx`v>Uj^%dFl+Y$YC-_FZ}$_cW8EKWPQk&P2H6w4I~Yx}i_x%=ZbkzEild2X z5cIh8CrOp`tc^*#fL48{EpoB z{}srYXayBmD=nUWR7eMawXrrFSay5inN)T)+`g*5<>&*Rc_9*6GLXLys^Ld@0h--g zFV3{y3pz$sq({D3#B-mt&I)`_y)FZtrTkxTn%2aw`kRWN|0~4g-6Qi$cr3{*y?B@x z4sX7tioqntx{l;2_^CtWbXcDG>EK;PpB3x9pn7ld$BfT#Q#CllO|=9rybG$lqZtts zyLH%C6^!_X(D*9#&VMbxj%MZ;h-UCDl|P_u+FZ>@-LK|eg0yrV4|dCYe-p32Qi%V! zlC|uOdyr>+^+wLt_@iNXc2$4K8{y~eZhl~bquWvNHAnz5LUlaX7AeNlYH8K}H*G=7 zvNOkgIE4eQr>$Ma2`_p!u+7JwwB0R8>SdeM{0NNs*4{sJHoeLvaGU#lQh_7RE-n2g)g+%+SU8t7;6_Uyxj!QTe@%ZvVN6D=R5uRTm(XjH2o0lJ-6 z2IP$fWX)PVqo5CLTtB!wpZiL0eI9+_RWv=vhwruRgZixe3w-!q>py;4(DpvsmO%z! zv=cakFANc9!FlB<&sZ1H?Z`z9KuUYj)2S*s{QzbC(KJsil%g6|Msuhi(cSHDdC~Ev zLFNU25;HvbOo`vT=$__P`+P52%^hLb_IsGh51-edHf6>J>sFQ`^W={@&6Kyl2V-nj zo&M-UT=cI$x+=IU#J>+h=3`6$=&B)z`S-$$ga1>+4=}%_RUz)FMFVSOh`R2I+&jugM=*ZYgd?q11v)7Gz=o1)sv*gdH+E&O4RtfgY?d@bz5 zQaJ~99_8{?!Nc_-efV{NQG6-OuY#RlJ&bL(rbo^veYUqg-Slw#YZwm!|7qjo)Ne4{ z?6&HQ`2HZinw^-&P4hdC0=HA0xl`zLpmU;dYd>aw=VCBwpAx&LZ4AFuR6CZN;q#ff zDY+?NP=#JuvZ{Wt#;jX}i_`Q`=0N+yQugx#f9t85-*)VC>+6*A0T5l~Ww3O-NvJ>u zp~8=ROqqnykg~Xx=rliNk$R6U?d7j_--=gW}*gaD;u~Rg$m3Cqe2~lUOY=DXF5HoH~9+)8O zoMm~xKDQLg4!`{7?|j}C_;3F){jV?L?}&c_HXG@<19sh8E^}fn1l$M8_S>NUa<%{e zO@Bxa)t%wYL))0v+{{`HVr}NxR4!$B1N!$3q5l;4v)9s}qb-<+QMfzs=Oczcy(xA7 z99Ze^3zlZwX>Z&?;DcESL5~(k%$VCeX-36!AxG@NcMWCuhkTJw@k8~{jfL`}K@B9^ zhKKmA&#ava?;a2D9+#Ur6u)CevE#&YIe%o!uqoB)|BtSdRBL91LWHt+Y$GtXWEwtfHq_y6AhZa$MU&pb0{&YU@O=FIge zjAFb(pr>$8aK9SE_ClLcYTGw3258nr4q}x_qYRF0FVCW3{Hbl%>Noj-S5}#glk~kQ zCi}~ONqImue52fbiIGi1WGy7O@eKExxOxhWHr!K`PrY6FwmZMeP2i$y(vzzhTO%b* z4JuNet?>aLTH3=WYSo2Gk!uIJlLF1|4DVyJ+tDx5HM~@~&px-08|-^BPeUunX-B1h z^c76INuYMm#hxOzelMDh@gWn<8JPCYFcTHb6v!NQ_&FJaP#60LR|olyJqsHL9HaZ} ztr4k`J@)VqfzHSs-B0LqEA}V$kkGZ}b%R=Wx0@?wVY)n-CXbB0_Xg>h+1er}s9Y`? z(a8EQ88NrulDUys(wKvUc%Av0xv~ik zE(CT<4u0DeqUm z1t&R)V4nL`2V3(GhKsWf6_3sg&V*|Gs{gUlSEQ#8zNI;_{*YJOuL& z2)zK+Gb(bU&22k8Fu}4ryv4{zMtembb3EaSJU9HDC6=S`bB732MP6aWq+4D$*?H=r z9ge_LvFqo9t0HgUsSY>kdonW|2?QxTx@9VeQju^%MIqR2vNTs?>Cxp=vZ=b_A~vo$ z!$n!_vf=vELpw5q7o#2}BvBv3uya!K3ch6)v+WER6<^6ev;K6bEQl(0E|)jS=4H*N zM1~jNPJ2aPw6Q?)g4;Vko4!b90owL7e=&B*BXbi6)eau0l=+_~Y6N+V-SZ8s%D*J- zE$;s-VQvBDXOq=n+pgesTQ88>INU8pWU#_{sJkdoxz$0KXpbqSk?Zm%uVtH+Gqm3^ zZI}I#%rT+<*SJ-{xi)jOToTJ?bcO|Ep2#=2&G|)F{!6hV6dgs!rJ*$)Tc?0WQYUc@ zJR<#pU@Z&3IPi;<(h@#o{LVe8EYv&0e?W9XM;q*|Por;2+WpyldA+@PR7u+*r`^*N zik@lMuYum4W`O9CH+5fZ4(-k|%Ws9~qWuXLu$LwW2 zjPpZLyBgnbJSw|JcQKTAhCXfJn~+_lk?K6hoJiImm=M4iX5{*qAXek5HE$0dW~_9R zkYHc;3{*KIw{XbpEx8V#D^Lq0iI>aOkGO4rkNtff%q-*!821y=9X{AzcC0kNKipl| zYrp+>1Zm|X8Qa@7vr6JA{Ky`9hWco~X_WJEXQ*F;&Y(4rj}eLZ^Ms zPWRpeYq_|dX@uuh35E$8DRU!~V6M72wwJ#@U%t7%{xxBPl%e$!eC;L2IEfJXi3EZ# z|4**)OBT%B)I*B-$(1Yt zscfXMc6Hl@Yb_u6q(LzO7R~4>8Z3w}Fq>C%wh$2Nfp!94A(K@6!Tkdpb^ka-UrrKz z>6jLEwKrzB-)QMa?5|MtbS+jV)r4+zP!mh252Xpy)!@H#{@2nQMTd5_a3+=mG;0II zE^2vuH5yNzTXRH7soibxxy2d&g!?c&+hkvk{4ivdI-X#Z748UJP=YP}B~u|!kikrT zf9W|gJvuCy?d&lodocZy?Y*}wvdrc=%%O~4tl(j#ky&??*#=AVkA$KH&b8RmM@O$< zT*f}P*udQ#h}kU{FyC6DWW<>tWmI8bcQ4JM%**0WTnQRdR%ZJ&)_et_h3_y`K&rr~ z)~LPhGKvL;LNGv8eowe5i^ua1?~H=M-nNeaBFJm+!~_3CL$yAKKhd8Gi|nm0zyxPc zi7~|!339d1M~#kw3bP%ZKraRUMZNT6I`xG90BNundt%@VNxa&wA=I>u!ri7_*S6=D zg7zy%9eOfp9=(cQYI)NU=rt{yO9gdBp5WV|@eS2XTD~YW#SP?2=`N7c#m_=QllrJM z{2nYQGPiU|B!~1R@guY})n2fWwo3QwvHxcu)p?GsFMb2U_GPGsUxLaq`{}Q3VYkN(!P^6vVGn41h~DND>vl;6Uk%yIOcaJ zdfd3ohl!p~OZH_I*g~bE6108&6;9fxL|Xk8tv>#DC_{AOvxO(Byk+4x5k)ns6e8EJ zAVxf*3ZU@u`2f>IE>MCraMdnL zI6P5uMA2q`%Hf09m!0N-K$p}3!|%VaJGLV+m#>X%B^)-=&k%d20!q5-Gw~pCUfYPu?&q^ zQpq3;{7`f`=a$;u00-u=B(Oe2P`%wo=F+M%8i!O8Jgh?0SCV0t0ek<7~d!>V$ z2Gz1=>~Z4D0s``%&9~8NNyd`Q6ChT6$u*5tx3^sa!J$}& z8Z0OBb4Y&Ysr(Y-@zrm|bKnCl;H7OcoQe(OeOx{uWR}kqGuFF#uk!y3`-9T-N&GBH zU*im|a?r`h2iRrVXz;gEbuO%!Que!syJZL?6WRG)vST`!t3Fyp{e=$L!o9XW<_7Go zo#fxPXZ1VrTa$XH<*gC+wx96~A@M2T)$(S^tIA8YfL0r^l;H5)MmQM1k=P0ORzi%3 zJgInVU$+m~@>X{6nfNpKYC5>CgXFYEYReve6aV3T&ag8}+5^ef3BpbM3-`r;3^3(w z9wkS(+pYq}Ly~>sJ_>6-L5Zsq;+Zq!^qa?f#MyOZg1h$Fr8PA*?yjTfy7IdMXTe`@ zSJ%-APT3Gr8#Uh@IXAyI+!cJYeGd084+gV#&Y6q=n>Q>w3%fbfktGVrUe+CPXN0$N zXF`6jv#@uYaG%{rM77WDaa zGKW-QbJ?z9F5pA8IH{Jj;`6b&Ys~TdB3JwD?3eQ~%g|HJ*Ez9&^voTXMW}5BFY_Tv z88Mg2jGZIa!|`ePv>1;)&NWA5F*op4mfhWIUA;T@0*qtS8GDZnPcrgeBpV@={@*z} z8$o6(gV^~4KkhISF>%lx`v3`*L~`8GN^}DHR*g;I5KJkAqA86bgr0Q*LhB??GKAJx z8Ow-BeaiP#t?`wzMvk*%F8jxJ%!(pWtEqNOO8Lg=^1V1NrF^D5m8VvKeXT4f+go2x zFP+H9kkVat0P6X49JR1!P%6Rvt}e4k6J_`i8vOL;#oPx|_@~v2kp9+wFywy{uE^Fk zv zDP=qJlXR{l@uHB5v*MqE&FVi=_RB3|9x9t_gPGdF>R~}x#ue|7y(=>NQ)lRtlkMMS zCByL8pNRdIER8aGpOs0CU&%x_4a6P(JKDe*e%cxOi^I9*4XJ~HDJI>eg@0;1sl6#% zjQ3{g=4J4Z;fXZbJYDfjw57fE0~q0=0r%Tw|3&LYd8GCjtlTn+i!+&et$@4_SrK`V z{az~`S5qVV2DYkpwe4gmQafEd&&6H&&vm_?;ojR{YU{O!uOT~g`lTy2jI!8xErn1~ zVr(IDM&{e%7sDR(wkqdsCrgq9#%b@e7cPO*L}g0^2nLpzgWDi9_;8XJM-DwXDln#p zT__BCU3t?eOBNQjn@b&>kBKl7-@ZPVQXcARvA69N&y$!VH}Wp@dXc@Y3s0hZayFT_MX|3)fHF?LgwoCkILsa`k>FJGy){3*nv0<7@}{~A4>x`}emi+-dAC1W z?5IJfO|7AYwDs_U8C`qpw!sME?A{&tlWEU_@QWpU z(C0K=SN3)1KsS}%N)b6=Gh%8f(-rYK$eb|9F0&T&^VWNn+elIfdy;%vC*R_v@>NIh zMAVaibWY^VF>QOoyG!izo??HEkn@R+YV31{Ui`#eyGQyKdY}@fWV=2&H@`bTmqq(@ zd%L?nq5t6ms+9hxFWenuT<}UsKC-$aqs0Ei8zqG=+1I@%)sp|xp(p8_WB9e$WhMJR z;je=?v4_|oo~9o}Gg6UiUr2gyZ%3;lw_;xvFeHj!q8c}|&!$DmP%E}fvX(D25wA4-Li~H^B#Zyf z-y-*X0lp$D9;BmB+Dm)cm>>U+^!sE@lp z=YFy(ZGRw7l&b7b`J*emUB377!$OhglbP?Sl%=zFBYc;6b~AE4)5CRqZ(DYYDWeOb zP^+zZEudl<8Q)rb;a;u$?-M6EX6H%dtYEmKbywF=d=a4+;o5-Gqe!2 z4-yhOIx+Ygf`vBO-K>aX9;H>*C*9#!%ff#vnYO27+M!5!zdKSdNn1X-cICPDNA`HU zId_)Y_7CpY`WXXc>kNpKt6ga~Y^t5bp4uE*CU>9?v)?ubpNyc}JXkK~S->*(qfBtB zO>}GDAvBWN5^*{EoZ(?f`L{nZ2L`_cVS zC1=)qdgU;%-xL0`J3J7}c!#{MfpoYO98*Y**jw9)FINH!Rz=jdX34`$aJMweNzyE# zY1T3{2RXtkr)gW8v~A}-eLj8bK&bW{NdQdpaO32>foZDSe6~tub;n>7B0m z7Z}4cWxA9%;f(l?kVutj9rsI1kt9>NM7=78%(aO6n!UA|_p`oYZ@YUq^NP1*Ot-iG zO;QLM+a{7E_RH5HMD{K!xe>G>c*$ku7JCgT0w=WOv%;ZX2Q}4vf2O_l6NqTPhVAL2 zu{m7!C-$P_umv2KN6tLOxdxXxJ)tM*NbC)& zDK91ytHO$?u=qj()SD9hMw*4pvdYUDgrN&y(KtdRohdHz?o2D;nJ__Ksf5BwUJlA2 zO$z`u-!=k)@Ssq4GkA6!eyJqfHF~$M@vZ|CTs&;!J3p(Ph0od7zN>Jsq502@yWo;T zHa1VfK3z2;t3^bK;h}R)zr>$NVGJ3KNm`Z*NB8p2Ig0NF%jcS1qfv|R;^A8R?h zfn*uZR+h5&gbgSQ@-9*phFPyR%+Qro(zcD*sF32kmXLr^9T93)y`md$HGsscuQ+ zkuPz)S2FG8c(+CKgLo2>MM}Z@bL-^~Q(w{p$jh~axnY|_>jVdh&>uY*l8%iVTN#IP;8P3$^*$#d}!NTN?22b$K_N-P|K{AVrlkNy?dKR!O-{ z(IX~P;zyZpawV0|S>H-1pCf;Uf?;IHel)c|Yud}9qhGuwR}E+NIL#Kt@{5$8Uby?_ zk1+L1&ZG)+S&YbJFp#>xSkKR~%ZIslrX;_3i%+3pZ>;K{Ej-S`zSVyP(J|Q$#{eyy zYk%bA(nwL}T6ZQ(f*jmBP50+x$b~&6-L|!(T6eL}m=812Lb$3osGI0SBndYZcF(cBn3nZj?uh#saCeP;Nmq^ig`&*(DrisGV?*|f&6pN#Z{$N^PkX|93%{LX zzx^OJ#~9Qdxi;4wsm-0Xr_}p`r)z)46_Ht|dTa;UN0zi_MBK~`L=VdyV3VEUCz&g3 zyq+Bn%y04WD2!kBUMA)*o$WSfd$uRkmB}h+CaYjOIa=Z2rdV#KVlwwVhb*b*Gk4#` z?xACD?2*vxw$j39n|eyx$0$?c=TKD2hoZR6ZT&g2RMLDULo+t4Dt@)wTwKNz+D^T6 zWtQfH6{XM}c7p1FBedVAh@RjY0>2`5@ zHiPFZ;Y28w=`MV-@jXfAnXx2h#5k*-GJ1YoaQh?fGCX;M1yn=CLgNEWvV-27Sd0!pq$}7l_v5flj$KZ-#!D z6pn@ZpLK?90!Ol_Y;S#())F2F{Uwv$hKUUak23zN1iISKQCpj)?Q({WIc^z)e0JHK zg*#RsidCI~kr#304KlOL8FyG#p6h1i*c-hfl69LdudAJIKHL7d>yg~h!3p`@PWF1C*ao2+Dl<(0=}|5AD~C{pnJ}7>glu z8Iwz%uGbxc5rA=TzM!duhEjAY?3p3g^CVlB713_%Z1W z&_lxW>|A&+27pE&`~$(46asmOJwt3cvByO4@wn9f;negnNYcOPvk0)3Vq65w$>FUL ze-+b4@|UfDAH=2-NYfA@5OJ|>ce14cVTr$>qDeY^e~ZPUlD3yuot(}inI{ZCPj~1F z_qp;Hh<%AJ+|_P|x!TP*91Wt!xu}|uy$%q?UDsO0~(i2Eah&$NXZWImUo_yLw zuS~=a| z(0=_>G-dk7_xI5H`%y2lasyPM z7Fcq6e~h?{n?cVJwHo87^!{|R&K zkGKiqlXS&{Ek_FjZ=+w=j-p@-klZ2%OwDHx{Tn4C z;xZV-BhD=Y%OE2{9btsN53pGjzB#Ug3-VXWgyO1xC9Hq4m$y3L~&er)|5G z$j{;qhZdlyQt1!jwzNTMi!H#|G?bCBQXYhb@{((O$$t@j#JOmmTh4M7zQ|+?Z8{$` zsAw}+Xa07SHLIlDcO{2ot`Gv7Wlu-zSVUU5*cQbDHOx(Ya9qxR8h@gkdqhtrNL5r( z0DmmZ?Vd8U^b3&^OdlvZux-n5q>ObYE+%^uuk*19ye!Fo1!5|lp?5N!p*JSDZ2hkA zCdDgPct7LP=}HsJsMaqBEh;P+sFbS(r zOs9g_71$C&N&y(wi4jFY5iu=YJtL>dq%TrkeyQAa!s^zPx9c5iXD?zD67sLp~ROk#qFO_+bWPHO&5+ zmIC@{YVVCB?5%M|HnI#7@0XEL2eB)av`u>#>KDa-ulqabQ(e7^dZCQiCbU4VH5V>k zEG;Q8{d(X$)$eS~OzU?7<3UmYI}J5-=Rq}Xax7O;0)OW9q93H8Gkb&v#+Aog$V@dy zi70GBW5VFq4(}tk^7vD9oHE}IQ?-V|tF*E7m(=(omu)}QJppEllq@pqd=rgDY6YQJ zLh+<0{H_whkI@jN%(wehAIRra$V$>7IM;De{Jmsu+8@uwI#8`@;!qmM+D6*C>!j+u zg5VhEp^;2tR}oMu7ovvWBL9<+ss-GI|1S8QYp=@_{pBvyt-dvz)45RD;+*v~<2 zJwP}@TUvh17Yas>Rsv&_0SXsrX7t8a|tZ6lGbGpk@d<&qc^(L6bU`4I2 z2;q82a{uZUnvPqYHT@jdAp<;hh4hEomLy7(I z>NUB}&_@&E=V6=6{&6Op+s5I?Sj-fWB_DRVMgC5+521`~vbJk3nZyqy^^Wc*UO}lz zrI66iflkgp+-{zKc;1=61N9vqEj=)3h~Q4K7TJ<0+U+5dTY4z|SElE&3SLsHY(_~1X zi?E-+J90(A+LI{fZYWWMNxF+t;nCo)feYlY+D}A)2T`g@1%zKq_5<7FatMd=FNz06 zqEc-9WkR26<9CaVf55TkfNA6JceqDO^$agFqty4p=`Q{pMSqe>?B`K^c? znJqv9jClBCmD34rpuJgQL}-&tTVXWf_}?&ewTAktDXf0xix}4-UirI`UF^F(BAwDV zVaiF9ciQ&ac8ki2pF(Q0i9CXq8q!4WA!uk5ky|;Wez0{rZ#^9<ItT4yc+POrli1sCl3D0m za+K1ME?+d(`gf@gbXffBytKkOn<`tkxk zU|JS_p3TeB-Om`fQsFCEyHdVoAUoLM1DPI+odE;reWgGW#L57Zj#2qc%OJ`{RyD)y zDOV*rS+?Uv`~~VeXD^nqDZm!Hf>JXva&K99KS~9J(j^uMjWgJ}PUX@iegpEXnxYGI z(t79u=?~+_kQuSd-D+;~Y^*VMKWu;u`oncz9%)3w8y1@SNuUU;437x!72)re(eCxU z&@JIaquuyKR-xL1^c^MDms!a^Q(MfQnB*Ceo@V-4eK#$Wm6T=&rSVOzwS39v@DzNhnVg-n1fAwaUvc zm|epH+qP+B7GDVQ7+BY&Nn9z-r^pmLxkEHn?6zsL3zV?6MN1LMBKx{e5f;-x$$~6l zqXX<>mzW|XcLqX_&?@2xA!4b-ucjY_#Zgh*E6+ehcH6)AO_9#Fh5T>XkIs_sfSAK; zX&p$o7zYdD-=`1P7J`d;UI}!^?`8E?6!5gDvWelOXkEtY_Mb%UC0)9=2FC)?uwnqe zMhc~1o{C>480qYUct|v+y{k-lqAiMlELDt3!Ll}0tkd>Xazq*b1+SG1ys*19& zFGS#{MMJmb8dz7BX_ZHy+(jTieV>o_nw;<-J>lJ5pAIh^Sp5a(&e>Sl$luP{G%#z* zlL<-&5LLNuFQRtlU7pBA4}|tl2vob;PkzEdELm)tlbQb~=iYc3*CT{l2;&@~(qm4x zKygn~k6H%V+s_vZNB4&394l#eA2Xvj{@I;Bz@;R29dm^_x3(*AzRa@^n6@D)@JGiUh(a>i)rd1J1X z;f@w3i#=H#To%Am&bR??>|^yVni=YL5YTk0z8UHrc^L6m`(uW!n>#>E{^|8E?!W&j z4B+$AOlNMT+?ZaoWP|?s+@+KK+vI$WYDI&4+lSvnd?EziQ%TxLl3d=RA5r%BZNtAq zEDU51*W}%bmkX7=>{Vd%9TzD@uAC(Q$6~D+v4?XJlHpen$5eP9xDUS=dVP}HCZFP# zE0vh=KL}wUDpWaV1p3;CpRK&8gsb3AypeyrKZ8Iy?brfd^Zo`k6MEb5gG}c_Z9u*+ z$r_-lSBm$2g?8+I-tukP$XFZctxIa$gZ(Mn=isY(zee~emYTZ|<h~!TiDAb=~-TPLAL&_Vd z#kMa)HS?##!n>F|9Tsb1ce_kIoF2O5LcteaG%D=R3j7COpb@nxTsQCTXq6|!`m}3g z_=9&C#!$5F!|NuQ*cq6X=5tNQCtS0)+r8!!S;1Pljtg{px!Cb?A`Prp{L%Mc$hrMM zVQAH;(YK6M+>38#mIQvlhi$ZPu8m+%^YBam9S&b|8~?dgFRRZTJ`;CRfF)6r;YG3@ zu!`3zfGQ1g%r%9<$@5U{t2kctl*fCJD@sz=Bimi=x$5S0PQ-{;K9`Mo%tB=0v#a;J z**L(7nel};W!*B7@6f2j?0*giLb>6az!xK1^YjgaM3Y}~V}siklii-?C%OXrq@Px& z*CO(1RNYa31hWxa*%}g;xI=I!&a(=?3Y2(kM42&~_{vX6Nd56q^z6_jbv!XBAAc4w zAoHtt&85y3Q4TI`3vsumR;YWpuC5iu+o&fa_6;G8 z9G(DO3FtzXj4*R49~JzmC3u+x4UQl2h+r1MYqj!;@Cl;u_=V&(@C5alw!X=?8A`oX zId1Q_SYrNzhXwM&cWyp8%nt5nJ>GZNb5iQNek&_A=NXBlg`bv2CactH<-XKDJ0@$DX8n3tS|)MR6(#NC zWEGn^slWiamxe#2duDzv!_{6WtD&;~%<^`3sqILqEy`WBtU=3uD|g|6#+NP+XI`K& zH9pez^6HZZPS^Yk4cG!Fwa+=m#&2#!3>*wEM$RafDQDI#jdGa9q(|TLGD!}iKf|%+ zepZb{-*){CvG_PJ9}W59I;l_0?LJlBj|WbnUY`4#+)8(`NcSwhAAD%LE%1(-#)!u} zN_NeUmm@2%i21FLW3#D+=l&LCx+X>JJCy#8jgY~MiS6vrC4c%6LK*x8vNmFaNJ;-g z2*XdTc@(Bfj>hC56q^{Nxlzg~`cqW{+qtioy$0jIdxcC(MBN+Ob8d|ls_Iwtw6RD# zIPKkdGr+`W=KozRPD=WIM3po!A#_QF&|q&Ht5o>dTX-5AzX&?i*8t2!Y){l;#btD= zfB80jg*$x7{emWZd5(K0;|ECpHJ7?|5GFq|{~P>C@ZWh=$_Wk!8z;gqw0qc1^+zY8 zh@7s!4|mc$vTT2i++Ztw@|H0jYJI)%$<>Er8DuZ3;nG8I0<$~PoXc739Cz1SRNc{O zdpMoT%`dS%jQ%{Vp>R1?ATPHxlJ#9KJDy-~y_Gc1-#Ylur5bE9&Oab_K>G+6Uv*Y1 z@;#Y*nTvmpi%Hl?(SFsjy$4DQpJ{xR`=mTe0@anE6NH5cWvBzIdbVNx%so>^-|rL$=& z#4SfI=0zr-)Sh{=JD-bA;dka3(XeO$F1==21Io@|?}^+6hJjLihGMp-Fj{2)L60YY zUs+_?+n%m>Gq~atE^W>fj@eyZp2(TDvPfp8x@+r278UHF|KOD=_wBO70^_kO)7;_0 znZjdcW@?km(Z*NZTn^&Se~r(-9rHvoXSBT=ly=`n={B`alEeq|(ca=b~} zZel->*o$5#jQmC}<-OI_{>@|VNYgQ={qd*5&u(*oI&^q~lQpsYBd%~pXmHFe-*H9e zee5axuxUYi#St-#D_r?36zvPYKr~l)mk52=-uLj0KDc*3R^gGcvXMhJQ+Nt@H*pT= z%8!VFmCa69rkP}#FN#5EmV4Ts(BMhIYrvOCl^Ul*2B!vZ(N6hC_r?;DgrJeG_TnSX zy|Ku=@viVod-uCWcc-RRazaulo3>|fekCCX)gjpy-J`uV!yd(KMAZ zw!=PI7iZ5%S3F1U-?ej7Os{iIufb&#?BGUwmDo#qo#EXa6sU*dzq;&?ah|hlZ;Z9W zz<9-8SNJJ5VD9}3!R+kb8xPNB`uC_!>BG=I_B>p1Cc_B2x!{#MrQgfx z>L2b3j|_b5pZw zT=t5cDI4XDmDjAzRxBgpqGbOr+aya}l8tQ8TxM=Ul0}~x)Ale3E`@o?EB(bda}16* zHmmX0D2C^yF#A7Zr$fw6<9woSrSWm7j2yJRVUE`dFvmr+lEnq=u;M1I*i*wPz|PoX z0Q;jft)p10&TusLbwRKJ6a~^|I@0>T0yg{c^=vSeGEuokF!aQ31(dZ<&SMiBYo>0w zMDF^8&sjBly!$$74%P)O!c+=_m_N z&KtNVDSw`wnR&uv51Y~k+G{!#42h(fKN-iI!qiF>x!dW?mZSzvTnRigwvu?@w+0)k z|7@r8dcmE~Hq(#YU7xZq=1q=#9Ed$2C~4d!?l2z+&naLm;^rjkz^`ueh97fE zRAQ+PPi|izvq*~}eI+T>b@wKjM`UugcTinzm)PI>toiO~-eLI(2i)z8`TXbQiU;bY zecZ^{WAYv&Gf!X3&t;hG&xFADPQI%1wi$wG6xLO*N+bw6w_R>o2 zttdB6zS2RD%N4r^60Y2Ia=5351XN%}+t%!r!sI8|Ork(9T!3-T^8vz7Kf%gR_*9~=WMH62!f zrUcq7l>oh(uC@28Ns>rxf=rYq+x@`zA;r1YwiR(7lo6W?4ld9mh$4tV1GMdulNFbW zCb*P?d!h=ETWQWRb12KuzXVdFu5fkUB=+};%HjJ-*WFI2EQ7vSYA<;vRzem^OB9(6 z6rQwFelVok%S83T%QG?@S1*xMg!kdl8#MTN;TPC zFR?PjxUueP3<+@blO-BpNa@ zJ}x~LqFvY*7^jxeWD1ZuE}4t#P(3&!)xafE?W+6yU=t}L{;6Eb9tro){9#8B9+GKl9(DMU{py;{gU!ms>Dqho5W#%uUpy;{Ae9riX z^yH<|V|7+QzDGoIfqOLU*6=|MH)?o?hPP_isNoU~=W94y!|58HtKlgcW@z}~J!Zc9 zHQb|Nw}uaDxKYD9G`v;AMh%x}IA6or8cx^nTn$grFhj!+H|qQ~+@oQ)f|h===2N&P z7iEOy%^nc34o^4tcu>V^!Y}TkT}r!;72{!>uYBB-HN(it`dQ)D^ZzP{cK(My*L0I^jKH*C z=j0u#Jkj$_Xua&qH=iAoOxTxa!pa;IW}mI0ezwvLg`MY_@rH&EeI7UUrPT%BZ7A-X z_VL&5eXr~4z(whNZr9I0)bM8--mBq*8gAF{j~d1_{7}PV8jk#GI{pv8^R?eR@YJ*8 zez@CcS?s?1jt_=BZyULB;$QzUu<{zwYbfA*>g=8osphzr4Fy0rQa{q@h>r9%S;oNf8B(Y`Z;=; z`Rs6+ut>+Z@{g+A4daoooAk6)>v$GiWnZzxQ2n(06TOu&q-KhE-u}ekV3a#_EZuT)N}SGh>Fwv>i1g>| zk?GF|N1M+M4GjxwoE;~p$4|-XbML>vf#QZ>UA3dWA>gR0t84I91$>SLr4^3a`f6YE zRMLCpef^gM4sWARUV0mr`|Esx+J<_Ezo9WO(_thr)HPI9J66`#H#K-~khJpq2k8UL zKv@k2qj*_WeZ8;FDHJqWPjec5z9wUKu%^b>sNa|m<_lL=lUseGr0y*;8YE*@S)FExCQ^&dn?{& z!EcdY;nV^}`oj9EC3QYWputhy;Ap7#O$pR4_fg%of!eCN+8e7>&DJzDI%+B4R73vH zoVg^hSYqfWDOZy*eQLqf=_*A1^CtLUaVQBbyGLLPXG~_`SkD>k^~{-OH9F@Qwe>#e;NREGGzs}?@cZf=X0(|~G#&N9f9`r9Nn%>sv})uJAdQs0QR{ z@>h9Pye6cxwx-rsJ=JJB0o_xUS0TUk)wT6YrFAwrQUX>s)&_h{Q<@qYy(>WLz}Hk? zjSqU3WkYj{YZ=GpES!XG(C!FO+==VVnBw;d%a;2L5PNSheTsd?6idA7ml`PqDGE~q zObSy&s=QvGA8}36nxNm9a)Zz3r&d;wV|~@qI-f9h$nRg3lYdelDMVWonHl}}%E>>K zN3)q5-YQYrNT$Q$>P$=aYMYe4*DUo_iwGMXr_pDR9~>Nodp0NOR;E0Em+&3Ldjq&B z#g!7rdJcH~Nu8h8CMPyYi4%2?(lxuAtIZChxUSY$A4tCUH#7wtQo(4cM7vJuY^?ss z)YDdXlT6>}Yig)N{w0?>M^$w-0ZqQ9nGWbUo`?lre`#$~z*p~UOvyvhpzDhqruZ6a z=ocCdv-PS9gwN7>(v%!is+v~SdmXxIiwT7eWCsA|QKp>#ocRh=~dRf+hD`Z=nD6*LJ2vaG6UnIoXg8c1(kMc-)3 zr!YvG7iF6YnU>+BHHXPQF>su!k0=J=k}B^FO~%BgiGE{hiXKSUH4dXHNdDf&x*B6j zfDX5*PMJ+93c}QNQYNtz=W8CO=TltgtEvxTGFDeDuUd-rVJaGm>uqSPHVb5+9IG2v zHknq0!`o0zwboS|3%qs97B39eRvWq=E%qG;rzW^D8$$tG2n$Er49d`9T;`tdbb016 zR5Oxqo#h@!O|afHc-6pQ1w0q#O?4P%0^+CCx>sp#F~1%Dsz&;6k#EZ`F=o%3S6bqn z>qyBqVVZnZjoxM2(h`!cu5gyl zn|I~Ha-CkKD^84SbT^Yk)L2zr+klcX+_Os;mMoYzZ-INU;=|%Op1Ce#c3>If1b@?1 zufHzImN`_!oZ7~wz^8+%$T0~njuIOa+gnvn2avD3icC) zn_}$mQ_0(h7%h0I8WHe(hOJYh?+i}%4dBH*hOT6$j zS%FnrVSee+&PlZcS`SsY|9)L&4({VJI}*5cE5+C#vgwU?>s8-BII1FN(HB%VCd5<)=%I zDafYiq|rAtAF~{m_$IPZrAjT=&~z%#BBgi<`8S+D+P&u^LW$ozsr7I643QMar)Hh%`^Yu|Kl~AV@|2_@(M#YP}N}T zy}r3JWReyh@HtJ*ZfH2CYI!YF8&;@JS6s)C*CSmAmQQ(OL!iOiP)EaBE=H*_yRKnL znjC+2J7f52_O1+NX%vbMg&bsRR)nzybopc$j@KCsEF*_nnPjRqN~@Xz6|h)&7B&&0 ze#v$!W0z#pHd{e@hGu8v(2C z;Fo#b;NZjGWNd`H6L+)tuVoActOzcVDFirYlwk(cFu@dTWblzRQxbDBsTz*XIXNja zbEeY%Zl$rb^(zf@NMB{m;g~OyomF~xvU5{NQ_!XLStm&3GCM6Pv5}H2L52B#DAple zTM{qXnJH8kmJA=-2@QQ?bu}p@4C$VdDbjnMbh`gRhnPf=)}bbY&B0Cz1*NJ}(G71l zc3}Nyy)3D({;XG0WsF?Xn$Hw^N2_5?37V6ZiD?oV)0Eo!ZwHy+#6mSi+?cXV39+iB z%@O2FjXlP-u22`ekPN!1h6wNX5U#{Ui4XyODMv&^P@XvtO zl0xD&Q-xv;GbM=C=|@*#dj=Wjqqk8{GFy#pE;F7@f#8xR$8w)c>@wW35IBDIPu6+* zO!`M@_*lezE{K}Y!t+_M4BKJll$Bq*$k7-?_c7NhQ-n12nVg#Br**T8+Ze|%G2^I} z(Ey#RGQbUonsKU0^*>Ixq?QSh+8brMF%e&= zCHVMEpkBtTDYKW88T&M%{l)5<=%4IZRqLyh`AFvCt=l;NiGua9vVc*bD{JZfF*Y6uivtFnBRZ4mb&wnrM)j0g$ zH}ky@xDPj4=f7&587_FYr1*#C^Ic6}YWP82w$!kp{+$}O?G&H|!id2N6^(I`q-GtvdqF_twRHwY}JH@MD>z{t4 zpzjZV5a?LknK2s}Tj9P~;Kn<@upC(Q=3NCFfgNY>cMPK^(hOAw$M?W`x7R{we4_@Ipx89J5tt>1E^ z$yi}{jYcEDlu5N}frhc7$TKfBrK>Bl0Ub^0` z9I`%b80qn|uS$(tnn>q`$ZJS6K8n523GR$n)jJ#WNj~SkIb1*)cmL>i@yLk8#f7mIdCyrd`ZY%yi;KyegMlXKjEXE^e8%95VfvyRL@s7j?ZpIzM zzXkXlF7w;u1w7AT7^CnP0N=*t;6DgF_Z;{o;lM6j0scPV2e?`IjdMwhE5cs@Y{0qj z`z7o=&Whj{_(R-v_&b4L&4UjT4%~$c;O_%|ccNk3jK2f;o1PjgYf z_#MD!<`~9D`1^ow;WC@3J79~OI>j&WC%7E^of3A1VVsFS8+fD?*~V{_K|8Kg;sbw* zTO{#;qvsk%rNjqbgY)CB1im%TFq-in1in0O%%JdC>^zfl3r3n>@=DDYj}4v7!^ z*&^x-e<$$QxK|_&aQ*d^3x5Z&!3%x({lFevc7QShGpnH=e>QLyE)RbZa5b&~e+%$F z+${WCfM4_>llUFLzv9aA8#RV8b1CJ*Uj)1x7m)bCiQy<9>sOyH)>h` z!`+YH4|INu`odohtibi+uLQn=dm4W~@JBcBjK3532JRjF2Z6t?qm1~Yz(3=%n33q0 zFkFtzPAtcdJ5&7ihH(;ZGX88}F0KH-19%JWBK$4DlNugz(Y~Z=LKKup1+i=h0?*RT0*N?v+cu5dm;V%M?SV3FHpAB4& z%jP>~e&9>EGx7HW>sCUOgaZqj;R$|$A)E_;2XOl;c!IwV_=Owc34RCgI-DPWCGd=! zXb%z&{O!%OW&Ba#``>{l_zwbCu7)T0TY$ke$bk5PKfIN4;qL@q+Dg6SF9QCtjk4hH z2mau@v{{J*Y+q}D4BCJ%;BxWz1FOSKy5aW&JHJP{@C%&ZPPy<`0#CUeJ&WG~{5`G| ze-zmJ1KKbCK49&glncKf7~ep-@E-*J+fS$`{GGt1|4tqf2l!{)7X1Cd^>)6vST+d>nTKe-!wIdni{kv;kZ1MepFRypQtYa`1NmpTnJr zKl^^e823xsvcv~oi<=?wfxB@<`1^qG<4W-#1Wx!BZCT<2@5U{{zXkXZt{Q(7nEwEM z4gLb)9h;GP{GGs0x6sD%8^4A(xDEIPzJ|LWe?PG5L1bIPflD4jw#5&;3)hRk6L=8! zG=71Rhv5nS4q*0gsCWDVFUR%cF9&YNy@9_ED7RF_Bo6S$xOeb(0&5-xFMdC;3HK5H zD6j{21b-j!1>7Tzk|>C9l+|xkRSYh;G?)A z{88Zi$KfshN?^E~HiEwcSlC0m!e0bjx0AY*aA3_-@D0Boc;MX$Y0sbQ3yx}(E zBmCvSt45ISCgK2Z#bx2|0B*%)e{F93dZtj)L` ze-Us4t`mRD$u^@6w*`L(aJ=1SJdED~{5dX)zc1TnTs+QZJdM8`xDfXW{{NXj$<3BE z(#V=Us$gX02>uNxT!wo04wk2C!vh8ZJfODOTijx@&F97ZxH!jOZ^*}&l$ z#_;L30$XZ)37b#Y(EPaJKl|3Kf|Dx8_(zwIijL^Z?9Vs|4xzD8(>SzD7JeyHY92%5 zrM^q`TfDI7{YU9id1aMB!sct*tgsW+#kkSNICwOsV07gu|H$$Y(agS#erO(=pYUMG z33%{X;a2%m>5#Y^Pk=+pxP&|vE$1Iki+z+~x1D38N|Ej4QO3!8(y3PRlbY{?y8Koh z?ALN3W!saK3MC6t?^fDW*hu)PV~kU6>y4q5oAQ;LE?8E%ZRMO^aeWl`US(3LFU7r=M^8M!VI92?L-;%~^FR6K2v|4Q- zHJ%mrk6VDrH)tQzpwae#}I|Ev=snw~zd) zeON-%M3wzCRF@6Sb7P7uT7HRR$(GdFP`#^UWBl>5Vev3E58;`V-zr6_P4!u_Jz{uz zKPxy=aa#GL^4tm=T3!j4b|*Ag980BPXng4xrw{2DCI1uY)?vev{YWZ(sd8e~rA41S zZ`9?nbcf_Q{{(psr7blNDZA(hKe&eWPlB&;2tL7;suzXERQ{#SPo_#PG_6lL0zhP{f5?0Q~h588ltl#~QnD4qZ?9=dohM#D7 z%A;ny^EI5MVX20d8a8UUPQwQ@?A7oU4G(IVxz)sbriPO>oT1@d4XZW0Rl~b9d|1QB zG~B1*0Symnn7Pf&=M)X|G`vW|D>Pi9p~bIe{k%@YO&UI^q4~KI=J@pU3mU$v;X4{0 z(lGP4nhp)m)$n``r)xM%Lzjl78eXMgwTAT?HfwmRhPP|@ZyIjY@Bs}U(Xd;?r!}<7 zy-zojcE@D2^{*D$K#ZVjK;aKDBJG(4hV#$%d) z8agzbs^KgROEtV!!(|%!HM~*7TQ%&^@GcFla(&)DOn!gfK1@DXa`AclFgal}oL+UH zrDFcorQBms((J7ZHp$f;KQvNLLG3q+IW^8{)&>r=>6?nwK{sQ#&3v;!T|2~0FgBNu zn~Z$L+-ky^w7Lp?;^InMiJT=F8f|38 z>R_GEJd;J77bJ``7+&iTp7D`{aGE3Nt4@+w!JRt6CS(0DF{IQ@JU-6QRpW1{zpARS zwn~oI-jVut2-QDMd8b)qY#zn|sM*C8r5vN5%~6q(X2bXo3G*!pF6Hu=a!!HEk-vk( zh_T{0Mi?IrE9FL~(%L1BRgJ5h?CHpa^Hp_ny^@iaSr)7d)C$@K4OiDz`-=JY1i+ z(I;nE&$dF;865a9;_9l}0Mbywsgyb&7iu6n#z}KVlf`$aoj3Q(4;>C5| z#d4(Ik2o!sBlkwJgf3sQ*c)tI%tbtAps|3^+J+_i>1Jbb)$*pLi+#;vazAS<=8Bwp zR;<5aQL=7i3li8d$}@FcTS2T%4a-LtT4~t21jvB#i3J8g&5=D}0$msH)>8gZdS$ zon|iADelVT(3;o3$}s+`__#HNRme;ltU~y7Cih!-moqYYPJEQBftB;U;;-YNsPTgM z>LtR_%oQ~j^`{PB>1|T)&l6vuVfhlMo|rM;@2yZ0DDsLFUYH>lCYU;atEy9;DtzA# z%E8pCI`+Y+@HveQ%gw}2<6;%Epuu`}7+g4@FK+;kJRwO*H85R=%;VHlU6tSOtDf&` zqOGw0C~rIs-T?U&iU0W1foKfpac|^kVDWLGaw_m@;|d9%@AIxO=83O@tDB5#m5)PI z0ok#z$ymZI0_>zRymA+TKB9Opm*A)~y2gEm)SjFmtCdR$jQiz2tHk32@+4O=sh6AO z$+}R;*dkAI@NdZ~by?Gc>e;%w=^^WhQ?rKg8)I3Zp}xuZol&Rm*O21+be<3Xgy8c(VaQm%=fc+|3AZBT`_%ek~3!C{rUg@138eLevZm2fpuQ?;4e@7-ir4* z{G0=ybKw5~4rH^h1~Ql3%|D0DxJy9ijLxFY^3Fw_m7U8v{hiI78#?|HhV$Yd3am+_2HOXVg8}_vGHwdC&d#Y`G_TPwzc__w2i;|DM=A2k$v@ zkFjagrtD3QP5wsdiZn}TdmQB%35LLjw9--|QlYUX~3%>kO#2zA^jeAGkn|*KY zy^eb)-CJ;P<-N=9_1|mU=eTdueFgW;xX=GU%L5$`bUv`8$d}?JR%`lEMGozo|7`SV|FxG!5KkEu>fCj7yF4Cqm?0MKaafgczr!hOiyZ zC`b!AhY){S$c2Oyotm0z4k2HKDDs-F^aFfHUkLZa8D#@Q`+(n8)KL>hFmTt_A3`qYqY)K+#{$a#b~(n2mFq$Mrn zI!U!S6;C4}{11rSJ{f0#D zE1T39nVPVERva?d6fUi*UCeBkngeqtT6gwHYWYVRGdgYP&%(n5*a`4loxpQ8csl+C zJQQ8ysbOw2JvS?_mH$Eo}2!KII9zJ=G)%m%J zPRWK92XWST%nO^iZHx)sB(7ED-uY9D9xD|BrBhJ~KAcdUOu!q?4v@-!x0Omlwh+>v z7SbniHd=8k%s(cs?uqJ}mdm79f`tid5N7B&g82>bB!3p3`v@}e_*we!rlL`ik4WzK z=-f}xz9iB~qbR!-N2U4#AqUe!E+nL5lT`|dR7yzp&r?IH2`NttSxHD=TF7mZ>fY2; zn+WMlgjnTzfE>(nG2^$m#y_nf$sitOohRU@@Jef)pjXrm%1_ex#H?B%z zmU8t#PxK#PwkUi!70-|s=c=nKlg88cFW}jlz++0UwQwP{$nSaR>Hin-{EoOL$4oqa zbA2hHA>RUzvBToLRf_IJs%q}cPH7N?%kL1W`kwi0K1-b8j6brUwJ=M_ctSeTLL7ud z|660{ADib@-SMZ{#L6}x9kisZTfIT10E2~uAK6w3JB>*UP3$I4noVqvo!77Jm)L&a z`F)c(-6YIlQ`A2?vq@RTPb2ZeXiRdqY6>uhhs4OJm^_J=5Yfr}u`N}hO$&7CP+^~Q z&;51YA8y|&m7ni(?z!ild+)jDKKI^pBNB4|S7=q{sfkv7+*CwaI;`mbkKVZlJ`W{6 zT}$x>a$mpNn~%u+J3{kVC`I7>?Z~++<6=Ll`X3LegwH6*&hO>O7|60g=0L{2pVKUW z^gWRy?+00YGDrRrq(oBR|TKOOooDfP}I?8q3bO zJx5>3>c=C+Qb^UM4+cKpjeJBYA@jW8ooJ{F7$h1 zc3o0E+gl#q7B}%xqA$=0P`n=GT0}x!ka@5YCy@C}-s5K#Db{`*Bosaa@R4(J>K{Vm zRwnzO3rOJnf01)<&k8m^_ue!=jWj9dTGB;Pyh}<7k;)TtzkzyE9rot@GcSOK!*rfc zu)~vi=EX5`UnlQg<4%z03U*WFsibd2vXsd$c)=q-38fIU3g-h9L(8SB-Q*SZ9M*&z zCRKw}tC1$GMemF>VV!zMM8b;lLPR9@KZegrt?M@0C18a~UBb67Rs6KUY%*31t?L{fc$R2O3^U8m>JqoMgKG;&^3bttLU zK`y0Kel>rap}7Lh)>y8v`(Q1m(p>^{+=w8d>jlRRpQqu|pK8$WOxTF2!V3R%Or^UF z$UP!uxlk$ZrCi7^-F}M{qIeF@=U$|!A}{>~juK#SF*BKBtVmp~MulIwz`6W5>agZ^S*8?CMM#3+H$WM4EpMM8g za2gB9qagB?LCUW*&wyMu$d+FenRRFPmbmJF{d}&CmqT;a%+o_U#imw9MYF@eh7p_8S+0MwxrL3ti;@%HYTVGQmHmdCSWD@Q|c<|@aC6dk!}k* zge%Z2npDq#Y?!*(LPab^K9bi>AnQhE zS(mu{T;99y06A`C-VZWn$~6kI)6h(V$S>9MX^=isJ4+z4!jfv|BOtaneHrAU$?IVd z+nc@*vSxHxuugK(w6Rx#>@a+`gIqTHc@xMsBhOxtJ+W-=U&cuiZ)W;xgDhMmARf@3$n+wXIU{DF`N&Ah>c2o z{t#rvw4e8YG)zg)f*de>{v0H=dEISCeVN=p3C%yjC-s!Cfb<#tJPa~|Bk{EZUinn~3V zKKzo*_klFF1-rr6R1*)a51Q1T&qK2o`$fZjtfc1}nomKq)5!cK5HTHz&o_j|w5Z2H zR%7dhxOmacs`+HcZUVV%Xx;>}VruySi2PDszYo%9+C~**!)W6S$nq_DJ3I%nZu0sl zh}G)nKrR}Ze*q~>xxNdsVzhAs`D`;S;WiNYB`@W-KsF4Y{UEERmQR3OFs1ktkag25 zmq4U0w3fpT&eikzDDwa`t45wnAU7I~Jp!^P@)22o2+}a}+(17rOztlO={GdDfgCq{ zc1bFlcPjfJ$fA+w7|2zl=hGl}kTA90`#~<6R38F4ZgO7%88b9r0qIi#TpwevGRU&Y z>oJgZLvsTSe$jB=3PQ26Qfvpg9{K2wSz5yRT=RE8b1u?2Z;&3>51-U}-zj{I<)1LA zATItCWIC2B?8Cia^7mCPLGcwPBEJi+(`+Gsw4e}Qt z4;o9m0&>;xc@X5HN%d8bJ*HfbfGioBr$Gw(m}p6+f*(}ii8IF4G^p6142W}B+t7+M3_`x3n1G}Ugtn|8k+MU8z!%R z0C`O;7dFbBcIh`7`#LoJhR=6E8b+SSL9C_yG02EX^^)76F`QomvO(^tpZ+e$CDY^f zgRI4#Qp!gXKf56zDUb8s&CiDp4UHClWs>pmTTaS-r!4*pIKeG< zdReZAWj|smC;OUu=dy-M*xQmbeiOz2?xs9DMY!Yp=tN z>Kq#i{dMfwz3A?gt=5gNOHV4d+i`QD+%T?(Uq#<~hE*>r_IDJsSI5-^H9B41Ge6!4 zcNaV0=-io2q0#XvE=SC02IkHVPaYfacOiGY2{$7js?+?!9oDKVm!Ho(yfcy>oeMqL zz{jfd=+;1V;~vE3jgz%KVRv?lW(i<4#J-DaW2otFe2(f%PpX&nOplibQfjB$S0Cn{ zQ1>^r4FmKrSC>PMw1nNe!V9wD4n5j-ZNVwZ5=he;;5Z|za?()bkpsl$S#L#5uj>t1 zaKJdy*Bg9-a*s)CwnvA9m6GInXolzXPDvs4N4qg0YV%wk2Qgn1HisyFFoV^}aZY7w zQw}GhgIO&{xFoRRn1lw>TTHZ#8Y`TCjtW0G-T;!DyS2rMh~?p`+%+!;MSHz7I5*sO zo)0C5J7*`j-574^O6P?3zP!@Y9PvkD>$BJ2+R=f{KExt_x<}fxv*)rA zugM@dQ7@;sJ$8JiWP8fIMJfR~JLeWBoB=NRlb$BjMcJs>90v-h?OJ6?oC;8 ze`4wO>9DJ;O-cqcBq+;pgW+TI`^GZ-*R z4u97sByfOSzqps<=tU`4l#UFBxB&KX?^$= zWv`Yqa*;l+(MQDZdg6Khbm>;8%YC~Bc${l{mZewObHuAZI#ox>o^HN5U7KwAht@n- zTi)v*_U7#NN_zidmR=`Ss2SCEoyc(iXQO`REkfoWXYx!~>-O+OZF188d16=KHxc>C zy10_Y{Vzyw_OKMJ?z^ohv{h-4I%t*ozLFyOdLEX8`YsLuC-AG4Q?06{=k&STDZ;gQ zdA_sF?2LW>XWUSOa2-egd&Ruy7CRt|3XmPXPcZ>{MOnxq0T5;Y?Xp);)=A_zwu~bF z#C&b;t<)O5Z|*FT3SEAj9DxoWJv2IWaIhRXGI(^~aCvy}uHie&yM~U8;$PRWe`;F9 z9TtaK7+5s`=tc_SBGgXM;rai`>O2nY?p@_LBav4iOR4;e3n`-ZVR>@z%)~ zvG$D)9}Z&pY&-SrU!b>>MMumG$srF2z>eARi6o<*!p8fbAeKXzzDh*r>Ib;^Uk;* zh?&9`V7G{`5rdmx5n5%V>8!BNg2KsKkPLL(R*f%&t66C}Ez51MWu5CDTi91(TFlO} zFH6)fwUyk#5SYXlSAuwS4c-w;Y9tJ1Z`MUvDQis;hdgZNvey-V)a5=q{+{}L*<^G& z+iYR8a8u>RZEYB`nT=fpL3^5K_mD72vv+p1Cf!<-U($<6rDHBT__ZCjbhmml%q0la z5a*MwC!~lTDX~sX+gjUv1J-YjEzNJgQQ=d6B5p@&8USO#G7<=xv20o%UeL z1cL~4Q#xgkxq|bBd<*k!BQr2G?6&F_&8YS1h~^WxZ*F!}Ei%FrJ|rZOvIZZp*5rEV z%5=fnD0yrA7flqte#c~D!tl?;GU{|m#(&ft0;iyI(=|nSzcmZocyfcd&}Dg|nYym# z8Kc8w5q0#9em$gV^ft1ChxWxD*I}_@AHj5CxNZMz)4KW=G92O^PjCfv$<}CQ4=Rb+ zFi{t_Dpwap3@zHk3=*x$B%PQ^Vg}wZ!z|8JMEh5O@KO@yBSyf35g@Qi**e=0v5dgY zQ}rk>wchj&g4Bi`DcXYkgl^}or5`&aQP;>w#*in;p09G~ATnfm^RG6d;lDtscHM-= z8wqR%6S9$+@s{(iM_TyEQdTTl9cLn%G910mXjMrlmS4xC%EQhR&CTu9!+vVuEBwFO z)Z8pyrfWxj48g?8ZhgF<@g}jCoK@j|bv|RXHEMUc{XM>w_+5&cjq!tq21b2}wR~CG zMBRQ%=4K6DhLz|bG9^OXiPn9?ZYIbgSg#MYE&M=zH)awm5M^h5A&10v*v{RXF zj!P=@L^jnd}pe4OLM)_&EDC2xzlW@m`O zPj@~@?xyQV*h-q~HV49XZ~{P-QA!_Zhps!}QZ~2+gLCVc&nz z-6@L$Q1Hji{It`zka?P#a#5R~U29HlQwJ$UnzXlhH)7J%8-q>TRFYI7MxZjzzXG@( z-aS4cL)s>W5`_-9lMBkTTi`=|gZXgB8M<4-_75+iTh-)iTjtDk=RZRj%IbVMiT5b; z&2|eES~k<+G(s{@J=tuXEN7dtK$N)|-G;)%p@-Y!^ryRTw76L{rVeZ8xB2HEsa1a@ zMboE59VWuJKnA;x_>f-Jv>zfT+Tn=SCC1A6hpDF|H!-|x*SNBbfgxQ%ySPv%+(4YW z@ojxw`N%v#B`b;n7+Q zPGUVhj(2=q>(}+HwcYp=cb+G{_~OyTVjAyp6rGeRsT2wMQ@<7D6G{;Egv(2>s!6@E8x z->5An-@Z|EmsQk>HMP}CYfF}kr6pBW)d6vdU#tyQi4|3%`{p^~^6E1G`1JHaS)6o{ zM-a;H8{DU&USa&@gHuvQ2y+qrAi^L)XdMPCj63S4;**;>gAY}KG_~ZS8P<)PZ z;{*Ox0hD`uHomBx)OO;>@F^CGipQ6g1WFKBLp;?WSP>Q=7(Py+I6A&o%NQAggnERb z2*n77k5edej<3=3u{NTuj9!%?6Huq%aE@Q4<-3Dq(}Y0vu0Sw+SiU87briL1Aii1+ zi3YTu`15P@n9QR{B)V%1nSe5S6>XNR*Uu53~tF&w#Be zD92uwI}_q)e_u0VOSTGR4;LM-?Xopk!A3vTzMT_8#ixQ=#U@+B zO~|LDHAZa>pMWTFgUCUMY>e3&4zPMG-$8asf!Lyf^g>yEhV;fKzZvTcS^n)Of?zt6 z-7{l+%9uw%#;Xj;QU~7zdW}&@-WizggN#^wp`aeW5%F?|6#9!*%KPieVUjX{>ikQr zlSi%iK$5#v^Ua{s0V9i#x5J!8Felo$hJ~f!`w{ z64w?HqOL}f#siZ4l6+9Uzz4;dt5_EP2jZfEiRwoXgjZH`kp7waav^(=U!{jFh6k6t zLtXXPSPWW|>dAS>Eq8UKf%zCyQ>Gy8>Jy+8&X1w@R*0ae5eb1v=<`&R4?f@19M|V~ z`e;Tp@bh@YG!$rzB7b0E=yOwWv%2nj#?v!QGGHr5&tPi|f`d5k7isKj4LR*IwCcIj zu~@g4WIbLVPath;oQSBNqni9f?@N+m`2WzbYr)PlDqHbHBAh22z^;7zy_DKAb6K%JR z6cXv$NT=}$jwB^blJ`oA*Df{g38ck}92BF>6zOqbEE2)UrkIX=kyw}HqsYQc2nH|c zLces&0Xp(4pdYb=NmxFlZ+7$-9oX z*nZG%dpJ6_-L~zu#@7Oir0~>VQ93y_XQta4P7w`d#56Fs^#X>c#v<96&m|?*C%@&B zkNLuum7oxd)T51ZdrxkHKab+EO5wY+L`a|vc}V|zXHZ$ov2W4iDU5n1mMFv`AAv-Q zb;IbovpoXSvWN18Gg466C-0HMcVvlP#hew3Pzgp~K8o}-q^ti9(v&FCAnDe!u;*~b z^A%Jbk3%stx)a4-LbQ5kpJF+$X$8BNLyuX`aDX*n10!)vy$0FQ00(QpN5%$pUr(bU zl4w88D=v&&R|gF_4BaRax1c-?{%o3}pkH!lV|3Lw z@?6%t$H*oYuehwH&Yo`D$a*5EnDFq2M181-`iPO~dAdBp=1b)k%x$FU9WD`xLSV9D z0!bLX<*5InXsGK8pE58MO&hg6NFCJy9(2uh#U9cX7S&_w#Q4bcDr-fUWELUCx(w~r zrcx2@qx$5-Zh2Q=wqjClG1D+B2%800UgwIewcE)-0lM-gc@B~+?^JUM#%e zrZ!*cE@+5)SVBWnO}#hdTjkUhsWLz>tNYHy?QS{W?3TCp{2xtTY&_=XL>U7wy5)9B z-XFNhtrS?53s7BPk~=(nm|KpH?eyez+NK?8d~Hpplsez;D{c2N3v?Ii@{!FkUB>&2 zDP=Aj@J>PZ6<|Sf7|Ut$VM*S%m1gZ&QFdbt?ucgSI-E4%1bLO&_TXk9FeLJ4$bzA7 zN0+Z$3cVW&4fZK%WLn$2%9c)&Ni4Dessi!5w$iMJtkzortcEgUBB3<(Mob}+GK^GA`62NXd$`xEb4emTm=o>34F%%! zY0iP3UGd2Th^Aj?-9){0d(}*IK71Kl!$?#K#mHPvB=^9+K=ym7%5a7i+1XU$&JxiA zw8~D?fRd(VlpsPDj}m(@VW7n8{&VIU4M8B+v**rLcs|SZALq_>_!5@u?sMlF1;c_G z7a4O=8Cu590Fe{yEL+iH~MKilJTy zK=c`O1v(7cgc&3j=_L7v)9sG}$h#09i?j}e62iZDgjM}|8Kr!tK8TuFvGjM45VE|C zZ&NI?1(^}eloBK4SS)fsxcgk~N14gLEI%zCo-(#4fSQb9NXObo)vi;-p`-#$X&+o5O`rnw}mw>sm&+nT+;cw0Fk0K9A-=0s+ zw46t+{SxzgH%)%wj0q@xuKE31q^nO&JL~*@Y6(@BnBT41{2oKJx~5ODbIwt}>X!ZKB z7%TZ!mmDSI&Zhh%{@?=h5(8kS;q8(??^uXA!->Ytv}+UTG&Gf2=CL5Ac|UqKgFl{_ zPp7_#iHKH(m`zcXW>Y?e(h7g>l}HSyPc8w#1gakTJQnjQ1Nh9@dX|~9s)(|0kG%rL zh&%CcDNFGm*GcON=HGnFY}&PS~D=Kc?vid4Q69 zFpvYSIz=-nLkwPP8YQ zFr^Gjwr9!f2kx&)%G4!nhC(YzhjTT{ir#eSLQ-Am6=jat7+v#iW#(`!StiP_gx*aF z+(W`tW}=8@Uk5;l&^Dz$?jqi~7;2~aXWP;QK^XN)5S0wjbr#Z~1!sI^1h<9}1PMlw zbdZsN<|_Hax0yi#{_|E=eQ+%?sWwADu!jCw57WYg{NwGLd@s#TY{3VQE#5+9j=k|f zU@RpVnve!nyL>U_cajZ{SMQ|yQFUtQ&!5B9gC~rFfg5PWcPU!&Yjl*}C}V3FZTUOm z84bez1LXK7Hx%4RfMm~$djGp_?X$vLhKX@2O>&qMX`y$xkmf;eZlSzSp9}qVPvbj* z>yX3sd(4-}S{UydOr+`GypzV3h@A$sY#I4tv7VZ8i5s{j9HFqzfvQBNe;~FRi*Yk%jOe9RAIJi z)(Ypb!mRf9ZeXm#o zi%Ojx+B6QMIp>6NbPa$O-k~v;Fq~{Bv^>)I0Y;M1o+RY}9fwO+ecG`d^tLN8-`71tO*U?O(Ni!}-HlQ&e&qG4rF{#WM&ZUcqSu;zv z1~`=hwl8(lQG}xlNy)cj$;4JedIIe#QEa{z+u_7=fpU^*b<#{HsGCI=k*)rcAuP9+ zOJDElQz5I&%6Fb)nJM!sVN$8k1oItEoSAg~;dyb8-ilXv#6bI~}SVVsZ-rQoV`ucHNOr=j_1l`y#+=mGG z0JL8Bk|j*@DNW=HoVkcj^%9-!uGpcR1K6_i$~z=-17MmTHx@V~X;< zfHWi-(-IVeud=mxI15{DW6r{60Nq(gizohEOKdSF>bq0%ovIzTNP~E7Mr`M5i;&S4`@2JqPNg7w0NhC zt;+@wF>+I=R`T7jBwt3}LBIY?d^~R;%L)q2eX%kZT=%~b*=K+l_ve!RBlx`S#&pQb z3(C)s+X6=oJ80UFQwHh|4m=x!eM$DOn18J~^YE3iyQWw^gXuG_0yTon(PsWY@Hl*E zxDm+uB4rI{tVHu!3BD6&Wj+b>bol&`D}^Vo#WsO5s%<(qJj6si_0@&kg+TSX z7l8>z8zPnsD5N6_t=u0+Kf_IJ+h?-Bo;(A+9`(ifrz$I?9RfzTgEaPqU4GoJYkwRP$eGJ)`dgGLsbF2~60e*Ri&sF>#RAGt=$VfpNP;BI=mV zlqqhabg`v>2i2gXRs+OMR6MatORPvFvMtXqxt{q38RKxq4JdsUbCSHCQML8VF&Ln) zyq-ynf1>vHkNU=PMIYCF}B z-#fcKP~$#Im5Z2X_TaM)Pr@E}G{P_lo_-7h7GUYm-@**SZh*c9ffAq961Vi5_zNwO z0555<7TW$eJMq%@I|eaRoS`89m$VOjzhgfv%0JIOgoxVTZy(U3UX=Vw_Q2r@FG$sP z%se*l5ntFl+|~AZO3of{+uJEU8E$3xAWzQlfl{c=EUo>FZczm+zPzKsca?m5xCmDR z`>#Zq{t-3>qKYV`@^aARJ&8vb|;Q-HQl$2Q=EB>!IWma3BcqE|j7h5l-`-PcJo zM)-R5v9sL78t4Pm47k zkPGa=H{d%{x;h$6DEHa%-BG}}NR$Y^=>7DMQs~d8XNBFrJS~Mf%((Fp7;PXE7)$kd z!$YP}4dh!+Q9rsGhEj#0l=^tHQg{|N6v@|rGBsY8Rx#ZX$nM!n7ZqYXKi2%!e(bAE z*67^!RS_{vG?!~3*@cc#Bp_&5Bm%XNP%Oq_fP zm+LN6eijppStjBCpTqwe1rkPk`!MN-GWB*?pmW$;Tn6dS|H?usga5S?+4|aB$n;r? zl#hCphAe8Os(#~3m|Q(~A0MnvqolTg0!HX2*p{>SU{A4A^nT=H)1U9J6-(E^|CZ1#aER5i|Kao!hSj$_VE?FdmD$wP}}?EMjG*A7$_i_~CKjfQet z*ATaCzY z7ZC#)3sK}t+T-1tzviNXfA9XYYd%(>%Gx22gfPZdqnAsj?L@QurY=?1&_H zO5rEXSZVpfxGo?cCwt^pGHI(T)NYqTQG45;(_J|y-KqJ#ZuyP2zoa;An`3Te(lZ{@ zE|2M$Tb6oVZQX-Bw#|E{DO0}hHof7I+g-Jo#NgVj=7H&NIF3g2fG zj=4;qfrFF%aqw*`SU3&Zc8!J8=U{=dF%}9^hy2hR+HS{{KaV`)wC#Jn)DQB(uSEip zyq*vVl!>&+Ei?6k=HuQ_V!zUOKT3QSY9@3rZKU3W>*d1C*ue_22&;caG|jce+j6U8Xi9THQJCdE|W_ z({7jP0MgOpNPpR5+V3&7BOOzHw@3b?$MmY(vkVO^M-qJj=SX7sdXPx>pb$JOmAqX=*~F?>BydUOpy4ZE|)w{b;*TrLEg1B zj-b`f+Fj%ql}A!N=F~+(>O3@ahpX)!H1ln@;%BY=gU5t|hg@y%Ypv`;0vfr)V|vrW zGHZ=|3(;ufA&==TkNg61Yi*?5Fg0ijm3Z4jgx=Ab`6iMsb+Z<};3i^!Kq;+-Z@FYd zp@r{Q-DsVJrg(DRcFBLB7QWB=PhND=o%5!toiz?^L(|YQDepsD<2BSW9GD+I6^nHY zpi9kcofzNDgZ-AmzhVuKp9jf1NC#QNsj;MkXm3sznyBj_c*&u*={krOQaW0!>EJtq z+;YJXPY$(2*TeU5iE0+|QG;|{q&Ce*k0rI|&P=U61!mLp@?}zJmnhFyO^3SrwHrv+)Qtq`3)n}su;K$%#?imfHKnL~F^;`RV*aJTV%HsrFK+70`6 z%zso|_K=DL+r4nTUG{sfk`x@i--2Ct5ly+CZ6wpFElvj=ti#x$dIL2`>p;W(%$#Uw zwcTX_9-7mgRxhSss=V)hCTqGA_hX}h;mDZC#Lw%J_fMJpAUTOP zNV(K~iYk5B8 zIm?bd!|JUdQ_Ei{(07EOc~%2AgS zHJ7{dcDv=hwpmA{($Br}N%j9+#`f1txW>t8<-ErC+vq}9f}W(z1}jkYf=p>FZq=TJ z-d#F9IGh@9edxveYa%mDf>e4eJjZvwyEbpC4csKtLH*1ucU>khk%+8|?J#;(k@&a`I%xrByQ-`Cs(dS>H z!-tKa8mPn37Iwju>8H*gT`9tiNvxbZ-13WSaHnJjx_%mUy(_P6%|fVV8-}AY17k^m z=cL(#Y+oq`rQD`Ibt$Mr*SvBYowed(2H4H^$Nz%zct$^z9@DQ6 z!{}Uc4eyC|)eg4+3WN<88fiGJ)cQiEV^rKBru)C9Ue*GGT=~4K0}Skb?du@OTCM&J zT1r=SYI4;_I8I}|S8F~r4)$h-z57-$!u3nD?+Pm(JxRr9+STtN9UUZ6gZeSFMtiTz zQ?Y!6y-PN4v03aDZ3rzIA8-Bnj3Jm1(n9EBD(-N@1gqa?l?^5ls5cS^@f9z|fWYZ1 zw_E8wFc^`JHB-oL;6BU@yE|{M%Xafo^;VF^g>SfCG*(w%O#Lv+F2AaNhy?k_*gLfY zHnJba!B6(nc>jS;%lE1?z(d|i+XL${n?MMc7@DQENgl`Bw2Sk56ng>cFlrvnW;<2& zQ=s9D63T}w>Q_tfs^p{c8fbQvUA-NtK6Y7$X{2^fBL2P&GO9##Uz8+1_NL3;1~O!ZF3- z6E{&PPtU9I{WdY@1GeS+682v462zL7Fwr zqfNY9$>-OlPxmkyS#P#6D=;0W+tI+aTx-MIS@HYy%H+4F5`B#Gz1+d&*e#Bc?JSkV`Dndq%V+m646oyy6Qbygh(QF*i}A2y({Hs#vYs{yVN<9ybgx zeZN?el-s~LOcLDn!?c>oCGK!z%GOYU#mm|oUV3K3X^CYhjmk*e3$P4J@7ZK!3fp_bj0PkU`5BU(l{+KPr>Ba!V--^ik9 zGM)_so&F#_${F_%;2Ze*dZ)t@L@a%z4$G1Rdb5u9&F{Y68gwv+^Rpf++T*@H1(zn! zA~Y9H{&th6AsT$ow(Wd0lp6WHTWJk_mIk##$PgPYkdz~rw%!R_DiAAm6tGbXF9)eo zV!cZ98|ugUCH-h6_Nb?rsYxd>CLpumC}_$zxhA6Bm_d%9z3BP)`jWk6r&bL|DJA^} zMBxpV!-dYKapet;a9TX9!m5P&e`YC$E z>iTW1Y-0TC{%ZO#D2t?=7djP_I|F&3Bg)%p(Hy>Xzch9`Zb0roN%LiFm=x;48^6bc zpTN0ULyR%I&^?Fs2v+DlDRQSf>@>{?O=-gs!uDWq&y$Jz6w8bybSEW{rp-JZmTU2h zAs}>ri+U|?Z!w5RTti%MnXvwiasTToV0rCv#Ryy|eTqvHBSugT>M?zPTnaZX zb`3ol%UDh{a7k}6aaxZD@VgdAs8k%jdMx7(#Hm-EM-)8pz%BJuNf_^m%9aeE-L%cA z+fdTi(5ckGg|I!t*ySpwk@4}SF2rjsiTL<>%2IkQUi`^T38ZxhDd0JDDkU(C-aU54 zGt;&Z_SZ&y_HR`Ev0G=yd3<&jZ$w>I`)~ z7hDpuEJkiqC}yo2>pan6wlzkO6IYJyw#^;^F)8=sZNDE7?a+=QJ_zNLp}%0KXs+cX z%0U-XPu9K}`t!-0C=5yz2FaHp_k9LUA^jb zbPisGZbcJ1EcBieeXvLRYeYz6caXy%V(_R)p_RheUd&hn);xyq!O8`;2Wc*EJYZ{R z2BaPJ&}uZy7hXY|@&n*J-%n)1-+iKR?8n}=_fuzu%igv24AT5}Z)lfQnPqluF;kWD zDcrD?PxwsTsV3R;gnA=<#KOFHgGUw1QdFLE0$e_}hDray;iv@TOR?O76eycp5#SI9 zhDu5img%2Kie$xQ+)z7oc31WdBd$cQyZ3j$j!r;*jeBg3w}5~$-`aQ}I6|2}1i6Ml zzGz2dE+rJ0sf)*HdDt&#TmuoJHP29wAhY*5aE#vwqNWC)K)Y+D?s`zS-M0ntp;Mx* z0h$5CX>05Pg2~Lag`5JP{3&KI>>Eh(yeIHxfx3V;b3l>F?tH3omRU)&ZJv+O*X=W@ zp;Oo#u?Ab*wx><(O@vPiQ@`d_TgcSHi$@MUm`Vkeq5gDMYn=qW+&Ug*QB>9cw($9V2F(1+z zP$comhY{l)`#zAK@w*1#veGUL-1Z@Ed8t{EhR8ENPL*JOPvW+ZRT`_}GLSqI4`0yl zVPH)dOk+F*cVay)e`ISQ-;Mj@=@qCSyKk`D-^S!m{`@mkWzRdrAKz~}Pf5YFD7USB z6UpM*&+_x9WBfjcY?`Vr(^U0tuBxp_uzio|r>3eurJSUy!I8R})I3NL?i}PZC^DVIg?7if zQ{vH@{v#_z_du=VE>V0xDV%-`ni$LY^-8o@8L@-Z64U2IY@BATqNu4)Atj;=rk`Hg z6+4UpFqAfHaO}jkA!t>z5a(8}1^<8{v5|_ig$M01jrk#pYV(_3AJ%oql5+wle*2)~ z!NK|_D7LDf$%L%g5u>_X(_<{r0#jvrx5Y<<LMjB((atTF*(aX%!nMe*{)ntXIQ;>Z^ZUrzbBNgk z%+7SlN6wgyl%6AO{L%E<0NX6oV$GcrxC9P)@Djz6iyYAI^VI3+!Ja{EJgW-&HeqPm z70dZ7m#y($lvgZfJVAfaDN1_p9biFw-Rd^GI*6Gm?2sG{{soL3r^F7+ZUFjpSaxwN zmO;NY)$=2*{}jt_5DzXc=&&>c!-zFj5C1;>XNZnv{FDm47Eyl$lhz%8@cDdK>Y4&S zHz&F_kAB)OU~12amako%IXDHYXY$ByH11DIT6x>kuHkUQOqsT) zy=iFD*ibi?!b4q_T}vkzD<-3`*HgIxcs8p zmI?`kPFh#qh25@q#zJG)USD{7a1nOuR1X?}M&BZp&H(%Swd(Jn>RtG~uRtDKiJR;3 z;$nMt4~%ttwv_jI;Bw8uVQ)DfW%tf7YTnXz5wv4C>~|X;;ct;Y;KTkfRqtAxTX?%9 zKOejmO;NX_(^1t-Z>bYM%&{NmDmV^u*`6*+!+uuLD@E8CtVV4T zwG7=Tp&M216R`Tg?_{cvbr-hs_p6UV=^CTtcM6itvt4VY=08OB2R=SEem}XAhHjkW z06Tf6_7dk3qZWAoZ%97JD0$sp_alG1K^GY_iZkoe&Ng)fRQ^_gA`4(<7fMxKkhCA59GG6QRb-~eUX5KugV6P|ni z1RV7;rsLB|aC$shB)q3JnybaSUUrEB`EwZIckW;I_WS z*^6m{A&!|k!*gs*K`r^xu`$KUi$k5>KMI{U_T}pzrK6WC;`E9+Y~?V@VJC-O z9QJakmp=pEr19tEu%631%KM`}Nv>yd{=4n?QNZxM$P4m){z zCx1R$MMcxK@(~VuIc$U4D&S<*K?*-ycCSf7u6SgJ?vMFl5Zb`7b++SN! zwN!B60R-$p*Nb(_s)Lnf^j86wgIMXZ zk}C10KBdH};PNH@+M6z?%BpKu$HkaCK8K>Hh)`ZpUd_HH2vxz#N};x-s;qjs@NIu> zwJ>4)M4^!9ix&n$Z8LtptUOp<=V~-3de(Ll0lhWXFlwyC+}hO;nHZ=RSC#}ymx(1} znZKk=r1n+$EAh7g@${wsfLL706U%F>mnUZ(SF3mJN{;XdQGcekC+MxLtqAz3 z;^Zom_^Ks-`m69uiR7H}n9xTc6R6^GW&X;N)q?g-46~m7+k!u9Jyae`8U1qxeWoqv zqQ9h~sssu+&RQ04J|H=T@`_ddGCqze3ja#suPQ59Qt3BD^1NRrT_1DopFt{? z*HrqK`>O&aq_)Jtv|zbdR_(77>7PRCf;BbOwSa-(k~&_8US6-VqAK98t*Q0nTMX8r zAWTICdSZDEXo5?vu(;e$Bc`HmxiGgBqbBijKBd|7;#LicMSbmiA$oM>xRuvVqCYJO zR4n(4j8g0Ob{*w zPXE0YL%15afLqn)Axr`;0$zbI1-JunB7z&Z6Hq~rfJXpNBg_RZWZ-YU5Q>4@0h=$t z|7!tn1ss2&AgluJ0Q`4^df-if7vrwL{lG=QyAd7*UJv-)i;zw@VBliVARMp*=N3`m zoq#VRya2ol@XkvS2fP@t6yXoRYX~BAQ69i|5#9&h3)pZOAn*v_%LwLm$OD)^61)R1 z0t_N#0;Nu&1OJ1E&N2mIyL(3$nX z0lzg4G67x>cnl#MxPbdpsR)yR+X3AOxxkA6=h8FqV!#Fj33vqXF$5p*CctMAihxG} zFTDox0WJbY5sE1u@LBjSHG~8HGzawqZvs3ItH=oD2ON#?W8e&!ue=`e0qy|&*;GNuqIkg95pscd0bX|_XaIKtx)6N8ivZ^#6ag;=Y(ba{ycO^n zggbyo0e2u21MdVpj8F!=3$O=a8Sq}fPY^1B3pYV05o&-t0cRrI4ZH}j451!)4d5dP z4ZxcKwXu1)4)3c|B7I3 zMqT-WFc4ulaQaV$ix4t_i-2PgE(PuY+=Czj?*y!IBMvyh=MbF0qkyA4=ojD)z)1*q z0CxgbBNPL#0bGqx3A`TgHwZPrqktm{VC#U3fOjL%MdEru&veKMcoE=Qgl6Coz_$>7 z0lXKmNrK#f6ZCk&BjpF&jL-!<3b+H|ec+vdH_t#h$^+Ppa2j|k;B7wGw2j~u@Pb0f z1h@#8hj1xyCtwgE3wS-?y9ieU?*+8Yg02F$1HOweh2jC#n;{dz0Y?@=Ccs6&M-a+@ zHvw)yr~%#z_&bCE@F?I8gjK*h0smt*bd2%?E}Vz|!~|XpDBOy2zzM#OumyN8;Njb# zKft>HXWR}Nz>5H1Lg)hC30Sibd4Ut0aR+P{jAGQhFmE>;6wz8;sNhN z@BxniK88>PythOU=%V-C;IkO;n+S6e|IhxIEGdFzg2`bTGC&wI&FUCXoK|BgGDlN7 zarbLbnlNZWn!{3Tu1PIQiJHtN!F(^`%_$ahs;MXr!IUX2RY;qb;xH8_;gmMiDh&P2 zAjiOBYt4Y7w5Yi=rHj#;hBUqY0cK&qgj7dLv8jg9TEb~rErNBL*^yeDQe!GYetN#w z@J#jUd8sa<^BYblnXj|)V&q-t#|URbKFNF-%Me|?ZoLi0_Sgps_U%I)gNp~%3@oxn z(>l#vDZRWc#2gZ7c&XRuUP+hg>XtT~ALA_nno+So3QePi1^!49ruaU3*=>IG0 zA==a@B#(Rhk;j*;znA@dXW5Vq@A2bbdu@n?J<@!;{A;QvO8oelIi!C_gsCd z^P}4f;zb|ZMw_C?>-Fj|xh+)JY{QrtWU*ijrLvJow(4xHF4trkB|q!=bg1(}(AW=pJzo|Y>G1ghQD1-l8%SX8Pqvm4 zo6T*|0l+JG$d1A{Ls)=#S}Ty>OTIPvsr%2(3Me5NIt!YnxVj zMjrBOI{|6!Kx+x|Tgj&-pOySsT6Z|990lOC_Mmkwt*L3vPwPusThSVi*7~%@{|$J1 z7U2lOhX`LF*tQG8g$P$7UsUU}1=m zj=y5J;cD!8!ui55Y^P)h7YG*$nZiZF#lj`RrNRi|GGU}33ZsO}h0#KmFh)POo-lEI z8BVovWVSdThp+*^0rkX>>5z;gctcVFo#>qrJDVN-5iPgBw05??ytEqUU96ZpSfj%t z93Ze`Ey`F>>-V1#fn%jMkAiuH#hkI`Nhk7c2n7ONK;;DqsDp{^Qd#jRa_2P~I zE9I>t6xRiCB2~s?<}53ze2l~6$Lb5F&eI# z8k}~rO4;*loOLIj=Fs&8Y8XqqmCnoAV?5Vv|I&&&oJ_`B9WOE0&wEZQ#ZNVPKNjFB zz=V8$!hdE`|0&3adl3Dn&B7UX|0(EpT#4vE?RK10C#SGp(OH^PQtoFFq~2eD{zph) zN@t=Or(eeY*nUCy*bsNeD_D0PNaQhMr+NJFgIes>96C9i&tWBpcXRj{hb0u~=PnLE*-qb`CG*FpIRRmK)AO9wm@$^m#Z41eT|q^qpDVAd3x&2#*Lc~zLm$}0V3o>irOc0CQP zf;5FE&2uXK{u<#q^IU)Ja$KSc_;n!)yUcSd>(J!+C6(|d4w}zOL^x*Vm7*-&lfnNo z=`pOi*zpt=FNU9l`-rRHQOw0tMfH-!TXnoE}8hLUix8R~)39Bl_zsSF8 zUQy1I;pk!*{0qWN&+J(q-^2;btwa^WQO7?sEU#NpS{u-*>sIY+_}@YT_P+O!be{G5 za=kbApRVWo8vYNFK(E%%#T>rR@1Hr6?yda~Y3=`i%_zp%7FxY(J-%w-`;!EktWEZ& z%qFoZyD7KH*(5bJH9gVP+O(x9+H|C;t4VEgu9Makty{3Jc-^XX_3Q3m*R@Vv*Sik) z0oL2sXRa65XRohcfB*W(`p4Hdt#4hwWqox0?)Bn^>rtwyrL`rB zQe7w^Y_cQ2W0P}J(Wc@}HJj=;MK(2UYTXni8eLc`0Aw~fATJ=j+B3QMO*P`qBJj_pQKu?SH&QX^SSL=Hu*KeBGN@|V_| zDntk>l_D%FQjX=$fdhYp5!|>$+$wS4h*ZRd3kO7aZ$9GLc&)Y)?MOTG_RVkJ%)EW? z_x9bzieKBhnn<&7%jEKxGxilbpJUACQ+m%>CO;<^loJ4O62Q0`eoo8+44)6T>$g{0 zUa%HySG!>OZoL!uR#0njMJe#1x?R`hm6pKL)T-O8ZB<%NF`$gd)7pKyNIU=ip3!F~0H_KMa{ z822gy7UkI#wg`67>WtzX#7*N}snNi3o4#8M?zvov-6B5gqxJ^rtU`#fh{OH}u2f8C z=0m{K_;!G}1-UO>sd&0n{CLfI@JGSvbF|Z+bb3E8_kLLE9XNZhkpZW7J;|KCvqFA+ zSekMk4ubPeUuYiw5?pZlH%>bJH=LeOJ@~0`^VW~2E`CW%fK2Y4#iW4c%)seAKHM*z z06!ZzD(m$ljf>uz^CZ68OTIov!LlO{}7 zW42Ki?_Y=kSgO+S?zC#{W^>^Z2B(%pmu4>?pH*3bQ@H-x($WH(*(v01=d!6zyX)*S;ueR#gF$M%Ug4nEf1g#6y0y(6NhQ+&v9u6 zdy1JjEF9@pQJUhqI`(1~1)x|r5ve5d=IdOC9lVW?0ao2>sNnhc5a5oXWn_(TqP;Ii z1G6V~EUp!%AO$+NY0z#%AcdNqn>1q@cqknRuQ<3lT` z3C-syg*AJ8R&+7=1_i-K$=%&uB`R{)D+Z=Xs3Z{QB%^>KbI(fPh}E)fX=_brBSvoqyAfrnD2z6c_~jjx1pN{JM{%`wJ^F*HAq?BH7;wdSuHKLs%Bd)>rfFgxYR}H+^bN$#_(JSn z9G@!&R6F5b+P{_H^9YRgU-j&KesQ>oCMnkC-);<_KazOzvq+ZjrDn+R1JtOK3e17T z&>%FwhBUw9TZocF#4Rg01-wEA;O^n&pptMnN& literal 0 HcmV?d00001 diff --git a/src/vfs/punk8win.vfs/lib_tcl8/Memchan2.3/pkgIndex.tcl b/src/vfs/punk8win.vfs/lib_tcl8/Memchan2.3/pkgIndex.tcl new file mode 100644 index 00000000..ac83c2b5 --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/Memchan2.3/pkgIndex.tcl @@ -0,0 +1,2 @@ +package ifneeded Memchan 2.3 \ + [list load [file join $dir Memchan23.dll]] diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/LICENSE b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/LICENSE new file mode 100644 index 00000000..a0dfecff --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2019, apnadkarni +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. diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/README.md b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/README.md new file mode 100644 index 00000000..b07b558b --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/README.md @@ -0,0 +1,28 @@ +# Packages iocp, iocp\_inet, iocp\_bt + +Tcl extension implementing TCP and Bluetooth channels based on +I/O completion ports. The iocp_inet package is API-compatible +with Tcl sockets while offering much higher performance. + +Requires Tcl 8.6 or Tcl 9 and Windows 7 or later. + +Binary downloads are at https://sourceforge.net/projects/magicsplat/files/iocp/. +The single package supports all combinations of Tcl 8.6/9 and +32/64-bit Intel platforms. + +Documentation at https://iocp.magicsplat.com. + +## Changes in 2.0.2 + +- Support for Tcl 9 +- Added `-authenticate` option to `::iocp::bt::socket` + +No functional or API changes. + +## Changes in 1.1.0 + +- enable lazy loading of Bluetooth so TCP sockets can still be used on servers +without Bluetooth libraries installed +- fixed crash in Bluetooth asynchronous connect +- fixed reversed sense of -nagle option to TCP sockets + diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/bt.tcl b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/bt.tcl new file mode 100644 index 00000000..3ec58d30 --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/bt.tcl @@ -0,0 +1,697 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +package require iocp +iocp::bt_init + +namespace eval iocp::bt { + variable script_dir [file dirname [info script]] + variable version + + namespace eval radio { + namespace path [namespace parent] + namespace export configure devices info + namespace ensemble create + } + namespace eval device { + namespace path [namespace parent] + namespace export address addresses port print printn remove service_references services + namespace ensemble create + } +} + +set iocp::bt::version [package require iocp] +source [file join $iocp::bt::script_dir btsdr.tcl] +source [file join $iocp::bt::script_dir btnames.tcl] + +proc iocp::bt::radios {{detailed false}} { + # Enumerate Bluetooth radios on the local system. + # detailed - If true, detailed information about each radio is returned. + # If false (default), only the radio addresses are returned. + # + # When $detailed is passed as a boolean false value, a list of radio + # addresses is returned. + # + # When $detailed is passed as a boolean true value, + # each element of the returned list contains the following keys: + # Address - The Bluetooth address of the radio. + # Name - Name assigned to the local system as advertised by the radio. + # Class - Device class as a numeric value. + # DeviceClasses - Human readable list of general device class categories + # Subversion - Integer value whose interpretation is manufacturer-specific. + # Manufacturer - Integer identifier assigned to the manufacturer. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # + # Returns a list of radio addresses or radio information elements. + + set pair [FindFirstRadio] + if {[llength $pair] == 0} { + return {} + } + lassign $pair finder hradio + set radios {} + try { + while {1} { + set radio [GetRadioInfo $hradio] + if {$detailed} { + lappend radios [dict merge $radio [DeviceClass [dict get $radio Class]]] + } else { + lappend radios [dict get $radio Address] + } + CloseHandle $hradio + set hradio [FindNextRadio $finder] + } + } finally { + FindFirstRadioClose $finder + } + return $radios +} + +proc iocp::bt::radio::Open {{radio {}}} { + # Get a handle to a radio. + # radio - The address or name associated with a radio on the system. + # If unspecified or the empty string, a handle to the first + # radio found is returned. + + # The command will raise an error if no matching radio is present. + # + # The returned handle must be closed with the [close] command. + # + # Returns a handle to a Bluetooth radio. + + set pair [FindFirstRadio] + if {[llength $pair] == 0} { + error "Radio not found." + } + lassign $pair finder hradio + try { + while {1} { + set info [GetRadioInfo $hradio] + if {$radio eq "" || + [string equal -nocase $radio [dict get $info Name]] || + [string equal -nocase $radio [dict get $info Address]]} { + return $hradio + } + CloseHandle $hradio + set hradio [FindNextRadio $finder] + } + } finally { + FindFirstRadioClose $finder + } + error "Radio not found." +} + +proc iocp::bt::radio::Close {hradio} { + # Closes a radio handle. + # hradio - The radio handle to close. + CloseHandle $hradio +} + +proc iocp::bt::radio::info {{radio {}}} { + # Get detailed information about a radio on the system + # radio - The address or name associated with a radio on the system. + # If unspecified or the empty string, information about + # the first radio found is returned. + # + # The returned dictionary has the following keys: + # + # Address - The Bluetooth address of the radio. + # Name - Name assigned to the local system as advertised by the radio. + # Class - Device class as a numeric value. + # DeviceClasses - Human readable list of general device class categories + # Subversion - Integer value whose interpretation is manufacturer-specific. + # Manufacturer - Integer identifier assigned to the manufacturer. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # + # Returns a dictionary containing information about the radio. + + set hradio [Open $radio] + try { + set radio [GetRadioInfo $hradio] + set radio [dict merge $radio [DeviceClass [dict get $radio Class]]] + } finally { + Close $hradio + } +} + +proc iocp::bt::radio::configure {radio args} { + # Gets or modifies a radio configuration. + # radio - The address or name associated with a radio on the system. + # args - See below. + # + # Returns an option value, a dictionary of options and values, or an empty + # string. + # + # If no arguments are given to the command, it returns the current + # values of all options in the form of a dictionary. + # + # If exactly one argument is given, it must be the name of an option + # and the command returns the value of the option. + # + # Otherwise, the arguments must be a list of option name and + # values. The radio's options are set accordingly. The command returns + # an empty string for this case. Note that in case of raised exceptions + # the state of the radio options is indeterminate. + # + # Supported option names are `-discoverable` and `-connectable` both + # of which take boolean values. They control whether the Bluetooth radio + # is discoverable and connectable from other devices. + # + # Known bug: the command does not work reliably on all Windows systems. + # + + set hradio [Open $radio] + try { + if {[llength $args] == 0} { + return [list \ + -discoverable [IsDiscoverable $hradio] \ + -connectable [IsConnectable $hradio]] + } elseif {[llength $args] == 1} { + return [switch -exact -- [lindex $args 0] { + -discoverable {IsDiscoverable $hradio} + -connectable {IsConnectable $hradio} + default { error "Unknown option \"[lindex $args 0]\"."} + }] + } else { + set unchanged {} + foreach {opt val} $args { + switch -exact -- $opt { + -discoverable { + set changed [EnableDiscovery $val] + } + -connectable { + set changed [EnableIncoming $val] + } + default { error "Unknown option \"$opt\"."} + } + if {! $changed} { + lappend unchanged $opt + } + } + if {[llength $unchanged]} { + error "Option(s) [join $unchanged {, }] could not be modified." + } + } + } finally { + Close $hradio + } +} + +proc iocp::bt::radio::devices {radio args} { + # Discover devices accessible through the specified radio. + # radio - Name or address of Bluetooth radio + # args - Passed on to [::iocp::bt::devices] + # This command has the same functionality as the [::iocp::bt::devices] + # command except that it restricts discovery only to those devices + # accessible through the specified radio. + # + # Returns a list of device information dictionaries. See + # [::iocp::bt::devices] for the dictionary format. + + set hradio [Open $radio] + try { + return [::iocp::bt::devices {*}$args -hradio $hradio] + } finally { + Close $hradio + } +} + +proc iocp::bt::devices {args} { + # Discover Bluetooth devices. + # -authenticated - filter for authenticated devices + # -connected - filter for connected devices + # -inquire - issue a new inquiry. Without this option, devices that are + # not already known to the system will not be discovered. + # -remembered - filter for remembered devices + # -timeout MS - timeout for the inquiry in milliseconds. Defaults to 10240ms. + # Ignored if `-inquire` is not specified. + # -unknown - filter for unknown devices + # + # Each device information element is returned as a dictionary with + # the following keys: + # Authenticated - Boolean value indicating whether the device has + # been authenticated + # Address - Bluetooth address of the devicec + # Class - Device class as a numeric value + # Connected - Boolean value indicating whether the device is connected + # DeviceClasses - Human readable list of general device class categories + # LastSeen - Time when device was last seen. The format is a list of + # year, month, day, hour, minutes, seconds and milliseconds. + # LastUsed - Time when device was last used. The format is a list of + # year, month, day, hour, minutes, seconds and milliseconds. + # MajorClassName - Human readable major device class name. + # MinorClassName - Human readable minor device class name. + # Name - Human readable name of the device + # Remembered - Boolean value indicating whether the device is connected + # + # The filtering options may be specified to limit the devices + # returned. If none are specified, all devices are returned. + # + # Returns a list of device information dictionaries. + + set pair [FindFirstDevice {*}$args] + if {[llength $pair] == 0} { + # No devices found + return {} + } + lassign $pair finder device + set device [dict merge $device [DeviceClass [dict get $device Class]]] + set devices [list $device] + try { + while {1} { + set device [FindNextDevice $finder] + set device [dict merge $device [DeviceClass [dict get $device Class]]] + lappend devices $device + } + } finally { + FindFirstDeviceClose $finder + } + return $devices +} + +proc iocp::bt::device::address {name args} { + # Returns a Bluetooth address for a given name. + # name - name of device of interest + # args - Options to control device enquiry. See [devices]. + # If a device has multiple addresses, the command may return any one + # them. If no addresses are found for the device, an error is raised. + set addrs [addresses $name {*}$args] + if {[llength $addrs]} { + return [lindex $addrs 0] + } + error "Could not find address for device \"$name\"." +} + +proc iocp::bt::device::addresses {name args} { + # Returns a list of Bluetooth addresses for a given name. + # name - name of device of interest + # args - Options to control device enquiry. See [devices]. + + set addresses [lmap device [devices {*}$args] { + if {[string compare -nocase $name [dict get $device Name]]} { + continue + } + dict get $device Address + }] + # Also resolve local system radios + foreach radio [radios 1] { + if {[string equal -nocase $name [dict get $radio Name]]} { + lappend addresses [dict get $radio Address] + } + } + return $addresses +} + +proc iocp::bt::device::port {device service_class} { + # Resolve the port for a Bluetooth service running over RFCOMM. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service_class - UUID or name of service class of interest. Note the + # service **name** cannot be used for lookup. + # + # In case multiple services of the same service class are available on the + # device, the port for the first one discovered is returned. + # + # Returns the port number for the service or raises an error if it + # cannot be resolved. + + #TBD - maybe use device::services and loop through so we can match service_class + #against service name as well + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::service_class_uuid $service_class]] + try { + while {1} { + # 0x100 -> LUP_RETURN_ADDR + set rec [LookupServiceNext $h 0x100] + if {[dict exists $rec RemoteAddress] && + [dict get $rec RemoteAddress AddressFamily] == 32} { + # 32 -> AF_BTH (Bluetooth) + # Further we are looking for RFCOMM (protocol 3) + if {[dict exists $rec Protocol] && + [dict get $rec Protocol] == 3} { + return [dict get $rec RemoteAddress Port] + } + } + } + } finally { + LookupServiceEnd $h + } + error "Could not resolve service \"$service_class\" to a port on device \"$device\"." +} + +proc iocp::bt::device::remove {device} { + # Removes cached authentication information for a device from the system cache. + # device - bluetooth address or name of a device. if specified as a name, + # it must resolve to a single address. + # The command will raise an error if $device is a name that cannot be + # resolved. + + RemoveDevice [ResolveDeviceUnique $device] +} + +proc iocp::bt::device::service_references {device service} { + # Retrieve service discovery records that refer to a specified service. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service - the UUID of a service or service class or its mnemonic. + # The command will return all service discovery records that contain + # an attribute referring to the specified service. + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service discovery records. + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::to_uuid $service]] + set recs {} + try { + while {1} { + # 0x0200 -> LUP_RETURN_BLOB. Returns {Blob BINDATA} + lappend recs [lindex [LookupServiceNext $h 0x200] 1] + } + } finally { + LookupServiceEnd $h + } + return $recs +} + +proc iocp::bt::device::services {device} { + # Retrieve the service discovery records for top level services + # advertised by a device. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # + # The command will return all service discovery records that reference + # the `PublicBrowseRoot` service class. This is not necessarily all the + # services on the device, only those the device advertises as the + # top-level services. + # + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service dscovery records. + + # TBD - add a browse group parameter + # TBD - perhaps check that the sdr acually refernces browse group in + # the appropriate attribute + return [service_references $device 00001002-0000-1000-8000-00805f9b34fb] +} + +# TBD - is this needed? Less functional version of services +proc iocp::bt::device::enumerate_services {device} { + # Get installed services on a device. + # device - Bluetooth address or name of device. If specified as a name, + # it must resolve to a single address. + # Returns a list of service UUID's. + + return [EnumerateInstalledServices [ResolveDeviceUnique $device]] +} + + +proc iocp::bt::device::print {devinfo} { + # Prints device information in human-readable form to stdout. + # devinfo - A device information record as returned by + # the [devices] command. + dict with devinfo { + puts "Device $Name" + puts "Address: $Address" + puts "Class: $Class ($MajorClassName:$MinorClassName)" + puts "Device categories: ([join $DeviceClasses {, }])" + puts "Authenticated: $Authenticated" + puts "Remembered: $Remembered" + puts "Connected: $Connected" + puts "Last seen: $LastSeen" + puts "Last used: $LastUsed" + } +} + +proc iocp::bt::device::printn {dinfolist {detailed false}} { + # Prints device information in human-readable form to stdout. + # dinfolist - A list of device information records as returned by + # the [devices] command. + # detailed - If a true value, detailed information about the device + # is printed. If false (default), only the address and + # name are printed in compact form. + set sep "" + foreach dinfo $dinfolist { + if {$detailed} { + puts -nonewline $sep + set sep "----------------------------------------------\n" + print $dinfo + } else { + dict with dinfo { + puts "$Address $Name" + } + } + } +} + +proc iocp::bt::IsAddress {addr} { + # Returns boolean indicating whether addr is a valid Bluetooth address + # addr - value to be checked + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){5}$} $addr] + +} + +proc iocp::bt::Uuid16 {uuid16} { + if {![regexp {^[[:xdigit:]]{4}$} $uuid16]} { + error "\"$uuid16\" is not a valid 16 bit UUID." + } + return 0000${uuid16}-0000-1000-8000-00805f9b34fb +} + +proc iocp::bt::IsUuid {uuid} { + + # Check passed parameter for UUID syntax + + # Only checks syntax. Not RFC 4122 validity + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid] +} + +proc iocp::bt::IsBluetoothUuid {uuid} { + + # Check passed parameter for UUID syntax + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-0000-1000-8000-00805f9b34fb$} $uuid] +} + + +proc iocp::bt::BinToHex {bin {start 0} {end end}} { + regexp -inline -all .. [binary encode hex [string range $bin $start $end]] +} + +proc iocp::bt::EncodingTclName {mibenum} { + set encoding_map { + 3 ascii + 2026 big5 + 437 cp437 + 2087 cp775 + 2009 cp850 + 2010 cp852 + 2046 cp855 + 2047 cp857 + 2048 cp860 + 2049 cp861 + 2013 cp862 + 2050 cp863 + 2051 cp864 + 2052 cp865 + 2086 cp866 + 2054 cp869 + 2109 cp874 + 113 cp936 + 2250 cp1250 + 2251 cp1251 + 2252 cp1252 + 2253 cp1253 + 2254 cp1254 + 2255 cp1255 + 2256 cp1256 + 2257 cp1257 + 2258 cp1258 + 2088 ebcdic + 18 euc-jp + 38 euc-kr + 56 gb1988 + 2025 gb2312 + 57 gb2312-raw + 39 iso2022-jp + 37 iso2022-kr + 4 iso8859-1 + 5 iso8859-2 + 6 iso8859-3 + 7 iso8859-4 + 8 iso8859-5 + 9 iso8859-6 + 10 iso8859-7 + 11 iso8859-8 + 12 iso8859-9 + 13 iso8859-10 + 109 iso8859-13 + 110 iso8859-14 + 111 iso8859-15 + 112 iso8859-16 + 15 jis0201 + 63 jis0208 + 98 jis0212 + 2084 koi8-r + 2088 koi8-u + 36 ksc5601 + 17 shiftjis + 2259 tis-620 + 106 utf-8 + } + incr mibenum 0; # Convert to canonical decimal string + if {[dict exists $encoding_map $mibenum]} { + return [dict get $encoding_map $mibenum] + } + return $mibenum +} + +proc iocp::AddressFamilyName {af} { + if {$af == 2} { + return IPv4 + } elseif {$af == 23} { + return IPv6 + } elseif {$af == 32} { + return Bluetooth + } elseif {$af == 1} { + return Unix + } elseif {$af == 6} { + return IPX + } elseif {$af == 17} { + return Netbios + } else { + return AF$af + } +} + +proc DeviceClass {class} { + if {[expr {$class & 3}] != 0} { + # Not format type that we understand + return {} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/baseband/ + + set minor_class [expr {($class >> 2) & 0x3f}] + set major_class [expr {($class >> 8) & 0x1f}] + + if {$major_class == 0} { + set major_name "Miscellaneous" + set minor_name "Miscellaneous" + } elseif {$major_class == 1} { + set major_name Computer + set minor_name [lindex { + Uncategorized Desktop Server Laptop "Handheld PC/PDA" "Palm-size PC/PDA" Wearable Tablet + } $minor_class] + } elseif {$major_class == 2} { + set major_name Phone + set minor_name [lindex { + Uncategorized Cellular Cordless Smartphone "Wired modem" ISDN + } $minor_class] + } elseif {$major_class == 3} { + set major_name "LAN/Network Access point" + set minor_name [lindex { + 0% 1-17% 17-33% 33-50% 50-67% 67-83% 83-99% 100% + } $minor_class] + } elseif {$major_class == 4} { + set major_name "Audio/Video" + set minor_name [lindex { + Uncategorized "Wearable Headset Device" "Hands-free Device" Reserved + Microphone Loudspeaker Headphones "Portable Audio" "Car Audio" + "Set-top box" "HiFi Audio Device" VCR Camcorder "Video Monitor" + "Video Display and Loudspeaker" "Video Conferencing" Reserved + "Gaming/Toy" + } $minor_class] + } elseif {$major_class == 5} { + set major_name Peripheral + set bits [expr {$minor_class >> 4}] + set minor_name [lindex { + Other Keyboard "Pointing device" "Combo keyboard/pointing device" + } $bits] + set bits [expr {$minor_class & 0xf}] + if {$bits < 10} { + append minor_name /[lindex { + Uncategorized Joystick Gamepad "Remote control" "Sensing device" + "Digitizer tabler" "Card reader" "Digital pen" "Handheld scanner" + "Handheld gesture input device" + }] + } + } elseif {$major_class == 6} { + set major_name Imaging + set minor_name {} + if {$minor_class & 4} {lappend minor_name Display} + if {$minor_class & 8} {lappend minor_name Camera} + if {$minor_class & 16} {lappend minor_name Scanner} + if {$minor_class & 32} {lappend minor_name Printer} + set minor_name [join $minor_name /] + } elseif {$major_class == 7} { + set major_name Wearable + set minor_name [lindex { + Wristwatch Pager Jacket Helmet Glasses + } $minor_class] + } elseif {$major_class == 8} { + set major_name Toy + set minor_name [lindex { + Robot Vehicle Doll Controller Game + } $minor_class] + } elseif {$major_class == 9} { + set major_name Health + set minor_name [lindex { + Undefined "Blood Pressure Monitor" Thermometer "Weighing Scale" + "Glucose Meter" "Pulse Oximeter" "Heart/Pulse Rate Monitor" + "Health Data Display" "Step Counter" "Body Composition Analyzer" + "Peak Flow Monitor" "Medication Monitor" "Knee Prosthesis" + "Andle Prosthesis" "Generic Health Manager" "Personal Mobility Device" + } $minor_class] + } elseif {$major_class == 31} { + set major_name Uncategorized + set minor_name "" + } else { + set major_name Reserved + set minor_name "" + } + + set service_class {} + if {$class & (1<<13)} {lappend service_class "Limited Discoverable Mode"} + if {$class & (1<<16)} {lappend service_class "Positioning (Location identification)"} + if {$class & (1<<17)} {lappend service_class "Networking"} + if {$class & (1<<18)} {lappend service_class "Rendering"} + if {$class & (1<<19)} {lappend service_class "Capturing"} + if {$class & (1<<20)} {lappend service_class "Object Transfer"} + if {$class & (1<<21)} {lappend service_class "Audio"} + if {$class & (1<<22)} {lappend service_class "Telephony"} + if {$class & (1<<23)} {lappend service_class "Information"} + + return [dict create MajorClassName $major_name \ + MinorClassName $minor_name \ + DeviceClasses $service_class] +} + +proc iocp::bt::ResolveDeviceUnique {device} { + if {[IsAddress $device]} { + return $device + } + set addrs [device address $device] + if {[llength $addrs] == 0} { + error "Could not resolve Bluetooth device name \"$device\"." + } elseif {[llength $addrs] > 1} { + error "Device \"$device\" resolves to multiple addresses." + } + return [lindex $addrs 0] +} + +package provide iocp_bt $iocp::bt::version diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl new file mode 100644 index 00000000..8b4c96c3 --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl @@ -0,0 +1,383 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::names { + namespace path [namespace parent] + # Maps UUID to Service class name + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + # UUID may be either 16-bit Bluetooth universal UUID as or the full UUID + # Uses lower case hex. + variable service_class_names { + 1000 ServiceDiscoveryServerServiceClassID + 1001 BrowseGroupDescriptorServiceClassID + 1002 PublicBrowseRoot + 1101 SerialPort + 1102 LANAccessUsingPPP + 1103 DialupNetworking + 1104 IrMCSync + 1105 OBEXObjectPush + 1106 OBEXFileTransfer + 1107 IrMCSyncCommand + 1108 Headset + 1109 CordlessTelephony + 110a AudioSource + 110b AudioSink + 110c A/V_RemoteControlTarget + 110d AdvancedAudioDistribution + 110e A/V_RemoteControl + 110f A/V_RemoteControlController + 1110 Intercom + 1111 Fax + 1112 {Headset - Audio Gateway (AG)} + 1115 PANU + 1116 NAP + 1117 GN + 1118 DirectPrinting + 1119 ReferencePrinting + 111a {Basic Imaging Profile} + 111b ImagingResponder + 111c ImagingAutomaticArchive + 111d ImagingReferencedObjects + 111e Handsfree + 111f HandsfreeAudioGateway + 1120 DirectPrintingReferenceObjectsService + 1121 ReflectedUI + 1122 BasicPrinting + 1123 PrintingStatus + 1124 HumanInterfaceDeviceService + 1125 HardcopyCableReplacement + 1126 HCR_Print + 1127 HCR_Scan + 1128 Common_ISDN_Access + 112d SIM_Access + 112e {Phonebook Access - PCE} + 112f {Phonebook Access - PSE} + 1130 {Phonebook Access} + 1131 {Headset - HS} + 1132 {Message Access Server} + 1133 {Message Notification Server} + 1134 {Message Access Profile} + 1135 GNSS + 1136 GNSS_Server + 1137 {3D Display} + 1138 {3D Glasses} + 1139 {3D Synchronization} + 113a {MPS Profile UUID} + 113b {MPS SC UUID} + 113c {CTN Access Service} + 113d {CTN Notification Service} + 113e {CTN Profile} + 1200 PnPInformation + 1201 GenericNetworking + 1202 GenericFileTransfer + 1203 GenericAudio + 1204 GenericTelephony + 1303 VideoSource + 1304 VideoSink + 1305 VideoDistribution + 1400 HDP + 1401 {HDP Source} + 1402 {HDP Sink} + 02030302-1d19-415f-86f2-22a2106a0a77 {Wireless iAP v2} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + variable protocol_names { + 0001 SDP + 0002 UDP + 0003 RFCOMM + 0004 TCP + 0005 TCS-BIN + 0006 TCS-AT + 0007 ATT + 0008 OBEX + 0009 IP + 000a FTP + 000c HTTP + 000e WSP + 000f BNEP + 0010 UPNP + 0011 HIDP + 0012 HardcopyControlChannel + 0014 HardcopyDataChannel + 0016 HardcopyNotification + 0017 AVCTP + 0019 AVDTP + 001b CMTP + 001e MCAPControlChannel + 001f MCAPDataChannel + 0100 L2CAP + } + + # Universal attribute names + variable attribute_names + set attribute_names { + ServiceRecordHandle 0 + ServiceClassIDList 1 + ServiceRecordState 2 + ServiceID 3 + ProtocolDescriptorList 4 + BrowseGroupList 5 + LanguageBaseAttributeIDList 6 + ServiceInfoTimeToLive 7 + ServiceAvailability 8 + BluetoothProfileDescriptorList 9 + DocumentationURL 10 + ClientExecutableURL 11 + IconURL 12 + AdditionalProtocolDescriptorList 13 + ServiceName 256 + ServiceDescription 257 + ProviderName 258 + } + +} + +proc iocp::bt::names::is_uuid {uuid} { + regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid +} + +proc iocp::bt::names::to_name {uuid} { + # Map a UUID to a name. + # uuid - Bluetooth UUID + # The command attempts to map the UUID as a service name or protocol + # name in that order. + # + # Returns the mapped name or the uuid if no mapping is possible. + + set name [service_class_name $uuid] + if {$name ne $uuid && ![is_uuid $name]} { + return $name + } + return [protocol_name $uuid] +} + +proc iocp::bt::names::to_uuid {name_or_uuid} { + # Map a name to a UUID. + # name_or_uuid - Bluetooth UUID or name + # The command attempts to map the name as a service class name or protocol + # name in that order. + # + # Returns the mapped uuid raises an error if no mapping is possible. + variable service_class_names + if {[MapNameToUuidV $name_or_uuid service_class_names name]} { + return $name + } + return [protocol_uuid $name_or_uuid] +} + + +proc iocp::bt::names::service_class_name {uuid} { + # Maps a UUID to a service class name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable service_class_names + set name [MapUuidToName $uuid service_class_names] + if {$name ne $uuid} { + return $name + } + # Well known private uuids + if {[string equal -nocase "02030302-1d19-415f-86f2-22a2106a0a77" $uuid]} { + return "Wireless iAP v2"; # iPod Accessory Protocol + } + return $uuid +} + +proc iocp::bt::names::service_class_uuid {name} { + # Maps a service class name to a UUID. + # name - Service class name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable service_class_names + return [MapNameToUuid $name service_class_names] +} + +proc iocp::bt::names::profile_name {uuid} { + # Maps a UUID to a Bluetooth profile name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + + # Profiles seem to come from same service class UUID space. + return [service_class_name $uuid] +} + +proc iocp::bt::names::protocol_name {uuid} { + # Maps a UUID to a protocol name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable protocol_names + return [MapUuidToName $uuid protocol_names] +} + +proc iocp::bt::names::protocol_uuid {name} { + # Maps a protocol name to a UUID. + # name - Protocol name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable protocol_names + return [MapNameToUuid $name protocol_names] +} + +proc iocp::bt::names::attribute_name {attr_id} { + # Maps a attribute id (numeric or name) to a attribute name. + # attr_id - numeric attribute identifier + # Returns a human-readable name for the attribute or + # the numeric id itself if no mapping could be performed. + variable attribute_names + dict for {name id} $attribute_names { + if {$attr_id == $id || [string equal -nocase $name $attr_id]} { + return $name + } + } + if {[string is integer -strict $attr_id]} { + return $attr_id + } + error "Unknown attribute \"$attr_id\"." +} + +proc iocp::bt::names::attribute_id {name} { + # Maps a attribute name to it numeric attribute identifier. + # name - Attribute name to be mapped + # The command will raise an error if the name is unknown. + # Returns the numeric attribute identifier corresponding to + # the passed name. + variable attribute_names + if {[string is integer -strict $name]} { + return $name + } + return [dict get $attribute_names $name] +} + +proc iocp::bt::names::print {} { + # Prints known UUID's and their mapped mnemonics. + variable service_class_names + dict for {uuid16 name} $service_class_names { + puts "[Uuid16 $uuid16] $name" + } +} + +proc iocp::bt::names::MapUuidToName {uuid dictvar} { + upvar 1 $dictvar names + + # uuid may be a full UUID or a 16-bit Bluetooth UUID + if {[IsUuid $uuid]} { + # See if the exact match exists + set uuid [string tolower $uuid] + if {[dict exists $names $uuid]} { + return [dict get $names $uuid] + } + # Nope. Now see if it is a 16-bit Bluetooth UUID + if {[IsBluetoothUuid $uuid] && [string range $uuid 0 3] eq "0000"} { + # 16-bit UUID + set uuid16 [string tolower [string range $uuid 4 7]] + if {[dict exists $names $uuid16]} { + return [dict get $names $uuid16] + } + } + # Nope. Cannot be mapped. Return the UUID. + return $uuid + } + + # Not a full UUID. Retry as a 16-but UUID. tailcall so upvar works right. + tailcall MapUuidToName [Uuid16 $uuid] $dictvar +} + +proc iocp::bt::names::MapNameToUuidV {name dictvar resultvar} { + upvar 1 $dictvar names + upvar 1 $resultvar result + + if {[IsUuid $name]} { + set result $name + return 1 + } + dict for {uuid mapped_name} $names { + if {[string equal -nocase $name $mapped_name]} { + if {[string length $uuid] == 4} { + # 16-bit UUID -> full UUID + set result "0000${uuid}-0000-1000-8000-00805f9b34fb" + } else { + set result $uuid + } + return 1 + } + } + return 0 +} + +proc iocp::bt::names::MapNameToUuid {name dictvar} { + upvar 1 $dictvar names + if {[MapNameToUuidV $name names result]} { + return $result + } + error "Name \"$name\" could not be mapped to a UUID" +} + +proc iocp::bt::names::Asciify {svar} { + # Replaces non-ascii or non-printable chars with Tcl sequences + # + # Transformed string is stored back in svar. + + upvar 1 $svar s + set ascii "" + set was_escaped 0 + # Split can be expensive for large strings so string index + set len [string length $s] + for {set i 0} {$i < $len} {incr i} { + set char [string index $s $i] + scan $char %c codepoint + if {$codepoint < 32 || $codepoint >= 126} { + append ascii "\\u[format %.4x $codepoint]" + set was_escaped 1 + } else { + append ascii $char + } + } + set s $ascii + return $was_escaped +} + +proc iocp::bt::names::ClipSpec {{use_list 0}} { + package require twapi + + set csv [twapi::read_clipboard_text] + if {$use_list} { + set tcl "\[list \n" + } else { + set tcl "\{ \n" + } + foreach line [split $csv \n] { + lassign [split $line \t] name uuid description + set name [string trim $name] + set uuid [string trim $uuid] + if {![regexp {^0x[[:xdigit:]]{4}} $uuid]} continue + if {$use_list} { + # Unicode chars, needs escaping + Asciify name + append tcl " " [list [string range $uuid 2 end]] " " "\"$name\"" " \\" \n + } else { + if {[Asciify name]} { + # Need to use the list command. Just restart + return [ScrapeBTSpec 1] + } + append tcl " " [string range $uuid 2 end] " " "{$name}" \n + } + } + if {$use_list} { + append tcl "\]\n" + } else { + append tcl "\}" + } + + twapi::write_clipboard_text $tcl +} + diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl new file mode 100644 index 00000000..cd014a1b --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl @@ -0,0 +1,962 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::sdr { + namespace path [namespace parent] + namespace export attribute attributes decode print printn + + namespace eval attribute { + namespace path [list [namespace parent] [namespace parent [namespace parent]]] + namespace export exists get raw text + namespace ensemble create + } +} + +proc iocp::bt::sdr::decode {binsdr} { + # Decodes a binary service discovery record + # binsdr - a raw Bluetooth service discovery record in binary + # form. + # + # The returned value should be treated as opaque. The attributes + # stored in the record should be accessed with the commands in + # the `sdr` namespace. + # + # Returns a container of attributes stored in the record. + + # A SDR record is a single data element which is a sequence + # containing nested data elements. + # {{sequence {nested data elements}}} + set rec [lindex [DecodeElements $binsdr] 0 1] + + # $rec contains alternating attribute value pairs. The result + # is built as a list as its faster to do that and shimmer + # to a dict on access than to build a dictionary to begin with. + set sdr {} + foreach {attr val} $rec { + lappend sdr [lindex $attr 1] $val + } + return $sdr +} + +proc iocp::bt::sdr::attribute::exists {sdr attr_id {varname {}}} { + # Checks if an attribute exists in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # attr_id - attribute integer id or Bluetooth universal attribute name + # varname - If not the empty string, the attribute + # value is stored in a variable of this name in the + # caller's context. + # + # The value returned in $varname is the attribute type-independent data + # value. To get the value formatted for the attribute type, use the + # [attribute get] command. + # + # Returns 1 if the attribute exists, and 0 otherwise. + + if {[string is integer -strict $attr_id]} { + set key [expr {$attr_id + 0}]; # Force decimal rep. Faster than format + } else { + set key [names::attribute_id $attr_id]; # name -> id + } + + if {[dict exists $sdr $key]} { + if {$varname ne ""} { + upvar 1 $varname value + set value [dict get $sdr $key] + } + return 1 + } + + return 0 +} + +proc iocp::bt::sdr::attributes {sdr} { + # Get the list of attributes in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # Returns a list of numeric attribute ids. + return [dict keys $sdr] +} + +proc iocp::bt::sdr::attribute::raw {sdr attr_id} { + # Get an attribute value from an service discovery record. + # sdr - a decoded service discovery record in the form returned by + # sdr_decode. + # attr_id - attribute integer id + # + # The command will raise an error if the attribute does not exist + # in the sdr. + # + # Returns the attribute value as a pair consisting of the type and + # the raw data element value. + + if {[exists $sdr $attr_id value]} { + return $value + } else { + error "Attribute with id \"$attr_id\" not found." + } +} + +proc iocp::bt::sdr::attribute::get {sdr attr_id {varname {}}} { + # Get the value of an universal attribute from a service + # discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # varname - Optional name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The attribute value is decoded into a attribute type-dependent format. + # + # The $attr_id argument must be one of those defined in + # the *Universal Attributes* section in the + # [Bluetooth Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/) + # specification. Otherwise an error is raised. The recognized attributes + # are listed below. Refer to the Bluetooth specification for their + # exact semantics. + # + # AdditionalProtocolDescriptorList - List of protocol stacks. This + # supplements the `ProtocolDescriptorList` attribute. + # BluetoothProfileDescriptorList - List of Bluetooth profile descriptors + # to which the service conforms. Each element is a dictionary + # with keys `Uuid`, `Name`, `MajorVersion` and `MinorVersion` + # corresponding to the referenced profile. + # BrowseGroupList - List of browse group descriptors representing + # the browse groups to which the service record belongs. Each + # descriptor is dictionary with keys `Uuid` and `Name` identifying + # the a browse group. + # ClientExecutableURL - URL pointing to the client application that + # may be used to utilize the service. + # DocumentationURL - URL for the service documentation. + # IconURL - URL for icon that may be used to represent the service. + # LanguageBaseAttributeIDList - Nested dictionary of language-specific + # attribute offsets (see below). + # ProtocolDescriptorList - List of protocol stacks (see below) that may + # be used to access the service described by the service record. + # ProviderName - Name of entity providing the service in the primary + # language of the service record. + # ServiceAvailability - A value in the range 0-255 that indicates + # relative availability of the service in terms of the number of + # clients it can accept. Note this is a scaled estimate. + # ServiceClassIDList - List with each element being a dictionary + # corresponding to a service class that the record conforms to. + # The keys of the dictionary are `Name` and `Uuid`. + # ServiceDescription - A description of the service in the primary + # language of the record. + # ServiceID - UUID that uniquely identifies a service instance. + # ServiceInfoTimeToLive - The number of seconds from the time it was + # generated that the service record information is expected to be valid. + # ServiceName - The human-readable name of the service in the primary + # language of the record. + # ServiceRecordHandle - A numeric value that uniquely identifies a service + # record within a SDP server. + # ServiceRecordState - Numeric value that is changed any time the service + # record is modified by the SDP server. + # + # In the case of ProtocolDescriptorList and AdditionalProtocolDescriptorList, + # the attribute value is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # In the case of LanguageBaseAttributeIDList, the attribute value is + # a dictionary indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. + # + # Returns a boolean or the decoded attribute value. + + set attr_name [names::attribute_name $attr_id] + set attr_id [names::attribute_id $attr_id] + switch -exact -- $attr_name { + BrowseGroupList - + ServiceClassIDList { + tailcall Uuids $sdr $attr_id $varname + } + BluetoothProfileDescriptorList - + ProtocolDescriptorList - + AdditionalProtocolDescriptorList - + LanguageBaseAttributeIDList { + tailcall $attr_name $sdr $varname + } + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceAvailability - + ServiceInfoTimeToLive - + ServiceID - + ServiceRecordState - + ServiceRecordHandle { + tailcall AttributeValue $sdr $attr_id $varname + } + ServiceName - + ServiceDescription - + ProviderName { + # Special case because they need a language argument + tailcall text $sdr $attr_name primary $varname + } + default { + if {[llength [info commands [namespace current]::universal::$attr_name]]} { + tailcall [namespace current]::universal::$attr_name $sdr $varname + } + } + } + error "Unknown universal attribute \"$attr_id\"." +} + +proc iocp::bt::sdr::attribute::text {sdr attr_id lang {varname {}}} { + # Get the value of an text attribute in the specified language + # from a service discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # lang - language identifier as specified in iso639, e.g. `en` for + # english, `fr` for french etc. or the keyword `primary`. + # varname - Name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # If the record does not contain a value for the specified language, + # the value for the primary language will be retrieved. + # + # Returns a boolean or the attribute text value. + # + set lang_offset [names::attribute_id $attr_id] + if {$lang_offset < 256 || $lang_offset >= 512} { + error "Invalid text attribute id \"$attr_id\"." + } + incr lang_offset -256 + tailcall TextAttribute $sdr $lang_offset $lang $varname +} + + +proc iocp::bt::sdr::attribute::LanguageBaseAttributeIDList {sdr {varname {}}} { + # Retrieve the language base attributes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a nested dictionary + # corresponding to the `LanguageBaseAttributeIDList` + # universal attribute in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The dictionary value returned or stored is indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. fs + # + # Returns a boolean or a dictionary of language attributes. + if {![AttributeValue $sdr 6 sequence]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # attr is {sequence {langid encoding baseoffset ...}} + set langs [dict create] + foreach {langid enc offset} $sequence { + # langid is ISO 639 language code + set langid [lindex $langid 1] + set langid "[format %c [expr {$langid >> 8}]][format %c [expr {$langid &0xff}]]" + dict set langs $langid Encoding [::iocp::bt::EncodingTclName [lindex $enc 1]] + dict set langs $langid BaseOffset [lindex $offset 1] + } + if {$varname eq ""} { + return $langs + } else { + upvar 1 $varname var + set var $langs + return 1 + } +} + + +proc iocp::bt::sdr::attribute::ProtocolDescriptorList {sdr {varname {}}} { + # Retrieve the protocol information from a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # Information about the protocols that may be used to access the + # service described by SDR is stored in the ProtocolDescriptorList + # universal attribute as described in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The returned information is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # Returns a boolean or the list of protocols. + + if {! [attribute exists $sdr 4 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + if {$varname eq ""} { + return [Protocols $attrval] + } else { + upvar 1 $varname var + set var [Protocols $attrval] + return 1 + } +} + +proc iocp::bt::sdr::attribute::BluetoothProfileDescriptorList {sdr {varname {}}} { + # Retrieve the service classes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a list of profile descriptors + # corresponding to the BluetoothProfileDescriptorList + # universal attribute in the Bluetooth Core specification. + # Each descriptor is a dictionary with the following keys: + # `Uuid` - the UUID of the profile + # `Name` - the mnemonic name of the profile or its UUID if the + # UUID cannot be mapped to a name + # `MajorVersion` - the profile's major version + # `MinorVersion` - the profile's minor version + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # Returns a boolean or the list of service class descriptors. + + if {! [attribute exists $sdr 9 attr]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # Sequence of profile descriptors. Each profile descriptor + # is sequence of profile uuid and version. + set profiles {} + foreach profile [lindex $attr 1] { + set uuid [lindex $profile 1 0 1] + set version [lindex $profile 1 1 1] + lappend profiles [dict create \ + Uuid $uuid \ + Name [iocp::bt::names::to_name $uuid] \ + MajorVersion [expr {$version >> 8}] \ + MinorVersion [expr {$version & 0xff}] + ] + } + + if {$varname eq ""} { + return $profiles + } else { + upvar 1 $varname var + set var $profiles + return 1 + } + +} + +proc iocp::bt::sdr::attribute::AdditionalProtocolDescriptorList {sdr {varname {}}} { + # Returns protocol information from the additional protocols field + # in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # This command is identical to [protocols] except it extracts + # protocol information from the `AdditionalProtocolDescriptorList` + # attribute instead of `ProtocolDescriptorList`. + + if {! [attribute exists $sdr 13 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # AdditionalProtocolDescriptorList is a sequence each element of which + # is formatted like a ProtocolDescriptorList + # assert [lindex $attrval 0] eq "sequence" + set protocols [lmap elem [lindex $attrval 1] { + Protocols $elem + }] + + if {$varname eq ""} { + return $protocols + } else { + upvar 1 $varname var + set var $protocols + return 1 + } +} + + +proc iocp::bt::sdr::Uuids {sdr attr_id {varname {}}} { + if {! [attribute exists $sdr $attr_id attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + set uuids [lmap elem [lindex $attrval 1] { + set uuid [lindex $elem 1] + dict create Uuid $uuid Name [names::to_name $uuid] + }] + + if {$varname eq ""} { + return $uuids + } else { + upvar 1 $varname var + set var $uuids + return 1 + } +} + +proc iocp::bt::sdr::Protocols {attrval} { + # The attribute value may either be a single protocol stack encoded + # as a sequence, or multiple stacks encoded as a selection of sequences. + if {[lindex $attrval 0] eq "sequence"} { + # Single protocol stack + set stacks [list [lindex $attrval 1]] + } else { + # selection - Multiple stacks + set stacks [lindex $attrval 1] + } + + set protocols [lmap stack $stacks { + # stack -> {protocol_descriptor protocol_descriptor ...} + # protocol_descriptor -> {sequence {data_element ...}} + lmap protocol_descriptor $stack { + # Each descriptor is a sequence, the first element identifying the + # protocol and the remaining being (optional) parameters. + set elements [lindex $protocol_descriptor 1] + set protocol_uuid [lindex $elements 0 1] + dict create \ + Protocol $protocol_uuid \ + ProtocolName [iocp::bt::names::protocol_name $protocol_uuid] \ + ProtocolParams [lrange $elements 1 end] + } + }] + + return $protocols +} + +proc iocp::bt::sdr::AttributeValue {sdr attr_id {varname {}}} { + if {$varname eq ""} { + return [lindex [attribute raw $sdr $attr_id] 1] + } else { + if {[attribute exists $sdr $attr_id attrval]} { + upvar 1 $varname value + set value [lindex $attrval 1] + return 1 + } else { + return 0 + } + } +} + +proc iocp::bt::sdr::TextAttribute {sdr attr_offset lang {varname {}}} { + + # Assume that if sdr does not explicitly specify encoding, it is UTF8 + set enc utf-8 + set base_offset 256 + + # Primary language's ServiceName attribute index always 256 + # Otherwise get the offset from the base index 256 + if {$lang ne "primary"} { + # See if the specified language has an entry in the languages table + if {[attribute get $sdr LanguageBaseAttributeIDList base_offsets] && + [dict exists $base_offsets $lang]} { + # Yes, so the language-specific text will be at an offset + # from the base attribute index + set base_offset [dict get $base_offsets $lang BaseOffset] + set enc [dict get $base_offsets $lang Encoding] + } + } + set attr_index [expr {$base_offset + $attr_offset}] + if {! [attribute exists $sdr $attr_index name]} { + # This language does not exist. Return primary language if possible. + # If base_offset was 256, that's already primary so don't retry + if {$base_offset == 256 || + ![attribute exists $sdr [expr {256 + $attr_offset}] name] } { + if {$varname ne ""} { + return 0 + } else { + error "Attribute not found in SDR." + } + } + } + + set name [lindex $name 1] + + # $name is encoded. If Tcl supports the encoding, well and good. + # Else return a hex form + if {[string is integer $enc]} { + # SDR specified an encoding but not one that Tcl knows about + set text [binary encode hex $name] + } else { + # The string may be null terminated. TBD - always? + if {[string index $name end] eq "\0"} { + set name [string range $name 0 end-1] + } + set text [encoding convertfrom $enc $name] + } + + if {$varname ne ""} { + upvar 1 $varname var + set var $text + return 1 + } else { + return $text + } +} + +proc iocp::bt::sdr::DecodeElements {delem} { + # Decodes a Data Element as defined in the Bluetooth specification. + # delem - Data element in binary format + + set decoded {} + while {[string length $delem]} { + lassign [ExtractFirstElement $delem] type value delem + switch -exact -- $type { + sequence { lappend decoded [list sequence [DecodeElements $value]]} + selection { lappend decoded [list selection [DecodeElements $value]]} + default { + lappend decoded [list $type $value] + } + } + } + return $decoded +} + +proc iocp::bt::sdr::ExtractFirstElement {bin} { + # Extracts leading element from an SDP record. + # bin - tail of SDP record + # Returns a pair consisting of the extracted element and + # the remaining part of $bin. + + set binlen [string length $bin] + if {$binlen == 0} { + return {} + } + binary scan $bin c header + set type [expr {($header >> 3) & 0xff}] + set lenbits [expr {$header & 0x7}] + + # The nil type is formatted slightly differently. Treat as a special case. + if {$type == 0} { + if {$lenbits != 0} { + error "Invalid non-0 length for nil type." + } + return [list nil "" [string range $bin 1 end]] + } + + if {$lenbits < 5} { + set len [expr {1 << $lenbits}] + set dataoffset 1 + } else { + if {$lenbits == 5} { + set dataoffset 2 + set fmt c + set mask 0xff + } elseif {$lenbits == 6} { + set dataoffset 3 + set fmt S + set mask 0xffff + } else { + # lenbits == 7 + set dataoffset 5 + set fmt I + set mask 0xffffffff + } + # Ensure input length enough for at least length field + if {$binlen < $dataoffset} { + error "Truncated binary header." + } + binary scan $bin @1$fmt len + set len [expr {$len & $mask}]; # Since binary scan is signed + } + if {$binlen < ($dataoffset+$len)} { + error "Truncated binary data." + } + set elem [string range $bin $dataoffset [expr {$dataoffset+$len-1}]] + switch -exact $type { + 1 { + set type uint + if {$len == 1} { + binary scan $elem cu elem + } elseif {$len == 2} { + binary scan $elem Su elem + } elseif {$len == 4} { + binary scan $elem Iu elem + } elseif {$len == 8} { + binary scan $elem Wu elem + } elseif {$len == 16} { + binary scan $elem WuWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 2 { + set type int + if {$len == 1} { + binary scan $elem c elem + } elseif {$len == 2} { + binary scan $elem S elem + } elseif {$len == 4} { + binary scan $elem I elem + } elseif {$len == 8} { + binary scan $elem W elem + } elseif {$len == 16} { + # Note hi is signed, lo unsigned + binary scan $elem WWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 3 { + set type uuid + if {$len == 2} { + set elem [binary encode hex $elem] + set elem "0000$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 4} { + set elem [binary encode hex $elem] + set elem "$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 16} { + set hex [binary encode hex $elem] + set elem [string range $hex 0 7] + append elem - [string range $hex 8 11] + append elem - [string range $hex 12 15] + append elem - [string range $hex 16 19] + append elem - [string range $hex 20 end] + } else { + error "Invalid length $len for UUID." + } + } + 4 { + set type text + # TBD - keep elem as binary as we do not know encoding + } + 5 { + set type boolean + if {$len != 1} { + error "Invalid length for boolean type." + } + binary scan $elem c elem + set elem [expr {!!$elem}] ; # Convert to 0/1 + } + 6 { + set type sequence + # elem remains the same. Caller will have to recurse to parse + } + 7 { + set type selection + # elem remains the same. Caller will have to recurse to parse + } + 8 { + set type url + # elem remains same. Encoding unknown. TBD + } + default { + set type unknown + # elem remains same + } + } + + return [list $type $elem [string range $bin $dataoffset+$len end]] +} + +proc iocp::bt::sdr::printn {recs {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # recs - a list of binary SDP records in the form returned by + # [::iocp::bt::device services] or [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + set sep "" + foreach rec $recs { + puts $sep + set sep "--------------------------------------------" + print $rec $attrfilter + } +} + +proc iocp::bt::sdr::print {rec {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # rec - a binary SDP record in the form returned by [::iocp::bt::device services] or + # [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + + # Alternating attribute value pairs + set rec [decode $rec] + foreach attr [attributes $rec] { + set attrname [names::attribute_name $attr] + if {[string is integer -strict $attrname]} { + # For easier matching with Bluetooth specs + set attrname [format 0x%x $attrname] + } + if {![string match -nocase $attrfilter $attrname]} { + continue + } + switch -exact -- $attrname { + ServiceID - + ServiceRecordState - + ServiceInfoTimeToLive - + ServiceAvailability - + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceName - + ServiceDescription - + ProviderName - + ServiceRecordHandle { + puts "$attrname: [attribute get $rec $attrname]" + } + ServiceClassIDList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + # We use this for printing service-specific attributes + lappend service_class_uuids $Uuid + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + ProtocolDescriptorList { + puts "$attrname:" + foreach protocol [attribute get $rec $attrname] { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + AdditionalProtocolDescriptorList { + # Like ProtocolDescriptorList but an additional level + # of nesting. + puts "$attrname:" + foreach additional_protocol [attribute get $rec $attrname] { + foreach protocol $additional_protocol { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + } + BrowseGroupList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + LanguageBaseAttributeIDList { + puts "$attrname: sequence" + dict for {lang val} [attribute get $rec $attrname] { + puts " $lang: [dict get $val Encoding], [dict get $val BaseOffset]" + } + } + BluetoothProfileDescriptorList { + puts "$attrname: sequence" + foreach profile [attribute get $rec $attrname] { + # Sequence of profiles. Each profile is a sequence of + # profile uuid and version. + # puts profile:$profile + dict with profile { + if {$Uuid eq $Name} { + puts " $Uuid v$MajorVersion.$MinorVersion" + } else { + puts " $Uuid $Name v$MajorVersion.$MinorVersion" + } + } + } + } + default { + if {$attr >= 0x100 && $attr <= 0x1ff} { + # TBD - special handling for 0x100-0x1ff attributes by + # looking up languages table + puts "$attrname: [PrintableElement [attribute raw $rec $attr]]" + } else { + lassign [PrintableServiceSpecificAttribute \ + $service_class_uuids $rec $attr] attrname attrval + puts "$attrname: $attrval" + } + } + } + } +} + +proc iocp::bt::sdr::PrintableServiceSpecificAttribute {service_class_uuids rec attr} { + # Returns pair of human readable attribute name and value + set attrname $attr + set attrval [PrintableElement [attribute raw $rec $attr]] + + # Maybe we know about attributes for one of these services + foreach uuid $service_class_uuids { + switch -exact -- $uuid { + 00001105-0000-1000-8000-00805f9b34fb { + # ObjectPush + if {$attr == 0x200} { + set attrname GoepL2CapPsm + break + } elseif {$attr == 0x303} { + set attrname SupportedFormats + set attrval [lmap val [lindex [attribute raw $rec $attr] 1] { + # Val is a sequence of {type value} where type is int + set val [lindex $val 1] + if {$val == 255} { + set val any + } else { + set formats {unknown vcard-2.1 vcard-3.0 vcal-1.0 ical-2.0 vnote vmessage} + if {$val < [llength $formats]} { + set val [lindex $formats $val] + } + } + set val + }] + break + } + } + } + } + return [list $attrname $attrval] +} + +proc iocp::bt::sdr::PrintableElement {delem {parent_indent {}}} { + lassign $delem type val + switch -exact -- $type { + nil - + uint - + int { return $val } + uuid { + set name [names::to_name $val] + if {$name eq $val} { + return $val + } else { + return "$val $name" + } + } + text - + url { + # Assume UTF-8 binary form + # Strip terminating null. TBD - is null alwys present? + set printable [encoding convertfrom utf-8 $val] + } + boolean { return [expr {$val ? "true" : "false"}] } + selection - + sequence { + set printable [list $type] + set new_indent "${parent_indent} " + foreach elem $val { + lappend printable "${new_indent}[PrintableElement $elem $new_indent]" + } + set printable [join $printable \n] + } + unknown { + set printable [BinToHex $val] + } + } + return $printable +} diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl new file mode 100644 index 00000000..2452b04a --- /dev/null +++ b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl @@ -0,0 +1,36 @@ +# Core iocp package +package ifneeded iocp 2.0.2 \ + [list apply [list {dir} { + package require platform + set initName [string totitle iocp] + if {[package vsatisfies [package require Tcl] 9]} { + set fileName "tcl9iocp202.dll" + } else { + set fileName "iocp202.dll" + } + set platformId [platform::identify] + set searchPaths [list [file join $dir $platformId] \ + {*}[lmap platformId [platform::patterns $platformId] { + file join $dir $platformId + }] \ + $dir] + foreach path $searchPaths { + set lib [file join $path $fileName] + if {[file exists $lib]} { + uplevel #0 [list load $lib $initName] + return + } + } + error "Could not locate $fileName in directories [join $searchPaths {, }]" + }] $dir] + +# iocp_inet doesn't need anything other than core iocp +package ifneeded iocp_inet 2.0.2 \ + "package require iocp" + +if {1} { + # iocp_bt needs supporting script files + package ifneeded iocp_bt 2.0.2 \ + "[list source [file join $dir bt.tcl]]" +} + diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..09d3cf0aa9f0d82e775357fbf226283e817d89d7 GIT binary patch literal 125251 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 Qz?}EdISggrdsOa!0Bc?jNB{r; literal 0 HcmV?d00001 diff --git a/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll b/src/vfs/punk8win.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe296717af838bb5537a33cc4a6e8a58c4ab5296 GIT binary patch literal 125285 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 T0^7`neV1@w;)30ZGqwK*)4n6q literal 0 HcmV?d00001 diff --git a/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..e3c902838daa88a79277fd3fd37f93dccf7aaf44 GIT binary patch literal 109672 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 LUP_RETURN_ADDR + set rec [LookupServiceNext $h 0x100] + if {[dict exists $rec RemoteAddress] && + [dict get $rec RemoteAddress AddressFamily] == 32} { + # 32 -> AF_BTH (Bluetooth) + # Further we are looking for RFCOMM (protocol 3) + if {[dict exists $rec Protocol] && + [dict get $rec Protocol] == 3} { + return [dict get $rec RemoteAddress Port] + } + } + } + } finally { + LookupServiceEnd $h + } + error "Could not resolve service \"$service_class\" to a port on device \"$device\"." +} + +proc iocp::bt::device::remove {device} { + # Removes cached authentication information for a device from the system cache. + # device - bluetooth address or name of a device. if specified as a name, + # it must resolve to a single address. + # The command will raise an error if $device is a name that cannot be + # resolved. + + RemoveDevice [ResolveDeviceUnique $device] +} + +proc iocp::bt::device::service_references {device service} { + # Retrieve service discovery records that refer to a specified service. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service - the UUID of a service or service class or its mnemonic. + # The command will return all service discovery records that contain + # an attribute referring to the specified service. + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service discovery records. + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::to_uuid $service]] + set recs {} + try { + while {1} { + # 0x0200 -> LUP_RETURN_BLOB. Returns {Blob BINDATA} + lappend recs [lindex [LookupServiceNext $h 0x200] 1] + } + } finally { + LookupServiceEnd $h + } + return $recs +} + +proc iocp::bt::device::services {device} { + # Retrieve the service discovery records for top level services + # advertised by a device. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # + # The command will return all service discovery records that reference + # the `PublicBrowseRoot` service class. This is not necessarily all the + # services on the device, only those the device advertises as the + # top-level services. + # + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service dscovery records. + + # TBD - add a browse group parameter + # TBD - perhaps check that the sdr acually refernces browse group in + # the appropriate attribute + return [service_references $device 00001002-0000-1000-8000-00805f9b34fb] +} + +# TBD - is this needed? Less functional version of services +proc iocp::bt::device::enumerate_services {device} { + # Get installed services on a device. + # device - Bluetooth address or name of device. If specified as a name, + # it must resolve to a single address. + # Returns a list of service UUID's. + + return [EnumerateInstalledServices [ResolveDeviceUnique $device]] +} + + +proc iocp::bt::device::print {devinfo} { + # Prints device information in human-readable form to stdout. + # devinfo - A device information record as returned by + # the [devices] command. + dict with devinfo { + puts "Device $Name" + puts "Address: $Address" + puts "Class: $Class ($MajorClassName:$MinorClassName)" + puts "Device categories: ([join $DeviceClasses {, }])" + puts "Authenticated: $Authenticated" + puts "Remembered: $Remembered" + puts "Connected: $Connected" + puts "Last seen: $LastSeen" + puts "Last used: $LastUsed" + } +} + +proc iocp::bt::device::printn {dinfolist {detailed false}} { + # Prints device information in human-readable form to stdout. + # dinfolist - A list of device information records as returned by + # the [devices] command. + # detailed - If a true value, detailed information about the device + # is printed. If false (default), only the address and + # name are printed in compact form. + set sep "" + foreach dinfo $dinfolist { + if {$detailed} { + puts -nonewline $sep + set sep "----------------------------------------------\n" + print $dinfo + } else { + dict with dinfo { + puts "$Address $Name" + } + } + } +} + +proc iocp::bt::IsAddress {addr} { + # Returns boolean indicating whether addr is a valid Bluetooth address + # addr - value to be checked + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){5}$} $addr] + +} + +proc iocp::bt::Uuid16 {uuid16} { + if {![regexp {^[[:xdigit:]]{4}$} $uuid16]} { + error "\"$uuid16\" is not a valid 16 bit UUID." + } + return 0000${uuid16}-0000-1000-8000-00805f9b34fb +} + +proc iocp::bt::IsUuid {uuid} { + + # Check passed parameter for UUID syntax + + # Only checks syntax. Not RFC 4122 validity + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid] +} + +proc iocp::bt::IsBluetoothUuid {uuid} { + + # Check passed parameter for UUID syntax + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-0000-1000-8000-00805f9b34fb$} $uuid] +} + + +proc iocp::bt::BinToHex {bin {start 0} {end end}} { + regexp -inline -all .. [binary encode hex [string range $bin $start $end]] +} + +proc iocp::bt::EncodingTclName {mibenum} { + set encoding_map { + 3 ascii + 2026 big5 + 437 cp437 + 2087 cp775 + 2009 cp850 + 2010 cp852 + 2046 cp855 + 2047 cp857 + 2048 cp860 + 2049 cp861 + 2013 cp862 + 2050 cp863 + 2051 cp864 + 2052 cp865 + 2086 cp866 + 2054 cp869 + 2109 cp874 + 113 cp936 + 2250 cp1250 + 2251 cp1251 + 2252 cp1252 + 2253 cp1253 + 2254 cp1254 + 2255 cp1255 + 2256 cp1256 + 2257 cp1257 + 2258 cp1258 + 2088 ebcdic + 18 euc-jp + 38 euc-kr + 56 gb1988 + 2025 gb2312 + 57 gb2312-raw + 39 iso2022-jp + 37 iso2022-kr + 4 iso8859-1 + 5 iso8859-2 + 6 iso8859-3 + 7 iso8859-4 + 8 iso8859-5 + 9 iso8859-6 + 10 iso8859-7 + 11 iso8859-8 + 12 iso8859-9 + 13 iso8859-10 + 109 iso8859-13 + 110 iso8859-14 + 111 iso8859-15 + 112 iso8859-16 + 15 jis0201 + 63 jis0208 + 98 jis0212 + 2084 koi8-r + 2088 koi8-u + 36 ksc5601 + 17 shiftjis + 2259 tis-620 + 106 utf-8 + } + incr mibenum 0; # Convert to canonical decimal string + if {[dict exists $encoding_map $mibenum]} { + return [dict get $encoding_map $mibenum] + } + return $mibenum +} + +proc iocp::AddressFamilyName {af} { + if {$af == 2} { + return IPv4 + } elseif {$af == 23} { + return IPv6 + } elseif {$af == 32} { + return Bluetooth + } elseif {$af == 1} { + return Unix + } elseif {$af == 6} { + return IPX + } elseif {$af == 17} { + return Netbios + } else { + return AF$af + } +} + +proc DeviceClass {class} { + if {[expr {$class & 3}] != 0} { + # Not format type that we understand + return {} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/baseband/ + + set minor_class [expr {($class >> 2) & 0x3f}] + set major_class [expr {($class >> 8) & 0x1f}] + + if {$major_class == 0} { + set major_name "Miscellaneous" + set minor_name "Miscellaneous" + } elseif {$major_class == 1} { + set major_name Computer + set minor_name [lindex { + Uncategorized Desktop Server Laptop "Handheld PC/PDA" "Palm-size PC/PDA" Wearable Tablet + } $minor_class] + } elseif {$major_class == 2} { + set major_name Phone + set minor_name [lindex { + Uncategorized Cellular Cordless Smartphone "Wired modem" ISDN + } $minor_class] + } elseif {$major_class == 3} { + set major_name "LAN/Network Access point" + set minor_name [lindex { + 0% 1-17% 17-33% 33-50% 50-67% 67-83% 83-99% 100% + } $minor_class] + } elseif {$major_class == 4} { + set major_name "Audio/Video" + set minor_name [lindex { + Uncategorized "Wearable Headset Device" "Hands-free Device" Reserved + Microphone Loudspeaker Headphones "Portable Audio" "Car Audio" + "Set-top box" "HiFi Audio Device" VCR Camcorder "Video Monitor" + "Video Display and Loudspeaker" "Video Conferencing" Reserved + "Gaming/Toy" + } $minor_class] + } elseif {$major_class == 5} { + set major_name Peripheral + set bits [expr {$minor_class >> 4}] + set minor_name [lindex { + Other Keyboard "Pointing device" "Combo keyboard/pointing device" + } $bits] + set bits [expr {$minor_class & 0xf}] + if {$bits < 10} { + append minor_name /[lindex { + Uncategorized Joystick Gamepad "Remote control" "Sensing device" + "Digitizer tabler" "Card reader" "Digital pen" "Handheld scanner" + "Handheld gesture input device" + }] + } + } elseif {$major_class == 6} { + set major_name Imaging + set minor_name {} + if {$minor_class & 4} {lappend minor_name Display} + if {$minor_class & 8} {lappend minor_name Camera} + if {$minor_class & 16} {lappend minor_name Scanner} + if {$minor_class & 32} {lappend minor_name Printer} + set minor_name [join $minor_name /] + } elseif {$major_class == 7} { + set major_name Wearable + set minor_name [lindex { + Wristwatch Pager Jacket Helmet Glasses + } $minor_class] + } elseif {$major_class == 8} { + set major_name Toy + set minor_name [lindex { + Robot Vehicle Doll Controller Game + } $minor_class] + } elseif {$major_class == 9} { + set major_name Health + set minor_name [lindex { + Undefined "Blood Pressure Monitor" Thermometer "Weighing Scale" + "Glucose Meter" "Pulse Oximeter" "Heart/Pulse Rate Monitor" + "Health Data Display" "Step Counter" "Body Composition Analyzer" + "Peak Flow Monitor" "Medication Monitor" "Knee Prosthesis" + "Andle Prosthesis" "Generic Health Manager" "Personal Mobility Device" + } $minor_class] + } elseif {$major_class == 31} { + set major_name Uncategorized + set minor_name "" + } else { + set major_name Reserved + set minor_name "" + } + + set service_class {} + if {$class & (1<<13)} {lappend service_class "Limited Discoverable Mode"} + if {$class & (1<<16)} {lappend service_class "Positioning (Location identification)"} + if {$class & (1<<17)} {lappend service_class "Networking"} + if {$class & (1<<18)} {lappend service_class "Rendering"} + if {$class & (1<<19)} {lappend service_class "Capturing"} + if {$class & (1<<20)} {lappend service_class "Object Transfer"} + if {$class & (1<<21)} {lappend service_class "Audio"} + if {$class & (1<<22)} {lappend service_class "Telephony"} + if {$class & (1<<23)} {lappend service_class "Information"} + + return [dict create MajorClassName $major_name \ + MinorClassName $minor_name \ + DeviceClasses $service_class] +} + +proc iocp::bt::ResolveDeviceUnique {device} { + if {[IsAddress $device]} { + return $device + } + set addrs [device address $device] + if {[llength $addrs] == 0} { + error "Could not resolve Bluetooth device name \"$device\"." + } elseif {[llength $addrs] > 1} { + error "Device \"$device\" resolves to multiple addresses." + } + return [lindex $addrs 0] +} + +package provide iocp_bt $iocp::bt::version diff --git a/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl new file mode 100644 index 00000000..8b4c96c3 --- /dev/null +++ b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl @@ -0,0 +1,383 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::names { + namespace path [namespace parent] + # Maps UUID to Service class name + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + # UUID may be either 16-bit Bluetooth universal UUID as or the full UUID + # Uses lower case hex. + variable service_class_names { + 1000 ServiceDiscoveryServerServiceClassID + 1001 BrowseGroupDescriptorServiceClassID + 1002 PublicBrowseRoot + 1101 SerialPort + 1102 LANAccessUsingPPP + 1103 DialupNetworking + 1104 IrMCSync + 1105 OBEXObjectPush + 1106 OBEXFileTransfer + 1107 IrMCSyncCommand + 1108 Headset + 1109 CordlessTelephony + 110a AudioSource + 110b AudioSink + 110c A/V_RemoteControlTarget + 110d AdvancedAudioDistribution + 110e A/V_RemoteControl + 110f A/V_RemoteControlController + 1110 Intercom + 1111 Fax + 1112 {Headset - Audio Gateway (AG)} + 1115 PANU + 1116 NAP + 1117 GN + 1118 DirectPrinting + 1119 ReferencePrinting + 111a {Basic Imaging Profile} + 111b ImagingResponder + 111c ImagingAutomaticArchive + 111d ImagingReferencedObjects + 111e Handsfree + 111f HandsfreeAudioGateway + 1120 DirectPrintingReferenceObjectsService + 1121 ReflectedUI + 1122 BasicPrinting + 1123 PrintingStatus + 1124 HumanInterfaceDeviceService + 1125 HardcopyCableReplacement + 1126 HCR_Print + 1127 HCR_Scan + 1128 Common_ISDN_Access + 112d SIM_Access + 112e {Phonebook Access - PCE} + 112f {Phonebook Access - PSE} + 1130 {Phonebook Access} + 1131 {Headset - HS} + 1132 {Message Access Server} + 1133 {Message Notification Server} + 1134 {Message Access Profile} + 1135 GNSS + 1136 GNSS_Server + 1137 {3D Display} + 1138 {3D Glasses} + 1139 {3D Synchronization} + 113a {MPS Profile UUID} + 113b {MPS SC UUID} + 113c {CTN Access Service} + 113d {CTN Notification Service} + 113e {CTN Profile} + 1200 PnPInformation + 1201 GenericNetworking + 1202 GenericFileTransfer + 1203 GenericAudio + 1204 GenericTelephony + 1303 VideoSource + 1304 VideoSink + 1305 VideoDistribution + 1400 HDP + 1401 {HDP Source} + 1402 {HDP Sink} + 02030302-1d19-415f-86f2-22a2106a0a77 {Wireless iAP v2} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + variable protocol_names { + 0001 SDP + 0002 UDP + 0003 RFCOMM + 0004 TCP + 0005 TCS-BIN + 0006 TCS-AT + 0007 ATT + 0008 OBEX + 0009 IP + 000a FTP + 000c HTTP + 000e WSP + 000f BNEP + 0010 UPNP + 0011 HIDP + 0012 HardcopyControlChannel + 0014 HardcopyDataChannel + 0016 HardcopyNotification + 0017 AVCTP + 0019 AVDTP + 001b CMTP + 001e MCAPControlChannel + 001f MCAPDataChannel + 0100 L2CAP + } + + # Universal attribute names + variable attribute_names + set attribute_names { + ServiceRecordHandle 0 + ServiceClassIDList 1 + ServiceRecordState 2 + ServiceID 3 + ProtocolDescriptorList 4 + BrowseGroupList 5 + LanguageBaseAttributeIDList 6 + ServiceInfoTimeToLive 7 + ServiceAvailability 8 + BluetoothProfileDescriptorList 9 + DocumentationURL 10 + ClientExecutableURL 11 + IconURL 12 + AdditionalProtocolDescriptorList 13 + ServiceName 256 + ServiceDescription 257 + ProviderName 258 + } + +} + +proc iocp::bt::names::is_uuid {uuid} { + regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid +} + +proc iocp::bt::names::to_name {uuid} { + # Map a UUID to a name. + # uuid - Bluetooth UUID + # The command attempts to map the UUID as a service name or protocol + # name in that order. + # + # Returns the mapped name or the uuid if no mapping is possible. + + set name [service_class_name $uuid] + if {$name ne $uuid && ![is_uuid $name]} { + return $name + } + return [protocol_name $uuid] +} + +proc iocp::bt::names::to_uuid {name_or_uuid} { + # Map a name to a UUID. + # name_or_uuid - Bluetooth UUID or name + # The command attempts to map the name as a service class name or protocol + # name in that order. + # + # Returns the mapped uuid raises an error if no mapping is possible. + variable service_class_names + if {[MapNameToUuidV $name_or_uuid service_class_names name]} { + return $name + } + return [protocol_uuid $name_or_uuid] +} + + +proc iocp::bt::names::service_class_name {uuid} { + # Maps a UUID to a service class name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable service_class_names + set name [MapUuidToName $uuid service_class_names] + if {$name ne $uuid} { + return $name + } + # Well known private uuids + if {[string equal -nocase "02030302-1d19-415f-86f2-22a2106a0a77" $uuid]} { + return "Wireless iAP v2"; # iPod Accessory Protocol + } + return $uuid +} + +proc iocp::bt::names::service_class_uuid {name} { + # Maps a service class name to a UUID. + # name - Service class name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable service_class_names + return [MapNameToUuid $name service_class_names] +} + +proc iocp::bt::names::profile_name {uuid} { + # Maps a UUID to a Bluetooth profile name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + + # Profiles seem to come from same service class UUID space. + return [service_class_name $uuid] +} + +proc iocp::bt::names::protocol_name {uuid} { + # Maps a UUID to a protocol name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable protocol_names + return [MapUuidToName $uuid protocol_names] +} + +proc iocp::bt::names::protocol_uuid {name} { + # Maps a protocol name to a UUID. + # name - Protocol name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable protocol_names + return [MapNameToUuid $name protocol_names] +} + +proc iocp::bt::names::attribute_name {attr_id} { + # Maps a attribute id (numeric or name) to a attribute name. + # attr_id - numeric attribute identifier + # Returns a human-readable name for the attribute or + # the numeric id itself if no mapping could be performed. + variable attribute_names + dict for {name id} $attribute_names { + if {$attr_id == $id || [string equal -nocase $name $attr_id]} { + return $name + } + } + if {[string is integer -strict $attr_id]} { + return $attr_id + } + error "Unknown attribute \"$attr_id\"." +} + +proc iocp::bt::names::attribute_id {name} { + # Maps a attribute name to it numeric attribute identifier. + # name - Attribute name to be mapped + # The command will raise an error if the name is unknown. + # Returns the numeric attribute identifier corresponding to + # the passed name. + variable attribute_names + if {[string is integer -strict $name]} { + return $name + } + return [dict get $attribute_names $name] +} + +proc iocp::bt::names::print {} { + # Prints known UUID's and their mapped mnemonics. + variable service_class_names + dict for {uuid16 name} $service_class_names { + puts "[Uuid16 $uuid16] $name" + } +} + +proc iocp::bt::names::MapUuidToName {uuid dictvar} { + upvar 1 $dictvar names + + # uuid may be a full UUID or a 16-bit Bluetooth UUID + if {[IsUuid $uuid]} { + # See if the exact match exists + set uuid [string tolower $uuid] + if {[dict exists $names $uuid]} { + return [dict get $names $uuid] + } + # Nope. Now see if it is a 16-bit Bluetooth UUID + if {[IsBluetoothUuid $uuid] && [string range $uuid 0 3] eq "0000"} { + # 16-bit UUID + set uuid16 [string tolower [string range $uuid 4 7]] + if {[dict exists $names $uuid16]} { + return [dict get $names $uuid16] + } + } + # Nope. Cannot be mapped. Return the UUID. + return $uuid + } + + # Not a full UUID. Retry as a 16-but UUID. tailcall so upvar works right. + tailcall MapUuidToName [Uuid16 $uuid] $dictvar +} + +proc iocp::bt::names::MapNameToUuidV {name dictvar resultvar} { + upvar 1 $dictvar names + upvar 1 $resultvar result + + if {[IsUuid $name]} { + set result $name + return 1 + } + dict for {uuid mapped_name} $names { + if {[string equal -nocase $name $mapped_name]} { + if {[string length $uuid] == 4} { + # 16-bit UUID -> full UUID + set result "0000${uuid}-0000-1000-8000-00805f9b34fb" + } else { + set result $uuid + } + return 1 + } + } + return 0 +} + +proc iocp::bt::names::MapNameToUuid {name dictvar} { + upvar 1 $dictvar names + if {[MapNameToUuidV $name names result]} { + return $result + } + error "Name \"$name\" could not be mapped to a UUID" +} + +proc iocp::bt::names::Asciify {svar} { + # Replaces non-ascii or non-printable chars with Tcl sequences + # + # Transformed string is stored back in svar. + + upvar 1 $svar s + set ascii "" + set was_escaped 0 + # Split can be expensive for large strings so string index + set len [string length $s] + for {set i 0} {$i < $len} {incr i} { + set char [string index $s $i] + scan $char %c codepoint + if {$codepoint < 32 || $codepoint >= 126} { + append ascii "\\u[format %.4x $codepoint]" + set was_escaped 1 + } else { + append ascii $char + } + } + set s $ascii + return $was_escaped +} + +proc iocp::bt::names::ClipSpec {{use_list 0}} { + package require twapi + + set csv [twapi::read_clipboard_text] + if {$use_list} { + set tcl "\[list \n" + } else { + set tcl "\{ \n" + } + foreach line [split $csv \n] { + lassign [split $line \t] name uuid description + set name [string trim $name] + set uuid [string trim $uuid] + if {![regexp {^0x[[:xdigit:]]{4}} $uuid]} continue + if {$use_list} { + # Unicode chars, needs escaping + Asciify name + append tcl " " [list [string range $uuid 2 end]] " " "\"$name\"" " \\" \n + } else { + if {[Asciify name]} { + # Need to use the list command. Just restart + return [ScrapeBTSpec 1] + } + append tcl " " [string range $uuid 2 end] " " "{$name}" \n + } + } + if {$use_list} { + append tcl "\]\n" + } else { + append tcl "\}" + } + + twapi::write_clipboard_text $tcl +} + diff --git a/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl new file mode 100644 index 00000000..cd014a1b --- /dev/null +++ b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl @@ -0,0 +1,962 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::sdr { + namespace path [namespace parent] + namespace export attribute attributes decode print printn + + namespace eval attribute { + namespace path [list [namespace parent] [namespace parent [namespace parent]]] + namespace export exists get raw text + namespace ensemble create + } +} + +proc iocp::bt::sdr::decode {binsdr} { + # Decodes a binary service discovery record + # binsdr - a raw Bluetooth service discovery record in binary + # form. + # + # The returned value should be treated as opaque. The attributes + # stored in the record should be accessed with the commands in + # the `sdr` namespace. + # + # Returns a container of attributes stored in the record. + + # A SDR record is a single data element which is a sequence + # containing nested data elements. + # {{sequence {nested data elements}}} + set rec [lindex [DecodeElements $binsdr] 0 1] + + # $rec contains alternating attribute value pairs. The result + # is built as a list as its faster to do that and shimmer + # to a dict on access than to build a dictionary to begin with. + set sdr {} + foreach {attr val} $rec { + lappend sdr [lindex $attr 1] $val + } + return $sdr +} + +proc iocp::bt::sdr::attribute::exists {sdr attr_id {varname {}}} { + # Checks if an attribute exists in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # attr_id - attribute integer id or Bluetooth universal attribute name + # varname - If not the empty string, the attribute + # value is stored in a variable of this name in the + # caller's context. + # + # The value returned in $varname is the attribute type-independent data + # value. To get the value formatted for the attribute type, use the + # [attribute get] command. + # + # Returns 1 if the attribute exists, and 0 otherwise. + + if {[string is integer -strict $attr_id]} { + set key [expr {$attr_id + 0}]; # Force decimal rep. Faster than format + } else { + set key [names::attribute_id $attr_id]; # name -> id + } + + if {[dict exists $sdr $key]} { + if {$varname ne ""} { + upvar 1 $varname value + set value [dict get $sdr $key] + } + return 1 + } + + return 0 +} + +proc iocp::bt::sdr::attributes {sdr} { + # Get the list of attributes in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # Returns a list of numeric attribute ids. + return [dict keys $sdr] +} + +proc iocp::bt::sdr::attribute::raw {sdr attr_id} { + # Get an attribute value from an service discovery record. + # sdr - a decoded service discovery record in the form returned by + # sdr_decode. + # attr_id - attribute integer id + # + # The command will raise an error if the attribute does not exist + # in the sdr. + # + # Returns the attribute value as a pair consisting of the type and + # the raw data element value. + + if {[exists $sdr $attr_id value]} { + return $value + } else { + error "Attribute with id \"$attr_id\" not found." + } +} + +proc iocp::bt::sdr::attribute::get {sdr attr_id {varname {}}} { + # Get the value of an universal attribute from a service + # discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # varname - Optional name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The attribute value is decoded into a attribute type-dependent format. + # + # The $attr_id argument must be one of those defined in + # the *Universal Attributes* section in the + # [Bluetooth Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/) + # specification. Otherwise an error is raised. The recognized attributes + # are listed below. Refer to the Bluetooth specification for their + # exact semantics. + # + # AdditionalProtocolDescriptorList - List of protocol stacks. This + # supplements the `ProtocolDescriptorList` attribute. + # BluetoothProfileDescriptorList - List of Bluetooth profile descriptors + # to which the service conforms. Each element is a dictionary + # with keys `Uuid`, `Name`, `MajorVersion` and `MinorVersion` + # corresponding to the referenced profile. + # BrowseGroupList - List of browse group descriptors representing + # the browse groups to which the service record belongs. Each + # descriptor is dictionary with keys `Uuid` and `Name` identifying + # the a browse group. + # ClientExecutableURL - URL pointing to the client application that + # may be used to utilize the service. + # DocumentationURL - URL for the service documentation. + # IconURL - URL for icon that may be used to represent the service. + # LanguageBaseAttributeIDList - Nested dictionary of language-specific + # attribute offsets (see below). + # ProtocolDescriptorList - List of protocol stacks (see below) that may + # be used to access the service described by the service record. + # ProviderName - Name of entity providing the service in the primary + # language of the service record. + # ServiceAvailability - A value in the range 0-255 that indicates + # relative availability of the service in terms of the number of + # clients it can accept. Note this is a scaled estimate. + # ServiceClassIDList - List with each element being a dictionary + # corresponding to a service class that the record conforms to. + # The keys of the dictionary are `Name` and `Uuid`. + # ServiceDescription - A description of the service in the primary + # language of the record. + # ServiceID - UUID that uniquely identifies a service instance. + # ServiceInfoTimeToLive - The number of seconds from the time it was + # generated that the service record information is expected to be valid. + # ServiceName - The human-readable name of the service in the primary + # language of the record. + # ServiceRecordHandle - A numeric value that uniquely identifies a service + # record within a SDP server. + # ServiceRecordState - Numeric value that is changed any time the service + # record is modified by the SDP server. + # + # In the case of ProtocolDescriptorList and AdditionalProtocolDescriptorList, + # the attribute value is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # In the case of LanguageBaseAttributeIDList, the attribute value is + # a dictionary indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. + # + # Returns a boolean or the decoded attribute value. + + set attr_name [names::attribute_name $attr_id] + set attr_id [names::attribute_id $attr_id] + switch -exact -- $attr_name { + BrowseGroupList - + ServiceClassIDList { + tailcall Uuids $sdr $attr_id $varname + } + BluetoothProfileDescriptorList - + ProtocolDescriptorList - + AdditionalProtocolDescriptorList - + LanguageBaseAttributeIDList { + tailcall $attr_name $sdr $varname + } + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceAvailability - + ServiceInfoTimeToLive - + ServiceID - + ServiceRecordState - + ServiceRecordHandle { + tailcall AttributeValue $sdr $attr_id $varname + } + ServiceName - + ServiceDescription - + ProviderName { + # Special case because they need a language argument + tailcall text $sdr $attr_name primary $varname + } + default { + if {[llength [info commands [namespace current]::universal::$attr_name]]} { + tailcall [namespace current]::universal::$attr_name $sdr $varname + } + } + } + error "Unknown universal attribute \"$attr_id\"." +} + +proc iocp::bt::sdr::attribute::text {sdr attr_id lang {varname {}}} { + # Get the value of an text attribute in the specified language + # from a service discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # lang - language identifier as specified in iso639, e.g. `en` for + # english, `fr` for french etc. or the keyword `primary`. + # varname - Name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # If the record does not contain a value for the specified language, + # the value for the primary language will be retrieved. + # + # Returns a boolean or the attribute text value. + # + set lang_offset [names::attribute_id $attr_id] + if {$lang_offset < 256 || $lang_offset >= 512} { + error "Invalid text attribute id \"$attr_id\"." + } + incr lang_offset -256 + tailcall TextAttribute $sdr $lang_offset $lang $varname +} + + +proc iocp::bt::sdr::attribute::LanguageBaseAttributeIDList {sdr {varname {}}} { + # Retrieve the language base attributes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a nested dictionary + # corresponding to the `LanguageBaseAttributeIDList` + # universal attribute in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The dictionary value returned or stored is indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. fs + # + # Returns a boolean or a dictionary of language attributes. + if {![AttributeValue $sdr 6 sequence]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # attr is {sequence {langid encoding baseoffset ...}} + set langs [dict create] + foreach {langid enc offset} $sequence { + # langid is ISO 639 language code + set langid [lindex $langid 1] + set langid "[format %c [expr {$langid >> 8}]][format %c [expr {$langid &0xff}]]" + dict set langs $langid Encoding [::iocp::bt::EncodingTclName [lindex $enc 1]] + dict set langs $langid BaseOffset [lindex $offset 1] + } + if {$varname eq ""} { + return $langs + } else { + upvar 1 $varname var + set var $langs + return 1 + } +} + + +proc iocp::bt::sdr::attribute::ProtocolDescriptorList {sdr {varname {}}} { + # Retrieve the protocol information from a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # Information about the protocols that may be used to access the + # service described by SDR is stored in the ProtocolDescriptorList + # universal attribute as described in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The returned information is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # Returns a boolean or the list of protocols. + + if {! [attribute exists $sdr 4 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + if {$varname eq ""} { + return [Protocols $attrval] + } else { + upvar 1 $varname var + set var [Protocols $attrval] + return 1 + } +} + +proc iocp::bt::sdr::attribute::BluetoothProfileDescriptorList {sdr {varname {}}} { + # Retrieve the service classes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a list of profile descriptors + # corresponding to the BluetoothProfileDescriptorList + # universal attribute in the Bluetooth Core specification. + # Each descriptor is a dictionary with the following keys: + # `Uuid` - the UUID of the profile + # `Name` - the mnemonic name of the profile or its UUID if the + # UUID cannot be mapped to a name + # `MajorVersion` - the profile's major version + # `MinorVersion` - the profile's minor version + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # Returns a boolean or the list of service class descriptors. + + if {! [attribute exists $sdr 9 attr]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # Sequence of profile descriptors. Each profile descriptor + # is sequence of profile uuid and version. + set profiles {} + foreach profile [lindex $attr 1] { + set uuid [lindex $profile 1 0 1] + set version [lindex $profile 1 1 1] + lappend profiles [dict create \ + Uuid $uuid \ + Name [iocp::bt::names::to_name $uuid] \ + MajorVersion [expr {$version >> 8}] \ + MinorVersion [expr {$version & 0xff}] + ] + } + + if {$varname eq ""} { + return $profiles + } else { + upvar 1 $varname var + set var $profiles + return 1 + } + +} + +proc iocp::bt::sdr::attribute::AdditionalProtocolDescriptorList {sdr {varname {}}} { + # Returns protocol information from the additional protocols field + # in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # This command is identical to [protocols] except it extracts + # protocol information from the `AdditionalProtocolDescriptorList` + # attribute instead of `ProtocolDescriptorList`. + + if {! [attribute exists $sdr 13 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # AdditionalProtocolDescriptorList is a sequence each element of which + # is formatted like a ProtocolDescriptorList + # assert [lindex $attrval 0] eq "sequence" + set protocols [lmap elem [lindex $attrval 1] { + Protocols $elem + }] + + if {$varname eq ""} { + return $protocols + } else { + upvar 1 $varname var + set var $protocols + return 1 + } +} + + +proc iocp::bt::sdr::Uuids {sdr attr_id {varname {}}} { + if {! [attribute exists $sdr $attr_id attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + set uuids [lmap elem [lindex $attrval 1] { + set uuid [lindex $elem 1] + dict create Uuid $uuid Name [names::to_name $uuid] + }] + + if {$varname eq ""} { + return $uuids + } else { + upvar 1 $varname var + set var $uuids + return 1 + } +} + +proc iocp::bt::sdr::Protocols {attrval} { + # The attribute value may either be a single protocol stack encoded + # as a sequence, or multiple stacks encoded as a selection of sequences. + if {[lindex $attrval 0] eq "sequence"} { + # Single protocol stack + set stacks [list [lindex $attrval 1]] + } else { + # selection - Multiple stacks + set stacks [lindex $attrval 1] + } + + set protocols [lmap stack $stacks { + # stack -> {protocol_descriptor protocol_descriptor ...} + # protocol_descriptor -> {sequence {data_element ...}} + lmap protocol_descriptor $stack { + # Each descriptor is a sequence, the first element identifying the + # protocol and the remaining being (optional) parameters. + set elements [lindex $protocol_descriptor 1] + set protocol_uuid [lindex $elements 0 1] + dict create \ + Protocol $protocol_uuid \ + ProtocolName [iocp::bt::names::protocol_name $protocol_uuid] \ + ProtocolParams [lrange $elements 1 end] + } + }] + + return $protocols +} + +proc iocp::bt::sdr::AttributeValue {sdr attr_id {varname {}}} { + if {$varname eq ""} { + return [lindex [attribute raw $sdr $attr_id] 1] + } else { + if {[attribute exists $sdr $attr_id attrval]} { + upvar 1 $varname value + set value [lindex $attrval 1] + return 1 + } else { + return 0 + } + } +} + +proc iocp::bt::sdr::TextAttribute {sdr attr_offset lang {varname {}}} { + + # Assume that if sdr does not explicitly specify encoding, it is UTF8 + set enc utf-8 + set base_offset 256 + + # Primary language's ServiceName attribute index always 256 + # Otherwise get the offset from the base index 256 + if {$lang ne "primary"} { + # See if the specified language has an entry in the languages table + if {[attribute get $sdr LanguageBaseAttributeIDList base_offsets] && + [dict exists $base_offsets $lang]} { + # Yes, so the language-specific text will be at an offset + # from the base attribute index + set base_offset [dict get $base_offsets $lang BaseOffset] + set enc [dict get $base_offsets $lang Encoding] + } + } + set attr_index [expr {$base_offset + $attr_offset}] + if {! [attribute exists $sdr $attr_index name]} { + # This language does not exist. Return primary language if possible. + # If base_offset was 256, that's already primary so don't retry + if {$base_offset == 256 || + ![attribute exists $sdr [expr {256 + $attr_offset}] name] } { + if {$varname ne ""} { + return 0 + } else { + error "Attribute not found in SDR." + } + } + } + + set name [lindex $name 1] + + # $name is encoded. If Tcl supports the encoding, well and good. + # Else return a hex form + if {[string is integer $enc]} { + # SDR specified an encoding but not one that Tcl knows about + set text [binary encode hex $name] + } else { + # The string may be null terminated. TBD - always? + if {[string index $name end] eq "\0"} { + set name [string range $name 0 end-1] + } + set text [encoding convertfrom $enc $name] + } + + if {$varname ne ""} { + upvar 1 $varname var + set var $text + return 1 + } else { + return $text + } +} + +proc iocp::bt::sdr::DecodeElements {delem} { + # Decodes a Data Element as defined in the Bluetooth specification. + # delem - Data element in binary format + + set decoded {} + while {[string length $delem]} { + lassign [ExtractFirstElement $delem] type value delem + switch -exact -- $type { + sequence { lappend decoded [list sequence [DecodeElements $value]]} + selection { lappend decoded [list selection [DecodeElements $value]]} + default { + lappend decoded [list $type $value] + } + } + } + return $decoded +} + +proc iocp::bt::sdr::ExtractFirstElement {bin} { + # Extracts leading element from an SDP record. + # bin - tail of SDP record + # Returns a pair consisting of the extracted element and + # the remaining part of $bin. + + set binlen [string length $bin] + if {$binlen == 0} { + return {} + } + binary scan $bin c header + set type [expr {($header >> 3) & 0xff}] + set lenbits [expr {$header & 0x7}] + + # The nil type is formatted slightly differently. Treat as a special case. + if {$type == 0} { + if {$lenbits != 0} { + error "Invalid non-0 length for nil type." + } + return [list nil "" [string range $bin 1 end]] + } + + if {$lenbits < 5} { + set len [expr {1 << $lenbits}] + set dataoffset 1 + } else { + if {$lenbits == 5} { + set dataoffset 2 + set fmt c + set mask 0xff + } elseif {$lenbits == 6} { + set dataoffset 3 + set fmt S + set mask 0xffff + } else { + # lenbits == 7 + set dataoffset 5 + set fmt I + set mask 0xffffffff + } + # Ensure input length enough for at least length field + if {$binlen < $dataoffset} { + error "Truncated binary header." + } + binary scan $bin @1$fmt len + set len [expr {$len & $mask}]; # Since binary scan is signed + } + if {$binlen < ($dataoffset+$len)} { + error "Truncated binary data." + } + set elem [string range $bin $dataoffset [expr {$dataoffset+$len-1}]] + switch -exact $type { + 1 { + set type uint + if {$len == 1} { + binary scan $elem cu elem + } elseif {$len == 2} { + binary scan $elem Su elem + } elseif {$len == 4} { + binary scan $elem Iu elem + } elseif {$len == 8} { + binary scan $elem Wu elem + } elseif {$len == 16} { + binary scan $elem WuWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 2 { + set type int + if {$len == 1} { + binary scan $elem c elem + } elseif {$len == 2} { + binary scan $elem S elem + } elseif {$len == 4} { + binary scan $elem I elem + } elseif {$len == 8} { + binary scan $elem W elem + } elseif {$len == 16} { + # Note hi is signed, lo unsigned + binary scan $elem WWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 3 { + set type uuid + if {$len == 2} { + set elem [binary encode hex $elem] + set elem "0000$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 4} { + set elem [binary encode hex $elem] + set elem "$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 16} { + set hex [binary encode hex $elem] + set elem [string range $hex 0 7] + append elem - [string range $hex 8 11] + append elem - [string range $hex 12 15] + append elem - [string range $hex 16 19] + append elem - [string range $hex 20 end] + } else { + error "Invalid length $len for UUID." + } + } + 4 { + set type text + # TBD - keep elem as binary as we do not know encoding + } + 5 { + set type boolean + if {$len != 1} { + error "Invalid length for boolean type." + } + binary scan $elem c elem + set elem [expr {!!$elem}] ; # Convert to 0/1 + } + 6 { + set type sequence + # elem remains the same. Caller will have to recurse to parse + } + 7 { + set type selection + # elem remains the same. Caller will have to recurse to parse + } + 8 { + set type url + # elem remains same. Encoding unknown. TBD + } + default { + set type unknown + # elem remains same + } + } + + return [list $type $elem [string range $bin $dataoffset+$len end]] +} + +proc iocp::bt::sdr::printn {recs {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # recs - a list of binary SDP records in the form returned by + # [::iocp::bt::device services] or [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + set sep "" + foreach rec $recs { + puts $sep + set sep "--------------------------------------------" + print $rec $attrfilter + } +} + +proc iocp::bt::sdr::print {rec {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # rec - a binary SDP record in the form returned by [::iocp::bt::device services] or + # [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + + # Alternating attribute value pairs + set rec [decode $rec] + foreach attr [attributes $rec] { + set attrname [names::attribute_name $attr] + if {[string is integer -strict $attrname]} { + # For easier matching with Bluetooth specs + set attrname [format 0x%x $attrname] + } + if {![string match -nocase $attrfilter $attrname]} { + continue + } + switch -exact -- $attrname { + ServiceID - + ServiceRecordState - + ServiceInfoTimeToLive - + ServiceAvailability - + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceName - + ServiceDescription - + ProviderName - + ServiceRecordHandle { + puts "$attrname: [attribute get $rec $attrname]" + } + ServiceClassIDList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + # We use this for printing service-specific attributes + lappend service_class_uuids $Uuid + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + ProtocolDescriptorList { + puts "$attrname:" + foreach protocol [attribute get $rec $attrname] { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + AdditionalProtocolDescriptorList { + # Like ProtocolDescriptorList but an additional level + # of nesting. + puts "$attrname:" + foreach additional_protocol [attribute get $rec $attrname] { + foreach protocol $additional_protocol { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + } + BrowseGroupList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + LanguageBaseAttributeIDList { + puts "$attrname: sequence" + dict for {lang val} [attribute get $rec $attrname] { + puts " $lang: [dict get $val Encoding], [dict get $val BaseOffset]" + } + } + BluetoothProfileDescriptorList { + puts "$attrname: sequence" + foreach profile [attribute get $rec $attrname] { + # Sequence of profiles. Each profile is a sequence of + # profile uuid and version. + # puts profile:$profile + dict with profile { + if {$Uuid eq $Name} { + puts " $Uuid v$MajorVersion.$MinorVersion" + } else { + puts " $Uuid $Name v$MajorVersion.$MinorVersion" + } + } + } + } + default { + if {$attr >= 0x100 && $attr <= 0x1ff} { + # TBD - special handling for 0x100-0x1ff attributes by + # looking up languages table + puts "$attrname: [PrintableElement [attribute raw $rec $attr]]" + } else { + lassign [PrintableServiceSpecificAttribute \ + $service_class_uuids $rec $attr] attrname attrval + puts "$attrname: $attrval" + } + } + } + } +} + +proc iocp::bt::sdr::PrintableServiceSpecificAttribute {service_class_uuids rec attr} { + # Returns pair of human readable attribute name and value + set attrname $attr + set attrval [PrintableElement [attribute raw $rec $attr]] + + # Maybe we know about attributes for one of these services + foreach uuid $service_class_uuids { + switch -exact -- $uuid { + 00001105-0000-1000-8000-00805f9b34fb { + # ObjectPush + if {$attr == 0x200} { + set attrname GoepL2CapPsm + break + } elseif {$attr == 0x303} { + set attrname SupportedFormats + set attrval [lmap val [lindex [attribute raw $rec $attr] 1] { + # Val is a sequence of {type value} where type is int + set val [lindex $val 1] + if {$val == 255} { + set val any + } else { + set formats {unknown vcard-2.1 vcard-3.0 vcal-1.0 ical-2.0 vnote vmessage} + if {$val < [llength $formats]} { + set val [lindex $formats $val] + } + } + set val + }] + break + } + } + } + } + return [list $attrname $attrval] +} + +proc iocp::bt::sdr::PrintableElement {delem {parent_indent {}}} { + lassign $delem type val + switch -exact -- $type { + nil - + uint - + int { return $val } + uuid { + set name [names::to_name $val] + if {$name eq $val} { + return $val + } else { + return "$val $name" + } + } + text - + url { + # Assume UTF-8 binary form + # Strip terminating null. TBD - is null alwys present? + set printable [encoding convertfrom utf-8 $val] + } + boolean { return [expr {$val ? "true" : "false"}] } + selection - + sequence { + set printable [list $type] + set new_indent "${parent_indent} " + foreach elem $val { + lappend printable "${new_indent}[PrintableElement $elem $new_indent]" + } + set printable [join $printable \n] + } + unknown { + set printable [BinToHex $val] + } + } + return $printable +} diff --git a/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl new file mode 100644 index 00000000..2452b04a --- /dev/null +++ b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl @@ -0,0 +1,36 @@ +# Core iocp package +package ifneeded iocp 2.0.2 \ + [list apply [list {dir} { + package require platform + set initName [string totitle iocp] + if {[package vsatisfies [package require Tcl] 9]} { + set fileName "tcl9iocp202.dll" + } else { + set fileName "iocp202.dll" + } + set platformId [platform::identify] + set searchPaths [list [file join $dir $platformId] \ + {*}[lmap platformId [platform::patterns $platformId] { + file join $dir $platformId + }] \ + $dir] + foreach path $searchPaths { + set lib [file join $path $fileName] + if {[file exists $lib]} { + uplevel #0 [list load $lib $initName] + return + } + } + error "Could not locate $fileName in directories [join $searchPaths {, }]" + }] $dir] + +# iocp_inet doesn't need anything other than core iocp +package ifneeded iocp_inet 2.0.2 \ + "package require iocp" + +if {1} { + # iocp_bt needs supporting script files + package ifneeded iocp_bt 2.0.2 \ + "[list source [file join $dir bt.tcl]]" +} + diff --git a/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..09d3cf0aa9f0d82e775357fbf226283e817d89d7 GIT binary patch literal 125251 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 Qz?}EdISggrdsOa!0Bc?jNB{r; literal 0 HcmV?d00001 diff --git a/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll b/src/vfs/punk8_statictwapi.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe296717af838bb5537a33cc4a6e8a58c4ab5296 GIT binary patch literal 125285 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 T0^7`neV1@w;)30ZGqwK*)4n6q literal 0 HcmV?d00001 diff --git a/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll b/src/vfs/punk86.vfs/lib_tcl8/iocp-2.0.2/win32-x86_64/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..e3c902838daa88a79277fd3fd37f93dccf7aaf44 GIT binary patch literal 109672 zcmdSCdwf*Y)&D;U83;<8piE1x)Un1It@tD=l_<0`B*2Ld8Y?PRs#uWHib{2&_(&yj z67}?SH2T=qwmz*^Yi(<*t*rvyLO>#3FoHLv-fw4&R!~a;Tj%%w?0wG61hL=W_s=gc z%{gbEz1LoQ?X}l!ud`>yH+mw&B9TZH{}&38$TF__mo$I>^Pdi0@3HTSJtBV@`S^az zD$*vE=Vjo|GaOXf95%fxo4hx?s=KSx6VqO-+pf5JLe`^jz2MR&Uy3BI<%%{ zx4Hmy+tf&8-sQVTuK4n?-;T_yh&Yvz$mYnGzPNj3I!fDi$-|Rzs=OA@TuXIj7yCZrr5UCtpQTJ6iaTbh@|y!XGLW89=Zws0phyd z!w0S}S40v({j`6H=@pUsUA(;VF4`Hyckz;0$e;vZ_EDO)j{6^|&`qD$|Ct<@wzhmo}Ibm)L zZI1gyz1~BczdjQPky-lF8^Ak#7xgj=4St3T27-EXc2V#AvlgB=H)7}d<)GfTK2dLq zy8D0mpOdTEeqKc+|KYYsB(`rdk~%3h^Q2QwKGDg(Q}4J>%y1v-Uf=%TUi2L*b@yI+ zYSyoCym4D6RYV%!eNg0ZqfhcG?H#b==8DK*^>QC3PZgeWTz|6@`_s{myTrXrVjBlo9r@px=wr`gqYj#IP`%7mz zUey5&Jk#|hKykc9)s64c=jG}%F1($lHLD<|I4-@`_ZuW%k$sRm^QM%$#>0UEb_b%8M}mSpKl(UjU4$$N7hb)5m;7H#RX*!Yz`&>f<4Ap^xJ} z0!!SwkR`S9ACX8842G(|OGcvmaMv5*@F9 z>^3h{>3Vn$Zni8?od1r;Dh6LH16hpRx9w;JbW)-^1zgg}LO6=1zZ0 zozdrMuO>m0#`TSBDN67vlFmhE?4u^9Sdi5r$d}a6i}p-Zeegb#e*2?azKi5AZz6@B zZS=)BJ68+$(@rnak7lRR%<1nFva~%7{3MFJ>lYwi3O&+U@U!Fg6*k-}aOF@WFKRBP zpF;eu|5dx_s3E}G1gt-LA~p2y)8G!?T9Qw>`1a4yh!u1b>TvLf!MbEp_7gD5UCQaU z(sVmX-7?ni*d7Y;vze0oiHyarTQgr*n!d*^I#P;bFUNf*?LG|SR%9Am-QIMrW{@6N z2ms(yg=^cYu}jTk=V~>DTqXR9gt~`DR5tXPTQd^`9G8iL$qGGfF<=#I{@e_Rc{T5X z1_uTW8g|j(qM*UV@&-E=*dCI*XmDcCz$tI=c+jA27Yz;#8q6whaAVM5&Mq445j0p( z-r$0u!J<+F7(20CHl2vEnDQbciJ~DRvB*lKEiEqOL0}1Z6IM;Ah%DDiaJ=D#HrDbg z$6Z;7-~Mx`;0>Q2d#|^@d8{TWHoCdG(6b1s=eW;%H6MLjWNJ`O|^xucY1QNeHD^lJ-w#d$=j&c{;GNF=F^@S(A`n!>0pGmkg5YzU1pYd z=jtJtv}nw-yPgae1l$;J1tq&KHhe}4W>2#;hO7$+r6pn7uddHecV*0(amkQNc+rdJ zOX;_d4g_{-{XlcCW;$=JoGipY|1-@}%_NFedBt zio0{eg0JSHf0Xq{KR7FRh@S&{gne54yqZ^dyW$To3iLAo8~k0_^!8YO^J-R837hR9 zDilyct7M8HDK`4MZ`Hg98e&zCSMxJ!Qbq5*nw))bR}F5k^ze_yEEs%~hc1Jk=+TgU zwYq&c+|_W}D&Lcs%`UD6=&H<@keJUOhI$W#zLQHcfuX!dTo!sJB89=jw9c>l5M4B6 zy%hDG2m2+f;Z0#ySr}k29YA(+->)6~QmKjKIlhyd)$4Sx%?vk7izy#2j%sTV+a7{L z1BCWxo!pFi|8)8@uk!$J2(_eENI1UKDXEjoY(kUuX)yiG#uIb;rzJH%&e7*i zY`>YmPUe4sw0taOM%tX~&?N2Fx5|82RO`^6QtcC`~a5vkKt zXP{U+1Xvv=Xp^5URaBzCwRe(!7iI@_z6HdmUv5I^oGxD3r>!7?ErQc=gr19CUI z&13z6_l3iR-!_OhspQSzU(5Ul_ff}vw#8jDRaTN#PmB9d_Ko4$e-Dp!J;Y-=cP8`r zihr>IISg*iHn&xzTpzh#XsdPmlUvtRW~#EsE=*?MtTHzVui13lOt(w-VL1L{7}@SN zXp|T#j|F(q7Ob10?^r+ILq2ZttBRe{TWXo2AoSVaJX3)Lx1WBPH0%Z@4K`JOm-eXL zKENEjsx*HDVuOA9|ix$F~(V9~J8)9^AH%8Zkp8@C)w9442$H-gv1Xj?aPJc>@ zH$o2w(ZglxVeTVFh%Bnn91W9b_<|XGkslD2{YH4&;+|j-VI@!+smUA*z(xTC$-k{}N?+$9OaoiVzp`=@z$t`65P(L@|Ri5__@i0;fNj5FKKp`7c>( zOoe2dO56nt+{FvsMGaIr!d6KpySKLQ&9h7mXe2>jVvVF#qI1`Uy)@TLI6fyN`kNbU z6N%2oO+%X&l}inme|>5TB+y z)B^kxm2@4jquXhAG~Z?Gdg<=@bGL_fx0ZN*UuJkG{y^=gj4gsA+VT2pBL|$^379!6 z6K*Q)6b=|q$7L<>FR)LjipQGTZ=ij2i`y$MDcKYMT(8Fa^|>dc3-SM=b;?-oVZAc` zalF(-O@O4!P1%iYNI3wUlniSWU*SgRhV1;!*(*F6*SH>cRT1KvHKOkH~p}hX*z&FXN-+rMd6n7bXTTlRnu%TEN~us`I)$b&`8 zfCNxyqW_bvQg$?OSNgkaaPG#&_5PVsT?{dR^8TPk9ewm5+~M~z)n6TD!L%iM$~eN< zQ;soqm>|t&W_V$MH8~&oJ2Sz&4vsc@56?qJO_EpDUh|x$=GjXIQ4LiXplG$w#0m{B z|0$!^qBH2-S|y?F1qaG*3&%$%9qQySfu6VYYH-Tq7tb-07Vp@$R4ye|pisukZ@GQh zN2;ZvVpIE2Pt1qWP-DQx|MuTayzB55_t>_n-D{WPVpxL@;(i|5nK+LK)gW@OeVbJk z;?uZ3971tSezhG-XZ_A&K})sdoWkHwO7h##(;e_mmYBTZdj;dJq zEj&TOS(Xa&_eX01{hDIOo01UyUoQ-N3Q?D$Y?;z+2kwkWk*LUzg^~b>1t-^#xligMw>(0o+2&j{PkAW8B12r3WqZ`pEe-W!OEcd1~2+7YH zCK=82Re^ebJYdXv>pXU02L+VNGs+(xmOJuXlNw*FOR-1#EDV52{z4wg__56Pt#=WV z>#{Fbx9e*9KFhJOI+k&N?!PD-Q7qeWz$F~HMiSNvJ{GlR{Rt_CWuJv~Mf2S7&m zeZq3T8=*U-HBwKm_Yhc1?-gZEd>_MeGhvUC2cG(7N`bfzpeb%grE?VmZh#iW^_}*{ zFRzmum8nbMQ?wdhu^|E&lo4l-iewfv78DJi;{uLTxxfCG?>%L$<%d#tlNkW+xpMY5!H@*8Kj$t|ULo>~|Y+HPJPNTxYnC z`Rm>+pQzgm*(};j)J!I7P9!sfm@Hb)Z0`6uBw;W=GY#VJ&p>r*EOU&JU7Vk01$7^M zV`s}4`4n~c*JSKqTJ{>QyDg<@k)0lbSbD!Gejf}m#JC( zYI!c;gfu>c`h^+p6aE0`NqE7jx51~__^8qL8LFZwXEkPGss0g{avwGWzi`pVt{xD* zz;P@%64^NfsWQ=eE#e&GYa&3#Z@<#I*@vWa7bFBUnXjQM%MWS?7i3UETUWD%@i$;1 zUyEj#`mN39$iZdHz&{W3Q3Tsol^Kc1 zk99weiBLw8rL7{z$oJrp{t?<3HkM@mb!vtBFn9r!kfizR1jLXbcJ&S1=da|C*r($) z*7aY@m>GO_p5K)9j#}L(VC<>xLryM*6Z*rt>&@J(RFt#ce+xh6}|N^FS|Fzx-dl| z-ic_9vwOSO$GZ2_WAir0JJC-yJ(R*vjve=p8FSy1%!}hzeK^B?S%}Owx)+ZUJFp%V8o*UuTJ&$=x zbsoR40~|HR!Rba$v!And`Z*OuLo{)B|C36*EX~})s94r%t{~ks>5_J7q-GMQ(z`X1u6HGS)Dtk9Vj}^g5d6x{yxnD z$QFc_rsMskGy(2#Gth48!J4ySsjDj)6$I?wp)z;+ELQm6rkE8zRfu?z!Fy8P@w2Qsdd3&G7EtIFp<$W_b~x*%F$96`@%9_Vfq#w0G{=l}doB;eUhrrj6b zN#4x@xyH)`U|gTzxa-ong>{aixJie*Ni(F_Uq)*nYLKuA02K<)roBv^Gee^6c9t`& zIVt6yJh5@T2=L#O%gY6~z_vrffx3THxJ+hC#^L{071{%HxZl7z;2Xa)sdufU4Pe;ve#A7z)$tZIK= z{n^OC3T50D~!eVKSF zd;G#GBCsnW)wb(-G!+W(sOx806M@`GWzU;k5w?qU9S2gaxs#I;0DdBA+8Qub3@sRm z(f5LNqhNUk7CIwnH2Af!QRDhzi#N@{xBKU+#i?46p*}3ckLf^;^mX?x`;z2D%Rv6T z^7m%5WY!R?Vo1UO@t27()zcr7HltWl+D+HZO0!KdOLz7Ds4jREyCk$@huH55FSU5L z)ZNLIFFZSXTa(z9nf?i75QJ~&_D#LT|g#Z{HMm5+RQEBMJl4eq1lImRn!PrLRp^-oB~2~Hp!=yf z$GPa2L4nyclUs<_n0O>A*beo^u*v(4YYRQar!8sk#s&l#JLAY9fTRo%Wc+riqoigG zZb-YCx`}Bl-Ui2ASO?o7|4F@LM!t~thKbR(r!YMB3ed7$ZXOXns>|XNI1gB4%vf}< zpwOa_=EgHd-V-+M0U+y#xgPSKU?`Hxgd_V?i(schf9En`%D?{9944E~0$pOGQ;TDx zJFhgiowD3{x{V9#BeYkD%i~LLFJ$&8#5=yv6!bS6nDi<0q?nLp?TI*mhrGX5B0a%40?0cpM;Z|KvAl9qXD% zN%m|-z_+lUnowW>w;?msQz55BNWlMP*V)V_iQL#*jktSGE@^EprSpK-D%B$rpIj8ldoOb;{!$P0(!7yg?-n zO9c@x11Ub50buvVv(2+B%(O2q%fBkb`!J9UK{GGzL`<5e{d>U-?=?NR#>ksx)6wG4 z)51W0gxRx;NYyiOGIqw+5PKnQZ^_<)_iB0#mR7)T0T;lD5k&uwWhhIrp_hLr^gX9O z%`Tn$wtvZbJx;IJM7bAf^CMv>B3(V(Lb03zNf`8;G7>DQtA`#5;4$rcF-)isOSwnX zWt-nW2w~XRnYT zXX;YRY8PoPwClF0gh~Wd+&(!W?X{4S=za-aA$}P6n87yBD#Y7)@S10ZycyIkRo|uj zND4wAKWMyS1}H+=meAKJHQ%-TiIlac$071Z<4bx|+7Lcab0n{m+MK`+VYO?R44aXf zh0k@8h%XanQoxL}7i$_awM13IwjHWdh#$q9!8?Zd1D)?IMjyCO$L<*o5!rD~yX)p> zcT{E$;_qiO`!D+%BjYv^>uQ0MP@Ae>65)iX z=9|-mrHG=eJqzf3+5t79;^x0+3?dU{wb+-(SW%5z>@(KU zL*?>Li5PQI%LJSakxBw^LB8YoKkvct6ptgGsSN|sL3g&yK6}l3uTWER8nYyygr>B+ z-AbY}+C`zEO+cWWe6ZMNyumcTD zw3_RE_A{3oZ74kIM+CYmFEgV=A1|dK8sQvD6$0%#o|_VKkHhMu#4CI7FcKnu0n9*S zoBkdk+eEnuRKdSD5uoGP6zNUHezbaO#p~Uh0*V<~5$(;GwKuVY5jwG{=2^KkY^TP$ zZl<1k7ab~(hSXx%j^*p{a=mdMS!(7=u-b z%Kg)3Y1;C80wJ)Uyy*7;k-d0k74vfTW?ZTL=M-wy=C ziwZ0bO&aE+nT zpV_an^ksJEfZFLrDdLg+SmW#^6 zc{aJ-4AbaD^G#4-C$-KckQn{a-)JYnJBHMs%e2FX)JcgL(!Zv@u|boR&qEKpThMM~ z%uWP9jdVY*PH}+;fBJg$mBDPr&c!-&V#LZA~ck1^v6K2{@Y3y!I`6Tq!B33s|u?zl3 zqeN~&Cx1Uuq@*p%SGJ98!FRGLG|u86+mjk#ZVIb-uZvF2epHodaZg!*cKMnF7_*VE ztKvjF(pGPU=r8QenlKaa?9P3i4L9|;r_2GyN8ASM7)T^xqr-AF3ygCcERQls8wUSE z@|CLA+}G=m%T?``%Dz&Y*~8t9=UM5dz6=R95qCE-&*!+u3{cSi ze$Y4mb1K)iop0u1mn*WVz}UWany)i-8zT8^)LdD%(t>3waPZF3`qysO+sMMz(eics zrpbtcBD)B~58i9#=%&mqz&4*ly=1EIqVvJd(BJo05i-pZ{>MdUG8 z*H+=4Tz53Jb8Ae?eZ1qwws_MNJ5hSEi3vQ#8x-ma_=0&}$yTggq~?39<9T`b4k9^a%JNwM;PpX^xiI)Lxrzw_u3n zZ`W%f{wg$KOCN#_4!|#F;SFvH*XIy=cfRv5-G*x=NCzh21Hm-i=-9h?4YhwWtYr3V zM_ipUXl)Q{^JG7+j$JWJ>m^t7Bi=Z3$Nb5uc%<;Egy>R*4R5R8^xn8ig zgw+e3tA*N-0UEymY(57lY1X2RkQX#X=&Z3h*ay8p>Y~4bQ2kJZBHpo>I+^Dc-&*gV zH(qE=co@+wn;4c`r697F^nx)Oxiqw~ASkxr6#yio!EyIeiqFz(!uo99Fgy7cW;Mj~ zdjjiaI~a$t!@~IQIR@R0E%^L}_}`whq%@XuDXwZqlK)3h4jUu1*UMfBw7|M_y!L9v zJreNORZ%)-L7ekl%@o^PIDlFhlLp6IzR(^KsW&oOh(BDu#U#K@YbEcbrRiP)ja}78 zXUFEMmcqG{g!ByMBfRgde3><9N8weoPGF8dv)}d|YkTKtgy8JfW*Q&(Nho@iCe4M= zq%0pgK12~N0@Xc`JO4!*Fh82W5Cat*Er$$Y` z5RI0yhZ+5u*{6F_rYn2*{g(@({`DI)>y^mRSV!NFC{|iocpB#P$8L~d+Tw3MfWEL;HaXb`O$Qmm z21;8rxp4UkI_e3Z=*v_IZnB`wK(l31dw?xcy+-d&|{ZEBaS~#~TFF8(&RF@My|F4eW(h?-$aVCILfhi1qy) z-VfG>`xoA6?AdI}9Pb@l)pUGS`+K3#7Ae+9mS^S>fJn5E*>2>x!sFj!UCoyiBrj@9 zA)WxBW8t(fE5cn0T*4ZcK&4dwG?R|l;%=MjUaH*7Vm4uz_ag=2_UF%`wprhfcM5$f zuY?Zz>5O@BL)=VZ-@KC0#FC93W_HMG`A&`(nzpYL;$PraU{3<9U8m5)PQa#E_hz%U z0z5bHV7{{yvI$7WGH-P+T$EQTU`d}3nUBtQZo?-{dlT;dHzdxb!1i%zDyLLm`5lgb z>;9So?4%0Bc-s3z2k^bc2{E2YH@uMIqwg1Dv7G#MXD*9l(O7-llv*w==j6tGo$LVa z(z#peYGrM$e6^~z;%R+{d`?{j8S!Jm6TzgYEhv!4MF3NG%=Ag>G#tC#j{nsK;Mw0DPcn@aCf$(mhHaC>RwR6pYA?OYigta#uJ5t zb=+I|)K7DE$+yfb#JavNWOFqe27v!D)np=w6i2DtU9?DCWxhW_@qPR{z>|jjGjiwE z`yn)2`J$1JS(EzpBnU3$jDB!+hL}Qto+oji14PP`b}wc%vXw z-hW?7+u83gip09U&0H%FV3Ct+uV|g}`zK}ov-JR-h+yy%M__}Z%8u@y0bp**8p=BUhi~h)v{k}PAuTvfMr%YBdF;=rP z-YJgut`Uc!6TSsF!&nG0FKghq7r@$MpaV!V~rZHsF7S_f`wMcaAN382;-rBtzV<7+}+_}M0uGMgiQJmKP zB^W1w5D9j;x>j-@4qrR-uAzjPz&Rz}3*rKW_-^2zVtbZQ9J|nPQ7Z*u*gtLUto&K5 z93!9M@Suz~`Qs?I^40PF=SupWVLCMPmUa#5;)HH($+LqHWT&g-JcL$@>$kXPOD*gQ zuk+yz`Uv*ulx7*GiLfoK9TtB91Pd%fMAE^hoBAg8J$1j?P8m-l%W|9~Qd%qGvod|n z7MtBKZ4)U)Dh0TreP0;wf010x zeMbu#|s*|G^=PrHAqYonyq-J0@Fu1B~Ugjnx8MVn8l< zAd>tir=ptPZ68S@Sgd<*gH3c8TUy~(3wjZiw00K6(d<-1m{#xP1T?eu_mQUsnI0?? z1$7HcLAVfK92QR(t-WdZvG*GKxktKxciib?$xJ@1IQ zGp_*#%FpsYi&crNB4toqF=`Va$9zH4kQRThl^-eF`9`Wq)d@2M$4w7mK5cdI`iG`t z1ul@a{|fQTm@EeA_T%?x-teJbf+p_Y8w(;@_j(=zwuK)Sih2r`^4ZySb$W1eRmmYi z1!tfT|EnPNW>P8l!^T4XYCr^ZFU0rcmD|rZbVfdF-djX1hR1n8!bKyU?@p=AuaOG8 z=ZAHd(Tl}1cF*R)+f6&>&dBkv@poXp z{EH>LM87*RrDOo-;%`1@Nq8=~d7;|-7g(s)QFp2Af@6PFY+Q^8o-+Nv|-!mc~1I3Gs}NrmxCHUtoU%DxF> z;tL%w;x8E^49fTbHZ1?HuW@ZY@sTl>?0hpD`$JZfYwTN$1h1qXdNlMhB1%2v?>03< z3ibe?@O0kg(lbP!XWv;Y7J*O<|LLjv-c0-26|t*sLvG6AoyYsMdGC$ekA73;vn4I8 zHdD&YEZuN*Oac6#96Y8pe#who7+g@hf5}aX6+{qkYSYKDt2Rl(dg&vpNf$E_KS@P$ zq5CV$>j5CnnEMvB8`mF`JK_y>kj@=)WUDuWiBg2HW8(N&_r+R2M(umUQp>ANrRXh8 zX)F6?AXYz;B8TiWyKPvk`)wXE z*VDo}*7e(92nNCC68=Kk;v`bggkGSHs$H$Tu~2@%`VN33k>u+uz`U^70zJm}u;8-? zucqdv!G92HTdeyT0M3~E1Z40=Jz=}iM%My(ckpR-rEOCG#-|Yj{c69J`cMQ}6YJUs zSjtzIH1Z$%1GRIDG!_ZUHu7#alkAHtMB1^xRByduM%nRB@z*vGUI2WK24TH?m5{vh zOWg%lf%z}+Axya7zNwCsxRUoTWXA(Q!52DS{KrIrIq8QArepep_(Re$#kNI3psdW0 z!vkm8?gJaskW?^xh++BpBjs#`632Qlj8&r5LTL5iQ^2*f$~`K_#0|6U1@sY)Gb~sFGCZ`XUwr~ZHY7EmmbVCy`{)Y$P;!h znO(!+s?Mx-9c)(VuyCZgn$Oa6kqCw&2HC=f!TM6AM>OG~$Skj9*<&D0>^tLaLr zp{>DY=Qh{F9pv0{uuazNw3WnRaQ+hbZOE$;n;l@W*vJuWe2|0LV8frm{(5+xM)YP= zANIxM-Vm-(2W`wD7?m9T;cx~RJNKg=04#^+SRku{d_6BZFKn*dz=VmMHz$N zwg!d7lPqB(;w_Od{>~Dbywax`-^ZBr#YAwZoOf)!)=Yz5NSa+Il&x#-UnO}bA!|L-LaU`!ql6v z<}GWGprdS}f!Xc?W2ndxXoM0fGbiC$XV^5+F>Yd%JCh7Hly5a3t_M&q{tsPj7JC9x z;NkQZo)I}T&&H=Vr@xjMQa)FMmI-EN1sPPM5K5|fa` zYOR9(M5_`t9s5ZFApt|odZZ?aietq&d%!T&x(Ux&IeTo{JsSqQq~5HyG8_$m1p7yD z_Mc4Xq;ZH0^_%4@PD2IlG(0+6_Si|#PS7@ zn{I=>ZMHON#+K$X^jxghsEwreW=Xu&+L$W$DvA(2pDqv8NrgQu)HgTI_l# ze-6#=pC9r^J>GApx-i+U`tr;QB(bR}g8ztGg2E*URt-oUC^YZBnWEfc7-lN3_*|H^ zvQ3^4A0V8scdV6MbmB7INf5J!%lOp9heCQMsTeaA+2Yb^$O?TnO&Oc1@we?TwpMos z6Q-`C9bb*Ke+G$=aNH4w&dHF@Jsc2%pZ|J>b1$$YIfo1^kQVEe0on{5Z+KN0PmZp` zAl6=b9;@^*9*}(-bb+cSUW%4)pgW#WUwrS1tTgh(kazHhMg0}%*Ie|(dsG?3*BGov z`cn+n22Vyl+~CPN58~nBG?|jp6qU<+BEIAQeYchffij5l44-#7-oA>|o$NH7J2tb2 zB~R!$I-<|)PR6cSpBaV7@JIa0Fy!YQl4n}os=5$|;u+pa2Fy-ZwjYtY zsr`5C6`CZ#i(X+Y#Q@^tY)_PPn`}nxD3dr^HjDshneZ|4OzVB8Zy>dk7_4#DI{WKr zfT+JG!Qvt~mVcMXALdVUD5SMq3}bL+BmU1dW_q^xNHZd+WGN4t^h3-Xxq~r! z2-`lKiy6K7zve>WM85^I0EFhO0@g=0RJxz!6Tx@h^wkk^EW6dV>d+W`34=u=EM# zyXXQ9;Iv0sIR39qw}Fu11Xu^BdQNiW~x*W>iLeP*ti91Kc* z9OpIHTeQqHyzO6Lm(L%B({121c(WP*r;PWnW(1!$UWis2?A{je(*itDKG>b$GtN&P z@5z?DyB=?=0!HQIoybjDe+N)Pc zD=W3yN^|!V1X3x1Bg`vC!&slqoWoNTPIs==sDl7txi?!BC3Y2?pDbt*rr$~o=1KxL zv=6<>m$7AK1^^Kul1zVn6}!L1_DGc)8+~WG=AzHtDWY7!U_$+7s2Io%0@-gho9O6$ znx0(Eo}g@(IQZE98xSLWTBlXHIrgXBac0s}Dk2Z)nZQ@(VB$&{r5mThQAy$;n3Cq) z7^<|tX&M)vbvFaxa6FzvFw76phm|iBMHCI7;mQKX3}j-;P>b+W2Y+Cxg2yrx9FL8? zgX_yAxXv$&o=W^#^1m>rK0Hq%LGhHJSORB_`=m&QW;zetC+dw1935Xf+&B>$Ihq?g zzeB3ja6VRo%|{B7ay!6)j?AVUXzt{tY17Fur!$DSc()dBSS28~m4{BYDU#V6`@Y)g z*N{MD3XFxFeL}UyZ};aa9e1=TVSL&f;=IoODb=<*?{vWKXlg^3p#FR(c5dJN*uvh} z=!){?fxQ8Euae#8%$Qae^Mq^KO{%XB3(xF5y_0rRrvIS7&&gK;2Q)Q^23`kYMy$=sHg!rx1ctxI&uJ_^x1V@k0CBV%qn?XmDBxQpJ;#vi!+O8m;l>>O zqA0f!CRgAQeK;O$Lu3x1)NEB0R%=|(Q+F$O z#R^ub1hWji)$4~V4 zB;eH_FJYBclZ?#OndF7|i#$B0o_TkgoYWsFQi?T_tJ$)OJGuF?xsv7frZ@gPYNc}r zET)QCN^)}K8==XDc=5;3NnnA#UpR3V8bZRCRfUkbK%unIKt!IhX#_aGN-JjQORFzn ziBARq)NwHaAfDmF&{vtttJpY{-(^Ao=BPzuABAT_O!#9Ev&@+T^Fdwtr{R(LoQ1(M z!ri2OVs}wN5k#VGaqdf7h~*NfXb;Xp%%5O@k3jSR7mH9LJKV;}#joxIO04CPp<+Dy zZ%&cfOs}7VTR4Z8`sjkrSCn~3<9enp(z&uAY{WzYXxy+u{P*PZk@dS&F*sDg{Opnw zKHe=e$C*1|Zfjy)E^YmP8AlyHw1GQfj^@ZChGw$9Z76P()KM6UGXO1o>ztYpQYgP0 z*K^QT=C;9~gz}uPPZ&ThUa9ekT=mBJR`kU-wCkHzpq5|AG?z)clK#{}eWnn9p^42u z)F#U`Uhx$B!R(`K)^8@*FMh~z*LgPd!;lNlhT7j~JljuE9upG*P6FOuzQJx^Frh}5 z`c4T%F_tI4Q&DNtegPrGEUl~riQO~O22W%69E+aOG)R`~H##VRN91Odl_2DprSg*9 zR6}NC#IsFgVvTHe9o4!VIur^o6*yj#kAjkWYQ4uEsBFo#4Wv4DjJ)WQtlvPk zd`s+suUAaXeS<>23%f#WmCC-|&@ytMC3g4M$u933M)jj&m;Zz}bf~U4FY@{!@zlCr z-sR%^>VjVhA1A2I%Iz(MXdLN;K;MJJgFxj|rXNtvppf;C1e+ovmtO?(JW=fx%DCZ$|9qCu5hL!QGXqsFTfCv^EWJuAU%taW>vGZV*vNIsLU=zqEMmNt>*H3Ml2DhmgT~03qHBq4y~Sp;JI6uVm*k{+y`q zT@1%_EDbj4(ui97^?KvwTk_xbEce6xN7FZhkAST~9e zq7#SbJG^1a9*D0UQ1Ba<|3@p$%K4dbw68V4DHFK#W>k2ltFZT?KczmJQ^~VIh}Fb- zdrCiJW4u^v|FwE2R0obVxfnG!(8ynGzP^Zc>DW8-Q#>t2lxBcZegWm7y@ZfXsiUPr z8*B{d{4_-myl4zePzY^Q2vksc{AXe+m;Bf|Ti8NGfwTvH#qR|uJ9$&3q2532(+l>A zT>!aa%M>95@9?unbW^$x7fWBk*CX7`fwS$LcN^0AV>YH){t@?WeO)VtM^4AOeQxl} z>H>0KOefkBFVgzY<%Bj8>KT4}%nmV#28rGWfllf^`7$Or2sSlW+#k|jR+|XFbHk8+ zW&P(XpzS4wwn^DMdGd6#~!w-x?V|0Gt)1#v~{N~PxB)=%Msv;S1G>vf%AK{;y2Acu4+Hjgm3xC z4x>8U=wiQShC;b4DZ@5KBX`x^sqQEt>k+ZPBkj(s(?KC~7E|8eRd*$qTK*pb1^c!f ziq@{CqQRUj=V0AkuNLu2vb8XozFLUCe1yIVjNWuFeep{keg0<&k4T|wFc>QXcIdV_W|` znKJHjFuRW~PZ*51fu{@&^FiV&ineB#@wYN_C?vt+)C_Uzf^phAAOfWc8K=fSD#U+F zbAA8vStoZQC+vKIH!@1%O_W230}AnH4_E4xDcpxbz7?>?Zo8Mvg5i^}(JJiHP>69} zJ=kMTncXI2T6F27nk_bIj%$>e(VJ;(EC3=wPnz2c7L@@Cqc(H>)T0zI?Z%a+3kQiO}kAv9z{MU&)Jbosxjp~$V|4kB(D z&nSrJ`3vRhL^-=DD`}>mSg`5I*P3#CIwt&)<iMym`@wo6p3n#I zGd)dy8LTsJf!Qc9v-!Qp5Dm=hh;+kuyxNc6%H)`#NxqI+vQNxr3gRUC9<}_xgOKCJ z6htuQEb(1*!quM#8aVb6STZEm4s(7uh2eQQxtcXJFT@}CFO5E`->$Fqf5#h|j^W7^4}Mf$91V+~vcbPqZe3txm|2l*(<-_K4)F{KA{nDZO zQDTy%J3**my2w>t73;bPWvH{iB7R@s?xVAJZx;W8^cuk$Eza z0+VSuuP({s5k!$;*2&Kj4AR%JN2J|1rSt@N@DQ_}THQjr>CMc@z{BGp;B?&)Is}!Z zZ-pp%ME0*CSBcMK)9%}a_z$)-{8f|5!HHy6D*0^-cbv3zKAots%=>==!LD`E+%TMv zfPxLSKRF!R^<#F~{@S#6=OT^i+ni7fY}^87FI9Nv=-G8goc}a1Y@t;=LbH}tyBqz; zGE@wrL2(0oZ;^3lBcThlH2M=|&x~PDMM{*hMbo5AIukS;8qzsEeTWIw{n zmrnxD--j(LIYYe^dyV3Qy#E^tH7EI5@U6Mg=kHceYQIjwxjsz!+?0=zKNsx|AJ*sJ zXAj84i<>afL2+-uS@|E@c%jti=V*%fDIcn#jQjr!?%%fBustw(K^^YjiSa`?97NU= z+D#xOqw8uK`2OpL9xnuT>z@uW`jnp^y@ERi+5=-#9mC=Q45}d~np=O^DMRQhAoOhz z0{B9J{~wkk!rS@{EP~BjnJ>FPvJ;m33(LbesLrfQ3>!=$sWuFd`)CABGe;KUD-U7| z07i&R#;J&8ehKE)U~ZRc6_^utmk3awD6x<~%Iu%No821n%U?AFG5g=xDWL&mbcJl7 ze3I$4dXP@8q18}*l>adnbEqeco%D}-Z1LS_W#R`D8STLut?~0Vz!{mPa^4Puy}wL{ zp&4MMSgz)qaIfiHrm-1=e6wIZ75y#E=i$`!$G#R&>ZxExAN+?U(rWb5mjw%5eKSY- zBD?++$X&*<7Gt$+9CVRiN2N0Hg`5lUFNt5x_eFhXzL!9V|HFJ2@rDA`U=_&U(A8iB zM*_q@nF%m+{YWr^7e2HjSiuNxvP|y3@~#=dZwwv4f8;0e-(%PK@650FtTgy9{=ebZ zT(tKNiDpZF*~dlykWq{`IAXv0gRcZbc~C={Sct#GvPV~29XC-nm) zYIUEQs`QS>ZL0WHQ6>=KzJOP7#kd{4xLEhY@hrJaU4}RF^oHx4;5u7Z#iW(}HaOnU zOM*MeBe`y~$+LaB%C-=gRQ4hA!QYErbrlb-bN}rqADk0PWM0z8ya|kY6z4oCP-GIq zYiUI#(nOwQZ?x4LQx~L#>wFN$`=RNV36c!~T0wsJ66J~5gz)y0kmDHeZr7^y%Cni5S-ir4ImoT5Z>zWd(dFCie} z>@tyD9|C?wZ%c{fXjsRU+D^N3swr3csZv8pKyS#u36e;(e>0Vs7>r1pHg5zTU#z>I z*WSXq#kr}ZwZ~bZij0mL+Jflt*!?v8$1YQ-S;M_y0*yDxxwmEo^4X+%4IM(c z{qc|Yw_hZt-SZYvup&Xz*yyf04aa(%^I1mf3ExI9e!(r@eIFo=8S6d^c-k)G7vGNO zgB$DLYpo`!MiZPa`&pU!jEohr4#0XpQSZ6huh`(QL?OO+0JK&>v-g=;0qRj-F`y=% zx=*Ovvvlc8PU}y`6DMj#Tr%%Bvm;GrKQaQ45p3R%th!w3R~>#5cIsT6TZ2+aEHIP1@m>!*tbcwaw2}L+cVR? z-JkZqK}pwrEH9|{Yd~q;o%5D$5@&+)X^St0aIU7V97W|T7Rm4>YVmy*lWh*J@A@tf zIsb!aIM#ir5Z@0b_D8*B#MLVTvV3qB5i0u#5f7xC5VPD|+UCx1qc zaVnZ_VlHoM=hb}Qb%Fs?*jIxJe&Wa4XV%|q!-?4w%#D}w=yJ$`@lW`nrW?e9jm{M+ zMdrosmZW11IkCH&E4U`PR&t%lbr{zMu2o#?xkkCN)^1I(bmX$^DiKJ#e(X&1%f6%4 zLw0qW0!e|K-^rgdp%@uQ>rmFiGAStj4I&vsBb^`Ku8{s z!J?FX_tI|=oZI;B2>*q&dLo_PgzX4H=@0Vx$RT0ZM@j_V=;eopz2VCpY(|}^P%rtS z2T{&DU;{(uq?B7U^4#K4d{7`^3#J?x_SQBY7wPe!Z8>LgWADoRFe0G*_R(U>IKHC3 zmh0f@*n@a$lfQ5vQnGuBQJKRPag@3lE+btYWNvXau@k}V7eokH0*~{5(Dv-g#k+)0 zgo#XE%;y>I_8G2E`X58OS{`3x;kMe$RDSV@V=%r-S1a}c?t@4dX{tZ|o+0)~5VU&7 zGi`lv8_UvaCVv!`r5>)NvOwuat4vG>Xmb#+ZIWL=tg*n_i@<<-KKReVYZ*!WW?4@ELPC zx7BR);5{Xan}sOuaI=s&yaCgfn_l5uAC8mvE8v`fuFxPiy)xThRjMFDICC9Bhruw9 zal2mcpoM6BTSul1@+KmgRsjkKa2ox9TZO^N49hz{Vac2AhmMxQ!Tk9%UoI4ez)WOX zfmv&Q#C>>3tG9pqNtm^tfa(8}Gv~^IZCzcNIUZ<>A{k9bNYIr#L;D0IhP_B1aRi@f z+{l!NaDn6GuB#IboViaF(^+MpXq}pLR_fU0lK%*y9@^zypJVp#XEjA8U&_kLD4IDL zRIDR=wET&O23H622Y)Qc)bYk`{tFJAp{|U*b+E+x+lq5$eo$5p$?p)g;RrtmSn&pN z9=;>8On6+fxMq-*{-zhHj>{nqdk=7`4uc2Pz2hLSVQ&h=SSv=_s*eO!p=VH4$=CY6 zaoQLXATS4hv@&S1n{Dx{papy$0zY}IZ8V1>GMI}kg2j1B&|*Qc1*abFW1GP3d?Tnm z&Y+iS+sjEo8(2Q*#mOB9+Xdob9BgJdozWg{ss=_}S#3nIH7x!2UP73#k`gu#lH%*t zi|U$r4l|j`;x|FQ$pVBG;{5wJiJ+V<^>qS#ng(~Be+*wr>U`bI7!*(EYSolh2=m;` z{+tP9agZtorGGFetA6_c0=_}AhF|6(}6T{W~uDSl$(Tbv9kh1X8<&g5BiD!H5`$fYeq>nC~96TqOA zBU*Abdja{=LS3bJMAMC7JVKC~!2-O7(y!%ajkb(V;1Z+Uzgz`;YQB@vub6+hm3M7ioLiQu67cbkg@b zeEKx`2)eb=o+lOUfmW5~GnL7}f*?zN_2~o&;!GSoDl#ZvCx}0O?w95C*&l?ZkB7#W zpEK+b{o`vwnco-e=a(Cdo&FRr=ie2b41IHCw9};|a*Xm}Mmad_0?<_j=;j>Jt5C@o(kV9&$B*Vw^TMK$L)J)TD>h zlcSAF;;Cl6nGE?YIzY)B=p=Cxh6!TbI_D;pT~mSAK5O_zXQNzZ6@R`fjJHiCT=37> zRp&53vZNswm_K798jyGT|!u{i0a+1y{HZO6%l_aMMkDQ<%?^I>VYpG|L~w_sj< zq$~;gUJT07Z@fyQu7hc$qsng7IMBRtXX+-Di@pQqwTYy&4H8@ zX_JKi{&9mr$Y1w_EX!Dz*$Xr4_FmIi;cQD^;AKqFpkc(2U=fqT0{=w|RdD6wk3II7 z6T73&S?M2qrDdc1pJ{+em)YGaMKV(;&@albWziWP%il$b9H;}?zb6KqiLyI5Otza-g9CHf-bZ2trw_`K0ufq?$G;T7XVty4%9p~czggqtk zuAqjyDu1p@;msBMLt(i0PQ4D@RHH2In%|Ir#?&++s1*m;%BN8|P^n?9JE_&UK0It% z(;4Jrb-r^wbIv@R^KIz#c|eW)&p;hd7uZv&FGqS3z)@05*j zqh6Ih^55wTf5o;Ej8Nje@+H&afE%?gYNbfL;2ePRTDP;y1KhFyxz>dWo8;oV{pVWx z=}>A_{^wecY%{e!K%4#tyjs{Z1lGI%xz-*Q)|>yi*2Cl~0BhrauJuKP5VamHuO-hR zB&q%S3&L)H&Q!eb(Z(F(BUl=@pq3RlLYnyrI0Jo%v$#)Y?yBzg+y<|6J=> z%u{Mz^Pg))?Sx+TpKJZnlH{E7T98Djg;W8rjdJmG@p?sPPf`DVf-+E7;>h7V~N(N9$6fGR^e2vBPk>I)_xc)A< z{>)rM`Cj~3d^ah*K-G_F4VbHeH*@d*>UG0(tcRE+DC6SA7my%||J*u5Dn2 zhn_p&+X&-S)5=))o0KRCeAsUBL6uywQDVw zzPlq8dthbb+8$D-$w02q@BRElpC?wVB%`{kxBac`b^=ik&=kx&M!0Q4J0%7gLISWWPI~+sDk77F10nR-tVcI0$0QBnS$n*O zIjzF*Ki^S`+54NJiE0i*Weh&8->=BLW{7e!uT#sP;AfKaFB>h`p^Ad#?ik!Vq(3A$ zhFlL$6y%r>(c<)$iPf3B9U((CBpfox!5^Fc`=h^pO#p>M^xEJye~htWTTJ0O6!KWe zSDV5vQ#tlP1QLt9SI8(E{BenO%$@Payy!nJ1_TQkH~Wi5e`Bb7(U(-3GbDnD@c}*J zSic5!$iXo6%>H*hd)_DhN)RgDl}br$Vw)>Ls&<|{vwixUgC6GGg}!YXM`14~J6P>B zebD|7)a<2TDqw7_-@L%+`^H`iwuUX>Qk3`Z<6?*8bl zJj5G7yv}{N=l?Svb|%A z^km~C-L&d_jg}U{rrY-Gk8Eo_;ES+$>)JP8UJk?2V zpSzuvuMOizr?n5)Pg9WE3s2WOWmIaJgdxXn)`GwYQ=9k_pZSgD$6)Q`95{< zVENeKU)fhb%2emtdb6)S*uG~r`ozm{2P_usQIKiCuM^0p#`WTNeUdaZKkm~KYw2}{ zjr%p#IclQX9FlS(n}|oy1uTDeA%6aFO?yrDX9|h<^UM-wzP9G~6BB#U@6y!c+hcxn zf+~2~W5@bBugZQV_Tf(%(|_^UGi1UeCxrnDOL#{ytYnLKz!;!$4v3O_&#J~kmmpHQ zcNq8TkN*87%lH04a5RzUTvZ4EKSazC1N!C7{$&u5Wd2RaYO*3;=h7d2fHwN)97M1I zJ%hlzi=s69EeD~cwvD*7$76wR+dX!*+;5_cga^LEdhnX7bbGpPPpsAt&~#tRJ-j|D z-82}>o(v7xJvm*9;X&>TgJ;=woZVg>>*7m<3HgVv-g*6~IEo=)e-k&Z%P$ywa!_uiIrWc+sjOyYC%T|QU|`g{@?-Z0SdE~!da zY>wUiNZPATSNt=(JrU~~1>6fS%WhA`x;Lo{??^syG97fjqR!c$QE~Uy#dnw^I^0JP zzRcYbnx`v>Uj^%dFl+Y$YC-_FZ}$_cW8EKWPQk&P2H6w4I~Yx}i_x%=ZbkzEild2X z5cIh8CrOp`tc^*#fL48{EpoB z{}srYXayBmD=nUWR7eMawXrrFSay5inN)T)+`g*5<>&*Rc_9*6GLXLys^Ld@0h--g zFV3{y3pz$sq({D3#B-mt&I)`_y)FZtrTkxTn%2aw`kRWN|0~4g-6Qi$cr3{*y?B@x z4sX7tioqntx{l;2_^CtWbXcDG>EK;PpB3x9pn7ld$BfT#Q#CllO|=9rybG$lqZtts zyLH%C6^!_X(D*9#&VMbxj%MZ;h-UCDl|P_u+FZ>@-LK|eg0yrV4|dCYe-p32Qi%V! zlC|uOdyr>+^+wLt_@iNXc2$4K8{y~eZhl~bquWvNHAnz5LUlaX7AeNlYH8K}H*G=7 zvNOkgIE4eQr>$Ma2`_p!u+7JwwB0R8>SdeM{0NNs*4{sJHoeLvaGU#lQh_7RE-n2g)g+%+SU8t7;6_Uyxj!QTe@%ZvVN6D=R5uRTm(XjH2o0lJ-6 z2IP$fWX)PVqo5CLTtB!wpZiL0eI9+_RWv=vhwruRgZixe3w-!q>py;4(DpvsmO%z! zv=cakFANc9!FlB<&sZ1H?Z`z9KuUYj)2S*s{QzbC(KJsil%g6|Msuhi(cSHDdC~Ev zLFNU25;HvbOo`vT=$__P`+P52%^hLb_IsGh51-edHf6>J>sFQ`^W={@&6Kyl2V-nj zo&M-UT=cI$x+=IU#J>+h=3`6$=&B)z`S-$$ga1>+4=}%_RUz)FMFVSOh`R2I+&jugM=*ZYgd?q11v)7Gz=o1)sv*gdH+E&O4RtfgY?d@bz5 zQaJ~99_8{?!Nc_-efV{NQG6-OuY#RlJ&bL(rbo^veYUqg-Slw#YZwm!|7qjo)Ne4{ z?6&HQ`2HZinw^-&P4hdC0=HA0xl`zLpmU;dYd>aw=VCBwpAx&LZ4AFuR6CZN;q#ff zDY+?NP=#JuvZ{Wt#;jX}i_`Q`=0N+yQugx#f9t85-*)VC>+6*A0T5l~Ww3O-NvJ>u zp~8=ROqqnykg~Xx=rliNk$R6U?d7j_--=gW}*gaD;u~Rg$m3Cqe2~lUOY=DXF5HoH~9+)8O zoMm~xKDQLg4!`{7?|j}C_;3F){jV?L?}&c_HXG@<19sh8E^}fn1l$M8_S>NUa<%{e zO@Bxa)t%wYL))0v+{{`HVr}NxR4!$B1N!$3q5l;4v)9s}qb-<+QMfzs=Oczcy(xA7 z99Ze^3zlZwX>Z&?;DcESL5~(k%$VCeX-36!AxG@NcMWCuhkTJw@k8~{jfL`}K@B9^ zhKKmA&#ava?;a2D9+#Ur6u)CevE#&YIe%o!uqoB)|BtSdRBL91LWHt+Y$GtXWEwtfHq_y6AhZa$MU&pb0{&YU@O=FIge zjAFb(pr>$8aK9SE_ClLcYTGw3258nr4q}x_qYRF0FVCW3{Hbl%>Noj-S5}#glk~kQ zCi}~ONqImue52fbiIGi1WGy7O@eKExxOxhWHr!K`PrY6FwmZMeP2i$y(vzzhTO%b* z4JuNet?>aLTH3=WYSo2Gk!uIJlLF1|4DVyJ+tDx5HM~@~&px-08|-^BPeUunX-B1h z^c76INuYMm#hxOzelMDh@gWn<8JPCYFcTHb6v!NQ_&FJaP#60LR|olyJqsHL9HaZ} ztr4k`J@)VqfzHSs-B0LqEA}V$kkGZ}b%R=Wx0@?wVY)n-CXbB0_Xg>h+1er}s9Y`? z(a8EQ88NrulDUys(wKvUc%Av0xv~ik zE(CT<4u0DeqUm z1t&R)V4nL`2V3(GhKsWf6_3sg&V*|Gs{gUlSEQ#8zNI;_{*YJOuL& z2)zK+Gb(bU&22k8Fu}4ryv4{zMtembb3EaSJU9HDC6=S`bB732MP6aWq+4D$*?H=r z9ge_LvFqo9t0HgUsSY>kdonW|2?QxTx@9VeQju^%MIqR2vNTs?>Cxp=vZ=b_A~vo$ z!$n!_vf=vELpw5q7o#2}BvBv3uya!K3ch6)v+WER6<^6ev;K6bEQl(0E|)jS=4H*N zM1~jNPJ2aPw6Q?)g4;Vko4!b90owL7e=&B*BXbi6)eau0l=+_~Y6N+V-SZ8s%D*J- zE$;s-VQvBDXOq=n+pgesTQ88>INU8pWU#_{sJkdoxz$0KXpbqSk?Zm%uVtH+Gqm3^ zZI}I#%rT+<*SJ-{xi)jOToTJ?bcO|Ep2#=2&G|)F{!6hV6dgs!rJ*$)Tc?0WQYUc@ zJR<#pU@Z&3IPi;<(h@#o{LVe8EYv&0e?W9XM;q*|Por;2+WpyldA+@PR7u+*r`^*N zik@lMuYum4W`O9CH+5fZ4(-k|%Ws9~qWuXLu$LwW2 zjPpZLyBgnbJSw|JcQKTAhCXfJn~+_lk?K6hoJiImm=M4iX5{*qAXek5HE$0dW~_9R zkYHc;3{*KIw{XbpEx8V#D^Lq0iI>aOkGO4rkNtff%q-*!821y=9X{AzcC0kNKipl| zYrp+>1Zm|X8Qa@7vr6JA{Ky`9hWco~X_WJEXQ*F;&Y(4rj}eLZ^Ms zPWRpeYq_|dX@uuh35E$8DRU!~V6M72wwJ#@U%t7%{xxBPl%e$!eC;L2IEfJXi3EZ# z|4**)OBT%B)I*B-$(1Yt zscfXMc6Hl@Yb_u6q(LzO7R~4>8Z3w}Fq>C%wh$2Nfp!94A(K@6!Tkdpb^ka-UrrKz z>6jLEwKrzB-)QMa?5|MtbS+jV)r4+zP!mh252Xpy)!@H#{@2nQMTd5_a3+=mG;0II zE^2vuH5yNzTXRH7soibxxy2d&g!?c&+hkvk{4ivdI-X#Z748UJP=YP}B~u|!kikrT zf9W|gJvuCy?d&lodocZy?Y*}wvdrc=%%O~4tl(j#ky&??*#=AVkA$KH&b8RmM@O$< zT*f}P*udQ#h}kU{FyC6DWW<>tWmI8bcQ4JM%**0WTnQRdR%ZJ&)_et_h3_y`K&rr~ z)~LPhGKvL;LNGv8eowe5i^ua1?~H=M-nNeaBFJm+!~_3CL$yAKKhd8Gi|nm0zyxPc zi7~|!339d1M~#kw3bP%ZKraRUMZNT6I`xG90BNundt%@VNxa&wA=I>u!ri7_*S6=D zg7zy%9eOfp9=(cQYI)NU=rt{yO9gdBp5WV|@eS2XTD~YW#SP?2=`N7c#m_=QllrJM z{2nYQGPiU|B!~1R@guY})n2fWwo3QwvHxcu)p?GsFMb2U_GPGsUxLaq`{}Q3VYkN(!P^6vVGn41h~DND>vl;6Uk%yIOcaJ zdfd3ohl!p~OZH_I*g~bE6108&6;9fxL|Xk8tv>#DC_{AOvxO(Byk+4x5k)ns6e8EJ zAVxf*3ZU@u`2f>IE>MCraMdnL zI6P5uMA2q`%Hf09m!0N-K$p}3!|%VaJGLV+m#>X%B^)-=&k%d20!q5-Gw~pCUfYPu?&q^ zQpq3;{7`f`=a$;u00-u=B(Oe2P`%wo=F+M%8i!O8Jgh?0SCV0t0ek<7~d!>V$ z2Gz1=>~Z4D0s``%&9~8NNyd`Q6ChT6$u*5tx3^sa!J$}& z8Z0OBb4Y&Ysr(Y-@zrm|bKnCl;H7OcoQe(OeOx{uWR}kqGuFF#uk!y3`-9T-N&GBH zU*im|a?r`h2iRrVXz;gEbuO%!Que!syJZL?6WRG)vST`!t3Fyp{e=$L!o9XW<_7Go zo#fxPXZ1VrTa$XH<*gC+wx96~A@M2T)$(S^tIA8YfL0r^l;H5)MmQM1k=P0ORzi%3 zJgInVU$+m~@>X{6nfNpKYC5>CgXFYEYReve6aV3T&ag8}+5^ef3BpbM3-`r;3^3(w z9wkS(+pYq}Ly~>sJ_>6-L5Zsq;+Zq!^qa?f#MyOZg1h$Fr8PA*?yjTfy7IdMXTe`@ zSJ%-APT3Gr8#Uh@IXAyI+!cJYeGd084+gV#&Y6q=n>Q>w3%fbfktGVrUe+CPXN0$N zXF`6jv#@uYaG%{rM77WDaa zGKW-QbJ?z9F5pA8IH{Jj;`6b&Ys~TdB3JwD?3eQ~%g|HJ*Ez9&^voTXMW}5BFY_Tv z88Mg2jGZIa!|`ePv>1;)&NWA5F*op4mfhWIUA;T@0*qtS8GDZnPcrgeBpV@={@*z} z8$o6(gV^~4KkhISF>%lx`v3`*L~`8GN^}DHR*g;I5KJkAqA86bgr0Q*LhB??GKAJx z8Ow-BeaiP#t?`wzMvk*%F8jxJ%!(pWtEqNOO8Lg=^1V1NrF^D5m8VvKeXT4f+go2x zFP+H9kkVat0P6X49JR1!P%6Rvt}e4k6J_`i8vOL;#oPx|_@~v2kp9+wFywy{uE^Fk zv zDP=qJlXR{l@uHB5v*MqE&FVi=_RB3|9x9t_gPGdF>R~}x#ue|7y(=>NQ)lRtlkMMS zCByL8pNRdIER8aGpOs0CU&%x_4a6P(JKDe*e%cxOi^I9*4XJ~HDJI>eg@0;1sl6#% zjQ3{g=4J4Z;fXZbJYDfjw57fE0~q0=0r%Tw|3&LYd8GCjtlTn+i!+&et$@4_SrK`V z{az~`S5qVV2DYkpwe4gmQafEd&&6H&&vm_?;ojR{YU{O!uOT~g`lTy2jI!8xErn1~ zVr(IDM&{e%7sDR(wkqdsCrgq9#%b@e7cPO*L}g0^2nLpzgWDi9_;8XJM-DwXDln#p zT__BCU3t?eOBNQjn@b&>kBKl7-@ZPVQXcARvA69N&y$!VH}Wp@dXc@Y3s0hZayFT_MX|3)fHF?LgwoCkILsa`k>FJGy){3*nv0<7@}{~A4>x`}emi+-dAC1W z?5IJfO|7AYwDs_U8C`qpw!sME?A{&tlWEU_@QWpU z(C0K=SN3)1KsS}%N)b6=Gh%8f(-rYK$eb|9F0&T&^VWNn+elIfdy;%vC*R_v@>NIh zMAVaibWY^VF>QOoyG!izo??HEkn@R+YV31{Ui`#eyGQyKdY}@fWV=2&H@`bTmqq(@ zd%L?nq5t6ms+9hxFWenuT<}UsKC-$aqs0Ei8zqG=+1I@%)sp|xp(p8_WB9e$WhMJR z;je=?v4_|oo~9o}Gg6UiUr2gyZ%3;lw_;xvFeHj!q8c}|&!$DmP%E}fvX(D25wA4-Li~H^B#Zyf z-y-*X0lp$D9;BmB+Dm)cm>>U+^!sE@lp z=YFy(ZGRw7l&b7b`J*emUB377!$OhglbP?Sl%=zFBYc;6b~AE4)5CRqZ(DYYDWeOb zP^+zZEudl<8Q)rb;a;u$?-M6EX6H%dtYEmKbywF=d=a4+;o5-Gqe!2 z4-yhOIx+Ygf`vBO-K>aX9;H>*C*9#!%ff#vnYO27+M!5!zdKSdNn1X-cICPDNA`HU zId_)Y_7CpY`WXXc>kNpKt6ga~Y^t5bp4uE*CU>9?v)?ubpNyc}JXkK~S->*(qfBtB zO>}GDAvBWN5^*{EoZ(?f`L{nZ2L`_cVS zC1=)qdgU;%-xL0`J3J7}c!#{MfpoYO98*Y**jw9)FINH!Rz=jdX34`$aJMweNzyE# zY1T3{2RXtkr)gW8v~A}-eLj8bK&bW{NdQdpaO32>foZDSe6~tub;n>7B0m z7Z}4cWxA9%;f(l?kVutj9rsI1kt9>NM7=78%(aO6n!UA|_p`oYZ@YUq^NP1*Ot-iG zO;QLM+a{7E_RH5HMD{K!xe>G>c*$ku7JCgT0w=WOv%;ZX2Q}4vf2O_l6NqTPhVAL2 zu{m7!C-$P_umv2KN6tLOxdxXxJ)tM*NbC)& zDK91ytHO$?u=qj()SD9hMw*4pvdYUDgrN&y(KtdRohdHz?o2D;nJ__Ksf5BwUJlA2 zO$z`u-!=k)@Ssq4GkA6!eyJqfHF~$M@vZ|CTs&;!J3p(Ph0od7zN>Jsq502@yWo;T zHa1VfK3z2;t3^bK;h}R)zr>$NVGJ3KNm`Z*NB8p2Ig0NF%jcS1qfv|R;^A8R?h zfn*uZR+h5&gbgSQ@-9*phFPyR%+Qro(zcD*sF32kmXLr^9T93)y`md$HGsscuQ+ zkuPz)S2FG8c(+CKgLo2>MM}Z@bL-^~Q(w{p$jh~axnY|_>jVdh&>uY*l8%iVTN#IP;8P3$^*$#d}!NTN?22b$K_N-P|K{AVrlkNy?dKR!O-{ z(IX~P;zyZpawV0|S>H-1pCf;Uf?;IHel)c|Yud}9qhGuwR}E+NIL#Kt@{5$8Uby?_ zk1+L1&ZG)+S&YbJFp#>xSkKR~%ZIslrX;_3i%+3pZ>;K{Ej-S`zSVyP(J|Q$#{eyy zYk%bA(nwL}T6ZQ(f*jmBP50+x$b~&6-L|!(T6eL}m=812Lb$3osGI0SBndYZcF(cBn3nZj?uh#saCeP;Nmq^ig`&*(DrisGV?*|f&6pN#Z{$N^PkX|93%{LX zzx^OJ#~9Qdxi;4wsm-0Xr_}p`r)z)46_Ht|dTa;UN0zi_MBK~`L=VdyV3VEUCz&g3 zyq+Bn%y04WD2!kBUMA)*o$WSfd$uRkmB}h+CaYjOIa=Z2rdV#KVlwwVhb*b*Gk4#` z?xACD?2*vxw$j39n|eyx$0$?c=TKD2hoZR6ZT&g2RMLDULo+t4Dt@)wTwKNz+D^T6 zWtQfH6{XM}c7p1FBedVAh@RjY0>2`5@ zHiPFZ;Y28w=`MV-@jXfAnXx2h#5k*-GJ1YoaQh?fGCX;M1yn=CLgNEWvV-27Sd0!pq$}7l_v5flj$KZ-#!D z6pn@ZpLK?90!Ol_Y;S#())F2F{Uwv$hKUUak23zN1iISKQCpj)?Q({WIc^z)e0JHK zg*#RsidCI~kr#304KlOL8FyG#p6h1i*c-hfl69LdudAJIKHL7d>yg~h!3p`@PWF1C*ao2+Dl<(0=}|5AD~C{pnJ}7>glu z8Iwz%uGbxc5rA=TzM!duhEjAY?3p3g^CVlB713_%Z1W z&_lxW>|A&+27pE&`~$(46asmOJwt3cvByO4@wn9f;negnNYcOPvk0)3Vq65w$>FUL ze-+b4@|UfDAH=2-NYfA@5OJ|>ce14cVTr$>qDeY^e~ZPUlD3yuot(}inI{ZCPj~1F z_qp;Hh<%AJ+|_P|x!TP*91Wt!xu}|uy$%q?UDsO0~(i2Eah&$NXZWImUo_yLw zuS~=a| z(0=_>G-dk7_xI5H`%y2lasyPM z7Fcq6e~h?{n?cVJwHo87^!{|R&K zkGKiqlXS&{Ek_FjZ=+w=j-p@-klZ2%OwDHx{Tn4C z;xZV-BhD=Y%OE2{9btsN53pGjzB#Ug3-VXWgyO1xC9Hq4m$y3L~&er)|5G z$j{;qhZdlyQt1!jwzNTMi!H#|G?bCBQXYhb@{((O$$t@j#JOmmTh4M7zQ|+?Z8{$` zsAw}+Xa07SHLIlDcO{2ot`Gv7Wlu-zSVUU5*cQbDHOx(Ya9qxR8h@gkdqhtrNL5r( z0DmmZ?Vd8U^b3&^OdlvZux-n5q>ObYE+%^uuk*19ye!Fo1!5|lp?5N!p*JSDZ2hkA zCdDgPct7LP=}HsJsMaqBEh;P+sFbS(r zOs9g_71$C&N&y(wi4jFY5iu=YJtL>dq%TrkeyQAa!s^zPx9c5iXD?zD67sLp~ROk#qFO_+bWPHO&5+ zmIC@{YVVCB?5%M|HnI#7@0XEL2eB)av`u>#>KDa-ulqabQ(e7^dZCQiCbU4VH5V>k zEG;Q8{d(X$)$eS~OzU?7<3UmYI}J5-=Rq}Xax7O;0)OW9q93H8Gkb&v#+Aog$V@dy zi70GBW5VFq4(}tk^7vD9oHE}IQ?-V|tF*E7m(=(omu)}QJppEllq@pqd=rgDY6YQJ zLh+<0{H_whkI@jN%(wehAIRra$V$>7IM;De{Jmsu+8@uwI#8`@;!qmM+D6*C>!j+u zg5VhEp^;2tR}oMu7ovvWBL9<+ss-GI|1S8QYp=@_{pBvyt-dvz)45RD;+*v~<2 zJwP}@TUvh17Yas>Rsv&_0SXsrX7t8a|tZ6lGbGpk@d<&qc^(L6bU`4I2 z2;q82a{uZUnvPqYHT@jdAp<;hh4hEomLy7(I z>NUB}&_@&E=V6=6{&6Op+s5I?Sj-fWB_DRVMgC5+521`~vbJk3nZyqy^^Wc*UO}lz zrI66iflkgp+-{zKc;1=61N9vqEj=)3h~Q4K7TJ<0+U+5dTY4z|SElE&3SLsHY(_~1X zi?E-+J90(A+LI{fZYWWMNxF+t;nCo)feYlY+D}A)2T`g@1%zKq_5<7FatMd=FNz06 zqEc-9WkR26<9CaVf55TkfNA6JceqDO^$agFqty4p=`Q{pMSqe>?B`K^c? znJqv9jClBCmD34rpuJgQL}-&tTVXWf_}?&ewTAktDXf0xix}4-UirI`UF^F(BAwDV zVaiF9ciQ&ac8ki2pF(Q0i9CXq8q!4WA!uk5ky|;Wez0{rZ#^9<ItT4yc+POrli1sCl3D0m za+K1ME?+d(`gf@gbXffBytKkOn<`tkxk zU|JS_p3TeB-Om`fQsFCEyHdVoAUoLM1DPI+odE;reWgGW#L57Zj#2qc%OJ`{RyD)y zDOV*rS+?Uv`~~VeXD^nqDZm!Hf>JXva&K99KS~9J(j^uMjWgJ}PUX@iegpEXnxYGI z(t79u=?~+_kQuSd-D+;~Y^*VMKWu;u`oncz9%)3w8y1@SNuUU;437x!72)re(eCxU z&@JIaquuyKR-xL1^c^MDms!a^Q(MfQnB*Ceo@V-4eK#$Wm6T=&rSVOzwS39v@DzNhnVg-n1fAwaUvc zm|epH+qP+B7GDVQ7+BY&Nn9z-r^pmLxkEHn?6zsL3zV?6MN1LMBKx{e5f;-x$$~6l zqXX<>mzW|XcLqX_&?@2xA!4b-ucjY_#Zgh*E6+ehcH6)AO_9#Fh5T>XkIs_sfSAK; zX&p$o7zYdD-=`1P7J`d;UI}!^?`8E?6!5gDvWelOXkEtY_Mb%UC0)9=2FC)?uwnqe zMhc~1o{C>480qYUct|v+y{k-lqAiMlELDt3!Ll}0tkd>Xazq*b1+SG1ys*19& zFGS#{MMJmb8dz7BX_ZHy+(jTieV>o_nw;<-J>lJ5pAIh^Sp5a(&e>Sl$luP{G%#z* zlL<-&5LLNuFQRtlU7pBA4}|tl2vob;PkzEdELm)tlbQb~=iYc3*CT{l2;&@~(qm4x zKygn~k6H%V+s_vZNB4&394l#eA2Xvj{@I;Bz@;R29dm^_x3(*AzRa@^n6@D)@JGiUh(a>i)rd1J1X z;f@w3i#=H#To%Am&bR??>|^yVni=YL5YTk0z8UHrc^L6m`(uW!n>#>E{^|8E?!W&j z4B+$AOlNMT+?ZaoWP|?s+@+KK+vI$WYDI&4+lSvnd?EziQ%TxLl3d=RA5r%BZNtAq zEDU51*W}%bmkX7=>{Vd%9TzD@uAC(Q$6~D+v4?XJlHpen$5eP9xDUS=dVP}HCZFP# zE0vh=KL}wUDpWaV1p3;CpRK&8gsb3AypeyrKZ8Iy?brfd^Zo`k6MEb5gG}c_Z9u*+ z$r_-lSBm$2g?8+I-tukP$XFZctxIa$gZ(Mn=isY(zee~emYTZ|<h~!TiDAb=~-TPLAL&_Vd z#kMa)HS?##!n>F|9Tsb1ce_kIoF2O5LcteaG%D=R3j7COpb@nxTsQCTXq6|!`m}3g z_=9&C#!$5F!|NuQ*cq6X=5tNQCtS0)+r8!!S;1Pljtg{px!Cb?A`Prp{L%Mc$hrMM zVQAH;(YK6M+>38#mIQvlhi$ZPu8m+%^YBam9S&b|8~?dgFRRZTJ`;CRfF)6r;YG3@ zu!`3zfGQ1g%r%9<$@5U{t2kctl*fCJD@sz=Bimi=x$5S0PQ-{;K9`Mo%tB=0v#a;J z**L(7nel};W!*B7@6f2j?0*giLb>6az!xK1^YjgaM3Y}~V}siklii-?C%OXrq@Px& z*CO(1RNYa31hWxa*%}g;xI=I!&a(=?3Y2(kM42&~_{vX6Nd56q^z6_jbv!XBAAc4w zAoHtt&85y3Q4TI`3vsumR;YWpuC5iu+o&fa_6;G8 z9G(DO3FtzXj4*R49~JzmC3u+x4UQl2h+r1MYqj!;@Cl;u_=V&(@C5alw!X=?8A`oX zId1Q_SYrNzhXwM&cWyp8%nt5nJ>GZNb5iQNek&_A=NXBlg`bv2CactH<-XKDJ0@$DX8n3tS|)MR6(#NC zWEGn^slWiamxe#2duDzv!_{6WtD&;~%<^`3sqILqEy`WBtU=3uD|g|6#+NP+XI`K& zH9pez^6HZZPS^Yk4cG!Fwa+=m#&2#!3>*wEM$RafDQDI#jdGa9q(|TLGD!}iKf|%+ zepZb{-*){CvG_PJ9}W59I;l_0?LJlBj|WbnUY`4#+)8(`NcSwhAAD%LE%1(-#)!u} zN_NeUmm@2%i21FLW3#D+=l&LCx+X>JJCy#8jgY~MiS6vrC4c%6LK*x8vNmFaNJ;-g z2*XdTc@(Bfj>hC56q^{Nxlzg~`cqW{+qtioy$0jIdxcC(MBN+Ob8d|ls_Iwtw6RD# zIPKkdGr+`W=KozRPD=WIM3po!A#_QF&|q&Ht5o>dTX-5AzX&?i*8t2!Y){l;#btD= zfB80jg*$x7{emWZd5(K0;|ECpHJ7?|5GFq|{~P>C@ZWh=$_Wk!8z;gqw0qc1^+zY8 zh@7s!4|mc$vTT2i++Ztw@|H0jYJI)%$<>Er8DuZ3;nG8I0<$~PoXc739Cz1SRNc{O zdpMoT%`dS%jQ%{Vp>R1?ATPHxlJ#9KJDy-~y_Gc1-#Ylur5bE9&Oab_K>G+6Uv*Y1 z@;#Y*nTvmpi%Hl?(SFsjy$4DQpJ{xR`=mTe0@anE6NH5cWvBzIdbVNx%so>^-|rL$=& z#4SfI=0zr-)Sh{=JD-bA;dka3(XeO$F1==21Io@|?}^+6hJjLihGMp-Fj{2)L60YY zUs+_?+n%m>Gq~atE^W>fj@eyZp2(TDvPfp8x@+r278UHF|KOD=_wBO70^_kO)7;_0 znZjdcW@?km(Z*NZTn^&Se~r(-9rHvoXSBT=ly=`n={B`alEeq|(ca=b~} zZel->*o$5#jQmC}<-OI_{>@|VNYgQ={qd*5&u(*oI&^q~lQpsYBd%~pXmHFe-*H9e zee5axuxUYi#St-#D_r?36zvPYKr~l)mk52=-uLj0KDc*3R^gGcvXMhJQ+Nt@H*pT= z%8!VFmCa69rkP}#FN#5EmV4Ts(BMhIYrvOCl^Ul*2B!vZ(N6hC_r?;DgrJeG_TnSX zy|Ku=@viVod-uCWcc-RRazaulo3>|fekCCX)gjpy-J`uV!yd(KMAZ zw!=PI7iZ5%S3F1U-?ej7Os{iIufb&#?BGUwmDo#qo#EXa6sU*dzq;&?ah|hlZ;Z9W zz<9-8SNJJ5VD9}3!R+kb8xPNB`uC_!>BG=I_B>p1Cc_B2x!{#MrQgfx z>L2b3j|_b5pZw zT=t5cDI4XDmDjAzRxBgpqGbOr+aya}l8tQ8TxM=Ul0}~x)Ale3E`@o?EB(bda}16* zHmmX0D2C^yF#A7Zr$fw6<9woSrSWm7j2yJRVUE`dFvmr+lEnq=u;M1I*i*wPz|PoX z0Q;jft)p10&TusLbwRKJ6a~^|I@0>T0yg{c^=vSeGEuokF!aQ31(dZ<&SMiBYo>0w zMDF^8&sjBly!$$74%P)O!c+=_m_N z&KtNVDSw`wnR&uv51Y~k+G{!#42h(fKN-iI!qiF>x!dW?mZSzvTnRigwvu?@w+0)k z|7@r8dcmE~Hq(#YU7xZq=1q=#9Ed$2C~4d!?l2z+&naLm;^rjkz^`ueh97fE zRAQ+PPi|izvq*~}eI+T>b@wKjM`UugcTinzm)PI>toiO~-eLI(2i)z8`TXbQiU;bY zecZ^{WAYv&Gf!X3&t;hG&xFADPQI%1wi$wG6xLO*N+bw6w_R>o2 zttdB6zS2RD%N4r^60Y2Ia=5351XN%}+t%!r!sI8|Ork(9T!3-T^8vz7Kf%gR_*9~=WMH62!f zrUcq7l>oh(uC@28Ns>rxf=rYq+x@`zA;r1YwiR(7lo6W?4ld9mh$4tV1GMdulNFbW zCb*P?d!h=ETWQWRb12KuzXVdFu5fkUB=+};%HjJ-*WFI2EQ7vSYA<;vRzem^OB9(6 z6rQwFelVok%S83T%QG?@S1*xMg!kdl8#MTN;TPC zFR?PjxUueP3<+@blO-BpNa@ zJ}x~LqFvY*7^jxeWD1ZuE}4t#P(3&!)xafE?W+6yU=t}L{;6Eb9tro){9#8B9+GKl9(DMU{py;{gU!ms>Dqho5W#%uUpy;{Ae9riX z^yH<|V|7+QzDGoIfqOLU*6=|MH)?o?hPP_isNoU~=W94y!|58HtKlgcW@z}~J!Zc9 zHQb|Nw}uaDxKYD9G`v;AMh%x}IA6or8cx^nTn$grFhj!+H|qQ~+@oQ)f|h===2N&P z7iEOy%^nc34o^4tcu>V^!Y}TkT}r!;72{!>uYBB-HN(it`dQ)D^ZzP{cK(My*L0I^jKH*C z=j0u#Jkj$_Xua&qH=iAoOxTxa!pa;IW}mI0ezwvLg`MY_@rH&EeI7UUrPT%BZ7A-X z_VL&5eXr~4z(whNZr9I0)bM8--mBq*8gAF{j~d1_{7}PV8jk#GI{pv8^R?eR@YJ*8 zez@CcS?s?1jt_=BZyULB;$QzUu<{zwYbfA*>g=8osphzr4Fy0rQa{q@h>r9%S;oNf8B(Y`Z;=; z`Rs6+ut>+Z@{g+A4daoooAk6)>v$GiWnZzxQ2n(06TOu&q-KhE-u}ekV3a#_EZuT)N}SGh>Fwv>i1g>| zk?GF|N1M+M4GjxwoE;~p$4|-XbML>vf#QZ>UA3dWA>gR0t84I91$>SLr4^3a`f6YE zRMLCpef^gM4sWARUV0mr`|Esx+J<_Ezo9WO(_thr)HPI9J66`#H#K-~khJpq2k8UL zKv@k2qj*_WeZ8;FDHJqWPjec5z9wUKu%^b>sNa|m<_lL=lUseGr0y*;8YE*@S)FExCQ^&dn?{& z!EcdY;nV^}`oj9EC3QYWputhy;Ap7#O$pR4_fg%of!eCN+8e7>&DJzDI%+B4R73vH zoVg^hSYqfWDOZy*eQLqf=_*A1^CtLUaVQBbyGLLPXG~_`SkD>k^~{-OH9F@Qwe>#e;NREGGzs}?@cZf=X0(|~G#&N9f9`r9Nn%>sv})uJAdQs0QR{ z@>h9Pye6cxwx-rsJ=JJB0o_xUS0TUk)wT6YrFAwrQUX>s)&_h{Q<@qYy(>WLz}Hk? zjSqU3WkYj{YZ=GpES!XG(C!FO+==VVnBw;d%a;2L5PNSheTsd?6idA7ml`PqDGE~q zObSy&s=QvGA8}36nxNm9a)Zz3r&d;wV|~@qI-f9h$nRg3lYdelDMVWonHl}}%E>>K zN3)q5-YQYrNT$Q$>P$=aYMYe4*DUo_iwGMXr_pDR9~>Nodp0NOR;E0Em+&3Ldjq&B z#g!7rdJcH~Nu8h8CMPyYi4%2?(lxuAtIZChxUSY$A4tCUH#7wtQo(4cM7vJuY^?ss z)YDdXlT6>}Yig)N{w0?>M^$w-0ZqQ9nGWbUo`?lre`#$~z*p~UOvyvhpzDhqruZ6a z=ocCdv-PS9gwN7>(v%!is+v~SdmXxIiwT7eWCsA|QKp>#ocRh=~dRf+hD`Z=nD6*LJ2vaG6UnIoXg8c1(kMc-)3 zr!YvG7iF6YnU>+BHHXPQF>su!k0=J=k}B^FO~%BgiGE{hiXKSUH4dXHNdDf&x*B6j zfDX5*PMJ+93c}QNQYNtz=W8CO=TltgtEvxTGFDeDuUd-rVJaGm>uqSPHVb5+9IG2v zHknq0!`o0zwboS|3%qs97B39eRvWq=E%qG;rzW^D8$$tG2n$Er49d`9T;`tdbb016 zR5Oxqo#h@!O|afHc-6pQ1w0q#O?4P%0^+CCx>sp#F~1%Dsz&;6k#EZ`F=o%3S6bqn z>qyBqVVZnZjoxM2(h`!cu5gyl zn|I~Ha-CkKD^84SbT^Yk)L2zr+klcX+_Os;mMoYzZ-INU;=|%Op1Ce#c3>If1b@?1 zufHzImN`_!oZ7~wz^8+%$T0~njuIOa+gnvn2avD3icC) zn_}$mQ_0(h7%h0I8WHe(hOJYh?+i}%4dBH*hOT6$j zS%FnrVSee+&PlZcS`SsY|9)L&4({VJI}*5cE5+C#vgwU?>s8-BII1FN(HB%VCd5<)=%I zDafYiq|rAtAF~{m_$IPZrAjT=&~z%#BBgi<`8S+D+P&u^LW$ozsr7I643QMar)Hh%`^Yu|Kl~AV@|2_@(M#YP}N}T zy}r3JWReyh@HtJ*ZfH2CYI!YF8&;@JS6s)C*CSmAmQQ(OL!iOiP)EaBE=H*_yRKnL znjC+2J7f52_O1+NX%vbMg&bsRR)nzybopc$j@KCsEF*_nnPjRqN~@Xz6|h)&7B&&0 ze#v$!W0z#pHd{e@hGu8v(2C z;Fo#b;NZjGWNd`H6L+)tuVoActOzcVDFirYlwk(cFu@dTWblzRQxbDBsTz*XIXNja zbEeY%Zl$rb^(zf@NMB{m;g~OyomF~xvU5{NQ_!XLStm&3GCM6Pv5}H2L52B#DAple zTM{qXnJH8kmJA=-2@QQ?bu}p@4C$VdDbjnMbh`gRhnPf=)}bbY&B0Cz1*NJ}(G71l zc3}Nyy)3D({;XG0WsF?Xn$Hw^N2_5?37V6ZiD?oV)0Eo!ZwHy+#6mSi+?cXV39+iB z%@O2FjXlP-u22`ekPN!1h6wNX5U#{Ui4XyODMv&^P@XvtO zl0xD&Q-xv;GbM=C=|@*#dj=Wjqqk8{GFy#pE;F7@f#8xR$8w)c>@wW35IBDIPu6+* zO!`M@_*lezE{K}Y!t+_M4BKJll$Bq*$k7-?_c7NhQ-n12nVg#Br**T8+Ze|%G2^I} z(Ey#RGQbUonsKU0^*>Ixq?QSh+8brMF%e&= zCHVMEpkBtTDYKW88T&M%{l)5<=%4IZRqLyh`AFvCt=l;NiGua9vVc*bD{JZfF*Y6uivtFnBRZ4mb&wnrM)j0g$ zH}ky@xDPj4=f7&587_FYr1*#C^Ic6}YWP82w$!kp{+$}O?G&H|!id2N6^(I`q-GtvdqF_twRHwY}JH@MD>z{t4 zpzjZV5a?LknK2s}Tj9P~;Kn<@upC(Q=3NCFfgNY>cMPK^(hOAw$M?W`x7R{we4_@Ipx89J5tt>1E^ z$yi}{jYcEDlu5N}frhc7$TKfBrK>Bl0Ub^0` z9I`%b80qn|uS$(tnn>q`$ZJS6K8n523GR$n)jJ#WNj~SkIb1*)cmL>i@yLk8#f7mIdCyrd`ZY%yi;KyegMlXKjEXE^e8%95VfvyRL@s7j?ZpIzM zzXkXlF7w;u1w7AT7^CnP0N=*t;6DgF_Z;{o;lM6j0scPV2e?`IjdMwhE5cs@Y{0qj z`z7o=&Whj{_(R-v_&b4L&4UjT4%~$c;O_%|ccNk3jK2f;o1PjgYf z_#MD!<`~9D`1^ow;WC@3J79~OI>j&WC%7E^of3A1VVsFS8+fD?*~V{_K|8Kg;sbw* zTO{#;qvsk%rNjqbgY)CB1im%TFq-in1in0O%%JdC>^zfl3r3n>@=DDYj}4v7!^ z*&^x-e<$$QxK|_&aQ*d^3x5Z&!3%x({lFevc7QShGpnH=e>QLyE)RbZa5b&~e+%$F z+${WCfM4_>llUFLzv9aA8#RV8b1CJ*Uj)1x7m)bCiQy<9>sOyH)>h` z!`+YH4|INu`odohtibi+uLQn=dm4W~@JBcBjK3532JRjF2Z6t?qm1~Yz(3=%n33q0 zFkFtzPAtcdJ5&7ihH(;ZGX88}F0KH-19%JWBK$4DlNugz(Y~Z=LKKup1+i=h0?*RT0*N?v+cu5dm;V%M?SV3FHpAB4& z%jP>~e&9>EGx7HW>sCUOgaZqj;R$|$A)E_;2XOl;c!IwV_=Owc34RCgI-DPWCGd=! zXb%z&{O!%OW&Ba#``>{l_zwbCu7)T0TY$ke$bk5PKfIN4;qL@q+Dg6SF9QCtjk4hH z2mau@v{{J*Y+q}D4BCJ%;BxWz1FOSKy5aW&JHJP{@C%&ZPPy<`0#CUeJ&WG~{5`G| ze-zmJ1KKbCK49&glncKf7~ep-@E-*J+fS$`{GGt1|4tqf2l!{)7X1Cd^>)6vST+d>nTKe-!wIdni{kv;kZ1MepFRypQtYa`1NmpTnJr zKl^^e823xsvcv~oi<=?wfxB@<`1^qG<4W-#1Wx!BZCT<2@5U{{zXkXZt{Q(7nEwEM z4gLb)9h;GP{GGs0x6sD%8^4A(xDEIPzJ|LWe?PG5L1bIPflD4jw#5&;3)hRk6L=8! zG=71Rhv5nS4q*0gsCWDVFUR%cF9&YNy@9_ED7RF_Bo6S$xOeb(0&5-xFMdC;3HK5H zD6j{21b-j!1>7Tzk|>C9l+|xkRSYh;G?)A z{88Zi$KfshN?^E~HiEwcSlC0m!e0bjx0AY*aA3_-@D0Boc;MX$Y0sbQ3yx}(E zBmCvSt45ISCgK2Z#bx2|0B*%)e{F93dZtj)L` ze-Us4t`mRD$u^@6w*`L(aJ=1SJdED~{5dX)zc1TnTs+QZJdM8`xDfXW{{NXj$<3BE z(#V=Us$gX02>uNxT!wo04wk2C!vh8ZJfODOTijx@&F97ZxH!jOZ^*}&l$ z#_;L30$XZ)37b#Y(EPaJKl|3Kf|Dx8_(zwIijL^Z?9Vs|4xzD8(>SzD7JeyHY92%5 zrM^q`TfDI7{YU9id1aMB!sct*tgsW+#kkSNICwOsV07gu|H$$Y(agS#erO(=pYUMG z33%{X;a2%m>5#Y^Pk=+pxP&|vE$1Iki+z+~x1D38N|Ej4QO3!8(y3PRlbY{?y8Koh z?ALN3W!saK3MC6t?^fDW*hu)PV~kU6>y4q5oAQ;LE?8E%ZRMO^aeWl`US(3LFU7r=M^8M!VI92?L-;%~^FR6K2v|4Q- zHJ%mrk6VDrH)tQzpwae#}I|Ev=snw~zd) zeON-%M3wzCRF@6Sb7P7uT7HRR$(GdFP`#^UWBl>5Vev3E58;`V-zr6_P4!u_Jz{uz zKPxy=aa#GL^4tm=T3!j4b|*Ag980BPXng4xrw{2DCI1uY)?vev{YWZ(sd8e~rA41S zZ`9?nbcf_Q{{(psr7blNDZA(hKe&eWPlB&;2tL7;suzXERQ{#SPo_#PG_6lL0zhP{f5?0Q~h588ltl#~QnD4qZ?9=dohM#D7 z%A;ny^EI5MVX20d8a8UUPQwQ@?A7oU4G(IVxz)sbriPO>oT1@d4XZW0Rl~b9d|1QB zG~B1*0Symnn7Pf&=M)X|G`vW|D>Pi9p~bIe{k%@YO&UI^q4~KI=J@pU3mU$v;X4{0 z(lGP4nhp)m)$n``r)xM%Lzjl78eXMgwTAT?HfwmRhPP|@ZyIjY@Bs}U(Xd;?r!}<7 zy-zojcE@D2^{*D$K#ZVjK;aKDBJG(4hV#$%d) z8agzbs^KgROEtV!!(|%!HM~*7TQ%&^@GcFla(&)DOn!gfK1@DXa`AclFgal}oL+UH zrDFcorQBms((J7ZHp$f;KQvNLLG3q+IW^8{)&>r=>6?nwK{sQ#&3v;!T|2~0FgBNu zn~Z$L+-ky^w7Lp?;^InMiJT=F8f|38 z>R_GEJd;J77bJ``7+&iTp7D`{aGE3Nt4@+w!JRt6CS(0DF{IQ@JU-6QRpW1{zpARS zwn~oI-jVut2-QDMd8b)qY#zn|sM*C8r5vN5%~6q(X2bXo3G*!pF6Hu=a!!HEk-vk( zh_T{0Mi?IrE9FL~(%L1BRgJ5h?CHpa^Hp_ny^@iaSr)7d)C$@K4OiDz`-=JY1i+ z(I;nE&$dF;865a9;_9l}0Mbywsgyb&7iu6n#z}KVlf`$aoj3Q(4;>C5| z#d4(Ik2o!sBlkwJgf3sQ*c)tI%tbtAps|3^+J+_i>1Jbb)$*pLi+#;vazAS<=8Bwp zR;<5aQL=7i3li8d$}@FcTS2T%4a-LtT4~t21jvB#i3J8g&5=D}0$msH)>8gZdS$ zon|iADelVT(3;o3$}s+`__#HNRme;ltU~y7Cih!-moqYYPJEQBftB;U;;-YNsPTgM z>LtR_%oQ~j^`{PB>1|T)&l6vuVfhlMo|rM;@2yZ0DDsLFUYH>lCYU;atEy9;DtzA# z%E8pCI`+Y+@HveQ%gw}2<6;%Epuu`}7+g4@FK+;kJRwO*H85R=%;VHlU6tSOtDf&` zqOGw0C~rIs-T?U&iU0W1foKfpac|^kVDWLGaw_m@;|d9%@AIxO=83O@tDB5#m5)PI z0ok#z$ymZI0_>zRymA+TKB9Opm*A)~y2gEm)SjFmtCdR$jQiz2tHk32@+4O=sh6AO z$+}R;*dkAI@NdZ~by?Gc>e;%w=^^WhQ?rKg8)I3Zp}xuZol&Rm*O21+be<3Xgy8c(VaQm%=fc+|3AZBT`_%ek~3!C{rUg@138eLevZm2fpuQ?;4e@7-ir4* z{G0=ybKw5~4rH^h1~Ql3%|D0DxJy9ijLxFY^3Fw_m7U8v{hiI78#?|HhV$Yd3am+_2HOXVg8}_vGHwdC&d#Y`G_TPwzc__w2i;|DM=A2k$v@ zkFjagrtD3QP5wsdiZn}TdmQB%35LLjw9--|QlYUX~3%>kO#2zA^jeAGkn|*KY zy^eb)-CJ;P<-N=9_1|mU=eTdueFgW;xX=GU%L5$`bUv`8$d}?JR%`lEMGozo|7`SV|FxG!5KkEu>fCj7yF4Cqm?0MKaafgczr!hOiyZ zC`b!AhY){S$c2Oyotm0z4k2HKDDs-F^aFfHUkLZa8D#@Q`+(n8)KL>hFmTt_A3`qYqY)K+#{$a#b~(n2mFq$Mrn zI!U!S6;C4}{11rSJ{f0#D zE1T39nVPVERva?d6fUi*UCeBkngeqtT6gwHYWYVRGdgYP&%(n5*a`4loxpQ8csl+C zJQQ8ysbOw2JvS?_mH$Eo}2!KII9zJ=G)%m%J zPRWK92XWST%nO^iZHx)sB(7ED-uY9D9xD|BrBhJ~KAcdUOu!q?4v@-!x0Omlwh+>v z7SbniHd=8k%s(cs?uqJ}mdm79f`tid5N7B&g82>bB!3p3`v@}e_*we!rlL`ik4WzK z=-f}xz9iB~qbR!-N2U4#AqUe!E+nL5lT`|dR7yzp&r?IH2`NttSxHD=TF7mZ>fY2; zn+WMlgjnTzfE>(nG2^$m#y_nf$sitOohRU@@Jef)pjXrm%1_ex#H?B%z zmU8t#PxK#PwkUi!70-|s=c=nKlg88cFW}jlz++0UwQwP{$nSaR>Hin-{EoOL$4oqa zbA2hHA>RUzvBToLRf_IJs%q}cPH7N?%kL1W`kwi0K1-b8j6brUwJ=M_ctSeTLL7ud z|660{ADib@-SMZ{#L6}x9kisZTfIT10E2~uAK6w3JB>*UP3$I4noVqvo!77Jm)L&a z`F)c(-6YIlQ`A2?vq@RTPb2ZeXiRdqY6>uhhs4OJm^_J=5Yfr}u`N}hO$&7CP+^~Q z&;51YA8y|&m7ni(?z!ild+)jDKKI^pBNB4|S7=q{sfkv7+*CwaI;`mbkKVZlJ`W{6 zT}$x>a$mpNn~%u+J3{kVC`I7>?Z~++<6=Ll`X3LegwH6*&hO>O7|60g=0L{2pVKUW z^gWRy?+00YGDrRrq(oBR|TKOOooDfP}I?8q3bO zJx5>3>c=C+Qb^UM4+cKpjeJBYA@jW8ooJ{F7$h1 zc3o0E+gl#q7B}%xqA$=0P`n=GT0}x!ka@5YCy@C}-s5K#Db{`*Bosaa@R4(J>K{Vm zRwnzO3rOJnf01)<&k8m^_ue!=jWj9dTGB;Pyh}<7k;)TtzkzyE9rot@GcSOK!*rfc zu)~vi=EX5`UnlQg<4%z03U*WFsibd2vXsd$c)=q-38fIU3g-h9L(8SB-Q*SZ9M*&z zCRKw}tC1$GMemF>VV!zMM8b;lLPR9@KZegrt?M@0C18a~UBb67Rs6KUY%*31t?L{fc$R2O3^U8m>JqoMgKG;&^3bttLU zK`y0Kel>rap}7Lh)>y8v`(Q1m(p>^{+=w8d>jlRRpQqu|pK8$WOxTF2!V3R%Or^UF z$UP!uxlk$ZrCi7^-F}M{qIeF@=U$|!A}{>~juK#SF*BKBtVmp~MulIwz`6W5>agZ^S*8?CMM#3+H$WM4EpMM8g za2gB9qagB?LCUW*&wyMu$d+FenRRFPmbmJF{d}&CmqT;a%+o_U#imw9MYF@eh7p_8S+0MwxrL3ti;@%HYTVGQmHmdCSWD@Q|c<|@aC6dk!}k* zge%Z2npDq#Y?!*(LPab^K9bi>AnQhE zS(mu{T;99y06A`C-VZWn$~6kI)6h(V$S>9MX^=isJ4+z4!jfv|BOtaneHrAU$?IVd z+nc@*vSxHxuugK(w6Rx#>@a+`gIqTHc@xMsBhOxtJ+W-=U&cuiZ)W;xgDhMmARf@3$n+wXIU{DF`N&Ah>c2o z{t#rvw4e8YG)zg)f*de>{v0H=dEISCeVN=p3C%yjC-s!Cfb<#tJPa~|Bk{EZUinn~3V zKKzo*_klFF1-rr6R1*)a51Q1T&qK2o`$fZjtfc1}nomKq)5!cK5HTHz&o_j|w5Z2H zR%7dhxOmacs`+HcZUVV%Xx;>}VruySi2PDszYo%9+C~**!)W6S$nq_DJ3I%nZu0sl zh}G)nKrR}Ze*q~>xxNdsVzhAs`D`;S;WiNYB`@W-KsF4Y{UEERmQR3OFs1ktkag25 zmq4U0w3fpT&eikzDDwa`t45wnAU7I~Jp!^P@)22o2+}a}+(17rOztlO={GdDfgCq{ zc1bFlcPjfJ$fA+w7|2zl=hGl}kTA90`#~<6R38F4ZgO7%88b9r0qIi#TpwevGRU&Y z>oJgZLvsTSe$jB=3PQ26Qfvpg9{K2wSz5yRT=RE8b1u?2Z;&3>51-U}-zj{I<)1LA zATItCWIC2B?8Cia^7mCPLGcwPBEJi+(`+Gsw4e}Qt z4;o9m0&>;xc@X5HN%d8bJ*HfbfGioBr$Gw(m}p6+f*(}ii8IF4G^p6142W}B+t7+M3_`x3n1G}Ugtn|8k+MU8z!%R z0C`O;7dFbBcIh`7`#LoJhR=6E8b+SSL9C_yG02EX^^)76F`QomvO(^tpZ+e$CDY^f zgRI4#Qp!gXKf56zDUb8s&CiDp4UHClWs>pmTTaS-r!4*pIKeG< zdReZAWj|smC;OUu=dy-M*xQmbeiOz2?xs9DMY!Yp=tN z>Kq#i{dMfwz3A?gt=5gNOHV4d+i`QD+%T?(Uq#<~hE*>r_IDJsSI5-^H9B41Ge6!4 zcNaV0=-io2q0#XvE=SC02IkHVPaYfacOiGY2{$7js?+?!9oDKVm!Ho(yfcy>oeMqL zz{jfd=+;1V;~vE3jgz%KVRv?lW(i<4#J-DaW2otFe2(f%PpX&nOplibQfjB$S0Cn{ zQ1>^r4FmKrSC>PMw1nNe!V9wD4n5j-ZNVwZ5=he;;5Z|za?()bkpsl$S#L#5uj>t1 zaKJdy*Bg9-a*s)CwnvA9m6GInXolzXPDvs4N4qg0YV%wk2Qgn1HisyFFoV^}aZY7w zQw}GhgIO&{xFoRRn1lw>TTHZ#8Y`TCjtW0G-T;!DyS2rMh~?p`+%+!;MSHz7I5*sO zo)0C5J7*`j-574^O6P?3zP!@Y9PvkD>$BJ2+R=f{KExt_x<}fxv*)rA zugM@dQ7@;sJ$8JiWP8fIMJfR~JLeWBoB=NRlb$BjMcJs>90v-h?OJ6?oC;8 ze`4wO>9DJ;O-cqcBq+;pgW+TI`^GZ-*R z4u97sByfOSzqps<=tU`4l#UFBxB&KX?^$= zWv`Yqa*;l+(MQDZdg6Khbm>;8%YC~Bc${l{mZewObHuAZI#ox>o^HN5U7KwAht@n- zTi)v*_U7#NN_zidmR=`Ss2SCEoyc(iXQO`REkfoWXYx!~>-O+OZF188d16=KHxc>C zy10_Y{Vzyw_OKMJ?z^ohv{h-4I%t*ozLFyOdLEX8`YsLuC-AG4Q?06{=k&STDZ;gQ zdA_sF?2LW>XWUSOa2-egd&Ruy7CRt|3XmPXPcZ>{MOnxq0T5;Y?Xp);)=A_zwu~bF z#C&b;t<)O5Z|*FT3SEAj9DxoWJv2IWaIhRXGI(^~aCvy}uHie&yM~U8;$PRWe`;F9 z9TtaK7+5s`=tc_SBGgXM;rai`>O2nY?p@_LBav4iOR4;e3n`-ZVR>@z%)~ zvG$D)9}Z&pY&-SrU!b>>MMumG$srF2z>eARi6o<*!p8fbAeKXzzDh*r>Ib;^Uk;* zh?&9`V7G{`5rdmx5n5%V>8!BNg2KsKkPLL(R*f%&t66C}Ez51MWu5CDTi91(TFlO} zFH6)fwUyk#5SYXlSAuwS4c-w;Y9tJ1Z`MUvDQis;hdgZNvey-V)a5=q{+{}L*<^G& z+iYR8a8u>RZEYB`nT=fpL3^5K_mD72vv+p1Cf!<-U($<6rDHBT__ZCjbhmml%q0la z5a*MwC!~lTDX~sX+gjUv1J-YjEzNJgQQ=d6B5p@&8USO#G7<=xv20o%UeL z1cL~4Q#xgkxq|bBd<*k!BQr2G?6&F_&8YS1h~^WxZ*F!}Ei%FrJ|rZOvIZZp*5rEV z%5=fnD0yrA7flqte#c~D!tl?;GU{|m#(&ft0;iyI(=|nSzcmZocyfcd&}Dg|nYym# z8Kc8w5q0#9em$gV^ft1ChxWxD*I}_@AHj5CxNZMz)4KW=G92O^PjCfv$<}CQ4=Rb+ zFi{t_Dpwap3@zHk3=*x$B%PQ^Vg}wZ!z|8JMEh5O@KO@yBSyf35g@Qi**e=0v5dgY zQ}rk>wchj&g4Bi`DcXYkgl^}or5`&aQP;>w#*in;p09G~ATnfm^RG6d;lDtscHM-= z8wqR%6S9$+@s{(iM_TyEQdTTl9cLn%G910mXjMrlmS4xC%EQhR&CTu9!+vVuEBwFO z)Z8pyrfWxj48g?8ZhgF<@g}jCoK@j|bv|RXHEMUc{XM>w_+5&cjq!tq21b2}wR~CG zMBRQ%=4K6DhLz|bG9^OXiPn9?ZYIbgSg#MYE&M=zH)awm5M^h5A&10v*v{RXF zj!P=@L^jnd}pe4OLM)_&EDC2xzlW@m`O zPj@~@?xyQV*h-q~HV49XZ~{P-QA!_Zhps!}QZ~2+gLCVc&nz z-6@L$Q1Hji{It`zka?P#a#5R~U29HlQwJ$UnzXlhH)7J%8-q>TRFYI7MxZjzzXG@( z-aS4cL)s>W5`_-9lMBkTTi`=|gZXgB8M<4-_75+iTh-)iTjtDk=RZRj%IbVMiT5b; z&2|eES~k<+G(s{@J=tuXEN7dtK$N)|-G;)%p@-Y!^ryRTw76L{rVeZ8xB2HEsa1a@ zMboE59VWuJKnA;x_>f-Jv>zfT+Tn=SCC1A6hpDF|H!-|x*SNBbfgxQ%ySPv%+(4YW z@ojxw`N%v#B LUP_RETURN_ADDR + set rec [LookupServiceNext $h 0x100] + if {[dict exists $rec RemoteAddress] && + [dict get $rec RemoteAddress AddressFamily] == 32} { + # 32 -> AF_BTH (Bluetooth) + # Further we are looking for RFCOMM (protocol 3) + if {[dict exists $rec Protocol] && + [dict get $rec Protocol] == 3} { + return [dict get $rec RemoteAddress Port] + } + } + } + } finally { + LookupServiceEnd $h + } + error "Could not resolve service \"$service_class\" to a port on device \"$device\"." +} + +proc iocp::bt::device::remove {device} { + # Removes cached authentication information for a device from the system cache. + # device - bluetooth address or name of a device. if specified as a name, + # it must resolve to a single address. + # The command will raise an error if $device is a name that cannot be + # resolved. + + RemoveDevice [ResolveDeviceUnique $device] +} + +proc iocp::bt::device::service_references {device service} { + # Retrieve service discovery records that refer to a specified service. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # service - the UUID of a service or service class or its mnemonic. + # The command will return all service discovery records that contain + # an attribute referring to the specified service. + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service discovery records. + + set h [LookupServiceBegin \ + [ResolveDeviceUnique $device] \ + [names::to_uuid $service]] + set recs {} + try { + while {1} { + # 0x0200 -> LUP_RETURN_BLOB. Returns {Blob BINDATA} + lappend recs [lindex [LookupServiceNext $h 0x200] 1] + } + } finally { + LookupServiceEnd $h + } + return $recs +} + +proc iocp::bt::device::services {device} { + # Retrieve the service discovery records for top level services + # advertised by a device. + # device - Bluetooth address or name of a device. If specified as a name, + # it must resolve to a single address. + # + # The command will return all service discovery records that reference + # the `PublicBrowseRoot` service class. This is not necessarily all the + # services on the device, only those the device advertises as the + # top-level services. + # + # The returned service discovery records should be treated as + # opaque and accessed through the service record decoding commands. + # + # Returns a list of service dscovery records. + + # TBD - add a browse group parameter + # TBD - perhaps check that the sdr acually refernces browse group in + # the appropriate attribute + return [service_references $device 00001002-0000-1000-8000-00805f9b34fb] +} + +# TBD - is this needed? Less functional version of services +proc iocp::bt::device::enumerate_services {device} { + # Get installed services on a device. + # device - Bluetooth address or name of device. If specified as a name, + # it must resolve to a single address. + # Returns a list of service UUID's. + + return [EnumerateInstalledServices [ResolveDeviceUnique $device]] +} + + +proc iocp::bt::device::print {devinfo} { + # Prints device information in human-readable form to stdout. + # devinfo - A device information record as returned by + # the [devices] command. + dict with devinfo { + puts "Device $Name" + puts "Address: $Address" + puts "Class: $Class ($MajorClassName:$MinorClassName)" + puts "Device categories: ([join $DeviceClasses {, }])" + puts "Authenticated: $Authenticated" + puts "Remembered: $Remembered" + puts "Connected: $Connected" + puts "Last seen: $LastSeen" + puts "Last used: $LastUsed" + } +} + +proc iocp::bt::device::printn {dinfolist {detailed false}} { + # Prints device information in human-readable form to stdout. + # dinfolist - A list of device information records as returned by + # the [devices] command. + # detailed - If a true value, detailed information about the device + # is printed. If false (default), only the address and + # name are printed in compact form. + set sep "" + foreach dinfo $dinfolist { + if {$detailed} { + puts -nonewline $sep + set sep "----------------------------------------------\n" + print $dinfo + } else { + dict with dinfo { + puts "$Address $Name" + } + } + } +} + +proc iocp::bt::IsAddress {addr} { + # Returns boolean indicating whether addr is a valid Bluetooth address + # addr - value to be checked + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){5}$} $addr] + +} + +proc iocp::bt::Uuid16 {uuid16} { + if {![regexp {^[[:xdigit:]]{4}$} $uuid16]} { + error "\"$uuid16\" is not a valid 16 bit UUID." + } + return 0000${uuid16}-0000-1000-8000-00805f9b34fb +} + +proc iocp::bt::IsUuid {uuid} { + + # Check passed parameter for UUID syntax + + # Only checks syntax. Not RFC 4122 validity + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid] +} + +proc iocp::bt::IsBluetoothUuid {uuid} { + + # Check passed parameter for UUID syntax + + # TBD - implement in C? + return [regexp {^[[:xdigit:]]{8}-0000-1000-8000-00805f9b34fb$} $uuid] +} + + +proc iocp::bt::BinToHex {bin {start 0} {end end}} { + regexp -inline -all .. [binary encode hex [string range $bin $start $end]] +} + +proc iocp::bt::EncodingTclName {mibenum} { + set encoding_map { + 3 ascii + 2026 big5 + 437 cp437 + 2087 cp775 + 2009 cp850 + 2010 cp852 + 2046 cp855 + 2047 cp857 + 2048 cp860 + 2049 cp861 + 2013 cp862 + 2050 cp863 + 2051 cp864 + 2052 cp865 + 2086 cp866 + 2054 cp869 + 2109 cp874 + 113 cp936 + 2250 cp1250 + 2251 cp1251 + 2252 cp1252 + 2253 cp1253 + 2254 cp1254 + 2255 cp1255 + 2256 cp1256 + 2257 cp1257 + 2258 cp1258 + 2088 ebcdic + 18 euc-jp + 38 euc-kr + 56 gb1988 + 2025 gb2312 + 57 gb2312-raw + 39 iso2022-jp + 37 iso2022-kr + 4 iso8859-1 + 5 iso8859-2 + 6 iso8859-3 + 7 iso8859-4 + 8 iso8859-5 + 9 iso8859-6 + 10 iso8859-7 + 11 iso8859-8 + 12 iso8859-9 + 13 iso8859-10 + 109 iso8859-13 + 110 iso8859-14 + 111 iso8859-15 + 112 iso8859-16 + 15 jis0201 + 63 jis0208 + 98 jis0212 + 2084 koi8-r + 2088 koi8-u + 36 ksc5601 + 17 shiftjis + 2259 tis-620 + 106 utf-8 + } + incr mibenum 0; # Convert to canonical decimal string + if {[dict exists $encoding_map $mibenum]} { + return [dict get $encoding_map $mibenum] + } + return $mibenum +} + +proc iocp::AddressFamilyName {af} { + if {$af == 2} { + return IPv4 + } elseif {$af == 23} { + return IPv6 + } elseif {$af == 32} { + return Bluetooth + } elseif {$af == 1} { + return Unix + } elseif {$af == 6} { + return IPX + } elseif {$af == 17} { + return Netbios + } else { + return AF$af + } +} + +proc DeviceClass {class} { + if {[expr {$class & 3}] != 0} { + # Not format type that we understand + return {} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/baseband/ + + set minor_class [expr {($class >> 2) & 0x3f}] + set major_class [expr {($class >> 8) & 0x1f}] + + if {$major_class == 0} { + set major_name "Miscellaneous" + set minor_name "Miscellaneous" + } elseif {$major_class == 1} { + set major_name Computer + set minor_name [lindex { + Uncategorized Desktop Server Laptop "Handheld PC/PDA" "Palm-size PC/PDA" Wearable Tablet + } $minor_class] + } elseif {$major_class == 2} { + set major_name Phone + set minor_name [lindex { + Uncategorized Cellular Cordless Smartphone "Wired modem" ISDN + } $minor_class] + } elseif {$major_class == 3} { + set major_name "LAN/Network Access point" + set minor_name [lindex { + 0% 1-17% 17-33% 33-50% 50-67% 67-83% 83-99% 100% + } $minor_class] + } elseif {$major_class == 4} { + set major_name "Audio/Video" + set minor_name [lindex { + Uncategorized "Wearable Headset Device" "Hands-free Device" Reserved + Microphone Loudspeaker Headphones "Portable Audio" "Car Audio" + "Set-top box" "HiFi Audio Device" VCR Camcorder "Video Monitor" + "Video Display and Loudspeaker" "Video Conferencing" Reserved + "Gaming/Toy" + } $minor_class] + } elseif {$major_class == 5} { + set major_name Peripheral + set bits [expr {$minor_class >> 4}] + set minor_name [lindex { + Other Keyboard "Pointing device" "Combo keyboard/pointing device" + } $bits] + set bits [expr {$minor_class & 0xf}] + if {$bits < 10} { + append minor_name /[lindex { + Uncategorized Joystick Gamepad "Remote control" "Sensing device" + "Digitizer tabler" "Card reader" "Digital pen" "Handheld scanner" + "Handheld gesture input device" + }] + } + } elseif {$major_class == 6} { + set major_name Imaging + set minor_name {} + if {$minor_class & 4} {lappend minor_name Display} + if {$minor_class & 8} {lappend minor_name Camera} + if {$minor_class & 16} {lappend minor_name Scanner} + if {$minor_class & 32} {lappend minor_name Printer} + set minor_name [join $minor_name /] + } elseif {$major_class == 7} { + set major_name Wearable + set minor_name [lindex { + Wristwatch Pager Jacket Helmet Glasses + } $minor_class] + } elseif {$major_class == 8} { + set major_name Toy + set minor_name [lindex { + Robot Vehicle Doll Controller Game + } $minor_class] + } elseif {$major_class == 9} { + set major_name Health + set minor_name [lindex { + Undefined "Blood Pressure Monitor" Thermometer "Weighing Scale" + "Glucose Meter" "Pulse Oximeter" "Heart/Pulse Rate Monitor" + "Health Data Display" "Step Counter" "Body Composition Analyzer" + "Peak Flow Monitor" "Medication Monitor" "Knee Prosthesis" + "Andle Prosthesis" "Generic Health Manager" "Personal Mobility Device" + } $minor_class] + } elseif {$major_class == 31} { + set major_name Uncategorized + set minor_name "" + } else { + set major_name Reserved + set minor_name "" + } + + set service_class {} + if {$class & (1<<13)} {lappend service_class "Limited Discoverable Mode"} + if {$class & (1<<16)} {lappend service_class "Positioning (Location identification)"} + if {$class & (1<<17)} {lappend service_class "Networking"} + if {$class & (1<<18)} {lappend service_class "Rendering"} + if {$class & (1<<19)} {lappend service_class "Capturing"} + if {$class & (1<<20)} {lappend service_class "Object Transfer"} + if {$class & (1<<21)} {lappend service_class "Audio"} + if {$class & (1<<22)} {lappend service_class "Telephony"} + if {$class & (1<<23)} {lappend service_class "Information"} + + return [dict create MajorClassName $major_name \ + MinorClassName $minor_name \ + DeviceClasses $service_class] +} + +proc iocp::bt::ResolveDeviceUnique {device} { + if {[IsAddress $device]} { + return $device + } + set addrs [device address $device] + if {[llength $addrs] == 0} { + error "Could not resolve Bluetooth device name \"$device\"." + } elseif {[llength $addrs] > 1} { + error "Device \"$device\" resolves to multiple addresses." + } + return [lindex $addrs 0] +} + +package provide iocp_bt $iocp::bt::version diff --git a/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl new file mode 100644 index 00000000..8b4c96c3 --- /dev/null +++ b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btnames.tcl @@ -0,0 +1,383 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::names { + namespace path [namespace parent] + # Maps UUID to Service class name + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + # UUID may be either 16-bit Bluetooth universal UUID as or the full UUID + # Uses lower case hex. + variable service_class_names { + 1000 ServiceDiscoveryServerServiceClassID + 1001 BrowseGroupDescriptorServiceClassID + 1002 PublicBrowseRoot + 1101 SerialPort + 1102 LANAccessUsingPPP + 1103 DialupNetworking + 1104 IrMCSync + 1105 OBEXObjectPush + 1106 OBEXFileTransfer + 1107 IrMCSyncCommand + 1108 Headset + 1109 CordlessTelephony + 110a AudioSource + 110b AudioSink + 110c A/V_RemoteControlTarget + 110d AdvancedAudioDistribution + 110e A/V_RemoteControl + 110f A/V_RemoteControlController + 1110 Intercom + 1111 Fax + 1112 {Headset - Audio Gateway (AG)} + 1115 PANU + 1116 NAP + 1117 GN + 1118 DirectPrinting + 1119 ReferencePrinting + 111a {Basic Imaging Profile} + 111b ImagingResponder + 111c ImagingAutomaticArchive + 111d ImagingReferencedObjects + 111e Handsfree + 111f HandsfreeAudioGateway + 1120 DirectPrintingReferenceObjectsService + 1121 ReflectedUI + 1122 BasicPrinting + 1123 PrintingStatus + 1124 HumanInterfaceDeviceService + 1125 HardcopyCableReplacement + 1126 HCR_Print + 1127 HCR_Scan + 1128 Common_ISDN_Access + 112d SIM_Access + 112e {Phonebook Access - PCE} + 112f {Phonebook Access - PSE} + 1130 {Phonebook Access} + 1131 {Headset - HS} + 1132 {Message Access Server} + 1133 {Message Notification Server} + 1134 {Message Access Profile} + 1135 GNSS + 1136 GNSS_Server + 1137 {3D Display} + 1138 {3D Glasses} + 1139 {3D Synchronization} + 113a {MPS Profile UUID} + 113b {MPS SC UUID} + 113c {CTN Access Service} + 113d {CTN Notification Service} + 113e {CTN Profile} + 1200 PnPInformation + 1201 GenericNetworking + 1202 GenericFileTransfer + 1203 GenericAudio + 1204 GenericTelephony + 1303 VideoSource + 1304 VideoSink + 1305 VideoDistribution + 1400 HDP + 1401 {HDP Source} + 1402 {HDP Sink} + 02030302-1d19-415f-86f2-22a2106a0a77 {Wireless iAP v2} + } + + # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/ + variable protocol_names { + 0001 SDP + 0002 UDP + 0003 RFCOMM + 0004 TCP + 0005 TCS-BIN + 0006 TCS-AT + 0007 ATT + 0008 OBEX + 0009 IP + 000a FTP + 000c HTTP + 000e WSP + 000f BNEP + 0010 UPNP + 0011 HIDP + 0012 HardcopyControlChannel + 0014 HardcopyDataChannel + 0016 HardcopyNotification + 0017 AVCTP + 0019 AVDTP + 001b CMTP + 001e MCAPControlChannel + 001f MCAPDataChannel + 0100 L2CAP + } + + # Universal attribute names + variable attribute_names + set attribute_names { + ServiceRecordHandle 0 + ServiceClassIDList 1 + ServiceRecordState 2 + ServiceID 3 + ProtocolDescriptorList 4 + BrowseGroupList 5 + LanguageBaseAttributeIDList 6 + ServiceInfoTimeToLive 7 + ServiceAvailability 8 + BluetoothProfileDescriptorList 9 + DocumentationURL 10 + ClientExecutableURL 11 + IconURL 12 + AdditionalProtocolDescriptorList 13 + ServiceName 256 + ServiceDescription 257 + ProviderName 258 + } + +} + +proc iocp::bt::names::is_uuid {uuid} { + regexp {^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$} $uuid +} + +proc iocp::bt::names::to_name {uuid} { + # Map a UUID to a name. + # uuid - Bluetooth UUID + # The command attempts to map the UUID as a service name or protocol + # name in that order. + # + # Returns the mapped name or the uuid if no mapping is possible. + + set name [service_class_name $uuid] + if {$name ne $uuid && ![is_uuid $name]} { + return $name + } + return [protocol_name $uuid] +} + +proc iocp::bt::names::to_uuid {name_or_uuid} { + # Map a name to a UUID. + # name_or_uuid - Bluetooth UUID or name + # The command attempts to map the name as a service class name or protocol + # name in that order. + # + # Returns the mapped uuid raises an error if no mapping is possible. + variable service_class_names + if {[MapNameToUuidV $name_or_uuid service_class_names name]} { + return $name + } + return [protocol_uuid $name_or_uuid] +} + + +proc iocp::bt::names::service_class_name {uuid} { + # Maps a UUID to a service class name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable service_class_names + set name [MapUuidToName $uuid service_class_names] + if {$name ne $uuid} { + return $name + } + # Well known private uuids + if {[string equal -nocase "02030302-1d19-415f-86f2-22a2106a0a77" $uuid]} { + return "Wireless iAP v2"; # iPod Accessory Protocol + } + return $uuid +} + +proc iocp::bt::names::service_class_uuid {name} { + # Maps a service class name to a UUID. + # name - Service class name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable service_class_names + return [MapNameToUuid $name service_class_names] +} + +proc iocp::bt::names::profile_name {uuid} { + # Maps a UUID to a Bluetooth profile name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + + # Profiles seem to come from same service class UUID space. + return [service_class_name $uuid] +} + +proc iocp::bt::names::protocol_name {uuid} { + # Maps a UUID to a protocol name. + # uuid - UUID to be mapped + # Returns a human-readable name or the UUID itself + # if no mapping could be performed. + variable protocol_names + return [MapUuidToName $uuid protocol_names] +} + +proc iocp::bt::names::protocol_uuid {name} { + # Maps a protocol name to a UUID. + # name - Protocol name to be mapped. + # The command raises an error if the name could not be mapped to + # a UUID. + # + # Returns the UUID corresponding to the name. + variable protocol_names + return [MapNameToUuid $name protocol_names] +} + +proc iocp::bt::names::attribute_name {attr_id} { + # Maps a attribute id (numeric or name) to a attribute name. + # attr_id - numeric attribute identifier + # Returns a human-readable name for the attribute or + # the numeric id itself if no mapping could be performed. + variable attribute_names + dict for {name id} $attribute_names { + if {$attr_id == $id || [string equal -nocase $name $attr_id]} { + return $name + } + } + if {[string is integer -strict $attr_id]} { + return $attr_id + } + error "Unknown attribute \"$attr_id\"." +} + +proc iocp::bt::names::attribute_id {name} { + # Maps a attribute name to it numeric attribute identifier. + # name - Attribute name to be mapped + # The command will raise an error if the name is unknown. + # Returns the numeric attribute identifier corresponding to + # the passed name. + variable attribute_names + if {[string is integer -strict $name]} { + return $name + } + return [dict get $attribute_names $name] +} + +proc iocp::bt::names::print {} { + # Prints known UUID's and their mapped mnemonics. + variable service_class_names + dict for {uuid16 name} $service_class_names { + puts "[Uuid16 $uuid16] $name" + } +} + +proc iocp::bt::names::MapUuidToName {uuid dictvar} { + upvar 1 $dictvar names + + # uuid may be a full UUID or a 16-bit Bluetooth UUID + if {[IsUuid $uuid]} { + # See if the exact match exists + set uuid [string tolower $uuid] + if {[dict exists $names $uuid]} { + return [dict get $names $uuid] + } + # Nope. Now see if it is a 16-bit Bluetooth UUID + if {[IsBluetoothUuid $uuid] && [string range $uuid 0 3] eq "0000"} { + # 16-bit UUID + set uuid16 [string tolower [string range $uuid 4 7]] + if {[dict exists $names $uuid16]} { + return [dict get $names $uuid16] + } + } + # Nope. Cannot be mapped. Return the UUID. + return $uuid + } + + # Not a full UUID. Retry as a 16-but UUID. tailcall so upvar works right. + tailcall MapUuidToName [Uuid16 $uuid] $dictvar +} + +proc iocp::bt::names::MapNameToUuidV {name dictvar resultvar} { + upvar 1 $dictvar names + upvar 1 $resultvar result + + if {[IsUuid $name]} { + set result $name + return 1 + } + dict for {uuid mapped_name} $names { + if {[string equal -nocase $name $mapped_name]} { + if {[string length $uuid] == 4} { + # 16-bit UUID -> full UUID + set result "0000${uuid}-0000-1000-8000-00805f9b34fb" + } else { + set result $uuid + } + return 1 + } + } + return 0 +} + +proc iocp::bt::names::MapNameToUuid {name dictvar} { + upvar 1 $dictvar names + if {[MapNameToUuidV $name names result]} { + return $result + } + error "Name \"$name\" could not be mapped to a UUID" +} + +proc iocp::bt::names::Asciify {svar} { + # Replaces non-ascii or non-printable chars with Tcl sequences + # + # Transformed string is stored back in svar. + + upvar 1 $svar s + set ascii "" + set was_escaped 0 + # Split can be expensive for large strings so string index + set len [string length $s] + for {set i 0} {$i < $len} {incr i} { + set char [string index $s $i] + scan $char %c codepoint + if {$codepoint < 32 || $codepoint >= 126} { + append ascii "\\u[format %.4x $codepoint]" + set was_escaped 1 + } else { + append ascii $char + } + } + set s $ascii + return $was_escaped +} + +proc iocp::bt::names::ClipSpec {{use_list 0}} { + package require twapi + + set csv [twapi::read_clipboard_text] + if {$use_list} { + set tcl "\[list \n" + } else { + set tcl "\{ \n" + } + foreach line [split $csv \n] { + lassign [split $line \t] name uuid description + set name [string trim $name] + set uuid [string trim $uuid] + if {![regexp {^0x[[:xdigit:]]{4}} $uuid]} continue + if {$use_list} { + # Unicode chars, needs escaping + Asciify name + append tcl " " [list [string range $uuid 2 end]] " " "\"$name\"" " \\" \n + } else { + if {[Asciify name]} { + # Need to use the list command. Just restart + return [ScrapeBTSpec 1] + } + append tcl " " [string range $uuid 2 end] " " "{$name}" \n + } + } + if {$use_list} { + append tcl "\]\n" + } else { + append tcl "\}" + } + + twapi::write_clipboard_text $tcl +} + diff --git a/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl new file mode 100644 index 00000000..cd014a1b --- /dev/null +++ b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/btsdr.tcl @@ -0,0 +1,962 @@ +# +# Copyright (c) 2020, Ashok P. Nadkarni +# All rights reserved. +# +# See the file LICENSE for license + +namespace eval iocp::bt::sdr { + namespace path [namespace parent] + namespace export attribute attributes decode print printn + + namespace eval attribute { + namespace path [list [namespace parent] [namespace parent [namespace parent]]] + namespace export exists get raw text + namespace ensemble create + } +} + +proc iocp::bt::sdr::decode {binsdr} { + # Decodes a binary service discovery record + # binsdr - a raw Bluetooth service discovery record in binary + # form. + # + # The returned value should be treated as opaque. The attributes + # stored in the record should be accessed with the commands in + # the `sdr` namespace. + # + # Returns a container of attributes stored in the record. + + # A SDR record is a single data element which is a sequence + # containing nested data elements. + # {{sequence {nested data elements}}} + set rec [lindex [DecodeElements $binsdr] 0 1] + + # $rec contains alternating attribute value pairs. The result + # is built as a list as its faster to do that and shimmer + # to a dict on access than to build a dictionary to begin with. + set sdr {} + foreach {attr val} $rec { + lappend sdr [lindex $attr 1] $val + } + return $sdr +} + +proc iocp::bt::sdr::attribute::exists {sdr attr_id {varname {}}} { + # Checks if an attribute exists in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # attr_id - attribute integer id or Bluetooth universal attribute name + # varname - If not the empty string, the attribute + # value is stored in a variable of this name in the + # caller's context. + # + # The value returned in $varname is the attribute type-independent data + # value. To get the value formatted for the attribute type, use the + # [attribute get] command. + # + # Returns 1 if the attribute exists, and 0 otherwise. + + if {[string is integer -strict $attr_id]} { + set key [expr {$attr_id + 0}]; # Force decimal rep. Faster than format + } else { + set key [names::attribute_id $attr_id]; # name -> id + } + + if {[dict exists $sdr $key]} { + if {$varname ne ""} { + upvar 1 $varname value + set value [dict get $sdr $key] + } + return 1 + } + + return 0 +} + +proc iocp::bt::sdr::attributes {sdr} { + # Get the list of attributes in a service discovery record + # sdr - a decoded service discovery record in the form returned by + # [decode]. + # Returns a list of numeric attribute ids. + return [dict keys $sdr] +} + +proc iocp::bt::sdr::attribute::raw {sdr attr_id} { + # Get an attribute value from an service discovery record. + # sdr - a decoded service discovery record in the form returned by + # sdr_decode. + # attr_id - attribute integer id + # + # The command will raise an error if the attribute does not exist + # in the sdr. + # + # Returns the attribute value as a pair consisting of the type and + # the raw data element value. + + if {[exists $sdr $attr_id value]} { + return $value + } else { + error "Attribute with id \"$attr_id\" not found." + } +} + +proc iocp::bt::sdr::attribute::get {sdr attr_id {varname {}}} { + # Get the value of an universal attribute from a service + # discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # varname - Optional name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The attribute value is decoded into a attribute type-dependent format. + # + # The $attr_id argument must be one of those defined in + # the *Universal Attributes* section in the + # [Bluetooth Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/) + # specification. Otherwise an error is raised. The recognized attributes + # are listed below. Refer to the Bluetooth specification for their + # exact semantics. + # + # AdditionalProtocolDescriptorList - List of protocol stacks. This + # supplements the `ProtocolDescriptorList` attribute. + # BluetoothProfileDescriptorList - List of Bluetooth profile descriptors + # to which the service conforms. Each element is a dictionary + # with keys `Uuid`, `Name`, `MajorVersion` and `MinorVersion` + # corresponding to the referenced profile. + # BrowseGroupList - List of browse group descriptors representing + # the browse groups to which the service record belongs. Each + # descriptor is dictionary with keys `Uuid` and `Name` identifying + # the a browse group. + # ClientExecutableURL - URL pointing to the client application that + # may be used to utilize the service. + # DocumentationURL - URL for the service documentation. + # IconURL - URL for icon that may be used to represent the service. + # LanguageBaseAttributeIDList - Nested dictionary of language-specific + # attribute offsets (see below). + # ProtocolDescriptorList - List of protocol stacks (see below) that may + # be used to access the service described by the service record. + # ProviderName - Name of entity providing the service in the primary + # language of the service record. + # ServiceAvailability - A value in the range 0-255 that indicates + # relative availability of the service in terms of the number of + # clients it can accept. Note this is a scaled estimate. + # ServiceClassIDList - List with each element being a dictionary + # corresponding to a service class that the record conforms to. + # The keys of the dictionary are `Name` and `Uuid`. + # ServiceDescription - A description of the service in the primary + # language of the record. + # ServiceID - UUID that uniquely identifies a service instance. + # ServiceInfoTimeToLive - The number of seconds from the time it was + # generated that the service record information is expected to be valid. + # ServiceName - The human-readable name of the service in the primary + # language of the record. + # ServiceRecordHandle - A numeric value that uniquely identifies a service + # record within a SDP server. + # ServiceRecordState - Numeric value that is changed any time the service + # record is modified by the SDP server. + # + # In the case of ProtocolDescriptorList and AdditionalProtocolDescriptorList, + # the attribute value is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # In the case of LanguageBaseAttributeIDList, the attribute value is + # a dictionary indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. + # + # Returns a boolean or the decoded attribute value. + + set attr_name [names::attribute_name $attr_id] + set attr_id [names::attribute_id $attr_id] + switch -exact -- $attr_name { + BrowseGroupList - + ServiceClassIDList { + tailcall Uuids $sdr $attr_id $varname + } + BluetoothProfileDescriptorList - + ProtocolDescriptorList - + AdditionalProtocolDescriptorList - + LanguageBaseAttributeIDList { + tailcall $attr_name $sdr $varname + } + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceAvailability - + ServiceInfoTimeToLive - + ServiceID - + ServiceRecordState - + ServiceRecordHandle { + tailcall AttributeValue $sdr $attr_id $varname + } + ServiceName - + ServiceDescription - + ProviderName { + # Special case because they need a language argument + tailcall text $sdr $attr_name primary $varname + } + default { + if {[llength [info commands [namespace current]::universal::$attr_name]]} { + tailcall [namespace current]::universal::$attr_name $sdr $varname + } + } + } + error "Unknown universal attribute \"$attr_id\"." +} + +proc iocp::bt::sdr::attribute::text {sdr attr_id lang {varname {}}} { + # Get the value of an text attribute in the specified language + # from a service discovery record. + # sdr - Decoded service discovery record. + # attr_id - Universal attribute name or numeric identifier. + # lang - language identifier as specified in iso639, e.g. `en` for + # english, `fr` for french etc. or the keyword `primary`. + # varname - Name of variable in caller's context. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # If the record does not contain a value for the specified language, + # the value for the primary language will be retrieved. + # + # Returns a boolean or the attribute text value. + # + set lang_offset [names::attribute_id $attr_id] + if {$lang_offset < 256 || $lang_offset >= 512} { + error "Invalid text attribute id \"$attr_id\"." + } + incr lang_offset -256 + tailcall TextAttribute $sdr $lang_offset $lang $varname +} + + +proc iocp::bt::sdr::attribute::LanguageBaseAttributeIDList {sdr {varname {}}} { + # Retrieve the language base attributes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a nested dictionary + # corresponding to the `LanguageBaseAttributeIDList` + # universal attribute in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The dictionary value returned or stored is indexed by language + # identifiers as defined in ISO639, e.g. `en`, `fr` etc. + # The corresponding value is itself a dictionary with keys `BaseOffset` + # and `Encoding`. See the Bluetooth specification for the former. + # The latter is either a Tcl encoding name or in case the encoding + # is not supported by Tcl, an integer value that identifies an encoding + # as per the Bluetooth specification. fs + # + # Returns a boolean or a dictionary of language attributes. + if {![AttributeValue $sdr 6 sequence]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # attr is {sequence {langid encoding baseoffset ...}} + set langs [dict create] + foreach {langid enc offset} $sequence { + # langid is ISO 639 language code + set langid [lindex $langid 1] + set langid "[format %c [expr {$langid >> 8}]][format %c [expr {$langid &0xff}]]" + dict set langs $langid Encoding [::iocp::bt::EncodingTclName [lindex $enc 1]] + dict set langs $langid BaseOffset [lindex $offset 1] + } + if {$varname eq ""} { + return $langs + } else { + upvar 1 $varname var + set var $langs + return 1 + } +} + + +proc iocp::bt::sdr::attribute::ProtocolDescriptorList {sdr {varname {}}} { + # Retrieve the protocol information from a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # Information about the protocols that may be used to access the + # service described by SDR is stored in the ProtocolDescriptorList + # universal attribute as described in the Bluetooth Core specification. + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # The returned information is in the form of a list each element of + # which describes a protocol stack that may be used to access the + # service. Each such element is itself a list whose elements correspond + # to layers in that protocol stack starting with the lowest layer first. + # Each layer is described as a dictionary with the following keys: + # Uuid - the UUID of the protocol for the layer + # Name - the name of the protocol + # Params - the protocol parameters. This is a list of raw data elements + # of the form `{type value}` the interpretation of which is + # protocol dependent. + # + # Returns a boolean or the list of protocols. + + if {! [attribute exists $sdr 4 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + if {$varname eq ""} { + return [Protocols $attrval] + } else { + upvar 1 $varname var + set var [Protocols $attrval] + return 1 + } +} + +proc iocp::bt::sdr::attribute::BluetoothProfileDescriptorList {sdr {varname {}}} { + # Retrieve the service classes in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # + # The retrieved value is a list of profile descriptors + # corresponding to the BluetoothProfileDescriptorList + # universal attribute in the Bluetooth Core specification. + # Each descriptor is a dictionary with the following keys: + # `Uuid` - the UUID of the profile + # `Name` - the mnemonic name of the profile or its UUID if the + # UUID cannot be mapped to a name + # `MajorVersion` - the profile's major version + # `MinorVersion` - the profile's minor version + # + # If $varname is not the empty string, it should be the name of + # a variable in the caller's context. + # - If the attribute is present, the command returns `1` and + # stores the attribute value in the variable. + # - If the attribute is not present, the command returns 0. + # + # If $varname is an empty string, + # - If the attribute is present, the command returns its value. + # - If the attribute is not present, the command raises an error. + # + # Returns a boolean or the list of service class descriptors. + + if {! [attribute exists $sdr 9 attr]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # Sequence of profile descriptors. Each profile descriptor + # is sequence of profile uuid and version. + set profiles {} + foreach profile [lindex $attr 1] { + set uuid [lindex $profile 1 0 1] + set version [lindex $profile 1 1 1] + lappend profiles [dict create \ + Uuid $uuid \ + Name [iocp::bt::names::to_name $uuid] \ + MajorVersion [expr {$version >> 8}] \ + MinorVersion [expr {$version & 0xff}] + ] + } + + if {$varname eq ""} { + return $profiles + } else { + upvar 1 $varname var + set var $profiles + return 1 + } + +} + +proc iocp::bt::sdr::attribute::AdditionalProtocolDescriptorList {sdr {varname {}}} { + # Returns protocol information from the additional protocols field + # in a service discovery record. + # sdr - decoded service discovery record + # varname - optional name of variable in caller's context. + # This command is identical to [protocols] except it extracts + # protocol information from the `AdditionalProtocolDescriptorList` + # attribute instead of `ProtocolDescriptorList`. + + if {! [attribute exists $sdr 13 attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + # AdditionalProtocolDescriptorList is a sequence each element of which + # is formatted like a ProtocolDescriptorList + # assert [lindex $attrval 0] eq "sequence" + set protocols [lmap elem [lindex $attrval 1] { + Protocols $elem + }] + + if {$varname eq ""} { + return $protocols + } else { + upvar 1 $varname var + set var $protocols + return 1 + } +} + + +proc iocp::bt::sdr::Uuids {sdr attr_id {varname {}}} { + if {! [attribute exists $sdr $attr_id attrval]} { + if {$varname eq ""} { + error "Attribute not found." + } else { + return 0 + } + } + + set uuids [lmap elem [lindex $attrval 1] { + set uuid [lindex $elem 1] + dict create Uuid $uuid Name [names::to_name $uuid] + }] + + if {$varname eq ""} { + return $uuids + } else { + upvar 1 $varname var + set var $uuids + return 1 + } +} + +proc iocp::bt::sdr::Protocols {attrval} { + # The attribute value may either be a single protocol stack encoded + # as a sequence, or multiple stacks encoded as a selection of sequences. + if {[lindex $attrval 0] eq "sequence"} { + # Single protocol stack + set stacks [list [lindex $attrval 1]] + } else { + # selection - Multiple stacks + set stacks [lindex $attrval 1] + } + + set protocols [lmap stack $stacks { + # stack -> {protocol_descriptor protocol_descriptor ...} + # protocol_descriptor -> {sequence {data_element ...}} + lmap protocol_descriptor $stack { + # Each descriptor is a sequence, the first element identifying the + # protocol and the remaining being (optional) parameters. + set elements [lindex $protocol_descriptor 1] + set protocol_uuid [lindex $elements 0 1] + dict create \ + Protocol $protocol_uuid \ + ProtocolName [iocp::bt::names::protocol_name $protocol_uuid] \ + ProtocolParams [lrange $elements 1 end] + } + }] + + return $protocols +} + +proc iocp::bt::sdr::AttributeValue {sdr attr_id {varname {}}} { + if {$varname eq ""} { + return [lindex [attribute raw $sdr $attr_id] 1] + } else { + if {[attribute exists $sdr $attr_id attrval]} { + upvar 1 $varname value + set value [lindex $attrval 1] + return 1 + } else { + return 0 + } + } +} + +proc iocp::bt::sdr::TextAttribute {sdr attr_offset lang {varname {}}} { + + # Assume that if sdr does not explicitly specify encoding, it is UTF8 + set enc utf-8 + set base_offset 256 + + # Primary language's ServiceName attribute index always 256 + # Otherwise get the offset from the base index 256 + if {$lang ne "primary"} { + # See if the specified language has an entry in the languages table + if {[attribute get $sdr LanguageBaseAttributeIDList base_offsets] && + [dict exists $base_offsets $lang]} { + # Yes, so the language-specific text will be at an offset + # from the base attribute index + set base_offset [dict get $base_offsets $lang BaseOffset] + set enc [dict get $base_offsets $lang Encoding] + } + } + set attr_index [expr {$base_offset + $attr_offset}] + if {! [attribute exists $sdr $attr_index name]} { + # This language does not exist. Return primary language if possible. + # If base_offset was 256, that's already primary so don't retry + if {$base_offset == 256 || + ![attribute exists $sdr [expr {256 + $attr_offset}] name] } { + if {$varname ne ""} { + return 0 + } else { + error "Attribute not found in SDR." + } + } + } + + set name [lindex $name 1] + + # $name is encoded. If Tcl supports the encoding, well and good. + # Else return a hex form + if {[string is integer $enc]} { + # SDR specified an encoding but not one that Tcl knows about + set text [binary encode hex $name] + } else { + # The string may be null terminated. TBD - always? + if {[string index $name end] eq "\0"} { + set name [string range $name 0 end-1] + } + set text [encoding convertfrom $enc $name] + } + + if {$varname ne ""} { + upvar 1 $varname var + set var $text + return 1 + } else { + return $text + } +} + +proc iocp::bt::sdr::DecodeElements {delem} { + # Decodes a Data Element as defined in the Bluetooth specification. + # delem - Data element in binary format + + set decoded {} + while {[string length $delem]} { + lassign [ExtractFirstElement $delem] type value delem + switch -exact -- $type { + sequence { lappend decoded [list sequence [DecodeElements $value]]} + selection { lappend decoded [list selection [DecodeElements $value]]} + default { + lappend decoded [list $type $value] + } + } + } + return $decoded +} + +proc iocp::bt::sdr::ExtractFirstElement {bin} { + # Extracts leading element from an SDP record. + # bin - tail of SDP record + # Returns a pair consisting of the extracted element and + # the remaining part of $bin. + + set binlen [string length $bin] + if {$binlen == 0} { + return {} + } + binary scan $bin c header + set type [expr {($header >> 3) & 0xff}] + set lenbits [expr {$header & 0x7}] + + # The nil type is formatted slightly differently. Treat as a special case. + if {$type == 0} { + if {$lenbits != 0} { + error "Invalid non-0 length for nil type." + } + return [list nil "" [string range $bin 1 end]] + } + + if {$lenbits < 5} { + set len [expr {1 << $lenbits}] + set dataoffset 1 + } else { + if {$lenbits == 5} { + set dataoffset 2 + set fmt c + set mask 0xff + } elseif {$lenbits == 6} { + set dataoffset 3 + set fmt S + set mask 0xffff + } else { + # lenbits == 7 + set dataoffset 5 + set fmt I + set mask 0xffffffff + } + # Ensure input length enough for at least length field + if {$binlen < $dataoffset} { + error "Truncated binary header." + } + binary scan $bin @1$fmt len + set len [expr {$len & $mask}]; # Since binary scan is signed + } + if {$binlen < ($dataoffset+$len)} { + error "Truncated binary data." + } + set elem [string range $bin $dataoffset [expr {$dataoffset+$len-1}]] + switch -exact $type { + 1 { + set type uint + if {$len == 1} { + binary scan $elem cu elem + } elseif {$len == 2} { + binary scan $elem Su elem + } elseif {$len == 4} { + binary scan $elem Iu elem + } elseif {$len == 8} { + binary scan $elem Wu elem + } elseif {$len == 16} { + binary scan $elem WuWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 2 { + set type int + if {$len == 1} { + binary scan $elem c elem + } elseif {$len == 2} { + binary scan $elem S elem + } elseif {$len == 4} { + binary scan $elem I elem + } elseif {$len == 8} { + binary scan $elem W elem + } elseif {$len == 16} { + # Note hi is signed, lo unsigned + binary scan $elem WWu hi lo + # TBD - check this + # (2**64)*hi + lo + set elem [expr {(18446744073709551616 * $hi) + $lo}] + } else { + error "Invalid integer width." + } + } + 3 { + set type uuid + if {$len == 2} { + set elem [binary encode hex $elem] + set elem "0000$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 4} { + set elem [binary encode hex $elem] + set elem "$elem-0000-1000-8000-00805f9b34fb" + } elseif {$len == 16} { + set hex [binary encode hex $elem] + set elem [string range $hex 0 7] + append elem - [string range $hex 8 11] + append elem - [string range $hex 12 15] + append elem - [string range $hex 16 19] + append elem - [string range $hex 20 end] + } else { + error "Invalid length $len for UUID." + } + } + 4 { + set type text + # TBD - keep elem as binary as we do not know encoding + } + 5 { + set type boolean + if {$len != 1} { + error "Invalid length for boolean type." + } + binary scan $elem c elem + set elem [expr {!!$elem}] ; # Convert to 0/1 + } + 6 { + set type sequence + # elem remains the same. Caller will have to recurse to parse + } + 7 { + set type selection + # elem remains the same. Caller will have to recurse to parse + } + 8 { + set type url + # elem remains same. Encoding unknown. TBD + } + default { + set type unknown + # elem remains same + } + } + + return [list $type $elem [string range $bin $dataoffset+$len end]] +} + +proc iocp::bt::sdr::printn {recs {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # recs - a list of binary SDP records in the form returned by + # [::iocp::bt::device services] or [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + set sep "" + foreach rec $recs { + puts $sep + set sep "--------------------------------------------" + print $rec $attrfilter + } +} + +proc iocp::bt::sdr::print {rec {attrfilter *}} { + # Prints a SDP record to a more human readable form. + # rec - a binary SDP record in the form returned by [::iocp::bt::device services] or + # [::iocp::bt::device service_references]. + # attrfilter - If specified, only attribute names matching the filter + # using `string match` are printed. + # + + # Alternating attribute value pairs + set rec [decode $rec] + foreach attr [attributes $rec] { + set attrname [names::attribute_name $attr] + if {[string is integer -strict $attrname]} { + # For easier matching with Bluetooth specs + set attrname [format 0x%x $attrname] + } + if {![string match -nocase $attrfilter $attrname]} { + continue + } + switch -exact -- $attrname { + ServiceID - + ServiceRecordState - + ServiceInfoTimeToLive - + ServiceAvailability - + DocumentationURL - + ClientExecutableURL - + IconURL - + ServiceName - + ServiceDescription - + ProviderName - + ServiceRecordHandle { + puts "$attrname: [attribute get $rec $attrname]" + } + ServiceClassIDList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + # We use this for printing service-specific attributes + lappend service_class_uuids $Uuid + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + ProtocolDescriptorList { + puts "$attrname:" + foreach protocol [attribute get $rec $attrname] { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + AdditionalProtocolDescriptorList { + # Like ProtocolDescriptorList but an additional level + # of nesting. + puts "$attrname:" + foreach additional_protocol [attribute get $rec $attrname] { + foreach protocol $additional_protocol { + puts " ProtocolStack:" + foreach layer $protocol { + set name [dict get $layer ProtocolName] + set uuid [dict get $layer Protocol] + set params [lmap param [dict get $layer ProtocolParams] { + PrintableElement $param + }] + if {$name eq ""} { + puts " $uuid ([join $params {, }])" + } else { + puts " $uuid $name ([join $params {, }])" + } + } + } + } + } + BrowseGroupList { + puts "$attrname: sequence" + foreach elem [attribute get $rec $attrname] { + dict with elem { + if {$Name eq $Uuid} { + puts " $Uuid" + } else { + puts " $Uuid $Name" + } + } + } + } + LanguageBaseAttributeIDList { + puts "$attrname: sequence" + dict for {lang val} [attribute get $rec $attrname] { + puts " $lang: [dict get $val Encoding], [dict get $val BaseOffset]" + } + } + BluetoothProfileDescriptorList { + puts "$attrname: sequence" + foreach profile [attribute get $rec $attrname] { + # Sequence of profiles. Each profile is a sequence of + # profile uuid and version. + # puts profile:$profile + dict with profile { + if {$Uuid eq $Name} { + puts " $Uuid v$MajorVersion.$MinorVersion" + } else { + puts " $Uuid $Name v$MajorVersion.$MinorVersion" + } + } + } + } + default { + if {$attr >= 0x100 && $attr <= 0x1ff} { + # TBD - special handling for 0x100-0x1ff attributes by + # looking up languages table + puts "$attrname: [PrintableElement [attribute raw $rec $attr]]" + } else { + lassign [PrintableServiceSpecificAttribute \ + $service_class_uuids $rec $attr] attrname attrval + puts "$attrname: $attrval" + } + } + } + } +} + +proc iocp::bt::sdr::PrintableServiceSpecificAttribute {service_class_uuids rec attr} { + # Returns pair of human readable attribute name and value + set attrname $attr + set attrval [PrintableElement [attribute raw $rec $attr]] + + # Maybe we know about attributes for one of these services + foreach uuid $service_class_uuids { + switch -exact -- $uuid { + 00001105-0000-1000-8000-00805f9b34fb { + # ObjectPush + if {$attr == 0x200} { + set attrname GoepL2CapPsm + break + } elseif {$attr == 0x303} { + set attrname SupportedFormats + set attrval [lmap val [lindex [attribute raw $rec $attr] 1] { + # Val is a sequence of {type value} where type is int + set val [lindex $val 1] + if {$val == 255} { + set val any + } else { + set formats {unknown vcard-2.1 vcard-3.0 vcal-1.0 ical-2.0 vnote vmessage} + if {$val < [llength $formats]} { + set val [lindex $formats $val] + } + } + set val + }] + break + } + } + } + } + return [list $attrname $attrval] +} + +proc iocp::bt::sdr::PrintableElement {delem {parent_indent {}}} { + lassign $delem type val + switch -exact -- $type { + nil - + uint - + int { return $val } + uuid { + set name [names::to_name $val] + if {$name eq $val} { + return $val + } else { + return "$val $name" + } + } + text - + url { + # Assume UTF-8 binary form + # Strip terminating null. TBD - is null alwys present? + set printable [encoding convertfrom utf-8 $val] + } + boolean { return [expr {$val ? "true" : "false"}] } + selection - + sequence { + set printable [list $type] + set new_indent "${parent_indent} " + foreach elem $val { + lappend printable "${new_indent}[PrintableElement $elem $new_indent]" + } + set printable [join $printable \n] + } + unknown { + set printable [BinToHex $val] + } + } + return $printable +} diff --git a/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl new file mode 100644 index 00000000..2452b04a --- /dev/null +++ b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/pkgIndex.tcl @@ -0,0 +1,36 @@ +# Core iocp package +package ifneeded iocp 2.0.2 \ + [list apply [list {dir} { + package require platform + set initName [string totitle iocp] + if {[package vsatisfies [package require Tcl] 9]} { + set fileName "tcl9iocp202.dll" + } else { + set fileName "iocp202.dll" + } + set platformId [platform::identify] + set searchPaths [list [file join $dir $platformId] \ + {*}[lmap platformId [platform::patterns $platformId] { + file join $dir $platformId + }] \ + $dir] + foreach path $searchPaths { + set lib [file join $path $fileName] + if {[file exists $lib]} { + uplevel #0 [list load $lib $initName] + return + } + } + error "Could not locate $fileName in directories [join $searchPaths {, }]" + }] $dir] + +# iocp_inet doesn't need anything other than core iocp +package ifneeded iocp_inet 2.0.2 \ + "package require iocp" + +if {1} { + # iocp_bt needs supporting script files + package ifneeded iocp_bt 2.0.2 \ + "[list source [file join $dir bt.tcl]]" +} + diff --git a/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..09d3cf0aa9f0d82e775357fbf226283e817d89d7 GIT binary patch literal 125251 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 Qz?}EdISggrdsOa!0Bc?jNB{r; literal 0 HcmV?d00001 diff --git a/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll b/src/vfs/punk86bawt.vfs/lib_tcl8/iocp-2.0.2/win32-ix86/tcl9iocp202.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe296717af838bb5537a33cc4a6e8a58c4ab5296 GIT binary patch literal 125285 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