From cab0a3954c900058ad3a79dab4f0a929170f643b Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Mon, 29 Dec 2025 15:56:21 +0900 Subject: [PATCH 1/5] feat: support the "startup{,-core}" directory * chore(pkg-config): expose the "startupdir" pkg-config variable Co-authored-by: Yedaya Katsman --- Makefile.am | 4 +- README.md | 5 +-- bash-completion.pc.in | 1 + bash_completion | 43 ++++++++++++++++--- configure.ac | 2 + .../000_bash_completion_compat.bash | 0 startup-core/Makefile.am | 4 ++ startup/Makefile.am | 4 ++ startup/README.md | 26 +++++++++++ test/config/bashrc | 2 +- test/t/conftest.py | 3 ++ 11 files changed, 81 insertions(+), 13 deletions(-) rename {bash_completion.d => startup-core}/000_bash_completion_compat.bash (100%) create mode 100644 startup-core/Makefile.am create mode 100644 startup/Makefile.am create mode 100644 startup/README.md diff --git a/Makefile.am b/Makefile.am index 22408098204..7a9fd13b944 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,11 +1,11 @@ SUBDIRS = doc completions-core completions-fallback helpers-core \ - test completions helpers + startup-core test completions helpers startup pkgdata_DATA = bash_completion # Empty, but here just to get the compat dir created with install compatdir = $(sysconfdir)/bash_completion.d -compat_DATA = bash_completion.d/000_bash_completion_compat.bash +compat_DATA = profiledir = $(sysconfdir)/profile.d profile_DATA = bash_completion.sh diff --git a/README.md b/README.md index 3a7baaa6d10..d1fc3581a79 100644 --- a/README.md +++ b/README.md @@ -142,9 +142,8 @@ A. Install a local completion of your own appropriately for the desired command, and it will take precedence over the one shipped by us. See the next answer for details where to install it, if you are doing it on per user basis. If you want to do it system wide, you can install eagerly loaded - files in `compatdir` (see a couple of questions further down for more - info. To get the path of `compatdir` for the current system, the output of - `pkg-config bash-completion --variable compatdir` can be used) and install a + files in ``, whose value in the current system can be retrieved + by `pkg-config bash-completion --variable=startupdir`, and install a completion for the commands to override our completion for in them. If you want to use bash's default completion instead of one of ours, diff --git a/bash-completion.pc.in b/bash-completion.pc.in index f0a3572c261..905e3869c61 100644 --- a/bash-completion.pc.in +++ b/bash-completion.pc.in @@ -5,6 +5,7 @@ sysconfdir=@sysconfdir@ compatdir=${sysconfdir}/bash_completion.d completionsdir=${datadir}/@PACKAGE@/completions helpersdir=${datadir}/@PACKAGE@/helpers +startupdir=${datadir}/@PACKAGE@/startup Name: @PACKAGE@ Description: programmable completion for the bash shell diff --git a/bash_completion b/bash_completion index e67cc3d51a4..0ed93035ec5 100644 --- a/bash_completion +++ b/bash_completion @@ -3584,7 +3584,38 @@ _comp__init_collect_startup_configs() local base_path=${1:-${BASH_SOURCE[1]}} _comp__init_startup_configs=() - # source compat completion directory definitions + # list startup directories + local -a startup_dirs=() + local paths + _comp_split -F : paths "${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}" && + startup_dirs+=("${paths[@]/%//startup}") + _comp_split -F : paths "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" && + startup_dirs+=("${paths[@]/%//bash-completion/startup}") + startup_dirs+=("$_comp__base_directory/startup") + startup_dirs+=("$_comp__base_directory/startup-core") + + # collect startup files + local -A startup_visited=() + local startup_dir startup_files + for startup_dir in "${startup_dirs[@]}"; do + [[ -d $startup_dir && -r $startup_dir && -x $startup_dir ]] || continue + _comp_expand_glob startup_files "$startup_dir/*.bash" || continue + for startup_file in "${startup_files[@]}"; do + [[ ! -d $startup_file && -r $startup_file ]] || continue + + # Disable the startup files with the same name as one of already + # selected startup files. This allows users to override a startup + # file with another file with the same name placed in an earlier + # directory in "${startup_files[@]}". + local name=${startup_file##*/?([0-9][0-9][0-9]_)} + [[ ${startup_visited[$name]-} ]] && continue + startup_visited[$name]=set + + _comp__init_startup_configs+=("$startup_file") + done + done + + # collect compat completion directory definitions local -a compat_dirs=() local compat_dir if [[ ${BASH_COMPLETION_COMPAT_DIR-} ]]; then @@ -3599,16 +3630,14 @@ _comp__init_collect_startup_configs() # functions. if [[ $_comp__base_directory == */share/bash-completion ]]; then compat_dir=${_comp__base_directory%/share/bash-completion}/etc/bash_completion.d - else - compat_dir=$_comp__base_directory/bash_completion.d + [[ ${compat_dirs[0]} == "$compat_dir" ]] || + compat_dirs+=("$compat_dir") fi - [[ ${compat_dirs[0]} == "$compat_dir" ]] || - compat_dirs+=("$compat_dir") fi for compat_dir in "${compat_dirs[@]}"; do [[ -d $compat_dir && -r $compat_dir && -x $compat_dir ]] || continue local compat_files - _comp_expand_glob compat_files '"$compat_dir"/*' + _comp_expand_glob compat_files '"$compat_dir"/*' || continue local compat_file for compat_file in "${compat_files[@]}"; do [[ ${compat_file##*/} != @($_comp_backup_glob|Makefile*|${BASH_COMPLETION_COMPAT_IGNORE-}) && @@ -3617,7 +3646,7 @@ _comp__init_collect_startup_configs() done done - # source user completion file + # collect the user completion file # # Remark: We explicitly check that $user_completion is not '/dev/null' # since /dev/null may be a regular file in broken systems and can contain diff --git a/configure.ac b/configure.ac index 59ac0d3397e..30034a635c7 100644 --- a/configure.ac +++ b/configure.ac @@ -40,6 +40,8 @@ completions-fallback/Makefile doc/Makefile helpers/Makefile helpers-core/Makefile +startup/Makefile +startup-core/Makefile test/Makefile test/t/Makefile test/t/unit/Makefile diff --git a/bash_completion.d/000_bash_completion_compat.bash b/startup-core/000_bash_completion_compat.bash similarity index 100% rename from bash_completion.d/000_bash_completion_compat.bash rename to startup-core/000_bash_completion_compat.bash diff --git a/startup-core/Makefile.am b/startup-core/Makefile.am new file mode 100644 index 00000000000..bb52fca38e4 --- /dev/null +++ b/startup-core/Makefile.am @@ -0,0 +1,4 @@ +startupcoredir = $(datadir)/$(PACKAGE)/startup-core +startupcore_DATA = 000_bash_completion_compat.bash + +EXTRA_DIST = $(startupcore_DATA) diff --git a/startup/Makefile.am b/startup/Makefile.am new file mode 100644 index 00000000000..0da318b9454 --- /dev/null +++ b/startup/Makefile.am @@ -0,0 +1,4 @@ +startupdir = $(datadir)/$(PACKAGE)/startup +startup_DATA = README.md + +EXTRA_DIST = $(startup_DATA) diff --git a/startup/README.md b/startup/README.md new file mode 100644 index 00000000000..17dd20209cf --- /dev/null +++ b/startup/README.md @@ -0,0 +1,26 @@ +# External startup directory - bash-completion + +This directory `startup` is the place to put _external startup scripts_ +for initializing [bash-completion](https://github.com/scop/bash-completion). + +The `bash-completion` framework has been providing `/etc/bash_completion.d` as +a directory to put scripts that are eagerly loaded on the initialization stage +of `bash-completion`. However, the historical purpose of +`/etc/bash_completion.d` has been the place to put completion files for +`bash-completion < 2.0`, which did not support dynamic loading through +`complete -D` (only available in `bash >= 4.1`). This original usage was +already deprecated. Apart from that, there are scripts that need to be loaded +on the initialization stage of `bash-completion`, such as +`/etc/bash_completion.d/fzf` and `/etc/bash_completion.d/_python-argcomplete`, +which want to set up a custom default completion with `complete -D`. + +In `bash-completion >= 2.18`, we started to provide the `startup` directory as +a dedicated directory for such scripts that need to be loaded on the +initialization stage. + +* Eagerly loaded scripts should be placed in this directory instead of + `/etc/bash_completion.d`. +* External completion files of API v1 should be updated to use API v2 or v3 and + should be moved to `completions/.bash`. Although we recommend to update + the completion file to use newer API, the completion file relying on API v1 + should continue to be placed in `/etc/bash_completion.d`. diff --git a/test/config/bashrc b/test/config/bashrc index d701136f383..27f1a487993 100644 --- a/test/config/bashrc +++ b/test/config/bashrc @@ -38,7 +38,7 @@ export BASH_COMPLETION_USER_FILE=/dev/null # possibly (system-)installed upstream ones. # shellcheck disable=SC2154 export BASH_COMPLETION_USER_DIR="$_comp__test_session_tmpdir/bash-completion:$_comp__test_session_tmpdir/bash-completion-fallback" -export BASH_COMPLETION_COMPAT_DIR="$SRCDIRABS/../bash_completion.d" +export BASH_COMPLETION_COMPAT_DIR="$_comp__test_session_tmpdir/bash_completion.d" export XDG_DATA_DIRS=/var/empty # Make sure default settings are in effect diff --git a/test/t/conftest.py b/test/t/conftest.py index 803d57cf69f..53421bb9cd1 100644 --- a/test/t/conftest.py +++ b/test/t/conftest.py @@ -207,6 +207,9 @@ def test_session_tmpdir(tmp_path_factory) -> Path: helpers_1.symlink_to(bash_completion_root / "helpers-core") helpers_2.symlink_to(bash_completion_root / "helpers-core") + compat_dir = tmpdir / "bash_completion.d" + compat_dir.mkdir() + # Create an empty temporary file for HISTFILE. # # To prevent the tested Bash processes from writing to the user's From 8b34beeec8c017aa802806573126ea6135ba3e50 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Mon, 29 Dec 2025 18:26:55 +0900 Subject: [PATCH 2/5] fix(compatdir): shadow compat files of the same name --- bash_completion | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bash_completion b/bash_completion index 0ed93035ec5..17b1392a01b 100644 --- a/bash_completion +++ b/bash_completion @@ -3634,6 +3634,7 @@ _comp__init_collect_startup_configs() compat_dirs+=("$compat_dir") fi fi + local -A compat_visited=() for compat_dir in "${compat_dirs[@]}"; do [[ -d $compat_dir && -r $compat_dir && -x $compat_dir ]] || continue local compat_files @@ -3641,8 +3642,17 @@ _comp__init_collect_startup_configs() local compat_file for compat_file in "${compat_files[@]}"; do [[ ${compat_file##*/} != @($_comp_backup_glob|Makefile*|${BASH_COMPLETION_COMPAT_IGNORE-}) && - -f $compat_file && -r $compat_file ]] && - _comp__init_startup_configs+=("$compat_file") + -f $compat_file && -r $compat_file ]] || continue + + # Disable the compat files with the same name as one of already + # selected ones. This allows users to override a compat file with + # another file with the same name placed in an earlier directory in + # "${compat_dirs[@]}". + local name=${compat_file##*/} + [[ ${compat_visited[$name]-} ]] && continue + compat_visited[$name]=set + + _comp__init_startup_configs+=("$compat_file") done done From 9beeedf768d86942f84ff07fded6bc3e72d176ff Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 30 Jan 2026 11:33:57 +0900 Subject: [PATCH 3/5] docs(README): reorganize Q&A for external completion settings This patch reorganizes the descriptions for installing/overriding completion settings to solve multiple issues. * First, the systemwide settings and eagerly loaded settings were conflated. The systemwide settings are not necessarily eagerly loaded completions. * The paths obtained by "pkg-config" usually point to the directory under `/usr/share`, which are the places for system packages provided by the distributions. User startup files and systemwide local settings should not be placed under the tree `/usr/local` in typical distributions or with typical package managers. The user startup files should be installed into the user's directory, and systemwide local settings should be installed in `/usr/local/share`. * We also distinguish the new startup files from the traditional eagerly loaded files --- README.md | 137 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index d1fc3581a79..64b45f54df8 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ tracing on in it before doing anything else there. ## FAQ -**Q. The bash completion code inhibits some commands from completing on +**Q1. The bash completion code inhibits some commands from completing on files with extensions that are legitimate in my environment. Do I have to disable completion for that command in order to complete on the files that I need to?** @@ -136,47 +136,85 @@ A. No. If needed just once in a while, circumvent any file type restrictions put in place by the bash completion code. If needed more regularly, see the next question: -**Q. How can I override a completion shipped by bash-completion?** - -A. Install a local completion of your own appropriately for the desired - command, and it will take precedence over the one shipped by us. See the - next answer for details where to install it, if you are doing it on per user - basis. If you want to do it system wide, you can install eagerly loaded - files in ``, whose value in the current system can be retrieved - by `pkg-config bash-completion --variable=startupdir`, and install a - completion for the commands to override our completion for in them. - - If you want to use bash's default completion instead of one of ours, - something like this should work (where `$cmd` is the command to override - completion for): `complete -o default -o bashdefault $cmd` - -**Q. Where should I install my own local completions?** - -A. Put them in the `completions` subdir of `$BASH_COMPLETION_USER_DIR` - (defaults to `$XDG_DATA_HOME/bash-completion` or - `~/.local/share/bash-completion` if `$XDG_DATA_HOME` is not set) to have - them loaded automatically on demand when the respective command is being - completed. - See also the next question's answer for considerations for these - files' names, they apply here as well. Alternatively, you can write - them directly in `~/.bash_completion` which is loaded eagerly by - our main script. - -**Q. I author/maintain package X and would like to maintain my own - completion code for this package. Where should I put it to be sure - that interactive bash shells will find it and source it?** - -A. [ Disclaimer: Here, how to make the completion code visible to - bash-completion is explained. We do not require always making the - completion code visible to bash-completion. In what condition the - completion code is installed should be determined at the author/maintainers' - own discretion. ] +**Q2. How can I override a completion shipped by bash-completion or install a + new completion for a user account?** + +A. To override a completion for a specific command, you can simply install your + own completion file for the command into an appropriate place. It will take + precedence over the one shipped by us or the one installed systemwide. To + install a completion on per user basis, put the completion file at + `/completions/.bash`, where `` is the command name and + `` is one of the user directories. The user directories are + specified by the colon-separated `$BASH_COMPLETION_USER_DIR` (defaults to + `$XDG_DATA_HOME/bash-completion` or `~/.local/share/bash-completion` if + `$XDG_DATA_HOME` is not set). They will be loaded automatically on demand + when the respective command is being completed. + + > [!NOTE] + > See also the answer to Q4 for considerations for those files' names, they + > apply here as well. + + Completion settings that should be loaded on initialization of + bash-completion should be defined in a startup completion file. A startup + completion file can be added in the directory `/startup` (see the + previous paragraph for ``). A prefix of the form + `[0-9][0-9][0-9]_` may be prepended to the filename of the startup files to + control the loading order. To override a existing startup file installed + systemwide or shipped by us, you can put your own version with the same + filename in `/startup`. In identifying the overridden startup + file, the prefixes `[0-9][0-9][0-9]_` are ignored. If you want to disable a + startup completion file, you can put an empty file. + + You can also define eagerly loaded settings in the user file + (`${BASH_COMPLETION_USER_FILE:-$HOME/.bash_completion}`). Instead of + preparing separate files for specific commands, you may write completion + settings for specific commands directly in the user file. When you want to + modify the default completion setting set by `complete -D` (which is + available in Bash >= 4.1), you can override it in the user file or in a user + startup file. If you want to use bash's default completion instead of one + of ours, in the user file or in a user startup script, you can define + something like this: + + ```bash + complete -o default -o bashdefault $cmd + ``` + + where `$cmd` is the command to override completion for. + +**Q3. How can I override a completion shipped by bash-completion systemwide?** + +A. Install a local completion appropriately for the desired command, and it + will take precedence over the one shipped by us. + + A completion file for a specific command `` can be placed at + `/completions/.bash`, where `` is + `/usr/local/share/bash-completion` if `XDG_DATA_DIRS` is not defined, or + `/bash-completion` where `` is one of the directories + listed in `XDG_DATA_DIRS`. + + > [!NOTE] + > See also the answer to Q4 for considerations for those files' names, they + > apply here as well. + + Similarly, systemwide startup files can be placed in the directory + `/startup`. + +**Q4. I author/maintain a distribution package and would like to maintain my + own completion code for this package. Where should I put it to be sure that + interactive bash shells will find it and source it?** + +A. > [!NOTE] + > Here, how to make the completion code visible to bash-completion is + > explained. We do not require always making the completion code visible to + > bash-completion. In what condition the completion code is installed + > should be determined at the author/maintainers' own discretion. Install it in one of the directories pointed to by bash-completion's - `pkgconfig` file variables. There are two alternatives: + `pkgconfig` file variables. - - The recommended directory is ``, which you can get with - `pkg-config --variable=completionsdir bash-completion`. From this + - The recommended directory for the completions of specific commands is + ``, which you can get with + `pkg-config --variable=completionsdir bash-completion`. From this directory, completions are automatically loaded on demand based on invoked commands' names, so be sure to name your completion file accordingly, and to include (for example) symbolic links in case the file provides @@ -188,6 +226,11 @@ A. [ Disclaimer: Here, how to make the completion code visible to --variable=helpersdir bash-completion`. The completion files in `` can reference the helper script `/` as `${BASH_SOURCE[0]%/*}/../helpers/`. + - The directory for startup files is ``, whose value in the + current system can be retrieved by + `pkg-config bash-completion --variable=startupdir`. A typical value is + `/usr/share/bash-completion/startup`. Typically, the startup file can be + used to override the `complete -D` settings set by bash-completion. - The other directory, which is only present for backwards compatibility and is not recommended to use, is `` (get it with `pkg-config --variable=compatdir bash-completion`). From this @@ -234,8 +277,7 @@ A. [ Disclaimer: Here, how to make the completion code visible to bash-completion is 2.12 or higher, the completion script can actually be installed to `$PREFIX/share/bash-completion/completions/` under the same installation prefix as the target program installed under `$PREFIX/bin/` or - `$PREFIX/sbin/`. For the detailed search order, see also "Q. What is the - search order for the completion file of each target command?" below. + `$PREFIX/sbin/`. For the detailed search order, see also Q10 below. Example for `Makefile.am`: @@ -250,7 +292,7 @@ A. [ Disclaimer: Here, how to make the completion code visible to install(FILES your-completion-file DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/bash-completion/completions") ``` -**Q. When completing on a symlink to a directory, bash does not append +**Q5. When completing on a symlink to a directory, bash does not append the trailing `/` and I have to hit <Tab> again. I don't like this.** @@ -263,7 +305,7 @@ A. This has nothing to do with `bash_completion`. It's the default for mark-symlinked-directories on` in your `/etc/inputrc` or `~/.inputrc` file. -**Q. Completion goes awry when I try to complete on something that contains +**Q6. Completion goes awry when I try to complete on something that contains a colon.** A. This is actually a 'feature' of bash. bash recognises a colon as @@ -291,7 +333,7 @@ A. This is actually a 'feature' of bash. bash recognises a colon as Unfortunately, there's no way to turn this off. The only thing you can do is escape the colons with a backslash. -**Q. Why is `rpm` completion so slow with `-q`?** +**Q7. Why is `rpm` completion so slow with `-q`?** A. Probably because the database is being queried every time and this uses a lot of memory. @@ -314,7 +356,7 @@ A. Probably because the database is being queried every time and this uses a unless it detects that the database has changed since the file was created, in which case it will still use the database to ensure accuracy. -**Q. bash-completion interferes with my `command_not_found_handle` function +**Q8. bash-completion interferes with my `command_not_found_handle` function (or the other way around)!** A. If your `command_not_found_handle` function is not intended to address @@ -334,7 +376,7 @@ A. If your `command_not_found_handle` function is not intended to address > context. It is safer to test `COMP_POINT` as one does not need to care > about the differences between the set and non-empty states of variables. -**Q. Can tab completion be made even easier?** +**Q9. Can tab completion be made even easier?** A. The `readline(3)` library offers a few settings that can make tab completion easier (or at least different) to use. @@ -363,7 +405,8 @@ A. The `readline(3)` library offers a few settings that can make tab This turns off the use of the internal pager when returning long completion lists. -**Q. What is the search order for the completion file of each target command?** +**Q10. What is the search order for the completion file of each target + command?** A. The completion files of commands are looked up by the shell function `_comp_load`. Here, the search order in bash-completion >= 2.18 is From 3076ecdc7a736279b716c2769440d644a60aeda4 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 31 Jan 2026 18:14:48 +0900 Subject: [PATCH 4/5] test(_comp__init_collect_startup_configs): add test --- test/config/bashrc | 6 ++++ .../host/startup/000_foo.bash | 6 ++++ .../host/startup/999_bar.bash | 6 ++++ .../host/startup/baz.bash | 6 ++++ .../host/startup/quux.bash | 5 +++ .../user/startup/000_foo.bash | 5 +++ .../user/startup/100_bar.bash | 5 +++ .../user/startup/200_baz.bash | 5 +++ .../user/startup/300_qux.bash | 5 +++ test/t/unit/Makefile.am | 1 + .../test_unit_init_collect_startup_configs.py | 36 +++++++++++++++++++ 11 files changed, 86 insertions(+) create mode 100644 test/fixtures/_comp__init_collect_startup_configs/host/startup/000_foo.bash create mode 100644 test/fixtures/_comp__init_collect_startup_configs/host/startup/999_bar.bash create mode 100644 test/fixtures/_comp__init_collect_startup_configs/host/startup/baz.bash create mode 100644 test/fixtures/_comp__init_collect_startup_configs/host/startup/quux.bash create mode 100644 test/fixtures/_comp__init_collect_startup_configs/user/startup/000_foo.bash create mode 100644 test/fixtures/_comp__init_collect_startup_configs/user/startup/100_bar.bash create mode 100644 test/fixtures/_comp__init_collect_startup_configs/user/startup/200_baz.bash create mode 100644 test/fixtures/_comp__init_collect_startup_configs/user/startup/300_qux.bash create mode 100644 test/t/unit/test_unit_init_collect_startup_configs.py diff --git a/test/config/bashrc b/test/config/bashrc index 27f1a487993..61d45a76b00 100644 --- a/test/config/bashrc +++ b/test/config/bashrc @@ -57,6 +57,12 @@ unset -v \ COMP_KNOWN_HOSTS_WITH_HOSTFILE \ COMP_TAR_INTERNAL_PATHS +if [[ $PWD == */_comp__init_collect_startup_configs ]]; then + # To test the loading order and masking of startup files, we specify + # directories containing the subdirectory "startup". + BASH_COMPLETION_USER_DIR=$PWD/user:$PWD/host:$BASH_COMPLETION_USER_DIR +fi + # @param $1 Char to add to $COMP_WORDBREAKS add_comp_wordbreak_char() { diff --git a/test/fixtures/_comp__init_collect_startup_configs/host/startup/000_foo.bash b/test/fixtures/_comp__init_collect_startup_configs/host/startup/000_foo.bash new file mode 100644 index 00000000000..4f5b2a5d017 --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/host/startup/000_foo.bash @@ -0,0 +1,6 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__foo=host:$_comp__test_startup__loading_order +_comp__test_startup__error='host/foo: should not be loaded' diff --git a/test/fixtures/_comp__init_collect_startup_configs/host/startup/999_bar.bash b/test/fixtures/_comp__init_collect_startup_configs/host/startup/999_bar.bash new file mode 100644 index 00000000000..518488a0bea --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/host/startup/999_bar.bash @@ -0,0 +1,6 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__bar=host:$_comp__test_startup__loading_order +_comp__test_startup__error='host/bar: should not be loaded' diff --git a/test/fixtures/_comp__init_collect_startup_configs/host/startup/baz.bash b/test/fixtures/_comp__init_collect_startup_configs/host/startup/baz.bash new file mode 100644 index 00000000000..1a722ed155e --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/host/startup/baz.bash @@ -0,0 +1,6 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__baz=host:$_comp__test_startup__loading_order +_comp__test_startup__error='host/baz: should not be loaded' diff --git a/test/fixtures/_comp__init_collect_startup_configs/host/startup/quux.bash b/test/fixtures/_comp__init_collect_startup_configs/host/startup/quux.bash new file mode 100644 index 00000000000..4baf0f84d6b --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/host/startup/quux.bash @@ -0,0 +1,5 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__quux=host:$_comp__test_startup__loading_order diff --git a/test/fixtures/_comp__init_collect_startup_configs/user/startup/000_foo.bash b/test/fixtures/_comp__init_collect_startup_configs/user/startup/000_foo.bash new file mode 100644 index 00000000000..a3235f9a407 --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/user/startup/000_foo.bash @@ -0,0 +1,5 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__foo=user:$_comp__test_startup__loading_order diff --git a/test/fixtures/_comp__init_collect_startup_configs/user/startup/100_bar.bash b/test/fixtures/_comp__init_collect_startup_configs/user/startup/100_bar.bash new file mode 100644 index 00000000000..88ce8a5c69e --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/user/startup/100_bar.bash @@ -0,0 +1,5 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__bar=user:$_comp__test_startup__loading_order diff --git a/test/fixtures/_comp__init_collect_startup_configs/user/startup/200_baz.bash b/test/fixtures/_comp__init_collect_startup_configs/user/startup/200_baz.bash new file mode 100644 index 00000000000..d552b520a37 --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/user/startup/200_baz.bash @@ -0,0 +1,5 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__baz=user:$_comp__test_startup__loading_order diff --git a/test/fixtures/_comp__init_collect_startup_configs/user/startup/300_qux.bash b/test/fixtures/_comp__init_collect_startup_configs/user/startup/300_qux.bash new file mode 100644 index 00000000000..81ae942ef5a --- /dev/null +++ b/test/fixtures/_comp__init_collect_startup_configs/user/startup/300_qux.bash @@ -0,0 +1,5 @@ +# startup file for bash-completion -*- shell-script -*- + +((_comp__test_startup__loading_order = ${_comp__test_startup__loading_order:-0} + 1)) + +_comp__test_startup__qux=user:$_comp__test_startup__loading_order diff --git a/test/t/unit/Makefile.am b/test/t/unit/Makefile.am index 000dd8ec636..07a425fb10a 100644 --- a/test/t/unit/Makefile.am +++ b/test/t/unit/Makefile.am @@ -28,6 +28,7 @@ EXTRA_DIST = \ test_unit_get_cword.py \ test_unit_get_first_arg.py \ test_unit_get_words.py \ + test_unit_init_collect_startup_configs.py \ test_unit_initialize.py \ test_unit_load.py \ test_unit_looks_like_path.py \ diff --git a/test/t/unit/test_unit_init_collect_startup_configs.py b/test/t/unit/test_unit_init_collect_startup_configs.py new file mode 100644 index 00000000000..e34c6faf870 --- /dev/null +++ b/test/t/unit/test_unit_init_collect_startup_configs.py @@ -0,0 +1,36 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None, cwd="_comp__init_collect_startup_configs") +class TestUnitInitCollectStartupConfigs: + def test_count(self, bash): + assert_bash_exec( + bash, + "[[ ${_comp__test_startup__loading_order-} == 5 ]]", + ) + + @pytest.mark.parametrize( + "module,expected", + [ + ("foo", "user:1"), + ("bar", "user:2"), + ("baz", "user:3"), + ("qux", "user:4"), + ("quux", "host:5"), + ], + ) + def test_order(self, bash, module, expected): + output = assert_bash_exec( + bash, + 'echo "${_comp__test_startup__%s-}"' % module, + want_output=True, + ) + assert output.strip() == expected + + def test_masking(self, bash): + output = assert_bash_exec( + bash, 'echo "${_comp__test_startup__error-}"', want_output=True + ) + assert output.strip() == "" From 6c1af0ef29bc59445d31218c598068ad4fb14a2a Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 31 Jan 2026 18:20:04 +0900 Subject: [PATCH 5/5] feat(_comp__init): source system startup files earlier --- bash_completion | 26 +++++++++++++++++-- .../test_unit_init_collect_startup_configs.py | 10 +++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/bash_completion b/bash_completion index 17b1392a01b..4588d0a30b2 100644 --- a/bash_completion +++ b/bash_completion @@ -3596,10 +3596,14 @@ _comp__init_collect_startup_configs() # collect startup files local -A startup_visited=() - local startup_dir startup_files for startup_dir in "${startup_dirs[@]}"; do [[ -d $startup_dir && -r $startup_dir && -x $startup_dir ]] || continue + + local -a startup_files=() _comp_expand_glob startup_files "$startup_dir/*.bash" || continue + + local -a selected_startup_files=() + local startup_dir for startup_file in "${startup_files[@]}"; do [[ ! -d $startup_file && -r $startup_file ]] || continue @@ -3611,8 +3615,26 @@ _comp__init_collect_startup_configs() [[ ${startup_visited[$name]-} ]] && continue startup_visited[$name]=set - _comp__init_startup_configs+=("$startup_file") + selected_startup_files+=("$startup_file") done + ((${#selected_startup_files[@]})) || continue + + # Note: bash < 4.4 has a bug that all elements are connected with + # ${a[@]+"${a[@]}"} when IFS does not contain whitespace. + local IFS=$' \t\n' + # Prepend the selected startup files because we want to source the + # system startup files earlier. + _comp__init_startup_configs=( + "${selected_startup_files[@]}" + + # Note: bash < 4.3 has a bug that "${a[@]}" fails for "set -u" when + # an array "a" exists but has no elements. We use + # ${a[@]+"${a[@]}"} as a workaround. Similar patterns are also + # used in completions/*.bash, 000_bash_completion_compat.bash, and + # test_unit_compgen.py. They need to be updated when the minimum + # required Bash version is lifted up. + ${_comp__init_startup_configs[@]+"${_comp__init_startup_configs[@]}"} + ) done # collect compat completion directory definitions diff --git a/test/t/unit/test_unit_init_collect_startup_configs.py b/test/t/unit/test_unit_init_collect_startup_configs.py index e34c6faf870..8d6aaad4360 100644 --- a/test/t/unit/test_unit_init_collect_startup_configs.py +++ b/test/t/unit/test_unit_init_collect_startup_configs.py @@ -14,11 +14,11 @@ def test_count(self, bash): @pytest.mark.parametrize( "module,expected", [ - ("foo", "user:1"), - ("bar", "user:2"), - ("baz", "user:3"), - ("qux", "user:4"), - ("quux", "host:5"), + ("quux", "host:1"), + ("foo", "user:2"), + ("bar", "user:3"), + ("baz", "user:4"), + ("qux", "user:5"), ], ) def test_order(self, bash, module, expected):