3 # Copyright (C) 2011, 2012, 2013, 2014, 2016 Joerg Jaspert <joerg@debian.org>
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # 2. Redistributions in binary form must reproduce the above copyright
12 # notice, this list of conditions and the following disclaimer in the
13 # documentation and/or other materials provided with the distribution.
15 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 # Always exit on errors
28 # Undefined variables, we don't like you
30 # ERR traps are inherited by shell functions, command substitutions and
31 # commands executed in a subshell environment.
34 ########################################################################
35 # The following variables can be overwritten outside the script.
38 # We want a useful tmpdir, so set one if it isn't already. Thats the
39 # place where tmux puts its socket, so you want to ensure it doesn't
40 # change under your feet - like for those with a daily-changing tmpdir
42 declare -r TMPDIR
=${TMPDIR:-"/tmp"}
44 # Do you want me to sort the arguments when opening an ssh/multi-ssh session?
45 # The only use of the sorted list is for the session name, to allow you to
46 # get the same session again no matter how you order the hosts on commandline.
47 declare -r TMSORT
=${TMSORT:-"true"}
49 # Want some extra options given to tmux? Define TMOPTS in your environment.
50 # Note, this is only used in the final tmux call where we actually
51 # attach to the session!
52 TMOPTS
=${TMOPTS:-"-2"}
54 # The following directory can hold session config for us, so you can use them
56 declare -r TMDIR
=${TMDIR:-"${HOME}/.tmux.d"}
58 # Should we prepend the hostname to autogenerated session names?
59 # Example: Call tm ms host1 host2.
60 # TMSESSHOST=true -> session name is HOSTNAME_host1_host2
61 # TMSESSHOST=false -> session name is host1_host2
62 declare -r TMSESSHOST
=${TMSESSHOST:-"true"}
64 # Allow to globally define a custom ssh command line.
65 TMSSHCMD
=${TMSSHCMD:-"ssh"}
68 declare -r DEBUG
=${DEBUG:-"false"}
70 # Save the last argument, it may be used (traditional style) for
75 # Where does your tmux starts numbering its windows? Mine does at 1,
76 # default for tmux is 0. We try to find it out, but if we fail, (as we
77 # only check $HOME/.tmux.conf you can set this variable to whatever it
78 # is for your environment.
79 if [[ -f ${HOME}/.tmux.conf
]]; then
80 bindex
=$
(grep ' base-index ' ${HOME}/.tmux.conf ||
echo 0 )
85 declare TMWIN
=${TMWIN:-$bindex}
88 ########################################################################
89 # Nothing below here to configure
91 # Should we group the session to another? Set to true if -g on
95 # Should we open another session, even if we already have one with
96 # this name? (Ie. second multisession to the same set of hosts)
97 # This is either set by the getopts option -n or by having -n
98 # as very first parameter after the tm command
99 if [[ $# -ge 1 ]] && [[ "${1}" = "-n" ]]; then
101 # And now get rid of it. getopts won't see it, as it was first and
102 # we remove it - but it doesn't matter, we set it already.
103 # getopts is only used if it appears somewhere else in the
110 # Store the first commandline parameter
113 # Get the tmux version and split it in major/minor
114 TMUXVERS
=$
(tmux
-V 2>/dev
/null ||
echo "tmux 1.3")
115 declare -r TMUXVERS
=${TMUXVERS##* }
116 declare -r TMUXMAJOR
=${TMUXVERS%%.*}
117 declare -r TMUXMINOR
=${TMUXVERS##*.}
120 declare -r OLDIFS
=${IFS}
122 # To save session file data
125 # Freeform .cfg file or other session file?
128 ########################################################################
130 echo "tmux helper by Joerg Jaspert <joerg@ganneff.de>"
131 echo "There are two ways to call it. Traditional and \"getopts\" style."
132 echo "Traditional call as: $0 CMD [host]...[host]"
133 echo "Getopts call as: $0 [-s host] [-m hostlist] [-k name] [-l] [-n] [-h] [-c config] [-e]"
134 echo "Note that traditional and getopts can be mixed, sometimes."
138 echo " ls List running sessions"
139 echo " s Open ssh session to host"
140 echo " ms Open multi ssh sessions to hosts, synchronizing input"
141 echo " To open a second session to the same set of hosts put a"
142 echo " -n in front of ms"
143 echo " k Kill a session. Note that this needs the exact session name"
144 echo " as shown by tm ls"
145 echo " \$anything Either plain tmux session with name of \$anything or"
146 echo " session according to TMDIR file"
148 echo "Getopts style:"
149 echo "-l List running sessions"
150 echo "-s host Open ssh session to host"
151 echo "-m hostlist Open multi ssh sessions to hosts, synchronizing input"
152 echo " Due to the way getopts works, hostlist must be enclosed in \"\""
153 echo "-n Open a second session to the same set of hosts"
154 echo "-g Group session - attach to an existing session, but keep seperate"
155 echo " window control"
156 echo "-k name Kill a session. Note that this needs the exact session name"
157 echo " as shown by tm ls"
158 echo "-c config Setup session according to TMDIR file"
159 echo "-e SESSION Use existion session named SESSION"
160 echo "-r REPLACE Value to use for replacing in session files"
163 echo "Each file in \$TMDIR defines a tmux session. There are two types of files,"
164 echo "those without an extension and those with the extension \".cfg\" (no \"\")."
165 echo "The filename corresponds to the commandline \$anything (or -c)."
167 echo "Content of extensionless files is defined as:"
168 echo " First line: Session name"
169 echo " Second line: extra tmux commandline options"
170 echo " Any following line: A hostname to open a shell with in the normal"
171 echo " ssh syntax. (ie [user@]hostname)"
173 echo "Content of .cfg files is defined as:"
174 echo " First line: Session name"
175 echo " Second line: extra tmux commandline options"
176 echo " Third line: The new-session command to use. Place NONE here if you want plain"
177 echo " defaults, though that may mean just a shell. Otherwise put the full"
178 echo " new-session command with all options you want here."
179 echo " Any following line: Any tmux command you can find in the tmux manpage."
180 echo " You should ensure that commands arrive at the right tmux session / window."
181 echo " To help you with this, there are some variables available which you"
182 echo " can use, they are replaced with values right before commands are executed:"
183 echo " SESSION - replaced with the session name"
184 echo " TMWIN - see below for explanation of TMWIN Environment variable"
186 echo "NOTE: Both types of files accept external listings of hostnames."
187 echo " That is, the output of any shell command given will be used as a list"
188 echo " of hostnames to connect to (or a set of tmux commands to run)."
190 echo "NOTE: Session files can include the Token ++TMREPLACETM++ at any point. This"
191 echo " will be replaced by the value of the -r option (if you use getopts style) or"
192 echo " by the LAST argument on the line if you use traditional calling."
193 echo " Note that with traditional calling, the argument will also be tried as a hostname,"
194 echo " so it may not make much sense there, unless using a session file that contains"
195 echo " solely of LIST commands."
197 echo "NOTE: Session files can include any existing environment variable at any point (but"
198 echo " only one per line). Those get replaced during tm execution time with the actual"
199 echo " value of the environment variable. Common usage is $HOME, but any existing var"
202 echo "Environment variables recognized by this script:"
203 echo "TMPDIR - Where tmux stores its session information"
204 echo " DEFAULT: If unset: /tmp"
205 echo "TMSORT - Should ms sort the hostnames, so it always opens the same"
206 echo " session, no matter in which order hostnames are presented"
207 echo " DEFAULT: true"
208 echo "TMOPTS - Extra options to give to the tmux call"
209 echo " Note that this ONLY affects the final tmux call to attach"
210 echo " to the session, not to the earlier ones creating it"
212 echo "TMDIR - Where are session information files stored"
213 echo " DEFAULT: ${HOME}/.tmux.d"
214 echo "TMWIN - Where does your tmux starts numbering its windows?"
215 echo " This script tries to find the information in your config,"
216 echo " but as it only checks $HOME/.tmux.conf it might fail".
217 echo " So if your window numbers start at anything different to 0,"
218 echo " like mine do at 1, then you can set TMWIN to 1"
219 echo "TMSESSHOST - Should the hostname appear in session names?"
220 echo " DEFAULT: true"
221 echo "TMSSHCMD - Allow to globally define a custom ssh command line."
222 echo " This can be just the command or any option one wishes to have"
225 echo "DEBUG - Show debug output (remember to redirect it to a file)"
230 # Simple "cleanup" of a variable, removing space and dots as we don't
231 # want them in our tmux session name
232 function clean_session
() {
233 local toclean
=${*:-""}
235 # Neither space nor dot nor : or " are friends in the SESSION name
236 toclean
=${toclean// /_}
237 toclean
=${toclean//:/_}
238 toclean
=${toclean//\"/}
242 # Merge the commandline parameters (hosts) into a usable session name
244 function ssh_sessname
() {
245 if [[ ${TMSORT} = true
]]; then
247 # get rid of first argument (s|ms), we don't want to sort this
249 local sess
=$
(for i
in $@
; do echo $i; done |
sort |
tr '\n' ' ')
250 sess
="${one} ${sess% *}"
255 clean_session
${sess}
258 # Setup functions for all tmux commands
259 function setup_command_aliases
() {
263 if [[ ${TMUXMAJOR} -lt 2 ]] ||
[[ ${TMUXMINOR} -lt 2 ]]; then
264 # Starting tmux 2.2, this is no longer needed
265 # Debian Bug #718777 - tmux needs a session to have lscm work
266 tmux new-session
-d -s ${SESNAME} -n "check" "sleep 3"
268 for command in $
(tmux list-commands|
awk '{print $1}'); do
269 eval "tm_$command() { tmux $command \"\$@\" >/dev/null; }"
271 if [[ ${TMUXMAJOR} -lt 2 ]] ||
[[ ${TMUXMINOR} -lt 2 ]]; then
272 tmux kill-session
-t ${SESNAME} || true
276 # Run a command (function) after replacing variables
280 if [[ ${cmd1} =~ ^
# ]]; then
282 elif [[ ${cmd1} =~ new-window
]]; then
283 TMWIN
=$
(( TMWIN
+ 1 ))
286 cmd
=${cmd//SESSION/$SESSION}
287 cmd
=${cmd//TMWIN/$TMWIN}
293 # Use a configuration file to setup the tmux parameters/session
294 function own_config
() {
295 if [[ ${1} =~ .cfg$
]]; then
298 # Set IFS to be NEWLINE only, not also space/tab, as our input files
299 # are \n seperated (one entry per line) and lines may well have spaces.
302 # Fill an array with our config
303 if [[ -n ${TMDATA[@]:-""} ]] && [[ ${#TMDATA[@]} -gt 0 ]]; then
304 olddata
=("${TMDATA[@]}")
307 TMDATA
=( $
(sed -e "s/++TMREPLACETM++/${TMREPARG}/g" "${TMDIR}/$1") )
311 SESSION
=${SESSION:-$(clean_session ${TMDATA[0]})}
313 if [ "${TMDATA[1]}" != "NONE" ]; then
317 # Seperate the lines we work with
319 local -a workdata
=(${TMDATA[@]:2})
322 # Lines (starting with line 3) may start with LIST, then we get
323 # the list of hosts from elsewhere. So if one does, we exec the
324 # command given, then append the output to TMDATA - while deleting
325 # the actual line with LIST in.
326 local TMPDATA
=$
(mktemp
-u -p ${TMPDIR} .tmux_tm_XXXXXXXXXX
)
327 trap "rm -f ${TMPDATA}" EXIT ERR HUP INT QUIT TERM
329 while [[ ${index} -lt ${#workdata[@]} ]]; do
330 if [[ "${workdata[${index}]}" =~ ^LIST\
(.
*)$
]]; then
331 # printf -- 'workdata: %s\n' "${workdata[@]}"
332 local cmd
=${BASH_REMATCH[1]}
333 if [[ ${cmd} =~ \$\
{([0-9a-zA-Z_]+)\
} ]]; then
334 repvar
=${BASH_REMATCH[1]}
336 cmd
=${cmd//\$\{$repvar\}/$reptext}
338 echo "Line ${index}: Fetching hostnames using provided shell command '${cmd}', please stand by..."
340 $
( ${cmd} >|
"${TMPDATA}" )
341 # Set IFS to be NEWLINE only, not also space/tab, the list may have ssh options
342 # and what not, so \n is our seperator, not more.
345 out
=( $
(tr -d '\r' < "${TMPDATA}" ) )
350 workdata
+=( "${out[@]}" )
351 unset workdata
[${index}]
353 # printf -- 'workdata: %s\n' "${workdata[@]}"
354 elif [[ "${workdata[${index}]}" =~ ^SSHCMD\
(.
*)$
]]; then
355 TMSSHCMD
=${BASH_REMATCH[1]}
357 index
=$
(( index
+ 1 ))
360 trap - EXIT ERR HUP INT QUIT TERM
361 debug
"TMDATA: ${TMDATA[@]}"
362 debug
"olddata: ${olddata[@]:-''}"
363 if [[ -n ${olddata[@]:-""} ]]; then
364 TMDATA
=( "${olddata[@]}" "${workdata[@]}" )
366 TMDATA
=( "${TMDATA[@]:0:2}" "${workdata[@]}" )
368 declare -r TMDATA
=( "${TMDATA[@]}" )
369 debug
"TMDATA now ${TMDATA[@]}"
372 # Simple overview of running sessions
373 function list_sessions
() {
375 if output
=$
(tmux list-sessions
2>/dev
/null
); then
378 echo "No tmux sessions available"
382 # We either have a debug function that shows output, or one that
384 if [[ ${DEBUG} == true
]]; then
385 eval "debug() { >&2 echo \$* ; }"
387 eval "debug() { return ; }"
390 setup_command_aliases
392 ########################################################################
393 # MAIN work follows here
394 # Check the first cmdline parameter, we might want to prepare something
401 # Yay, we want ssh to a remote host - or even a multi session setup - or kill one
402 # So we have to prepare our session name to fit in what tmux (and shell)
403 # allow us to have. And so that we can reopen an existing session, if called
404 # with the same hosts again.
405 SESSION
=$
(ssh_sessname $@
)
410 while getopts "lnhs:m:c:e:r:k:g:" OPTION
; do
417 SESSION
=$
(ssh_sessname s
${OPTARG})
422 SESSION
=$
(ssh_sessname s
${OPTARG})
426 m
) # ms (needs hostnames in "")
427 SESSION
=$
(ssh_sessname ms
${OPTARG})
428 declare -r cmdline
=ms
431 c
) # pre-defined config
434 e
) # existing session name
435 SESSION
=$
(clean_session
${OPTARG})
438 n
) # new session even if same name one already exists
444 g
) # Group session, not simple attach
445 declare -r GROUPSESSION
=true
446 SESSION
=$
(clean_session
${OPTARG})
456 # Nothing special (or something in our tmux.d)
457 if [ $# -lt 1 ]; then
458 SESSION
=${SESSION:-""}
459 if [[ -n "${SESSION}" ]]; then
460 # Environment has SESSION set, wherever from. So lets
461 # see if its an actual tmux session
462 if ! tmux has-session
-t "${SESSION}" 2>/dev
/null
; then
463 # It is not. And no argument. Show usage
469 elif [ -r "${TMDIR}/${cmdline}" ]; then
474 # Not a config file, so just session name.
481 if tmux has-session
-t ${SESSION} 2>/dev
/null
; then
484 declare -r havesession
486 # And now check if we would end up with a doubled session name.
487 # If so add something "random" to the new name, like our pid.
488 if [[ ${DOUBLENAME} == true
]] && [[ ${havesession} == true
]]; then
489 # Session exist but we are asked to open another one,
490 # so adjust our session name
491 if [[ ${#TMDATA} -eq 0 ]] && [[ ${SESSION} =~
([ms
]+)_
(.
*) ]]; then
492 SESSION
="${BASH_REMATCH[1]}_$$_${BASH_REMATCH[2]}"
494 SESSION
="$$_${SESSION}"
498 if [[ ${TMSESSHOST} = true
]]; then
499 declare -r SESSION
="$(uname -n|cut -d. -f1)_${SESSION}"
504 # We only do special work if the SESSION does not already exist.
505 if [[ ${cmdline} != k
]] && [[ ${havesession} == false
]]; then
506 # In case we want some extra things...
508 if [ $# -lt 1 ]; then
511 tm_pane_error
="create pane failed: pane too small"
514 # The user wants to open ssh to one or more hosts
515 do_cmd new-session
-d -s ${SESSION} -n "${1}" "'${TMSSHCMD} ${1}'"
516 # We disable any automated renaming, as that lets tmux set
517 # the pane title to the process running in the pane. Which
518 # means you can end up with tons of "bash". With this
519 # disabled you will have panes named after the host.
520 do_cmd set-window-option -t ${SESSION} automatic-rename off >/dev/null
521 # If we have at least tmux 1.7, allow-rename works, such also disabling
522 # any rename based on shell escape codes.
523 if [ ${TMUXMINOR//[!0-9]/} -ge 7 ] || [ ${TMUXMAJOR//[!0-9]/} -gt 1 ]; then
524 do_cmd set-window-option -t ${SESSION} allow-rename off >/dev/null
528 while [ $# -gt 0 ]; do
529 do_cmd new-window -d -t ${SESSION}:${count} -n "${1}" "${TMSSHCMD} ${1}"
530 do_cmd set-window-option -t ${SESSION}:${count} automatic-rename off >/dev/null
531 # If we have at least tmux 1.7, allow-rename works, such also disabling
532 # any rename based on shell escape codes.
533 if [ ${TMUXMINOR//[!0-9]/} -ge 7 ] || [ ${TMUXMAJOR//[!0-9]/} -gt 1 ]; then
534 do_cmd set-window-option -t ${SESSION}:${count} allow-rename off >/dev/null
536 count=$(( count + 1 ))
541 # We open a multisession window. That is, we tile the window as many times
542 # as we have hosts, display them all and have the user input send to all
544 do_cmd new-session -d -s ${SESSION} -n "Multisession" "'${TMSSHCMD} ${1}'"
546 while [ $# -gt 0 ]; do
548 output=$(do_cmd split-window -d -t ${SESSION}:${TMWIN} "'${TMSSHCMD} ${1}'" 2>&1)
551 if [[ ${ret} -ne 0 ]] && [[ ${output} == ${tm_pane_error} ]]; then
552 # No more space -> have tmux redo the
553 # layout, so all windows are evenly sized.
554 do_cmd select-layout
-t ${SESSION}:${TMWIN} main-horizontal
>/dev
/null
555 # And dont shift parameter away
560 # Now synchronize them
561 do_cmd set-window-option
-t ${SESSION}:${TMWIN} synchronize-pane
>/dev
/null
562 # And set a final layout which ensures they all have about the same size
563 do_cmd select-layout
-t ${SESSION}:${TMWIN} tiled
>/dev
/null
566 # Whatever string, so either a plain session or something from our tmux.d
567 if [ -z "${TMDATA}" ]; then
568 # the easy case, just a plain session name
569 do_cmd new-session
-d -s ${SESSION}
571 # data in our data array, the user wants his own config
572 if [[ ${TMSESCFG} = free
]]; then
573 if [[ ${TMDATA[2]} = NONE
]]; then
574 # We have a free form config where we get the actual tmux commands
575 # supplied by the user, so just issue them after creating the session.
576 do_cmd new-session
-d -s ${SESSION} -n "'${TMDATA[0]}'"
580 tmcount
=${#TMDATA[@]}
582 while [ ${index} -lt ${tmcount} ]; do
583 do_cmd
${TMDATA[$index]}
587 # So lets start with the "first" line, before dropping into a loop
588 do_cmd new-session
-d -s ${SESSION} -n "${TMDATA[0]}" "'${TMSSHCMD} ${TMDATA[2]}'"
590 tmcount=${#TMDATA[@]}
592 while [ ${index} -lt ${tmcount} ]; do
593 # List of hostnames, open a new connection per line
595 output=$(do_cmd split-window -d -t ${SESSION}:${TMWIN} "'${TMSSHCMD} ${TMDATA[$index]}'" 2>&1)
597 if [[ ${output} =~ ${tm_pane_error} ]]; then
598 # No more space -> have tmux redo the
599 # layout, so all windows are evenly sized.
600 do_cmd select-layout -t ${SESSION}:${TMWIN} main-horizontal >/dev/null
601 # And again, don't increase index
606 # Now synchronize them
607 do_cmd set-window-option -t ${SESSION}:${TMWIN} synchronize-pane >/dev/null
608 # And set a final layout which ensures they all have about the same size
609 do_cmd select-layout -t ${SESSION}:${TMWIN} tiled >/dev/null
614 # Build up new session, ensure we start in the first window
615 do_cmd select-window -t ${SESSION}:${TMWIN}
616 elif [[ ${cmdline} == k ]]; then
617 # So we are asked to kill a session
618 tokill=${SESSION//k_/}
619 do_cmd kill-session -t ${tokill}
623 # If we should group our session or not
624 if [[ ${GROUPSESSION} == true ]]; then
625 # Grouping means opening a new session, but sharing the sessions with
626 # another session (existing and new windows). But window control is separate.
627 sesname="$
$_${SESSION}"
628 tmux ${TMOPTS} new-session -s ${sesname} -t ${SESSION}
629 tmux ${TMOPTS} kill-session -t ${sesname}
631 # Do not group, just attach
632 tmux ${TMOPTS} attach -t ${SESSION}