bash: don't apply -o filenames post-processing to non-file completions - #250
bash: don't apply -o filenames post-processing to non-file completions#250ThomasWaldmann wants to merge 5 commits into
-o filenames post-processing to non-file completions#250Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #250 +/- ##
=======================================
Coverage 85.20% 85.20%
=======================================
Files 3 3
Lines 473 473
Branches 93 93
=======================================
Hits 403 403
Misses 39 39
Partials 31 31 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Review of tqdm/shtab PR #250
I've reviewed the PR and tested the changes against the codebase. The PR does a good job of addressing the main issue for subcommands, but there is an edge case with custom completions that still suffers from the trailing slash bug.
1. What the PR does well
The PR successfully fixes issue #67 for subcommands and static option choices. By removing the global complete -o filenames flag and selectively applying compopt -o filenames when needed, it correctly prevents readline from appending a trailing slash when a subcommand matches a directory name.
2. The Edge Case (Bug in the PR)
The PR applies compopt -o filenames right before executing any current_action_compgen.
[ -n "${current_action_compgen}" ] && {
compopt -o filenames 2>/dev/null || : # bash >= 4.0; no-op outside completion
while IFS= read -r line; do COMPREPLY+=("$line"); done < <(
"${current_action_compgen}" "${completing_word}")
}While this correctly applies filename post-processing to shtab.FILE, shtab.DIRECTORY, and shtab.glob(...), it incorrectly applies it to shtab.cmd(...) and any other custom non-file compgens.
For example, shtab.cmd("git branch") is used to complete branch names. If master is a branch and there happens to be a directory named master/ in the current working directory, bash will still append a trailing slash (e.g. git checkout master/).
3. Why this happens
The obvious fix would be to move compopt -o filenames inside the specific _shtab_compgen_files function. However, because shtab executes current_action_compgen in a process substitution subshell (< <(...)), any compopt calls made inside the compgen function are lost and never reach the main shell or readline. This is why the PR author placed it outside the subshell, applying it broadly.
4. Suggested Solution
To fix this completely, we need to selectively run compopt -o filenames only for compgens that actually produce filenames.
We can do this by distinguishing the bash functions by name:
- In shtab/init.py, change
glob(...)to generate bash functions named_shtab_glob_compgen_*andcmd(...)to generate_shtab_cmd_compgen_*(currently, both use_shtab_pattern_compgen_*). - Update the bash template to conditionally apply
compoptbased on the function name:[ -n "${current_action_compgen}" ] && { if [[ "${current_action_compgen}" == "_shtab_compgen_files" || \ "${current_action_compgen}" == "_shtab_compgen_dirs" || \ "${current_action_compgen}" == _shtab_glob_compgen_* ]]; then compopt -o filenames 2>/dev/null || : fi while IFS= read -r line; do COMPREPLY+=("$line"); done < <( "${current_action_compgen}" "${completing_word}") }
This approach maintains the fix for subcommands, supports bash 4.0+ filename completion where appropriate, and perfectly fixes the trailing slash issue for custom non-file completions like shtab.cmd("git branch").
^ Review by Gemini 3.1 Pro.
|
Good catch — fixed in c2e92b4.
Verified with a new pty test ( One behavior note: fully custom ^ By Claude Fable 5. |
There was a problem hiding this comment.
Re-Review of PR #250
Thank you for implementing the suggested changes so quickly!
I have reviewed the updated PR and run the tests locally. The changes look excellent:
- Renaming the generated compgen functions cleanly distinguishes
glob(path-completing) fromcmd(non-path-completing). - The
casestatement correctly gates thecompopt -o filenamesinvocation so that it only applies to_shtab_compgen_files,_shtab_compgen_dirs, and_shtab_glob_compgen_*. - The new
test_bash_compgen_dir_collisionperfectly verifies this exact edge-case.
This successfully prevents non-path candidates from acquiring a trailing slash when matching a directory name, without regressing behavior for actual paths and subcommands.
This looks fully ready to go!
^ By Gemini.
c2e92b4 to
b451c22
Compare
| line = bash_completed_line(completion, "myprog cre", change_dir) | ||
| assert line == "myprog create ", "subcommand was completed like a directory" | ||
| # dir/file completions must still get filename treatment (trailing `/` on dirs) | ||
| line = bash_completed_line(completion, "myprog create alpha sub", change_dir) | ||
| assert line == "myprog create alpha subdir/" |
There was a problem hiding this comment.
ideally these two bash_completed_lines should be combined (similar to tcsh_candidates) to avoid multiple pty.forks
| # NOTE: _files -g '(*.txt)' doesn't work inside $() | ||
| 'zsh': "($(ls -1 *.txt 2>/dev/null ; echo hello salut hola ciao))", | ||
| # NOTE: f:{*.txt} doesn't work alongside () | ||
| 'tcsh': "(hello salut hola ciao)", |
There was a problem hiding this comment.
any ideas on how to work around these notes?
also FYI shtab.cmd("ls -1 *.txt 2>/dev/null") doesn't work on tcsh because the literal / in the command seems to confuse the logic
b451c22 to
52c2509
Compare
52c2509 to
bba95fb
Compare
Fixes tqdm#67 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Gemini 3.1 Pro <gemini-code-assist@google.com>
bba95fb to
8abfb9b
Compare
4682106 to
8abfb9b
Compare
Fixes #67.
Problem
The bash script is registered with
complete -o filenames -F ..., which makes readline apply filename post-processing to every candidate inCOMPREPLY. When a candidate happens to match an existing directory in the CWD, readline appends a trailing/and suppresses the trailing space. So a subcommandbuildcolliding with a./builddirectory completes asbuild/even though the completion function correctly returnsbuild.Fix
Register without the global
-o filenamesand enable it per-invocation viacompopt -o filenamesonly in the branches that actually complete paths:compgen -f)COMPGENs (shtab.FILE/shtab.DIRand custom compgens)This preserves the wanted behavior for file/dir completions (trailing
/on directories so recursion into subdirs works, escaping of special characters) while plain choices/subcommands/option strings no longer get filename treatment.compoptexists since bash 4.0; the call is guarded with2>/dev/null || :so it is a no-op on older bash (degrading to no filename post-processing rather than breaking) and doesn't abort scripts run underset -e(the existing redirection test executes the function non-interactively withbash -e, wherecompoptfails).Note: custom user-supplied compgens now also get filenames processing, but they already did (the flag was global before), so this is strictly narrower.
Trade-off on bash < 4.0 (e.g. the macOS system bash 3.2)
Without
compoptthere is no way to turn filename post-processing on per invocation, so on bash 3.2FILE/DIRcompletions lose it altogether rather than only the unwanted part. Measured with thetest_parserfixture, bash 3.2.57 vs 5.3.15:myprog cre(#67)create/createcreatemyprog create alpha sub(a dir)subdir/subdirsubdir/myprog create alpha with(with space.txt)with\ space.txtwith space.txtwith\ space.txtmyprog create alpha > pla(redirection)plain.txtplain.txtplain.txtNothing breaks — the script sources cleanly on 3.2 and the guarded
compoptis a silent no-op, also underset -e— and #67 is fixed there too. But directories no longer get a trailing/(so tabbing down into subdirs stops working), and, more annoyingly, a filename containing a space is inserted unquoted, which bash then parses as two arguments.If that regression for bash 3.2 users is not acceptable, the registration can branch instead, leaving bash 3.2 exactly as it is today (including #67, which is unfixable there without
compopt) and fixing 4.0+:Happy to switch to that if preferred.
Test
test_bash_subcommand_dir_collisiondrives an interactive bash through a pty (modeled on the existingtcsh_candidateshelper), since readline's post-processing happens afterCOMPREPLYand is invisible tocompgen-based tests. With directoriescreate/andsubdir/present, using the existingtest_parserfixture:myprog cre<TAB>->myprog create(was:myprog create/)myprog create alpha sub<TAB>->myprog create alpha subdir/(slash on dirs still works)Verified the test fails on
mainwithout theshtab/__init__.pychange. Existing assertions on the registration line were updated (complete -o filenames -F _shtab_shtab ...->complete -F _shtab_shtab ...).Review carefully
Made by Claude Opus 5.