Merge branch 'master' of git.ganneff.de:misc
[misc.git] / bin / tm
diff --git a/bin/tm b/bin/tm
index c34c7de..b532c31 100755 (executable)
--- a/bin/tm
+++ b/bin/tm
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# Copyright (C) 2011, 2012, 2013, 2014 Joerg Jaspert <joerg@debian.org>
+# Copyright (C) 2011, 2012, 2013, 2014, 2016 Joerg Jaspert <joerg@debian.org>
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions
@@ -61,6 +61,17 @@ declare -r TMDIR=${TMDIR:-"${HOME}/.tmux.d"}
 # TMSESSHOST=false -> session name is host1_host2
 declare -r TMSESSHOST=${TMSESSHOST:-"true"}
 
+# Allow to globally define a custom ssh command line.
+TMSSHCMD=${TMSSHCMD:-"ssh"}
+
+# Debug output
+declare -r DEBUG=${DEBUG:-"false"}
+
+# Save the last argument, it may be used (traditional style) for
+# replacing
+args=$#
+TMREPARG=${!args}
+
 # Where does your tmux starts numbering its windows? Mine does at 1,
 # default for tmux is 0. We try to find it out, but if we fail, (as we
 # only check $HOME/.tmux.conf you can set this variable to whatever it
@@ -71,12 +82,16 @@ if [[ -f ${HOME}/.tmux.conf ]]; then
 else
     bindex=0
 fi
-TMWIN=${TMWIN:-$bindex}
+declare TMWIN=${TMWIN:-$bindex}
 unset bindex
 
 ########################################################################
 # Nothing below here to configure
 
+# Should we group the session to another? Set to true if -g on
+# commandline
+GROUPSESSION=false
+
 # Should we open another session, even if we already have one with
 # this name? (Ie. second multisession to the same set of hosts)
 # This is either set by the getopts option -n or by having -n
@@ -114,8 +129,9 @@ TMSESCFG=""
 function usage() {
     echo "tmux helper by Joerg Jaspert <joerg@ganneff.de>"
     echo "There are two ways to call it. Traditional and \"getopts\" style."
-    echo "Traditional call as: $0 CMD [host]..."
-    echo "Getopts call as: $0 [-s host] [-m hostlist] [-l] [-n] [-h] [-c config] [-e]"
+    echo "Traditional call as: $0 CMD [host]...[host]"
+    echo "Getopts call as: $0 [-s host] [-m hostlist] [-k name] [-l] [-n] [-h] [-c config] [-e]"
+    echo "Note that traditional and getopts can be mixed, sometimes."
     echo ""
     echo "Traditional:"
     echo "CMD is one of"
@@ -124,7 +140,9 @@ function usage() {
     echo " ms          Open multi ssh sessions to hosts, synchronizing input"
     echo "             To open a second session to the same set of hosts put a"
     echo "             -n in front of ms"
-    echo " \$anything  Either plain tmux session with name of \$anything or"
+    echo " k           Kill a session. Note that this needs the exact session name"
+    echo "             as shown by tm ls"
+    echo " \$anything   Either plain tmux session with name of \$anything or"
     echo "             session according to TMDIR file"
     echo ""
     echo "Getopts style:"
@@ -133,9 +151,13 @@ function usage() {
     echo "-m hostlist  Open multi ssh sessions to hosts, synchronizing input"
     echo "             Due to the way getopts works, hostlist must be enclosed in \"\""
     echo "-n           Open a second session to the same set of hosts"
+    echo "-g           Group session - attach to an existing session, but keep seperate"
+    echo "             window control"
+    echo "-k name      Kill a session. Note that this needs the exact session name"
+    echo "             as shown by tm ls"
     echo "-c config    Setup session according to TMDIR file"
     echo "-e SESSION   Use existion session named SESSION"
-    echo ""
+    echo "-r REPLACE   Value to use for replacing in session files"
     echo ""
     echo "TMDIR file:"
     echo "Each file in \$TMDIR defines a tmux session. There are two types of files,"
@@ -161,6 +183,22 @@ function usage() {
     echo "              SESSION - replaced with the session name"
     echo "              TMWIN   - see below for explanation of TMWIN Environment variable"
     echo ""
+    echo "NOTE: Both types of files accept external listings of hostnames."
+    echo "      That is, the output of any shell command given will be used as a list"
+    echo "      of hostnames to connect to (or a set of tmux commands to run)."
+    echo ""
+    echo "NOTE: Session files can include the Token ++TMREPLACETM++ at any point. This"
+    echo "      will be replaced by the value of the -r option (if you use getopts style) or"
+    echo "      by the LAST argument on the line if you use traditional calling."
+    echo "      Note that with traditional calling, the argument will also be tried as a hostname,"
+    echo "      so it may not make much sense there, unless using a session file that contains"
+    echo "      solely of LIST commands."
+    echo ""
+    echo "NOTE: Session files can include any existing environment variable at any point (but"
+    echo "      only one per line). Those get replaced during tm execution time with the actual"
+    echo "      value of the environment variable. Common usage is $HOME, but any existing var"
+    echo "      works fine."
+    echo ""
     echo "Environment variables recognized by this script:"
     echo "TMPDIR     - Where tmux stores its session information"
     echo "             DEFAULT: If unset: /tmp"
@@ -180,6 +218,12 @@ function usage() {
     echo "             like mine do at 1, then you can set TMWIN to 1"
     echo "TMSESSHOST - Should the hostname appear in session names?"
     echo "             DEFAULT: true"
+    echo "TMSSHCMD   - Allow to globally define a custom ssh command line."
+    echo "             This can be just the command or any option one wishes to have"
+    echo "             everywhere."
+    echo "             DEFAULT: ssh"
+    echo "DEBUG      - Show debug output (remember to redirect it to a file)"
+    echo ""
     exit 42
 }
 
@@ -188,8 +232,10 @@ function usage() {
 function clean_session() {
     local toclean=${*:-""}
 
-    # Neither space nor dot are friends in the SESSION name
+    # Neither space nor dot nor : or " are friends in the SESSION name
     toclean=${toclean// /_}
+    toclean=${toclean//:/_}
+    toclean=${toclean//\"/}
     echo ${toclean//./_}
 }
 
@@ -200,7 +246,7 @@ function ssh_sessname() {
         local one=$1
         # get rid of first argument (s|ms), we don't want to sort this
         shift
-        local sess=$(for i in $*; do echo $i; done | sort | tr '\n' ' ')
+        local sess=$(for i in $@; do echo $i; done | sort | tr '\n' ' ')
         sess="${one} ${sess% *}"
     else
         # no sorting wanted
@@ -214,21 +260,33 @@ function setup_command_aliases() {
     local command
     local SESNAME
     SESNAME="tmlscm$$"
-    # Debian Bug #718777 - tmux needs a session to have lscm work
-    tmux new-session -d -s ${SESNAME} -n "check" "sleep 3"
+    if [[ ${TMUXMAJOR} -lt 2 ]] || [[ ${TMUXMINOR} -lt 2 ]]; then
+        # Starting tmux 2.2, this is no longer needed
+        # Debian Bug #718777 - tmux needs a session to have lscm work
+        tmux new-session -d -s ${SESNAME} -n "check" "sleep 3"
+    fi
     for command in $(tmux list-commands|awk '{print $1}'); do
-        eval "$(echo "tm_$command() { tmux $command \"\$@\" >/dev/null; }")"
+        eval "tm_$command() { tmux $command \"\$@\" >/dev/null; }"
     done
-    tmux kill-session -t ${SESNAME} || true
+    if [[ ${TMUXMAJOR} -lt 2 ]] || [[ ${TMUXMINOR} -lt 2 ]]; then
+        tmux kill-session -t ${SESNAME} || true
+    fi
 }
 
 # Run a command (function) after replacing variables
 function do_cmd() {
-    local cmd=$@
+    local cmd=$*
+    cmd1=${cmd%% *}
+    if [[ ${cmd1} =~ ^# ]]; then
+        return
+    elif  [[ ${cmd1} =~ new-window ]]; then
+        TMWIN=$(( TMWIN + 1 ))
+    fi
+
     cmd=${cmd//SESSION/$SESSION}
     cmd=${cmd//TMWIN/$TMWIN}
-    cmd1=${cmd%% *}
     cmd=${cmd/$cmd1 /}
+    debug $cmd1 $cmd
     eval tm_$cmd1 $cmd
 }
 
@@ -236,34 +294,79 @@ function do_cmd() {
 function own_config() {
     if [[ ${1} =~ .cfg$ ]]; then
         TMSESCFG="free"
-        setup_command_aliases
     fi
     # Set IFS to be NEWLINE only, not also space/tab, as our input files
-    # are \n seperated (one entry per line) and lines may well have spaces
-    IFS="
+    # are \n seperated (one entry per line) and lines may well have spaces.
+    local IFS="
 "
     # Fill an array with our config
-    TMDATA=( $(cat "${TMDIR}/$1") )
+    if [[ -n ${TMDATA[@]:-""} ]] && [[ ${#TMDATA[@]} -gt 0 ]]; then
+        olddata=("${TMDATA[@]}")
+    fi
+
+    TMDATA=( $(sed -e "s/++TMREPLACETM++/${TMREPARG}/g" "${TMDIR}/$1") )
     # Restore IFS
     IFS=${OLDIFS}
 
-    SESSION=$(clean_session ${TMDATA[0]})
+    SESSION=${SESSION:-$(clean_session ${TMDATA[0]})}
 
     if [ "${TMDATA[1]}" != "NONE" ]; then
         TMOPTS=${TMDATA[1]}
     fi
 
-    # Line 3 may start with REMOTE, then we get the list of hosts from elsewhere.
-    # So if it does, we exec the command given, then take the output as one host per
-    # line and appen it to TMDATA - while deleting the actual line with REMOTE in.
-    if [[ "${TMDATA[2]}" =~ ^REMOTE\ (.*) ]]; then
-        echo "Fetching hostnames from remote, please stand by..."
-        out=( $( ${BASH_REMATCH[1]} ) )
-        unset TMDATA[2]
-        TMDATA=( "${TMDATA[@]}" "${out[@]}" )
-        unset out
-    fi
+    # Seperate the lines we work with
+    local IFS=""
+    local -a workdata=(${TMDATA[@]:2})
+    IFS=${OLDIFS}
+
+    # Lines (starting with line 3) may start with LIST, then we get
+    # the list of hosts from elsewhere. So if one does, we exec the
+    # command given, then append the output to TMDATA - while deleting
+    # the actual line with LIST in.
+    local TMPDATA=$(mktemp -u -p ${TMPDIR} .tmux_tm_XXXXXXXXXX)
+    trap "rm -f ${TMPDATA}" EXIT ERR HUP INT QUIT TERM
+    local index=0
+    while [[ ${index} -lt ${#workdata[@]} ]]; do
+        if [[ "${workdata[${index}]}" =~ ^LIST\ (.*)$ ]]; then
+            # printf -- 'workdata: %s\n' "${workdata[@]}"
+            local cmd=${BASH_REMATCH[1]}
+            if [[ ${cmd} =~ \$\{([0-9a-zA-Z_]+)\} ]]; then
+                repvar=${BASH_REMATCH[1]}
+                reptext=${!repvar}
+                cmd=${cmd//\$\{$repvar\}/$reptext}
+            fi
+            echo "Line ${index}: Fetching hostnames using provided shell command '${cmd}', please stand by..."
+
+            $( ${cmd} >| "${TMPDATA}" )
+            # Set IFS to be NEWLINE only, not also space/tab, the list may have ssh options
+            # and what not, so \n is our seperator, not more.
+            IFS="
+"
+            out=( $(tr -d '\r' < "${TMPDATA}" ) )
 
+            # Restore IFS
+            IFS=${OLDIFS}
+
+            workdata+=( "${out[@]}" )
+            unset workdata[${index}]
+            unset out
+            # printf -- 'workdata: %s\n' "${workdata[@]}"
+        elif [[ "${workdata[${index}]}" =~ ^SSHCMD\ (.*)$ ]]; then
+            TMSSHCMD=${BASH_REMATCH[1]}
+        fi
+        index=$(( index + 1 ))
+    done
+    rm -f "${TMPDATA}"
+    trap - EXIT ERR HUP INT QUIT TERM
+    debug "TMDATA: ${TMDATA[@]}"
+    debug "olddata: ${olddata[@]:-''}"
+    if [[ -n ${olddata[@]:-""} ]]; then
+        TMDATA=( "${olddata[@]}" "${workdata[@]}" )
+    else
+        TMDATA=( "${TMDATA[@]:0:2}" "${workdata[@]}"  )
+    fi
+    declare -r TMDATA=( "${TMDATA[@]}" )
+    debug "TMDATA now ${TMDATA[@]}"
 }
 
 # Simple overview of running sessions
@@ -276,6 +379,16 @@ function list_sessions() {
     fi
 }
 
+# We either have a debug function that shows output, or one that
+# plainly returns
+if [[ ${DEBUG} == true ]]; then
+        eval "debug() { >&2 echo \$* ; }"
+else
+        eval "debug() { return ; }"
+fi
+
+setup_command_aliases
+
 ########################################################################
 # MAIN work follows here
 # Check the first cmdline parameter, we might want to prepare something
@@ -284,8 +397,8 @@ case ${cmdline} in
         list_sessions
         exit 0
         ;;
-    s|ms)
-        # Yay, we want ssh to a remote host - or even a multi session setup
+    s|ms|k)
+        # Yay, we want ssh to a remote host - or even a multi session setup - or kill one
         # So we have to prepare our session name to fit in what tmux (and shell)
         # allow us to have. And so that we can reopen an existing session, if called
         # with the same hosts again.
@@ -294,7 +407,7 @@ case ${cmdline} in
         shift
         ;;
     -*)
-        while getopts "lnhs:m:c:e:" OPTION; do
+        while getopts "lnhs:m:c:e:r:k:g:" OPTION; do
             case ${OPTION} in
                 l) # ls
                     list_sessions
@@ -305,6 +418,11 @@ case ${cmdline} in
                     declare -r cmdline=s
                     shift
                     ;;
+                k) # kill session
+                    SESSION=$(ssh_sessname s ${OPTARG})
+                    declare -r cmdline=k
+                    shift
+                    ;;
                 m) # ms (needs hostnames in "")
                     SESSION=$(ssh_sessname ms ${OPTARG})
                     declare -r cmdline=ms
@@ -315,10 +433,19 @@ case ${cmdline} in
                     ;;
                 e) # existing session name
                     SESSION=$(clean_session ${OPTARG})
+                    shift
                     ;;
                 n) # new session even if same name one already exists
                     DOUBLENAME=true
                     ;;
+                r) # replacement arg
+                    TMREPARG=${OPTARG}
+                    ;;
+                g) # Group session, not simple attach
+                    declare -r GROUPSESSION=true
+                    SESSION=$(clean_session ${OPTARG})
+                    shift
+                    ;;
                 h)
                     usage
                     ;;
@@ -340,7 +467,9 @@ case ${cmdline} in
                 usage
             fi
         elif [ -r "${TMDIR}/${cmdline}" ]; then
-            own_config $1
+            for arg in "$@"; do
+                own_config ${arg}
+            done
         else
             # Not a config file, so just session name.
             SESSION=${cmdline}
@@ -348,9 +477,15 @@ case ${cmdline} in
         ;;
 esac
 
+havesession="false"
+if tmux has-session -t ${SESSION} 2>/dev/null; then
+    havesession="true"
+fi
+declare -r havesession
+
 # And now check if we would end up with a doubled session name.
 # If so add something "random" to the new name, like our pid.
-if [[ ${DOUBLENAME} == true ]] && tmux has-session -t ${SESSION} 2>/dev/null; then
+if [[ ${DOUBLENAME} == true ]] && [[ ${havesession} == true ]]; then
     # Session exist but we are asked to open another one,
     # so adjust our session name
     if [[ ${#TMDATA} -eq 0 ]] && [[ ${SESSION} =~ ([ms]+)_(.*) ]]; then
@@ -361,41 +496,42 @@ if [[ ${DOUBLENAME} == true ]] && tmux has-session -t ${SESSION} 2>/dev/null; th
 fi
 
 if [[ ${TMSESSHOST} = true ]]; then
-    declare -r SESSION="$(hostname -s)_${SESSION}"
+    declare -r SESSION="$(uname -n|cut -d. -f1)_${SESSION}"
 else
     declare -r SESSION
 fi
 
 # We only do special work if the SESSION does not already exist.
-if ! tmux has-session -t ${SESSION} 2>/dev/null; then
+if [[ ${cmdline} != k ]] && [[ ${havesession} == false ]]; then
     # In case we want some extra things...
     # Check stupid users
     if [ $# -lt 1 ]; then
         usage
     fi
+    tm_pane_error="create pane failed: pane too small"
     case ${cmdline} in
         s)
             # The user wants to open ssh to one or more hosts
-            tmux new-session -d -s ${SESSION} -n "${1}" "ssh ${1}"
+            do_cmd new-session -d -s ${SESSION} -n "${1}" "'${TMSSHCMD} ${1}'"
             # We disable any automated renaming, as that lets tmux set
             # the pane title to the process running in the pane. Which
             # means you can end up with tons of "bash". With this
             # disabled you will have panes named after the host.
-            tmux set-window-option -t ${SESSION} automatic-rename off >/dev/null
+            do_cmd set-window-option -t ${SESSION} automatic-rename off >/dev/null
             # If we have at least tmux 1.7, allow-rename works, such also disabling
             # any rename based on shell escape codes.
-            if [ ${TMUXMINOR} -ge 7 ] || [ ${TMUXMAJOR} -gt 1 ]; then
-                tmux set-window-option -t ${SESSION} allow-rename off >/dev/null
+            if [ ${TMUXMINOR//[!0-9]/} -ge 7 ] || [ ${TMUXMAJOR//[!0-9]/} -gt 1 ]; then
+                do_cmd set-window-option -t ${SESSION} allow-rename off >/dev/null
             fi
             shift
             count=2
             while [ $# -gt 0 ]; do
-                tmux new-window -d -t ${SESSION}:${count} -n "${1}" "ssh ${1}"
-                tmux set-window-option -t ${SESSION}:${count} automatic-rename off >/dev/null
+                do_cmd new-window -d -t ${SESSION}:${count} -n "${1}" "${TMSSHCMD} ${1}"
+                do_cmd set-window-option -t ${SESSION}:${count} automatic-rename off >/dev/null
                 # If we have at least tmux 1.7, allow-rename works, such also disabling
                 # any rename based on shell escape codes.
-                if [ ${TMUXMINOR} -ge 7 ] || [ ${TMUXMAJOR} -gt 1 ]; then
-                    tmux set-window-option -t ${SESSION}:${count} allow-rename off >/dev/null
+                if [ ${TMUXMINOR//[!0-9]/} -ge 7 ] || [ ${TMUXMAJOR//[!0-9]/} -gt 1 ]; then
+                    do_cmd set-window-option -t ${SESSION}:${count} allow-rename off >/dev/null
                 fi
                 count=$(( count + 1 ))
                 shift
@@ -405,34 +541,39 @@ if ! tmux has-session -t ${SESSION} 2>/dev/null; then
             # We open a multisession window. That is, we tile the window as many times
             # as we have hosts, display them all and have the user input send to all
             # of them at once.
-            tmux new-session -d -s ${SESSION} -n "Multisession" "ssh ${1}"
+            do_cmd new-session -d -s ${SESSION} -n "Multisession" "'${TMSSHCMD} ${1}'"
             shift
             while [ $# -gt 0 ]; do
-                tmux split-window -d -t ${SESSION}:${TMWIN} "ssh ${1}"
-                # Always have tmux redo the layout, so all windows are evenly sized.
-                # Otherwise you quickly end up with tmux telling you there is no
-                # more space available for tiling - and also very different amount
-                # of visible space per host.
-                tmux select-layout -t ${SESSION}:${TMWIN} main-horizontal >/dev/null
+                set +e
+                output=$(do_cmd split-window -d -t ${SESSION}:${TMWIN} "'${TMSSHCMD} ${1}'" 2>&1)
+                ret=$?
+                set -e
+                if [[ ${ret} -ne 0 ]] && [[ ${output} == ${tm_pane_error} ]]; then
+                    # No more space -> have tmux redo the
+                    # layout, so all windows are evenly sized.
+                    do_cmd select-layout -t ${SESSION}:${TMWIN} main-horizontal >/dev/null
+                    # And dont shift parameter away
+                    continue
+                fi
                 shift
             done
             # Now synchronize them
-            tmux set-window-option -t ${SESSION}:${TMWIN} synchronize-pane >/dev/null
+            do_cmd set-window-option -t ${SESSION}:${TMWIN} synchronize-pane >/dev/null
             # And set a final layout which ensures they all have about the same size
-            tmux select-layout -t ${SESSION}:${TMWIN} tiled >/dev/null
+            do_cmd select-layout -t ${SESSION}:${TMWIN} tiled >/dev/null
             ;;
         *)
             # Whatever string, so either a plain session or something from our tmux.d
             if [ -z "${TMDATA}" ]; then
                 # the easy case, just a plain session name
-                tmux new-session -d -s ${SESSION}
+                do_cmd new-session -d -s ${SESSION}
             else
                 # data in our data array, the user wants his own config
                 if [[ ${TMSESCFG} = free ]]; then
                     if [[ ${TMDATA[2]} = NONE ]]; then
                         # We have a free form config where we get the actual tmux commands
                         # supplied by the user, so just issue them after creating the session.
-                        tmux new-session -d -s ${SESSION} -n "${TMDATA[0]}"
+                        do_cmd new-session -d -s ${SESSION} -n "'${TMDATA[0]}'"
                     else
                         do_cmd ${TMDATA[2]}
                     fi
@@ -444,31 +585,49 @@ if ! tmux has-session -t ${SESSION} 2>/dev/null; then
                     done
                 else
                     # So lets start with the "first" line, before dropping into a loop
-                    tmux new-session -d -s ${SESSION} -n "${TMDATA[0]}" "ssh ${TMDATA[2]}"
+                    do_cmd new-session -d -s ${SESSION} -n "${TMDATA[0]}" "'${TMSSHCMD} ${TMDATA[2]}'"
 
                     tmcount=${#TMDATA[@]}
                     index=3
                     while [ ${index} -lt ${tmcount} ]; do
                         # List of hostnames, open a new connection per line
-                        tmux split-window -d -t ${SESSION}:${TMWIN} "ssh ${TMDATA[$index]}"
-                        # Always have tmux redo the layout, so all windows are evenly sized.
-                        # Otherwise you quickly end up with tmux telling you there is no
-                        # more space available for tiling - and also very different amount
-                        # of visible space per host.
-                        tmux select-layout -t ${SESSION}:${TMWIN} main-horizontal >/dev/null
+                        set +e
+                        output=$(do_cmd split-window -d -t ${SESSION}:${TMWIN} "'${TMSSHCMD} ${TMDATA[$index]}'" 2>&1)
+                        set -e
+                        if [[ ${output} =~ ${tm_pane_error} ]]; then
+                            # No more space -> have tmux redo the
+                            # layout, so all windows are evenly sized.
+                            do_cmd select-layout -t ${SESSION}:${TMWIN} main-horizontal >/dev/null
+                            # And again, don't increase index
+                            continue
+                        fi
                         (( index++ ))
                     done
                     # Now synchronize them
-                    tmux set-window-option -t ${SESSION}:${TMWIN} synchronize-pane >/dev/null
+                    do_cmd set-window-option -t ${SESSION}:${TMWIN} synchronize-pane >/dev/null
                     # And set a final layout which ensures they all have about the same size
-                    tmux select-layout -t ${SESSION}:${TMWIN} tiled >/dev/null
+                    do_cmd select-layout -t ${SESSION}:${TMWIN} tiled >/dev/null
                 fi
             fi
             ;;
     esac
     # Build up new session, ensure we start in the first window
-    tmux select-window -t ${SESSION}:${TMWIN}
+    do_cmd select-window -t ${SESSION}:${TMWIN}
+elif [[ ${cmdline} == k ]]; then
+    # So we are asked to kill a session
+    tokill=${SESSION//k_/}
+    do_cmd kill-session -t ${tokill}
+    exit 0
 fi
 
-# And last, but not least, attach to it
-tmux ${TMOPTS} attach -t ${SESSION}
+# If we should group our session or not
+if [[ ${GROUPSESSION} == true ]]; then
+    # Grouping means opening a new session, but sharing the sessions with
+    # another session (existing and new windows). But window control is separate.
+    sesname="$$_${SESSION}"
+    tmux ${TMOPTS} new-session -s ${sesname} -t ${SESSION}
+    tmux ${TMOPTS} kill-session -t ${sesname}
+else
+    # Do not group, just attach
+    tmux ${TMOPTS} attach -t ${SESSION}
+fi