Skip to content

feat(tmux): new completion #1364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

feat(tmux): new completion #1364

wants to merge 1 commit into from

Conversation

dseomn
Copy link

@dseomn dseomn commented Apr 10, 2025

From tmux/tmux#259 it doesn't look like the tmux maintainers were interested in having a bash completion script in the upstream repo. However, I added license headers to put these new files under either bash-completion's license or tmux's, in case they want it there in the future.

I'm aware of a handful of existing tmux bash completion scripts, below. As far as I can tell, they all hard-code a decent amount of tmux's available options, commands, etc. Some are also abandoned and out of date with more recent versions of tmux. Rather than base this code off of those, I decided to implement completion using tmux's own introspection commands as much as possible. Hopefully that will reduce the ongoing maintenance work and make it stay up to date with most tmux changes automatically.

This commit has a relatively minimal set of completions, see the TODO in _comp_cmd_tmux__value(). I have code for more completions in varying states of readiness, but this commit is already pretty large. I'll make follow-up PR(s) for those.

I'm willing to maintain this script. (And I'm hoping that the design mentioned above will make that easier.)

Existing implementations that I'm aware of:

@dseomn
Copy link
Author

dseomn commented Apr 15, 2025

At a glance, the test failure looks unrelated. Let me know if I need to do anything about it?

Copy link
Collaborator

@akinomyoga akinomyoga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general quality of this PR seems very good, but I'm busy this week. I'll look at this later.

From tmux/tmux#259 it doesn't look like the tmux
maintainers were interested in having a bash completion script in the
upstream repo. However, I added license headers to put these new files
under either bash-completion's license or tmux's, in case they want it
there in the future.

I'm aware of a handful of existing tmux bash completion scripts, below.
As far as I can tell, they all hard-code a decent amount of tmux's
available options, commands, etc. Some are also abandoned and out of
date with more recent versions of tmux. Rather than base this code off
of those, I decided to implement completion using tmux's own
introspection commands as much as possible. Hopefully that will reduce
the ongoing maintenance work and make it stay up to date with most tmux
changes automatically.

This commit has a relatively minimal set of completions, see the TODO in
_comp_cmd_tmux__value(). I have code for more completions in varying
states of readiness, but this commit is already pretty large. I'll make
follow-up PR(s) for those.

I'm willing to maintain this script. (And I'm hoping that the design
mentioned above will make that easier.)

Existing implementations that I'm aware of:

* https://github.com/Bash-it/bash-it/blob/master/completion/available/tmux.completion.bash
* https://github.com/Boruch-Baum/tmux_bash_completion
* https://github.com/imomaliev/tmux-bash-completion
* scop#81
* https://github.com/srsudar/tmux-completion
@dseomn
Copy link
Author

dseomn commented Apr 15, 2025

The general quality of this PR seems very good, but I'm busy this week. I'll look at this later.

Thanks, and no rush.

(In addition to the changes in comments above, I also added a new test test_parse_usage_alternative_options.)

Copy link
Owner

@scop scop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall to me on a quick glance, thanks!

Left a few remarks, but I'll happily defer remainder of this review to someone else, as I don't use tmux myself (yet, anyway, I guess).

Comment on lines +4 to +15
# Log a message to help with debugging.
# If BASH_COMPLETION_DEBUG is set, it will be printed to stderr.
# When running with `set -x`, the _comp_cmd_tmux__log call itself will be
# printed.
#
# @param $1 Message to log
_comp_cmd_tmux__log()
{
if [[ ${BASH_COMPLETION_DEBUG-} ]]; then
printf 'tmux bash completion: %s\n' "$1" >&2
fi
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd like to see this as a general purpose public API function rather than a tmux specific one. We'd likely want to make use of it from some other completions, too.

I seem to remember there might have been some related discussion/issue/pull request lately, but can't find it right now. Could be I'm just mistaken though.

Copy link
Collaborator

@akinomyoga akinomyoga Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was #1356. Sorry, I disliked it because no code has used it in the codebase at that time.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree adding it "just in case" without any uses would have been premature. But here, we do have a use for it now.

I'm not 100% convinced the debug logging is something we need to keep around after this is in though. So I guess I'm still inclined to either generalize this, or just remove the debug logging from here before merging. No that strong opinions though, can leave it as is if others feel it's better that way. If we do, we could look into generalizing later instead of now when/if a 2nd or 3rd use pops up.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that strong opinions though, can leave it as is if others feel it's better that way. If we do, we could look into generalizing later instead of now when/if a 2nd or 3rd use pops up.

My feeling is this. For now, we can confine this function inside completions/tmux because the only user is the tmux completion. When another completion file wants to use this function, we can move the function to the main bash_completion to avoid duplicates. From the perspective of the actual use in bash-completion, one use case is enough. In that sense, now that we have one for tmux, I'm not strongly opposed to adding this function as a general utility in bash_completion. However, from the perspective of abstraction, I usually would like to see at least two use cases to consider the proper interface (which is not too tightly bound to a specific use case).

Copy link
Collaborator

@akinomyoga akinomyoga Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% convinced the debug logging is something we need to keep around after this is in though.

From an objective point of view, the logged information seems only useful for developing the completion settings, and it doesn't seem useful in the actual situation of completing the command lines. Any outputs in the middle of the programmable completion break the terminal layout, so the completion setting shouldn't output anything to the terminal.

However, this feature would be interesting to me. I have to admit that this is out of scope for this project (bash-completion), but my framework (ble.sh) may utilize this kind of information in the actual situation. If an error message is provided by the completion setting, I can technically show the message below the command line to help users understand what is wrong. (In that case, I would overwrite the _comp_log function with a ble.sh version to steal the information).

In that sense, I agree that maintaining the error detection and reporting within bash-completion seems an unnecessary complication, but at the same time, I find it interesting.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% convinced the debug logging is something we need to keep around after this is in though.

From an objective point of view, the logged information seems only useful for developing the completion settings, and it doesn't seem useful in the actual situation of completing the command lines.

While it is useful for development, that actually was not the main reason I added it. The issue template below links to guidance to use set -x, and I wanted to have the debug logs in any bug reports.

## Debug trace
<!--
See Troubleshooting section in README.md how to generate a debug trace,
copy-paste it into a separate file and attach the file here.
-->

Any outputs in the middle of the programmable completion break the terminal layout, so the completion setting shouldn't output anything to the terminal.

In general, I agree. Do you think it should be silent even when $BASH_COMPLETION_DEBUG is set though? As long as there's a way for me to see the log while developing it and when reading bug reports, I don't care too much about the specifics.

However, this feature would be interesting to me. I have to admit that this is out of scope for this project (bash-completion), but my framework (ble.sh) may utilize this kind of information in the actual situation. If an error message is provided by the completion setting, I can technically show the message below the command line to help users understand what is wrong. (In that case, I would overwrite the _comp_log function with a ble.sh version to steal the information).

Wouldn't you want different messages for that though, if they're geared more at users understanding why there's no completion, rather than developers fixing a bug? I guess that's what log levels are for in most logging frameworks. For now I'd rather just use something to help debug this completion, but I'd be fine splitting the log messages into info/warning/whatever for users and debug for developers if you or anyone else want to make the log framework support that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue template below links to guidance to use set -x, and I wanted to have the debug logs in any bug reports.

I haven't counted the situation with set -x as an actual situation of completing command lines. Debugging is a part of development. Then, questions are whether it would indeed help debugging (when the words are printed by set -x in the previous loop) and whether we should always internally process the error reporting even when the user do not have a problem (which is 99.9999% the case. We only receive several reports with the set -x log per year).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it should be silent even when $BASH_COMPLETION_DEBUG is set though?

No. BASH_COMPLETION_DEBUG is a part of development/debugging.

Wouldn't you want different messages for that though, if they're geared more at users understanding

Yes, only some of the existing messages seem to be useful for that purpose.

but I'd be fine splitting the log messages into info/warning/whatever for users and debug for developers if you or anyone else want to make the log framework support that.

I'd support the info message only after a significant number of completions support logging in a systematic way. However, that is clearly not the scope of this PR. The log level can be introduced when it becomes necessary, so I wouldn't suggest introducing it now.

case "$option_type" in
command)
_comp_compgen_split -l -- \
"$(LC_ALL=C tmux list-commands -F "#{command_list_name}")"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to pass around the original tmux command invoked instead of hardcoding just tmux from $PATH (or as an alias or a function) -- tmux might not be in $PATH (or the like) in the first place, or the tmux there might not be the one that was invoked (maybe another one with a full path to it was).

Copy link
Collaborator

@akinomyoga akinomyoga Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add more context, one simple way is to use "$1" (at the top-level function) or "${comp_args[0]}" (in the helper functions that are called from the top-level function). A more robust way is to use _comp_dequote "${comp_args[0]}" as _comp_compgen_help__get_help_lines is doing:

local REPLY
_comp_dequote "${comp_args[0]-}" || REPLY=${comp_args[0]-}
help_cmd=("${REPLY:-false}" "$@")
;;
esac
local REPLY
_comp_split -l REPLY "$(LC_ALL=C "${help_cmd[@]}" 2>&1)" &&
_lines=("${REPLY[@]}")

Note: I try to slightly change the behavior of _comp_dequote in 3e088b7 (PR #1357), so that one does not need to prefix || REPLY=${comp_args[0]-}.

Copy link
Collaborator

@akinomyoga akinomyoga Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After checking the implementation, I think maybe we can just use this function _comp_compgen_help__get_help_lines for broader purposes (with a small adjustment). Even with the current implementation, we can use _comp_compgen_help__get_help_lines for the present purpose in the following way:

_comp_compgen_help__get_help_lines -- list-commands -F "#{command_list_name}" &&
  REPLY=("${_lines[@]}") &&
  _comp_compgen -- -W '"${REPLY[@]}"'

Then, we may consider renaming this function to _comp_run_cmd and change its output variable from _lines to REPLY.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_comp_compgen_help__get_help_lines is private, so I shouldn't use it as is, right?

Should I wait for changes to _comp_compgen_help__get_help_lines and/or _comp_dequote, or use "${comp_args[0]}" / "$1" for now?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_comp_compgen_help__get_help_lines is private, so I shouldn't use it as is, right?

Yes, so I wrote

Then, we may consider renaming this function to _comp_run_cmd and change its output variable from _lines to REPLY.


Should I wait for changes to _comp_compgen_help__get_help_lines and/or _comp_dequote, or use "${comp_args[0]}" / "$1" for now?

You can change _comp_compgen_help__get_help_lines if other maintainers agree with it.

_comp_cmd_tmux__subcommand()
{
local subcommand_args=("$@")
local usage="$(LC_ALL=C tmux list-commands \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded tmux consideration as above.

_comp_initialize -- "$@" || return

local usage
usage="$(LC_ALL=C tmux -h 2>&1)"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
usage="$(LC_ALL=C tmux -h 2>&1)"
usage="$(LC_ALL=C "$1" -h 2>&1)"

# Before https://github.com/tmux/tmux/pull/4455 (merged 2025-04-09), `-h`
# produced usage information because it was an error, so we have to trim
# the error message too.
usage="${usage#$'tmux: unknown option -- h\n'}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
usage="${usage#$'tmux: unknown option -- h\n'}"
usage=${usage#$'tmux: unknown option -- h\n'}

If one double-quotes the right-hand side of an assignment, which includes $'...' inside a variable expansion, it would be affected by shopt -u extquote. (Somehow extquote of bash-5.2 doesn't work, but I think it is a 5.2 bug. You can confirm the breakage with bash <= 5.1).

The solution is just not to quote the right-hand sides of assignments. The right-hand sides are not subject to word splitting and pathname expanaions, so one usually do not have to quote the right-hand sides.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, other occurrences of double quotations for var="$(...)", var="${...}", and case "$var" in are also unnecessary. See https://github.com/scop/bash-completion/blob/main/doc/styleguide.md#quoting-of-words. We already have mixed conventions in the existing codebase, but we prefer less quoting for better highlighting support by text editors.

Comment on lines +64 to +70
local arg
for arg in "${!options[@]}"; do
_comp_cmd_tmux__log "option: ${arg} ${options["$arg"]}"
done
for arg in "${args[@]}"; do
_comp_cmd_tmux__log "arg: ${arg}"
done
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we enclose the entire loop with if [[ ${BASH_COMPLETION_DEBUG-} ]]; then? We do not need to run the loop when _comp_cmd_tmux__log is nop, which is the case in production.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/scop/bash-completion/blob/main/README.md#troubleshooting (linked from the bug report template) doesn't mention $BASH_COMPLETION_DEBUG, so that would prevent those lines from showing up in bug reports. Do you think the README should mention BASH_COMPLETION_DEBUG?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I've written in another reply, I doubt we need these in bug reports at the cost of always running loops even when it is working.

Copy link
Collaborator

@akinomyoga akinomyoga Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or another option is to use [[ ${BASH_COMPLETION_DEBUG-} || -o xtrace ]].

;;
"[-"*)
# One option that does take an argument.
if [[ ${words[i + 1]} != *"]" ]]; then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if [[ ${words[i + 1]} != *"]" ]]; then
if [[ ${words[i + 1]-} != *"]" ]]; then

Comment on lines +211 to +213
# Repeat from the beginning of args.
usage_args_index=-1
((args_index--))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about what situation this is used in. Is there a subcommand whose usage is something like tmux subcommand [<word1> <word2> <word3>] ...repeated-multiword-args...?

What are the actual tmux subcommands whose usage contain ... as a positional argument?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With tmux 3.5a:

$ tmux list-commands | grep --fixed-strings ...
display-menu (menu) [-MO] [-b border-lines] [-c target-client] [-C starting-choice] [-H selected-style] [-s style] [-S border-style] [-t target-pane][-T title] [-x position] [-y position] name key command ...
send-keys (send) [-FHKlMRX] [-c target-client] [-N repeat-count] [-t target-pane] key ...
source-file (source) [-Fnqv] [-t target-pane] path ...

I'm not too familiar with display-menu, but I think it takes repeated name key command triples. Completing the commands might be useful, but I'm not sure. send-keys can send multiple keys, though completing keyboard keys probably doesn't make sense. source-file takes multiple files, which is useful to complete.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll look at display-menu later.

Copy link
Collaborator

@akinomyoga akinomyoga Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +273 to +285
if [[ ${words[i]} =~ (\\*)\;$ ]] &&
((${#BASH_REMATCH[1]} % 4 == 1)); then
# end of current command
((subcommand_start = i + 1))
elif [[ ${words[i]} =~ (\\*)\;\'$ ]] &&
((${#BASH_REMATCH[1]} % 2 == 0)); then
# end of current command
((subcommand_start = i + 1))
elif [[ ${words[i]} =~ (\\*)\;\"$ ]] &&
(((${#BASH_REMATCH[1]} + 1) % 4 <= 1)); then
# end of current command
((subcommand_start = i + 1))
fi
Copy link
Collaborator

@akinomyoga akinomyoga Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of trying to match several possible quoting patterns, can we use _comp_dequote "${words[i]}" to obtain the unquoted value and match it with (\\*)\;$?

Copy link
Collaborator

@akinomyoga akinomyoga Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering about the descriptions in man tmux:

       As  in  these  examples,  when running tmux from the shell extra care
       must be taken to properly quote semicolons:

             1.   Semicolons that should be interpreted as a command separa‐
                  tor should be escaped according to the shell  conventions.
                  For  sh(1)  this typically means quoted (such as ‘neww ';'
                  splitw’) or escaped (such as ‘neww \\\\; splitw’).

             2.   Individual semicolons or trailing semicolons  that  should
                  be  interpreted as arguments should be escaped twice: once
                  according to the shell conventions and a second  time  for
                  tmux; for example:

                        $ tmux neww 'foo\\;' bar
                        $ tmux neww foo\\\\; bar

             3.   Semicolons  that are not individual tokens or trailing an‐
                  other token should only be escaped once according to shell
                  conventions; for example:

                        $ tmux neww 'foo-;-bar'
                        $ tmux neww foo-\\;-bar

Are the numbers of backslashes correct in the above examples? It seems to me that they should be

neww ';' splitw
neww \; splitw

$ tmux neww 'foo \;' bar
$ tmux neww foo \\\; bar

$ tmux neww 'foo-;-bar'
$ tmux neww foo-\;-bar

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of trying to match several possible quoting patterns, can we use _comp_dequote "${words[i]}" to obtain the unquoted value and match it with (\\*)\:$?

I looked at that, but the thing about $(echo xxx >/dev/tty) looked risky. Looking at it again, does that mean that it's only insecure if the user explicitly used declare -n, not for a normal environment variable? If using _comp_dequote in situations like this is the generally accepted practice, I'll switch to that.

# Note: This function allows parameter expansions as safe strings, which might
# cause unexpected results:
#
# * This allows execution of arbitrary commands through extra expansions of
# array subscripts in name references. For example,
#
# declare -n v='dummy[$(echo xxx >/dev/tty)]'
# echo "$v" # This line executes the command 'echo xxx'.
# _comp_dequote '"$v"' # This line also executes it.
#
# * This may change the internal state of the variable that has side effects.
# For example, the state of the random number generator of RANDOM can change:
#
# RANDOM=1234 # Set seed
# echo "$RANDOM" # This produces 30658.
# RANDOM=1234 # Reset seed
# _comp_dequote '"$RANDOM"' # This line changes the internal state.
# echo "$RANDOM" # This fails to reproduce 30658.
#
# We allow these parameter expansions as a part of safe strings assuming the
# referential transparency of the simple parameter expansions and the sane
# setup of the variables by the user or other frameworks that the user loads.

I'm wondering about the descriptions in man tmux:

Agreed that the escaping there doesn't look right, so I filed tmux/tmux#4480

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of trying to match several possible quoting patterns, can we use _comp_dequote "${words[i]}" to obtain the unquoted value and match it with (\\*)\:$?

I just realized another difference with that approach. That would prevent something like tmux foo "$(bar) ;" <Tab> from completing the second subcommand, right? Though I think it would be worth it to simplify the code rather than supporting cases like that which are hopefully rare.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at it again, does that mean that it's only insecure if the user explicitly used declare -n, not for a normal environment variable?

Yes.

If using _comp_dequote in situations like this is the generally accepted practice,

Many places in the bash-completion codebase still do not use _comp_dequote, but I actually want to use it in more places to complete quoted words.

Agreed that the escaping there doesn't look right, so I filed tmux/tmux#4480

Thanks!

That would prevent something like tmux foo "$(bar) ;" <Tab> from completing the second subcommand, right?

Yes, but the original heuristic list of quoting patterns is incomplete too. E.g., it doesn't work for a different quoting pattern such as cmd';', cmd";", and "$(bar)"';'. There is an unlimited number of quoting patterns. It also fails to detect ; for cmd1='command;' & tmux "$cmd1". With _comp_dequote, we consistently give up completion for words with command substitutions.

@dseomn
Copy link
Author

dseomn commented Apr 21, 2025

Thanks for the reviews! I'll try to get to these comments soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants