Skip to content

Commit ad851f4

Browse files
committed
feat(tmux): new completion
From tmux/tmux#24 it doesn't look like the tmux maintainers were interested in having a bash completion script in the upstream repo in 2019. I didn't ask again more recently, but I did make tmux/tmux#4455 and wasn't asked to include the script there. However, I added license headers to put these new files under either bash-completion's license or tmux's, in case the current or future tmux maintainers want it in tmux's repo 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. 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 b1374fc commit ad851f4

File tree

4 files changed

+461
-0
lines changed

4 files changed

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

+297
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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+
let 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+
let 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+
let 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++
198+
)); do
199+
local arg_type="${args[usage_args_index]##+(\[)}"
200+
arg_type="${arg_type%%+(\])}"
201+
if [[ $arg_type == ... ]]; then
202+
if (( usage_args_index == 0 )); then
203+
# Prevent an infinite loop.
204+
_comp_cmd_tmux__log "'tmux $subcommand' first arg is '...'"
205+
return
206+
elif (( usage_args_index != ${#args[@]} - 1 )); then
207+
_comp_cmd_tmux__log \
208+
"'tmux $subcommand' usage has '...' before last arg"
209+
return
210+
fi
211+
# Repeat from the beginning of args.
212+
usage_args_index=-1
213+
let args_index--
214+
elif [[ $arg_type == arguments ]]; then
215+
if [[ $prev_arg_type == command ]] &&
216+
(( usage_args_index == ${#args[@]} - 1))
217+
then
218+
# The usage ends in `command arguments`, so recurse to the new
219+
# subcommand.
220+
_comp_cmd_tmux__subcommand \
221+
"${subcommand_args[@]:(( args_index - 1 ))}"
222+
return
223+
else
224+
_comp_cmd_tmux__log \
225+
"'tmux $subcommand' has unsupported 'arguments' in usage"
226+
return
227+
fi
228+
elif (( args_index == ${#subcommand_args[@]} )); then
229+
# The usage arg is 1 past the end of $subcommand_args, so complete
230+
# it.
231+
_comp_cmd_tmux__value "$subcommand" "$arg_type"
232+
return
233+
fi
234+
prev_arg_type="$arg_type"
235+
done
236+
237+
_comp_cmd_tmux__log "Too many args to 'tmux $subcommand'"
238+
}
239+
240+
_comp_cmd_tmux()
241+
{
242+
local cur prev words cword comp_args
243+
_comp_initialize -- "$@" || return
244+
245+
local usage
246+
usage="$(LC_ALL=C tmux -h 2>&1)"
247+
# Before https://github.com/tmux/tmux/pull/4455 (merged 2025-04-09), `-h`
248+
# produced usage information because it was an error, so we have to trim
249+
# the error message too.
250+
usage="${usage#$'tmux: unknown option -- h\n'}"
251+
usage="${usage#usage: tmux }"
252+
253+
local -A options
254+
local -a args
255+
_comp_cmd_tmux__parse_usage "$usage"
256+
257+
local option_type
258+
local positional_start
259+
_comp_cmd_tmux__options "${words[@]:0:$cword}"
260+
261+
if [[ $option_type ]]; then
262+
_comp_cmd_tmux__value tmux "$option_type"
263+
return
264+
elif (( positional_start < 0 )) && [[ $cur == -* ]]; then
265+
_comp_compgen -U options -- -W '"${!options[@]}"'
266+
return
267+
elif (( positional_start < 0 )); then
268+
let positional_start=cword
269+
fi
270+
271+
local i
272+
local subcommand_start="$positional_start"
273+
for (( i=positional_start; i < cword; i++ )); do
274+
if [[ ${words[i]} =~ (\\*)\;$ ]] &&
275+
(( ${#BASH_REMATCH[1]} % 4 == 1 )); then
276+
# end of current command
277+
let subcommand_start=i+1
278+
elif [[ ${words[i]} =~ (\\*)\;\'$ ]] &&
279+
(( ${#BASH_REMATCH[1]} % 2 == 0 )); then
280+
# end of current command
281+
let subcommand_start=i+1
282+
elif [[ ${words[i]} =~ (\\*)\;\"$ ]] &&
283+
(( (${#BASH_REMATCH[1]} + 1) % 4 <= 1 )); then
284+
# end of current command
285+
let subcommand_start=i+1
286+
fi
287+
done
288+
289+
if (( cword == subcommand_start )); then
290+
_comp_cmd_tmux__value tmux command
291+
else
292+
_comp_cmd_tmux__subcommand \
293+
"${words[@]:$subcommand_start:(( cword - subcommand_start ))}"
294+
fi
295+
}
296+
297+
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)