Compare commits

...

4 Commits

Author SHA1 Message Date
Guillaume Archambault
dc3fbc00d1 Add tag support and improve list display formatting
- Implement SSH host tagging with filtering capability
- Add --tag option to filter hosts by tag in list command
- Enhanced table formatting with dynamic column sizing
- Tags are sorted alphabetically for consistent display
2025-08-28 17:02:34 +02:00
Guillaume Archambault
37818f77a9 Feature: add automatic upgrade command 2025-08-28 15:12:35 +02:00
Guillaume Archambault
7c6c77d63d Feature: Optional ping check
- Make ping optional with 'sshm list --ping' instead of default behavior
- Update documentation
2025-08-28 14:48:18 +02:00
Guillaume Archambault
411199595f Fix host number selection to match display order in list command 2025-08-28 14:38:39 +02:00
2 changed files with 464 additions and 18 deletions

105
README.md
View File

@ -4,14 +4,16 @@ SSH Manager (sshm) is a bash script that simplifies and automates the management
## Features
- List all SSH hosts in the configuration file.
- Connect to an SSH host by name.
- List all SSH hosts in the configuration file (with optional ping check).
- Filter SSH hosts by tags for better organization.
- Connect to an SSH host by name or number from the list.
- View the configuration details of a specific SSH host.
- Add a new SSH host configuration.
- Edit an existing SSH host configuration.
- Delete an SSH host configuration.
- Check the availability of an SSH host using ping.
- Manage multiple SSH configuration contexts.
- Upgrade sshm to the latest version automatically.
## Requirements
@ -26,7 +28,7 @@ SSH Manager (sshm) is a bash script that simplifies and automates the management
1. Clone the repository:
```bash
git clone https://github.com/yourusername/sshm.git
git clone https://github.com/Gu1llaum-3/sshm.git
cd sshm
```
@ -50,12 +52,37 @@ SSH Manager (sshm) is a bash script that simplifies and automates the management
sshm list
```
To check host availability with ping (may be slower if hosts are unreachable):
```bash
sshm list --ping
```
To filter hosts by a specific tag:
```bash
sshm list --tag production
```
You can combine options:
```bash
sshm list --ping --tag staging
```
### Connect to an SSH Host
```bash
sshm <host>
```
You can also connect by selecting a number from the `sshm list` output:
```bash
sshm list
# Select a number when prompted, e.g., type "1" to connect to the first host
```
### View SSH Host Configuration
```bash
@ -124,8 +151,40 @@ sshm context delete <context_name>
Deletes the specified SSH configuration context.
### Upgrade sshm
```bash
sshm upgrade
```
Automatically checks for the latest version on GitHub and upgrades sshm if a newer version is available. The script will:
- Check for updates from the GitHub repository
- Display the current and available versions
- Show the installation path (`/usr/local/bin/sshm`)
- Ask for confirmation before proceeding
- Download and install the latest version
- Verify the installation
The upgrade will attempt to install to `/usr/local/bin/sshm` (may require sudo), and fall back to `~/.local/bin/sshm` if needed.
## Example
### Listing and Connecting to SSH Hosts
```bash
# Quick list without ping check (fast)
sshm list
# List with availability check (slower if hosts are down)
sshm list --ping
# Filter by specific tag
sshm list --tag production
# Combine filtering and ping check
sshm list --ping --tag staging
```
### Adding a New SSH Host
```bash
@ -138,6 +197,8 @@ You will be prompted to enter the following details:
- User (default: current user)
- Port (default: 22)
- IdentityFile (default: `~/.ssh/id_rsa`)
- ProxyJump (optional)
- Tags (optional, comma-separated)
### Editing an Existing SSH Host
@ -167,6 +228,44 @@ sshm context use myconfig
Switches to the `myconfig` SSH configuration context.
### Upgrading sshm
```bash
sshm upgrade
```
Checks for and installs the latest version of sshm. The command will show you the current version, the available version, and ask for confirmation before upgrading.
## Tags
SSH Manager supports tagging hosts for better organization and filtering. Tags are comma-separated labels that help you categorize your SSH hosts.
### Using Tags
When adding or editing a host, you can specify tags:
```bash
sshm add
# When prompted, enter tags like: production, webserver, ubuntu
```
### Filtering by Tags
Use the `--tag` option to filter hosts:
```bash
# Show only production hosts
sshm list --tag production
# Show only development hosts with ping check
sshm list --ping --tag development
```
### Tag Display
Tags are displayed in the list view with a `#` prefix and are automatically sorted alphabetically:
- Tags: `#database #production #ubuntu`
## License
This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.

377
sshm.bash
View File

@ -26,7 +26,7 @@ readonly BLUE='\033[0;34m'
readonly BOLD='\033[1m'
readonly NC='\033[0m' # No Color
readonly VERSION="2.1.0"
readonly VERSION="3.0.0"
readonly CONFIG_DIR="${HOME}/.config/sshm"
readonly DEFAULT_CONFIG="${HOME}/.ssh/config"
readonly CURRENT_CONTEXT_FILE="${CONFIG_DIR}/.current_context"
@ -61,19 +61,113 @@ sshm_version() {
# Compare with the current version
if [[ "$latest_version" != "$VERSION" ]]; then
echo -e "${YELLOW}A new version of sshm is available: $latest_version${NC} (current: $VERSION)"
echo -e "You can update by running: ${BOLD}git pull origin main${NC}"
echo -e "You can update by running: ${BOLD}sshm upgrade${NC}"
else
echo -e "${GREEN}This is the latest version${NC}"
fi
}
sshm_upgrade() {
echo -e "${BLUE}${BOLD}Checking for updates...${NC}"
echo
# Fetch the latest release tag from GitHub
local latest_version
latest_version=$(curl -s "https://api.github.com/repos/$GITHUB_REPO/releases/latest" | jq -r .tag_name)
if [[ "$latest_version" == "null" ]]; then
echo -e "${RED}Error: Unable to fetch the latest release from GitHub.${NC}" 1>&2
exit 1
fi
# Compare with the current version
if [[ "$latest_version" == "$VERSION" ]]; then
echo -e "${GREEN}You are already running the latest version ($VERSION)${NC}"
exit 0
fi
echo -e "${YELLOW}New version available: $latest_version${NC} (current: $VERSION)"
echo -e "${BLUE}Installation path: ${BOLD}/usr/local/bin/sshm${NC}"
echo
# Ask for confirmation
echo -ne "${BOLD}Do you want to upgrade to version $latest_version? [y/N]:${NC} "
read -r confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}Upgrade cancelled.${NC}"
exit 0
fi
echo -e "\n${BLUE}Downloading sshm $latest_version...${NC}"
# Create temporary directory
local tmp_dir
tmp_dir=$(mktemp -d)
local download_url="https://raw.githubusercontent.com/$GITHUB_REPO/$latest_version/sshm.bash"
# Download the new version
if ! curl -s -o "$tmp_dir/sshm.bash" "$download_url"; then
echo -e "${RED}Error: Failed to download the new version.${NC}" 1>&2
rm -rf "$tmp_dir"
exit 1
fi
# Verify the download
if [[ ! -s "$tmp_dir/sshm.bash" ]]; then
echo -e "${RED}Error: Downloaded file is empty.${NC}" 1>&2
rm -rf "$tmp_dir"
exit 1
fi
# Make it executable
chmod +x "$tmp_dir/sshm.bash"
echo -e "${BLUE}Installing sshm $latest_version to /usr/local/bin/sshm...${NC}"
# Install to /usr/local/bin (may require sudo)
if sudo cp "$tmp_dir/sshm.bash" "/usr/local/bin/sshm" 2>/dev/null; then
echo -e "${GREEN}✓ sshm successfully upgraded to version $latest_version${NC}"
echo -e "Installation location: ${BOLD}/usr/local/bin/sshm${NC}"
else
echo -e "${RED}Error: Failed to install to /usr/local/bin. Trying alternative locations...${NC}"
# Try alternative locations
if [[ -w "$HOME/.local/bin" ]] || mkdir -p "$HOME/.local/bin" 2>/dev/null; then
if cp "$tmp_dir/sshm.bash" "$HOME/.local/bin/sshm"; then
echo -e "${GREEN}✓ sshm successfully upgraded to version $latest_version${NC}"
echo -e "Installation location: ${BOLD}$HOME/.local/bin/sshm${NC}"
echo -e "${YELLOW}Note: Make sure $HOME/.local/bin is in your PATH${NC}"
else
echo -e "${RED}Error: Failed to install to $HOME/.local/bin${NC}" 1>&2
rm -rf "$tmp_dir"
exit 1
fi
else
echo -e "${RED}Error: No writable installation directory found.${NC}" 1>&2
echo -e "Please manually copy the file from: ${BOLD}$tmp_dir/sshm.bash${NC}"
exit 1
fi
fi
# Clean up
rm -rf "$tmp_dir"
echo -e "\n${BLUE}Verifying installation...${NC}"
if command -v sshm >/dev/null 2>&1; then
echo -e "${GREEN}✓ Installation verified successfully${NC}"
echo -e "Run ${BOLD}sshm version${NC} to confirm the new version."
else
echo -e "${YELLOW}Warning: sshm command not found in PATH. You may need to restart your shell or update your PATH.${NC}"
fi
}
sshm_help() {
echo -e "${BLUE}${BOLD}Usage:${NC} sshm [command] <command-specific-options>"
echo
echo -e "${BLUE}${BOLD}Commands:${NC}"
cat<<EOF | column -t -s $'\t'
<host> Connect directly to SSH host by name
list List SSH hosts and prompt for connection
list [--ping] [--tag <tag>] List SSH hosts and prompt for connection (--ping to check availability, --tag to filter by tag)
ping <name> Ping an SSH host to check availability
view <name> Check configuration of host
delete <name> Delete an SSH host from the configuration
@ -84,11 +178,32 @@ sshm_help() {
context delete <name> Delete an existing context
help Displays help
version Displays the current version
upgrade Upgrade sshm to the latest version
EOF
}
sshm_list() {
local config_file="$CONFIG_FILE"
local do_ping=false
local filter_tag=""
# Check for options
while [[ $# -gt 0 ]]; do
case $1 in
--ping)
do_ping=true
shift
;;
--tag)
filter_tag="$2"
shift 2
;;
*)
echo -e "${RED}Error: Unknown option $1${NC}" 1>&2
exit 1
;;
esac
done
# Check if the file exists and is not empty
if [[ ! -s "$config_file" ]]; then
@ -110,41 +225,196 @@ sshm_list() {
echo -e "\n${BLUE}${BOLD}Context: ${NC}${context_name}"
fi
echo -e "\n${BLUE}${BOLD}List of SSH hosts:${NC}"
if [[ -n "$filter_tag" ]]; then
if [[ "$do_ping" == true ]]; then
echo -e "\n${BLUE}${BOLD}List of SSH hosts with tag '$filter_tag' (with ping):${NC}"
else
echo -e "\n${BLUE}${BOLD}List of SSH hosts with tag '$filter_tag':${NC}"
fi
else
if [[ "$do_ping" == true ]]; then
echo -e "\n${BLUE}${BOLD}List of SSH hosts (with ping):${NC}"
else
echo -e "\n${BLUE}${BOLD}List of SSH hosts:${NC}"
fi
fi
# Create a temporary file to store results
local tmp_file
tmp_file=$(mktemp)
# Create a file to store the filtered host names in order
local filtered_hosts_file
filtered_hosts_file=$(mktemp)
# Process each host
while IFS= read -r line; do
host=$(echo "$line" | awk '{print $2}')
# Extract hostname from the host block
hostname=$(awk '/^Host '"$host"'$/,/^$/' "$config_file" | awk '/HostName/ {print $2}')
# Extract tags from the line immediately before the Host line
tags=$(awk '/^# Tags:.*/{tags=$0; getline; if($0 ~ /^Host '"$host"'$/) print tags}' "$config_file" | sed 's/^# Tags: //')
# Skip if no hostname found
if [[ -z "$hostname" ]]; then
continue
fi
# Filter by tag if specified
if [[ -n "$filter_tag" ]]; then
if [[ ! "$tags" =~ (^|,)[[:space:]]*$filter_tag[[:space:]]*(,|$) ]]; then
continue
fi
fi
if ping -c 1 -W 1 "$hostname" &> /dev/null; then
echo -e "${GREEN}${NC} $host ($hostname)" >> "$tmp_file"
# Store the host name in the filtered list
echo "$host" >> "$filtered_hosts_file"
# Format tags for display
if [[ -n "$tags" ]]; then
tags_display="[$tags]"
else
echo -e "${RED}${NC} $host ($hostname)" >> "$tmp_file"
tags_display=""
fi
if [[ "$do_ping" == true ]]; then
if ping -c 1 -W 1 "$hostname" &> /dev/null; then
echo -e "$host ($hostname) $tags_display" >> "$tmp_file"
else
echo -e "$host ($hostname) $tags_display" >> "$tmp_file"
fi
else
echo -e "$host ($hostname) $tags_display" >> "$tmp_file"
fi
done < <(grep -E '^Host ' "$config_file" | grep -v '^#' | sort)
# Display numbered results
nl "$tmp_file"
rm -f "$tmp_file"
# Check if we have any results
if [[ ! -s "$tmp_file" ]]; then
if [[ -n "$filter_tag" ]]; then
echo -e "\n${YELLOW}No hosts found with tag '$filter_tag'.${NC}"
else
echo -e "\n${YELLOW}No SSH hosts found.${NC}"
fi
rm -f "$tmp_file" "$filtered_hosts_file"
exit 0
fi
# Display results in a formatted table
echo
echo -ne "\n${BOLD}Enter the number or name of the host (or press Enter to exit):${NC} "
# First pass: calculate column widths
local max_host_len=4 # "Host" header length
local max_addr_len=7 # "Address" header length
local max_tags_len=4 # "Tags" header length
# Create arrays to store parsed data
declare -a hosts
declare -a addresses
declare -a statuses
declare -a tags_list
local counter=1
while IFS= read -r line; do
local status=""
local host_name=""
local address=""
local tags_part=""
# Check if line starts with status symbol
if [[ "$line" =~ ^[[:space:]]*[✓✗][[:space:]]+ ]]; then
status=$(echo "$line" | sed -E 's/^[[:space:]]*([✓✗])[[:space:]]+.*/\1/')
line=$(echo "$line" | sed -E 's/^[[:space:]]*[✓✗][[:space:]]+//')
fi
# Extract host and hostname: "host (hostname)"
if [[ "$line" =~ ^([^(]+)[[:space:]]*\(([^)]+)\)[[:space:]]*(.*)$ ]]; then
host_name=$(echo "${BASH_REMATCH[1]}" | xargs)
address="${BASH_REMATCH[2]}"
remainder="${BASH_REMATCH[3]}"
if [[ "$remainder" =~ ^\[([^]]*)\] ]]; then
tags_part="${BASH_REMATCH[1]}"
fi
else
host_name="$line"
fi
# Format tags
local formatted_tags=""
if [[ -n "$tags_part" ]]; then
IFS=',' read -ra TAG_ARRAY <<< "$tags_part"
# Sort tags alphabetically
IFS=$'\n' sorted_tags=($(sort <<<"${TAG_ARRAY[*]}"))
for tag in "${sorted_tags[@]}"; do
tag=$(echo "$tag" | xargs) # trim whitespace
if [[ -n "$formatted_tags" ]]; then
formatted_tags="$formatted_tags #${tag}"
else
formatted_tags="#${tag}"
fi
done
fi
# Store data and update max lengths
hosts[$counter]="$host_name"
addresses[$counter]="$address"
statuses[$counter]="$status"
tags_list[$counter]="$formatted_tags"
[[ ${#host_name} -gt $max_host_len ]] && max_host_len=${#host_name}
[[ ${#address} -gt $max_addr_len ]] && max_addr_len=${#address}
[[ ${#formatted_tags} -gt $max_tags_len ]] && max_tags_len=${#formatted_tags}
((counter++))
done < "$tmp_file"
# Add some padding
((max_host_len += 2))
((max_addr_len += 2))
((max_tags_len += 2))
# Print header based on ping option
if [[ "$do_ping" == true ]]; then
printf "%-4s %-${max_host_len}s %-${max_addr_len}s %-${max_tags_len}s %s\n" "No." "Host" "Address" "Tags" "Status"
printf "%-4s %-${max_host_len}s %-${max_addr_len}s %-${max_tags_len}s %s\n" "---" "$(printf '%*s' $max_host_len | tr ' ' '-')" "$(printf '%*s' $max_addr_len | tr ' ' '-')" "$(printf '%*s' $max_tags_len | tr ' ' '-')" "------"
else
printf "%-4s %-${max_host_len}s %-${max_addr_len}s %s\n" "No." "Host" "Address" "Tags"
printf "%-4s %-${max_host_len}s %-${max_addr_len}s %s\n" "---" "$(printf '%*s' $max_host_len | tr ' ' '-')" "$(printf '%*s' $max_addr_len | tr ' ' '-')" "----"
fi
# Print data rows
for ((i=1; i<counter; i++)); do
if [[ "$do_ping" == true ]]; then
# Add colors to status symbols
local colored_status=""
if [[ "${statuses[$i]}" == "✓" ]]; then
colored_status="${GREEN}${NC}"
elif [[ "${statuses[$i]}" == "✗" ]]; then
colored_status="${RED}${NC}"
else
colored_status="${statuses[$i]}"
fi
printf "%-4s %-${max_host_len}s %-${max_addr_len}s %-${max_tags_len}s " "$i" "${hosts[$i]}" "${addresses[$i]}" "${tags_list[$i]}"
echo -e "$colored_status"
else
printf "%-4s %-${max_host_len}s %-${max_addr_len}s %s\n" "$i" "${hosts[$i]}" "${addresses[$i]}" "${tags_list[$i]}"
fi
done
rm -f "$tmp_file"
echo
echo -ne "${BOLD}Enter the number or name of the host (or press Enter to exit):${NC} "
read host
if [[ -z "$host" ]]; then
echo "No host specified, exiting."
rm -f "$filtered_hosts_file"
exit 0
fi
sshm_connect "$config_file" "$host"
sshm_connect_filtered "$config_file" "$host" "$filtered_hosts_file"
rm -f "$filtered_hosts_file"
}
sshm_connect() {
@ -157,7 +427,40 @@ sshm_connect() {
if [[ "$host" =~ ^[0-9]+$ ]]; then
local host_name
host_name=$(grep -E '^Host ' "$config_file" | awk '{print $2}' | grep -v '^#' | sed -n "${host}p")
host_name=$(grep -E '^Host ' "$config_file" | awk '{print $2}' | grep -v '^#' | sort | sed -n "${host}p")
if [[ -n "$host_name" ]]; then
echo -e "\n${GREEN}Connecting to $host_name...${NC}\n"
ssh -F "$config_file" "$host_name"
else
echo -e "${RED}Error: Invalid host number.${NC}" 1>&2
exit 2
fi
else
# Check if the host exists in the SSH configuration
if ! grep -q "^Host $host$" "$config_file"; then
echo -e "${RED}Error: Host '$host' not found in SSH configuration.${NC}" 1>&2
echo -e "Use ${BOLD}sshm list${NC} to see available hosts or ${BOLD}sshm add $host${NC} to add it." 1>&2
exit 1
fi
echo -e "\n${GREEN}Connecting to $host...${NC}\n"
ssh -F "$config_file" "$host"
fi
}
sshm_connect_filtered() {
local config_file="$1"
local host="$2"
local filtered_hosts_file="$3"
if [[ -z "$host" ]]; then
echo -e "${RED}Error: please provide a host number or name.${NC}" 1>&2
exit 1
fi
if [[ "$host" =~ ^[0-9]+$ ]]; then
local host_name
host_name=$(sed -n "${host}p" "$filtered_hosts_file")
if [[ -n "$host_name" ]]; then
echo -e "\n${GREEN}Connecting to $host_name...${NC}\n"
ssh -F "$config_file" "$host_name"
@ -236,7 +539,34 @@ sshm_delete() {
# Create a temporary file for the new content
local tmp_file
tmp_file=$(mktemp)
sed '/^Host '"$host"'$/,/^$/d' "$config_file" > "$tmp_file"
# Remove host block including tags (look for "# Tags:" line before "Host")
awk '
/^# Tags:.*/ {
# Check if next non-empty line is the host we want to delete
tags_line = $0
while ((getline next_line) > 0) {
if (next_line ~ /^$/) continue
if (next_line ~ /^Host '"$host"'$/) {
# Skip this host block entirely
while ((getline) > 0 && !/^$/) continue
next
} else {
# Not our host, keep the tags line and the next line
print tags_line
print next_line
break
}
}
next
}
/^Host '"$host"'$/ {
# Skip this host block
while ((getline) > 0 && !/^$/) continue
next
}
{ print }
' "$config_file" > "$tmp_file"
# Check if the temporary file is not empty before overwriting
if [[ -s "$tmp_file" ]]; then
@ -300,12 +630,17 @@ sshm_add() {
read -p "Enter ProxyJump host (optional): " proxy_jump
read -p "Enter tags (comma-separated, optional): " tags
# Create the file if it doesn't exist
touch "$config_file"
# Add the new configuration
{
echo ""
if [[ -n "$tags" ]]; then
echo "# Tags: $tags"
fi
echo "Host $host"
echo " HostName $hostname"
echo " User $user"
@ -350,6 +685,9 @@ sshm_edit() {
local current_port=$(echo "$host_info" | awk '/Port/ {print $2}')
local current_identity_file=$(echo "$host_info" | awk '/IdentityFile/ {print $2}')
local current_proxyjump=$(echo "$host_info" | awk '/ProxyJump/ {print $2}')
# Extract tags from the line immediately before the Host line
local current_tags=$(awk '/^# Tags:.*/{tags=$0; getline; if($0 ~ /^Host '"$host"'$/) print tags}' "$config_file" | sed 's/^# Tags: //')
# Create backup of the original file
cp "$config_file" "$config_file.bak"
@ -377,6 +715,9 @@ sshm_edit() {
else
read -p "ProxyJump (leave empty if none): " new_proxyjump
fi
read -p "Tags [${current_tags}] (comma-separated): " new_tags
new_tags=${new_tags:-$current_tags}
# Create a temporary file for the new content
local tmp_file
@ -396,6 +737,9 @@ sshm_edit() {
# Add the new configuration
{
echo ""
if [[ -n "$new_tags" ]]; then
echo "# Tags: $new_tags"
fi
echo "Host $host"
echo " HostName $new_hostname"
echo " User $new_user"
@ -517,7 +861,7 @@ sshm_main() {
# Check if command is a known command, otherwise treat it as a host to connect to
case "$command" in
"list")
sshm_list
sshm_list "$@"
;;
"ping")
sshm_ping "$CONFIG_FILE" "$@"
@ -561,6 +905,9 @@ sshm_main() {
"version")
sshm_version
;;
"upgrade")
sshm_upgrade
;;
"help")
sshm_help
;;