From c78416c24782e6751e8d3d18f2bd5794bf9087b2 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 15:53:28 +0200 Subject: [PATCH 01/37] ts: add DejaGnu tool for shell Tab-completion testing Add a 'completion' DejaGnu tool, alongside modules/install/lint, that drives a real bash pty through Expect to press Tab against the built init/bash_completion script and check the resulting candidate list, instead of just calling the completion functions directly. The completion__{start,raw,list,inline,close} driving procs live in their own numbered file (020-bash-procs.exp) separate from the shell-agnostic assert helpers in 006-procs.exp, so a future shell only needs its own procs + test file. A dedicated 010-environ.exp sets up a clean, reproducible environment for every spawned session (sandboxed $HOME, no pager, no color, ...). Wired into 'make testcompletion' and 'script/mt comp'. Documented in doc/source/devel/testsuite.rst. Covers 'module'/'ml' completion: avail/load/unload module name and option flag listing and prefix filtering (including the ambiguous common-prefix and unique-inline cases), the 'nospace' directory-style completion path, use/unuse filesystem and modulepath completion, the saved-collection completion of restore, and the extra '-' candidates ml lists for already-loaded modules. Getting this reliable against a real pty needed a few infrastructure fixes along the way: 'after 300' before reading an inline completion so a trailing space in flight isn't raced past, widening the spawned pty (stty rows 40 columns 500) so a completed line landing exactly on the default 80-column width doesn't trigger a wrap-redraw that looks like stray output, disabling the readline query-items threshold so a large candidate list doesn't block on a "Display all N possibilities?" prompt, and sandboxing $HOME so 'module save' never writes real collection files outside the test tree. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .codespellrc | 4 +- .gitignore | 4 + .hunspell.en.dic | 14 + Makefile | 9 +- NEWS.rst | 5 + doc/source/devel/testsuite.rst | 111 ++++++-- script/mt | 7 + testsuite/completion.00-init/005-init_ts.exp | 68 +++++ testsuite/completion.00-init/006-procs.exp | 90 ++++++ testsuite/completion.00-init/010-environ.exp | 75 +++++ .../completion.00-init/011-save_test_env.exp | 23 ++ .../completion.00-init/020-bash-procs.exp | 197 +++++++++++++ testsuite/completion.00-init/021-bash.exp | 265 ++++++++++++++++++ 13 files changed, 846 insertions(+), 26 deletions(-) create mode 100644 testsuite/completion.00-init/005-init_ts.exp create mode 100644 testsuite/completion.00-init/006-procs.exp create mode 100644 testsuite/completion.00-init/010-environ.exp create mode 100644 testsuite/completion.00-init/011-save_test_env.exp create mode 100644 testsuite/completion.00-init/020-bash-procs.exp create mode 100644 testsuite/completion.00-init/021-bash.exp diff --git a/.codespellrc b/.codespellrc index c02b0ab97..76c4c7c9f 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,10 +1,10 @@ [codespell] -# ignore short names used in tests: ans, oT, ue, fo, haa, varius +# ignore short names used in tests: ans, oT, ue, fo, haa, varius, ure # ignore words joined to \t or \n character: \tThe, \tbe, \nIn # ignore variable names used in code: desig, matchin, compre, invers, compileTime, seh # ignore words added to spell check dictionary: lod, nd # ignore command names used in code: uncomplete -ignore-words-list = ans,ot,ue,fo,haa,varius,tthe,tbe,nin,desig,matchin,compre,invers,compiletime,seh,lod,nd,uncomplete +ignore-words-list = ans,ot,ue,fo,haa,varius,ure,tthe,tbe,nin,desig,matchin,compre,invers,compiletime,seh,lod,nd,uncomplete # skip git repository internal content # skip generated content: ChangeLog.gz, doc/build, modulecmd*.tcl, modules.sum # modules.log, lib/config.sub, lib/config.guess, lib/config.status, lib/configure, diff --git a/.gitignore b/.gitignore index 661e95b32..d69258ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ /install.sum /lint.log /lint.sum +/completion.log +/completion.sum /icdiff /.noicdiff /miniconda3 @@ -54,6 +56,8 @@ /testsuite/example/initrc-1 /testsuite/example/initrc /testsuite/.config +/testsuite/completion-use-fixture +/testsuite/completion-home /doc/build /doc/source/version.py /doc/demo/*/*.svg diff --git a/.hunspell.en.dic b/.hunspell.en.dic index 425c55944..60bbdbea6 100644 --- a/.hunspell.en.dic +++ b/.hunspell.en.dic @@ -18,6 +18,7 @@ CET CFLAGS CLICOLOR CMake +COMPREPLY CONFIGLIST ChangeLog ChemAppA @@ -258,9 +259,11 @@ autoreconf autotools availabilities avx +ba backticks badcommand baf +bar1 barbin basename basenames @@ -297,6 +300,7 @@ chmod cleanupAndExit cmake cmd +cmdline cmdModule cmdModuleAliases cmdModuleAvail @@ -329,11 +333,13 @@ cmdstring codecov codespell collectModuleTag +comgen commandexp commandname compA compB compat +completionhome compilerTag compopt config @@ -431,7 +437,9 @@ findModulesInCacheFile findable fishcompletiondir flavour +flatlcompat flavours +fo fooA fooB foobin @@ -518,6 +526,7 @@ isdefaultvalue ismodlo journalctl json +keypress kL ksh lappendConf @@ -649,6 +658,7 @@ nonpersist noop nop noparse +nospace notavail nothreads notuasked @@ -696,6 +706,7 @@ procedure's procs ps pthreads +pty pushenv py pwsh @@ -708,6 +719,7 @@ rcp reStructuredText readCollectionContent readModuleContent +readline readlink repo reportError @@ -844,6 +856,7 @@ tclxConfig tcsh tcshrc ter +testcompletion testdir testfile testfiles @@ -910,6 +923,7 @@ version's versionadded versionchanged versioncmp +versionless vimdatadir vimfiles virt diff --git a/Makefile b/Makefile index 99f471824..46fa23589 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ install-testmodspath-empty install-testmodspath-wild \ install-testmoguicmd uninstall-testconfig uninstall dist dist-tar \ dist-gzip dist-bzip2 dist-win srpm rpm clean distclean test-deps test \ - testinstall testlint + testinstall testlint testcompletion # commands to install files INSTALL = install @@ -1073,6 +1073,13 @@ testlint: initdir modulecmd.tcl $(NAGELFAR) script/add.modules script/modulecmd TESTSUITEDIR=`cd testsuite;pwd -P`; export TESTSUITEDIR; \ runtest --srcdir $$TESTSUITEDIR --objdir $$OBJDIR $(RUNTESTFLAGS) --tool lint $(RUNTESTFILES) +testcompletion: initdir + TCLSH=$(TCLSH); export TCLSH; \ + MODULECMD=$(MODULECMD); export MODULECMD; \ + OBJDIR=`pwd -P`; export OBJDIR; \ + TESTSUITEDIR=`cd testsuite;pwd -P`; export TESTSUITEDIR; \ + runtest --srcdir $$TESTSUITEDIR --objdir $$OBJDIR $(RUNTESTFLAGS) --tool completion $(RUNTESTFILES) + # install enhanced diff tool (to review test results) icdiff: diff --git a/NEWS.rst b/NEWS.rst index 55ae9da5d..9edcc73a5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -190,6 +190,11 @@ Modules 5.7.0 (not yet released) * Doc: add :ref:`add-new-hook-event` guide describing how to contribute a new hook event. * Doc: add :ref:`sync-remote-appdir` cookbook recipe. +* Testsuite: add *completion* testsuite to validate shell Tab completion of + this project. This testsuite can be invoked with ``make testcompletion`` or + ``script/mt comp``. It drives a real bash session through + `Expect `_ to press the Tab key against the + built shell completion script and check the resulting candidate list. .. _5.6 release notes: diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index b3aee6535..31f692aa0 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -13,12 +13,12 @@ line tools. DejaGnu groups test files (``.exp``, for *expect script*) under a *tool*: a run of ``runtest --tool `` sources every matching ``.exp`` file it finds for that tool and reports a ``PASS``/``FAIL``/``XFAIL``/ ``UNRESOLVED`` line for each individual check performed. Modules defines -three tools, matched to three kinds of tests (see `Kinds of tests`_ below). +four tools, matched to four kinds of tests (see `Kinds of tests`_ below). Kinds of tests -------------- -The testsuite exercises three different things, run as three separate +The testsuite exercises four different things, run as four separate DejaGnu tools: ``modules`` @@ -32,8 +32,9 @@ DejaGnu tools: ``install`` Checks a real ``make install``-ed tree: that the :command:`module` function/command is correctly defined once shell init scripts are - sourced, that shell completion works, that ``modulecmd`` is invoked - correctly from each shell's wrapper, etc. Driven by the + sourced, that the ``complete`` modulefile command emits the right + shell-specific completion registration snippet, that ``modulecmd`` is + invoked correctly from each shell's wrapper, etc. Driven by the :file:`install.00-init` directory. ``lint`` @@ -41,10 +42,18 @@ DejaGnu tools: scripts) over the repository's own scripts and reports any warning as a test failure. Driven by the :file:`lint.00-init` directory. +``completion`` + Drives a real, interactive shell process (via Expect ``spawn``/``send``/ + ``expect``, not just a captured non-interactive run like the other three + tools) to press Tab against the built shell completion script and check + that the resulting candidate list holds the expected module names and + option flags. Driven by the :file:`completion.00-init` directory; + currently covers bash only, see `completion.00-init layout`_. + Each tool corresponds to one Makefile target (``test``, ``testinstall``, -``testlint``, see `Running the testsuite`_) and to one log file -(:file:`modules.log`, :file:`install.log`, :file:`lint.log`) produced in the -top build directory. +``testlint``, ``testcompletion``, see `Running the testsuite`_) and to one +log file (:file:`modules.log`, :file:`install.log`, :file:`lint.log`, +:file:`completion.log`) produced in the top build directory. Two additional run modes apply to the ``modules`` tool rather than adding a new one: @@ -67,12 +76,12 @@ Test file directories Test files are grouped in numbered directories named ``.-``, e.g. :file:`modules.50-cmds`, -:file:`install.00-init`, :file:`lint.00-init`. The ```` prefix ties the -directory to one of the three DejaGnu tools above (DejaGnu only looks at -directories whose prefix matches the ``--tool`` given to ``runtest``); the -two-digit ```` number fixes run order and is what you pass to -:file:`script/mt` to select a whole directory (e.g. ``script/mt 50``); the -```` suffix is just a human-readable label. +:file:`install.00-init`, :file:`lint.00-init`, :file:`completion.00-init`. +The ```` prefix ties the directory to one of the four DejaGnu tools +above (DejaGnu only looks at directories whose prefix matches the ``--tool`` +given to ``runtest``); the two-digit ```` number fixes run order +and is what you pass to :file:`script/mt` to select a whole directory (e.g. +``script/mt 50``); the ```` suffix is just a human-readable label. Current ``modules.*`` series, in run order: @@ -137,14 +146,49 @@ Current ``modules.*`` series, in run order: ``99-finish`` Testsuite teardown (removes cache files created for the run) -``install.00-init`` and ``lint.00-init`` are each a single series (those -tools are much smaller and don't need topic splitting). +``install.00-init``, ``lint.00-init`` and ``completion.00-init`` are each a +single series (those tools are much smaller and don't need topic splitting). Every series directory ends with a ``999-cleanup.exp`` file (see `Test file anatomy`_) and, for the ``modules`` tool, most series begin with a ``0NN-init_ts.exp`` file that sets up whatever fixtures that series' tests need (e.g. :file:`modules.90-avail/010-init_ts.exp`). +``completion.00-init`` layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``completion`` tool follows the common ``005``/``006``/``010``/``011`` +setup numbering (see `Test file anatomy`_ and `Base test procedures`_ below) +plus one extra layer, since driving real Tab-key completion is inherently +shell-specific in a way none of the other three tools are: + +- ``005-init_ts.exp`` / ``006-procs.exp`` / ``010-environ.exp`` / + ``011-save_test_env.exp`` set up paths (the ``bash`` binary, the built + :file:`init/bash_completion` script, a clean fixture modulepath), the + shell-agnostic assert procedures (``completion_assert_contains``, + ``completion_assert_not_contains``, ``completion_assert_eq``), and the + clean baseline environment/``save_test_env`` checkpoint, exactly as for + the other tools. +- ``0NN--procs.exp`` defines one ``completion__start`` / + ``completion__raw`` / ``completion__list`` / + ``completion__inline`` / ``completion__close`` set per + shell -- e.g. ``020-bash-procs.exp`` spawns a real ``bash`` pty, sources + :file:`init/bash_completion`, and drives double-Tab listings + (``completion_bash_list``, for an ambiguous prefix) or single-Tab inline + completions (``completion_bash_inline``, for a prefix with exactly one + match -- e.g. checking a directory-style entry completes with a + trailing ``/`` and no trailing space) through Expect. + ``completion__list``/``_inline`` are responsible for recording the + cmdline they were passed into the shared ``completion_last_cmdline`` + variable, which the generic assert procedures use to build their test + label. +- ``0NN-.exp`` (e.g. ``021-bash.exp``) holds the actual test cases + for that shell, calling only its own ``completion__*`` procs plus + the shared asserts. + +Adding a new shell means adding its own ``completion__*`` procs file +and test file; nothing in ``006-procs.exp`` needs to change. + Fixture and support directories ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -554,6 +598,7 @@ With ``make`` make test COVERAGE=y # coverage-instrumented run, produces tcl/*.tcl_m make testinstall # 'install' suite against a tree already processed by 'make install' make testlint # 'lint' suite (Nagelfar + ShellCheck) + make testcompletion # 'completion' suite (interactive Tab-completion tests) Each target ends up calling ``runtest --tool $(RUNTESTFLAGS) $(RUNTESTFILES)``: ``RUNTESTFILES``, if set, restricts the run to specific @@ -578,19 +623,22 @@ calling ``make test`` directly when iterating on a specific area. script/mt cov # same as: make test COVERAGE=y script/mt install # same as: make testinstall script/mt lint # same as: make testlint + script/mt comp # same as: make testcompletion script/mt 50/470 # only testsuite/modules.50-cmds/470-*.exp script/mt 50 # every file in testsuite/modules.50-cmds script/mt 61 # collection series (always run whole, see below) script/mt lint 00/030 # only testsuite/lint.00-init/030-*.exp + script/mt comp 00/021 # only testsuite/completion.00-init/021-*.exp script/mt 50/{280,290} 61 # several selections at once script/mt --help # full usage Whichever files are selected, :file:`script/mt` always also runs the mandatory setup files for that tool (for ``modules``: ``00/005 00/006 00/010 00/050 00/060 00/080 00/085``; for ``install``: -``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``), -plus the ``999-cleanup.exp`` of every selected series. Passing a bare series +``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``; for +``completion``: ``00/005 00/006 00/010 00/011 00/020``), plus the +``999-cleanup.exp`` of every selected series. Passing a bare series number always expands to every file in that directory, because several of those series are order-sensitive or enumerate a whole modulepath (see `Adding new test fixtures`_). The collection series (``61``) is one such @@ -685,11 +733,11 @@ Debugging a broken test ``send_user`` progress messages emitted by ``config/base-config.exp`` helpers like ``setenv_var``/``change_file_perms``), set ``RUNTESTFLAGS='-v -v'`` and call ``make test``/``testinstall``/ - ``testlint`` directly, or invoke ``runtest`` yourself with the - environment variables :file:`script/mt`/the Makefile targets set up - (``TCLSH``, ``MODULECMD``, ``OBJDIR``, ``TESTSUITEDIR``) -- see the - ``test``/``testinstall``/``testlint`` targets in :file:`Makefile` for the - exact invocation. + ``testlint``/``testcompletion`` directly, or invoke ``runtest`` yourself + with the environment variables :file:`script/mt`/the Makefile targets set + up (``TCLSH``, ``MODULECMD``, ``OBJDIR``, ``TESTSUITEDIR``) -- see the + ``test``/``testinstall``/``testlint``/``testcompletion`` targets in + :file:`Makefile` for the exact invocation. 4. **Check for order dependence.** If a test passes alone but fails in a full run (or vice-versa), suspect a missing/incomplete ``reset_test_env`` footer in an earlier file, or a global-enumeration test (`Adding new test @@ -705,7 +753,24 @@ Debugging a broken test :file:`lint.00-init/0NN-*.exp` files configure linter exclusions (e.g. the ``-e SC1090`` ShellCheck exclusion in :file:`lint.00-init/020-sh.exp`) before adding a new one. -7. **Coverage regressions.** If a change is meant to add coverage for a new +7. **``completion`` tool hangs/timeouts.** A run that hangs (rather than + fails) almost always means an ``expect`` pattern in a + ``completion__*`` proc never matched, so it burned the default + timeout before falling through -- three recurring causes when scripting + readline-based completion: sending only a double-Tab against a prefix + that still has an unconsumed common-prefix extension (readline + auto-inserts it on the first Tab, so the listing needs a *third* Tab -- + type the full common prefix yourself instead, see the ``module load ba`` + case in + :file:`completion.00-init/021-bash.exp`); clearing an input line and then + matching on the prompt text reappearing (readline redraws a cleared line + with cursor-movement escapes, not by reprinting the prompt -- submit the + now-empty line instead, see ``completion_bash_list`` in + :file:`completion.00-init/020-bash-procs.exp`); and an interactive pager + (``less``) kicking in on an unexpected warning and blocking for a + keypress that never comes (set ``MODULES_PAGER=cat``, see + :file:`completion.00-init/010-environ.exp`). +8. **Coverage regressions.** If a change is meant to add coverage for a new branch, confirm it with ``make test COVERAGE=y`` (or ``script/mt cov /``) and check the relevant :file:`tcl/*.tcl_m` file no longer flags that line ``;# Not covered``. diff --git a/script/mt b/script/mt index fca7a9ca2..ed019d6d8 100755 --- a/script/mt +++ b/script/mt @@ -32,6 +32,7 @@ Test suites: cov Coverage mode of modulecmd.tcl testsuite install Test Modules installation lint Lint script files of this repository + comp Test shell completion scripts Select parts of testsuite: serienum Identification number of testsuite directory containing a @@ -46,6 +47,7 @@ Examples: $0 $0 quick $0 lint + $0 comp $0 50/37 61 70/{290,440} " } @@ -102,6 +104,11 @@ if [ $# -gt 0 ]; then testserie='lint' setuptestfiles=(00/005 00/006 00/011) shift + elif [ "$1" = "comp" ]; then + target='testcompletion' + testserie='completion' + setuptestfiles=(00/005 00/006 00/010 00/011 00/020) + shift fi fi diff --git a/testsuite/completion.00-init/005-init_ts.exp b/testsuite/completion.00-init/005-init_ts.exp new file mode 100644 index 000000000..2aee90294 --- /dev/null +++ b/testsuite/completion.00-init/005-init_ts.exp @@ -0,0 +1,68 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/07 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: +# Sub-Command: +# +# Comment: %C{ +# Defines all patterns used further in the testsuite +# }C% +# +############################################################################## + +proc find_bin {name} { + set path {} + + if {[info exists ::env(PATH)]} { + foreach dir [split $::env(PATH) ":"] { + if {[file executable "$dir/$name"]} { + set path "$dir/$name" + break + } + } + } + + return $path +} + +if {$verbose} { + send_user "\tInitializing the testsuite ...\n" +} + +set TCLSH $env(TCLSH) +set MODULECMD $env(MODULECMD) + +# a bash binary is required to drive interactive completion tests +set bashbin [find_bin bash] +if {$bashbin eq {}} { + unsupported {no bash binary found to drive completion tests} +} + +# built bash completion script under test +set bashcompletion "$env(OBJDIR)/init/bash_completion" +if {![file exists $bashcompletion]} { + unsupported "completion script not found: $bashcompletion (run 'make initdir' first)" +} + +# clean modulefile fixture tree: unlike testsuite/modulefiles, this one holds +# no modulefile deliberately broken to exercise error paths, which would get +# in the way of straightforward completion listing checks +set modpath "$env(TESTSUITEDIR)/modulefiles.3" + +# sandboxed $HOME for the spawned bash sessions: some completion tests (e.g. +# 'module save') write real files there, which must never land under a +# developer's actual home directory +set completionhome "$env(TESTSUITEDIR)/completion-home" +file delete -force $completionhome +file mkdir $completionhome + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/006-procs.exp b/testsuite/completion.00-init/006-procs.exp new file mode 100644 index 000000000..615c9a09d --- /dev/null +++ b/testsuite/completion.00-init/006-procs.exp @@ -0,0 +1,90 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/07 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Test procedures +# Command: +# Sub-Command: +# +# Comment: %C{ +# Defines shell-agnostic test procedures shared by every +# shell's completion tests: a shell-specific +# completion__list procedure is expected to +# record the cmdline it was passed in +# completion_last_cmdline, which the assert procedures +# below use to build their test label +# }C% +# +############################################################################## + +# prompt used as a distinctive sentinel by every shell-specific driver, to +# reliably tell apart the shell's own output from the test session's +set completion_prompt {MODULESTEST> } + +# escape a string so it is safe to embed in a Tcl regexp as a literal +proc completion_re_escape {str} { + return [regsub -all {[][$.+?*\\()&|^{}]} $str {\\&}] +} + +# assert every word in 'needed' is present in the 'got' candidate list; the +# test label is built from the cmdline last passed to completion__list +# plus the words looked for +proc completion_assert_contains {got needed} { + global completion_last_cmdline + + set label "'$completion_last_cmdline' completion contains $needed" + set missing {} + foreach word $needed { + if {$word ni $got} { + lappend missing $word + } + } + if {[llength $missing]} { + fail "$label (missing: $missing) (got: $got)" + } else { + pass $label + } +} + +# assert no word in 'unwanted' is present in the 'got' candidate list; the +# test label is built from the cmdline last passed to completion__list +# plus the words that must not appear +proc completion_assert_not_contains {got unwanted} { + global completion_last_cmdline + + set label "'$completion_last_cmdline' completion excludes $unwanted" + set found {} + foreach word $unwanted { + if {$word in $got} { + lappend found $word + } + } + if {[llength $found]} { + fail "$label (unexpected: $found) (got: $got)" + } else { + pass $label + } +} + +# assert 'got' is exactly 'expected'; the test label is built from the +# cmdline last passed to completion__list/_inline plus the expected +# value +proc completion_assert_eq {got expected} { + global completion_last_cmdline + + set label "'$completion_last_cmdline' completion inserts '$expected'" + if {$got ne $expected} { + fail "$label (got: '$got')" + } else { + pass $label + } +} + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/010-environ.exp b/testsuite/completion.00-init/010-environ.exp new file mode 100644 index 000000000..cc59903b2 --- /dev/null +++ b/testsuite/completion.00-init/010-environ.exp @@ -0,0 +1,75 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/07 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: initialization of the shell environment +# Command: +# Sub-Command: +# +# Comment: %C{ +# First time initialization: setup a clean, reproducible +# environment for the bash sessions spawned by completion +# tests +# }C% +# +############################################################################## + +if {$verbose} { + send_user "\tInitializing a clean environment ...\n" +} + +# clear testsuite-related variables +foreach var [concat [array names env -glob TESTSUITE_*]\ + [list TESTSUITE TS0 TS1 TS2 TS3 TS4 TS4 TS6 TS7 TS8 TS9]] { + # preserve variable externally set to configure testsuite run + if {$var ne {TESTSUITE_ENABLE_SITECONFIG}} { + unsetenv_var $var + } +} + +# start every completion test from a clean module environment +unsetenv_var MODULEPATH +unsetenv_var LOADEDMODULES +unsetenv_var _LMFILES_ +foreach var [array names env] { + switch -glob $var { + __MODULES_SHARE_* - __MODULES_PUSHENV_* - __MODULES_QUAR_* - \ + MODULES_FAMILY_* - MODULES_RUNENV_* - __MODULES_LM* { + unsetenv_var $var + } + } +} + +# drop any 'module'/'ml' shell function inherited from the parent shell +# environment, so autoinit defines them fresh in each spawned bash session +unsetenv_var {BASH_FUNC_module()} +unsetenv_var BASH_FUNC_module%% +unsetenv_var {BASH_FUNC__module_raw()} +unsetenv_var BASH_FUNC__module_raw%% +unsetenv_var {BASH_FUNC_ml()} +unsetenv_var BASH_FUNC_ml%% + +unsetenv_var MODULERCFILE + +# sandbox $HOME so any test writing real files there (e.g. 'module save', +# which defaults to $HOME/.module) never touches a developer's actual home +setenv_var HOME $completionhome + +# an interactive pager (e.g. less) would block an unattended pty waiting for +# a keypress: disable paging for all completion test sessions +setenv_var MODULES_PAGER cat + +# ensure colored output is disabled by default +setenv_var MODULES_COLOR never + +# setup basic locale for tests +setenv_var LANG C + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/011-save_test_env.exp b/testsuite/completion.00-init/011-save_test_env.exp new file mode 100644 index 000000000..b7d0984c8 --- /dev/null +++ b/testsuite/completion.00-init/011-save_test_env.exp @@ -0,0 +1,23 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/07 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: +# Sub-Command: +# +# Comment: %C{ +# Save variable set defined to perform test, which will +# be restored at end of each test set +# }C% +# +############################################################################## + +save_test_env diff --git a/testsuite/completion.00-init/020-bash-procs.exp b/testsuite/completion.00-init/020-bash-procs.exp new file mode 100644 index 000000000..aad9c9aff --- /dev/null +++ b/testsuite/completion.00-init/020-bash-procs.exp @@ -0,0 +1,197 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/07 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Test procedures +# Command: +# Sub-Command: +# +# Comment: %C{ +# Defines the test procedures driving a real bash pty to +# exercise the module/ml Tab completion script exactly +# as a user pressing Tab would, then collect the +# resulting candidate word list. Other shells get their +# own completion__{start,raw,list,close} set, +# since how a shell is scripted into pty interactive +# completion differs from one to the next +# }C% +# +############################################################################## + +# start a fresh bash session with the completion script under test sourced +# and 'module'/'ml' completion registered, ready to accept commands +proc completion_bash_start {} { + global bashbin bashcompletion completion_prompt completion_bash_spawn_id + global TCLSH MODULECMD verbose + + if {$verbose > 1} { + send_user "\tStarting bash for completion test\n" + } + + spawn $bashbin --norc --noprofile + set completion_bash_spawn_id $spawn_id + + # a narrow (default 80-column) pty can make a long completed line wrap, + # which makes readline redraw it with extra escape sequences that can be + # mistaken for stray inserted characters (e.g. a trailing space) by + # anything matching on raw output; go wide enough that no cmdline used + # in these tests is ever at risk of wrapping + stty rows 40 columns 500 < $spawn_out(slave,name) + + set spawn_id $completion_bash_spawn_id + expect { + -re {[$#] $} {} + timeout { unresolved {completion bash did not start}; return } + } + + # switch to a known, distinctive prompt first (matched with the default + # shell prompt pattern, since completion_bash_raw's own matching only + # works once this prompt is in effect); every setup command after this + # point goes through completion_bash_raw + send -- "bind 'set enable-bracketed-paste off' 2>/dev/null\r" + expect -re {[$#] $} + # readline asks "Display all N possibilities?" instead of listing outright + # once matches exceed this threshold (100 by default); some completions + # here (e.g. bare 'ml') list every sub-command and go well past it + send -- "bind 'set completion-query-items 0'\r" + expect -re {[$#] $} + send -- "unset PROMPT_COMMAND\r" + expect -re {[$#] $} + send -- "PS1='$completion_prompt'\r" + expect { + -re "$completion_prompt\$" {} + timeout { unresolved {completion bash prompt setup timed out}; return } + } + + # a real 'module' command is needed since the completion script shells + # out to it (module avail/spider) to list candidate module names + completion_bash_raw "eval \"\$('$TCLSH' '$MODULECMD' bash autoinit)\"" + completion_bash_raw "source '$bashcompletion'" + completion_bash_raw {complete -o default -F _module module} + completion_bash_raw {type -t _ml >/dev/null 2>&1 && complete -o default -F _ml ml} +} + +# send a line to the completion bash session and wait for the next prompt; +# used for setup commands whose output is not asserted on +proc completion_bash_raw {line} { + global completion_bash_spawn_id completion_prompt verbose + + set spawn_id $completion_bash_spawn_id + send -- "$line\r" + expect { + -re "\r\n$completion_prompt\$" {} + timeout { unresolved "completion setup command timed out: $line" } + } + if {$verbose > 1} { + send_user "\tcompletion> $line\n" + } +} + +# send a partial command line followed by a double-Tab (forcing readline to +# list every ambiguous match), and return the sorted, de-duplicated list of +# candidate words readline displayed. cmdline must be a prefix that has no +# unique completion (e.g. ends with a space) so the listing always fires +proc completion_bash_list {cmdline} { + global completion_bash_spawn_id completion_prompt completion_last_cmdline + global verbose + + set completion_last_cmdline $cmdline + set spawn_id $completion_bash_spawn_id + set recmdline [completion_re_escape $cmdline] + set reprompt [completion_re_escape $completion_prompt] + + send -- "$cmdline\t\t" + set candidates {} + expect { + -re "\r\n(.*)\r\n$reprompt$recmdline\$" { + set candidates $expect_out(1,string) + } + timeout { + unresolved "completion listing timed out for '$cmdline'" + } + } + if {$verbose > 1} { + send_user "\tcompletion raw listing for '$cmdline':\n$candidates\n" + } + + # clear the (uncompleted) input line so the session is ready to reuse. + # readline redraws a cleared line via cursor-movement escapes rather than + # reprinting the prompt text, so submit the now-empty line to force a + # real newline + fresh prompt we can reliably match on + send -- "\025\r" + expect -re "\r\n$reprompt\$" + + # collapse the multi-column listing into a flat, sorted word list + set words {} + foreach word [split $candidates] { + if {$word ne {}} { + lappend words $word + } + } + return [lsort -unique $words] +} + +# send a partial command line followed by a single Tab, and return the text +# readline inserted inline (no listing involved). Used to check unique, +# unambiguous completions -- e.g. that a directory-style entry gets +# completed with a trailing '/' and no trailing space (see +# 'compopt -o nospace' in init/bash_completion). cmdline must be a prefix +# with exactly one match, otherwise the single Tab may ring the bell instead +# of inserting text, or extend by less than a full match +proc completion_bash_inline {cmdline} { + global completion_bash_spawn_id completion_prompt completion_last_cmdline + global verbose + + set completion_last_cmdline $cmdline + set spawn_id $completion_bash_spawn_id + set recmdline [completion_re_escape $cmdline] + set reprompt [completion_re_escape $completion_prompt] + + send -- "$cmdline\t" + # let every byte readline is going to insert land in the pty buffer + # before reading it: an 'expect' pattern ending in a lazy '(.*)$' would + # otherwise match the instant the plain cmdline echo arrives, racing + # ahead of e.g. a trailing space that is still in flight + after 300 + set suffix {} + expect { + -re "${recmdline}(.*)\$" { + set suffix $expect_out(1,string) + } + timeout { + unresolved "completion inline insert timed out for '$cmdline'" + } + } + if {$verbose > 1} { + send_user "\tcompletion inline insert for '$cmdline': \[$suffix\]\n" + } + + # clear the (possibly completed) input line so the session is ready to + # reuse, same technique as completion_bash_list + send -- "\025\r" + expect -re "\r\n$reprompt\$" + + return $suffix +} + +proc completion_bash_close {} { + global completion_bash_spawn_id + + if {[info exists completion_bash_spawn_id]} { + set spawn_id $completion_bash_spawn_id + catch {send -- "exit\r"} + catch {expect eof} + catch {close -i $completion_bash_spawn_id} + catch {wait -i $completion_bash_spawn_id} + unset completion_bash_spawn_id + } +} + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/021-bash.exp b/testsuite/completion.00-init/021-bash.exp new file mode 100644 index 000000000..5ec2e35e0 --- /dev/null +++ b/testsuite/completion.00-init/021-bash.exp @@ -0,0 +1,265 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/07 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: avail, load, unload, ml, use, unuse, restore +# Sub-Command: +# +# Comment: %C{ +# Check bash Tab completion: module names and option +# flags get listed, 'unload' lists/completes the +# currently loaded modules, 'ml' mirrors 'load' and also +# lists loaded modules among the option flags, 'use' +# completes a filesystem directory path, 'unuse' +# lists/filters the enabled modulepaths, 'restore' +# lists/filters the saved collections +# }C% +# +############################################################################## + + +# +# module names and option flags both get listed on Tab completion +# + +setenv_path_var MODULEPATH $modpath + +completion_bash_start + +set got [completion_bash_list {module avail }] +completion_assert_contains $got {advvers/ bar/ baz/ foo/} +completion_assert_contains $got {--all --icase --terse} + +set got [completion_bash_list {module load }] +completion_assert_contains $got {advvers/ bar/ baz/ foo/} +completion_assert_contains $got {--force -f --icase} + +## "ba" is the common prefix of bar/ and baz/: a single Tab from "b" must +## auto-insert the "a" inline, with a bell right before it since bar/baz +## still leave the completion ambiguous beyond it +set got [completion_bash_inline {module load b}] +completion_assert_eq $got "\aa" + +## typed prefix is "ba", the full common prefix of bar/ and baz/: readline +## auto-inserts any *further* common prefix on the first Tab, which would +## eat one of the two Tabs the double-Tab below relies on to list matches +## instead of just ringing the bell +set got [completion_bash_list {module load ba}] +completion_assert_contains $got {bar/ baz/} +completion_assert_not_contains $got {foo/ advvers/} + +## "bar" uniquely matches the "bar/" module category: a single Tab must +## complete it inline with a trailing '/' and no trailing space (so typing +## can continue straight into a version), per the 'compopt -o nospace' +## logic in _module_comgen_words_and_files +set got [completion_bash_inline {module load bar}] +completion_assert_eq $got {/} + +## from "bar/", double-Tab must list the versions available under it +set got [completion_bash_list {module load bar/}] +completion_assert_contains $got {bar/1.0 bar/2.0} + +## "flatlcompat" is a single, versionless module (no further completion +## possible beneath it, so unlike "bar" above it does not end in '/'): a +## single Tab must complete it inline, with a trailing space since it is +## not a directory-style entry +set got [completion_bash_inline {module load fl}] +completion_assert_eq $got {atlcompat } + +completion_bash_close + + +# +# 'unload' lists the currently loaded modules alongside the option flags, +# and lists/completes them on the typed prefix like any other candidate list +# + +setenv_loaded_module [list bar/1.0 baz/1.0 foo/1.0]\ + [list $modpath/bar/1.0 $modpath/baz/1.0 $modpath/foo/1.0] + +completion_bash_start + +set got [completion_bash_list {module unload }] +completion_assert_contains $got {bar/1.0 baz/1.0 foo/1.0 --force -f --icase} + +## "ba" is the common prefix of bar/1.0 and baz/1.0: a single Tab from "b" +## must auto-insert the "a" inline, with a bell right before it since +## bar/1.0 and baz/1.0 still leave the completion ambiguous beyond it +set got [completion_bash_inline {module unload b}] +completion_assert_eq $got "\aa" + +## from "ba", nothing more can be auto-inserted (bar/1.0 and baz/1.0 +## diverge right there), so a double-Tab must list just the two matching +## loaded modules, not foo/1.0 +set got [completion_bash_list {module unload ba}] +completion_assert_contains $got {bar/1.0 baz/1.0} +completion_assert_not_contains $got {foo/1.0} + +## "foo/1.0" is the only loaded module starting with "f", and no option +## flag does either: a single Tab must complete the rest of it inline, +## with a trailing space since a loaded-module candidate is not a +## directory-style entry +set got [completion_bash_inline {module unload f}] +completion_assert_eq $got {oo/1.0 } + +completion_bash_close +unsetenv_loaded_module + + +# +# 'ml' mirrors 'module load' completion for the bare sub-command case, and +# additionally appends a "-" candidate for every currently loaded +# module once an option flag is being typed, letting it be typed straight +# after "ml -" to unload it +# + +setenv_path_var MODULEPATH $modpath + +completion_bash_start + +## unlike plain 'module load', a bare 'ml' also lists the top-level '-*' +## command-line options (e.g. '-h'/'--help'), since with no sub-command +## typed yet it cannot tell a bare 'module ' apart from a +## direct 'ml ' invocation +set got [completion_bash_list {ml }] +completion_assert_contains $got {advvers/ bar/ baz/ foo/ -h --help -D} + +completion_bash_raw {module load bar} + +## with bar/2.0 loaded (the default version, no .version file pins it), a +## "-bar/2.0" candidate must appear among the option flags +set got [completion_bash_list {ml -}] +completion_assert_contains $got {-bar/2.0 --force -f --icase} + +## "-bar/2.0" is the only candidate starting with "-ba": a single Tab must +## complete the rest of it inline, with a trailing space since it is not a +## directory-style entry +set got [completion_bash_inline {ml -ba}] +completion_assert_eq $got {r/2.0 } + +completion_bash_close + + +# +# 'use' completes a filesystem directory path: the completion script +# intentionally leaves COMPREPLY untouched for this sub-command, so it must +# fall through to readline's own default filename completion (registered +# via 'complete -o default') +# + +set usedir "$env(TESTSUITEDIR)/completion-use-fixture" +file delete -force $usedir +file mkdir $usedir + +completion_bash_start + +## drop the last 3 characters of the fixture dir name ("ure"): a single Tab +## must complete them back plus a trailing '/', with no trailing space, +## exactly like the "bar" -> "bar/" case above -- this time from plain +## readline directory completion rather than anything in the completion +## script itself +set got [completion_bash_inline "module use [string range $usedir 0 end-3]"] +completion_assert_eq $got {ure/} + +completion_bash_close +file delete -force $usedir + + +# +# 'unuse' lists the currently enabled modulepaths, and filters them on the +# typed prefix like any other candidate list +# + +setenv_path_var MODULEPATH $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp + +completion_bash_start + +## all three modulepaths are absolute, so they all share a leading '/': +## type it explicitly, otherwise readline auto-inserts that one-character +## common prefix on the first Tab and the double-Tab below would only ring +## the bell instead of listing (same trap as the "bar"/"ba" cases above) +set got [completion_bash_list {module unuse /}] +completion_assert_contains $got [list $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp] + +## $modpath and modulefiles.4 share the ".../modulefiles." prefix, diverging +## only on the trailing version digit: typing that full common prefix (so +## the first Tab has nothing left to auto-insert) and double-Tab must list +## only the two matching modulepaths, not the unrelated /tmp entry +set got [completion_bash_list "module unuse $env(TESTSUITEDIR)/modulefiles."] +completion_assert_contains $got [list $modpath "$env(TESTSUITEDIR)/modulefiles.4"] +completion_assert_not_contains $got {/tmp} + +completion_bash_close + + +# +# with only one modulepath left matching a typed prefix, a single Tab must +# complete the whole rest of it inline -- unlike the module-name case, a +# modulepath candidate is not a directory-style entry, so (still unlike +# "bar" -> "bar/" above) a trailing space is expected after it +# + +setenv_path_var MODULEPATH "$env(TESTSUITEDIR)/modulefiles.4" /tmp + +completion_bash_start + +set got [completion_bash_inline "module unuse $env(TESTSUITEDIR)/module"] +completion_assert_eq $got {files.4 } + +completion_bash_close + + +# +# 'restore' lists the saved collections, and filters them on the typed +# prefix like any other candidate list +# + +setenv_path_var MODULEPATH $modpath + +completion_bash_start + +completion_bash_raw {module save foo} +completion_bash_raw {module save fox} +completion_bash_raw {module save bar1} + +set got [completion_bash_list {module restore }] +completion_assert_contains $got {foo fox bar1} + +## "fo" is the common prefix of foo/fox: a single Tab from "f" must +## auto-insert the "o" inline (this is the same mechanism as the "bar"/"ba" +## trap above, caught in the act instead of just avoided). Readline also +## rings the bell right before that insertion, since foo/fox still leave +## the completion ambiguous beyond it +set got [completion_bash_inline {module restore f}] +completion_assert_eq $got "\ao" + +## from "fo", nothing more can be auto-inserted (foo/fox diverge right +## there), so a double-Tab must list just the two matching collections +set got [completion_bash_list {module restore fo}] +completion_assert_contains $got {foo fox} +completion_assert_not_contains $got {bar1} + +## "bar1" is the only collection starting with "ba": a single Tab must +## complete the rest of it inline, with a trailing space since a +## collection candidate is not a directory-style entry +set got [completion_bash_inline {module restore ba}] +completion_assert_eq $got {r1 } + +completion_bash_close + + +# +# Cleanup +# + +reset_test_env + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: From c315aea809034bd2aa2724646edf657c24bb02ff Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 15:54:12 +0200 Subject: [PATCH 02/37] gh: add completion_tests.yaml, document it in ci.rst Run the 'completion' DejaGnu tool (script/mt comp) on push and pull request, mirroring the shape of lint_tests.yaml but with the bash-only package set the completion.00-init tests actually need. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .github/workflows/completion_tests.yaml | 39 +++++++++++++++++++++++++ .hunspell.en.dic | 1 + doc/source/devel/ci.rst | 14 +++++++++ 3 files changed, 54 insertions(+) create mode 100644 .github/workflows/completion_tests.yaml diff --git a/.github/workflows/completion_tests.yaml b/.github/workflows/completion_tests.yaml new file mode 100644 index 000000000..7e24b4425 --- /dev/null +++ b/.github/workflows/completion_tests.yaml @@ -0,0 +1,39 @@ +name: completion-tests + +on: + push: + branches-ignore: + - c-main + - c-3.2 + pull_request: + +permissions: + contents: read + +jobs: + completion-all: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Install system packages + run: | + sudo apt-get -y update + sudo apt-get install -y expect-dev dejagnu python3-sphinx gzip bash tcl8.6 python3 + # libtclenvmodules build requirements + sudo apt-get install -y gcc autoconf tcl8.6-dev + - name: Build Modules + run: | + ./configure + make + - name: Test shell Tab completion + run: | + script/mt comp + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: testsuite-logs-${{ github.job }} + path: | + completion.log + retention-days: 5 diff --git a/.hunspell.en.dic b/.hunspell.en.dic index 60bbdbea6..4b3db399c 100644 --- a/.hunspell.en.dic +++ b/.hunspell.en.dic @@ -363,6 +363,7 @@ dbg de defaultval defs +dejagnu del delaruelle delim diff --git a/doc/source/devel/ci.rst b/doc/source/devel/ci.rst index ca078e8c4..cc4dcf3c8 100644 --- a/doc/source/devel/ci.rst +++ b/doc/source/devel/ci.rst @@ -28,6 +28,8 @@ Overview pull request. ``lint_tests.yaml`` Static analysis (Nagelfar, ShellCheck). Runs on push, pull request. +``completion_tests.yaml`` + Interactive shell Tab-completion tests. Runs on push, pull request. ``differential_shellcheck.yml`` ShellCheck diff annotations on the changed lines only. Runs on pull request (to ``main``). @@ -129,6 +131,18 @@ sh/bash/ksh scripts) via ``script/mt lint``, i.e. the same static analysis described as the ``lint`` tool in :doc:`testsuite`. On failure it uploads :file:`lint.log` as a build artifact. +Completion (:file:`completion_tests.yaml`) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Runs the ``completion`` DejaGnu tool (real, Expect-driven Tab-key presses +against the built :file:`init/bash_completion` script) via +``script/mt comp``, i.e. the same interactive shell Tab-completion testing +described as the ``completion`` tool in :doc:`testsuite`. Only needs the +baseline packages (``bash``, ``expect-dev``, ``dejagnu``) rather than the +full multi-shell set installed by :file:`linux_tests.yaml`, since the +``completion.00-init`` layout currently covers bash only. On failure it +uploads :file:`completion.log` as a build artifact. + Differential ShellCheck (:file:`differential_shellcheck.yml`) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From ffd830e1e6da20155b6e4a030cd4a5ce05a77d4e Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 15:55:19 +0200 Subject: [PATCH 03/37] Document per-sub-command options in module help usage module help's usage synopsis showed no options at all for several common sub-commands (load, unload, switch, display/show/test/path/ paths, is-avail, mod-to-sh), and an incomplete set for others (whatis and lint were both missing -i/--icase), even though the bash completion script already offers the full set for each. Add the missing brackets so the documented usage matches what completion actually proposes. Also document 'remove' next to 'rm | unload': it is a real, registered alias (tcl/main.tcl parseModuleCommandName), not just an abbreviation, and completion already offers it. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- tcl/report.tcl.in | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 916ad13e4..059989a5b 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2018,13 +2018,18 @@ proc reportUsage {} { report {Usage: module [options] [command] [args ...] Loading / Unloading commands: - add | load modulefile [...] Load modulefile(s) - try-add | try-load modfile [...] Load modfile(s), no complain if not found - add-any | load-any modfile [...] Load first available modulefile in list - rm | unload modulefile [...] Remove modulefile(s) + add | load [-f] [-i] [--auto|--no-auto] [--tag=LIST] modulefile [...] + Load modulefile(s) + try-add | try-load [-f] [-i] [--auto|--no-auto] [--tag=LIST] modfile [...] + Load modfile(s), no complain if not found + add-any | load-any [-f] [-i] [--auto|--no-auto] [--tag=LIST] modfile [...] + Load first available modulefile in list + rm | remove | unload [-f] [-i] [--auto|--no-auto] modulefile [...] + Remove modulefile(s) purge Unload all loaded modulefiles reload | update Unload then load all loaded modulefiles - switch | swap [mod1] mod2 Unload mod1 and load mod2 + switch | swap [-f] [-i] [--auto|--no-auto] [--tag=LIST] [mod1] mod2 + Unload mod1 and load mod2 refresh Refresh loaded module volatile components reset Restore initial environment @@ -2034,15 +2039,17 @@ Listing / Searching commands: avail [-a] [-t|-l|-j] [-S|-C] [-d|-L] [--indepth|--no-indepth] [mod ...] List all or matching available modules aliases [-a] List all module aliases - whatis [-a] [-j] [modulefile ...] Print whatis information of modulefile(s) + whatis [-a] [-i] [-j] [modulefile ...] + Print whatis information of modulefile(s) apropos | keyword | search [-a] [-j] str Search all name and whatis containing str spider [-a] [-t|-l|-j] [-S|-C] [-d|-L] [--indepth|--no-indepth] [mod ...] Scan all modulepaths and list all or matching available modules - is-loaded [modulefile ...] Test if any of the modulefile(s) are loaded - is-avail modulefile [...] Is any of the modulefile(s) available - info-loaded modulefile Get full name of matching loaded module(s) + is-loaded [-i] [modulefile ...] Test if any of the modulefile(s) are loaded + is-avail [-a] [-i] modulefile [...] + Is any of the modulefile(s) available + info-loaded [-i] modulefile Get full name of matching loaded module(s) Collection of modules handling commands: save [collection|file] Save current module list to collection @@ -2069,24 +2076,26 @@ Module cache handling commands: cacheclear Delete cache file in enabled modulepath(s) Other commands: - help [modulefile ...] Print this or modulefile(s) help info - display | show modulefile [...] Display information about modulefile(s) - test [modulefile ...] Test modulefile(s) + help [-i] [modulefile ...] Print this or modulefile(s) help info + display | show [-i] modulefile [...] + Display information about modulefile(s) + test [-i] [modulefile ...] Test modulefile(s) use [-a|-p] dir [...] Add dir(s) to MODULEPATH variable unuse dir [...] Remove dir(s) from MODULEPATH variable is-used [dir ...] Is any of the dir(s) enabled in MODULEPATH - path modulefile Print modulefile path - paths modulefile Print path of matching available modules + path [-i] modulefile Print modulefile path + paths [-i] modulefile Print path of matching available modules clear [-f] Reset Modules-specific runtime information source scriptfile [...] Execute scriptfile(s) config [--dump-state|name [val]] Display or set Modules configuration state [name] Display Modules state sh-to-mod shell shellscript [arg ...] Make modulefile from script env changes - mod-to-sh shell modulefile [...] + mod-to-sh [-f] [-i] [--auto|--no-auto] shell modulefile [...] Make shell code from modulefile env changes edit modulefile Open modulefile in editor - lint [-a] [modulefile ...] Check syntax of modulefile + lint [-a] [-i] [modulefile ...] + Check syntax of modulefile Switches: -t | --terse Display output in terse format From 051b65bddbdbc0040159a0f470cb1418134b6033 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 15:58:50 +0200 Subject: [PATCH 04/37] init: remove legacy init* commands from Tab completion candidates initadd, initclear, initlist, initprepend and initrm are old, hidden init-family commands intentionally left out of module help's documented sub-command list (they still work if typed out in full, kept only for backward compatibility). Stop offering them as Tab completion candidates, in bash, fish and zsh, so the completion candidate list matches the public, documented command surface. tcsh shares the bash candidate list (@comp_cmds@ in init/Makefile), so it is already fixed by the same change. Also drop every remaining reference to them: the now-unreachable no-op case arm handling them as the previous word in bash, the tcsh "n/initadd/n/"-style specs, and the fish predicate functions/ completions that only existed to complete their own arguments. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 +- init/bash_completion.in | 4 ---- init/fish_completion | 21 ++------------------- init/tcsh_completion.in | 5 ----- init/zsh-functions/_module.in | 6 ------ 5 files changed, 3 insertions(+), 35 deletions(-) diff --git a/init/Makefile b/init/Makefile index e30fc1513..dbcaaaf94 100644 --- a/init/Makefile +++ b/init/Makefile @@ -118,7 +118,7 @@ quarvarsre += s/@.*RUNENV_VAR.*@//;' endif # define variables for shell completion -comp_cmds := add add-any apropos aliases avail append-path cachebuild cacheclear clear config del describe disable display edit help initadd initclear initlist initprepend initrm is-loaded is-saved is-used is-avail info-loaded keyword lint list load load-any mod-to-sh path paths purge prepend-path refresh reload reset restore rm remove remove-path save savelist saveshow saverm search show sh-to-mod source spider stash stashclear stashlist stashpop stashrm stashshow state swap switch test try-add try-load unload unuse update use whatis +comp_cmds := add add-any apropos aliases avail append-path cachebuild cacheclear clear config del describe disable display edit help is-loaded is-saved is-used is-avail info-loaded keyword lint list load load-any mod-to-sh path paths purge prepend-path refresh reload reset restore rm remove remove-path save savelist saveshow saverm search show sh-to-mod source spider stash stashclear stashlist stashpop stashrm stashshow state swap switch test try-add try-load unload unuse update use whatis comp_long_opts := --debug --dumpname --help --silent --trace --verbose --version --paginate --no-pager --color --color= --width --width= --ignore-cache --ignore-user-rc comp_opts := -D -h -s -T -v -V -p -P -w $(comp_long_opts) comp_load_opts := --auto --no-auto --force -f --icase -i --tag --tag= diff --git a/init/bash_completion.in b/init/bash_completion.in index 9cef0eacd..cdc3d9cd7 100644 --- a/init/bash_completion.in +++ b/init/bash_completion.in @@ -113,8 +113,6 @@ _module() { COMPREPLY=( $(compgen -W "@comp_path_opts@" -- "$cur") );; remove-path) COMPREPLY=( $(compgen -W "@comp_rm_path_opts@" -- "$cur") );; - initadd|initclear|initlist|initprepend|initrm) - ;; *) if test "$COMP_CWORD" -gt 2 then _module_long_arg_list "$cur" @@ -176,8 +174,6 @@ if type -t ml >/dev/null; then COMPREPLY=( $(compgen -W "@comp_path_opts@" -- "$cur") );; remove-path) COMPREPLY=( $(compgen -W "@comp_rm_path_opts@" -- "$cur") );; - initadd|initclear|initlist|initprepend|initrm) - ;; *) if test "$COMP_CWORD" -gt 2 then _module_long_arg_list "$cur" diff --git a/init/fish_completion b/init/fish_completion index 9a670cd9f..83d912596 100644 --- a/init/fish_completion +++ b/init/fish_completion @@ -2,7 +2,7 @@ function __fish_module_no_subcommand --description 'Test if modulecmd has yet to be given the subcommand' for i in (commandline -opc) - if contains -- $i add add-any load load-any try-add try-load rm remove del unload swap switch show display lint list avail is-loaded is-saved is-used is-avail info-loaded aliases use unuse refresh reload purge source whatis apropos keyword search test save reset restore saverm saveshow savelist stash stashclear stashlist stashpop stashrm stashshow initadd initprepend initrm initswitch initlist initclear path paths append-path prepend-path remove-path clear config sh-to-mod edit state mod-to-sh cachebuild cacheclear spider update disable describe + if contains -- $i add add-any load load-any try-add try-load rm remove del unload swap switch show display lint list avail is-loaded is-saved is-used is-avail info-loaded aliases use unuse refresh reload purge source whatis apropos keyword search test save reset restore saverm saveshow savelist stash stashclear stashlist stashpop stashrm stashshow path paths append-path prepend-path remove-path clear config sh-to-mod edit state mod-to-sh cachebuild cacheclear spider update disable describe return 1 end end @@ -11,7 +11,7 @@ end function __fish_module_use_avail --description 'Test if module command should have available packages as potential completion' for i in (commandline -opc) - if contains -- $i help add add-any load load-any try-add try-load swap switch show display avail is-loaded is-avail info-loaded whatis test initadd initprepend path paths edit lint mod-to-sh + if contains -- $i help add add-any load load-any try-add try-load swap switch show display avail is-loaded is-avail info-loaded whatis test path paths edit lint mod-to-sh return 0 end end @@ -36,15 +36,6 @@ function __fish_module_use_list --description 'Test if module command should hav return 1 end -function __fish_module_use_initlist --description 'Test if module command should have packages in init file as potential completion' - for i in (commandline -opc) - if contains -- $i initrm initswitch - return 0 - end - end - return 1 -end - function __fish_module_use_savelist --description 'Test if module command should have saved collections as potential completion' for i in (commandline -opc) if contains -- $i save restore saverm disable saveshow describe savelist is-saved @@ -75,8 +66,6 @@ end complete -c module -n '__fish_module_use_avail' -f -a "(module avail --color=never -s -t -S --no-indepth -o 'alias:indesym' (commandline -ct) 2>&1)" complete -c module -n '__fish_module_use_spider' -f -a "(module spider --color=never -s -t -S --no-indepth -o 'alias:indesym' (commandline -ct) 2>&1)" complete -c module -n '__fish_module_use_list' -f -a "(module list --color=never -s -t -o '' 2>&1)" -complete -c module -n '__fish_module_use_initlist' -f -a "(module initlist --color=never -s 2>&1 | sed '\ - / loads modules:\$/d;')" complete -c module -n '__fish_module_use_savelist' -f -a "(module savelist --color=never -s -t 2>&1 | sed '\ /No named collection\.\$/d; \ /Named collection list\$/d; \ @@ -121,12 +110,6 @@ complete -f -n '__fish_module_no_subcommand' -c module -a 'saveshow' --descripti complete -f -n '__fish_module_no_subcommand' -c module -a 'describe' --description 'Display information about collection' complete -f -n '__fish_module_no_subcommand' -c module -a 'savelist' --description 'List all saved collections' complete -f -n '__fish_module_no_subcommand' -c module -a 'is-saved' --description 'Test if any of the collection(s) exists' -complete -f -n '__fish_module_no_subcommand' -c module -a 'initadd' --description 'Add modulefile to shell init file' -complete -f -n '__fish_module_no_subcommand' -c module -a 'initprepend' --description 'Add to beginning of list in init file' -complete -f -n '__fish_module_no_subcommand' -c module -a 'initrm' --description 'Remove modulefile from shell init file' -complete -f -n '__fish_module_no_subcommand' -c module -a 'initswitch' --description 'Switch mod1 with mod2 from init file' -complete -f -n '__fish_module_no_subcommand' -c module -a 'initlist' --description 'List all modules loaded from init file' -complete -f -n '__fish_module_no_subcommand' -c module -a 'initclear' --description 'Clear all modulefiles from init file' complete -f -n '__fish_module_no_subcommand' -c module -a 'path' --description 'Print modulefile path' complete -f -n '__fish_module_no_subcommand' -c module -a 'paths' --description 'Print path of matching available modules' complete -f -n '__fish_module_no_subcommand' -c module -a 'append-path' --description 'Append value to environment variable' diff --git a/init/tcsh_completion.in b/init/tcsh_completion.in index 65e4eb7b3..a8c789f60 100644 --- a/init/tcsh_completion.in +++ b/init/tcsh_completion.in @@ -83,11 +83,6 @@ complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ 'n/edit/`_module_avail`/' \ 'n/sh-to-mod/n/' \ 'n/source/n/' \ - 'n/initadd/n/' \ - 'n/initprepend/n/' \ - 'n/initrm/n/' \ - 'n/initlist/n/' \ - 'n/initclear/n/' \ "n/append-path/(@comp_path_opts@)/" \ "n/prepend-path/(@comp_path_opts@)/" \ "n/remove-path/(@comp_rm_path_opts@)/" \ diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index bb80c0836..06be99040 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -175,12 +175,6 @@ _module() { 'describe:Display information about collection' 'savelist:List all saved collections' 'is-saved:Test if any of the collection(s) exists' - 'initlist:List all modules loaded from init file' - 'initadd:Add modulefile to shell init file' - 'initrm:Remove modulefile from shell init file' - 'initprepend:Add to beginning of list in init file' - 'initswitch:Switch mod1 with mod2 from init file' - 'initclear:Clear all modulefiles from init file' 'help:Print this or modulefile(s) help info' 'display:Display information about modulefile(s)' 'show:Display information about modulefile(s)' From e98dc476c0af996d59a3049975f6216aadf866f2 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 15:59:21 +0200 Subject: [PATCH 05/37] Document delete alias in module help usage 'del' is offered as a Tab completion candidate for unload, but it is just the 3-character minimum abbreviation of 'delete', a real, case-sensitive shortcut resolved by parseModuleCommandName the same way 'remove' is -- 'delete' is the name worth advertising, not the arbitrary minimum-length form of it. Add it next to rm/remove/unload in module help's alias listing. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- tcl/report.tcl.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 059989a5b..1184e55b1 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2024,7 +2024,7 @@ Loading / Unloading commands: Load modfile(s), no complain if not found add-any | load-any [-f] [-i] [--auto|--no-auto] [--tag=LIST] modfile [...] Load first available modulefile in list - rm | remove | unload [-f] [-i] [--auto|--no-auto] modulefile [...] + rm | remove | delete | unload [-f] [-i] [--auto|--no-auto] modulefile [...] Remove modulefile(s) purge Unload all loaded modulefiles reload | update Unload then load all loaded modulefiles From 01f9121e7fdfae7965d2111cfdfa7649319995c5 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:00:55 +0200 Subject: [PATCH 06/37] init: add -a/-p option completion for use module use accepts real -a/--append and -p/--prepend options (parsed in tcl/main.tcl) and module help already documents 'use [-a|-p] dir [...]', but completion offered nothing for use: in bash falling through to plain filename completion unconditionally, in fish not offering them at all. Only offer the two options once the word being completed starts with '-' (bash), so directory completion still applies otherwise; fish gets them as top-level '--append'/'--prepend' entries. zsh already had both. tcsh cannot express this: its complete selectors are evaluated left-to-right with first-match-wins, so a second 'n/use/...' spec for the same previous word never fires once the earlier 'n/use/d/' directory-completion spec has already matched -- there is no selector combining "previous word is use" with "current word starts with -", so tcsh keeps plain directory completion only. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 ++ init/bash_completion.in | 12 ++++++++++-- init/fish_completion | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/init/Makefile b/init/Makefile index dbcaaaf94..a4735bc68 100644 --- a/init/Makefile +++ b/init/Makefile @@ -131,6 +131,7 @@ comp_mfile_opts := -i --icase comp_whatis_opts := -a -i -j --all --icase --json comp_search_opts := -a -j --all --json comp_aliases_opts := -a --all +comp_use_opts := -a --append -p --prepend comp_isavail_opts := -a -i --all --icase comp_lint_opts := -a -i --all --icase comp_modtosh_opts := --auto --no-auto --force -f --icase -i @@ -161,6 +162,7 @@ sed -e 's|@prefix@|$(prefix)|g' \ -e 's|@comp_whatis_opts@|$(comp_whatis_opts)|g' \ -e 's|@comp_search_opts@|$(comp_search_opts)|g' \ -e 's|@comp_aliases_opts@|$(comp_aliases_opts)|g' \ + -e 's|@comp_use_opts@|$(comp_use_opts)|g' \ -e 's|@comp_isavail_opts@|$(comp_isavail_opts)|g' \ -e 's|@comp_lint_opts@|$(comp_lint_opts)|g' \ -e 's|@comp_modtosh_opts@|$(comp_modtosh_opts)|g' \ diff --git a/init/bash_completion.in b/init/bash_completion.in index cdc3d9cd7..8ed264dc7 100644 --- a/init/bash_completion.in +++ b/init/bash_completion.in @@ -93,7 +93,11 @@ _module() { COMPREPLY=( $(compgen -W "@comp_unload_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; switch|swap) COMPREPLY=( $(compgen -W "@comp_load_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; unuse|is-used) COMPREPLY=( $(IFS=: compgen -W "${MODULEPATH}" -- "$cur") );; - use|-a|--append|cachebuild) ;; # let readline handle the completion + use) case "$cur" in + -*) COMPREPLY=( $(compgen -W "@comp_use_opts@" -- "$cur") );; + *) ;; # let readline handle the completion + esac;; + -a|--append|cachebuild) ;; # let readline handle the completion display|help|show|test|path|paths|is-loaded|info-loaded) _module_comgen_words_and_files "@comp_mfile_opts@ $(_module_avail "$cur")" "$cur";; is-avail) @@ -154,7 +158,11 @@ if type -t ml >/dev/null; then COMPREPLY=( $(compgen -W "@comp_unload_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; switch|swap) COMPREPLY=( $(compgen -W "@comp_load_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; unuse|is-used) COMPREPLY=( $(IFS=: compgen -W "${MODULEPATH}" -- "$cur") );; - use|-a|--append|cachebuild) ;; # let readline handle the completion + use) case "$cur" in + -*) COMPREPLY=( $(compgen -W "@comp_use_opts@" -- "$cur") );; + *) ;; # let readline handle the completion + esac;; + -a|--append|cachebuild) ;; # let readline handle the completion display|help|show|test|path|paths|is-loaded|info-loaded) _module_comgen_words_and_files "@comp_mfile_opts@ $(_module_avail "$cur")" "$cur";; is-avail) diff --git a/init/fish_completion b/init/fish_completion index 83d912596..15b05a9f3 100644 --- a/init/fish_completion +++ b/init/fish_completion @@ -164,3 +164,5 @@ complete -c module -s w -l width --description 'Set output width' complete -c module -l width= --description 'Set output width' complete -c module -l ignore-cache --description 'Ignore module cache' complete -c module -l ignore-user-rc --description 'Skip evaluation of user-specific module rc file' +complete -c module -l append --description 'Append directory to MODULEPATH' +complete -c module -l prepend --description 'Prepend directory to MODULEPATH' From 1e12a5d431276778e8c872da64f1214eaca5d856 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:01:30 +0200 Subject: [PATCH 07/37] Move --ignore-cache/--ignore-user-rc to module help Options section Both flags are parsed generically for every sub-command (tcl/main.tcl), matching the global, always-available 'Options:' section rather than the sub-command-contextual 'Switches:' section they were listed under. Bash completion already only offers them at the bare, no-sub-command 'module -' level, alongside the rest of Options, so this was simply filed under the wrong section. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- tcl/report.tcl.in | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 1184e55b1..a2dcddffe 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2121,9 +2121,6 @@ Switches: --tag=LIST Apply tag to loading module on 'load', 'try-load', 'load-any' or 'switch' sub-commands (LIST is made of tag names separated by ':') - --ignore-cache Ignore module cache - --ignore-user-rc - Skip evaluation of user-specific module rc file Options: -h | --help This usage info @@ -2141,7 +2138,10 @@ Options: --color[=WHEN] Colorize the output; WHEN can be 'always' (default if omitted), 'auto' or 'never' -w COLS | --width=COLS - Set output width to COLS columns.} + Set output width to COLS columns. + --ignore-cache Ignore module cache + --ignore-user-rc + Skip evaluation of user-specific module rc file} } # create appropriate message and kind of report when a requirement is not From a68d9d2841b8ffbb816afd65406e881712ef1841 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:04:02 +0200 Subject: [PATCH 08/37] Document avail/spider/list/stashlist missing options Their usage synopsis omitted -i/-o (avail, spider), -o (list) and every option at all (stashlist), even though bash completion already offers the full comp_avail_opts/comp_list_opts/comp_stashlist_opts set for each. Short forms only, matching the existing synopsis convention -- the short/long correspondence is already spelled out in the Switches section below. '-o' takes a LIST argument, per the '-o LIST | --output=LIST' Switches entry. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- tcl/report.tcl.in | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index a2dcddffe..5434cee53 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2014,7 +2014,7 @@ See 'module --help' to get available commands and options.} proc reportUsage {} { reportVersion - ##nagelfar ignore #111 Too long line + ##nagelfar ignore #141 Too long line report {Usage: module [options] [command] [args ...] Loading / Unloading commands: @@ -2034,16 +2034,18 @@ Loading / Unloading commands: reset Restore initial environment Listing / Searching commands: - list [-a] [-t|-l|-j] [-S|-C] [mod ...] + list [-a] [-t|-l|-j] [-S|-C] [-o LIST] [mod ...] List all or matching loaded modules - avail [-a] [-t|-l|-j] [-S|-C] [-d|-L] [--indepth|--no-indepth] [mod ...] + avail [-a] [-t|-l|-j] [-S|-C] [-d|-L] [-o LIST] + [-i] [--indepth|--no-indepth] [mod ...] List all or matching available modules aliases [-a] List all module aliases whatis [-a] [-i] [-j] [modulefile ...] Print whatis information of modulefile(s) apropos | keyword | search [-a] [-j] str Search all name and whatis containing str - spider [-a] [-t|-l|-j] [-S|-C] [-d|-L] [--indepth|--no-indepth] [mod ...] + spider [-a] [-t|-l|-j] [-S|-C] [-d|-L] [-o LIST] + [-i] [--indepth|--no-indepth] [mod ...] Scan all modulepaths and list all or matching available modules is-loaded [-i] [modulefile ...] Test if any of the modulefile(s) are loaded @@ -2064,7 +2066,7 @@ Collection of modules handling commands: stashrm [stash] Remove stash collection stashshow [stash] Display information about stash collection stashclear Remove all stash collections - stashlist List all stash collections + stashlist [-t|-l|-j] List all stash collections Environment direct handling commands: prepend-path [-d c] var val [...] Prepend value to environment variable From 6254eb414e2fd44b1a21f9fcaa257430fd459786 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:06:06 +0200 Subject: [PATCH 09/37] init: add --timer/--redirect/--no-redirect to top-level option completion module help's Options section documents these three as always-available global flags (tcl/main.tcl parses them generically for any sub-command), but completion never offered them: bash/tcsh (shared @comp_opts@ in init/Makefile), fish and zsh. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 +- init/fish_completion | 3 +++ init/zsh-functions/_module.in | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/init/Makefile b/init/Makefile index a4735bc68..dd6207d78 100644 --- a/init/Makefile +++ b/init/Makefile @@ -119,7 +119,7 @@ endif # define variables for shell completion comp_cmds := add add-any apropos aliases avail append-path cachebuild cacheclear clear config del describe disable display edit help is-loaded is-saved is-used is-avail info-loaded keyword lint list load load-any mod-to-sh path paths purge prepend-path refresh reload reset restore rm remove remove-path save savelist saveshow saverm search show sh-to-mod source spider stash stashclear stashlist stashpop stashrm stashshow state swap switch test try-add try-load unload unuse update use whatis -comp_long_opts := --debug --dumpname --help --silent --trace --verbose --version --paginate --no-pager --color --color= --width --width= --ignore-cache --ignore-user-rc +comp_long_opts := --debug --dumpname --help --silent --trace --verbose --version --paginate --no-pager --color --color= --width --width= --ignore-cache --ignore-user-rc --timer --redirect --no-redirect comp_opts := -D -h -s -T -v -V -p -P -w $(comp_long_opts) comp_load_opts := --auto --no-auto --force -f --icase -i --tag --tag= comp_unload_opts := --auto --no-auto --force -f --icase -i diff --git a/init/fish_completion b/init/fish_completion index 15b05a9f3..472c09294 100644 --- a/init/fish_completion +++ b/init/fish_completion @@ -166,3 +166,6 @@ complete -c module -l ignore-cache --description 'Ignore module cache' complete -c module -l ignore-user-rc --description 'Skip evaluation of user-specific module rc file' complete -c module -l append --description 'Append directory to MODULEPATH' complete -c module -l prepend --description 'Prepend directory to MODULEPATH' +complete -c module -l timer --description 'Report execution times' +complete -c module -l redirect --description 'Send output to stdout' +complete -c module -l no-redirect --description 'Send output to stderr' diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index 06be99040..9e9406ca2 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -134,6 +134,9 @@ _module() { '(--color --color=)'{--color,--color=}'[Colorize the output]' \ '--ignore-cache[Ignore module cache]' \ '--ignore-user-rc[Skip evaluation of user-specific module rc file]' \ + '--timer[Report execution times]' \ + '--redirect[Send output to stdout]' \ + '--no-redirect[Send output to stderr]' \ '(-): :->cmd' \ '(-)*:: :->arg' && ret=0 From d940a3da9cd3649a8c4e22d561631fa5f7fc2727 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:09:08 +0200 Subject: [PATCH 10/37] Document -d/--delim, --duplicates and --index in module help These path-family options (prepend-path, append-path, remove-path) were entirely undocumented in the Switches section, even though bash completion already offers them. Needed so the short/long correspondence for '-d' can be resolved unambiguously alongside the pre-existing avail/list '-d | --default' entry. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- tcl/report.tcl.in | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 5434cee53..567d3e0a1 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2123,6 +2123,13 @@ Switches: --tag=LIST Apply tag to loading module on 'load', 'try-load', 'load-any' or 'switch' sub-commands (LIST is made of tag names separated by ':') + -d C | --delim C + Set delimiter character instead of ':' (on 'prepend-path', + 'append-path' or 'remove-path' sub-commands) + --duplicates Allow duplicate entries (on 'prepend-path' or 'append-path' + sub-commands) + --index Given value is an index in the variable's list, instead of + a value to look up (on 'remove-path' sub-command) Options: -h | --help This usage info From 607ecdfec1c3ac210a53dde3183ffa4386498b3d Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:10:27 +0200 Subject: [PATCH 11/37] Add --duplicates/--index to path-family usage synopsis prepend-path/append-path were missing --duplicates and remove-path was missing --index in their own usage synopsis, even though bash completion already offers them (comp_path_opts/comp_rm_path_opts in init/Makefile). Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- tcl/report.tcl.in | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 567d3e0a1..4ac26cde0 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2069,9 +2069,12 @@ Collection of modules handling commands: stashlist [-t|-l|-j] List all stash collections Environment direct handling commands: - prepend-path [-d c] var val [...] Prepend value to environment variable - append-path [-d c] var val [...] Append value to environment variable - remove-path [-d c] var val [...] Remove value from environment variable + prepend-path [-d c] [--duplicates] var val [...] + Prepend value to environment variable + append-path [-d c] [--duplicates] var val [...] + Append value to environment variable + remove-path [-d c] [--index] var val [...] + Remove value from environment variable Module cache handling commands: cachebuild [modulepath ...] Create cache file for modulepath(s) From df6a334cdad60e78b3b59822d41a8214fa1ed4ac Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:13:14 +0200 Subject: [PATCH 12/37] init: offer -S/-C completion for list/savelist list and savelist both accept -S/--starts-with and -C/--contains (tcl/main.tcl passes search_match through to cmdModuleList/ cmdModuleSavelist, same as avail/spider), but completion never offered them, in bash/tcsh (shared comp_list_opts in init/Makefile) and zsh. fish already offers them, since its option completion is not scoped per sub-command. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .hunspell.en.dic | 1 + init/Makefile | 2 +- init/zsh-functions/_module.in | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.hunspell.en.dic b/.hunspell.en.dic index 4b3db399c..2c2dc976c 100644 --- a/.hunspell.en.dic +++ b/.hunspell.en.dic @@ -318,6 +318,7 @@ cmdModulePath cmdModulePaths cmdModuleReset cmdModuleRestore +cmdModuleSavelist cmdModuleSearch cmdModuleStash cmdModuleStashclear diff --git a/init/Makefile b/init/Makefile index dd6207d78..b30b892c7 100644 --- a/init/Makefile +++ b/init/Makefile @@ -123,7 +123,7 @@ comp_long_opts := --debug --dumpname --help --silent --trace --verbose --version comp_opts := -D -h -s -T -v -V -p -P -w $(comp_long_opts) comp_load_opts := --auto --no-auto --force -f --icase -i --tag --tag= comp_unload_opts := --auto --no-auto --force -f --icase -i -comp_list_opts := -a -j -l -o -t --all --json --long --output --output= --terse +comp_list_opts := -a -C -j -l -o -S -t --all --contains --json --long --output --output= --starts-with --terse comp_stashlist_opts := -j -l -t --json --long --terse comp_clear_opts := --force -f comp_avail_opts := -a -C -d -i -j -L -l -o -S -t --all --contains --default --icase --json --latest --long --output --output= --starts-with --terse --indepth --no-indepth diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index 9e9406ca2..563fda5fd 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -270,7 +270,9 @@ _module() { '(-t --terse)'{-t,--terse}'[Display output in terse format]' \ '(-j --json)'{-j,--json}'[Display output in JSON format]' \ '(-o --output=)'{-o,--output=}'[Define elements to output in addition to module names]' \ - '(-a --all)'{-a,--all}'[Include hidden modules in list]' && ret=0 + '(-a --all)'{-a,--all}'[Include hidden modules in list]' \ + '(-S --starts-with)'{-S,--starts-with}'[Search modules whose name begins with query string]' \ + '(-C --contains)'{-C,--contains}'[Search modules whose name contains query string]' && ret=0 ;; (savelist) local -a opts; opts=( @@ -282,6 +284,10 @@ _module() { '--json:Display output in JSON format' '-o:Define elements to output in addition to module names' '--output=:Define elements to output in addition to module names' + '-S:Search modules whose name begins with query string' + '--starts-with:Search modules whose name begins with query string' + '-C:Search modules whose name contains query string' + '--contains:Search modules whose name contains query string' ) _describe -t opts 'Switches' opts && ret=0 ;; From 46f9607d2df0d3baa4df346c6ef5dbef3bb3fdc1 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:21:27 +0200 Subject: [PATCH 13/37] ts: check bash completion against module help ground truth Parse module help's usage message into the documented sub-commands, global switches, and each sub-command's own option flags (completion.00-init/007-module_help.exp), then check that Tab completion proposes exactly that set, neither less nor more: a bare 'module ' against the sub-command/switch union, 'module -' against the switches alone, and 'module -' against that sub-command's own options (including sub-commands documented to take none at all). Per-sub-command option checks type '-' first rather than double-Tab from a bare trailing space: every option-bearing completion helper skips looking up real module/collection/modulepath candidates once the word starts with '-', isolating the option flags on their own, and it sidesteps the common-prefix auto-extension trap that a bare trailing space hits whenever a sub-command's entire candidate set already shares a leading '-' or '/' (e.g. plain 'aliases' or 'unuse' candidates). A sub-command with zero documented options still rings the bell readline sends for any Tab that finds zero completions. The short-form options in a sub-command's own usage line are resolved to their long form via the Switches section, which is also where a short flag reused with a different meaning on a specific sub-command (-a is --all almost everywhere but --append on 'use', -d is --default for avail/spider but --delim for the *-path commands) gets told apart, using the "(on 'x' sub-command)" annotation those entries carry. testcompletion never rebuilt $(MODULECMD) before running the testsuite, so the ground truth above could compare against a stale completion. Numbered 007 rather than right after the setup files it depends on (005/006), so that its ground-truth globals become part of the save_test_env baseline taken later at 011 -- and thus survive the reset_test_env call at the end of whichever shell's test file happens to run first, instead of being unset outright as newly-created state that was never part of that baseline. This matters once a second shell's test file needs the same ground truth after the first one's reset_test_env already ran. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- Makefile | 2 +- doc/source/devel/testsuite.rst | 2 +- script/mt | 2 +- testsuite/completion.00-init/006-procs.exp | 24 ++ .../completion.00-init/007-module_help.exp | 215 ++++++++++++++++++ testsuite/completion.00-init/021-bash.exp | 80 ++++++- 6 files changed, 321 insertions(+), 4 deletions(-) create mode 100644 testsuite/completion.00-init/007-module_help.exp diff --git a/Makefile b/Makefile index 46fa23589..8a198ab8d 100644 --- a/Makefile +++ b/Makefile @@ -1073,7 +1073,7 @@ testlint: initdir modulecmd.tcl $(NAGELFAR) script/add.modules script/modulecmd TESTSUITEDIR=`cd testsuite;pwd -P`; export TESTSUITEDIR; \ runtest --srcdir $$TESTSUITEDIR --objdir $$OBJDIR $(RUNTESTFLAGS) --tool lint $(RUNTESTFILES) -testcompletion: initdir +testcompletion: initdir $(MODULECMD) TCLSH=$(TCLSH); export TCLSH; \ MODULECMD=$(MODULECMD); export MODULECMD; \ OBJDIR=`pwd -P`; export OBJDIR; \ diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 31f692aa0..58b25e4bd 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -637,7 +637,7 @@ Whichever files are selected, :file:`script/mt` always also runs the mandatory setup files for that tool (for ``modules``: ``00/005 00/006 00/010 00/050 00/060 00/080 00/085``; for ``install``: ``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``; for -``completion``: ``00/005 00/006 00/010 00/011 00/020``), plus the +``completion``: ``00/005 00/006 00/007 00/010 00/011 00/020``), plus the ``999-cleanup.exp`` of every selected series. Passing a bare series number always expands to every file in that directory, because several of those series are order-sensitive or enumerate a whole modulepath (see diff --git a/script/mt b/script/mt index ed019d6d8..14fb3f0d7 100755 --- a/script/mt +++ b/script/mt @@ -107,7 +107,7 @@ if [ $# -gt 0 ]; then elif [ "$1" = "comp" ]; then target='testcompletion' testserie='completion' - setuptestfiles=(00/005 00/006 00/010 00/011 00/020) + setuptestfiles=(00/005 00/006 00/007 00/010 00/011 00/020) shift fi fi diff --git a/testsuite/completion.00-init/006-procs.exp b/testsuite/completion.00-init/006-procs.exp index 615c9a09d..a3318c9d3 100644 --- a/testsuite/completion.00-init/006-procs.exp +++ b/testsuite/completion.00-init/006-procs.exp @@ -73,6 +73,30 @@ proc completion_assert_not_contains {got unwanted} { } } +# assert every word in 'got' matching 'pattern' (a Tcl 'string match' glob) +# is a member of 'known'; used to check a candidate list holds no more than +# the fetched, expected words of a given kind (eg no option flag beyond the +# ones 'module help' documents) without having to also enumerate every other +# kind of candidate that may legitimately appear (module names, collection +# names, ...). The test label is built from the cmdline last passed to +# completion__list plus the pattern looked for +proc completion_assert_only_known {got pattern known} { + global completion_last_cmdline + + set label "'$completion_last_cmdline' completion has no unknown '$pattern' word" + set extra {} + foreach word $got { + if {[string match $pattern $word] && $word ni $known} { + lappend extra $word + } + } + if {[llength $extra]} { + fail "$label (unknown: $extra) (known: $known)" + } else { + pass $label + } +} + # assert 'got' is exactly 'expected'; the test label is built from the # cmdline last passed to completion__list/_inline plus the expected # value diff --git a/testsuite/completion.00-init/007-module_help.exp b/testsuite/completion.00-init/007-module_help.exp new file mode 100644 index 000000000..2b2ac3b69 --- /dev/null +++ b/testsuite/completion.00-init/007-module_help.exp @@ -0,0 +1,215 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/08 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Fetch ground-truth completion data from 'module help' +# Command: +# Sub-Command: +# +# Comment: %C{ +# Parse the 'module help' usage message into the set of +# documented sub-commands, global switches and, for each +# sub-command, the option flags that apply to it, so +# completion tests can check that Tab-completion proposes +# exactly this set -- neither less nor more. Shell-agnostic: +# built once here and reused by every shell's test file +# }C% +# +############################################################################## + +if {$verbose} { + send_user "\tParsing 'module help' output ...\n" +} + +catch {exec $TCLSH $MODULECMD bash help} help_output + +# every documented sub-command name (aliases counted separately, eg 'add' +# and 'load' on the same usage line both end up in this list) +set help_subcmds {} + +# option flags (short and long forms) that apply to a given sub-command, +# indexed by every alias name it goes by; a sub-command with no options at +# all (eg 'purge') simply has no entry +array set help_subcmd_opts {} + +# short and long forms of the global switches listed under 'Options:' -- +# these apply regardless of sub-command, and are what a bare +# 'module -' (no sub-command typed yet) proposes +set help_switches {} + +# 'Switches:' section entries, keyed by short form: switch_long(-x) is a +# list of {longform annotated_subcmds} pairs. Most short forms only ever +# mean one thing (eg '-f' is always '--force'), but a couple are reused +# with a different meaning on a specific sub-command (eg '-a' is '--all' +# almost everywhere but '--append' on 'use', '-d' is '--default' for +# avail/spider but '--delim' for the *-path commands) -- disambiguated +# below using the "... (on 'x' sub-command)" annotation those entries carry +array set switch_long {} + +# extract every '-'-prefixed token from a line, only counting tokens that +# start a new word (preceded by line start, a space, '[' or '|') so a hyphen +# embedded in a plain word (eg 'add-any') is never mistaken for an option. A +# token immediately glued to '=' or '[=' (eg '--output=LIST', +# '--color[=WHEN]') yields both the bare and the '=' form, since bash +# completion offers both +proc completion_help_extract_opts {line} { + set opts {} + set matches [regexp -all -inline -- {(^|[ \[|])(-{1,2}[A-Za-z][A-Za-z-]*)(=|\[=)?} $line] + foreach {whole bound tok eq} $matches { + lappend opts $tok + if {$eq ne {}} { lappend opts "${tok}=" } + } + return $opts +} + +# resolve a short flag used by a given sub-command to its long form(s) via +# the Switches: table built while parsing +proc completion_help_resolve_long {short_flag subcmd_name} { + global switch_long + + if {![info exists switch_long($short_flag)]} { + return {} + } + set candidates $switch_long($short_flag) + if {[llength $candidates] == 1} { + return [lindex $candidates 0 0] + } + foreach candidate $candidates { + lassign $candidate long annotated_subcmds + if {$subcmd_name in $annotated_subcmds} { + return $long + } + } + foreach candidate $candidates { + lassign $candidate long annotated_subcmds + if {[llength $annotated_subcmds] == 0} { + return $long + } + } + return {} +} + +set section none +set cur_names {} + +# state for the Switches: entry currently being accumulated -- its +# description can wrap over several lines, possibly carrying the "(on 'x' +# sub-command)" annotation on any of them +set sw_short {} +set sw_long {} +set sw_desc {} + +proc completion_help_flush_switch {} { + global sw_short sw_long sw_desc switch_long + + if {$sw_short eq {} && [llength $sw_long] == 0} { + return + } + set annotated_subcmds {} + if {[string match {*sub-command*} $sw_desc]} { + foreach {whole name} [regexp -all -inline {'([A-Za-z][A-Za-z0-9_-]*)'} $sw_desc] { + lappend annotated_subcmds $name + } + } + if {$sw_short ne {}} { + lappend switch_long($sw_short) [list $sw_long $annotated_subcmds] + } + set sw_short {} + set sw_long {} + set sw_desc {} +} + +foreach line [split $help_output "\n"] { + if {[regexp {^[A-Za-z][^:]*:$} $line]} { + if {$section eq {switches}} { completion_help_flush_switch } + if {[string match {*Options:} $line]} { + set section options + } elseif {[string match {*Switches:} $line]} { + set section switches + } elseif {[regexp {^[A-Za-z].* commands:$} $line]} { + set section subcmds + } else { + set section none + } + set cur_names {} + continue + } + if {[string trim $line] eq {}} { + if {$section eq {switches}} { completion_help_flush_switch } + set cur_names {} + continue + } + + if {$section eq {subcmds}} { + # a plain description-continuation line (36-space indent, matching + # the column every sub-command's description is aligned to) carries + # no names or options of its own + if {[string range $line 0 35] eq [string repeat { } 36]} { + continue + } + if {[regexp {^ ([A-Za-z][A-Za-z0-9_-]*(?: \| [A-Za-z][A-Za-z0-9_-]*)*)} $line all names]} { + # a new sub-command entry: remember its alias names, so option + # tokens on any wrapped usage-synopsis continuation line (indented + # less than the description column) still get attributed to it + set cur_names [regexp -all -inline {[A-Za-z][A-Za-z0-9_-]*} $names] + foreach n $cur_names { lappend help_subcmds $n } + } + set opts [completion_help_extract_opts $line] + foreach n $cur_names { + foreach o $opts { lappend help_subcmd_opts($n) $o } + } + } elseif {$section eq {options}} { + if {[string index $line 2] ne {-}} { continue } + foreach tok [completion_help_extract_opts $line] { + lappend help_switches $tok + } + } elseif {$section eq {switches}} { + # a new entry starts at a line indented by exactly 2 spaces; + # anything indented further is a continuation of the current entry's + # description + if {[string range $line 0 1] eq { } && [string index $line 2] ne { }} { + completion_help_flush_switch + foreach t [completion_help_extract_opts $line] { + if {[string match {--*} $t]} { + lappend sw_long $t + } elseif {$sw_short eq {}} { + set sw_short $t + } + } + } + append sw_desc { } [string trim $line] + } +} +if {$section eq {switches}} { completion_help_flush_switch } + +set help_subcmds [lsort -unique $help_subcmds] +set help_switches [lsort -unique $help_switches] + +# combine each sub-command's short-form options (as documented in its own +# usage synopsis) with their resolved long form(s), plus whichever tokens +# were already long-form only (eg '--auto', '--tag=LIST') +foreach n [array names help_subcmd_opts] { + set full {} + foreach tok [lsort -unique $help_subcmd_opts($n)] { + lappend full $tok + if {![string match {--*} $tok]} { + foreach long [completion_help_resolve_long $tok $n] { + lappend full $long + } + } + } + set help_subcmd_opts($n) [lsort -unique $full] +} + +if {$verbose > 1} { + send_user "\tfetched [llength $help_subcmds] sub-commands, [llength $help_switches] switches\n" +} + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/021-bash.exp b/testsuite/completion.00-init/021-bash.exp index 5ec2e35e0..749440296 100644 --- a/testsuite/completion.00-init/021-bash.exp +++ b/testsuite/completion.00-init/021-bash.exp @@ -20,7 +20,11 @@ # lists loaded modules among the option flags, 'use' # completes a filesystem directory path, 'unuse' # lists/filters the enabled modulepaths, 'restore' -# lists/filters the saved collections +# lists/filters the saved collections, and every +# sub-command/switch/option Tab-completion proposes is +# checked against the ground truth fetched from +# 'module help' (see 007-module_help.exp) -- neither +# less nor more # }C% # ############################################################################## @@ -256,6 +260,80 @@ completion_assert_eq $got {r1 } completion_bash_close +# +# ground-truth check against 'module help' (see completion.00-init/ +# 007-module_help.exp): a bare 'module ' must propose exactly the +# documented sub-commands plus the global switches, and 'module -' +# (isolating the switches from the sub-command names) must propose exactly +# the documented switches +# + +setenv_path_var MODULEPATH $modpath + +completion_bash_start + +set got [completion_bash_list {module }] +set got_subcmds [lsearch -all -inline -not -regexp $got {^-}] +completion_assert_contains $got_subcmds $help_subcmds +completion_assert_only_known $got_subcmds {*} $help_subcmds + +set got [completion_bash_list {module -}] +completion_assert_contains $got $help_switches +completion_assert_only_known $got {-*} $help_switches + +completion_bash_close + + +# +# ground-truth check, per sub-command: 'module -' +# must propose exactly the option flags 'module help' documents for that +# sub-command -- including none at all, for a sub-command that takes no +# option +# + +## typing '-' first, rather than double-Tab from a bare trailing space, +## matters for two reasons: every option-bearing sub-command's own +## completion helper (_module_avail, _module_not_yet_loaded, ...) skips +## looking up real module/collection/modulepath candidates once the word +## being completed starts with '-', isolating the option flags on their +## own; and it sidesteps the common-prefix auto-extension trap (as with +## "bar"/"ba" earlier in this file) that a bare trailing space hits for any +## sub-command whose *entire* candidate set is option flags (which, unlike +## module names, always share a leading '-') or modulepath-only entries +## (which always share a leading '/') -- eg "module aliases " +## silently completes the shared '-' inline on the first Tab, leaving only +## one Tab (a bell, no listing) for the second +setenv_path_var MODULEPATH $modpath + +completion_bash_start + +foreach subcmd $help_subcmds { + ## 'config' completes configuration key names, not option flags + if {$subcmd in {config}} { + continue + } + set expected {} + if {[info exists help_subcmd_opts($subcmd)]} { + set expected $help_subcmd_opts($subcmd) + } + if {[llength $expected] == 0} { + ## nothing is expected to complete at all: a single Tab must insert + ## nothing but a bell (not even a directory-style default-completion + ## fallback match, since no file in the test run's working directory + ## starts with '-') -- readline rings the bell on any Tab that finds + ## zero completions, not just an ambiguous one + set got [completion_bash_inline "module $subcmd -"] + completion_assert_eq $got "\a" + } else { + set got [completion_bash_list "module $subcmd -"] + completion_assert_contains $got $expected + completion_assert_only_known $got {-*} $expected + } +} + +completion_bash_close + + # # Cleanup # From 16b4a07a45fb5d2378d6ee52cba25461b2991521 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 16:26:13 +0200 Subject: [PATCH 14/37] ts: check module/ml argument kind and mirror ground truth for ml Classify each sub-command's positional argument kind from its own usage synopsis (module/collection/dir/stash/file/freeform), and check 'module ' proposes a real candidate of that kind, for the two kinds with predictable fixture data: an available/loaded module or a saved collection. 'list'/'savelist' are excluded: their argument is a search string filtering already-known state, not a name completion target, despite their usage synopsis documenting a module/collection-shaped argument. The per-sub-command dispatch of _ml (init/bash_completion.in) mirrors _module's case-for-case, so both the per-sub-command option ground truth and this new argument-kind ground truth apply identically to 'ml ' -- mirrored here as their own sections rather than folded into the 'module' loops, so a fixture/mechanism difference between the two commands shows up as its own failure. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .hunspell.en.dic | 1 + testsuite/completion.00-init/006-procs.exp | 19 +++ .../completion.00-init/007-module_help.exp | 54 ++++++- testsuite/completion.00-init/021-bash.exp | 132 +++++++++++++++++- 4 files changed, 198 insertions(+), 8 deletions(-) diff --git a/.hunspell.en.dic b/.hunspell.en.dic index 2c2dc976c..6d4f9cbf2 100644 --- a/.hunspell.en.dic +++ b/.hunspell.en.dic @@ -451,6 +451,7 @@ formatModuleCacheContent formatter foss fpath +freeform frontend fullname func diff --git a/testsuite/completion.00-init/006-procs.exp b/testsuite/completion.00-init/006-procs.exp index a3318c9d3..7a499e422 100644 --- a/testsuite/completion.00-init/006-procs.exp +++ b/testsuite/completion.00-init/006-procs.exp @@ -97,6 +97,25 @@ proc completion_assert_only_known {got pattern known} { } } +# assert at least one word in 'candidates' is present in the 'got' candidate +# list; used where the exact expected word is not known in advance (eg +# which of several plausible module names a fixture happens to offer), only +# that completion offers a word of that kind at all. The test label is +# built from the cmdline last passed to completion__list plus the +# candidate words looked for +proc completion_assert_any {got candidates} { + global completion_last_cmdline + + set label "'$completion_last_cmdline' completion contains any of $candidates" + foreach word $candidates { + if {$word in $got} { + pass $label + return + } + } + fail "$label (got: $got)" +} + # assert 'got' is exactly 'expected'; the test label is built from the # cmdline last passed to completion__list/_inline plus the expected # value diff --git a/testsuite/completion.00-init/007-module_help.exp b/testsuite/completion.00-init/007-module_help.exp index 2b2ac3b69..a9ad3c92f 100644 --- a/testsuite/completion.00-init/007-module_help.exp +++ b/testsuite/completion.00-init/007-module_help.exp @@ -16,10 +16,12 @@ # Comment: %C{ # Parse the 'module help' usage message into the set of # documented sub-commands, global switches and, for each -# sub-command, the option flags that apply to it, so -# completion tests can check that Tab-completion proposes -# exactly this set -- neither less nor more. Shell-agnostic: -# built once here and reused by every shell's test file +# sub-command, the option flags and expected positional +# argument kind (module, collection, directory, ...) that +# apply to it, so completion tests can check that +# Tab-completion proposes exactly this set -- neither +# less nor more. Shell-agnostic: built once here and +# reused by every shell's test file # }C% # ############################################################################## @@ -39,6 +41,30 @@ set help_subcmds {} # all (eg 'purge') simply has no entry array set help_subcmd_opts {} +# the kind of positional argument a sub-command's own usage synopsis +# documents, indexed by every alias name it goes by: 'module' (a module +# name/version, eg 'modulefile'/'modfile'/'mod1'/'mod2'/'mod'), +# 'collection' (a saved collection name, eg 'collection'/'coll'), 'dir' (a +# filesystem/modulepath directory, eg 'dir'/'modulepath'), 'stash' (a stash +# collection name), 'file' (an arbitrary file, eg 'scriptfile'/ +# 'shellscript'), or 'freeform' for anything else (a search string, shell +# name, variable name, config value, ...) or no argument at all -- neither +# of which bash completion can meaningfully complete against, so no +# candidate beyond the sub-command's own options is expected there +array set help_subcmd_argtype {} + +proc completion_help_classify_argtype {word} { + switch -glob -- $word { + modulepath { return dir } + mod* { return module } + coll* { return collection } + dir { return dir } + stash { return stash } + scriptfile - shellscript { return file } + default { return freeform } + } +} + # short and long forms of the global switches listed under 'Options:' -- # these apply regardless of sub-command, and are what a bare # 'module -' (no sub-command typed yet) proposes @@ -98,6 +124,7 @@ proc completion_help_resolve_long {short_flag subcmd_name} { set section none set cur_names {} +set cur_argtype_set 0 # state for the Switches: entry currently being accumulated -- its # description can wrap over several lines, possibly carrying the "(on 'x' @@ -154,17 +181,36 @@ foreach line [split $help_output "\n"] { if {[string range $line 0 35] eq [string repeat { } 36]} { continue } + set remainder $line if {[regexp {^ ([A-Za-z][A-Za-z0-9_-]*(?: \| [A-Za-z][A-Za-z0-9_-]*)*)} $line all names]} { # a new sub-command entry: remember its alias names, so option # tokens on any wrapped usage-synopsis continuation line (indented # less than the description column) still get attributed to it set cur_names [regexp -all -inline {[A-Za-z][A-Za-z0-9_-]*} $names] foreach n $cur_names { lappend help_subcmds $n } + set cur_argtype_set 0 + set remainder [string range $line [string length $all] end] } set opts [completion_help_extract_opts $line] foreach n $cur_names { foreach o $opts { lappend help_subcmd_opts($n) $o } } + if {!$cur_argtype_set} { + # strip every option bracket (content starting with '-'), so the + # first word left over is the positional argument's own name (eg + # 'modulefile' out of '[-i] [modulefile ...]') rather than an + # option's; a sub-command with no positional argument at all + # leaves only its description text here, whose first word never + # matches a known keyword and safely falls back to 'freeform' -- + # the same "nothing specific to complete" classification a real + # freeform argument gets + set stripped [regsub -all {\[-[^\]]*\]} $remainder {}] + if {[regexp {[A-Za-z][A-Za-z0-9_-]*} $stripped word]} { + set argtype [completion_help_classify_argtype $word] + foreach n $cur_names { set help_subcmd_argtype($n) $argtype } + set cur_argtype_set 1 + } + } } elseif {$section eq {options}} { if {[string index $line 2] ne {-}} { continue } foreach tok [completion_help_extract_opts $line] { diff --git a/testsuite/completion.00-init/021-bash.exp b/testsuite/completion.00-init/021-bash.exp index 749440296..197a33028 100644 --- a/testsuite/completion.00-init/021-bash.exp +++ b/testsuite/completion.00-init/021-bash.exp @@ -21,10 +21,10 @@ # completes a filesystem directory path, 'unuse' # lists/filters the enabled modulepaths, 'restore' # lists/filters the saved collections, and every -# sub-command/switch/option Tab-completion proposes is -# checked against the ground truth fetched from -# 'module help' (see 007-module_help.exp) -- neither -# less nor more +# sub-command/switch/option/argument-kind 'module' and +# 'ml' Tab-completion proposes is checked against the +# ground truth fetched from 'module help' (see +# 007-module_help.exp) -- neither less nor more # }C% # ############################################################################## @@ -334,6 +334,130 @@ foreach subcmd $help_subcmds { completion_bash_close +# +# same ground truth, this time against 'ml': its per-sub-command dispatch +# (_ml in init/bash_completion.in) mirrors 'module's case-for-case, so the +# exact same fetched option sets apply to 'ml -' +# + +setenv_path_var MODULEPATH $modpath + +completion_bash_start + +foreach subcmd $help_subcmds { + if {$subcmd in {config}} { + continue + } + set expected {} + if {[info exists help_subcmd_opts($subcmd)]} { + set expected $help_subcmd_opts($subcmd) + } + if {[llength $expected] == 0} { + set got [completion_bash_inline "ml $subcmd -"] + completion_assert_eq $got "\a" + } else { + set got [completion_bash_list "ml $subcmd -"] + completion_assert_contains $got $expected + completion_assert_only_known $got {-*} $expected + } +} + +completion_bash_close + + +# +# ground-truth check, per sub-command: 'module ' +# must propose a candidate of the kind 'module help' documents as that +# sub-command's positional argument, for the two kinds with fixture data +# predictable enough to check for by name -- an available/loaded module +# ('modulefile'/'modfile'/'mod1'/'mod2'/'mod' in the usage synopsis) or a +# saved collection ('collection'/'coll'). The remaining kinds +# (a filesystem directory, an arbitrary file, or nothing completable at +# all -- a search string, a shell name, a stash collection name generated +# with an unpredictable timestamp, ...) are not asserted on here: 'use' and +# 'unuse' already get a dedicated, fixture-appropriate check of their own +# earlier in this file, and the rest fall back to plain filename completion +# of the working directory, which is not fixture data this testsuite +# controls +# + +setenv_path_var MODULEPATH $modpath +setenv_loaded_module [list bar/1.0] [list $modpath/bar/1.0] + +completion_bash_start + +completion_bash_raw {module save foo} +completion_bash_raw {module save fox} +completion_bash_raw {module save bar1} + +foreach subcmd $help_subcmds { + ## 'list'/'savelist' take a search string filtering already-known state + ## (loaded modules / saved collections), not a name completion target: + ## their own completion only ever proposes their option flags, despite + ## their usage synopsis documenting a module/collection-shaped argument + if {$subcmd in {list savelist}} { + continue + } + if {![info exists help_subcmd_argtype($subcmd)]} { + continue + } + set argtype $help_subcmd_argtype($subcmd) + if {$argtype ni {module collection}} { + continue + } + set got [completion_bash_list "module $subcmd "] + if {$argtype eq {module}} { + ## covers every module-completion mechanism this fixture set can + ## produce: available modules not yet loaded (directory-style, + ## trailing '/'), available modules regardless of load state + ## (whatis/display/... style), and the one loaded module (flat + ## 'name/version', switch/unload style) + completion_assert_any $got {bar/ baz/ foo/ advvers/ flatlcompat bar/1.0} + } else { + completion_assert_any $got {foo fox bar1} + } +} + +completion_bash_close +unsetenv_loaded_module + + +# +# same per-sub-command argument-kind ground truth, this time against 'ml' +# + +setenv_path_var MODULEPATH $modpath +setenv_loaded_module [list bar/1.0] [list $modpath/bar/1.0] + +completion_bash_start + +completion_bash_raw {module save foo} +completion_bash_raw {module save fox} +completion_bash_raw {module save bar1} + +foreach subcmd $help_subcmds { + if {$subcmd in {list savelist}} { + continue + } + if {![info exists help_subcmd_argtype($subcmd)]} { + continue + } + set argtype $help_subcmd_argtype($subcmd) + if {$argtype ni {module collection}} { + continue + } + set got [completion_bash_list "ml $subcmd "] + if {$argtype eq {module}} { + completion_assert_any $got {bar/ baz/ foo/ advvers/ flatlcompat bar/1.0} + } else { + completion_assert_any $got {foo fox bar1} + } +} + +completion_bash_close +unsetenv_loaded_module + + # # Cleanup # From 934e5f78d430861566522baac47c8a13f3d63bad Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 20:47:03 +0200 Subject: [PATCH 15/37] init: stop offering -o/--output to savelist completion savelist does not accept -o/--output (see the module help fix), but shared a single comp_list_opts candidate set with list, which does. Split it into a dedicated comp_savelist_opts (bash, tcsh via the shared Makefile variable, zsh); fish is unaffected, since its option completion is not scoped per sub-command in the first place. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 ++ init/bash_completion.in | 6 ++++-- init/tcsh_completion.in | 2 +- init/zsh-functions/_module.in | 2 -- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/init/Makefile b/init/Makefile index b30b892c7..486f490e2 100644 --- a/init/Makefile +++ b/init/Makefile @@ -124,6 +124,7 @@ comp_opts := -D -h -s -T -v -V -p -P -w $(comp_long_opts) comp_load_opts := --auto --no-auto --force -f --icase -i --tag --tag= comp_unload_opts := --auto --no-auto --force -f --icase -i comp_list_opts := -a -C -j -l -o -S -t --all --contains --json --long --output --output= --starts-with --terse +comp_savelist_opts := -a -C -j -l -S -t --all --contains --json --long --starts-with --terse comp_stashlist_opts := -j -l -t --json --long --terse comp_clear_opts := --force -f comp_avail_opts := -a -C -d -i -j -L -l -o -S -t --all --contains --default --icase --json --latest --long --output --output= --starts-with --terse --indepth --no-indepth @@ -155,6 +156,7 @@ sed -e 's|@prefix@|$(prefix)|g' \ -e 's|@comp_load_opts@|$(comp_load_opts)|g' \ -e 's|@comp_unload_opts@|$(comp_unload_opts)|g' \ -e 's|@comp_list_opts@|$(comp_list_opts)|g' \ + -e 's|@comp_savelist_opts@|$(comp_savelist_opts)|g' \ -e 's|@comp_stashlist_opts@|$(comp_stashlist_opts)|g' \ -e 's|@comp_clear_opts@|$(comp_clear_opts)|g' \ -e 's|@comp_avail_opts@|$(comp_avail_opts)|g' \ diff --git a/init/bash_completion.in b/init/bash_completion.in index 8ed264dc7..eb6c1a577 100644 --- a/init/bash_completion.in +++ b/init/bash_completion.in @@ -82,7 +82,8 @@ _module() { spider) _module_comgen_words_and_files "@comp_avail_opts@ $(_module_spider "$cur")" "$cur";; edit) _module_comgen_words_and_files "$(_module_avail "$cur")" "$cur";; aliases) COMPREPLY=( $(compgen -W "@comp_aliases_opts@" -- "$cur") );; - list|savelist) COMPREPLY=( $(compgen -W "@comp_list_opts@" -- "$cur") );; + list) COMPREPLY=( $(compgen -W "@comp_list_opts@" -- "$cur") );; + savelist) COMPREPLY=( $(compgen -W "@comp_savelist_opts@" -- "$cur") );; stashlist) COMPREPLY=( $(compgen -W "@comp_stashlist_opts@" -- "$cur") );; stashpop|stashshow|stashrm) COMPREPLY=( $(compgen -W "$(_module_stashlist)" -- "$cur") );; @@ -147,7 +148,8 @@ if type -t ml >/dev/null; then spider) _module_comgen_words_and_files "@comp_avail_opts@ $(_module_spider "$cur")" "$cur";; edit) _module_comgen_words_and_files "$(_module_avail "$cur")" "$cur";; aliases) COMPREPLY=( $(compgen -W "@comp_aliases_opts@" -- "$cur") );; - list|savelist) COMPREPLY=( $(compgen -W "@comp_list_opts@" -- "$cur") );; + list) COMPREPLY=( $(compgen -W "@comp_list_opts@" -- "$cur") );; + savelist) COMPREPLY=( $(compgen -W "@comp_savelist_opts@" -- "$cur") );; stashlist) COMPREPLY=( $(compgen -W "@comp_stashlist_opts@" -- "$cur") );; stashpop|stashshow|stashrm) COMPREPLY=( $(compgen -W "$(_module_stashlist)" -- "$cur") );; diff --git a/init/tcsh_completion.in b/init/tcsh_completion.in index a8c789f60..7e47f8653 100644 --- a/init/tcsh_completion.in +++ b/init/tcsh_completion.in @@ -58,7 +58,7 @@ complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ 'n/is-loaded/`_module_avail; echo "@comp_mfile_opts@"`/' \ 'n/info-loaded/`_module_avail; echo "@comp_mfile_opts@"`/' \ 'n/restore/`_module_savelist`/' \ - "n/savelist/(@comp_list_opts@)/" \ + "n/savelist/(@comp_savelist_opts@)/" \ 'n/saveshow/`_module_savelist`/' \ 'n/describe/`_module_savelist`/' \ 'n/saverm/`_module_savelist`/' \ diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index 563fda5fd..8f268e0fe 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -282,8 +282,6 @@ _module() { '--terse:Display output in terse format' '-j:Display output in JSON format' '--json:Display output in JSON format' - '-o:Define elements to output in addition to module names' - '--output=:Define elements to output in addition to module names' '-S:Search modules whose name begins with query string' '--starts-with:Search modules whose name begins with query string' '-C:Search modules whose name contains query string' From 65119ae329a77ac1458e31f7e689976dd22332c8 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 8 Aug 2026 20:50:18 +0200 Subject: [PATCH 16/37] doc: highlight completion/usage-message consistency work in NEWS.rst Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- NEWS.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 9edcc73a5..53dc6e6ee 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -195,6 +195,17 @@ Modules 5.7.0 (not yet released) ``script/mt comp``. It drives a real bash session through `Expect `_ to press the Tab key against the built shell completion script and check the resulting candidate list. +* Init: fix numerous inconsistencies between ``module help``'s usage message + and the option flags and positional arguments each sub-command actually + accepts, and between that usage message and what bash, fish, tcsh and zsh + Tab completion offer for each sub-command. Guarded against regressing with + a new ground-truth check in the *completion* testsuite that compares + ``module``/``ml`` Tab completion against the sub-commands, switches, + options and argument kinds parsed straight out of ``module help``. +* Init: no longer offer the legacy :subcmd:`initadd`, :subcmd:`initclear`, + :subcmd:`initlist`, :subcmd:`initprepend` and :subcmd:`initrm` + sub-commands as Tab completion candidates (bash, fish, tcsh, zsh), since + ``module help`` does not document them. .. _5.6 release notes: From cea37f6675f74bf7f25c7568af54fa6dc035f7f2 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sun, 9 Aug 2026 09:32:32 +0200 Subject: [PATCH 17/37] init: advertise delete alias instead of its del abbreviation 'del' was offered as the Tab completion candidate for unload across bash, fish, tcsh and zsh, but it is only the 3-character minimum abbreviation of 'delete' (see the shortcut matching done by parseModuleCommandName in tcl/main.tcl), now also documented as such in module help. Advertise 'delete' as the candidate/top-level completion entry instead, and drop the special-cased 'del' dispatch entries: it was only one arbitrary abbreviation length among several valid ones that all resolve the same way, so hard-coding just that one added no real value; a manually typed abbreviation still works, it just falls back to default argument completion instead of getting unload-specific candidates. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 +- init/bash_completion.in | 6 +++--- init/fish_completion | 6 +++--- init/tcsh_completion.in | 2 +- init/zsh-functions/_module.in | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/init/Makefile b/init/Makefile index 486f490e2..850bbfd34 100644 --- a/init/Makefile +++ b/init/Makefile @@ -118,7 +118,7 @@ quarvarsre += s/@.*RUNENV_VAR.*@//;' endif # define variables for shell completion -comp_cmds := add add-any apropos aliases avail append-path cachebuild cacheclear clear config del describe disable display edit help is-loaded is-saved is-used is-avail info-loaded keyword lint list load load-any mod-to-sh path paths purge prepend-path refresh reload reset restore rm remove remove-path save savelist saveshow saverm search show sh-to-mod source spider stash stashclear stashlist stashpop stashrm stashshow state swap switch test try-add try-load unload unuse update use whatis +comp_cmds := add add-any apropos aliases avail append-path cachebuild cacheclear clear config delete describe disable display edit help is-loaded is-saved is-used is-avail info-loaded keyword lint list load load-any mod-to-sh path paths purge prepend-path refresh reload reset restore rm remove remove-path save savelist saveshow saverm search show sh-to-mod source spider stash stashclear stashlist stashpop stashrm stashshow state swap switch test try-add try-load unload unuse update use whatis comp_long_opts := --debug --dumpname --help --silent --trace --verbose --version --paginate --no-pager --color --color= --width --width= --ignore-cache --ignore-user-rc --timer --redirect --no-redirect comp_opts := -D -h -s -T -v -V -p -P -w $(comp_long_opts) comp_load_opts := --auto --no-auto --force -f --icase -i --tag --tag= diff --git a/init/bash_completion.in b/init/bash_completion.in index eb6c1a577..736f20133 100644 --- a/init/bash_completion.in +++ b/init/bash_completion.in @@ -63,7 +63,7 @@ _module_long_arg_list() { add|load) _module_comgen_words_and_files "$(_module_not_yet_loaded "$cur")" "$cur" break;; - rm|del|remove|unload|switch|swap) + rm|delete|remove|unload|switch|swap) COMPREPLY=( $(IFS=: compgen -W "${LOADEDMODULES}" -- "$cur") ) break;; esac @@ -90,7 +90,7 @@ _module() { clear) COMPREPLY=( $(compgen -W "@comp_clear_opts@" -- "$cur") );; restore|save|saveshow|describe|saverm|disable|is-saved) COMPREPLY=( $(compgen -W "$(_module_savelist)" -- "$cur") );; - rm|del|remove|unload) + rm|delete|remove|unload) COMPREPLY=( $(compgen -W "@comp_unload_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; switch|swap) COMPREPLY=( $(compgen -W "@comp_load_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; unuse|is-used) COMPREPLY=( $(IFS=: compgen -W "${MODULEPATH}" -- "$cur") );; @@ -156,7 +156,7 @@ if type -t ml >/dev/null; then clear) COMPREPLY=( $(compgen -W "@comp_clear_opts@" -- "$cur") );; restore|save|saveshow|describe|saverm|disable|is-saved) COMPREPLY=( $(compgen -W "$(_module_savelist)" -- "$cur") );; - rm|del|remove|unload) + rm|delete|remove|unload) COMPREPLY=( $(compgen -W "@comp_unload_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; switch|swap) COMPREPLY=( $(compgen -W "@comp_load_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; unuse|is-used) COMPREPLY=( $(IFS=: compgen -W "${MODULEPATH}" -- "$cur") );; diff --git a/init/fish_completion b/init/fish_completion index 472c09294..c8a11b200 100644 --- a/init/fish_completion +++ b/init/fish_completion @@ -2,7 +2,7 @@ function __fish_module_no_subcommand --description 'Test if modulecmd has yet to be given the subcommand' for i in (commandline -opc) - if contains -- $i add add-any load load-any try-add try-load rm remove del unload swap switch show display lint list avail is-loaded is-saved is-used is-avail info-loaded aliases use unuse refresh reload purge source whatis apropos keyword search test save reset restore saverm saveshow savelist stash stashclear stashlist stashpop stashrm stashshow path paths append-path prepend-path remove-path clear config sh-to-mod edit state mod-to-sh cachebuild cacheclear spider update disable describe + if contains -- $i add add-any load load-any try-add try-load rm remove delete unload swap switch show display lint list avail is-loaded is-saved is-used is-avail info-loaded aliases use unuse refresh reload purge source whatis apropos keyword search test save reset restore saverm saveshow savelist stash stashclear stashlist stashpop stashrm stashshow path paths append-path prepend-path remove-path clear config sh-to-mod edit state mod-to-sh cachebuild cacheclear spider update disable describe return 1 end end @@ -29,7 +29,7 @@ end function __fish_module_use_list --description 'Test if module command should have loaded packages as potential completion' for i in (commandline -opc) - if contains -- $i swap switch rm remove del unload + if contains -- $i swap switch rm remove delete unload return 0 end end @@ -85,7 +85,7 @@ complete -f -n '__fish_module_no_subcommand' -c module -a 'is-avail' --descripti complete -f -n '__fish_module_no_subcommand' -c module -a 'add load' --description 'Load modulefile(s)' complete -f -n '__fish_module_no_subcommand' -c module -a 'add-any load-any' --description 'Load first available modulefile in list' complete -f -n '__fish_module_no_subcommand' -c module -a 'try-add try-load' --description 'Attempt to load modulefile(s), no complain' -complete -f -n '__fish_module_no_subcommand' -c module -a 'rm remove del unload' --description 'Remove modulefile(s)' +complete -f -n '__fish_module_no_subcommand' -c module -a 'rm remove delete unload' --description 'Remove modulefile(s)' complete -f -n '__fish_module_no_subcommand' -c module -a 'swap switch' --description 'Unload mod1 and load mod2' complete -f -n '__fish_module_no_subcommand' -c module -a 'show display' --description 'Display information about modulefile(s)' complete -f -n '__fish_module_no_subcommand' -c module -a 'list' --description 'List loaded modules' diff --git a/init/tcsh_completion.in b/init/tcsh_completion.in index 7e47f8653..c52a2271f 100644 --- a/init/tcsh_completion.in +++ b/init/tcsh_completion.in @@ -41,7 +41,7 @@ complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ 'n/try-add/`_module_not_yet_loaded; echo "@comp_load_opts@"`/' \ 'n/try-load/`_module_not_yet_loaded; echo "@comp_load_opts@"`/' \ 'n/rm/`_module_loaded; echo "@comp_unload_opts@"`/' \ - 'n/del/`_module_loaded; echo "@comp_unload_opts@"`/' \ + 'n/delete/`_module_loaded; echo "@comp_unload_opts@"`/' \ 'n/remove/`_module_loaded; echo "@comp_unload_opts@"`/' \ 'n/unload/`_module_loaded; echo "@comp_unload_opts@"`/' \ 'n/swap/`_module_loaded; echo "@comp_load_opts@"`/' \ diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index 8f268e0fe..4f345eebf 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -151,7 +151,7 @@ _module() { 'try-load:Attempt to load modulefile(s), no complain' 'rm:Remove modulefile(s)' 'remove:Remove modulefile(s)' - 'del:Remove modulefile(s)' + 'delete:Remove modulefile(s)' 'unload:Remove modulefile(s)' 'purge:Unload all loaded modulefiles' 'reload:Unload then load all loaded modulefiles' @@ -307,7 +307,7 @@ _module() { _alternative 'avail-colls:collections:{_module_saved_colls}' \ && ret=0 ;; - (rm|del|remove|unload) + (rm|delete|remove|unload) _arguments \ '--auto[Enable automated module handling mode]' \ '--no-auto[Disable automated module handling mode]' \ From 26cf894f1a14df5dd27bd43069f172d247bc9bd7 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Mon, 10 Aug 2026 08:49:33 +0200 Subject: [PATCH 18/37] doc: document remove/delete aliases and prepend option for use Cross-checked the Module Sub-Commands section in module.rst against the reportUsage usage synopsis and the bash completion script: - 'remove' has been a real, registered alias of 'unload' since long before this rewrite (see NEWS.rst issue#200) and 'delete' is now advertised the same way, but neither ever got its own entry in module.rst; only 'rm' did. Add matching 'See unload' entries for both, alongside the existing 'rm' one. - 'use' accepts a real, functional -p/--prepend option (parsed in tcl/main.tcl) that was completely undocumented: the synopsis and prose only mentioned -a/--append. Document -p/--prepend next to it. Every other sub-command name and per-sub-command option already matches across the three; the legacy initadd/initclear/initlist/ initprepend/initrm/initswitch sub-commands and autoinit remain correctly absent from reportUsage and completion, since module help does not document them either. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- doc/source/module.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/source/module.rst b/doc/source/module.rst index ba386eea7..11b9bfb36 100644 --- a/doc/source/module.rst +++ b/doc/source/module.rst @@ -1891,6 +1891,10 @@ Module Sub-Commands *Value* prefixes ``+`` and ``-`` added to append and subtract elements to list-value options +.. subcmd:: delete [--auto|--no-auto] [-f] modulefile... + + See :subcmd:`unload`. + .. subcmd:: describe [collection] See :subcmd:`saveshow`. @@ -2414,6 +2418,10 @@ Module Sub-Commands .. versionchanged:: 5.4 Option :option:`--force`/:option:`-f` added +.. subcmd:: remove [--auto|--no-auto] [-f] modulefile... + + See :subcmd:`unload`. + .. subcmd:: remove-path [options] variable value... Remove *value* from the colon, or *delimiter*, separated list in environment @@ -3072,11 +3080,12 @@ Module Sub-Commands .. versionadded:: 5.6 -.. subcmd:: use [-a|--append] directory... +.. subcmd:: use [-a|--append|-p|--prepend] directory... Prepend one or more *directories* to the :envvar:`MODULEPATH` environment - variable. The ``--append`` flag will append the *directory* to - :envvar:`MODULEPATH`. + variable. The ``--append``/``-a`` flag will append the *directory* to + :envvar:`MODULEPATH` instead. The ``--prepend``/``-p`` flag explicitly + requests the default prepend behavior. When *directory* is already defined in :envvar:`MODULEPATH`, it is not added again or moved at the end or at the beginning of the environment variable. From b8c7db374b2de62ecf7daaa56ab8d783213c23cd Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Mon, 10 Aug 2026 09:41:22 +0200 Subject: [PATCH 19/37] init: complete config sub-command completion with 4 missing options 'module config' reports 72 configuration options, but Tab completion only offered 68 of them as candidates: csh_limit, ignored_dirs, siteconfig and tcl_ext_lib were missing. These four have no MODULES_* environment variable override (unlike every other option) and cannot be altered via 'module config name value', but they remain valid, real option names that 'module config name' can query, so they belong in the candidate list like the rest. Bash and Tcsh completion share the same comp_config_opts Makefile variable (init/Makefile), fixed in one place. Fish (init/fish_completion) and Zsh (init/zsh-functions/_module.in) each spell out their own copy of that same option list by hand instead, so they need the same four names added directly. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 +- init/fish_completion | 2 +- init/zsh-functions/_module.in | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/init/Makefile b/init/Makefile index 850bbfd34..ed8d35936 100644 --- a/init/Makefile +++ b/init/Makefile @@ -138,7 +138,7 @@ comp_lint_opts := -a -i --all --icase comp_modtosh_opts := --auto --no-auto --force -f --icase -i comp_path_opts := -d --delim --duplicates comp_rm_path_opts := -d --delim --index -comp_config_opts := --dump-state --reset abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277 +comp_config_opts := --dump-state --reset abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact csh_limit domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc ignored_dirs implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug siteconfig source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_ext_lib tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277 define translate-in-script $(ECHO_GEN) diff --git a/init/fish_completion b/init/fish_completion index c8a11b200..0dd49088d 100644 --- a/init/fish_completion +++ b/init/fish_completion @@ -76,7 +76,7 @@ complete -c module -n '__fish_module_use_stashlist' -f -a "(module stashlist --c /Stash collection list\$/d; \ /:\$/d; \ /:ERROR:/d;')" -complete -c module -n '__fish_module_use_config' -f -a "--dump-state --reset abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277" +complete -c module -n '__fish_module_use_config' -f -a "--dump-state --reset abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact csh_limit domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc ignored_dirs implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug siteconfig source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_ext_lib tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277" complete -f -n '__fish_module_no_subcommand' -c module -a 'help' --description 'Print this or modulefile(s) help info' complete -f -n '__fish_module_no_subcommand' -c module -a 'avail' --description 'List all or matching available modules' diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index 4f345eebf..f47a6147c 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -379,7 +379,7 @@ _module() { _arguments \ '--dump-state[Report each state value of current Modules execution]' \ '--reset[Unset environment variable relative to configuration key]' \ - '1:configuration key:(abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277)' \ + '1:configuration key:(abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact csh_limit domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc ignored_dirs implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug siteconfig source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_ext_lib tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277)' \ && ret=0 ;; (edit) From 7b252bf75f6b32614620b40bc1792fe598109232 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Mon, 10 Aug 2026 09:57:40 +0200 Subject: [PATCH 20/37] Document --reset in config sub-command usage synopsis The reportUsage 'config' line only mentioned '[--dump-state|name [val]]', omitting the '--reset name' form that module.rst already documents and that 'module config --reset name' really accepts. This also lets '--reset' resolve through the generic per-sub-command option parsing used by the completion ground-truth testsuite, the same way every other sub-command's flags do. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- tcl/report.tcl.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 4ac26cde0..85f630fbf 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2092,7 +2092,8 @@ Other commands: paths [-i] modulefile Print path of matching available modules clear [-f] Reset Modules-specific runtime information source scriptfile [...] Execute scriptfile(s) - config [--dump-state|name [val]] Display or set Modules configuration + config [--dump-state|name [val]|--reset name] + Display or set Modules configuration state [name] Display Modules state sh-to-mod shell shellscript [arg ...] Make modulefile from script env changes From 3ebc15bf62f6c043274d68890f8ab8c42af24c33 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Mon, 10 Aug 2026 10:07:35 +0200 Subject: [PATCH 21/37] ts: check config sub-command completion against module config ground truth Fetch every existing configuration option name from a bare 'modulecmd bash config' execution, the same way 007-module_help.exp already does for 'module help', into a new 008-module_config.exp. Check it against 'config' sub-command completion two ways: its option-flag check folds into the same per-sub-command loop every other sub-command goes through (typing '--' instead of '-' first, since --dump-state/--reset are its only options and share that extra dash as a common prefix, which would otherwise eat one of the two Tabs the double-Tab listing relies on), and its configuration-option-name check folds into the loop that checks module/collection arguments, as a specific case alongside those. Neither loop needs 'config' to be skipped anymore. Numbered 008, right after 007-module_help.exp, for the same save_test_env-baseline reason that file is numbered ahead of 010/011 (see its own commit message) -- and add it to the mandatory completion setup file list in script/mt and testsuite.rst, an omission in the original addition of this file. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- doc/source/devel/testsuite.rst | 4 +- script/mt | 2 +- .../completion.00-init/008-module_config.exp | 49 +++++++++++++++++++ testsuite/completion.00-init/021-bash.exp | 44 ++++++++++++----- 4 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 testsuite/completion.00-init/008-module_config.exp diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 58b25e4bd..33762ab29 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -637,8 +637,8 @@ Whichever files are selected, :file:`script/mt` always also runs the mandatory setup files for that tool (for ``modules``: ``00/005 00/006 00/010 00/050 00/060 00/080 00/085``; for ``install``: ``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``; for -``completion``: ``00/005 00/006 00/007 00/010 00/011 00/020``), plus the -``999-cleanup.exp`` of every selected series. Passing a bare series +``completion``: ``00/005 00/006 00/007 00/008 00/010 00/011 00/020``), plus +the ``999-cleanup.exp`` of every selected series. Passing a bare series number always expands to every file in that directory, because several of those series are order-sensitive or enumerate a whole modulepath (see `Adding new test fixtures`_). The collection series (``61``) is one such diff --git a/script/mt b/script/mt index 14fb3f0d7..f9e927080 100755 --- a/script/mt +++ b/script/mt @@ -107,7 +107,7 @@ if [ $# -gt 0 ]; then elif [ "$1" = "comp" ]; then target='testcompletion' testserie='completion' - setuptestfiles=(00/005 00/006 00/007 00/010 00/011 00/020) + setuptestfiles=(00/005 00/006 00/007 00/008 00/010 00/011 00/020) shift fi fi diff --git a/testsuite/completion.00-init/008-module_config.exp b/testsuite/completion.00-init/008-module_config.exp new file mode 100644 index 000000000..e6c813d69 --- /dev/null +++ b/testsuite/completion.00-init/008-module_config.exp @@ -0,0 +1,49 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/10 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Fetch ground-truth completion data from 'module config' +# Command: +# Sub-Command: +# +# Comment: %C{ +# Parse the 'module config' output into the set of every +# existing configuration option name, so completion tests +# can check that the 'config' sub-command's Tab-completion +# proposes exactly this set -- neither less nor more. +# Shell-agnostic: built once here and reused by every +# shell's test file +# }C% +# +############################################################################## + +if {$verbose} { + send_user "\tParsing 'module config' output ...\n" +} + +catch {exec $TCLSH $MODULECMD bash config --color=never} config_output + +# every existing configuration option name, as reported by 'module config' +# (one per line, name in the first column, followed by its current value) +set config_opts {} + +foreach line [split $config_output \n] { + if {[regexp {^([a-z][a-z0-9_]*)\s} $line -> name]} { + lappend config_opts $name + } +} + +set config_opts [lsort -unique $config_opts] + +if {$verbose > 1} { + send_user "\tfetched [llength $config_opts] configuration option names\n" +} + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/021-bash.exp b/testsuite/completion.00-init/021-bash.exp index 197a33028..db285fa47 100644 --- a/testsuite/completion.00-init/021-bash.exp +++ b/testsuite/completion.00-init/021-bash.exp @@ -20,11 +20,13 @@ # lists loaded modules among the option flags, 'use' # completes a filesystem directory path, 'unuse' # lists/filters the enabled modulepaths, 'restore' -# lists/filters the saved collections, and every +# lists/filters the saved collections, 'config' lists +# every configuration option name, and every # sub-command/switch/option/argument-kind 'module' and # 'ml' Tab-completion proposes is checked against the # ground truth fetched from 'module help' (see -# 007-module_help.exp) -- neither less nor more +# 007-module_help.exp) and 'module config' (see +# 008-module_config.exp) -- neither less nor more # }C% # ############################################################################## @@ -308,10 +310,6 @@ setenv_path_var MODULEPATH $modpath completion_bash_start foreach subcmd $help_subcmds { - ## 'config' completes configuration key names, not option flags - if {$subcmd in {config}} { - continue - } set expected {} if {[info exists help_subcmd_opts($subcmd)]} { set expected $help_subcmd_opts($subcmd) @@ -325,7 +323,15 @@ foreach subcmd $help_subcmds { set got [completion_bash_inline "module $subcmd -"] completion_assert_eq $got "\a" } else { - set got [completion_bash_list "module $subcmd -"] + ## 'config' is the only sub-command whose every option is long-form + ## only ('--dump-state'/'--reset', no '-x' short forms): from a + ## single '-' they still share one more '-' as a common prefix, + ## which the first Tab would silently auto-insert (the same + ## auto-extension trap as "bar"/"ba" earlier in this file), leaving + ## only one Tab for the double-Tab below -- type that second '-' + ## already, like every other sub-command's full common prefix here + set dash [expr {$subcmd eq {config} ? {--} : {-}}] + set got [completion_bash_list "module $subcmd $dash"] completion_assert_contains $got $expected completion_assert_only_known $got {-*} $expected } @@ -345,9 +351,6 @@ setenv_path_var MODULEPATH $modpath completion_bash_start foreach subcmd $help_subcmds { - if {$subcmd in {config}} { - continue - } set expected {} if {[info exists help_subcmd_opts($subcmd)]} { set expected $help_subcmd_opts($subcmd) @@ -356,7 +359,8 @@ foreach subcmd $help_subcmds { set got [completion_bash_inline "ml $subcmd -"] completion_assert_eq $got "\a" } else { - set got [completion_bash_list "ml $subcmd -"] + set dash [expr {$subcmd eq {config} ? {--} : {-}}] + set got [completion_bash_list "ml $subcmd $dash"] completion_assert_contains $got $expected completion_assert_only_known $got {-*} $expected } @@ -398,6 +402,17 @@ foreach subcmd $help_subcmds { if {$subcmd in {list savelist}} { continue } + if {$subcmd eq {config}} { + ## 'config' takes a configuration option name, not a module or + ## collection: check it against the ground truth fetched from + ## 'module config' (see 008-module_config.exp), plus the two real + ## flags mixed into the same candidate list -- neither less nor more + set got [completion_bash_list "module $subcmd "] + completion_assert_contains $got $config_opts + completion_assert_only_known $got {*} [concat $config_opts\ + $help_subcmd_opts(config)] + continue + } if {![info exists help_subcmd_argtype($subcmd)]} { continue } @@ -439,6 +454,13 @@ foreach subcmd $help_subcmds { if {$subcmd in {list savelist}} { continue } + if {$subcmd eq {config}} { + set got [completion_bash_list "ml $subcmd "] + completion_assert_contains $got $config_opts + completion_assert_only_known $got {*} [concat $config_opts\ + $help_subcmd_opts(config)] + continue + } if {![info exists help_subcmd_argtype($subcmd)]} { continue } From cc5efa9576f5760d3b61c0a24e4be014085984bd Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Mon, 10 Aug 2026 16:59:34 +0000 Subject: [PATCH 22/37] init: complete zsh savelist completion with missing -a/--all The 'savelist' case in _module.in defines its own option list by hand (rather than sharing 'list' _arguments spec), and it was missing -a/--all: per module.rst, --all applies to 'savelist' the same way it does to avail/list/lint/search/spider/whatis, but this static list never picked it up. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/zsh-functions/_module.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index f47a6147c..89ef3bb68 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -282,6 +282,8 @@ _module() { '--terse:Display output in terse format' '-j:Display output in JSON format' '--json:Display output in JSON format' + '-a:Include hidden modules in search' + '--all:Include hidden modules in search' '-S:Search modules whose name begins with query string' '--starts-with:Search modules whose name begins with query string' '-C:Search modules whose name contains query string' From f9cce0220a830e4d0d41f6a666fc471ec380c2ed Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Tue, 11 Aug 2026 08:39:39 +0000 Subject: [PATCH 23/37] ts: check zsh completion, mirroring the bash completion testsuite Add completion.00-init/030-zsh-procs.exp (a completion_zsh_{start,raw,list,close} set, mirroring 020-bash-procs.exp) and 031-zsh.exp (mirroring 021-bash.exp section-for-section), driving a real zsh pty through init/zsh- functions/_module the same way the bash tests drive init/ bash_completion. zle redraws candidates through a completely different byte stream than readline, so the raw-output parsing is specific to zsh: no bell is rung when a Tab silently extends the typed word to a longer, still- ambiguous common prefix (only a Tab that cannot insert anything at all rings the bell); AUTO_MENU inserts the first listed candidate inline right after a listing; the default 2000-byte match_max Expect uses silently truncates listings padded to a wide pty width, unless increased; and a listing too tall for the pty scrolls it, leaving neither a cursor-up marker nor the usual post-listing redraw to key off of. Two behavioral differences from bash are real, not test bugs, and the assertions in 021-bash.exp are adjusted accordingly: zsh does not ring that bell on a still-ambiguous common-prefix extension (see above, so every such case drops the leading bell bash expects), and _module.in offers modulefile/collection candidates and option flags as two mutually exclusive sets gated on whether the word being completed starts with '-', rather than the always-mixed-together comp_*_opts bash uses, so a bare trailing-space listing only ever shows one set at a time. _module.in also has no 'ml' support at all (only "compdef module"), so every section mirroring 'ml' behavior in 021-bash.exp is dropped here. 005-init_ts.exp gains the zsh binary/built-completion-script checks already present there for bash, and the comp setup file list in script/mt picks up 00/030 alongside the existing bash one. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .hunspell.en.dic | 2 + script/mt | 2 +- testsuite/completion.00-init/005-init_ts.exp | 12 + .../completion.00-init/030-zsh-procs.exp | 376 ++++++++++++++++ testsuite/completion.00-init/031-zsh.exp | 402 ++++++++++++++++++ 5 files changed, 793 insertions(+), 1 deletion(-) create mode 100644 testsuite/completion.00-init/030-zsh-procs.exp create mode 100644 testsuite/completion.00-init/031-zsh.exp diff --git a/.hunspell.en.dic b/.hunspell.en.dic index 6d4f9cbf2..936934eb1 100644 --- a/.hunspell.en.dic +++ b/.hunspell.en.dic @@ -340,6 +340,7 @@ commandname compA compB compat +compdef completionhome compilerTag compopt @@ -945,6 +946,7 @@ xrdb xtrace zA zipball +zle zlogin zsh zshcompletiondir diff --git a/script/mt b/script/mt index f9e927080..47b27ff83 100755 --- a/script/mt +++ b/script/mt @@ -107,7 +107,7 @@ if [ $# -gt 0 ]; then elif [ "$1" = "comp" ]; then target='testcompletion' testserie='completion' - setuptestfiles=(00/005 00/006 00/007 00/008 00/010 00/011 00/020) + setuptestfiles=(00/005 00/006 00/007 00/008 00/010 00/011 00/020 00/030) shift fi fi diff --git a/testsuite/completion.00-init/005-init_ts.exp b/testsuite/completion.00-init/005-init_ts.exp index 2aee90294..3d14dfdc6 100644 --- a/testsuite/completion.00-init/005-init_ts.exp +++ b/testsuite/completion.00-init/005-init_ts.exp @@ -53,6 +53,18 @@ if {![file exists $bashcompletion]} { unsupported "completion script not found: $bashcompletion (run 'make initdir' first)" } +# a zsh binary is required to drive interactive completion tests +set zshbin [find_bin zsh] +if {$zshbin eq {}} { + unsupported {no zsh binary found to drive completion tests} +} + +# built zsh completion script under test +set zshcompletion "$env(OBJDIR)/init/zsh-functions/_module" +if {![file exists $zshcompletion]} { + unsupported "completion script not found: $zshcompletion (run 'make initdir' first)" +} + # clean modulefile fixture tree: unlike testsuite/modulefiles, this one holds # no modulefile deliberately broken to exercise error paths, which would get # in the way of straightforward completion listing checks diff --git a/testsuite/completion.00-init/030-zsh-procs.exp b/testsuite/completion.00-init/030-zsh-procs.exp new file mode 100644 index 000000000..cea7f8564 --- /dev/null +++ b/testsuite/completion.00-init/030-zsh-procs.exp @@ -0,0 +1,376 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/10 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Test procedures +# Command: +# Sub-Command: +# +# Comment: %C{ +# Defines the test procedures driving a real zsh pty to +# exercise the module Tab completion script exactly as a +# user pressing Tab would, then collect the resulting +# candidate word list. This mirrors 020-bash-procs.exp's +# completion_bash_{start,raw,list,close} set (see that +# file's header for why each shell gets its own), but +# the zsh line editor (zle) redraws candidates through a +# completely different byte stream than readline, so the +# raw-output parsing below is specific to zsh: no bell +# is rung when a Tab silently extends the typed word to +# a longer (still ambiguous) common prefix -- unlike +# readline, only a Tab that cannot insert anything at +# all rings the bell -- and every candidate list is +# preceded by a cursor-up escape sequence that this file +# uses as the reliable end-of-list marker, plus (for +# option flags, which zsh's _arguments lists one per +# line together with a "-- description" suffix, unlike +# bash's bare compgen listing) a per-line " -- " cut +# that drops the description text before it can pollute +# the candidate word list +# }C% +# +############################################################################## + +# tail pattern matching zero or more trailing ANSI CSI escape sequences (eg +# the bracketed-paste mode toggle zle emits right after redrawing a prompt); +# folded into every prompt-wait regexp below since such sequences may or may +# not have landed in the read buffer yet by the time the prompt text itself +# is matched +set completion_zsh_csitail {(\x1b\[[0-9;?]*[a-zA-Z])*} + +# start a fresh zsh session with the completion script under test wired into +# FPATH and 'module' completion registered (via the script's own '#compdef +# module' pragma, picked up by compinit), ready to accept commands +proc completion_zsh_start {} { + global zshbin zshcompletion completion_prompt completion_zsh_spawn_id + global completion_zsh_csitail TCLSH MODULECMD verbose + + if {$verbose > 1} { + send_user "\tStarting zsh for completion test\n" + } + + spawn $zshbin -f + set completion_zsh_spawn_id $spawn_id + + # Expect's default 'match_max' (2000 bytes) truncates (silently drops + # the oldest bytes of) any read buffer that grows past it before a + # pattern matches -- a real risk here: a wide pty (see below) pads every + # candidate line out to the terminal width, so even a modest listing of + # option flags with descriptions is several thousand bytes: raise it + # well past the largest listing this file can produce (module bare + # 'module ', dozens of sub-commands, still under it) + match_max -i $completion_zsh_spawn_id 100000 + + # a narrow (default 80-column) pty can make a long completed line wrap, + # which makes zle redraw it with extra escape sequences that can be + # mistaken for stray inserted characters (e.g. a trailing space) by + # anything matching on raw output; go wide enough that no cmdline used + # in these tests is ever at risk of wrapping + stty rows 40 columns 500 < $spawn_out(slave,name) + + set spawn_id $completion_zsh_spawn_id + expect { + -re {[%#] } {} + timeout { unresolved {completion zsh did not start}; return } + } + + # every wait up to (not including) the PS1 change below deliberately + # does not anchor on end-of-buffer ('$'): zsh's stock, unconfigured + # prompt is still followed by a bracketed-paste-mode escape sequence + # (until the first command below disables it) that may or may not have + # already landed in the read buffer by the time the prompt text itself + # is matched, same problem completion_zsh_csitail works around further + # down -- except here that toggle has not been disabled yet, so its + # exact tail length cannot be predicted; every setup command after this + # point goes through completion_zsh_raw, which matches on the + # (by-then-custom) prompt plus completion_zsh_csitail + send -- "zle_bracketed_paste=()\r" + expect -re {[%#] } + # zsh pads the line with reverse-video spaces (ending in a highlighted + # '%') before any prompt whose previous line did not end with a newline, + # to visually flag the truncation -- since every command here is sent + # terminated with a bare '\r', that marker would otherwise appear before + # every single prompt redraw + send -- "unsetopt PROMPT_SP PROMPT_CR\r" + expect -re {[%#] } + send -- "PROMPT_EOL_MARK=''\r" + expect -re {[%#] } + # zle asks "Do you wish to see all N possibilities (M lines)?" instead + # of listing outright once matches exceed this threshold (100 by + # default; 0 instead falls back to comparing against the terminal + # size, which a mere 40-row pty would still trip for a long listing); + # some completions here (e.g. bare 'module', dozens of sub-commands) + # go well past the default + send -- "LISTMAX=100000\r" + expect -re {[%#] } + # avoid filename-completion color codes (eg from a LS_COLORS inherited + # from the invoking terminal) getting mixed into the raw output the + # 'use' directory-completion test parses + send -- "unset LS_COLORS\r" + expect -re {[%#] } + send -- "PS1='$completion_prompt'\r" + expect { + -re "$completion_prompt$completion_zsh_csitail\$" {} + timeout { unresolved {completion zsh prompt setup timed out}; return } + } + + completion_zsh_raw {zstyle ':completion:*' list-colors ''} + # put the built completion script's directory at the front of FPATH and + # load it; compinit registers 'module' completion itself, from the + # script's own '#compdef module' pragma -- '-u' skips the + # owner/group-writability check compinit would otherwise run on every + # function file found on FPATH, which would need real ownership of the + # (sandboxed, throwaway) test tree to pass + completion_zsh_raw "fpath=('[file dirname $zshcompletion]' \$fpath)" + completion_zsh_raw {autoload -Uz compinit} + completion_zsh_raw {compinit -u} + # a real 'module' command is needed since the completion script shells + # out to it (module avail/spider) to list candidate module names + completion_zsh_raw "eval \"\$('$TCLSH' '$MODULECMD' zsh autoinit)\"" +} + +# send a line to the completion zsh session and wait for the next prompt; +# used for setup commands whose output is not asserted on +proc completion_zsh_raw {line} { + global completion_zsh_spawn_id completion_prompt completion_zsh_csitail + global verbose + + set spawn_id $completion_zsh_spawn_id + send -- "$line\r" + expect { + -re "\r\n$completion_zsh_csitail$completion_prompt$completion_zsh_csitail\$" {} + timeout { unresolved "completion setup command timed out: $line" } + } + if {$verbose > 1} { + send_user "\tcompletion> $line\n" + } +} + +# strip every ANSI CSI escape sequence from a chunk of raw zle output; used +# by both completion_zsh_list and completion_zsh_inline once the byte range +# of interest has been isolated +proc completion_zsh_strip_csi {str} { + return [regsub -all {\x1b\[[0-9;?]*[a-zA-Z]} $str {}] +} + +# clean up the text zle inserted right after a cmdline's echo: drop the +# cursor-left/spaces/cursor-left triplet zle uses to erase whatever longer +# text (eg a prior candidate, or the old end of the line) used to occupy +# the redrawn region, then strip any other escape sequence and the +# trailing carriage returns coming from zle's redraw -- a real bell +# character, if present, is deliberately left untouched. Used by both +# completion_zsh_inline and completion_zsh_list's single-candidate fallback +proc completion_zsh_clean_suffix {suffix} { + regsub -all {\x1b\[[0-9]*D[ ]*\x1b\[[0-9]*D} $suffix {} suffix + set suffix [completion_zsh_strip_csi $suffix] + regsub -all {[\r\x08]} $suffix {} suffix + return $suffix +} + +# send a partial command line followed by a double-Tab (the first Tab +# silently inserts any further common prefix zsh can determine and rings the +# bell only if that leaves the completion still ambiguous; the second Tab is +# what makes zle list every ambiguous match, per the 'AUTO_MENU' option this +# also puts zle into menu-selection -- inserting the first listed candidate +# inline -- but that is discarded here along with the rest of the line by +# the trailing ^U), and return the sorted, de-duplicated list of candidate +# words zle displayed. Unlike bash, where an option-bearing sub-command's +# candidate set always includes its option flags alongside its +# modulefile/collection candidates (so it is never accidentally a single +# candidate), zsh's _module.in offers only one of the two at a time (gated +# on whether the word being completed starts with '-', same as bash's own +# _module_avail() internally does for its half) -- so a cmdline whose +# non-flag candidate set genuinely has only one member (eg a sub-command +# ground-truth check against a fixture with only one loaded module) hits a +# unique completion instead of a listing; that case is detected by the +# absence of the epilogue marker below and handled by reconstructing the +# single candidate from the inline insertion instead, the same way +# completion_zsh_inline does +proc completion_zsh_list {cmdline} { + global completion_zsh_spawn_id completion_prompt completion_last_cmdline + global completion_zsh_csitail verbose + + set completion_last_cmdline $cmdline + set spawn_id $completion_zsh_spawn_id + set recmdline [completion_re_escape $cmdline] + + send -- "$cmdline\t\t" + # let the whole candidate block land in the pty buffer -- and zle + # finish drawing it -- before sending the cleanup '^U' below: sending + # it while zle is still mid-redraw (eg still waiting on a 'module + # avail'/'module spider' subprocess for some other, slower candidate + # set) corrupts the redraw (observed as entire leading rows of an + # otherwise-fine listing going missing), and interactively this margin + # is comfortable, but on a loaded machine it can be tight + after 1500 + # clear the (uncompleted, possibly menu-selected, or already-unique) + # input line so the session is ready to reuse; capture everything from + # right after the Tabs through this cleanup in one shot, rather than + # trying to spot where the listing itself ends on its own -- a listing + # taller than the pty scrolls it, and a scrolled listing has no reliable + # "done" marker of its own (no cursor-up-N is emitted: there is nothing + # above the top row left to return the cursor to) + send -- "\025\r" + set buffer {} + expect { + -re "(?s)(.*)\r\n$completion_zsh_csitail$completion_prompt$completion_zsh_csitail\$" { + set buffer $expect_out(1,string) + } + timeout { + unresolved "completion listing timed out for '$cmdline'" + return {} + } + } + if {$verbose > 1} { + send_user "\tcompletion raw listing for '$cmdline':\n$buffer\n" + } + + # the captured text starts with the raw echo of $cmdline plus the first + # Tab's response (a bell, or a silently inserted common-prefix + # extension) -- neither of which is candidate data -- followed by a + # real newline, then (if the completion was ambiguous) the actual + # multi-column (or, for option flags, one-per-line) listing. A listing + # short enough to fit in the pty ends in zle's 'sgr0/rmso/rmul'-alike + # epilogue right before it repaints the cmdline (auto-menu-selected + # onto its first candidate) and, after this proc's own '^U', the + # prompt -- a more reliable end-of-listing marker than a cursor-up-N + # escape sequence, which is only emitted in this same case. A listing + # too tall for the pty (eg bare 'module', enough sub-commands to + # scroll even a generously-tall terminal) instead just ends the + # captured text outright, with neither marker: zle leaves it as the + # last thing printed rather than trying to redraw a cmdline that has + # scrolled off + set epilogue {\x1b\[0m\x1b\[27m\x1b\[24m} + set cmdstart [string first $cmdline $buffer] + if {$cmdstart == -1} { + set cmdstart 0 + } + set nl [string first "\n" $buffer $cmdstart] + # this exact epilogue also prefixes every ordinary prompt redraw (eg the + # fresh prompt this proc's own '^U' produces), so search for it starting + # only from the real listing's start, not from the top of the buffer -- + # otherwise a straggler byte or two of the *previous* completion_zsh_list/ + # _inline call's own prompt redraw, still in flight when this call's + # Tabs were sent and thus prepended ahead of $cmdstart, would match here + # first and be mistaken for this listing's own end + set have_epilogue [expr {$nl != -1\ + && [regexp -indices -start $nl -- $epilogue $buffer epi_range]}] + + if {$have_epilogue} { + lassign $epi_range epi_start epi_end + set listing [string range $buffer $nl+1 $epi_start-1] + } elseif {$nl != -1 && [string first "\n" $buffer $nl+1] != -1} { + # no epilogue, but more than one line of content followed the echo: + # a scrolled, too-tall-for-the-pty listing (see above) rather than a + # single-candidate insertion, which never spans more than one line + set listing [string range $buffer $nl+1 end] + } else { + # neither: the second Tab found a single, already-unique candidate + # and inserted it inline instead of listing anything (see this + # proc's header); reconstruct that one candidate from the inline + # insertion. When the sub-command accepts more than one modulefile + # (eg 'unload'/'delete'), that second Tab can go on to immediately + # re-trigger completion for the (again unique) next argument too, + # duplicating the same word right after itself -- split on + # whitespace and de-duplicate rather than returning that whole + # blob as one candidate + set suffix {} + regexp "(?s)${recmdline}(.*)" $buffer -> suffix + set suffix [string trim [completion_zsh_clean_suffix $suffix]] + if {$suffix eq {} || $suffix eq "\a"} { + return {} + } + return [lsort -unique [split $suffix]] + } + + set listing [completion_zsh_strip_csi $listing] + + # collapse the listing into a flat, sorted word list: a plain line (eg a + # module/collection/modulepath candidate) is split as-is, while a line + # zsh's _arguments describes (eg an option flag) has its trailing + # '-- description text' cut off first, since that description is made of + # ordinary words that would otherwise pollute the candidate list + set words {} + foreach line [split $listing "\r\n"] { + set sep [string first { -- } $line] + if {$sep != -1} { + set line [string range $line 0 $sep-1] + } + foreach word [split $line] { + if {$word ne {}} { + lappend words $word + } + } + } + return [lsort -unique $words] +} + +# send a partial command line followed by a single Tab, and return the text +# zle inserted inline (no listing involved) -- a bell ('\a') if nothing at +# all could be completed, otherwise the extra characters zle appended (which +# may itself be just a further common-prefix extension, silently, with no +# bell, if the completion remains ambiguous beyond it -- unlike readline, +# see this file's header). Used to check unique, unambiguous completions -- +# eg that a directory-style entry gets completed with a trailing '/' and no +# trailing space. cmdline must be a prefix with exactly one match, or with a +# common prefix short enough that no further Tab is needed to reach it, +# otherwise a single Tab may extend by less than a full match +proc completion_zsh_inline {cmdline} { + global completion_zsh_spawn_id completion_prompt completion_last_cmdline + global completion_zsh_csitail verbose + + set completion_last_cmdline $cmdline + set spawn_id $completion_zsh_spawn_id + set recmdline [completion_re_escape $cmdline] + + send -- "$cmdline\t" + # let every byte zle is going to redraw land in the pty buffer before + # reading it: an 'expect' pattern ending in a lazy '(.*)$' would + # otherwise match the instant the plain cmdline echo arrives, racing + # ahead of e.g. a trailing space that is still in flight + after 300 + set suffix {} + expect { + -re "(?s)${recmdline}(.*)\$" { + set suffix $expect_out(1,string) + } + timeout { + unresolved "completion inline insert timed out for '$cmdline'" + } + } + + # clear the (possibly completed) input line so the session is ready to + # reuse, same technique as completion_zsh_list + send -- "\025\r" + expect -re "\r\n$completion_zsh_csitail$completion_prompt$completion_zsh_csitail\$" + + set suffix [completion_zsh_clean_suffix $suffix] + + if {$verbose > 1} { + send_user "\tcompletion inline insert for '$cmdline': \[$suffix\]\n" + } + + return $suffix +} + +proc completion_zsh_close {} { + global completion_zsh_spawn_id + + if {[info exists completion_zsh_spawn_id]} { + set spawn_id $completion_zsh_spawn_id + catch {send -- "exit\r"} + catch {expect eof} + catch {close -i $completion_zsh_spawn_id} + catch {wait -i $completion_zsh_spawn_id} + unset completion_zsh_spawn_id + } +} + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/031-zsh.exp b/testsuite/completion.00-init/031-zsh.exp new file mode 100644 index 000000000..63f1ed05b --- /dev/null +++ b/testsuite/completion.00-init/031-zsh.exp @@ -0,0 +1,402 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/10 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: avail, load, unload, use, unuse, restore +# Sub-Command: +# +# Comment: %C{ +# Check zsh Tab completion: mirrors 021-bash.exp (see +# that file for the full rationale of each check), with +# every completion_bash_* call replaced by its +# completion_zsh_* counterpart -- and two kinds of +# deviation from bash's expectations, both driven by +# genuine differences in the two shells' completion +# systems (readline vs zle), not by test bugs: +# - zsh does not ring the bell when a Tab silently +# extends the typed word to a longer, still-ambiguous +# common prefix (only a Tab that cannot insert +# anything at all rings the bell), so every such case +# drops the leading "\a" bash expects +# - init/zsh-functions/_module.in has no 'ml' support +# at all (only '#compdef module'), so every section +# mirroring 'ml' behavior in 021-bash.exp is dropped; +# and its avail/load completion offers modulefile +# candidates and option flags as two mutually +# exclusive sets gated on whether the word being +# completed starts with '-', same as bash's own +# _module_avail() internally does for the modules +# half -- but unlike bash, which always mixes its +# @comp_*_opts@ flags into the candidate set +# regardless of prefix, so a bare trailing-space +# listing here only ever shows one set at a time +# }C% +# +############################################################################## + +# _module.in declares its mandatory-argument options zsh '_arguments' style +# ('--tag=[...]', '--color=[...]', ..., meaning "takes a value attached +# with an '='"), and zsh's own listing shows the bare option name it will +# insert rather than that spec syntax -- so it offers eg '--tag', not the +# module-help-documented '--tag=' the ground truth in $help_subcmd_opts / +# $help_switches otherwise matches verbatim +proc completion_zsh_expected_opts {opts} { + set out {} + foreach tok $opts { + if {[string index $tok end] eq {=}} { + set tok [string range $tok 0 end-1] + } + lappend out $tok + } + return $out +} + + +# +# module names and option flags each get listed on Tab completion, but +# (unlike bash) not mixed together in the same listing: which set shows up +# depends on whether the word being completed starts with '-' +# + +setenv_path_var MODULEPATH $modpath + +completion_zsh_start + +set got [completion_zsh_list {module avail }] +completion_assert_contains $got {advvers/ bar/ baz/ foo/} + +set got [completion_zsh_list {module avail -}] +completion_assert_contains $got {--all --icase --terse} + +set got [completion_zsh_list {module load }] +completion_assert_contains $got {advvers/ bar/ baz/ foo/} + +set got [completion_zsh_list {module load -}] +completion_assert_contains $got {--force -f --icase} + +## "ba" is the common prefix of bar/ and baz/: a single Tab from "b" must +## auto-insert the "a" inline -- with no bell, unlike bash, since zle only +## beeps when nothing at all can be inserted (see this file's header) +set got [completion_zsh_inline {module load b}] +completion_assert_eq $got {a} + +## typed prefix is "ba", the full common prefix of bar/ and baz/: zle +## auto-inserts any *further* common prefix on the first Tab, which would +## eat one of the two Tabs the double-Tab below relies on to list matches +## instead of just ringing the bell +set got [completion_zsh_list {module load ba}] +completion_assert_contains $got {bar/ baz/} +completion_assert_not_contains $got {foo/ advvers/} + +## "bar" uniquely matches the "bar/" module category: a single Tab must +## complete it inline with a trailing '/' and no trailing space (so typing +## can continue straight into a version), per the '_files -/'-alike nospace +## logic in _module_avail_mods +set got [completion_zsh_inline {module load bar}] +completion_assert_eq $got {/} + +## from "bar/", double-Tab must list the versions available under it +set got [completion_zsh_list {module load bar/}] +completion_assert_contains $got {bar/1.0 bar/2.0} + +## "flatlcompat" is a single, versionless module (no further completion +## possible beneath it, so unlike "bar" above it does not end in '/'): a +## single Tab must complete it inline, with a trailing space since it is +## not a directory-style entry +set got [completion_zsh_inline {module load fl}] +completion_assert_eq $got {atlcompat } + +completion_zsh_close + + +# +# 'unload' lists the currently loaded modules alongside the option flags +# (mutually exclusive by leading '-', same as avail/load above), and +# lists/completes them on the typed prefix like any other candidate list +# + +setenv_loaded_module [list bar/1.0 baz/1.0 foo/1.0]\ + [list $modpath/bar/1.0 $modpath/baz/1.0 $modpath/foo/1.0] + +completion_zsh_start + +set got [completion_zsh_list {module unload }] +completion_assert_contains $got {bar/1.0 baz/1.0 foo/1.0} + +set got [completion_zsh_list {module unload -}] +completion_assert_contains $got {--force -f --icase} + +## "ba" is the common prefix of bar/1.0 and baz/1.0: a single Tab from "b" +## must auto-insert the "a" inline, with no bell (see this file's header) +set got [completion_zsh_inline {module unload b}] +completion_assert_eq $got {a} + +## from "ba", nothing more can be auto-inserted (bar/1.0 and baz/1.0 +## diverge right there), so a double-Tab must list just the two matching +## loaded modules, not foo/1.0 +set got [completion_zsh_list {module unload ba}] +completion_assert_contains $got {bar/1.0 baz/1.0} +completion_assert_not_contains $got {foo/1.0} + +## "foo/1.0" is the only loaded module starting with "f": a single Tab must +## complete the rest of it inline, with a trailing space since a +## loaded-module candidate is not a directory-style entry +set got [completion_zsh_inline {module unload f}] +completion_assert_eq $got {oo/1.0 } + +completion_zsh_close +unsetenv_loaded_module + + +# +# 'use' completes a filesystem directory path, through _module.in's own +# '_files -/' action rather than a fallback to some outer default completer +# (there is no zsh equivalent of bash's 'complete -o default' registration +# -- the completion script itself must handle every sub-command, 'use' +# included) +# + +set usedir "$env(TESTSUITEDIR)/completion-use-fixture" +file delete -force $usedir +file mkdir $usedir + +completion_zsh_start + +## drop the last 3 characters of the fixture dir name ("ure"): a single Tab +## must complete them back plus a trailing '/', with no trailing space, +## exactly like the "bar" -> "bar/" case above +set got [completion_zsh_inline "module use [string range $usedir 0 end-3]"] +completion_assert_eq $got {ure/} + +completion_zsh_close +file delete -force $usedir + + +# +# 'unuse' lists the currently enabled modulepaths, and filters them on the +# typed prefix like any other candidate list +# + +setenv_path_var MODULEPATH $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp + +completion_zsh_start + +## all three modulepaths are absolute, so they all share a leading '/': +## type it explicitly, otherwise zle auto-inserts that one-character common +## prefix on the first Tab and the double-Tab below would only ring the +## bell instead of listing (same trap as the "bar"/"ba" cases above) +set got [completion_zsh_list {module unuse /}] +completion_assert_contains $got [list $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp] + +## $modpath and modulefiles.4 share the ".../modulefiles." prefix, diverging +## only on the trailing version digit: typing that full common prefix (so +## the first Tab has nothing left to auto-insert) and double-Tab must list +## only the two matching modulepaths, not the unrelated /tmp entry +set got [completion_zsh_list "module unuse $env(TESTSUITEDIR)/modulefiles."] +completion_assert_contains $got [list $modpath "$env(TESTSUITEDIR)/modulefiles.4"] +completion_assert_not_contains $got {/tmp} + +completion_zsh_close + + +# +# with only one modulepath left matching a typed prefix, a single Tab must +# complete the whole rest of it inline -- unlike the module-name case, a +# modulepath candidate is not a directory-style entry, so (still unlike +# "bar" -> "bar/" above) a trailing space is expected after it +# + +setenv_path_var MODULEPATH "$env(TESTSUITEDIR)/modulefiles.4" /tmp + +completion_zsh_start + +set got [completion_zsh_inline "module unuse $env(TESTSUITEDIR)/module"] +completion_assert_eq $got {files.4 } + +completion_zsh_close + + +# +# 'restore' lists the saved collections, and filters them on the typed +# prefix like any other candidate list +# + +setenv_path_var MODULEPATH $modpath + +completion_zsh_start + +completion_zsh_raw {module save foo} +completion_zsh_raw {module save fox} +completion_zsh_raw {module save bar1} + +set got [completion_zsh_list {module restore }] +completion_assert_contains $got {foo fox bar1} + +## "fo" is the common prefix of foo/fox: a single Tab from "f" must +## auto-insert the "o" inline, with no bell (see this file's header) +set got [completion_zsh_inline {module restore f}] +completion_assert_eq $got {o} + +## from "fo", nothing more can be auto-inserted (foo/fox diverge right +## there), so a double-Tab must list just the two matching collections +set got [completion_zsh_list {module restore fo}] +completion_assert_contains $got {foo fox} +completion_assert_not_contains $got {bar1} + +## "bar1" is the only collection starting with "ba": a single Tab must +## complete the rest of it inline, with a trailing space since a +## collection candidate is not a directory-style entry +set got [completion_zsh_inline {module restore ba}] +completion_assert_eq $got {r1 } + +completion_zsh_close + + +# +# ground-truth check against 'module help' (see completion.00-init/ +# 007-module_help.exp): a bare 'module ' must propose exactly the +# documented sub-commands, and 'module -' must propose exactly the +# documented switches +# + +setenv_path_var MODULEPATH $modpath + +completion_zsh_start + +set got [completion_zsh_list {module }] +set got_subcmds [lsearch -all -inline -not -regexp $got {^-}] +completion_assert_contains $got_subcmds $help_subcmds +completion_assert_only_known $got_subcmds {*} $help_subcmds + +set got [completion_zsh_list {module -}] +set zsh_help_switches [completion_zsh_expected_opts $help_switches] +completion_assert_contains $got $zsh_help_switches +completion_assert_only_known $got {-*} $zsh_help_switches + +completion_zsh_close + + +# +# ground-truth check, per sub-command: 'module -' +# must propose exactly the option flags 'module help' documents for that +# sub-command -- including none at all, for a sub-command that takes no +# option +# + +setenv_path_var MODULEPATH $modpath + +completion_zsh_start + +foreach subcmd $help_subcmds { + set expected {} + if {[info exists help_subcmd_opts($subcmd)]} { + set expected $help_subcmd_opts($subcmd) + } + set expected [completion_zsh_expected_opts $expected] + if {[llength $expected] == 0} { + ## nothing is expected to complete at all: a single Tab must insert + ## nothing but a bell -- unlike the ambiguous-but-something-to-insert + ## cases above, zle does still beep here, since it has genuinely + ## nothing to insert + set got [completion_zsh_inline "module $subcmd -"] + completion_assert_eq $got "\a" + } else { + ## 'config' is the only sub-command whose every option is long-form + ## only ('--dump-state'/'--reset', no '-x' short forms): from a + ## single '-' they still share one more '-' as a common prefix, which + ## the first Tab would silently auto-insert (the same auto-extension + ## trap as "bar"/"ba" earlier in this file), leaving only one Tab for + ## the double-Tab below -- type that second '-' already, like every + ## other sub-command's full common prefix here + set dash [expr {$subcmd eq {config} ? {--} : {-}}] + set got [completion_zsh_list "module $subcmd $dash"] + completion_assert_contains $got $expected + completion_assert_only_known $got {-*} $expected + } +} + +completion_zsh_close + + +# +# ground-truth check, per sub-command: 'module ' +# must propose a candidate of the kind 'module help' documents as that +# sub-command's positional argument, for the two kinds with fixture data +# predictable enough to check for by name -- an available/loaded module +# ('modulefile'/'modfile'/'mod1'/'mod2'/'mod' in the usage synopsis) or a +# saved collection ('collection'/'coll'). The remaining kinds (a filesystem +# directory, an arbitrary file, or nothing completable at all -- a search +# string, a shell name, a stash collection name generated with an +# unpredictable timestamp, ...) are not asserted on here: 'use' and 'unuse' +# already get a dedicated, fixture-appropriate check of their own earlier +# in this file, and the rest fall back to _module.in's own directory/file +# completion, which is not fixture data this testsuite controls +# + +setenv_path_var MODULEPATH $modpath +setenv_loaded_module [list bar/1.0] [list $modpath/bar/1.0] + +completion_zsh_start + +completion_zsh_raw {module save foo} +completion_zsh_raw {module save fox} +completion_zsh_raw {module save bar1} + +foreach subcmd $help_subcmds { + ## 'list'/'savelist' take a search string filtering already-known state + ## (loaded modules / saved collections), not a name completion target: + ## their own completion only ever proposes their option flags, despite + ## their usage synopsis documenting a module/collection-shaped argument + if {$subcmd in {list savelist}} { + continue + } + if {$subcmd eq {config}} { + ## 'config' takes a configuration option name, not a module or + ## collection: check it against the ground truth fetched from + ## 'module config' (see 008-module_config.exp), plus the two real + ## flags mixed into the same candidate list -- neither less nor more + set got [completion_zsh_list "module $subcmd "] + completion_assert_contains $got $config_opts + completion_assert_only_known $got {*} [concat $config_opts\ + $help_subcmd_opts(config)] + continue + } + if {![info exists help_subcmd_argtype($subcmd)]} { + continue + } + set argtype $help_subcmd_argtype($subcmd) + if {$argtype ni {module collection}} { + continue + } + set got [completion_zsh_list "module $subcmd "] + if {$argtype eq {module}} { + ## covers every module-completion mechanism this fixture set can + ## produce: available modules not yet loaded (directory-style, + ## trailing '/'), available modules regardless of load state + ## (whatis/display/... style), and the one loaded module (flat + ## 'name/version', switch/unload style) + completion_assert_any $got {bar/ baz/ foo/ advvers/ flatlcompat bar/1.0} + } else { + completion_assert_any $got {foo fox bar1} + } +} + +completion_zsh_close +unsetenv_loaded_module + + +# +# Cleanup +# + +reset_test_env + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: From 041beb1bf88d2124df06409b2264bc9c18737128 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Tue, 11 Aug 2026 10:19:33 +0000 Subject: [PATCH 24/37] init: remove non-functional -a/--all from is-avail usage and completions reportUsage and bash/tcsh/zsh completion all advertise -a/--all for is-avail, but the option parser in tcl/main.tcl.in only treats -a as --all on a specific list of sub-commands (avail, aliases, search, whatis, ml, list, lint, savelist, spider) that does not include is-avail; for is-avail it falls through and is treated as a positional module-spec instead, which errors out (documented as intentional behavior in testsuite/modules.20-locate/ 113-hide-all-opt.exp: passing -a before the module name errors, passing --all after it is a silent no-op). Drop [-a] from the is-avail reportUsage line, drop -a/--all from the is-avail completion option list in init/Makefile (shared by bash and tcsh), and drop the matching -a/--all _arguments entry from the is-avail case in init/zsh-functions/_module.in, so the usage message and completions match what is-avail actually accepts. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 +- init/zsh-functions/_module.in | 1 - tcl/report.tcl.in | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/init/Makefile b/init/Makefile index ed8d35936..6fe0c0a03 100644 --- a/init/Makefile +++ b/init/Makefile @@ -133,7 +133,7 @@ comp_whatis_opts := -a -i -j --all --icase --json comp_search_opts := -a -j --all --json comp_aliases_opts := -a --all comp_use_opts := -a --append -p --prepend -comp_isavail_opts := -a -i --all --icase +comp_isavail_opts := -i --icase comp_lint_opts := -a -i --all --icase comp_modtosh_opts := --auto --no-auto --force -f --icase -i comp_path_opts := -d --delim --duplicates diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index 89ef3bb68..a0fb97891 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -349,7 +349,6 @@ _module() { (is-avail) _arguments \ '(-i --icase)'{-i,--icase}'[Case insensitive match]' \ - '(-a --all)'{-a,--all}'[Include hidden modules in search]' \ "*::modulefile:{_module_avail_mods $cur}" && ret=0 ;; (whatis) diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 85f630fbf..b070432d2 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2049,8 +2049,7 @@ Listing / Searching commands: Scan all modulepaths and list all or matching available modules is-loaded [-i] [modulefile ...] Test if any of the modulefile(s) are loaded - is-avail [-a] [-i] modulefile [...] - Is any of the modulefile(s) available + is-avail [-i] modulefile [...] Is any of the modulefile(s) available info-loaded [-i] modulefile Get full name of matching loaded module(s) Collection of modules handling commands: From 10fd31ffadf010d605379083496c89ba471ad4ec Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Tue, 11 Aug 2026 11:58:22 +0000 Subject: [PATCH 25/37] init: add missing -f/--force to purge/reload/reset/restore/stash/stashpop module.rst documents -f/--force for purge, reload (and its update alias), reset, restore, stash and stashpop, and the option already works for all of them: -f/--force is parsed unconditionally in the top-level switch in tcl/main.tcl.in regardless of sub-command, unlike -a/--all which is only recognized for a specific list of sub-commands. None of the six ever showed [-f] in reportUsage though, and none of them completed --force/-f in bash, tcsh or zsh. Add [-f] to each of their reportUsage lines in tcl/report.tcl.in, mirroring the existing clear [-f] line. Add a completion option list per sub-command to init/Makefile (shared by bash and tcsh), and wire purge/reload+update/reset/stash into their own completion case arms in init/bash_completion.in (both the module and ml functions) and init/tcsh_completion.in instead of the shared no-completion group they used to share with sub-commands that really take no option. Split restore and stashpop out of the collection-name/stash-name completion groups they shared with save/saveshow/describe/saverm/disable/ is-saved and stashshow/stashrm respectively, so they gain --force/-f alongside their existing name completion without wrongly adding it to the other, option-less sub-commands in those groups. Mirror the same split in init/zsh-functions/_module.in, plus new case arms for purge, reload/update, reset and stash. zsh and fish completion already describe -f/--force as "By-pass dependency consistency" throughout init/zsh-functions/_module.in and init/fish_completion (established well before the codespell pre-commit hook was added), and the new case arms above add several more occurrences of that same, consistent phrasing. codespell flags it as a typo for "Bypass" on every newly touched line, so add it to the codespell ignore-words-list rather than diverge these new completion entries from the wording already used everywhere else for this option. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .codespellrc | 4 +++- init/Makefile | 12 ++++++++++++ init/bash_completion.in | 24 ++++++++++++++++++------ init/tcsh_completion.in | 14 +++++++------- init/zsh-functions/_module.in | 30 ++++++++++++++++++++++++++++-- tcl/report.tcl.in | 12 ++++++------ 6 files changed, 74 insertions(+), 22 deletions(-) diff --git a/.codespellrc b/.codespellrc index 76c4c7c9f..87aa2795c 100644 --- a/.codespellrc +++ b/.codespellrc @@ -4,7 +4,9 @@ # ignore variable names used in code: desig, matchin, compre, invers, compileTime, seh # ignore words added to spell check dictionary: lod, nd # ignore command names used in code: uncomplete -ignore-words-list = ans,ot,ue,fo,haa,varius,ure,tthe,tbe,nin,desig,matchin,compre,invers,compiletime,seh,lod,nd,uncomplete +# ignore hyphenated wording already used throughout Tab completion option +# descriptions: By-pass +ignore-words-list = ans,ot,ue,fo,haa,varius,ure,tthe,tbe,nin,desig,matchin,compre,invers,compiletime,seh,lod,nd,uncomplete,by-pass # skip git repository internal content # skip generated content: ChangeLog.gz, doc/build, modulecmd*.tcl, modules.sum # modules.log, lib/config.sub, lib/config.guess, lib/config.status, lib/configure, diff --git a/init/Makefile b/init/Makefile index 6fe0c0a03..a02066e5e 100644 --- a/init/Makefile +++ b/init/Makefile @@ -136,6 +136,12 @@ comp_use_opts := -a --append -p --prepend comp_isavail_opts := -i --icase comp_lint_opts := -a -i --all --icase comp_modtosh_opts := --auto --no-auto --force -f --icase -i +comp_purge_opts := --force -f +comp_reload_opts := --force -f +comp_reset_opts := --force -f +comp_restore_opts := --force -f +comp_stash_opts := --force -f +comp_stashpop_opts := --force -f comp_path_opts := -d --delim --duplicates comp_rm_path_opts := -d --delim --index comp_config_opts := --dump-state --reset abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact csh_limit domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc ignored_dirs implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug siteconfig source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_ext_lib tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277 @@ -168,6 +174,12 @@ sed -e 's|@prefix@|$(prefix)|g' \ -e 's|@comp_isavail_opts@|$(comp_isavail_opts)|g' \ -e 's|@comp_lint_opts@|$(comp_lint_opts)|g' \ -e 's|@comp_modtosh_opts@|$(comp_modtosh_opts)|g' \ + -e 's|@comp_purge_opts@|$(comp_purge_opts)|g' \ + -e 's|@comp_reload_opts@|$(comp_reload_opts)|g' \ + -e 's|@comp_reset_opts@|$(comp_reset_opts)|g' \ + -e 's|@comp_restore_opts@|$(comp_restore_opts)|g' \ + -e 's|@comp_stash_opts@|$(comp_stash_opts)|g' \ + -e 's|@comp_stashpop_opts@|$(comp_stashpop_opts)|g' \ -e 's|@comp_path_opts@|$(comp_path_opts)|g' \ -e 's|@comp_rm_path_opts@|$(comp_rm_path_opts)|g' \ -e 's|@comp_config_opts@|$(comp_config_opts)|g' \ diff --git a/init/bash_completion.in b/init/bash_completion.in index 736f20133..25e8b72dd 100644 --- a/init/bash_completion.in +++ b/init/bash_completion.in @@ -85,10 +85,12 @@ _module() { list) COMPREPLY=( $(compgen -W "@comp_list_opts@" -- "$cur") );; savelist) COMPREPLY=( $(compgen -W "@comp_savelist_opts@" -- "$cur") );; stashlist) COMPREPLY=( $(compgen -W "@comp_stashlist_opts@" -- "$cur") );; - stashpop|stashshow|stashrm) + stashpop) COMPREPLY=( $(compgen -W "@comp_stashpop_opts@ $(_module_stashlist)" -- "$cur") );; + stashshow|stashrm) COMPREPLY=( $(compgen -W "$(_module_stashlist)" -- "$cur") );; clear) COMPREPLY=( $(compgen -W "@comp_clear_opts@" -- "$cur") );; - restore|save|saveshow|describe|saverm|disable|is-saved) + restore) COMPREPLY=( $(compgen -W "@comp_restore_opts@ $(_module_savelist)" -- "$cur") );; + save|saveshow|describe|saverm|disable|is-saved) COMPREPLY=( $(compgen -W "$(_module_savelist)" -- "$cur") );; rm|delete|remove|unload) COMPREPLY=( $(compgen -W "@comp_unload_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; @@ -112,7 +114,11 @@ _module() { apropos|keyword|search) COMPREPLY=( $(compgen -W "@comp_search_opts@" -- "$cur") );; config|--reset) COMPREPLY=( $(compgen -W "@comp_config_opts@" -- "$cur") );; - -h|--help|-V|--version|--dumpname|purge|refresh|reload|sh-to-mod|source|state|reset|stash|stashclear|cacheclear|update) + purge) COMPREPLY=( $(compgen -W "@comp_purge_opts@" -- "$cur") );; + reload|update) COMPREPLY=( $(compgen -W "@comp_reload_opts@" -- "$cur") );; + reset) COMPREPLY=( $(compgen -W "@comp_reset_opts@" -- "$cur") );; + stash) COMPREPLY=( $(compgen -W "@comp_stash_opts@" -- "$cur") );; + -h|--help|-V|--version|--dumpname|refresh|sh-to-mod|source|state|stashclear|cacheclear) ;; append-path|prepend-path) COMPREPLY=( $(compgen -W "@comp_path_opts@" -- "$cur") );; @@ -151,10 +157,12 @@ if type -t ml >/dev/null; then list) COMPREPLY=( $(compgen -W "@comp_list_opts@" -- "$cur") );; savelist) COMPREPLY=( $(compgen -W "@comp_savelist_opts@" -- "$cur") );; stashlist) COMPREPLY=( $(compgen -W "@comp_stashlist_opts@" -- "$cur") );; - stashpop|stashshow|stashrm) + stashpop) COMPREPLY=( $(compgen -W "@comp_stashpop_opts@ $(_module_stashlist)" -- "$cur") );; + stashshow|stashrm) COMPREPLY=( $(compgen -W "$(_module_stashlist)" -- "$cur") );; clear) COMPREPLY=( $(compgen -W "@comp_clear_opts@" -- "$cur") );; - restore|save|saveshow|describe|saverm|disable|is-saved) + restore) COMPREPLY=( $(compgen -W "@comp_restore_opts@ $(_module_savelist)" -- "$cur") );; + save|saveshow|describe|saverm|disable|is-saved) COMPREPLY=( $(compgen -W "$(_module_savelist)" -- "$cur") );; rm|delete|remove|unload) COMPREPLY=( $(compgen -W "@comp_unload_opts@ ${LOADEDMODULES//:/ }" -- "$cur") );; @@ -178,7 +186,11 @@ if type -t ml >/dev/null; then apropos|keyword|search) COMPREPLY=( $(compgen -W "@comp_search_opts@" -- "$cur") );; config|--reset) COMPREPLY=( $(compgen -W "@comp_config_opts@" -- "$cur") );; - -h|--help|-V|--version|--dumpname|purge|refresh|reload|sh-to-mod|source|state|reset|stash|stashclear|cacheclear|update) + purge) COMPREPLY=( $(compgen -W "@comp_purge_opts@" -- "$cur") );; + reload|update) COMPREPLY=( $(compgen -W "@comp_reload_opts@" -- "$cur") );; + reset) COMPREPLY=( $(compgen -W "@comp_reset_opts@" -- "$cur") );; + stash) COMPREPLY=( $(compgen -W "@comp_stash_opts@" -- "$cur") );; + -h|--help|-V|--version|--dumpname|refresh|sh-to-mod|state|stashclear|cacheclear) ;; append-path|prepend-path) COMPREPLY=( $(compgen -W "@comp_path_opts@" -- "$cur") );; diff --git a/init/tcsh_completion.in b/init/tcsh_completion.in index c52a2271f..a87601bf6 100644 --- a/init/tcsh_completion.in +++ b/init/tcsh_completion.in @@ -57,7 +57,7 @@ complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ 'n/is-avail/`_module_avail; echo "@comp_isavail_opts@"`/' \ 'n/is-loaded/`_module_avail; echo "@comp_mfile_opts@"`/' \ 'n/info-loaded/`_module_avail; echo "@comp_mfile_opts@"`/' \ - 'n/restore/`_module_savelist`/' \ + 'n/restore/`_module_savelist; echo "@comp_restore_opts@"`/' \ "n/savelist/(@comp_savelist_opts@)/" \ 'n/saveshow/`_module_savelist`/' \ 'n/describe/`_module_savelist`/' \ @@ -70,10 +70,10 @@ complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ 'N/use/d/' \ 'n/unuse/`_module_modulepath`/' \ 'n/is-used/`_module_modulepath`/' \ - 'n/purge/n/' \ + "n/purge/(@comp_purge_opts@)/" \ 'n/refresh/n/' \ - 'n/reload/n/' \ - 'n/update/n/' \ + "n/reload/(@comp_reload_opts@)/" \ + "n/update/(@comp_reload_opts@)/" \ 'n/whatis/`_module_avail; echo "@comp_whatis_opts@"`/' \ "n/apropos/(@comp_search_opts@)/" \ "n/search/(@comp_search_opts@)/" \ @@ -91,11 +91,11 @@ complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ "n/state/n/" \ 'n/lint/`_module_avail; echo "@comp_lint_opts@"`/' \ 'n/mod-to-sh/`_module_not_yet_loaded; echo "@comp_modtosh_opts@"`/' \ - "n/reset/n/" \ - "n/stash/n/" \ + "n/reset/(@comp_reset_opts@)/" \ + "n/stash/(@comp_stash_opts@)/" \ "n/stashclear/n/" \ "n/stashlist/(@comp_stashlist_opts@)/" \ - 'n/stashpop/`_module_stashlist`/' \ + 'n/stashpop/`_module_stashlist; echo "@comp_stashpop_opts@"`/' \ 'n/stashrm/`_module_stashlist`/' \ 'n/stashshow/`_module_stashlist`/' \ 'n/cachebuild/d/' \ diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index a0fb97891..bcc5b68c3 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -297,7 +297,12 @@ _module() { '(-t --terse)'{-t,--terse}'[Display output in terse format]' \ '(-j --json)'{-j,--json}'[Display output in JSON format]' && ret=0 ;; - (stashpop|stashshow|stashrm) + (stashpop) + _arguments \ + '(-f --force)'{-f,--force}'[By-pass dependency consistency]' \ + '*::stash:{_module_stash_colls}' && ret=0 + ;; + (stashshow|stashrm) _alternative 'avail-stashs:collections:{_module_stash_colls}' \ && ret=0 ;; @@ -305,7 +310,12 @@ _module() { _arguments \ '(-f --force)'{-f,--force}'[Skip confirmation dialog]' && ret=0 ;; - (restore|save|saveshow|describe|saverm|disable|is-saved) + (restore) + _arguments \ + '(-f --force)'{-f,--force}'[By-pass dependency consistency]' \ + '*::collection:{_module_saved_colls}' && ret=0 + ;; + (save|saveshow|describe|saverm|disable|is-saved) _alternative 'avail-colls:collections:{_module_saved_colls}' \ && ret=0 ;; @@ -317,6 +327,22 @@ _module() { '(-i --icase)'{-i,--icase}'[Case insensitive match]' \ '*::modulefile:_module_loaded_mods' && ret=0 ;; + (purge) + _arguments \ + '(-f --force)'{-f,--force}'[By-pass dependency consistency]' && ret=0 + ;; + (reload|update) + _arguments \ + '(-f --force)'{-f,--force}'[By-pass dependency consistency]' && ret=0 + ;; + (reset) + _arguments \ + '(-f --force)'{-f,--force}'[By-pass dependency consistency]' && ret=0 + ;; + (stash) + _arguments \ + '(-f --force)'{-f,--force}'[By-pass dependency consistency]' && ret=0 + ;; (switch|swap) _arguments \ '--auto[Enable automated module handling mode]' \ diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index b070432d2..743c44719 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2026,12 +2026,12 @@ Loading / Unloading commands: Load first available modulefile in list rm | remove | delete | unload [-f] [-i] [--auto|--no-auto] modulefile [...] Remove modulefile(s) - purge Unload all loaded modulefiles - reload | update Unload then load all loaded modulefiles + purge [-f] Unload all loaded modulefiles + reload | update [-f] Unload then load all loaded modulefiles switch | swap [-f] [-i] [--auto|--no-auto] [--tag=LIST] [mod1] mod2 Unload mod1 and load mod2 refresh Refresh loaded module volatile components - reset Restore initial environment + reset [-f] Restore initial environment Listing / Searching commands: list [-a] [-t|-l|-j] [-S|-C] [-o LIST] [mod ...] @@ -2054,14 +2054,14 @@ Listing / Searching commands: Collection of modules handling commands: save [collection|file] Save current module list to collection - restore [collection|file] Restore module list from collection or file + restore [-f] [collection|file] Restore module list from collection or file saverm | disable [collection] Remove saved collection saveshow | describe [coll|file] Display information about collection savelist [-a] [-t|-l|-j] [-S|-C] [collection ...] List all or matching saved collections is-saved [collection ...] Test if any of the collection(s) exists - stash Save current environment and reset - stashpop [stash] Restore then remove stash collection + stash [-f] Save current environment and reset + stashpop [-f] [stash] Restore then remove stash collection stashrm [stash] Remove stash collection stashshow [stash] Display information about stash collection stashclear Remove all stash collections From 7e2d996333ba7d50904cf50b8eb1694025b94857 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Tue, 11 Aug 2026 11:59:08 +0000 Subject: [PATCH 26/37] init: add missing options and completion to source sub-command module.rst documents 'source [options] modulefile...' accepting --auto|--no-auto and -f|--force, both parsed unconditionally by tcl/main.tcl.in regardless of sub-command, but reportUsage showed no option at all for source, and it was bundled into the shared no-completion group in init/bash_completion.in and init/tcsh_completion.in alongside sub-commands that really take no option, so its options were never offered; zsh had no case arm for it whatsoever. Add [--auto|--no-auto] [-f] to the source reportUsage line in tcl/report.tcl.in, add a completion option list to init/Makefile (shared by bash and tcsh), and give source its own completion case arm in init/bash_completion.in (module and ml) and init/zsh-functions/_module.in. source takes a script file pathname, not a module name resolved against the modulepath, so unlike mod-to-sh its argument is left to plain filename completion rather than an avail-style candidate list: bash falls through to the shell's default file completion (the same 'case "$cur" in -*) ...; *) ;; esac' pattern already used for the directory argument of use), zsh calls the '_files' action, and tcsh uses its 'f' completion style (mirroring 'n/use/d/', which limits to directories instead since the argument of use is a directory). Fish already completes --auto/--no-auto/-f unconditionally after any sub-command, and already left source out of its avail-candidates function, so it already falls through to default file completion; no change needed there. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/Makefile | 2 ++ init/bash_completion.in | 10 +++++++++- init/tcsh_completion.in | 2 +- init/zsh-functions/_module.in | 7 +++++++ tcl/report.tcl.in | 3 ++- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/init/Makefile b/init/Makefile index a02066e5e..79e9f46d9 100644 --- a/init/Makefile +++ b/init/Makefile @@ -142,6 +142,7 @@ comp_reset_opts := --force -f comp_restore_opts := --force -f comp_stash_opts := --force -f comp_stashpop_opts := --force -f +comp_source_opts := --auto --no-auto --force -f comp_path_opts := -d --delim --duplicates comp_rm_path_opts := -d --delim --index comp_config_opts := --dump-state --reset abort_on_error advanced_version_spec auto_handling avail_indepth avail_output avail_terse_output cache_buffer_bytes cache_expiry_secs collection_pin_version collection_pin_tag collection_target color colors conflict_unload contact csh_limit domainname editor extended_default extra_siteconfig hide_auto_loaded home icase ignore_cache ignore_user_rc ignored_dirs implicit_default implicit_requirement init_envvars info_extension linked_envvars list_output list_terse_output locked_configs logged_events logger mcookie_check mcookie_version_check ml nearly_forbidden_days non_exportable_tags pager paginate path_entry_reorder protected_envvars quarantine_support rcfile redirect_output require_via reset_target_state run_quarantine search_match set_shell_startup shells_with_ksh_fpath silent_shell_debug siteconfig source_cache spider_indepth spider_output spider_terse_output sticky_purge tag_abbrev tag_color_name tcl_ext_lib tcl_linter term_background term_width unique_name_loaded unload_match_order variant_shortcut verbosity wa_277 @@ -180,6 +181,7 @@ sed -e 's|@prefix@|$(prefix)|g' \ -e 's|@comp_restore_opts@|$(comp_restore_opts)|g' \ -e 's|@comp_stash_opts@|$(comp_stash_opts)|g' \ -e 's|@comp_stashpop_opts@|$(comp_stashpop_opts)|g' \ + -e 's|@comp_source_opts@|$(comp_source_opts)|g' \ -e 's|@comp_path_opts@|$(comp_path_opts)|g' \ -e 's|@comp_rm_path_opts@|$(comp_rm_path_opts)|g' \ -e 's|@comp_config_opts@|$(comp_config_opts)|g' \ diff --git a/init/bash_completion.in b/init/bash_completion.in index 25e8b72dd..160aed0ad 100644 --- a/init/bash_completion.in +++ b/init/bash_completion.in @@ -118,7 +118,11 @@ _module() { reload|update) COMPREPLY=( $(compgen -W "@comp_reload_opts@" -- "$cur") );; reset) COMPREPLY=( $(compgen -W "@comp_reset_opts@" -- "$cur") );; stash) COMPREPLY=( $(compgen -W "@comp_stash_opts@" -- "$cur") );; - -h|--help|-V|--version|--dumpname|refresh|sh-to-mod|source|state|stashclear|cacheclear) + source) case "$cur" in + -*) COMPREPLY=( $(compgen -W "@comp_source_opts@" -- "$cur") );; + *) ;; # let readline handle the completion + esac;; + -h|--help|-V|--version|--dumpname|refresh|sh-to-mod|state|stashclear|cacheclear) ;; append-path|prepend-path) COMPREPLY=( $(compgen -W "@comp_path_opts@" -- "$cur") );; @@ -190,6 +194,10 @@ if type -t ml >/dev/null; then reload|update) COMPREPLY=( $(compgen -W "@comp_reload_opts@" -- "$cur") );; reset) COMPREPLY=( $(compgen -W "@comp_reset_opts@" -- "$cur") );; stash) COMPREPLY=( $(compgen -W "@comp_stash_opts@" -- "$cur") );; + source) case "$cur" in + -*) COMPREPLY=( $(compgen -W "@comp_source_opts@" -- "$cur") );; + *) ;; # let readline handle the completion + esac;; -h|--help|-V|--version|--dumpname|refresh|sh-to-mod|state|stashclear|cacheclear) ;; append-path|prepend-path) diff --git a/init/tcsh_completion.in b/init/tcsh_completion.in index a87601bf6..b9532c647 100644 --- a/init/tcsh_completion.in +++ b/init/tcsh_completion.in @@ -82,7 +82,7 @@ complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ 'n/path/`_module_avail; echo "@comp_mfile_opts@"`/' \ 'n/edit/`_module_avail`/' \ 'n/sh-to-mod/n/' \ - 'n/source/n/' \ + 'n/source/f/' \ "n/append-path/(@comp_path_opts@)/" \ "n/prepend-path/(@comp_path_opts@)/" \ "n/remove-path/(@comp_rm_path_opts@)/" \ diff --git a/init/zsh-functions/_module.in b/init/zsh-functions/_module.in index bcc5b68c3..5953e84fd 100644 --- a/init/zsh-functions/_module.in +++ b/init/zsh-functions/_module.in @@ -427,6 +427,13 @@ _module() { '(-i --icase)'{-i,--icase}'[Case insensitive match]' \ "*::modulefile:{_module_notloaded_mods $cur}" && ret=0 ;; + (source) + _arguments \ + '--auto[Enable automated module handling mode]' \ + '--no-auto[Disable automated module handling mode]' \ + '(-f --force)'{-f,--force}'[By-pass dependency consistency]' \ + '*:scriptfile:_files' && ret=0 + ;; esac ;; esac diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 743c44719..5658c8680 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2090,7 +2090,8 @@ Other commands: path [-i] modulefile Print modulefile path paths [-i] modulefile Print path of matching available modules clear [-f] Reset Modules-specific runtime information - source scriptfile [...] Execute scriptfile(s) + source [--auto|--no-auto] [-f] scriptfile [...] + Execute scriptfile(s) config [--dump-state|name [val]|--reset name] Display or set Modules configuration state [name] Display Modules state From 021111a54bf7b3f678813c6a62858c6dd440b8d6 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Tue, 11 Aug 2026 12:09:52 +0000 Subject: [PATCH 27/37] doc: document zsh coverage in the completion testsuite The completion testsuite gained a zsh-driving counterpart to its original bash-only setup (030-zsh-procs.exp/031-zsh.exp, mirroring 020-bash-procs.exp/021-bash.exp), and 005-init_ts.exp/script/mt were updated accordingly at the time, but the testsuite documentation and NEWS.rst entry describing this testsuite still only mentioned bash. Update doc/source/devel/testsuite.rst to note both shells are covered, describe the zsh binary/built completion script set up alongside the bash one, and add 00/030 to the completion tool's mandatory setup file list documented for script/mt. Update the NEWS.rst entry introducing the completion testsuite to mention it drives a bash or zsh session. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- NEWS.rst | 2 +- doc/source/devel/testsuite.rst | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 53dc6e6ee..6b715c112 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -192,7 +192,7 @@ Modules 5.7.0 (not yet released) * Doc: add :ref:`sync-remote-appdir` cookbook recipe. * Testsuite: add *completion* testsuite to validate shell Tab completion of this project. This testsuite can be invoked with ``make testcompletion`` or - ``script/mt comp``. It drives a real bash session through + ``script/mt comp``. It drives a real bash or zsh session through `Expect `_ to press the Tab key against the built shell completion script and check the resulting candidate list. * Init: fix numerous inconsistencies between ``module help``'s usage message diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 33762ab29..d9d6f620f 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -48,7 +48,7 @@ DejaGnu tools: tools) to press Tab against the built shell completion script and check that the resulting candidate list holds the expected module names and option flags. Driven by the :file:`completion.00-init` directory; - currently covers bash only, see `completion.00-init layout`_. + currently covers bash and zsh, see `completion.00-init layout`_. Each tool corresponds to one Makefile target (``test``, ``testinstall``, ``testlint``, ``testcompletion``, see `Running the testsuite`_) and to one @@ -163,12 +163,12 @@ plus one extra layer, since driving real Tab-key completion is inherently shell-specific in a way none of the other three tools are: - ``005-init_ts.exp`` / ``006-procs.exp`` / ``010-environ.exp`` / - ``011-save_test_env.exp`` set up paths (the ``bash`` binary, the built - :file:`init/bash_completion` script, a clean fixture modulepath), the - shell-agnostic assert procedures (``completion_assert_contains``, - ``completion_assert_not_contains``, ``completion_assert_eq``), and the - clean baseline environment/``save_test_env`` checkpoint, exactly as for - the other tools. + ``011-save_test_env.exp`` set up paths (the ``bash`` and ``zsh`` binaries, + the built :file:`init/bash_completion` and :file:`init/zsh-functions/_module` + scripts, a clean fixture modulepath), the shell-agnostic assert procedures + (``completion_assert_contains``, ``completion_assert_not_contains``, + ``completion_assert_eq``), and the clean baseline environment/ + ``save_test_env`` checkpoint, exactly as for the other tools. - ``0NN--procs.exp`` defines one ``completion__start`` / ``completion__raw`` / ``completion__list`` / ``completion__inline`` / ``completion__close`` set per @@ -182,9 +182,9 @@ shell-specific in a way none of the other three tools are: cmdline they were passed into the shared ``completion_last_cmdline`` variable, which the generic assert procedures use to build their test label. -- ``0NN-.exp`` (e.g. ``021-bash.exp``) holds the actual test cases - for that shell, calling only its own ``completion__*`` procs plus - the shared asserts. +- ``0NN-.exp`` (e.g. ``021-bash.exp``, ``031-zsh.exp``) holds the + actual test cases for that shell, calling only its own + ``completion__*`` procs plus the shared asserts. Adding a new shell means adding its own ``completion__*`` procs file and test file; nothing in ``006-procs.exp`` needs to change. @@ -637,8 +637,9 @@ Whichever files are selected, :file:`script/mt` always also runs the mandatory setup files for that tool (for ``modules``: ``00/005 00/006 00/010 00/050 00/060 00/080 00/085``; for ``install``: ``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``; for -``completion``: ``00/005 00/006 00/007 00/008 00/010 00/011 00/020``), plus -the ``999-cleanup.exp`` of every selected series. Passing a bare series +``completion``: ``00/005 00/006 00/007 00/008 00/010 00/011 00/020 +00/030``), plus the ``999-cleanup.exp`` of every selected series. Passing a +bare series number always expands to every file in that directory, because several of those series are order-sensitive or enumerate a whole modulepath (see `Adding new test fixtures`_). The collection series (``61``) is one such From c9c7788b04422f0d454ea6a369302553c9616a52 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Tue, 11 Aug 2026 19:39:13 +0000 Subject: [PATCH 28/37] ts: check fish completion, mirroring the bash/zsh completion testsuite Add completion.00-init/040-fish-procs.exp (a completion_fish_{start,raw,list,inline,close} set, mirroring 020-bash-procs.exp/030-zsh-procs.exp) and 041-fish.exp (mirroring 021-bash.exp/031-zsh.exp section-for-section), driving a real fish pty through init/fish_completion the same way the bash/zsh tests drive their own completion scripts. The fish line editor differs from both readline and zle in ways that shape almost every proc in 040-fish-procs.exp: on startup, a modern fish queries the terminal for its capabilities and blocks waiting for a reply -- up to a real 10 seconds for the primary device attributes query alone -- so every proc that reads from the fish session answers each such query the instant it appears, exactly as a real terminal would (fish re-issues at least the cursor-position query on every prompt redraw, not just once at startup). A single Tab is the fish equivalent of the double-Tab bash and zsh both need: it extends the typed word to the longest common prefix and shows the full candidate pager in the same keypress, collapsing it back via one cursor-up escape per pager row. fish echoes a command line back piecemeal as it is typed, so recovering the final, settled state of a line means taking the *last* place the typed text appears in the raw output, not the first; the same applies to the listing the pager itself produces, which needs enough idle time to fully stream out before it can be captured without truncating a wide or tall one. Candidate names shown with a description get rendered as "name (description text)", requiring that parenthesized part to be stripped before splitting into words, handling the one level of nested parentheses a description can legitimately carry (e.g. "modulefile(s)"). Real behavioral differences from bash and zsh are documented in the header of 041-fish.exp and reflected in its assertions rather than worked around: init/fish_completion has no 'ml' support at all, does not gate its option flags by sub-command (so every sub-command's '-' proposes the exact same full set), has no '-h'/'--help' completion, and 'unuse' is not covered by any of its own predicates (falling through to plain filename completion, unlike 'use'). completion_fish_start also neutralizes any *other* 'module' completion fish can reach on the test machine once 'module' resolves to a real command via autoinit: a system package (e.g. environment-modules on Fedora, which installs its own, older completions/module.fish into fish's vendor completions directory, a location this testsuite does not control) can be loaded automatically by fish the first time completion is attempted for 'module', clobbering the manual registrations from 'source' with whatever that other version defines instead. That automatic load only ever happens once per session, so it is deliberately triggered and absorbed with a throwaway completion attempt, its result erased, and the script under test re-sourced fresh. 005-init_ts.exp gains the fish binary/built-completion-script checks already present there for bash and zsh, and the comp setup file list in script/mt picks up 00/040 alongside the existing bash/zsh ones. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- script/mt | 2 +- testsuite/completion.00-init/005-init_ts.exp | 12 + .../completion.00-init/040-fish-procs.exp | 331 +++++++++++++++ testsuite/completion.00-init/041-fish.exp | 380 ++++++++++++++++++ 4 files changed, 724 insertions(+), 1 deletion(-) create mode 100644 testsuite/completion.00-init/040-fish-procs.exp create mode 100644 testsuite/completion.00-init/041-fish.exp diff --git a/script/mt b/script/mt index 47b27ff83..34dcc69ad 100755 --- a/script/mt +++ b/script/mt @@ -107,7 +107,7 @@ if [ $# -gt 0 ]; then elif [ "$1" = "comp" ]; then target='testcompletion' testserie='completion' - setuptestfiles=(00/005 00/006 00/007 00/008 00/010 00/011 00/020 00/030) + setuptestfiles=(00/005 00/006 00/007 00/008 00/010 00/011 00/020 00/030 00/040) shift fi fi diff --git a/testsuite/completion.00-init/005-init_ts.exp b/testsuite/completion.00-init/005-init_ts.exp index 3d14dfdc6..9388ecd5d 100644 --- a/testsuite/completion.00-init/005-init_ts.exp +++ b/testsuite/completion.00-init/005-init_ts.exp @@ -65,6 +65,18 @@ if {![file exists $zshcompletion]} { unsupported "completion script not found: $zshcompletion (run 'make initdir' first)" } +# a fish binary is required to drive interactive completion tests +set fishbin [find_bin fish] +if {$fishbin eq {}} { + unsupported {no fish binary found to drive completion tests} +} + +# built fish completion script under test +set fishcompletion "$env(OBJDIR)/init/fish_completion" +if {![file exists $fishcompletion]} { + unsupported "completion script not found: $fishcompletion (run 'make initdir' first)" +} + # clean modulefile fixture tree: unlike testsuite/modulefiles, this one holds # no modulefile deliberately broken to exercise error paths, which would get # in the way of straightforward completion listing checks diff --git a/testsuite/completion.00-init/040-fish-procs.exp b/testsuite/completion.00-init/040-fish-procs.exp new file mode 100644 index 000000000..e3f29c7b3 --- /dev/null +++ b/testsuite/completion.00-init/040-fish-procs.exp @@ -0,0 +1,331 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/11 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Test procedures +# Command: +# Sub-Command: +# +# Comment: %C{ +# Defines the test procedures driving a real fish shell in +# a pty to exercise the module Tab completion script +# exactly as a user pressing Tab would, then collect the +# resulting candidate word list. This mirrors +# 020-bash-procs.exp's completion_bash_{start,raw,list, +# close} set (see that file's header for why each shell +# gets its own), but fish's line editor differs from both +# readline and zle in ways that shape almost every proc +# below: +# - on startup, a modern fish queries the terminal for +# its capabilities (primary device attributes, +# background color, cursor position, kitty keyboard +# protocol support, ...) and blocks waiting for a +# reply -- for the primary device attributes query, +# for up to 10 real seconds before giving up. A pty +# driven by Expect never answers these, so every proc +# that reads from the fish session folds in a +# 'completion_fish_query_responder' branch that +# answers each such query the instant it appears (a +# real terminal emulator would do the same), and fish +# re-issues at least the cursor-position query on +# every single prompt redraw, not just once at startup +# - a single Tab is fish's equivalent of bash/zsh's +# double-Tab: it both extends the typed word to the +# longest common prefix (if any) *and* shows the full +# candidate pager below the cmdline in the same +# keypress, collapsing it back via one '\x1b[A' +# (cursor-up) escape sequence per pager row once done +# -- unlike zsh's own double-Tab, it does not also +# auto-select/insert the first candidate when there is +# no further common prefix to extend into; the typed +# word is left exactly as-is, same as bash +# - fish echoes a command line back keystroke-by-keystroke +# (here: word-by-word, since this file sends a whole +# cmdline in one 'send') as it is typed, each echo +# wrapped in its own cursor-positioning escape +# sequences, rather than readline/zle's single redraw +# once done; the *only* reliable way to recover the +# final, settled state of the line is to take the +# *last* place in the raw output where the typed +# cmdline text appears, not the first -- earlier +# occurrences are just intermediate echo frames +# - candidate names fish shows with a '--description' +# (eg every sub-command name, every option flag) get +# rendered by the pager as "name (description text)", +# several such pairs packed per row -- the +# parenthesized part must be stripped before the +# listing can be split into plain candidate words, and +# since a description can itself legitimately contain +# one level of parentheses (eg "(Load modulefile(s))"), +# a plain 'no parenthesis inside' pattern is not +# enough +# }C% +# +############################################################################## + +# terminal-capability queries fish issues that this pty never gets asked by +# a real terminal to answer on its own -- reply to each the instant it +# appears, exactly as a real terminal emulator would, so fish never blocks +# waiting on one. Folded into every 'expect' below that reads from the fish +# session via 'exp_continue', since fish re-issues at least the cursor +# position query on every single prompt redraw, not just once at startup +proc completion_fish_query_responder {} { + return { + -re {\x1b\[6n} { send -- "\x1b\[1;1R"; exp_continue } + -re {\x1b\[0c} { send -- "\x1b\[?1;2c"; exp_continue } + -re {\x1b\]11;\?} { send -- "\x1b]11;rgb:0000/0000/0000\x1b\\"; exp_continue } + -re {\x1b\[\?u} { send -- "\x1b\[?0u"; exp_continue } + } +} + +# marker fish emits once a prompt has finished (re)drawing and is ready to +# accept input -- the OSC 133 "prompt end" marker, followed by a clear-to- +# end-of-line and a cursor-forward repositioning; appears after every fresh +# prompt, whether the still-default one (early in completion_fish_start, +# before the custom prompt below is installed) or the custom one, so it is +# used throughout as the one 'ready' signal to wait on +set completion_fish_promptready {\x1b\]133;B(\x07|\x1b\\)\x1b\[K\r\x1b\[[0-9]*C} + +# wait for the fish session to reach a freshly drawn, ready prompt, +# answering any terminal-capability query fish asks along the way +proc completion_fish_wait_ready {{tout 10}} { + global completion_fish_spawn_id completion_fish_promptready + + set spawn_id $completion_fish_spawn_id + set old_timeout $::timeout + set ::timeout $tout + set ok 1 + expect {*}[completion_fish_query_responder] -re $completion_fish_promptready {} \ + timeout { set ok 0 } + set ::timeout $old_timeout + return $ok +} + +# start a fresh fish session with the completion script under test sourced; +# 'TERM' is set to a capable value (rather than 'dumb', which silences the +# terminal-capability queries but also disables the candidate pager this +# file's checks rely on) and every such query is answered as soon as it is +# asked (see completion_fish_query_responder), so startup never hits fish's +# up-to-10-second wait for a reply that would otherwise never come +proc completion_fish_start {} { + global fishbin fishcompletion completion_prompt completion_fish_spawn_id + global TCLSH MODULECMD verbose + + if {$verbose > 1} { + send_user "\tStarting fish for completion test\n" + } + + spawn env TERM=xterm-256color $fishbin --no-config + set completion_fish_spawn_id $spawn_id + + # a narrow (default 80-column) pty can make a long completed line wrap, + # which makes fish redraw it with extra escape sequences that can be + # mistaken for stray inserted characters (e.g. a trailing space) by + # anything matching on raw output; go wide enough that no cmdline used + # in these tests is ever at risk of wrapping + stty rows 40 columns 500 < $spawn_out(slave,name) + + if {![completion_fish_wait_ready 10]} { + unresolved {completion fish did not start} + return + } + + # every setup command after this point goes through completion_fish_raw + completion_fish_raw {set -g fish_greeting ''} + completion_fish_raw "function fish_prompt; echo -n '$completion_prompt'; end" + # a real 'module' command is needed since the completion script shells + # out to it (module avail/spider) to list candidate module names + completion_fish_raw "source '$fishcompletion'" + completion_fish_raw "'$TCLSH' '$MODULECMD' fish autoinit | source -" + + # neutralize any *other* 'module' completion fish can reach on this + # machine (eg from a system package -- 'environment-modules' on Fedora + # installs its own, older completions/module.fish into fish's vendor + # completions directory, a fixed location outside this testsuite's + # control that plain sandboxing -- '--no-config', a scratch $HOME -- + # cannot hide from fish). Once 'module' resolves to a real command (the + # autoinit call just above defines it as a fish function), fish + # autoloads a same-named completions/module.fish -- if one exists + # anywhere on its search path -- the first time completion is attempted + # for it, regardless of the manual registrations from the 'source' + # above, potentially clobbering them with whatever that other version + # defines instead (observed: an older release's renamed sub-command, + # 'del' for today's 'delete', showing up alongside/instead of the + # built one under test). This autoload only ever happens once per + # session, though, so it is deliberately triggered and absorbed right + # here with a throwaway completion attempt, its result erased, and the + # script under test re-sourced fresh -- every real Tab-completion + # attempt later in this session then only ever sees that + completion_fish_raw {complete -C'module ' > /dev/null} + completion_fish_raw {complete -e -c module} + completion_fish_raw "source '$fishcompletion'" +} + +# send a line to the completion fish session and wait for the next ready +# prompt; used for setup commands whose output is not asserted on +proc completion_fish_raw {line} { + global completion_fish_spawn_id verbose + + set spawn_id $completion_fish_spawn_id + send -- "$line\r" + if {![completion_fish_wait_ready 8]} { + unresolved "completion setup command timed out: $line" + } + if {$verbose > 1} { + send_user "\tcompletion> $line\n" + } +} + +# read back everything the fish session produces until it falls quiet for +# 'idle_ms' (answering any terminal-capability query along the way), and +# return it all concatenated. A single fixed 'after N; read once' does not +# scale across every listing this file captures: a small candidate list +# renders near-instantly, while a wide, tall one (eg every sub-command name +# plus its description) takes long enough to stream out that a delay sized +# for the former still catches the latter mid-render, silently truncating +# it -- so this instead loops reading whatever has arrived, each time +# resetting the wait, until a whole 'idle_ms' window passes with nothing +# new; capped by an overall iteration count as a safety net against an +# unrecognized, endlessly-repeating query this proc does not know how to +# answer ever leaving it live-locked +proc completion_fish_read_quiet {{idle_ms 1000}} { + global completion_fish_spawn_id + + set spawn_id $completion_fish_spawn_id + set old_timeout $::timeout + set ::timeout [expr {$idle_ms / 1000.0}] + set buf {} + set iterations 0 + while {[incr iterations] <= 200} { + expect {*}[completion_fish_query_responder] \ + -re {(?s).+} { append buf $expect_out(buffer) } \ + timeout { break } + } + set ::timeout $old_timeout + return $buf +} + +# send a partial command line followed by a single Tab (fish's own +# equivalent of bash/zsh's double-Tab, see this file's header), let any +# subprocess-backed candidate generation (eg 'module avail') finish +# rendering, read back everything fish produced in response, and finally +# clear the line so the session is ready to reuse +proc completion_fish_capture {cmdline} { + global completion_fish_spawn_id completion_last_cmdline verbose + + set completion_last_cmdline $cmdline + set spawn_id $completion_fish_spawn_id + + send -- $cmdline + send -- "\t" + set buf [completion_fish_read_quiet] + if {$verbose > 1} { + send_user "\tcompletion raw output for '$cmdline': [string map {\x1b ESC} $buf]\n" + } + + # clear the (uncompleted or already-completed) input line so the + # session is ready to reuse + send -- "\x15\r" + completion_fish_wait_ready 5 + + return $buf +} + +# strip every '(...)' description fish's pager prints next to a candidate +# declared with '--description' (eg sub-command names, option flags); a +# description can itself carry one level of nested parentheses (eg +# "(Load modulefile(s))"), so a plain 'no paren inside' pattern is not +# enough -- this handles exactly one level, which covers every description +# in the completion script under test +proc completion_fish_strip_descriptions {text} { + return [regsub -all {\((?:[^()]|\([^()]*\))*\)} $text {}] +} + +# locate, in a completion_fish_capture buffer, the *last* place the typed +# cmdline text appears followed by plain (non-escape, non-CR) characters -- +# fish echoes the cmdline back piecemeal as it is typed (see this file's +# header), so only the last such occurrence reflects the final, settled +# state of the line; a bare '\r' is excluded from that run too, since fish +# emits one right after the settled text to return the cursor to column 0 +# before repositioning it with a following cursor-forward escape -- left +# in, it would be mistaken for part of the inserted suffix itself. Returns +# the run of characters right after the last occurrence (empty if the +# cmdline never appears followed by anything, ie nothing was inserted) +proc completion_fish_extract_suffix {buf cmdline} { + set recmdline [completion_re_escape $cmdline] + set matches [regexp -all -inline -- "${recmdline}(\[^\x1b\r\]*)" $buf] + if {[llength $matches] == 0} { + return {} + } + return [lindex $matches end] +} + +# send a partial command line followed by a single Tab, and return the +# sorted, de-duplicated list of candidate words fish's pager displayed. A +# listing is preceded by a real newline (fish moves to a fresh line to +# print the pager below the cmdline) and collapsed back via one '\x1b[A' +# (cursor-up) escape per pager row right after; a cmdline whose candidate +# set has only one member never shows this -- Tab completes it inline +# instead, exactly like completion_fish_inline -- so that case is detected +# by the absence of this marker and handled by reconstructing the single +# candidate from the inline insertion instead, the same way +# completion_zsh_list's own single-candidate fallback does +proc completion_fish_list {cmdline} { + set buf [completion_fish_capture $cmdline] + + if {[regexp {(?s)\n(.*?)(?:\x1b\[A)+} $buf -> listing]} { + set listing [completion_fish_strip_descriptions $listing] + set words {} + foreach word [split $listing] { + if {$word ne {}} { + lappend words $word + } + } + return [lsort -unique $words] + } + + set suffix [string trim [completion_fish_extract_suffix $buf $cmdline]] + if {$suffix eq {}} { + return {} + } + return [lsort -unique [split $suffix]] +} + +# send a partial command line followed by a single Tab, and return the text +# fish inserted inline -- the empty string if nothing at all could be +# completed (fish gives no equivalent of readline/zle's audible bell over a +# plain pty for this case, so unlike completion_bash_inline/ +# completion_zsh_inline there is no "\a" to check for here). Used to check +# unique, unambiguous completions -- eg that a directory-style entry gets +# completed with a trailing '/' and no trailing space. cmdline must be a +# prefix with exactly one match, or with a common prefix short enough that +# no further Tab is needed to reach it, otherwise the single Tab may extend +# by less than a full match (leaving the ambiguous set to still be shown by +# the pager, harmless here since only the text right after the cmdline is +# read back, not whether a listing also appeared) +proc completion_fish_inline {cmdline} { + set buf [completion_fish_capture $cmdline] + return [completion_fish_extract_suffix $buf $cmdline] +} + +proc completion_fish_close {} { + global completion_fish_spawn_id + + if {[info exists completion_fish_spawn_id]} { + set spawn_id $completion_fish_spawn_id + catch {send -- "exit\r"} + catch {expect eof} + catch {close -i $completion_fish_spawn_id} + catch {wait -i $completion_fish_spawn_id} + unset completion_fish_spawn_id + } +} + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/041-fish.exp b/testsuite/completion.00-init/041-fish.exp new file mode 100644 index 000000000..a131f4fc8 --- /dev/null +++ b/testsuite/completion.00-init/041-fish.exp @@ -0,0 +1,380 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/11 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: avail, load, unload, use, unuse, restore +# Sub-Command: +# +# Comment: %C{ +# Check fish Tab completion: mirrors 021-bash.exp / +# 031-zsh.exp (see 021-bash.exp for the full rationale of +# each check) as closely as fish's own completion script +# allows, with every completion_bash_*/completion_zsh_* +# call replaced by its completion_fish_* counterpart, and +# the following deviations, all driven by genuine +# differences between init/fish_completion and its bash/ +# zsh counterparts (see 040-fish-procs.exp's header for +# the differences in fish's own Tab-key behavior instead): +# - init/fish_completion has no 'ml' support at all (no +# 'complete -c ml' of its own), so every section +# mirroring 'ml' behavior in 021-bash.exp is dropped +# - init/fish_completion does not gate its option flags +# by sub-command at all: every 'complete -c module +# -s/-l ...' line applies unconditionally, so +# 'module -' always proposes the +# exact same full set of flags -- both the global ones +# and every other sub-command's own documented extras +# (eg 'avail's '--all'/'--terse' show up right alongside +# 'load's '--force') -- rather than just the subset +# 'module help' documents for that particular +# sub-command; so unlike 021-bash.exp/031-zsh.exp, this +# file has no per-sub-command option ground-truth +# section (only 'config' still gets one, its own option +# *names* rather than flags being unaffected by this). +# The one ground-truth check this file keeps for flags +# at all -- a bare 'module -' -- is checked against +# the union of every sub-command's documented options +# ($help_switches plus every $help_subcmd_opts(*) entry) +# rather than just $help_switches (the global +# 'Options:' section alone), to match this lack of +# gating; the 'module avail'/'module load'/ +# 'module unload' checks earlier only look for a small +# illustrative subset of flags, same as +# 021-bash.exp/031-zsh.exp, so they are unaffected +# - init/fish_completion also has no '-h'/'--help' +# completion at all (no 'complete -c module -s h -l +# help' line, unlike every other documented global +# switch) -- filtered out of the fetched ground truth +# before comparing against what fish actually proposes +# - 'unuse' is not covered by any of the fish completion +# script's own '__fish_module_use_*' predicates (unlike +# 'use', which is): Tab-completion here falls through +# entirely to fish's own default filename completion, +# so it never proposes the enabled modulepaths +# themselves the way 021-bash.exp/031-zsh.exp expect +# - like zsh's own _module.in, module names and option +# flags are two mutually exclusive candidate sets, only +# one of which is shown at a time depending on whether +# the word being completed starts with '-' -- so a bare +# trailing-space listing only ever shows one set, same +# as 031-zsh.exp +# }C% +# +############################################################################## + + +# +# module names and option flags each get listed on Tab completion, but +# (like zsh) not mixed together in the same listing: which set shows up +# depends on whether the word being completed starts with '-' +# + +setenv_path_var MODULEPATH $modpath + +completion_fish_start + +set got [completion_fish_list {module avail }] +completion_assert_contains $got {advvers/ bar/ baz/ foo/} + +set got [completion_fish_list {module avail -}] +completion_assert_contains $got {--all --icase --terse} + +set got [completion_fish_list {module load }] +completion_assert_contains $got {advvers/ bar/ baz/ foo/} + +set got [completion_fish_list {module load -}] +completion_assert_contains $got {--force -f --icase} + +## "ba" is the common prefix of bar/ and baz/: a single Tab from "b" must +## auto-insert the "a" inline -- fish, like zsh, does not additionally ring +## a bell just because the completion remains ambiguous beyond it (see +## 040-fish-procs.exp's header: fish gives no audible/visible feedback for +## this over a plain pty at all) +set got [completion_fish_inline {module load b}] +completion_assert_eq $got {a} + +## typed prefix is "ba", the full common prefix of bar/ and baz/: fish +## auto-inserts any *further* common prefix on the Tab, which would eat the +## one Tab the list check below relies on to show a real listing instead of +## silently completing further +set got [completion_fish_list {module load ba}] +completion_assert_contains $got {bar/ baz/} +completion_assert_not_contains $got {foo/ advvers/} + +## "bar" uniquely matches the "bar/" module category: a single Tab must +## complete it inline with a trailing '/' and no trailing space (so typing +## can continue straight into a version), per the directory-style handling +## in _module_avail_mods +set got [completion_fish_inline {module load bar}] +completion_assert_eq $got {/} + +## from "bar/", a Tab must list the versions available under it +set got [completion_fish_list {module load bar/}] +completion_assert_contains $got {bar/1.0 bar/2.0} + +## "flatlcompat" is a single, versionless module (no further completion +## possible beneath it, so unlike "bar" above it does not end in '/'): a +## single Tab must complete it inline, with a trailing space since it is +## not a directory-style entry +set got [completion_fish_inline {module load fl}] +completion_assert_eq $got {atlcompat } + +completion_fish_close + + +# +# 'unload' lists the currently loaded modules (mutually exclusive with the +# option flags, same as avail/load above), and lists/completes them on the +# typed prefix like any other candidate list +# + +setenv_loaded_module [list bar/1.0 baz/1.0 foo/1.0]\ + [list $modpath/bar/1.0 $modpath/baz/1.0 $modpath/foo/1.0] + +completion_fish_start + +set got [completion_fish_list {module unload }] +completion_assert_contains $got {bar/1.0 baz/1.0 foo/1.0} + +set got [completion_fish_list {module unload -}] +completion_assert_contains $got {--force -f --icase} + +## "ba" is the common prefix of bar/1.0 and baz/1.0: a single Tab from "b" +## must auto-insert the "a" inline, with no audible/visible feedback (see +## 040-fish-procs.exp's header) +set got [completion_fish_inline {module unload b}] +completion_assert_eq $got {a} + +## from "ba", nothing more can be auto-inserted (bar/1.0 and baz/1.0 +## diverge right there), so a Tab must list just the two matching loaded +## modules, not foo/1.0 +set got [completion_fish_list {module unload ba}] +completion_assert_contains $got {bar/1.0 baz/1.0} +completion_assert_not_contains $got {foo/1.0} + +## "foo/1.0" is the only loaded module starting with "f": a single Tab must +## complete the rest of it inline, with a trailing space since a +## loaded-module candidate is not a directory-style entry +set got [completion_fish_inline {module unload f}] +completion_assert_eq $got {oo/1.0 } + +completion_fish_close +unsetenv_loaded_module + + +# +# 'use' completes a filesystem directory path, falling through to fish's +# own default filename completion -- there is no fish equivalent of bash's +# 'complete -o default' registration, but 'use' is also not covered by any +# of init/fish_completion's own '__fish_module_use_*' predicates, so +# nothing in the completion script intercepts it either way +# + +set usedir "$env(TESTSUITEDIR)/completion-use-fixture" +file delete -force $usedir +file mkdir $usedir + +completion_fish_start + +## drop the last 3 characters of the fixture dir name ("ure"): a single Tab +## must complete them back plus a trailing '/', with no trailing space, +## exactly like the "bar" -> "bar/" case above +set got [completion_fish_inline "module use [string range $usedir 0 end-3]"] +completion_assert_eq $got {ure/} + +completion_fish_close +file delete -force $usedir + + +# +# 'unuse' is not covered by any '__fish_module_use_*' predicate (see this +# file's header): Tab-completion here falls through to plain filename +# completion of the working directory, the same fallback 'use' relies on +# above, and never proposes the enabled modulepaths themselves +# + +setenv_path_var MODULEPATH $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp + +completion_fish_start + +set got [completion_fish_list {module unuse }] +completion_assert_not_contains $got [list $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp] + +completion_fish_close + + +# +# 'restore' lists the saved collections, and filters them on the typed +# prefix like any other candidate list +# + +setenv_path_var MODULEPATH $modpath + +completion_fish_start + +completion_fish_raw {module save foo} +completion_fish_raw {module save fox} +completion_fish_raw {module save bar1} + +set got [completion_fish_list {module restore }] +completion_assert_contains $got {foo fox bar1} + +## "fo" is the common prefix of foo/fox: a single Tab from "f" must +## auto-insert the "o" inline, with no audible/visible feedback (see +## 040-fish-procs.exp's header) +set got [completion_fish_inline {module restore f}] +completion_assert_eq $got {o} + +## from "fo", nothing more can be auto-inserted (foo/fox diverge right +## there), so a Tab must list just the two matching collections +set got [completion_fish_list {module restore fo}] +completion_assert_contains $got {foo fox} +completion_assert_not_contains $got {bar1} + +## "bar1" is the only collection starting with "ba": a single Tab must +## complete the rest of it inline, with a trailing space since a +## collection candidate is not a directory-style entry +set got [completion_fish_inline {module restore ba}] +completion_assert_eq $got {r1 } + +completion_fish_close + + +# +# ground-truth check against 'module help' (see completion.00-init/ +# 007-module_help.exp): a bare 'module ' must propose exactly the +# documented sub-commands, and 'module -' must propose exactly the +# documented switches -- like the avail/load/unload checks above, these are +# two mutually exclusive candidate sets in fish, so each gets its own Tab. +# Since init/fish_completion does not gate its option flags by sub-command +# (see this file's header), 'module -' is checked against every +# documented option this testsuite knows about -- $help_switches (the +# global 'Options:' section) *and* every sub-command's own extras +# ($help_subcmd_opts(*)) -- combined into one set, minus '-h'/'--help' +# (fish proposes neither, another gap noted in this file's header). Two +# groups of sub-command-specific options are excluded from this union: +# 'config''s own '--dump-state'/'--reset' (real flags, but only declared in +# its own dedicated, gated completion entry, not the unconditional block +# every other flag comes from) and the '*-path' commands' +# '--delim'/'--duplicates'/'--index' (not declared anywhere in the +# completion script at all -- a further, narrower gap than the 'ml'/'-h'/ +# 'unuse' ones already noted in this file's header, not broad enough to +# warrant its own bullet there) +# + +setenv_path_var MODULEPATH $modpath + +set fish_all_opts $help_switches +foreach n [array names help_subcmd_opts] { + if {$n in {config append-path prepend-path remove-path}} { + continue + } + foreach o $help_subcmd_opts($n) { lappend fish_all_opts $o } +} +set fish_all_opts [lsort -unique [lsearch -all -inline -not -exact\ + [lsearch -all -inline -not -exact $fish_all_opts -h] --help]] + +completion_fish_start + +set got [completion_fish_list {module }] +completion_assert_contains $got $help_subcmds +completion_assert_only_known $got {*} $help_subcmds + +set got [completion_fish_list {module -}] +completion_assert_contains $got $fish_all_opts +completion_assert_only_known $got {-*} $fish_all_opts + +completion_fish_close + + +# +# unlike 021-bash.exp/031-zsh.exp, there is no per-sub-command option +# ground-truth section here: init/fish_completion does not gate its option +# flags by sub-command at all (see this file's header), so +# 'module -' always proposes the same full $fish_all_opts +# set checked above, for every sub-command alike +# + + +# +# ground-truth check, per sub-command: 'module ' must +# propose a candidate of the kind 'module help' documents as that +# sub-command's positional argument, for the two kinds with fixture data +# predictable enough to check for by name -- an available/loaded module +# ('modulefile'/'modfile'/'mod1'/'mod2'/'mod' in the usage synopsis) or a +# saved collection ('collection'/'coll'). The remaining kinds (a filesystem +# directory, an arbitrary file, or nothing completable at all -- a search +# string, a shell name, a stash collection name generated with an +# unpredictable timestamp, ...) are not asserted on here: 'use' and 'unuse' +# already get a dedicated, fixture-appropriate check of their own earlier +# in this file, and the rest fall back to fish's own directory/file +# completion, which is not fixture data this testsuite controls +# + +setenv_path_var MODULEPATH $modpath +setenv_loaded_module [list bar/1.0] [list $modpath/bar/1.0] + +completion_fish_start + +completion_fish_raw {module save foo} +completion_fish_raw {module save fox} +completion_fish_raw {module save bar1} + +foreach subcmd $help_subcmds { + ## 'list'/'savelist' take a search string filtering already-known state + ## (loaded modules / saved collections), not a name completion target: + ## their own completion only ever proposes their option flags, despite + ## their usage synopsis documenting a module/collection-shaped argument + if {$subcmd in {list savelist}} { + continue + } + if {$subcmd eq {config}} { + ## 'config' takes a configuration option name, not a module or + ## collection: check it against the ground truth fetched from + ## 'module config' (see 008-module_config.exp), plus the two real + ## flags mixed into the same candidate list -- neither less nor more + set got [completion_fish_list "module $subcmd "] + completion_assert_contains $got $config_opts + completion_assert_only_known $got {*} [concat $config_opts\ + $help_subcmd_opts(config)] + continue + } + if {![info exists help_subcmd_argtype($subcmd)]} { + continue + } + set argtype $help_subcmd_argtype($subcmd) + if {$argtype ni {module collection}} { + continue + } + set got [completion_fish_list "module $subcmd "] + if {$argtype eq {module}} { + ## covers every module-completion mechanism this fixture set can + ## produce: available modules not yet loaded (directory-style, + ## trailing '/'), available modules regardless of load state + ## (whatis/display/... style), and the one loaded module (flat + ## 'name/version', switch/unload style) + completion_assert_any $got {bar/ baz/ foo/ advvers/ flatlcompat bar/1.0} + } else { + completion_assert_any $got {foo fox bar1} + } +} + +completion_fish_close +unsetenv_loaded_module + + +# +# Cleanup +# + +reset_test_env + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: From e1521c5c7248a8a84d59d9246ecb02e4b8d0e5a0 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Tue, 11 Aug 2026 19:39:28 +0000 Subject: [PATCH 29/37] doc: document fish coverage in the completion testsuite The completion testsuite gained a fish-driving counterpart to its bash/zsh setup (040-fish-procs.exp/041-fish.exp, mirroring 020-bash-procs.exp/021-bash.exp and 030-zsh-procs.exp/031-zsh.exp), and 005-init_ts.exp/script/mt were updated accordingly at the time, but the testsuite documentation and NEWS.rst entry describing this testsuite still only mentioned bash and zsh. Update doc/source/devel/testsuite.rst to note all three shells are covered, describe the fish binary/built completion script set up alongside the bash and zsh ones, note that a shell whose completion script covers less ground documents each such gap in its own test file's header rather than forcing every file to test the same thing, and add 00/040 to the completion tool's mandatory setup file list documented for script/mt. Update the NEWS.rst entry introducing the completion testsuite to mention it drives a bash, zsh or fish session. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- NEWS.rst | 2 +- doc/source/devel/testsuite.rst | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 6b715c112..4fd5e5889 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -192,7 +192,7 @@ Modules 5.7.0 (not yet released) * Doc: add :ref:`sync-remote-appdir` cookbook recipe. * Testsuite: add *completion* testsuite to validate shell Tab completion of this project. This testsuite can be invoked with ``make testcompletion`` or - ``script/mt comp``. It drives a real bash or zsh session through + ``script/mt comp``. It drives a real bash, zsh or fish session through `Expect `_ to press the Tab key against the built shell completion script and check the resulting candidate list. * Init: fix numerous inconsistencies between ``module help``'s usage message diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index d9d6f620f..6080947c7 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -48,7 +48,7 @@ DejaGnu tools: tools) to press Tab against the built shell completion script and check that the resulting candidate list holds the expected module names and option flags. Driven by the :file:`completion.00-init` directory; - currently covers bash and zsh, see `completion.00-init layout`_. + currently covers bash, zsh and fish, see `completion.00-init layout`_. Each tool corresponds to one Makefile target (``test``, ``testinstall``, ``testlint``, ``testcompletion``, see `Running the testsuite`_) and to one @@ -163,8 +163,9 @@ plus one extra layer, since driving real Tab-key completion is inherently shell-specific in a way none of the other three tools are: - ``005-init_ts.exp`` / ``006-procs.exp`` / ``010-environ.exp`` / - ``011-save_test_env.exp`` set up paths (the ``bash`` and ``zsh`` binaries, - the built :file:`init/bash_completion` and :file:`init/zsh-functions/_module` + ``011-save_test_env.exp`` set up paths (the ``bash``, ``zsh`` and ``fish`` + binaries, the built :file:`init/bash_completion`, + :file:`init/zsh-functions/_module` and :file:`init/fish_completion` scripts, a clean fixture modulepath), the shell-agnostic assert procedures (``completion_assert_contains``, ``completion_assert_not_contains``, ``completion_assert_eq``), and the clean baseline environment/ @@ -182,9 +183,13 @@ shell-specific in a way none of the other three tools are: cmdline they were passed into the shared ``completion_last_cmdline`` variable, which the generic assert procedures use to build their test label. -- ``0NN-.exp`` (e.g. ``021-bash.exp``, ``031-zsh.exp``) holds the - actual test cases for that shell, calling only its own - ``completion__*`` procs plus the shared asserts. +- ``0NN-.exp`` (e.g. ``021-bash.exp``, ``031-zsh.exp``, + ``041-fish.exp``) holds the actual test cases for that shell, calling only + its own ``completion__*`` procs plus the shared asserts. A shell + whose completion script covers less ground than the others (e.g. fish's + has no ``ml`` support and does not gate its option flags by sub-command) + documents each such gap in its own file's header instead of forcing every + file to test the same thing. Adding a new shell means adding its own ``completion__*`` procs file and test file; nothing in ``006-procs.exp`` needs to change. @@ -638,8 +643,8 @@ mandatory setup files for that tool (for ``modules``: ``00/005 00/006 00/010 00/050 00/060 00/080 00/085``; for ``install``: ``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``; for ``completion``: ``00/005 00/006 00/007 00/008 00/010 00/011 00/020 -00/030``), plus the ``999-cleanup.exp`` of every selected series. Passing a -bare series +00/030 00/040``), plus the ``999-cleanup.exp`` of every selected series. +Passing a bare series number always expands to every file in that directory, because several of those series are order-sensitive or enumerate a whole modulepath (see `Adding new test fixtures`_). The collection series (``61``) is one such From da61564408be2d04344d6b7cb0d4c810a32b63ed Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 13:49:43 +0000 Subject: [PATCH 30/37] init: complete option flags for the tcsh 'help' sub-command The 'n/help/`_module_avail`/' rule in init/tcsh_completion.in only ever offered available module names for 'module help ', unlike the bash completion script, which groups 'help' together with 'show'/'display'/'test'/'path'/'paths'/'is-loaded'/'info-loaded' and completes '-i'/'--icase' alongside the module names for all of them -- matching the usage synopsis of 'module help' itself ('help [-i] [modulefile ...]'). Every one of those other sub-commands already carries this exact "`_module_avail`; echo "@comp_mfile_opts@"" pattern in init/tcsh_completion.in, except 'help', which was left calling '_module_avail' bare. Complete the pattern so 'help' matches its siblings and its own documented option. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- init/tcsh_completion.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init/tcsh_completion.in b/init/tcsh_completion.in index b9532c647..6e72ae3dd 100644 --- a/init/tcsh_completion.in +++ b/init/tcsh_completion.in @@ -33,7 +33,7 @@ alias _module_not_yet_loaded '\\ alias _module_modulepath 'echo ${MODULEPATH} | sed '"'"'s/:/\n/g;'"'"' ' complete module 'C,/*,f,' 'C,.*,f,' 'C,~/*,f,' \ - 'n/help/`_module_avail`/' \ + 'n/help/`_module_avail; echo "@comp_mfile_opts@"`/' \ 'n/add/`_module_not_yet_loaded; echo "@comp_load_opts@"`/' \ 'n/load/`_module_not_yet_loaded; echo "@comp_load_opts@"`/' \ 'n/add-any/`_module_not_yet_loaded; echo "@comp_load_opts@"`/' \ From 6b8333c71d2303fc93cda63d8eb419efe76d1e91 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 13:50:50 +0000 Subject: [PATCH 31/37] ts: check tcsh completion, mirroring the bash/zsh/fish completion testsuite Add completion.00-init/050-tcsh-procs.exp (a completion_tcsh_{start,raw,list,inline,close} set, mirroring 020-bash-procs.exp/030-zsh-procs.exp/040-fish-procs.exp) and 051-tcsh.exp (mirroring 021-bash.exp section-for-section), driving a real tcsh pty through the tcsh completion script the same way the existing tests drive their own shell completion script. The tcsh line editor differs from readline/zle/the fish line editor in ways that shape almost every proc in 050-tcsh-procs.exp: Tab only ever extends the typed word to the longest common prefix, ringing the bell whenever the result is still ambiguous even when it did insert further characters; listing every ambiguous match is bound to a separate key, '^D', which never edits the line and always lists every candidate matching the word exactly as typed (be there one, many, or none at all) -- so, unlike the bash/zsh/fish double/single-Tab convention, completion_tcsh_list sends no Tab at all, only '^D', sidestepping the common-prefix auto-extension trap the other three shells work around by choosing cmdline prefixes carefully. Every submitted command line echoes back with a doubled '\r' before the '\n', and killing an input line with '^U' redraws it via a cursor-column-position plus clear-to-end-of-line escape rather than reprinting the prompt text. init/tcsh_completion.in also does not shell out to the 'module' command itself the way bash/zsh do to fetch candidates: its own aliases invoke modulecmd.tcl directly through an absolute interpreter path baked in at './configure' time, so 005-init_ts.exp rewrites that one baked-in invocation to route through this test's own $TCLSH/$MODULECMD before sourcing it, leaving the real 'complete module ...'/'complete ml ...' registration under test untouched. Real behavioral differences are documented in the header of 051-tcsh.exp and reflected in its assertions rather than worked around: a unique "bar" -> "bar/" completion still rings a bell (tcsh has no equivalent of the nospace exception that stays silent right at a directory-style boundary); '^D' listings never collapse same-name modules into one "name/" category candidate the way the other shells own scripts do, showing the flat 'name/version' strings instead; 'ml' has a far sparser registration than 'module' (no global switches at all, and '-' offers only the loaded-module unload shortcut, not also the load option flags); 'unuse' only lists modulepaths from a bare, empty word, since any '/'-prefixed word trips the unconditional filename-completion rule declared ahead of the modulepath-specific one; and 'use'/'source' each document an option flag or two that init/tcsh_completion.in never wires into their own rule. 005-init_ts.exp gains the tcsh binary/built-completion-script checks already present there for the other three shells, and the comp setup file list in script/mt picks up 00/050 alongside the existing ones. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- script/mt | 2 +- testsuite/completion.00-init/005-init_ts.exp | 32 ++ .../completion.00-init/050-tcsh-procs.exp | 262 ++++++++++ testsuite/completion.00-init/051-tcsh.exp | 464 ++++++++++++++++++ 4 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 testsuite/completion.00-init/050-tcsh-procs.exp create mode 100644 testsuite/completion.00-init/051-tcsh.exp diff --git a/script/mt b/script/mt index 34dcc69ad..c69508919 100755 --- a/script/mt +++ b/script/mt @@ -107,7 +107,7 @@ if [ $# -gt 0 ]; then elif [ "$1" = "comp" ]; then target='testcompletion' testserie='completion' - setuptestfiles=(00/005 00/006 00/007 00/008 00/010 00/011 00/020 00/030 00/040) + setuptestfiles=(00/005 00/006 00/007 00/008 00/010 00/011 00/020 00/030 00/040 00/050) shift fi fi diff --git a/testsuite/completion.00-init/005-init_ts.exp b/testsuite/completion.00-init/005-init_ts.exp index 9388ecd5d..461b88f56 100644 --- a/testsuite/completion.00-init/005-init_ts.exp +++ b/testsuite/completion.00-init/005-init_ts.exp @@ -77,6 +77,18 @@ if {![file exists $fishcompletion]} { unsupported "completion script not found: $fishcompletion (run 'make initdir' first)" } +# a tcsh binary is required to drive interactive completion tests +set tcshbin [find_bin tcsh] +if {$tcshbin eq {}} { + unsupported {no tcsh binary found to drive completion tests} +} + +# built tcsh completion script under test +set tcshcompletion_src "$env(OBJDIR)/init/tcsh_completion" +if {![file exists $tcshcompletion_src]} { + unsupported "completion script not found: $tcshcompletion_src (run 'make initdir' first)" +} + # clean modulefile fixture tree: unlike testsuite/modulefiles, this one holds # no modulefile deliberately broken to exercise error paths, which would get # in the way of straightforward completion listing checks @@ -89,4 +101,24 @@ set completionhome "$env(TESTSUITEDIR)/completion-home" file delete -force $completionhome file mkdir $completionhome +# unlike the other shells' completion scripts, init/tcsh_completion.in does +# not shell out to the 'module' command itself to fetch candidates: it bakes +# an absolute '@TCLSH@ "@libexecdir@/modulecmd.tcl"' invocation into every +# alias (_module_avail, _module_loaded, ...) at './configure' time, pointing +# at whatever install prefix this tree happened to be configured with -- +# unrelated to (and, in a plain dev checkout, nonexistent relative to) the +# $TCLSH/$MODULECMD this testsuite actually runs against. Rewrite just that +# hardcoded interpreter invocation to route through the test's own +# $TCLSH/$MODULECMD instead, leaving the real 'complete module ...'/ +# 'complete ml ...' registration under test completely untouched +set tcshcompletion "$completionhome/tcsh_completion" +set in [open $tcshcompletion_src r] +set tcshcompletion_content [read $in] +close $in +set tcshcompletion_content [regsub -all {[^ \t\n]*tclsh "[^"]*modulecmd\.tcl"}\ + $tcshcompletion_content "'$TCLSH' '$MODULECMD'"] +set out [open $tcshcompletion w] +puts -nonewline $out $tcshcompletion_content +close $out + # vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/050-tcsh-procs.exp b/testsuite/completion.00-init/050-tcsh-procs.exp new file mode 100644 index 000000000..9087aedb0 --- /dev/null +++ b/testsuite/completion.00-init/050-tcsh-procs.exp @@ -0,0 +1,262 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/12 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Test procedures +# Command: +# Sub-Command: +# +# Comment: %C{ +# Defines the test procedures driving a real tcsh pty to +# exercise the module Tab completion script exactly as a +# user pressing Tab would, then collect the resulting +# candidate word list. This mirrors 020-bash-procs.exp's +# completion_bash_{start,raw,list,close} set (see that +# file's header for why each shell gets its own), but +# tcsh's own line editor differs from readline/zle/fish's +# line editor in ways that shape almost every proc below: +# - Tab (bound to 'complete-word') only ever extends the +# typed word to the longest common prefix it can +# determine, ringing the bell whenever the result is +# still ambiguous -- even when it did insert further +# characters, unlike bash (which stays silent once it +# reaches a directory-style '/' boundary) but like +# bash otherwise (eg still ambiguous after inserting +# as much of a common prefix as it can) +# - listing every ambiguous match is not a second Tab +# away (pressing Tab again once nothing more can be +# inserted just rings the bell again): it is bound to +# a separate key, Ctrl-D ('delete-char-or-list-or-eof', +# which lists instead of deleting when the cursor is +# at the end of a non-empty line). Unlike Tab, Ctrl-D +# never edits the line -- it always lists every +# candidate matching the word exactly as currently +# typed, be there one or many -- so, unlike bash/zsh/ +# fish's double/single-Tab convention, +# completion_tcsh_list below sends no Tab at all, only +# Ctrl-D: sending one first would risk its own +# common-prefix auto-extension silently eating part of +# the ambiguity a test cmdline is relying on, the same +# trap bash/zsh/fish's own header comments have to +# route around by choosing cmdline prefixes carefully +# -- Ctrl-D alone sidesteps it entirely, and also means +# this file needs no bash/zsh-style workaround for +# 'module config -' silently auto-extending to +# '--' before a listing can be requested. When the +# typed word matches zero candidates, Ctrl-D prints +# nothing at all -- not even a bell -- so +# completion_tcsh_list matches that case separately +# and returns an empty list, rather than the listing +# block every other outcome produces +# - every submitted command line (a real Enter, not a +# Tab/Ctrl-D) echoes back followed by a doubled '\r' +# before the '\n', rather than a plain '\r\n' -- folded +# into every regexp below that waits on one +# - killing the (uncompleted or already-completed) input +# line with Ctrl-U redraws via a cursor-column-position +# plus clear-to-end-of-line escape sequence, rather +# than reprinting the prompt text the way bash's +# readline does -- so, like zsh, this file submits the +# now-empty line afterwards to force a real newline and +# a fresh, reliably matchable prompt +# - init/tcsh_completion.in does not shell out to the +# 'module' command itself the way bash/zsh do to fetch +# candidates: its own aliases (_module_avail, +# _module_loaded, ...) invoke modulecmd.tcl directly, +# through an absolute interpreter path baked in at +# './configure' time -- see 005-init_ts.exp's comment +# on 'tcshcompletion' for how that one hardcoded +# invocation gets rewritten to route through this +# testsuite's own $TCLSH/$MODULECMD before +# completion_tcsh_start ever sources it below +# }C% +# +############################################################################## + +# start a fresh tcsh session with the completion script under test sourced +# and 'module'/'ml' completion registered, ready to accept commands +proc completion_tcsh_start {} { + global tcshbin tcshcompletion completion_prompt completion_tcsh_spawn_id + global TCLSH MODULECMD verbose + + if {$verbose > 1} { + send_user "\tStarting tcsh for completion test\n" + } + + spawn $tcshbin -f + set completion_tcsh_spawn_id $spawn_id + + # a listing as wide as the biggest fixture (eg bare 'module avail', + # hundreds of module names) easily exceeds Expect's default 'match_max' + # (2000 bytes), which silently drops the oldest bytes of the read buffer + # once it fills before a pattern matches -- raise it well past the + # largest listing this file can produce + match_max -i $completion_tcsh_spawn_id 200000 + + # a narrow (default 80-column) pty can make a long completed line wrap, + # which makes tcsh redraw it with extra escape sequences that can be + # mistaken for stray inserted characters (e.g. a trailing space) by + # anything matching on raw output; go wide enough that no cmdline used + # in these tests is ever at risk of wrapping + stty rows 40 columns 500 < $spawn_out(slave,name) + + set spawn_id $completion_tcsh_spawn_id + expect { + -re {[>%#] $} {} + timeout { unresolved {completion tcsh did not start}; return } + } + + # switch to a known, distinctive prompt first (matched with the default + # shell prompt pattern, since completion_tcsh_raw's own matching only + # works once this prompt is in effect); every setup command after this + # point goes through completion_tcsh_raw + send -- "set prompt='$completion_prompt'\r" + expect { + -re "$completion_prompt\$" {} + timeout { unresolved {completion tcsh prompt setup timed out}; return } + } + + # a real 'module' command is needed since the completion script's own + # aliases shell out to modulecmd.tcl (see this file's header) to list + # candidate module names + completion_tcsh_raw "eval \"\`'$TCLSH' '$MODULECMD' tcsh autoinit\`\"" + completion_tcsh_raw "source '$tcshcompletion'" +} + +# send a line to the completion tcsh session and wait for the next prompt; +# used for setup commands whose output is not asserted on +proc completion_tcsh_raw {line} { + global completion_tcsh_spawn_id completion_prompt verbose + + set spawn_id $completion_tcsh_spawn_id + send -- "$line\r" + expect { + -re "\r+\n$completion_prompt\$" {} + timeout { unresolved "completion setup command timed out: $line" } + } + if {$verbose > 1} { + send_user "\tcompletion> $line\n" + } +} + +# send a partial command line followed by Ctrl-D (tcsh's 'list-choices' +# binding, see this file's header), and return the sorted, de-duplicated +# list of candidate words tcsh displayed +proc completion_tcsh_list {cmdline} { + global completion_tcsh_spawn_id completion_prompt completion_last_cmdline + global verbose + + set completion_last_cmdline $cmdline + set spawn_id $completion_tcsh_spawn_id + set recmdline [completion_re_escape $cmdline] + set reprompt [completion_re_escape $completion_prompt] + + send -- "$cmdline" + send -- "\x04" + # let any subprocess-backed candidate generation (eg 'module avail') + # finish rendering before reading it back -- generous, like zsh's own + # equivalent wait, since this can shell out to modulecmd.tcl + after 1500 + set candidates {} + expect { + -re "(?s)\r+\n(.*)\r+\n$reprompt$recmdline\$" { + set candidates $expect_out(1,string) + } + -re "\r+\n$reprompt$recmdline\$" { + # zero matching candidates: unlike every other case, Ctrl-D prints + # nothing at all here -- no listing line, not even a bell -- so + # there is no listing block for the pattern above to capture + # between the echoed cmdline and the redrawn prompt + } + timeout { + unresolved "completion listing timed out for '$cmdline'" + } + } + if {$verbose > 1} { + send_user "\tcompletion raw listing for '$cmdline':\n$candidates\n" + } + + # clear the (untouched -- Ctrl-D never edits the line) input line so the + # session is ready to reuse; tcsh's Ctrl-U redraws via a cursor-column- + # position plus clear-to-end-of-line escape rather than reprinting the + # prompt text (see this file's header), so submit the now-empty line to + # force a real newline + fresh prompt we can reliably match on + send -- "\x15\r" + expect -re "\r+\n$reprompt\$" + + # collapse the multi-column listing into a flat, sorted word list + set words {} + foreach word [split $candidates] { + if {$word ne {}} { + lappend words $word + } + } + return [lsort -unique $words] +} + +# send a partial command line followed by a single Tab, and return the text +# tcsh inserted inline -- a bell ('\a') if the result is still ambiguous +# (whether or not further characters got inserted alongside it, see this +# file's header) or nothing at all could be completed, otherwise the extra +# characters tcsh appended. Used to check unique, unambiguous completions -- +# eg that a directory-style entry gets completed with a trailing '/' and no +# trailing space. cmdline must be a prefix with exactly one match, or with a +# common prefix short enough that no further Tab is needed to reach it, +# otherwise a single Tab may extend by less than a full match +proc completion_tcsh_inline {cmdline} { + global completion_tcsh_spawn_id completion_prompt completion_last_cmdline + global verbose + + set completion_last_cmdline $cmdline + set spawn_id $completion_tcsh_spawn_id + set recmdline [completion_re_escape $cmdline] + set reprompt [completion_re_escape $completion_prompt] + + send -- "$cmdline\t" + # let every byte tcsh is going to insert land in the pty buffer before + # reading it: an 'expect' pattern ending in a lazy '(.*)$' would + # otherwise match the instant the plain cmdline echo arrives, racing + # ahead of e.g. a trailing space that is still in flight + after 300 + set suffix {} + expect { + -re "${recmdline}(.*)\$" { + set suffix $expect_out(1,string) + } + timeout { + unresolved "completion inline insert timed out for '$cmdline'" + } + } + if {$verbose > 1} { + send_user "\tcompletion inline insert for '$cmdline': \[$suffix\]\n" + } + + # clear the (possibly completed) input line so the session is ready to + # reuse, same technique as completion_tcsh_list + send -- "\x15\r" + expect -re "\r+\n$reprompt\$" + + return $suffix +} + +proc completion_tcsh_close {} { + global completion_tcsh_spawn_id + + if {[info exists completion_tcsh_spawn_id]} { + set spawn_id $completion_tcsh_spawn_id + catch {send -- "exit\r"} + catch {expect eof} + catch {close -i $completion_tcsh_spawn_id} + catch {wait -i $completion_tcsh_spawn_id} + unset completion_tcsh_spawn_id + } +} + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/testsuite/completion.00-init/051-tcsh.exp b/testsuite/completion.00-init/051-tcsh.exp new file mode 100644 index 000000000..b36fc3d25 --- /dev/null +++ b/testsuite/completion.00-init/051-tcsh.exp @@ -0,0 +1,464 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/12 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: avail, load, unload, ml, use, unuse, restore +# Sub-Command: +# +# Comment: %C{ +# Check tcsh Tab completion: mirrors 021-bash.exp (see +# that file for the full rationale of each check) as +# closely as tcsh's own completion script allows -- every +# completion_bash_* call replaced by its completion_tcsh_* +# counterpart -- since init/tcsh_completion.in gates its +# option flags per sub-command via the same shared +# @comp_*_opts@ token lists bash's own script is built +# from (see init/Makefile), every per-sub-command +# ground-truth section bash has is kept here too, unlike +# 031-zsh.exp/041-fish.exp. The few genuine deviations +# from bash, all driven by init/tcsh_completion.in itself +# or by tcsh's own line editor (see 050-tcsh-procs.exp's +# header for the latter), are noted where they occur: +# - "bar" uniquely matching the "bar/" module category +# still rings a bell on completion: tcsh has no +# equivalent of bash's 'compopt -o nospace' exception +# that stays silent right at a directory-style '/' +# boundary even though the match remains ambiguous +# beyond it (see 050-tcsh-procs.exp's header) +# - unlike bash/zsh's own completion scripts, the +# aliases in init/tcsh_completion.in (_module_avail, +# ...) never collapse every module sharing a name into +# one directory-style "name/" candidate of their own: +# Ctrl-D's listing always shows the flat 'name/version' +# strings modulecmd.tcl produced, so every listing +# check in this file looks for those instead of the +# "name/" category entries 021-bash.exp checks for. A +# single Tab's own common-prefix computation can still +# land exactly on a "/" boundary and insert it (eg the +# "bar" case above) -- that is a computed common +# prefix, not a distinct listed candidate, so it is +# unaffected by this +# - 'ml' has a far sparser 'complete' registration of its +# own than 'module' does: no global switches at all for +# a bare 'ml' (no '-h'/'--help'/'-D', unlike bash), and +# 'ml -' offers only the loaded-module unload +# shortcuts ('-name', via a dedicated 'c/-/.../' rule) +# -- not also 'load's own option flags the way bash +# mixes in there. This file's 'ml' section is pared +# down to just the checks that still apply +# - 'unuse' is only usable from a bare, empty word: every +# modulepath in this fixture is an absolute path, and +# the moment the word being completed starts with '/' +# it matches init/tcsh_completion.in's own unconditional +# 'C,/*,f,' rule (plain filename completion for any +# word starting with '/'), which is declared ahead of +# the 'unuse'-specific 'n/unuse/`_module_modulepath`/' +# rule and wins over it -- so, unlike bash, this file +# cannot narrow the listing by prefix or check the +# single-remaining-modulepath inline-completion case +# - 'use' and 'source' each document an option flag or +# two in their usage synopsis ('-a'/'-p'; +# '--auto'/'--no-auto'/'-f'), but init/tcsh_completion.in +# never wires any @comp_*_opts@ macro into their own +# rule the way every other option-bearing sub-command's +# rule does -- so, like 'ml'/'unuse' above, the +# per-sub-command option ground-truth section below +# treats these two as if they took no option at all +# ('help' had the same gap -- no '@comp_mfile_opts@' in +# its own rule despite documenting '-i' -- fixed +# directly in init/tcsh_completion.in rather than +# worked around here, since 'help' otherwise dispatches +# exactly like 'show'/'display'/'test'/... which all +# already carry that macro) +# }C% +# +############################################################################## + + +# +# module names and option flags both get listed on Tab completion +# + +setenv_path_var MODULEPATH $modpath + +completion_tcsh_start + +## unlike bash/zsh's own completion scripts, init/tcsh_completion.in's +## aliases (_module_avail, ...) do not collapse every module sharing a name +## into one directory-style "name/" candidate -- Ctrl-D's listing shows +## exactly the flat 'name/version' strings modulecmd.tcl produced, so this +## file checks for those instead of the "name/" category entries +## 021-bash.exp looks for (a single Tab's own common-prefix computation can +## still land exactly on a "/" boundary, see the "bar" case further below -- +## that is a computed common prefix, not a distinct listed candidate) +set got [completion_tcsh_list {module avail }] +completion_assert_contains $got {advvers/1.0 bar/1.0 baz/1.0 foo/1.0} +completion_assert_contains $got {--all --icase --terse} + +set got [completion_tcsh_list {module load }] +completion_assert_contains $got {advvers/1.0 bar/1.0 baz/1.0 foo/1.0} +completion_assert_contains $got {--force -f --icase} + +## "ba" is the common prefix of bar/ and baz/: a single Tab from "b" must +## auto-insert the "a" inline, with a bell right before it since bar/baz +## still leave the completion ambiguous beyond it +set got [completion_tcsh_inline {module load b}] +completion_assert_eq $got "\aa" + +## from "ba", nothing more can be auto-inserted (bar/baz diverge right +## there): Ctrl-D must list every flat module matching that prefix (see +## this section's opening comment on why these are not "bar/"/"baz/" +## category entries) +set got [completion_tcsh_list {module load ba}] +completion_assert_contains $got {bar/1.0 bar/2.0 baz/1.0 baz/2.0} +completion_assert_not_contains $got {foo/1.0 advvers/1.0} + +## "bar" uniquely matches the "bar/" module category: a single Tab must +## complete it inline with a trailing '/' and no trailing space (so typing +## can continue straight into a version) -- but, unlike bash, still with a +## leading bell, since tcsh has no exception for a directory-style boundary +## that remains ambiguous beyond it (see this file's header) +set got [completion_tcsh_inline {module load bar}] +completion_assert_eq $got "\a/" + +## from "bar/", Ctrl-D must list the versions available under it +set got [completion_tcsh_list {module load bar/}] +completion_assert_contains $got {bar/1.0 bar/2.0} + +## "flatlcompat" is a single, versionless module (no further completion +## possible beneath it, so unlike "bar" above it does not end in '/'): a +## single Tab must complete it inline, with a trailing space since it is +## not a directory-style entry, and no bell since it is genuinely unique +set got [completion_tcsh_inline {module load fl}] +completion_assert_eq $got {atlcompat } + +completion_tcsh_close + + +# +# 'unload' lists the currently loaded modules alongside the option flags, +# and lists/completes them on the typed prefix like any other candidate list +# + +setenv_loaded_module [list bar/1.0 baz/1.0 foo/1.0]\ + [list $modpath/bar/1.0 $modpath/baz/1.0 $modpath/foo/1.0] + +completion_tcsh_start + +set got [completion_tcsh_list {module unload }] +completion_assert_contains $got {bar/1.0 baz/1.0 foo/1.0 --force -f --icase} + +## "ba" is the common prefix of bar/1.0 and baz/1.0: a single Tab from "b" +## must auto-insert the "a" inline, with a bell right before it since +## bar/1.0 and baz/1.0 still leave the completion ambiguous beyond it +set got [completion_tcsh_inline {module unload b}] +completion_assert_eq $got "\aa" + +## from "ba", nothing more can be auto-inserted (bar/1.0 and baz/1.0 +## diverge right there), so Ctrl-D must list just the two matching loaded +## modules, not foo/1.0 +set got [completion_tcsh_list {module unload ba}] +completion_assert_contains $got {bar/1.0 baz/1.0} +completion_assert_not_contains $got {foo/1.0} + +## "foo/1.0" is the only loaded module starting with "f", and no option +## flag does either: a single Tab must complete the rest of it inline, +## with a trailing space since a loaded-module candidate is not a +## directory-style entry, and no bell since it is genuinely unique +set got [completion_tcsh_inline {module unload f}] +completion_assert_eq $got {oo/1.0 } + +completion_tcsh_close +unsetenv_loaded_module + + +# +# 'ml' mirrors 'module load' completion for the bare sub-command case, but +# (unlike bash) proposes no global switches at all alongside the module +# names, and (once an option flag is being typed) offers only the "-" +# unload shortcut for each currently loaded module -- not also 'load's own +# option flags (see this file's header) +# + +setenv_path_var MODULEPATH $modpath + +completion_tcsh_start + +set got [completion_tcsh_list {ml }] +completion_assert_contains $got {advvers/1.0 bar/1.0 baz/1.0 foo/1.0} +completion_assert_not_contains $got {-h --help -D} + +completion_tcsh_raw {module load bar} + +## with bar/2.0 loaded (the default version, no .version file pins it), a +## "bar/2.0" candidate must appear -- and, unlike bash, nothing else. The +## listing itself drops the leading "-" the 'c/-/.../' rule matched on (it +## is only the single-Tab *insertion* below that appends the suffix right +## after the "-" already typed on the line, making the result look like +## "-bar/2.0" there) +set got [completion_tcsh_list {ml -}] +completion_assert_contains $got {bar/2.0} +completion_assert_not_contains $got {--force -f --icase} + +## "-bar/2.0" is the only candidate starting with "-ba": a single Tab must +## complete the rest of it inline, with a trailing space since it is not a +## directory-style entry, and no bell since it is genuinely unique +set got [completion_tcsh_inline {ml -ba}] +completion_assert_eq $got {r/2.0 } + +completion_tcsh_close + + +# +# 'use' completes a filesystem directory path: init/tcsh_completion.in +# registers it as directory-only completion ('n/use/d/'), and the +# unconditional 'C,/*,f,' rule (see this file's header) reaches the same +# filesystem entry regardless for this fixture's absolute path, so either +# way this behaves like plain directory completion +# + +set usedir "$env(TESTSUITEDIR)/completion-use-fixture" +file delete -force $usedir +file mkdir $usedir + +completion_tcsh_start + +## drop the last 3 characters of the fixture dir name ("ure"): a single Tab +## must complete them back plus a trailing '/', with no trailing space and +## no bell, exactly like the "flatlcompat" case above -- the match is +## genuinely unique, so unlike the "bar" -> "bar/" case earlier there is no +## further ambiguity beyond the boundary for tcsh to bell about +set got [completion_tcsh_inline "module use [string range $usedir 0 end-3]"] +completion_assert_eq $got {ure/} + +completion_tcsh_close +file delete -force $usedir + + +# +# 'unuse' lists the currently enabled modulepaths, but (unlike bash) only +# from a bare, empty word -- typing any prefix at all trips the '/' +# filename-completion catch-all before it ever reaches the modulepath list +# (see this file's header), so this section, unlike 031-zsh.exp's own +# 'unuse' checks, cannot narrow by prefix or exercise the single-remaining- +# modulepath inline-completion case +# + +setenv_path_var MODULEPATH $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp + +completion_tcsh_start + +set got [completion_tcsh_list {module unuse }] +completion_assert_contains $got [list $modpath "$env(TESTSUITEDIR)/modulefiles.4" /tmp] + +completion_tcsh_close + + +# +# 'restore' lists the saved collections, and filters them on the typed +# prefix like any other candidate list +# + +setenv_path_var MODULEPATH $modpath + +completion_tcsh_start + +completion_tcsh_raw {module save foo} +completion_tcsh_raw {module save fox} +completion_tcsh_raw {module save bar1} + +set got [completion_tcsh_list {module restore }] +completion_assert_contains $got {foo fox bar1} + +## "fo" is the common prefix of foo/fox: a single Tab from "f" must +## auto-insert the "o" inline, with a bell right before it since foo/fox +## still leave the completion ambiguous beyond it +set got [completion_tcsh_inline {module restore f}] +completion_assert_eq $got "\ao" + +## from "fo", nothing more can be auto-inserted (foo/fox diverge right +## there), so Ctrl-D must list just the two matching collections +set got [completion_tcsh_list {module restore fo}] +completion_assert_contains $got {foo fox} +completion_assert_not_contains $got {bar1} + +## "bar1" is the only collection starting with "ba": a single Tab must +## complete the rest of it inline, with a trailing space since a +## collection candidate is not a directory-style entry, and no bell since +## it is genuinely unique +set got [completion_tcsh_inline {module restore ba}] +completion_assert_eq $got {r1 } + +completion_tcsh_close + + +# +# ground-truth check against 'module help' (see completion.00-init/ +# 007-module_help.exp): a bare 'module ' must propose exactly the +# documented sub-commands plus the global switches, and 'module -' +# (isolating the switches from the sub-command names) must propose exactly +# the documented switches +# + +setenv_path_var MODULEPATH $modpath + +completion_tcsh_start + +set got [completion_tcsh_list {module }] +set got_subcmds [lsearch -all -inline -not -regexp $got {^-}] +completion_assert_contains $got_subcmds $help_subcmds +completion_assert_only_known $got_subcmds {*} $help_subcmds + +set got [completion_tcsh_list {module -}] +completion_assert_contains $got $help_switches +completion_assert_only_known $got {-*} $help_switches + +completion_tcsh_close + + +# +# ground-truth check, per sub-command: 'module -' must +# propose exactly the option flags 'module help' documents for that +# sub-command -- including none at all, for a sub-command that takes no +# option +# + +## typing '-' first, rather than a bare trailing space, matters for the +## same reason it does in 021-bash.exp: every option-bearing sub-command's +## own completion helper skips looking up real module/collection/modulepath +## candidates once the word being completed starts with '-', isolating the +## option flags on their own -- unlike bash/zsh, though, this needs no +## special-casing for 'config's own further '-' -> '--' auto-extension, +## since completion_tcsh_list's Ctrl-D never extends the typed word at all +## (see 050-tcsh-procs.exp's header) +## +## two further sub-commands, beyond 'ml'/'unuse' already noted in this +## file's header, turn out to have a gap of their own here: 'use' +## ('n/use/d/'/'N/use/d/') and 'source' ('n/source/f/') each document an +## option flag or two in their usage synopsis ('-a'/'-p'; +## '--auto'/'--no-auto'/'-f'), but their init/tcsh_completion.in rule never +## concatenates any @comp_*_opts@ macro the way every other option-bearing +## sub-command's rule does -- so these two are treated the same as a +## genuinely option-less sub-command below, overriding whatever +## 'module help' itself documents for them ('help' had the same gap, fixed +## directly in init/tcsh_completion.in instead, see this file's header) +set tcsh_no_opt_subcmds {use source} +setenv_path_var MODULEPATH $modpath + +completion_tcsh_start + +foreach subcmd $help_subcmds { + set expected {} + if {[info exists help_subcmd_opts($subcmd)] && $subcmd ni $tcsh_no_opt_subcmds} { + set expected $help_subcmd_opts($subcmd) + } + if {[llength $expected] == 0} { + ## nothing is expected to complete at all: a single Tab must insert + ## nothing but a bell (not even a directory-style default-completion + ## fallback match, since no file in the test run's working directory + ## starts with '-') -- tcsh rings the bell on any Tab that finds zero + ## completions, not just an ambiguous one + set got [completion_tcsh_inline "module $subcmd -"] + completion_assert_eq $got "\a" + } else { + set got [completion_tcsh_list "module $subcmd -"] + completion_assert_contains $got $expected + completion_assert_only_known $got {-*} $expected + } +} + +completion_tcsh_close + + +# +# same ground truth, this time against 'ml': unlike 021-bash.exp, its +# per-sub-command dispatch does not apply here at all -- 'ml's own +# 'complete' registration in init/tcsh_completion.in only ever completes a +# bare module name or an unload shortcut (see this file's header), it has +# no sub-command dispatch of its own to check +# + + +# +# ground-truth check, per sub-command: 'module ' must +# propose a candidate of the kind 'module help' documents as that +# sub-command's positional argument, for the two kinds with fixture data +# predictable enough to check for by name -- an available/loaded module +# ('modulefile'/'modfile'/'mod1'/'mod2'/'mod' in the usage synopsis) or a +# saved collection ('collection'/'coll'). The remaining kinds +# (a filesystem directory, an arbitrary file, or nothing completable at +# all -- a search string, a shell name, a stash collection name generated +# with an unpredictable timestamp, ...) are not asserted on here: 'use' and +# 'unuse' already get a dedicated, fixture-appropriate check of their own +# earlier in this file, and the rest fall back to plain filename completion +# of the working directory, which is not fixture data this testsuite +# controls +# + +setenv_path_var MODULEPATH $modpath +setenv_loaded_module [list bar/1.0] [list $modpath/bar/1.0] + +completion_tcsh_start + +completion_tcsh_raw {module save foo} +completion_tcsh_raw {module save fox} +completion_tcsh_raw {module save bar1} + +foreach subcmd $help_subcmds { + ## 'list'/'savelist' take a search string filtering already-known state + ## (loaded modules / saved collections), not a name completion target: + ## their own completion only ever proposes their option flags, despite + ## their usage synopsis documenting a module/collection-shaped argument + if {$subcmd in {list savelist}} { + continue + } + if {$subcmd eq {config}} { + ## 'config' takes a configuration option name, not a module or + ## collection: check it against the ground truth fetched from + ## 'module config' (see 008-module_config.exp), plus the two real + ## flags mixed into the same candidate list -- neither less nor more + set got [completion_tcsh_list "module $subcmd "] + completion_assert_contains $got $config_opts + completion_assert_only_known $got {*} [concat $config_opts\ + $help_subcmd_opts(config)] + continue + } + if {![info exists help_subcmd_argtype($subcmd)]} { + continue + } + set argtype $help_subcmd_argtype($subcmd) + if {$argtype ni {module collection}} { + continue + } + set got [completion_tcsh_list "module $subcmd "] + if {$argtype eq {module}} { + ## covers every module-completion mechanism this fixture set can + ## produce: available modules not yet loaded (directory-style, + ## trailing '/'), available modules regardless of load state + ## (whatis/display/... style), and the one loaded module (flat + ## 'name/version', switch/unload style) + completion_assert_any $got {bar/ baz/ foo/ advvers/ flatlcompat bar/1.0} + } else { + completion_assert_any $got {foo fox bar1} + } +} + +completion_tcsh_close +unsetenv_loaded_module + + +# +# Cleanup +# + +reset_test_env + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: From 472fc64b53a41c4d6cc23ccd78f69fdd5be9ac1e Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 13:52:03 +0000 Subject: [PATCH 32/37] doc: document tcsh coverage in the completion testsuite The completion testsuite gained a tcsh-driving counterpart to its bash/zsh/fish setup (050-tcsh-procs.exp/051-tcsh.exp, mirroring 020-bash-procs.exp/030-zsh-procs.exp/040-fish-procs.exp), and 005-init_ts.exp/script/mt were updated accordingly at the time, but the testsuite documentation and NEWS.rst entry describing this testsuite still only mentioned bash, zsh and fish. Update doc/source/devel/testsuite.rst to note all four shells are covered, describe the tcsh binary/built completion script set up alongside the other three, mention the two remaining sub-command gaps noted in that file's own header ('ml' and 'unuse'), and add 00/050 to the completion tool's mandatory setup file list documented for script/mt. Update the NEWS.rst entry introducing the completion testsuite to mention it drives a bash, zsh, fish or tcsh session. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- NEWS.rst | 7 +++--- doc/source/devel/testsuite.rst | 43 +++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 4fd5e5889..389951537 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -192,9 +192,10 @@ Modules 5.7.0 (not yet released) * Doc: add :ref:`sync-remote-appdir` cookbook recipe. * Testsuite: add *completion* testsuite to validate shell Tab completion of this project. This testsuite can be invoked with ``make testcompletion`` or - ``script/mt comp``. It drives a real bash, zsh or fish session through - `Expect `_ to press the Tab key against the - built shell completion script and check the resulting candidate list. + ``script/mt comp``. It drives a real bash, zsh, fish or tcsh session + through `Expect `_ to press the Tab key + against the built shell completion script and check the resulting + candidate list. * Init: fix numerous inconsistencies between ``module help``'s usage message and the option flags and positional arguments each sub-command actually accepts, and between that usage message and what bash, fish, tcsh and zsh diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 6080947c7..621f12583 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -48,7 +48,8 @@ DejaGnu tools: tools) to press Tab against the built shell completion script and check that the resulting candidate list holds the expected module names and option flags. Driven by the :file:`completion.00-init` directory; - currently covers bash, zsh and fish, see `completion.00-init layout`_. + currently covers bash, zsh, fish and tcsh, see `completion.00-init + layout`_. Each tool corresponds to one Makefile target (``test``, ``testinstall``, ``testlint``, ``testcompletion``, see `Running the testsuite`_) and to one @@ -163,13 +164,14 @@ plus one extra layer, since driving real Tab-key completion is inherently shell-specific in a way none of the other three tools are: - ``005-init_ts.exp`` / ``006-procs.exp`` / ``010-environ.exp`` / - ``011-save_test_env.exp`` set up paths (the ``bash``, ``zsh`` and ``fish`` - binaries, the built :file:`init/bash_completion`, - :file:`init/zsh-functions/_module` and :file:`init/fish_completion` - scripts, a clean fixture modulepath), the shell-agnostic assert procedures - (``completion_assert_contains``, ``completion_assert_not_contains``, - ``completion_assert_eq``), and the clean baseline environment/ - ``save_test_env`` checkpoint, exactly as for the other tools. + ``011-save_test_env.exp`` set up paths (the ``bash``, ``zsh``, ``fish`` and + ``tcsh`` binaries, the built :file:`init/bash_completion`, + :file:`init/zsh-functions/_module`, :file:`init/fish_completion` and + :file:`init/tcsh_completion` scripts, a clean fixture modulepath), the + shell-agnostic assert procedures (``completion_assert_contains``, + ``completion_assert_not_contains``, ``completion_assert_eq``), and the + clean baseline environment/``save_test_env`` checkpoint, exactly as for + the other tools. - ``0NN--procs.exp`` defines one ``completion__start`` / ``completion__raw`` / ``completion__list`` / ``completion__inline`` / ``completion__close`` set per @@ -184,12 +186,15 @@ shell-specific in a way none of the other three tools are: variable, which the generic assert procedures use to build their test label. - ``0NN-.exp`` (e.g. ``021-bash.exp``, ``031-zsh.exp``, - ``041-fish.exp``) holds the actual test cases for that shell, calling only - its own ``completion__*`` procs plus the shared asserts. A shell - whose completion script covers less ground than the others (e.g. fish's - has no ``ml`` support and does not gate its option flags by sub-command) - documents each such gap in its own file's header instead of forcing every - file to test the same thing. + ``041-fish.exp``, ``051-tcsh.exp``) holds the actual test cases for that + shell, calling only its own ``completion__*`` procs plus the shared + asserts. A shell whose completion script covers less ground than the + others (e.g. fish's has no ``ml`` support and does not gate its option + flags by sub-command; the tcsh ``ml`` has no global switches at all, and + its ``unuse`` only lists modulepaths from a bare, empty word) documents + each such gap in its own file's header instead of forcing every file to + test + the same thing. Adding a new shell means adding its own ``completion__*`` procs file and test file; nothing in ``006-procs.exp`` needs to change. @@ -642,11 +647,11 @@ Whichever files are selected, :file:`script/mt` always also runs the mandatory setup files for that tool (for ``modules``: ``00/005 00/006 00/010 00/050 00/060 00/080 00/085``; for ``install``: ``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``; for -``completion``: ``00/005 00/006 00/007 00/008 00/010 00/011 00/020 -00/030 00/040``), plus the ``999-cleanup.exp`` of every selected series. -Passing a bare series -number always expands to every file in that directory, because several of -those series are order-sensitive or enumerate a whole modulepath (see +``completion``: ``00/005 00/006 00/007 00/008 00/010 00/011 00/020 00/030 +00/040 00/050``), plus the ``999-cleanup.exp`` of every selected series. +Passing a bare series number always expands to every file in that +directory, because several of those series are order-sensitive or +enumerate a whole modulepath (see `Adding new test fixtures`_). The collection series (``61``) is one such case: its files create real collection files on disk that later files in the same series depend on -- e.g. :file:`modules.61-coll/040-restore.exp` From f7412a6159b3272d31f98a2e63d84f8d0f40039c Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 14:37:18 +0000 Subject: [PATCH 33/37] ts: delete the sandboxed $HOME after the completion testsuite finishes 005-init_ts.exp creates testsuite/completion-home once for the whole completion series and 010-environ.exp points $HOME at it for every completion__start session afterwards, but nothing ever removed it again once the series finished, leaving it behind as stray build output after every completion test run. Add completion.00-init/999-cleanup.exp, matching the pattern already used by several modules series directories to delete on-disk fixtures a series created along the way (e.g. modules.99-finish, whose own 999-cleanup.exp is the closest precedent here: a final, no-continuation cleanup step). It has to run last, once nothing in the series still needs $HOME pointed there. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- testsuite/completion.00-init/999-cleanup.exp | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 testsuite/completion.00-init/999-cleanup.exp diff --git a/testsuite/completion.00-init/999-cleanup.exp b/testsuite/completion.00-init/999-cleanup.exp new file mode 100644 index 000000000..06e8d0090 --- /dev/null +++ b/testsuite/completion.00-init/999-cleanup.exp @@ -0,0 +1,33 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: completion.00-init/%M% +# Revision: %I% +# First Edition: 2026/08/13 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: +# Sub-Command: +# +# Comment: %C{ +# Cleans out after end of all completion tests +# }C% +# +############################################################################## + +# remove the sandboxed $HOME created once for the whole completion series +# (see 005-init_ts.exp/010-environ.exp) -- every completion__start +# session keeps using it as $HOME right up through the last test file, so +# it can only be deleted here, once nothing in the series needs it anymore. +# This also removes the tcsh completion script rewritten for testing under +# it (see 005-init_ts.exp's comment on 'tcshcompletion') +if {$verbose} { + send_user "\tDeleting $completionhome\n" +} +file delete -force $completionhome + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: From b577cbdd97b6aef7ff51f29ee8351553a98d6f51 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 15:15:40 +0000 Subject: [PATCH 34/37] ts: drop the needless fixed wait before every tcsh completion listing completion_tcsh_list slept a blind, unconditional 1.5 real seconds before every single '^D' listing read, copied over from the zsh equivalent proc without re-checking whether tcsh actually needs it. zsh genuinely does: it has to send its cleanup keystroke blind, before reading anything back, since a listing taller than the pty has no reliable end marker of its own to match on, so a fixed settling delay is the only way to avoid corrupting the zle redraw. tcsh has no such constraint -- '^D' cannot print anything at all, not even a partial listing, until the whole candidate set is ready, so the 'expect' already right after it blocks for exactly as long as generating that candidate set genuinely takes, no fixed guess required. That blind sleep was multiplied by every per-sub-command ground-truth loop in 051-tcsh.exp -- 64 sub-commands, iterated over twice -- turning a handful of genuinely slow, child-process-backed calls into well over a hundred wasted seconds per run. Dropping it takes a full 'make testcompletion' run (all four shells) from roughly 380 real seconds to roughly 250, with no change in pass/fail results (737 passes, verified clean). A similar fixed-delay reduction was tried for the zsh equivalent wait and for the fish idle-quiet detection window, but both broke real assertions even at conservative values -- their designs commit to those constants for correctness, not just caution, so they are left untouched here. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- testsuite/completion.00-init/050-tcsh-procs.exp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/testsuite/completion.00-init/050-tcsh-procs.exp b/testsuite/completion.00-init/050-tcsh-procs.exp index 9087aedb0..a72a3742d 100644 --- a/testsuite/completion.00-init/050-tcsh-procs.exp +++ b/testsuite/completion.00-init/050-tcsh-procs.exp @@ -160,10 +160,11 @@ proc completion_tcsh_list {cmdline} { send -- "$cmdline" send -- "\x04" - # let any subprocess-backed candidate generation (eg 'module avail') - # finish rendering before reading it back -- generous, like zsh's own - # equivalent wait, since this can shell out to modulecmd.tcl - after 1500 + # no fixed wait is needed before reading back the result, unlike zsh's + # own equivalent proc: tcsh cannot print anything -- not even a partial + # listing -- until the whole candidate set (however long that takes to + # generate, eg shelling out to modulecmd.tcl) is ready, so the 'expect' + # below already blocks for exactly as long as needed on its own set candidates {} expect { -re "(?s)\r+\n(.*)\r+\n$reprompt$recmdline\$" { From 44a2ce966a13e7d5f342e24a50f9bab88ea06ae8 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 15:35:55 +0000 Subject: [PATCH 35/37] gh: install zsh, fish and tcsh for the completion CI job The package list in completion_tests.yaml was written back when the completion testsuite only drove bash, and never got updated as zsh, fish and tcsh support were added to it later on this same branch. The CI job (ubuntu-24.04) only had bash and Tcl installed, so every zsh/fish/tcsh session failed outright with 'couldn't execute "": no such file or directory' (an empty binary path, from the 'find_bin' lookup in 005-init_ts.exp coming up empty) as soon as 030-zsh-procs.exp, 040-fish-procs.exp and 050-tcsh-procs.exp tried to spawn one. Install zsh, fish and tcsh alongside bash so the job actually exercises every shell this testsuite is meant to cover. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .github/workflows/completion_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/completion_tests.yaml b/.github/workflows/completion_tests.yaml index 7e24b4425..cbccf5a76 100644 --- a/.github/workflows/completion_tests.yaml +++ b/.github/workflows/completion_tests.yaml @@ -20,7 +20,7 @@ jobs: - name: Install system packages run: | sudo apt-get -y update - sudo apt-get install -y expect-dev dejagnu python3-sphinx gzip bash tcl8.6 python3 + sudo apt-get install -y expect-dev dejagnu python3-sphinx gzip bash zsh fish tcsh tcl8.6 python3 # libtclenvmodules build requirements sudo apt-get install -y gcc autoconf tcl8.6-dev - name: Build Modules From bbcff048eb475d8f5c8d1a758ca27fee8ca04b9c Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 16:01:39 +0000 Subject: [PATCH 36/37] ts: recognize the zle no-color auto-menu-select redraw in zsh listings The end-of-listing detection in completion_zsh_list relies on a fixed escape-sequence epilogue (an attribute-reset sequence right before the redraw) that zle only emits when the pty actually has standout/ underline capability. On a pty without it (observed on GitHub Actions' own runners), the exact same auto-menu-selected redraw still happens -- a bare bell plus a plain erase-and-reprint, no escape sequences around it at all -- so the epilogue never matches, and the proc falls through to its "more than one line followed, must be a scrolled listing" branch, swallowing that trailing prompt-plus-cmdline redraw into the listing itself. The observed symptom on that CI job: a bare 'module ' listing whose real 64 sub-commands parsed out fine, but 'MODULESTEST> module' and a stray bell-prefixed word also got counted as "candidates", failing the ground-truth "no unknown word" check. Add a second, escape-sequence-independent detection: since that auto-menu-select redraw always re-echoes the prompt immediately followed by cmdline verbatim regardless of which escape sequences (if any) wrap it, search for that pair reappearing after the listing starts as a fallback listing-end boundary whenever the epilogue itself does not match. A genuinely scrolled listing (e.g. the same bare 'module', long enough to scroll a merely 40-row pty) still falls through unaffected, since zle never attempts that redraw once the current line has scrolled off screen -- there is nothing for this new check to find there either way. Verified against the exact raw byte sequence GitHub Actions' own log captured for the failing case (no live access to that exact zsh/pty combination from here), and against a full local 'make testcompletion' run (737 passes, 0 failures) to confirm the escape-sequence-epilogue path most environments hit is unaffected. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .../completion.00-init/030-zsh-procs.exp | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/testsuite/completion.00-init/030-zsh-procs.exp b/testsuite/completion.00-init/030-zsh-procs.exp index cea7f8564..573f9e6b7 100644 --- a/testsuite/completion.00-init/030-zsh-procs.exp +++ b/testsuite/completion.00-init/030-zsh-procs.exp @@ -263,13 +263,34 @@ proc completion_zsh_list {cmdline} { set have_epilogue [expr {$nl != -1\ && [regexp -indices -start $nl -- $epilogue $buffer epi_range]}] + # the escape sequences that epilogue matches are only emitted when the + # pty has standout/underline capability in the first place -- on a pty + # with none (eg observed on GitHub Actions' own runners), the exact same + # auto-menu-selected redraw happens with no escape sequences around it + # at all, just a bare bell plus plain erase-and-reprint. Since that + # redraw always re-echoes the prompt immediately followed by $cmdline + # verbatim (eg 'module -' becoming 'MODULESTEST> module --color=', + # 'module ' becoming "MODULESTEST> module \x07aliases"), that pair + # reappearing anywhere after the listing starts is a reliable, + # escape-sequence-independent fallback signal for the exact same + # end-of-listing boundary -- matching on the prompt too, not just + # $cmdline alone, keeps this from cutting the listing short if some + # candidate or description text happens to itself contain $cmdline as a + # substring (eg $cmdline "module " inside a "-- ... module list ..." + # description) + set reecho [string first "$completion_prompt$cmdline" $buffer\ + [expr {$nl + 1}]] + if {$have_epilogue} { lassign $epi_range epi_start epi_end set listing [string range $buffer $nl+1 $epi_start-1] + } elseif {$nl != -1 && $reecho != -1} { + set listing [string range $buffer $nl+1 $reecho-1] } elseif {$nl != -1 && [string first "\n" $buffer $nl+1] != -1} { - # no epilogue, but more than one line of content followed the echo: - # a scrolled, too-tall-for-the-pty listing (see above) rather than a - # single-candidate insertion, which never spans more than one line + # no epilogue and no cmdline re-echo either, but more than one line + # of content followed the echo: a scrolled, too-tall-for-the-pty + # listing (see above) rather than a single-candidate insertion, + # which never spans more than one line set listing [string range $buffer $nl+1 end] } else { # neither: the second Tab found a single, already-unique candidate From 954025c5859b20fe85c9a59ff58b60ca4937516d Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Wed, 12 Aug 2026 16:52:48 +0000 Subject: [PATCH 37/37] ts: stop relying on fish's own prompt-end marker to detect readiness completion_fish_wait_ready matched on the "prompt end" marker fish emits once a prompt has finished drawing -- but only on releases that support it under the running conditions. fish 3.7 (observed on a GitHub Actions runner, no recognized terminal-program hint) never emits it at all, no matter the prompt content, so the very first wait in completion_fish_start (before that CI job's own apt-get install of fish was even added) always timed out. That alone was survivable -- 'unresolved' does not abort the calling test -- but every setup command afterward went through completion_fish_raw, which relies on that exact same match, and since 'module' was consequently never actually sourced, every completion attempt for the rest of the file silently fell through to fish's own plain filename completion of the working directory (the observed symptom: real repository file names where module sub-command names were expected). Replace the marker matching entirely with the idle-quiet detection completion_fish_capture already relies on to read back a completion listing -- the same technique, applied to prompt waits instead of listings, needs no cooperation from fish itself and so works regardless of release or terminal-program recognition. Tried first: a version-independent fallback that also matched on the custom prompt own literal text once installed, alongside the marker -- abandoned because fish always appends cursor-positioning bytes right after printing a prompt, so a trailing-anchored text match never actually settles either. Verified with a full local 'make testcompletion' run (737 passes, 0 failures) and, before settling on this design, by deliberately breaking the marker pattern so it could never match, forcing every wait through a fallback path -- confirming the idle detection in completion_fish_capture (unaffected by any of this) was already carrying every real completion assertion throughout that run regardless. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle --- .../completion.00-init/040-fish-procs.exp | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/testsuite/completion.00-init/040-fish-procs.exp b/testsuite/completion.00-init/040-fish-procs.exp index e3f29c7b3..844f642b0 100644 --- a/testsuite/completion.00-init/040-fish-procs.exp +++ b/testsuite/completion.00-init/040-fish-procs.exp @@ -66,6 +66,18 @@ # one level of parentheses (eg "(Load modulefile(s))"), # a plain 'no parenthesis inside' pattern is not # enough +# - fish has no equivalent of bash/zsh/tcsh's own +# distinctive custom $PS1-alike prompt to match on and +# wait for: completion_fish_wait_ready instead waits for +# the pty to simply fall quiet after a prompt (re)draw, +# the same idle-quiet technique completion_fish_capture +# uses to read back a completion listing (matching on +# fish's own OSC 133 "prompt end" marker, emitted once a +# prompt has finished drawing on releases that support +# it under the running conditions, was tried first and +# abandoned: fish 3.7, observed on a GitHub Actions +# runner with no recognized terminal-program hint, never +# emits it at all, no matter the prompt content) # }C% # ############################################################################## @@ -85,27 +97,24 @@ proc completion_fish_query_responder {} { } } -# marker fish emits once a prompt has finished (re)drawing and is ready to -# accept input -- the OSC 133 "prompt end" marker, followed by a clear-to- -# end-of-line and a cursor-forward repositioning; appears after every fresh -# prompt, whether the still-default one (early in completion_fish_start, -# before the custom prompt below is installed) or the custom one, so it is -# used throughout as the one 'ready' signal to wait on -set completion_fish_promptready {\x1b\]133;B(\x07|\x1b\\)\x1b\[K\r\x1b\[[0-9]*C} - -# wait for the fish session to reach a freshly drawn, ready prompt, -# answering any terminal-capability query fish asks along the way -proc completion_fish_wait_ready {{tout 10}} { - global completion_fish_spawn_id completion_fish_promptready +# wait for the fish session to fall quiet after a prompt (re)draw -- +# reusing the exact same idle-quiet technique completion_fish_capture +# relies on to read back a completion listing (see completion_fish_read_ +# quiet's own header for why a fixed delay does not scale here either). An +# earlier version of this proc instead matched on the OSC 133 "prompt end" +# marker fish emits once a prompt has finished drawing, on releases that +# support it under the running conditions -- but fish 3.7 (observed on a +# GitHub Actions runner, no recognized terminal-program hint) never emits +# it at all, no matter the prompt content, so that marker cannot be relied +# on to even exist. Idle detection needs no such cooperation from fish +# itself, at the cost of no longer being able to tell "fish is ready" apart +# from "fish crashed and will never answer" -- a real difference in theory, +# but nothing downstream of this proc treats them differently either way +proc completion_fish_wait_ready {} { + global completion_fish_spawn_id set spawn_id $completion_fish_spawn_id - set old_timeout $::timeout - set ::timeout $tout - set ok 1 - expect {*}[completion_fish_query_responder] -re $completion_fish_promptready {} \ - timeout { set ok 0 } - set ::timeout $old_timeout - return $ok + completion_fish_read_quiet } # start a fresh fish session with the completion script under test sourced; @@ -132,10 +141,7 @@ proc completion_fish_start {} { # in these tests is ever at risk of wrapping stty rows 40 columns 500 < $spawn_out(slave,name) - if {![completion_fish_wait_ready 10]} { - unresolved {completion fish did not start} - return - } + completion_fish_wait_ready # every setup command after this point goes through completion_fish_raw completion_fish_raw {set -g fish_greeting ''} @@ -175,9 +181,7 @@ proc completion_fish_raw {line} { set spawn_id $completion_fish_spawn_id send -- "$line\r" - if {![completion_fish_wait_ready 8]} { - unresolved "completion setup command timed out: $line" - } + completion_fish_wait_ready if {$verbose > 1} { send_user "\tcompletion> $line\n" } @@ -233,7 +237,7 @@ proc completion_fish_capture {cmdline} { # clear the (uncompleted or already-completed) input line so the # session is ready to reuse send -- "\x15\r" - completion_fish_wait_ready 5 + completion_fish_wait_ready return $buf }