Skip to content

Commit 96c7c62

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 90162b0 commit 96c7c62

File tree

4 files changed

+482
-0
lines changed

4 files changed

+482
-0
lines changed

completions/Makefile.am

+1
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ cross_platform = 2to3 \
446446
tcpnice \
447447
timeout \
448448
tipc \
449+
tmux \
449450
_tokio-console \
450451
tox \
451452
tracepath \

completions/tmux

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

test/t/Makefile.am

+1
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ EXTRA_DIST = \
624624
test_time.py \
625625
test_timeout.py \
626626
test_tipc.py \
627+
test_tmux.py \
627628
test_totem.py \
628629
test_touch.py \
629630
test_tox.py \

0 commit comments

Comments
 (0)