Skip to content

Commit 8b935e1

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 * scop#81 * https://github.com/srsudar/tmux-completion
1 parent 90162b0 commit 8b935e1

File tree

4 files changed

+470
-0
lines changed

4 files changed

+470
-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

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