Sync changes from franck
[misc.git] / bin / tm
1 #!/bin/bash
2
3 # Copyright (C) 2011, 2012, 2013, 2014 Joerg Jaspert <joerg@debian.org>
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 # .
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.
14 # .
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.
25
26 # Always exit on errors
27 set -e
28 # Undefined variables, we don't like you
29 set -u
30 # ERR traps are inherited by shell functions, command substitutions and
31 # commands executed in a subshell environment.
32 set -E
33
34 ########################################################################
35 # The following variables can be overwritten outside the script.
36 #
37
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
41 # in their home...
42 declare -r TMPDIR=${TMPDIR:-"/tmp"}
43
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"}
48
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"}
53
54 # The following directory can hold session config for us, so you can use them
55 # as a shortcut.
56 declare -r TMDIR=${TMDIR:-"${HOME}/.tmux.d"}
57
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"}
63
64 # Allow to globally define a custom ssh command line.
65 TMSSHCMD=${TMSSHCMD:-"ssh"}
66
67 # Save the last argument, it may be used (traditional style) for
68 # replacing
69 args=$#
70 TMREPARG=${!args}
71
72 # Where does your tmux starts numbering its windows? Mine does at 1,
73 # default for tmux is 0. We try to find it out, but if we fail, (as we
74 # only check $HOME/.tmux.conf you can set this variable to whatever it
75 # is for your environment.
76 if [[ -f ${HOME}/.tmux.conf ]]; then
77     bindex=$(grep ' base-index ' ${HOME}/.tmux.conf || echo 0 )
78     bindex=${bindex//* }
79 else
80     bindex=0
81 fi
82 declare -r TMWIN=${TMWIN:-$bindex}
83 unset bindex
84
85 ########################################################################
86 # Nothing below here to configure
87
88 # Should we open another session, even if we already have one with
89 # this name? (Ie. second multisession to the same set of hosts)
90 # This is either set by the getopts option -n or by having -n
91 # as very first parameter after the tm command
92 if [[ $# -ge 1 ]] && [[ "${1}" = "-n" ]]; then
93     DOUBLENAME=true
94     # And now get rid of it. getopts won't see it, as it was first and
95     # we remove it - but it doesn't matter, we set it already.
96     # getopts is only used if it appears somewhere else in the
97     # commandline
98     shift
99 else
100     DOUBLENAME=false
101 fi
102
103 # Store the first commandline parameter
104 cmdline=${1:-""}
105
106 # Get the tmux version and split it in major/minor
107 TMUXVERS=$(tmux -V 2>/dev/null || echo "tmux 1.3")
108 declare -r TMUXVERS=${TMUXVERS##* }
109 declare -r TMUXMAJOR=${TMUXVERS%%.*}
110 declare -r TMUXMINOR=${TMUXVERS##*.}
111
112 # Save IFS
113 declare -r OLDIFS=${IFS}
114
115 # To save session file data
116 TMDATA=""
117
118 # Freeform .cfg file or other session file?
119 TMSESCFG=""
120
121 ########################################################################
122 function usage() {
123     echo "tmux helper by Joerg Jaspert <joerg@ganneff.de>"
124     echo "There are two ways to call it. Traditional and \"getopts\" style."
125     echo "Traditional call as: $0 CMD [host]...[host]"
126     echo "Getopts call as: $0 [-s host] [-m hostlist] [-l] [-n] [-h] [-c config] [-e]"
127     echo ""
128     echo "Traditional:"
129     echo "CMD is one of"
130     echo " ls          List running sessions"
131     echo " s           Open ssh session to host"
132     echo " ms          Open multi ssh sessions to hosts, synchronizing input"
133     echo "             To open a second session to the same set of hosts put a"
134     echo "             -n in front of ms"
135     echo " \$anything  Either plain tmux session with name of \$anything or"
136     echo "             session according to TMDIR file"
137     echo ""
138     echo "Getopts style:"
139     echo "-l           List running sessions"
140     echo "-s host      Open ssh session to host"
141     echo "-m hostlist  Open multi ssh sessions to hosts, synchronizing input"
142     echo "             Due to the way getopts works, hostlist must be enclosed in \"\""
143     echo "-n           Open a second session to the same set of hosts"
144     echo "-c config    Setup session according to TMDIR file"
145     echo "-e SESSION   Use existion session named SESSION"
146     echo "-r REPLACE   Value to use for replacing in session files"
147     echo ""
148     echo "TMDIR file:"
149     echo "Each file in \$TMDIR defines a tmux session. There are two types of files,"
150     echo "those without an extension and those with the extension \".cfg\" (no \"\")."
151     echo "The filename corresponds to the commandline \$anything (or -c)."
152     echo ""
153     echo "Content of extensionless files is defined as:"
154     echo "  First line: Session name"
155     echo "  Second line: extra tmux commandline options"
156     echo "  Any following line: A hostname to open a shell with in the normal"
157     echo "                      ssh syntax. (ie [user@]hostname)"
158     echo ""
159     echo "Content of .cfg files is defined as:"
160     echo "  First line: Session name"
161     echo "  Second line: extra tmux commandline options"
162     echo "  Third line: The new-session command to use. Place NONE here if you want plain"
163     echo "              defaults, though that may mean just a shell. Otherwise put the full"
164     echo "              new-session command with all options you want here."
165     echo "  Any following line: Any tmux command you can find in the tmux manpage."
166     echo "              You should ensure that commands arrive at the right tmux session / window."
167     echo "              To help you with this, there are some variables available which you"
168     echo "              can use, they are replaced with values right before commands are executed:"
169     echo "              SESSION - replaced with the session name"
170     echo "              TMWIN   - see below for explanation of TMWIN Environment variable"
171     echo ""
172     echo "NOTE: Both types of files accept external listings of hostnames."
173     echo "      That is, the output of any shell command given will be used as a list"
174     echo "      of hostnames to connect to (or a set of tmux commands to run)."
175     echo ""
176     echo "NOTE: Session files can include the Token ++TMREPLACETM++ at any point. This"
177     echo "      will be replaced by the value of the -r option (if you use getopts style) or"
178     echo "      by the LAST argument on the line if you use traditional calling."
179     echo "      Note that with traditional calling, the argument will also be tried as a hostname,"
180     echo "      so it may not make much sense there, unless using a session file that contains"
181     echo "      solely of LIST commands."
182     echo ""
183     echo "Environment variables recognized by this script:"
184     echo "TMPDIR     - Where tmux stores its session information"
185     echo "             DEFAULT: If unset: /tmp"
186     echo "TMSORT     - Should ms sort the hostnames, so it always opens the same"
187     echo "             session, no matter in which order hostnames are presented"
188     echo "             DEFAULT: true"
189     echo "TMOPTS     - Extra options to give to the tmux call"
190     echo "             Note that this ONLY affects the final tmux call to attach"
191     echo "             to the session, not to the earlier ones creating it"
192     echo "             DEFAULT: -2"
193     echo "TMDIR      - Where are session information files stored"
194     echo "             DEFAULT: ${HOME}/.tmux.d"
195     echo "TMWIN      - Where does your tmux starts numbering its windows?"
196     echo "             This script tries to find the information in your config,"
197     echo "             but as it only checks $HOME/.tmux.conf it might fail".
198     echo "             So if your window numbers start at anything different to 0,"
199     echo "             like mine do at 1, then you can set TMWIN to 1"
200     echo "TMSESSHOST - Should the hostname appear in session names?"
201     echo "             DEFAULT: true"
202     echo "TMSSHCMD   - Allow to globally define a custom ssh command line."
203     echo "             This can be just the command or any option one wishes to have"
204     echo "             everywhere."
205     echo "             DEFAULT: ssh"
206     echo ""
207     exit 42
208 }
209
210 # Simple "cleanup" of a variable, removing space and dots as we don't
211 # want them in our tmux session name
212 function clean_session() {
213     local toclean=${*:-""}
214
215     # Neither space nor dot nor : or " are friends in the SESSION name
216     toclean=${toclean// /_}
217     toclean=${toclean//:/_}
218     toclean=${toclean//\"/}
219     echo ${toclean//./_}
220 }
221
222 # Merge the commandline parameters (hosts) into a usable session name
223 # for tmux
224 function ssh_sessname() {
225     if [[ ${TMSORT} = true ]]; then
226         local one=$1
227         # get rid of first argument (s|ms), we don't want to sort this
228         shift
229         local sess=$(for i in $*; do echo $i; done | sort | tr '\n' ' ')
230         sess="${one} ${sess% *}"
231     else
232         # no sorting wanted
233         local sess="${*}"
234     fi
235     clean_session ${sess}
236 }
237
238 # Setup functions for all tmux commands
239 function setup_command_aliases() {
240     local command
241     local SESNAME
242     SESNAME="tmlscm$$"
243     # Debian Bug #718777 - tmux needs a session to have lscm work
244     tmux new-session -d -s ${SESNAME} -n "check" "sleep 3"
245     for command in $(tmux list-commands|awk '{print $1}'); do
246         eval "$(echo "tm_$command() { tmux $command \"\$@\" >/dev/null; }")"
247     done
248     tmux kill-session -t ${SESNAME} || true
249 }
250
251 # Run a command (function) after replacing variables
252 function do_cmd() {
253     local cmd=$@
254     cmd=${cmd//SESSION/$SESSION}
255     cmd=${cmd//TMWIN/$TMWIN}
256     cmd1=${cmd%% *}
257     cmd=${cmd/$cmd1 /}
258     eval tm_$cmd1 $cmd
259 }
260
261 # Use a configuration file to setup the tmux parameters/session
262 function own_config() {
263     if [[ ${1} =~ .cfg$ ]]; then
264         TMSESCFG="free"
265         setup_command_aliases
266     fi
267
268     # Set IFS to be NEWLINE only, not also space/tab, as our input files
269     # are \n seperated (one entry per line) and lines may well have spaces.
270     local IFS="
271 "
272     # Fill an array with our config
273     TMDATA=( $(cat "${TMDIR}/$1" | sed -e "s/++TMREPLACETM++/${TMREPARG}/g") )
274     # Restore IFS
275     IFS=${OLDIFS}
276
277     SESSION=$(clean_session ${TMDATA[0]})
278
279     if [ "${TMDATA[1]}" != "NONE" ]; then
280         TMOPTS=${TMDATA[1]}
281     fi
282
283     # Seperate the lines we work with
284     local IFS=""
285     local -a workdata=(${TMDATA[@]:2})
286     IFS=${OLDIFS}
287
288     # Lines (starting with line 3) may start with LIST, then we get
289     # the list of hosts from elsewhere. So if one does, we exec the
290     # command given, then append the output to TMDATA - while deleting
291     # the actual line with LIST in.
292
293     local TMPDATA=$(mktemp -u -p ${TMPDIR} .tmux_tm_XXXXXXXXXX)
294     trap "rm -f ${TMPDATA}" EXIT ERR HUP INT QUIT TERM
295
296     local index=0
297     while [[ ${index} -lt ${#workdata[@]} ]]; do
298         if [[ "${workdata[${index}]}" =~ ^LIST\ (.*)$ ]]; then
299             # printf -- 'workdata: %s\n' "${workdata[@]}"
300             local cmd=${BASH_REMATCH[1]}
301             echo "Line ${index}: Fetching hostnames using provided shell command '${cmd}', please stand by..."
302
303             $( ${cmd} >| "${TMPDATA}" )
304
305             # Set IFS to be NEWLINE only, not also space/tab, the list may have ssh options
306             # and what not, so \n is our seperator, not more.
307             IFS="
308 "
309             out=( $(cat "${TMPDATA}") )
310             # Restore IFS
311             IFS=${OLDIFS}
312
313             workdata=( "${workdata[@]}" "${out[@]}" )
314             unset workdata[${index}]
315             unset out
316             # printf -- 'workdata: %s\n' "${workdata[@]}"
317         elif [[ "${workdata[${index}]}" =~ ^SSHCMD\ (.*)$ ]]; then
318             TMSSHCMD=${BASH_REMATCH[1]}
319         fi
320         index=$(( index + 1 ))
321     done
322     rm -f "${TMPDATA}"
323     trap - EXIT ERR HUP INT QUIT TERM
324     TMDATA=( "${TMDATA[@]:0:2}" "${workdata[@]}"  )
325 }
326
327 # Simple overview of running sessions
328 function list_sessions() {
329     local IFS=""
330     if output=$(tmux list-sessions 2>/dev/null); then
331         echo $output
332     else
333         echo "No tmux sessions available"
334     fi
335 }
336
337 ########################################################################
338 # MAIN work follows here
339 # Check the first cmdline parameter, we might want to prepare something
340 case ${cmdline} in
341     ls)
342         list_sessions
343         exit 0
344         ;;
345     s|ms)
346         # Yay, we want ssh to a remote host - or even a multi session setup
347         # So we have to prepare our session name to fit in what tmux (and shell)
348         # allow us to have. And so that we can reopen an existing session, if called
349         # with the same hosts again.
350         SESSION=$(ssh_sessname $@)
351         declare -r cmdline
352         shift
353         ;;
354     -*)
355         while getopts "lnhs:m:c:e:r:" OPTION; do
356             case ${OPTION} in
357                 l) # ls
358                     list_sessions
359                     exit 0
360                     ;;
361                 s) # ssh
362                     SESSION=$(ssh_sessname s ${OPTARG})
363                     declare -r cmdline=s
364                     shift
365                     ;;
366                 m) # ms (needs hostnames in "")
367                     SESSION=$(ssh_sessname ms ${OPTARG})
368                     declare -r cmdline=ms
369                     shift
370                     ;;
371                 c) # pre-defined config
372                     own_config ${OPTARG}
373                     ;;
374                 e) # existing session name
375                     SESSION=$(clean_session ${OPTARG})
376                     ;;
377                 n) # new session even if same name one already exists
378                     DOUBLENAME=true
379                     ;;
380                 r) # replacement arg
381                     TMREPARG=${OPTARG}
382                     ;;
383                 h)
384                     usage
385                     ;;
386             esac
387         done
388         ;;
389     *)
390         # Nothing special (or something in our tmux.d)
391         if [ $# -lt 1 ]; then
392             SESSION=${SESSION:-""}
393             if [[ -n "${SESSION}" ]]; then
394                 # Environment has SESSION set, wherever from. So lets
395                 # see if its an actual tmux session
396                 if ! tmux has-session -t "${SESSION}" 2>/dev/null; then
397                     # It is not. And no argument. Show usage
398                     usage
399                 fi
400             else
401                 usage
402             fi
403         elif [ -r "${TMDIR}/${cmdline}" ]; then
404             own_config $1
405         else
406             # Not a config file, so just session name.
407             SESSION=${cmdline}
408         fi
409         ;;
410 esac
411
412 # And now check if we would end up with a doubled session name.
413 # If so add something "random" to the new name, like our pid.
414 if [[ ${DOUBLENAME} == true ]] && tmux has-session -t ${SESSION} 2>/dev/null; then
415     # Session exist but we are asked to open another one,
416     # so adjust our session name
417     if [[ ${#TMDATA} -eq 0 ]] && [[ ${SESSION} =~ ([ms]+)_(.*) ]]; then
418         SESSION="${BASH_REMATCH[1]}_$$_${BASH_REMATCH[2]}"
419     else
420         SESSION="$$_${SESSION}"
421     fi
422 fi
423
424 if [[ ${TMSESSHOST} = true ]]; then
425     declare -r SESSION="$(uname -n|cut -d. -f1)_${SESSION}"
426 else
427     declare -r SESSION
428 fi
429
430 # We only do special work if the SESSION does not already exist.
431 if ! tmux has-session -t ${SESSION} 2>/dev/null; then
432     # In case we want some extra things...
433     # Check stupid users
434     if [ $# -lt 1 ]; then
435         usage
436     fi
437     case ${cmdline} in
438         s)
439             # The user wants to open ssh to one or more hosts
440             tmux new-session -d -s ${SESSION} -n "${1}" "${TMSSHCMD} ${1}"
441             # We disable any automated renaming, as that lets tmux set
442             # the pane title to the process running in the pane. Which
443             # means you can end up with tons of "bash". With this
444             # disabled you will have panes named after the host.
445             tmux set-window-option -t ${SESSION} automatic-rename off >/dev/null
446             # If we have at least tmux 1.7, allow-rename works, such also disabling
447             # any rename based on shell escape codes.
448             if [ ${TMUXMINOR//[!0-9]/} -ge 7 ] || [ ${TMUXMAJOR//[!0-9]/} -gt 1 ]; then
449                 tmux set-window-option -t ${SESSION} allow-rename off >/dev/null
450             fi
451             shift
452             count=2
453             while [ $# -gt 0 ]; do
454                 tmux new-window -d -t ${SESSION}:${count} -n "${1}" "${TMSSHCMD} ${1}"
455                 tmux set-window-option -t ${SESSION}:${count} automatic-rename off >/dev/null
456                 # If we have at least tmux 1.7, allow-rename works, such also disabling
457                 # any rename based on shell escape codes.
458                 if [ ${TMUXMINOR//[!0-9]/} -ge 7 ] || [ ${TMUXMAJOR//[!0-9]/} -gt 1 ]; then
459                     tmux set-window-option -t ${SESSION}:${count} allow-rename off >/dev/null
460                 fi
461                 count=$(( count + 1 ))
462                 shift
463             done
464             ;;
465         ms)
466             # We open a multisession window. That is, we tile the window as many times
467             # as we have hosts, display them all and have the user input send to all
468             # of them at once.
469             tmux new-session -d -s ${SESSION} -n "Multisession" "${TMSSHCMD} ${1}"
470             shift
471             while [ $# -gt 0 ]; do
472                 tmux split-window -d -t ${SESSION}:${TMWIN} "${TMSSHCMD} ${1}"
473                 # Always have tmux redo the layout, so all windows are evenly sized.
474                 # Otherwise you quickly end up with tmux telling you there is no
475                 # more space available for tiling - and also very different amount
476                 # of visible space per host.
477                 tmux select-layout -t ${SESSION}:${TMWIN} main-horizontal >/dev/null
478                 shift
479             done
480             # Now synchronize them
481             tmux set-window-option -t ${SESSION}:${TMWIN} synchronize-pane >/dev/null
482             # And set a final layout which ensures they all have about the same size
483             tmux select-layout -t ${SESSION}:${TMWIN} tiled >/dev/null
484             ;;
485         *)
486             # Whatever string, so either a plain session or something from our tmux.d
487             if [ -z "${TMDATA}" ]; then
488                 # the easy case, just a plain session name
489                 tmux new-session -d -s ${SESSION}
490             else
491                 # data in our data array, the user wants his own config
492                 if [[ ${TMSESCFG} = free ]]; then
493                     if [[ ${TMDATA[2]} = NONE ]]; then
494                         # We have a free form config where we get the actual tmux commands
495                         # supplied by the user, so just issue them after creating the session.
496                         tmux new-session -d -s ${SESSION} -n "${TMDATA[0]}"
497                     else
498                         do_cmd ${TMDATA[2]}
499                     fi
500                     tmcount=${#TMDATA[@]}
501                     index=3
502                     while [ ${index} -lt ${tmcount} ]; do
503                         do_cmd ${TMDATA[$index]}
504                         (( index++ ))
505                     done
506                 else
507                     # So lets start with the "first" line, before dropping into a loop
508                     tmux new-session -d -s ${SESSION} -n "${TMDATA[0]}" "${TMSSHCMD} ${TMDATA[2]}"
509
510                     tmcount=${#TMDATA[@]}
511                     index=3
512                     while [ ${index} -lt ${tmcount} ]; do
513                         # List of hostnames, open a new connection per line
514                         tmux split-window -d -t ${SESSION}:${TMWIN} "${TMSSHCMD} ${TMDATA[$index]}"
515                         # Always have tmux redo the layout, so all windows are evenly sized.
516                         # Otherwise you quickly end up with tmux telling you there is no
517                         # more space available for tiling - and also very different amount
518                         # of visible space per host.
519                         tmux select-layout -t ${SESSION}:${TMWIN} main-horizontal >/dev/null
520                         (( index++ ))
521                     done
522                     # Now synchronize them
523                     tmux set-window-option -t ${SESSION}:${TMWIN} synchronize-pane >/dev/null
524                     # And set a final layout which ensures they all have about the same size
525                     tmux select-layout -t ${SESSION}:${TMWIN} tiled >/dev/null
526                 fi
527             fi
528             ;;
529     esac
530     # Build up new session, ensure we start in the first window
531     tmux select-window -t ${SESSION}:${TMWIN}
532 fi
533
534 # And last, but not least, attach to it
535 tmux ${TMOPTS} attach -t ${SESSION}