diff --git a/.codespellrc b/.codespellrc index c02b0ab97..87aa2795c 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,10 +1,12 @@ [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 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/.github/workflows/completion_tests.yaml b/.github/workflows/completion_tests.yaml new file mode 100644 index 000000000..cbccf5a76 --- /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 zsh fish tcsh 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/.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..936934eb1 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 @@ -314,6 +318,7 @@ cmdModulePath cmdModulePaths cmdModuleReset cmdModuleRestore +cmdModuleSavelist cmdModuleSearch cmdModuleStash cmdModuleStashclear @@ -329,11 +334,14 @@ cmdstring codecov codespell collectModuleTag +comgen commandexp commandname compA compB compat +compdef +completionhome compilerTag compopt config @@ -357,6 +365,7 @@ dbg de defaultval defs +dejagnu del delaruelle delim @@ -431,7 +440,9 @@ findModulesInCacheFile findable fishcompletiondir flavour +flatlcompat flavours +fo fooA fooB foobin @@ -441,6 +452,7 @@ formatModuleCacheContent formatter foss fpath +freeform frontend fullname func @@ -518,6 +530,7 @@ isdefaultvalue ismodlo journalctl json +keypress kL ksh lappendConf @@ -649,6 +662,7 @@ nonpersist noop nop noparse +nospace notavail nothreads notuasked @@ -696,6 +710,7 @@ procedure's procs ps pthreads +pty pushenv py pwsh @@ -708,6 +723,7 @@ rcp reStructuredText readCollectionContent readModuleContent +readline readlink repo reportError @@ -844,6 +860,7 @@ tclxConfig tcsh tcshrc ter +testcompletion testdir testfile testfiles @@ -910,6 +927,7 @@ version's versionadded versionchanged versioncmp +versionless vimdatadir vimfiles virt @@ -928,6 +946,7 @@ xrdb xtrace zA zipball +zle zlogin zsh zshcompletiondir diff --git a/Makefile b/Makefile index 99f471824..8a198ab8d 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 $(MODULECMD) + 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..389951537 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -190,6 +190,23 @@ 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, 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 + 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: 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`) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index b3aee6535..621f12583 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,19 @@ 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, zsh, fish and tcsh, 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 +77,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 +147,58 @@ 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``, ``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 + 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``, ``031-zsh.exp``, + ``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. + Fixture and support directories ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -554,6 +608,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,21 +633,25 @@ 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 -number always expands to every file in that directory, because several of -those series are order-sensitive or enumerate a whole modulepath (see +``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 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` @@ -685,11 +744,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 +764,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/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. diff --git a/init/Makefile b/init/Makefile index e30fc1513..79e9f46d9 100644 --- a/init/Makefile +++ b/init/Makefile @@ -118,12 +118,13 @@ 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_long_opts := --debug --dumpname --help --silent --trace --verbose --version --paginate --no-pager --color --color= --width --width= --ignore-cache --ignore-user-rc +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= 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_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 @@ -131,12 +132,20 @@ 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_isavail_opts := -a -i --all --icase +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_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 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) @@ -154,6 +163,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' \ @@ -161,9 +171,17 @@ 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' \ + -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_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 9cef0eacd..160aed0ad 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 @@ -82,18 +82,25 @@ _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) + 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|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") );; - 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) @@ -107,14 +114,20 @@ _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") );; + 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") );; 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" @@ -145,18 +158,25 @@ 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) + 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|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") );; - 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) @@ -170,14 +190,20 @@ 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") );; + 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") );; 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..0dd49088d 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 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 @@ -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 @@ -29,16 +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 - return 0 - end - end - 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 + if contains -- $i swap switch rm remove delete unload return 0 end end @@ -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; \ @@ -87,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' @@ -96,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' @@ -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' @@ -181,3 +164,8 @@ 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' +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/tcsh_completion.in b/init/tcsh_completion.in index 65e4eb7b3..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@"`/' \ @@ -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@"`/' \ @@ -57,8 +57,8 @@ 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/savelist/(@comp_list_opts@)/" \ + 'n/restore/`_module_savelist; echo "@comp_restore_opts@"`/' \ + "n/savelist/(@comp_savelist_opts@)/" \ 'n/saveshow/`_module_savelist`/' \ 'n/describe/`_module_savelist`/' \ 'n/saverm/`_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@)/" \ @@ -82,12 +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/initadd/n/' \ - 'n/initprepend/n/' \ - 'n/initrm/n/' \ - 'n/initlist/n/' \ - 'n/initclear/n/' \ + 'n/source/f/' \ "n/append-path/(@comp_path_opts@)/" \ "n/prepend-path/(@comp_path_opts@)/" \ "n/remove-path/(@comp_rm_path_opts@)/" \ @@ -96,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 bb80c0836..5953e84fd 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 @@ -148,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' @@ -175,12 +178,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)' @@ -273,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=( @@ -283,8 +282,12 @@ _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' + '-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' + '--contains:Search modules whose name contains query string' ) _describe -t opts 'Switches' opts && ret=0 ;; @@ -294,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 ;; @@ -302,11 +310,16 @@ _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 ;; - (rm|del|remove|unload) + (rm|delete|remove|unload) _arguments \ '--auto[Enable automated module handling mode]' \ '--no-auto[Disable automated module handling mode]' \ @@ -314,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]' \ @@ -346,7 +375,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) @@ -378,7 +406,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) @@ -399,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/script/mt b/script/mt index fca7a9ca2..c69508919 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/007 00/008 00/010 00/011 00/020 00/030 00/040 00/050) + shift fi fi diff --git a/tcl/report.tcl.in b/tcl/report.tcl.in index 916ad13e4..5658c8680 100644 --- a/tcl/report.tcl.in +++ b/tcl/report.tcl.in @@ -2014,79 +2014,94 @@ 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: - 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) - purge Unload all loaded modulefiles - reload | update Unload then load all loaded modulefiles - switch | swap [mod1] mod2 Unload mod1 and load mod2 + 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 | delete | unload [-f] [-i] [--auto|--no-auto] modulefile [...] + Remove modulefile(s) + 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] [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] [-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 ...] + 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 [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 [-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 - 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 - 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 - 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) 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 + 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 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 @@ -2112,9 +2127,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 ':') - --ignore-cache Ignore module cache - --ignore-user-rc - Skip evaluation of user-specific module rc file + -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 @@ -2132,7 +2151,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 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..461b88f56 --- /dev/null +++ b/testsuite/completion.00-init/005-init_ts.exp @@ -0,0 +1,124 @@ +############################################################################## +# 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)" +} + +# 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)" +} + +# 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)" +} + +# 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 +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 + +# 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/006-procs.exp b/testsuite/completion.00-init/006-procs.exp new file mode 100644 index 000000000..7a499e422 --- /dev/null +++ b/testsuite/completion.00-init/006-procs.exp @@ -0,0 +1,133 @@ +############################################################################## +# 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 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 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 +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/007-module_help.exp b/testsuite/completion.00-init/007-module_help.exp new file mode 100644 index 000000000..a9ad3c92f --- /dev/null +++ b/testsuite/completion.00-init/007-module_help.exp @@ -0,0 +1,261 @@ +############################################################################## +# 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 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% +# +############################################################################## + +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 {} + +# 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 +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 {} +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' +# 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 + } + 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] { + 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/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/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..db285fa47 --- /dev/null +++ b/testsuite/completion.00-init/021-bash.exp @@ -0,0 +1,489 @@ +############################################################################## +# 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, '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) and 'module config' (see +# 008-module_config.exp) -- neither less nor more +# }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 + + +# +# 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 { + 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 { + ## '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 + } +} + +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 { + 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 dash [expr {$subcmd eq {config} ? {--} : {-}}] + set got [completion_bash_list "ml $subcmd $dash"] + 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 {$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 + } + 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 {$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 + } + 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 +# + +reset_test_env + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: 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..573f9e6b7 --- /dev/null +++ b/testsuite/completion.00-init/030-zsh-procs.exp @@ -0,0 +1,397 @@ +############################################################################## +# 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]}] + + # 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 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 + # 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: 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..844f642b0 --- /dev/null +++ b/testsuite/completion.00-init/040-fish-procs.exp @@ -0,0 +1,335 @@ +############################################################################## +# 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 +# - 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% +# +############################################################################## + +# 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 } + } +} + +# 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 + completion_fish_read_quiet +} + +# 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) + + completion_fish_wait_ready + + # 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" + completion_fish_wait_ready + 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 + + 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: 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..a72a3742d --- /dev/null +++ b/testsuite/completion.00-init/050-tcsh-procs.exp @@ -0,0 +1,263 @@ +############################################################################## +# 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" + # 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\$" { + 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: 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: