11 Commits
Author SHA1 Message Date
matmoul ba79cedcb1 docs: remove outdated group naming note 2026-06-02 20:37:27 +02:00
matmoul 87945adae3 feat: require explicit group names in host configs 2026-06-02 20:36:51 +02:00
matmoul 69131534fe feat: support jump host ports in SSH menu
Allow jump host definitions to include an explicit port in the form user@host:port.
Default to port 22 when no port is provided and update the sample config and docs accordingly.
2026-06-02 20:26:25 +02:00
matmoul c29d0dcb5d docs: record known follow-up improvements for jump server config 2026-06-01 01:04:58 +02:00
matmoul c942d95ec6 fix: reset aliases when loading each server 2026-06-01 00:53:06 +02:00
matmoul ba7df89340 fix: default known-hosts user to current user 2026-06-01 00:22:51 +02:00
matmoul 8650b71c3c docs: align README with current script behavior
Update the usage, feature summary, and notes to reflect known_hosts support, current option flags, and the sample configuration layout.
2026-06-01 00:03:59 +02:00
matmoul 59f37d8455 feat: add known_hosts entries to ssh menu
Allow loading hosts from ~/.ssh/known_hosts via -k, with an optional user override. Also update the usage text and keep direct SSH connections working for generated known_hosts entries.
2026-05-31 23:59:29 +02:00
matmoul ccd38c5e31 fix: use fzf reverse layout in SSH menu 2026-05-31 22:57:40 +02:00
matmoul c0fcd67312 chore: push changes before release branch checkout 2026-05-31 22:17:20 +02:00
matmoul b8aa1aaa8d chore: add release automation script 2026-05-31 22:15:15 +02:00
8 changed files with 177 additions and 51 deletions
+5 -2
View File
@@ -5,6 +5,9 @@
- Main entry point: `mtm-ssh-menu` - Main entry point: `mtm-ssh-menu`
- Configuration: `~/.config/mtm-ssh-menu/global.yaml` and `hosts/*.yaml` by default. - Configuration: `~/.config/mtm-ssh-menu/global.yaml` and `hosts/*.yaml` by default.
- Dependencies: `bash`, `ssh`, `fzf`, `jq`, `yq`. - Dependencies: `bash`, `ssh`, `fzf`, `jq`, `yq`.
- Sample config is stored in `sample-config/`. - Sample config is stored in `sample-config/` and mirrors current script behavior.
- Documentation language: English. - Documentation language: English.
- Current task focus: keep README aligned with the working script and sample configuration. - Current task focus: keep README and memory aligned with the working script and sample configuration.
- Sample host files currently use numbered names: `01_prod.yaml`, `02_staging.yaml`, `03_dev.yaml`.
- Known follow-up improvements:
- Each group should have default values.
+51 -21
View File
@@ -1,18 +1,19 @@
# mtm-ssh-menu # mtm-ssh-menu
A small Bash utility to browse SSH targets from YAML configuration and connect to them with `fzf`. A Bash SSH target picker built around `fzf` and YAML configuration.
## Features ## What it does
- Interactive selection with `fzf` - Lists SSH targets from grouped YAML files
- Global defaults for SSH user, port, and jump hosts - Uses the `group` value from each YAML file as the displayed group name
- Per-host overrides in grouped YAML files - Allows the YAML file name to differ from the group name
- Support for jump hosts via named aliases - Supports global defaults for user, port, and jump hosts
- Simple, dependency-light Bash script - Supports per-host overrides
- Can also browse entries from `~/.ssh/known_hosts`
- Launches `ssh` directly from the selected entry
## Requirements ## Requirements
- `bash`
- `ssh` - `ssh`
- `fzf` - `fzf`
- `jq` - `jq`
@@ -21,13 +22,14 @@ A small Bash utility to browse SSH targets from YAML configuration and connect t
## Usage ## Usage
```bash ```bash
./mtm-ssh-menu [--config-dir DIR] ./mtm-ssh-menu [--config-dir DIR] [-k [user]]
``` ```
Options: Options:
- `--help`: show help - `-h`, `--help`: show help
- `--config-dir DIR`: use a custom configuration directory - `--config-dir DIR`: use a custom configuration directory
- `-k`, `--known-hosts [user]`: include hosts from `~/.ssh/known_hosts`
By default, the script reads configuration from: By default, the script reads configuration from:
@@ -45,12 +47,14 @@ ssh:
default_options: "" default_options: ""
jump_hosts: jump_hosts:
office: "user@192.168.10.11" office: "user@192.168.10.11"
office-alt: "user@192.168.10.11:2222"
``` ```
### `hosts/dev.yaml` ### `hosts/01_prod.yaml`
```yaml ```yaml
group: dev group: prod
servers: servers:
- name: api-1 - name: api-1
aliases: aliases:
@@ -59,8 +63,33 @@ servers:
host: 192.168.10.11 host: 192.168.10.11
user: dev user: dev
port: 2222 port: 2222
- name: web-1
aliases:
- frontend
- ui
host: 192.168.10.12
user: dev
port: 22
``` ```
### `hosts/03_dev.yaml`
```yaml
group: dev
servers:
- name: web-1
aliases:
- frontend
- web
host: web-1.prod.internal
jump_host: office
```
The file name does not need to match the `group` value. The script reads the `group` from the YAML content and uses the file path internally to load the selected host.
The sample configuration now uses numbered file names such as `01_prod.yaml`, `02_staging.yaml`, and `03_dev.yaml`.
Each host entry can define: Each host entry can define:
- `name` - `name`
@@ -70,19 +99,20 @@ Each host entry can define:
- `port` - `port`
- `jump_host` - `jump_host`
## Example workflow
1. Create the config directory.
2. Add `global.yaml` and one or more files under `hosts/`.
3. Run the script.
4. Select a target in the menu and connect.
## Sample configuration ## Sample configuration
A complete sample configuration is available in `sample-config/`. A complete sample configuration is available in `sample-config/`.
## Notes ## Notes
- The script requires `yq`, `jq`, `ssh`, and `fzf` to be installed. - `jump_host` values are resolved by name from `global.yaml`.
- Jump hosts are resolved by name from `global.yaml`. - Jump hosts support `user@host` and `user@host:port`; if the port is omitted, `22` is used.
- If a host does not define a user or port, global defaults are used. - If a host does not define a user or port, global defaults are used.
- `default_options` is present in the config but not consumed by the current script.
- The sample config includes `ssh_options` in one file, but the current script does not consume it yet.
- Sample host examples are stored as `sample-config/hosts/01_prod.yaml`, `02_staging.yaml`, and `03_dev.yaml`.
- The `group` key is required for each host config file; the file name is only an internal storage detail.
- When `-k` is used, entries from `known_hosts` are shown with the selected user or `root` by default.
- `known_hosts` entries use port `22`.
- The script uses strict Bash mode (`set -euo pipefail`).
- The script name shown in usage is `sshm`, while the repository file is `mtm-ssh-menu`.
Executable
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
set -euo pipefail
declare -r VERSION=${1:-}
declare -r MESSAGE=${2:-"Release ${VERSION}"}
declare -r TAGBRANCH=main
declare -r RELEASEBRANCH=dev
declare CURRENTBRANCH=""
showHelp() {
echo "Usage: ./_release.sh <version> [message]"
}
cleanup() {
if [ -n "${CURRENTBRANCH}" ] && [ "${CURRENTBRANCH}" != "$(git rev-parse --abbrev-ref HEAD)" ]; then
git checkout "${CURRENTBRANCH}" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
if [ "${VERSION}" == "" ]; then
showHelp
echo ""
echo "No version provided!"
exit 1
fi
CURRENTBRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "${CURRENTBRANCH}" != "${RELEASEBRANCH}" ]; then
echo "You are not on the ${RELEASEBRANCH} branch!"
echo "Use ${RELEASEBRANCH} branch to make a release!"
exit 1
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Working tree is not clean. Commit or stash your changes first."
exit 1
fi
if git rev-parse -q --verify "refs/tags/${VERSION}" >/dev/null; then
echo "Tag ${VERSION} already exists."
exit 1
fi
git push
git checkout "${TAGBRANCH}"
git merge "${RELEASEBRANCH}"
git push origin "${TAGBRANCH}"
git tag -a "${VERSION}" -m "${MESSAGE}"
git push origin "${VERSION}"
+66 -28
View File
@@ -2,14 +2,17 @@
set -euo pipefail set -euo pipefail
CONFIG_DIR="$HOME/.config/mtm-ssh-menu" CONFIG_DIR="$HOME/.config/mtm-ssh-menu"
USE_KNOWN_HOSTS=0
USE_KNOWN_HOSTS_USER=$(whoami)
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: sshm [--config-dir DIR] Usage: sshm [--config-dir DIR] [-k [user]]
Options: Options:
--help Show this help message -h | --help Show this help message
--config-dir DIR Use a custom config directory --config-dir DIR Use a custom config directory
-k | --known-hosts [user] Include known_hosts
The config directory must contain: The config directory must contain:
- global.yaml - global.yaml
@@ -28,6 +31,15 @@ parse_args() {
CONFIG_DIR="$2" CONFIG_DIR="$2"
shift 2 shift 2
;; ;;
--known-hosts|-k)
USE_KNOWN_HOSTS=1
if [ $# -eq 1 ]; then
shift
else
USE_KNOWN_HOSTS_USER="$2"
shift 2
fi
;;
--help|-h) --help|-h)
usage usage
exit 0 exit 0
@@ -82,12 +94,13 @@ load_config() {
for file in "$HOSTS_DIR"/*.yaml; do for file in "$HOSTS_DIR"/*.yaml; do
local FILE_CONTENT GROUP_NAME GROUP_SERVERS INDEX local FILE_CONTENT GROUP_NAME GROUP_SERVERS INDEX
FILE_CONTENT="$(<"$file")" FILE_CONTENT="$(<"$file")"
GROUP_NAME=$(basename "$file") GROUP_NAME=$(yq -r '.group // empty' <<<"$FILE_CONTENT")
GROUP_NAME=${GROUP_NAME%.*} if [ "$GROUP_NAME" = "" ]; then
GROUP_NAME=$(yq -r '.group // "'"$GROUP_NAME"'"' <<<"$FILE_CONTENT") printf 'Error: missing group name in config file: %s\n' "$file" >&2
exit 1
fi
GROUP_SERVERS=$(yq -r '.servers // []' <<<"$FILE_CONTENT") GROUP_SERVERS=$(yq -r '.servers // []' <<<"$FILE_CONTENT")
INDEX=0 INDEX=0
for ((i=0; i<$(jq -r '. | length' <<<"$GROUP_SERVERS"); i++)); do for ((i=0; i<$(jq -r '. | length' <<<"$GROUP_SERVERS"); i++)); do
local SSH_SERVER_NAME SSH_SERVER_ALIASES ALIASES SSH_SERVER_HOST SSH_SERVER_PORT SSH_SERVER_USER local SSH_SERVER_NAME SSH_SERVER_ALIASES ALIASES SSH_SERVER_HOST SSH_SERVER_PORT SSH_SERVER_USER
SSH_SERVER_NAME=$(jq -r '.'"[$i]"'.name // ""' <<<"$GROUP_SERVERS") SSH_SERVER_NAME=$(jq -r '.'"[$i]"'.name // ""' <<<"$GROUP_SERVERS")
@@ -95,6 +108,7 @@ load_config() {
SSH_SERVER_HOST=$(jq -r '.'"[$i]"'.host // ""' <<<"$GROUP_SERVERS") SSH_SERVER_HOST=$(jq -r '.'"[$i]"'.host // ""' <<<"$GROUP_SERVERS")
SSH_SERVER_PORT=$(jq -r '.'"[$i]"'.port // "'"$DEFAULT_SSH_PORT"'"' <<<"$GROUP_SERVERS") SSH_SERVER_PORT=$(jq -r '.'"[$i]"'.port // "'"$DEFAULT_SSH_PORT"'"' <<<"$GROUP_SERVERS")
SSH_SERVER_USER=$(jq -r '.'"[$i]"'.user // "'"$DEFAULT_SSH_USER"'"' <<<"$GROUP_SERVERS") SSH_SERVER_USER=$(jq -r '.'"[$i]"'.user // "'"$DEFAULT_SSH_USER"'"' <<<"$GROUP_SERVERS")
ALIASES=""
if [ "$SSH_SERVER_ALIASES" != "" ]; then if [ "$SSH_SERVER_ALIASES" != "" ]; then
# shellcheck disable=SC2027 # shellcheck disable=SC2027
ALIASES="("$(echo "$SSH_SERVER_ALIASES" | jq -r 'join(", ")')")" ALIASES="("$(echo "$SSH_SERVER_ALIASES" | jq -r 'join(", ")')")"
@@ -102,42 +116,66 @@ load_config() {
if [ "$SERVERS" != "" ]; then if [ "$SERVERS" != "" ]; then
SERVERS+="\n" SERVERS+="\n"
fi fi
SERVERS+="${GROUP_NAME} | ${INDEX} | ${SSH_SERVER_HOST} | ${SSH_SERVER_NAME} ${ALIASES} | ${SSH_SERVER_USER} | ${SSH_SERVER_PORT}" SERVERS+="${GROUP_NAME} | ${file} | ${INDEX} | ${SSH_SERVER_HOST} | ${SSH_SERVER_NAME} ${ALIASES} | ${SSH_SERVER_USER} | ${SSH_SERVER_PORT}"
INDEX=$((INDEX+1)) INDEX=$((INDEX+1))
done done
done done
shopt -u nullglob shopt -u nullglob
} }
load_known_hosts() {
local FILE_CONTENT GROUP_NAME
FILE_CONTENT=$(cat "$HOME"/.ssh/known_hosts | awk -F ' ' '{print $1}' | sort | awk '!seen[$1]++')
GROUP_NAME="Local"
for host in $FILE_CONTENT; do
if [ "$SERVERS" != "" ]; then
SERVERS+="\n"
fi
SERVERS+="${GROUP_NAME} | known_hosts | ${host} | ${host} | ${USE_KNOWN_HOSTS_USER} | 22"
done
}
popup_menu() { popup_menu() {
local SERVER local SERVER
SERVER=$(echo -e "${SERVERS}" | column -t -s "|" -o "|" | fzf -e --tac --with-nth=1,3,4,5,6 --delimiter="|") SERVER=$(echo -e "${SERVERS}" | column -t -s "|" -o "|" | fzf -e --layout=reverse --with-nth=1,4,5,6,7 --delimiter="|")
local GROUP_NAME HOST_INDEX local GROUP_NAME GROUP_SOURCE HOST_INDEX
GROUP_NAME=$(echo "$SERVER" | awk -F '|' '{print $1}' | xargs) GROUP_NAME=$(echo "$SERVER" | awk -F '|' '{print $1}' | xargs)
HOST_INDEX=$(echo "$SERVER" | awk -F '|' '{print $2}' | xargs) GROUP_SOURCE=$(echo "$SERVER" | awk -F '|' '{print $2}' | xargs)
ssh_connect "$GROUP_NAME" "$HOST_INDEX" HOST_INDEX=$(echo "$SERVER" | awk -F '|' '{print $3}' | xargs)
ssh_connect "$GROUP_SOURCE" "$HOST_INDEX"
} }
ssh_connect() { ssh_connect() {
local SERVER SSH_SERVER_USER SSH_SERVER_HOST SSH_SERVER_PORT SSH_SERVER_OPTIONS SSH_JUMP_HOST if [[ "$HOST_INDEX" != *"."* ]]; then
SERVER=$(cat "$HOSTS_DIR/$1.yaml" | yq -r '.servers'"[$2]") local SERVER SSH_SERVER_USER SSH_SERVER_HOST SSH_SERVER_PORT SSH_SERVER_OPTIONS SSH_JUMP_HOST
SSH_SERVER_USER=$(jq -r '.user // "'"$DEFAULT_SSH_USER"'"' <<<"$SERVER") SERVER=$(yq -r '.servers['"$2"']' "$1")
SSH_SERVER_HOST=$(jq -r '.host // ""' <<<"$SERVER") SSH_SERVER_USER=$(jq -r '.user // "'"$DEFAULT_SSH_USER"'"' <<<"$SERVER")
SSH_SERVER_PORT=$(jq -r '.port // "'"$DEFAULT_SSH_PORT"'"' <<<"$SERVER") SSH_SERVER_HOST=$(jq -r '.host // ""' <<<"$SERVER")
SSH_SERVER_PORT=$(jq -r '.port // "'"$DEFAULT_SSH_PORT"'"' <<<"$SERVER")
SSH_SERVER_OPTIONS="-p $SSH_SERVER_PORT" SSH_SERVER_OPTIONS="-p $SSH_SERVER_PORT"
SSH_JUMP_HOST=$(jq -r '.jump_host // ""' <<<"$SERVER") SSH_JUMP_HOST=$(jq -r '.jump_host // ""' <<<"$SERVER")
if [ "$SSH_JUMP_HOST" != "" ]; then if [ "$SSH_JUMP_HOST" != "" ]; then
SSH_JUMP_HOST=$(jq -r '.'"$SSH_JUMP_HOST"' // ""' <<<"${SSH_JUMP_HOSTS}") SSH_JUMP_HOST=$(jq -r '.'"$SSH_JUMP_HOST"' // ""' <<<"${SSH_JUMP_HOSTS}")
SSH_JUMP_HOST="-t $SSH_JUMP_HOST ssh -p $SSH_SERVER_PORT" SSH_JUMP_HOST_USER=$(cut -d'@' -f1 <<<"$SSH_JUMP_HOST")
SSH_SERVER_OPTIONS="" SSH_JUMP_HOST_TARGET=$(cut -d'@' -f2- <<<"$SSH_JUMP_HOST")
SSH_JUMP_HOST_TARGET_HOST=$(cut -d':' -f1 <<<"$SSH_JUMP_HOST_TARGET")
SSH_JUMP_HOST_TARGET_PORT=$(cut -s -d':' -f2 <<<"$SSH_JUMP_HOST_TARGET")
if [ "$SSH_JUMP_HOST_TARGET_PORT" = "" ]; then
SSH_JUMP_HOST_TARGET_PORT="22"
fi
SSH_JUMP_HOST="-t ${SSH_JUMP_HOST_USER}@${SSH_JUMP_HOST_TARGET_HOST} ssh -p ${SSH_JUMP_HOST_TARGET_PORT}"
SSH_SERVER_OPTIONS=""
fi
# shellcheck disable=SC2086
ssh ${SSH_SERVER_OPTIONS} ${SSH_JUMP_HOST} "${SSH_SERVER_USER}"@"${SSH_SERVER_HOST}"
else
ssh "${USE_KNOWN_HOSTS_USER}"@"${2}"
fi fi
#echo "ssh ${SSH_SERVER_OPTIONS} ${SSH_JUMP_HOST} ${SSH_SERVER_USER}@${SSH_SERVER_HOST}"
# shellcheck disable=SC2086
ssh ${SSH_SERVER_OPTIONS} ${SSH_JUMP_HOST} "${SSH_SERVER_USER}"@"${SSH_SERVER_HOST}"
} }
main() { main() {
if [ "$USE_KNOWN_HOSTS" == 1 ]; then
load_known_hosts
fi
load_config load_config
popup_menu popup_menu
} }
+2
View File
@@ -4,3 +4,5 @@ ssh:
default_options: "" default_options: ""
jump_hosts: jump_hosts:
office: "user@192.168.10.11" office: "user@192.168.10.11"
dev: "user@192.168.10.11:2222"
backup: "admin@10.0.0.5:2200"