Skip to content

Commit c1e913e

Browse files
committed
feat(tmux): new completion
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 * #81 * https://github.com/srsudar/tmux-completion
1 parent b1374fc commit c1e913e

File tree

4 files changed

+464
-0
lines changed

4 files changed

+464
-0
lines changed

completions/Makefile.am

+1
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ cross_platform = 2to3 \
445445
tcpnice \
446446
timeout \
447447
tipc \
448+
tmux \
448449
_tokio-console \
449450
tox \
450451
tracepath \

completions/tmux

+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# SPDX-License-Identifier: GPL-2.0-or-later OR ISC
2+
3+
# Log a message to help with debugging.
4+
# If BASH_COMPLETION_DEBUG is set, it will be printed to stderr.
5+
# When running with `set -x`, the _comp_cmd_tmux__log call itself will be
6+
# printed.
7+
#
8+
# @param $1 Message to log
9+
_comp_cmd_tmux__log()
10+
{
11+
if [[ ${BASH_COMPLETION_DEBUG-} ]]; then
12+
printf 'tmux bash completion: %s\n' "$1" >&2
13+
fi
14+
}
15+
16+
# Parse usage output from tmux.
17+
#
18+
# @param $1 Usage from tmux, not including the (sub)command name.
19+
# @var[out] options associative array mapping options to their value types, or
20+
# to the empty string if the option doesn't take a value
21+
# @var[out] args indexed array of positional arg types
22+
_comp_cmd_tmux__parse_usage()
23+
{
24+
options=()
25+
args=()
26+
27+
local i j
28+
local words
29+
_comp_split words "$1"
30+
for ((i = 0; i < ${#words[@]}; i++)); do
31+
case "${words[i]}" in
32+
"[-"*"]")
33+
# One or more options that don't take arguments, either of the
34+
# form `[-abc]` or `[-a|-b|-c]`
35+
for ((j = 2; j < ${#words[i]} - 1; j++)); do
36+
if [[ ${words[i]:j:1} != [-\|] ]]; then
37+
options+=(["-${words[i]:j:1}"]="")
38+
fi
39+
done
40+
;;
41+
"[-"*)
42+
# One option that does take an argument.
43+
if [[ ${words[i + 1]} != *"]" ]]; then
44+
_comp_cmd_tmux__log \
45+
"Can't parse option: '${words[*]:i:2}' in '$1'"
46+
break
47+
fi
48+
options+=(["${words[i]#"["}"]="${words[i + 1]%"]"}")
49+
((i++))
50+
;;
51+
-*)
52+
_comp_cmd_tmux__log "Can't parse option '${words[i]}' in '$1'"
53+
break
54+
;;
55+
*)
56+
# Start of positional arguments.
57+
args=("${words[@]:i}")
58+
break
59+
;;
60+
esac
61+
done
62+
63+
local arg
64+
for arg in "${!options[@]}"; do
65+
_comp_cmd_tmux__log "option: ${arg} ${options["$arg"]}"
66+
done
67+
for arg in "${args[@]}"; do
68+
_comp_cmd_tmux__log "arg: ${arg}"
69+
done
70+
}
71+
72+
# Complete a value either as the argument to an option or as a positional arg.
73+
#
74+
# @param $1 subcommand that the value is for, or 'tmux' if it's top-level
75+
# @param $2 type of the value, from _comp_cmd_tmux__parse_usage()
76+
_comp_cmd_tmux__value()
77+
{
78+
local subcommand="$1" option_type="$2"
79+
_comp_cmd_tmux__log \
80+
"Trying to complete '$option_type' for subcommand '$subcommand'"
81+
82+
# To get a list of these argument types, look at `tmux -h` and:
83+
#
84+
# tmux list-commands -F "#{command_list_usage}" |
85+
# sed 's/[][ ]/\n/g' |
86+
# grep -v ^- |
87+
# sort -u
88+
#
89+
# TODO: Complete more option types.
90+
case "$option_type" in
91+
command)
92+
_comp_compgen_split -l -- \
93+
"$(LC_ALL=C tmux list-commands -F "#{command_list_name}")"
94+
;;
95+
directory | *-directory)
96+
_comp_compgen_filedir -d
97+
;;
98+
file | *-file | path | *-path)
99+
_comp_compgen_filedir
100+
;;
101+
esac
102+
}
103+
104+
# Parse command line options to tmux or a subcommand.
105+
#
106+
# @param $@ args to tmux or a subcommand, starting with the (sub)command
107+
# itself, ending before the current word to complete
108+
# @var[in] options from _comp_cmd_tmux__parse_usage()
109+
# @var[out] option_type if the word to complete is the value of an option, this
110+
# is the type of that value, otherwise it's empty
111+
# @var[out] positional_start if option_type is empty, index in $@ of the first
112+
# positional argument, or the last index plus 1 if the next word is the
113+
# first positional argument, or -1 if the next word could be either the
114+
# first positional argument or another option
115+
_comp_cmd_tmux__options()
116+
{
117+
local command_args=("$@")
118+
option_type=""
119+
positional_start=-1
120+
121+
local i
122+
for ((i = 1; i < ${#command_args[@]}; i++)); do
123+
if [[ $option_type ]]; then
124+
# arg to the previous option
125+
option_type=""
126+
elif [[ ${command_args[i]} == -- ]]; then
127+
option_type=""
128+
((positional_start = i + 1))
129+
return
130+
elif [[ ${command_args[i]} == -?* ]]; then
131+
# 1 or more options, possibly also with the value of an option.
132+
# E.g., if `-a` and `-b` take no values and `-c` does, `-ab` would
133+
# be equivalent to `-a -b` and `-acb` would be `-a` and `-c` with a
134+
# value of `b`.
135+
local j
136+
for ((j = 1; j < ${#command_args[i]}; j++)); do
137+
if [[ $option_type ]]; then
138+
# arg has both the option and its value
139+
option_type=""
140+
break
141+
fi
142+
option_type="${options["-${command_args[i]:j:1}"]-}"
143+
done
144+
else
145+
# first positional arg
146+
((positional_start = i))
147+
return
148+
fi
149+
done
150+
}
151+
152+
# Complete arguments to a subcommand.
153+
#
154+
# @param $@ the subcommand followed by its args, ending before the current word
155+
# to complete
156+
_comp_cmd_tmux__subcommand()
157+
{
158+
local subcommand_args=("$@")
159+
local usage="$(LC_ALL=C tmux list-commands \
160+
-F "#{command_list_name} #{command_list_usage}" -- "$1" 2>/dev/null)"
161+
if [[ ! $usage ]]; then
162+
_comp_cmd_tmux__log "Unknown tmux subcommand: '$1'"
163+
return
164+
fi
165+
local subcommand="${usage%% *}" # not $1, because it could be an alias
166+
_comp_cmd_tmux__log "Attempting completion for 'tmux $subcommand'"
167+
168+
local -A options
169+
local -a args
170+
_comp_cmd_tmux__parse_usage "${usage#* }"
171+
172+
local option_type
173+
local positional_start
174+
_comp_cmd_tmux__options "${subcommand_args[@]}"
175+
176+
if [[ $option_type ]]; then
177+
_comp_cmd_tmux__value "$subcommand" "$option_type"
178+
return
179+
elif ((positional_start < 0)) && [[ $cur == -* ]]; then
180+
_comp_compgen -U options -- -W '"${!options[@]}"'
181+
return
182+
elif ((positional_start < 0)); then
183+
# $cur (one past the end of subcommand_args) is the first positional
184+
# arg
185+
positional_start=${#subcommand_args[@]}
186+
fi
187+
188+
_comp_cmd_tmux__log \
189+
"'tmux $subcommand' first positional arg: '${subcommand_args[positional_start]-}'"
190+
191+
local args_index="$positional_start"
192+
local usage_args_index
193+
local prev_arg_type=""
194+
for ((\
195+
usage_args_index = 0; \
196+
usage_args_index < ${#args[@]}; \
197+
args_index++, usage_args_index++)); do
198+
local arg_type="${args[usage_args_index]##+(\[)}"
199+
arg_type="${arg_type%%+(\])}"
200+
if [[ $arg_type == ... ]]; then
201+
if ((usage_args_index == 0)); then
202+
# Prevent an infinite loop.
203+
_comp_cmd_tmux__log "'tmux $subcommand' first arg is '...'"
204+
return
205+
elif ((usage_args_index != ${#args[@]} - 1)); then
206+
_comp_cmd_tmux__log \
207+
"'tmux $subcommand' usage has '...' before last arg"
208+
return
209+
fi
210+
# Repeat from the beginning of args.
211+
usage_args_index=-1
212+
((args_index--))
213+
elif [[ $arg_type == arguments ]]; then
214+
if [[ $prev_arg_type == command ]] &&
215+
((usage_args_index == ${#args[@]} - 1)); then
216+
# The usage ends in `command arguments`, so recurse to the new
217+
# subcommand.
218+
_comp_cmd_tmux__subcommand \
219+
"${subcommand_args[@]:args_index-1}"
220+
return
221+
else
222+
_comp_cmd_tmux__log \
223+
"'tmux $subcommand' has unsupported 'arguments' in usage"
224+
return
225+
fi
226+
elif ((args_index == ${#subcommand_args[@]})); then
227+
# The usage arg is 1 past the end of $subcommand_args, so complete
228+
# it.
229+
_comp_cmd_tmux__value "$subcommand" "$arg_type"
230+
return
231+
fi
232+
prev_arg_type="$arg_type"
233+
done
234+
235+
_comp_cmd_tmux__log "Too many args to 'tmux $subcommand'"
236+
}
237+
238+
_comp_cmd_tmux()
239+
{
240+
local cur prev words cword comp_args
241+
_comp_initialize -- "$@" || return
242+
243+
local usage
244+
usage="$(LC_ALL=C tmux -h 2>&1)"
245+
# Before https://github.com/tmux/tmux/pull/4455 (merged 2025-04-09), `-h`
246+
# produced usage information because it was an error, so we have to trim
247+
# the error message too.
248+
usage="${usage#$'tmux: unknown option -- h\n'}"
249+
usage="${usage#usage: tmux }"
250+
251+
local -A options
252+
local -a args
253+
_comp_cmd_tmux__parse_usage "$usage"
254+
255+
local option_type
256+
local positional_start
257+
_comp_cmd_tmux__options "${words[@]:0:cword}"
258+
259+
if [[ $option_type ]]; then
260+
_comp_cmd_tmux__value tmux "$option_type"
261+
return
262+
elif ((positional_start < 0)) && [[ $cur == -* ]]; then
263+
_comp_compgen -U options -- -W '"${!options[@]}"'
264+
return
265+
elif ((positional_start < 0)); then
266+
((positional_start = cword))
267+
fi
268+
269+
local i
270+
local subcommand_start="$positional_start"
271+
for ((i = positional_start; i < cword; i++)); do
272+
if [[ ${words[i]} =~ (\\*)\;$ ]] &&
273+
((${#BASH_REMATCH[1]} % 4 == 1)); then
274+
# end of current command
275+
((subcommand_start = i + 1))
276+
elif [[ ${words[i]} =~ (\\*)\;\'$ ]] &&
277+
((${#BASH_REMATCH[1]} % 2 == 0)); then
278+
# end of current command
279+
((subcommand_start = i + 1))
280+
elif [[ ${words[i]} =~ (\\*)\;\"$ ]] &&
281+
(((${#BASH_REMATCH[1]} + 1) % 4 <= 1)); then
282+
# end of current command
283+
((subcommand_start = i + 1))
284+
fi
285+
done
286+
287+
if ((cword == subcommand_start)); then
288+
_comp_cmd_tmux__value tmux command
289+
else
290+
_comp_cmd_tmux__subcommand \
291+
"${words[@]:subcommand_start:cword-subcommand_start}"
292+
fi
293+
}
294+
295+
complete -F _comp_cmd_tmux tmux

test/t/Makefile.am

+1
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ EXTRA_DIST = \
623623
test_time.py \
624624
test_timeout.py \
625625
test_tipc.py \
626+
test_tmux.py \
626627
test_totem.py \
627628
test_touch.py \
628629
test_tox.py \

0 commit comments

Comments
 (0)