├── .gitlab-ci.yml ├── .gitlab └── renovate.json ├── LICENSE.md ├── README.md └── tmux-cssh /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - template: Security/Secret-Detection.gitlab-ci.yml 4 | 5 | - project: renovate-bot/renovate-runner 6 | file: /templates/renovate-config-validator.gitlab-ci.yml 7 | 8 | stages: 9 | - test 10 | 11 | shellcheck: 12 | stage: test 13 | image: koalaman/shellcheck-alpine:v0.10.0 14 | script: 15 | - | 16 | shellcheck tmux-cssh 17 | rules: 18 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 19 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" 20 | changes: 21 | - tmux-cssh 22 | 23 | shfmt: 24 | stage: test 25 | image: mvdan/shfmt:v3.11.0-alpine 26 | script: 27 | - | 28 | shfmt -d -l -i 2 -ci tmux-cssh 29 | rules: 30 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 31 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" 32 | changes: 33 | - tmux-cssh 34 | -------------------------------------------------------------------------------- /.gitlab/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":disableRateLimiting" 5 | ], 6 | "assignAutomerge": true, 7 | "assignees": ["peikk0"], 8 | "labels": [ 9 | "dependencies" 10 | ], 11 | "semanticCommits": "enabled" 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Pierre Guinoiseau 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tmux-cssh 2 | 3 | Spawns multiple synchronized SSH sessions inside a tmux session. 4 | 5 | Think [Cluster SSH](https://github.com/duncs/clusterssh) except with only one terminal window and without the ugly Tk GUI. 6 | 7 | # Requirements 8 | 9 | - [tmux](https://tmux.github.io/) >=1.1 10 | 11 | # Installation 12 | 13 | Just copy `tmux-cssh` somewhere in your `$PATH`. 14 | 15 | You might want to add a keybinding in your `.tmux.conf` for toggling panes synchronization: 16 | ``` 17 | bind-key = set-window-option synchronize-panes 18 | ``` 19 | This example assigns the `=` key. 20 | 21 | # Usage 22 | 23 | ``` 24 | % tmux-cssh -h 25 | Usage: tmux-cssh [options] host [host ...] 26 | 27 | Spawns multiple synchronized SSH sessions inside a tmux session. 28 | 29 | Options: 30 | -h Show help 31 | -n Name of the tmux session or window (default: cssh) 32 | -o Additional SSH arguments 33 | -c Use the current tmux session and just spawn a new window instead 34 | 35 | % tmux-cssh web{1..4} 36 | 37 | % tmux-cssh -o '-p 2222 -l my_user' web{1..4} 38 | ``` 39 | 40 | # Contributing 41 | 42 | Please open issues and submit merge requests in [GitLab](https://gitlab.com/peikk0/tmux-cssh). 43 | 44 | # License 45 | 46 | [MIT](LICENSE.md) 47 | 48 | Handmade with love in Wellington, New Zealand. 49 | -------------------------------------------------------------------------------- /tmux-cssh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | hosts="" 4 | ssh_options="" 5 | tmux_name="cssh" 6 | tmux_attach_current_session="false" 7 | 8 | usage() { 9 | echo "Usage: $0 [options] host [host ...]" >&2 10 | echo "" >&2 11 | echo "Spawns multiple synchronized SSH sessions inside a tmux session." >&2 12 | echo "" >&2 13 | echo "Options:" >&2 14 | echo " -h Show help" >&2 15 | echo " -c Use the current tmux session and just spawn a new window instead" >&2 16 | echo " -n Name of the tmux session or window (default: cssh)" >&2 17 | echo " -o Additional SSH arguments" >&2 18 | } 19 | 20 | while [ $# -ne 0 ]; do 21 | case $1 in 22 | -n) 23 | shift 24 | if [ $# -eq 0 ]; then 25 | usage 26 | exit 2 27 | fi 28 | tmux_name="$1" 29 | shift 30 | ;; 31 | -c) 32 | tmux_attach_current_session="true" 33 | shift 34 | ;; 35 | -o) 36 | shift 37 | if [ $# -eq 0 ]; then 38 | usage 39 | exit 2 40 | fi 41 | ssh_options="$1" 42 | shift 43 | ;; 44 | -h) 45 | usage 46 | exit 0 47 | ;; 48 | -*) 49 | usage 50 | exit 2 51 | ;; 52 | *) 53 | hosts="${hosts}${hosts:+ }$1" 54 | shift 55 | ;; 56 | esac 57 | done 58 | 59 | if [ -z "${hosts}" ]; then 60 | usage 61 | exit 2 62 | fi 63 | 64 | # Find a name for a new session 65 | n=0 66 | while tmux has-session -t "${tmux_name}-${n}" 2>/dev/null; do n=$((n + 1)); done 67 | tmux_session="${tmux_name}-${n}" 68 | 69 | if [ "${tmux_attach_current_session}" = "true" ]; then 70 | tmux_session="$(tmux display-message -p '#S')" 71 | # Find a name for a new window 72 | n=0 73 | while tmux list-windows -F "#W" | grep -q "${tmux_name}-${n}" 2>/dev/null; do n=$((n + 1)); done 74 | tmux_window="${tmux_name}-${n}" 75 | tmux_window_options="-n ${tmux_window}" 76 | fi 77 | 78 | # If host doesn't look like a DNS name, it may be a CSSH cluster 79 | if ! echo "${hosts}" | grep -q '[. ]'; then 80 | for cfg in "${HOME}/.clusterssh/clusters" /etc/clusters; do 81 | if [ -r "${cfg}" ]; then 82 | h="$(sed -n "s/^${hosts} //p" <"${cfg}")" 83 | if [ -n "${h}" ]; then 84 | hosts="${h}" 85 | break 86 | fi 87 | fi 88 | # If there was no corresponding cluster name, 89 | # just assume we have an unqualified domain name 90 | done 91 | fi 92 | 93 | # Open a new session and split into new panes for each SSH session 94 | for host in ${hosts}; do 95 | if ! tmux has-session -t "${tmux_session}" 2>/dev/null; then 96 | tmux new-session -s "${tmux_session}" -d "ssh ${ssh_options} ${host}" 97 | elif [ "${tmux_attach_current_session}" = "true" ]; then 98 | if ! tmux list-windows -F "#W" | grep -q "${tmux_window}" >/dev/null; then 99 | # shellcheck disable=SC2086 100 | tmux new-window ${tmux_window_options} "ssh ${ssh_options} ${host}" 101 | else 102 | tmux split-window -t "${tmux_window}" -d "ssh ${ssh_options} ${host}" 103 | # We have to reset the layout after each new pane otherwise the panes 104 | # quickly become too small to spawn any more 105 | tmux select-layout -t "${tmux_session}" tiled 106 | fi 107 | else 108 | tmux split-window -t "${tmux_session}" -d "ssh ${ssh_options} ${host}" 109 | # We have to reset the layout after each new pane otherwise the panes 110 | # quickly become too small to spawn any more 111 | tmux select-layout -t "${tmux_session}" tiled 112 | fi 113 | done 114 | 115 | # Synchronize panes by default 116 | if [ "${tmux_attach_current_session}" = "true" ]; then 117 | tmux set-window-option -t "${tmux_window}" synchronize-panes on 118 | else 119 | tmux set-window-option -t "${tmux_session}" synchronize-panes on 120 | fi 121 | 122 | if [ -n "${TMUX}" ]; then 123 | # We are in a tmux, just switch to the new session 124 | tmux switch-client -t "${tmux_session}" 125 | else 126 | # We are NOT in a tmux, attach to the new session 127 | tmux attach-session -t "${tmux_session}" 128 | fi 129 | 130 | exit 0 131 | --------------------------------------------------------------------------------