Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ Better host completion for ssh in Zsh.
[![asciicast](https://asciinema.org/a/381405.svg)](https://asciinema.org/a/381405)

- [zsh-ssh](#zsh-ssh)
- [Installation](#installation)
- [Zinit](#zinit)
- [Antigen](#antigen)
- [Oh My Zsh](#oh-my-zsh)
- [Sheldon](#sheldon)
- [Manual (Git Clone)](#manual-git-clone)
- [Usage](#usage)
- [SSH Config Example](#ssh-config-example)
- [Installation](#installation)
- [Zinit](#zinit)
- [Antigen](#antigen)
- [Oh My Zsh](#oh-my-zsh)
- [Sheldon](#sheldon)
- [Manual (Git Clone)](#manual-git-clone)
- [Usage](#usage)
- [SSH Config Example](#ssh-config-example)

## Installation

Expand All @@ -34,26 +34,26 @@ antigen bundle sunlei/zsh-ssh

1. Clone this repository into `$ZSH_CUSTOM/plugins` (by default `~/.oh-my-zsh/custom/plugins`)

```shell
git clone https://github.com/sunlei/zsh-ssh ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-ssh
```
```shell
git clone https://github.com/sunlei/zsh-ssh ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-ssh
```

2. Add the plugin to the list of plugins for Oh My Zsh to load (inside `~/.zshrc`):

```shell
plugins=(zsh-ssh $plugins)
```
```shell
plugins=(zsh-ssh $plugins)
```

3. Start a new terminal session.

### Sheldon

1. Add this config to `~/.config/sheldon/plugins.toml`

```toml
[plugins.zsh-ssh]
github = 'sunlei/zsh-ssh'
```
```toml
[plugins.zsh-ssh]
github = 'sunlei/zsh-ssh'
```

2. Run `sheldon lock` to install the plugin.

Expand All @@ -63,15 +63,15 @@ antigen bundle sunlei/zsh-ssh

1. Clone this repository somewhere on your machine. For example: `~/.zsh/zsh-ssh`.

```shell
git clone https://github.com/sunlei/zsh-ssh ~/.zsh/zsh-ssh
```
```shell
git clone https://github.com/sunlei/zsh-ssh ~/.zsh/zsh-ssh
```

2. Add the following to your `.zshrc`:

```shell
source ~/.zsh/zsh-ssh/zsh-ssh.zsh
```
```shell
source ~/.zsh/zsh-ssh/zsh-ssh.zsh
```

3. Start a new terminal session.

Expand All @@ -95,3 +95,17 @@ Host Development-Host
IdentityFile ~/.ssh/development-host
#_Desc For Development
```

Include files are also supported. For example, your main config can include separate files:

~/.ssh/config

```text
Include ~/.ssh/config.d/company.ssh_config
Include ~/.ssh/config.d/home.ssh_config
Include ~/.ssh/config.d/work.ssh_config

# OR

Include ~/.ssh/config.d/*.ssh_config
```
107 changes: 68 additions & 39 deletions zsh-ssh.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,92 @@ setopt no_beep # don't beep
zstyle ':completion:*:ssh:*' hosts off # disable built-in hosts completion

SSH_CONFIG_FILE="${SSH_CONFIG_FILE:-$HOME/.ssh/config}"
typeset -gi _zsh_ssh_parse_depth=0
typeset -gA _zsh_ssh_seen_config_files

# Parse the file and handle the include directive.
_parse_config_file() {
# Enable PCRE matching and handle local options
setopt localoptions rematchpcre
unsetopt nomatch

local input_path="$1"
local config_file_path
local line raw_path expanded include_file_path
local -a include_paths

# Resolve the full path of the input config file
local config_file_path=$(realpath "$1")

# Read the file line by line
while IFS= read -r line || [[ -n "$line" ]]; do
# Match lines starting with 'Include'
if [[ $line =~ ^[Ii]nclude[[:space:]=]+(.*) ]] && (( $#match > 0 )); then
# Split the rest of the line into individual paths
local include_paths=(${(z)match[1]})

for raw_path in "${include_paths[@]}"; do
# Expand ~ and environment variables in the path
eval "local expanded=\${(e)raw_path}"
# local expanded="${raw_path/#~/$HOME}"

# If path is relative, resolve it relative to the current config file
if [[ "$expanded" != /* ]]; then
if [[ "$expanded" == ~* ]]; then
config_file_path=$(realpath "$input_path" 2>/dev/null) || return 0

# If previous parse was interrupted, reset stale global state.
if (( _zsh_ssh_parse_depth <= 0 )); then
_zsh_ssh_parse_depth=0
unset _zsh_ssh_seen_config_files
typeset -gA _zsh_ssh_seen_config_files
fi

(( _zsh_ssh_parse_depth++ ))
{
if [[ -n "${_zsh_ssh_seen_config_files[$config_file_path]}" ]]; then
return 0
fi
_zsh_ssh_seen_config_files[$config_file_path]=1

# Read the file line by line
while IFS= read -r line || [[ -n "$line" ]]; do
# Match lines starting with 'Include'
if [[ $line =~ ^[[:space:]]*[Ii][Nn][Cc][Ll][Uu][Dd][Ee][[:space:]=]+(.*) ]] && (( $#match > 0 )); then
# Split the rest of the line into individual paths
include_paths=(${(z)match[1]})

for raw_path in "${include_paths[@]}"; do
# Expand ~ and environment variables in the path
expanded="${(e)raw_path}"

# Expand a literal leading ~ before testing whether the path is relative.
if [[ $expanded == '~'* ]]; then
expanded="${expanded/#\~/$HOME}"
else
expanded="$(dirname "$config_file_path")/$expanded"
fi
fi

# Expand wildcards (e.g. *.conf) and loop over each matched file
for include_file_path in $~expanded; do
if [[ -f "$include_file_path" ]]; then
# Separate includes with a blank line (for readability)
echo ""
# Recursively parse included files
_parse_config_file "$include_file_path"
# If path is relative, resolve it relative to the current config file
if [[ "$expanded" != /* ]]; then
expanded="$(dirname "$config_file_path")/$expanded"
fi

# Expand wildcards (e.g. *.conf) and loop over each matched file
for include_file_path in ${~expanded}(N); do
if [[ -f "$include_file_path" ]]; then
# Separate includes with a blank line (for readability)
echo ""
# Recursively parse included files
_parse_config_file "$include_file_path"
fi
done
done
done
else
# Print normal (non-Include) lines
echo "$line"
else
# Print normal (non-Include) lines
echo "$line"
fi
done < "$config_file_path"
} always {
(( _zsh_ssh_parse_depth-- ))
if (( _zsh_ssh_parse_depth <= 0 )); then
_zsh_ssh_parse_depth=0
unset _zsh_ssh_seen_config_files
typeset -gA _zsh_ssh_seen_config_files
fi
done < "$config_file_path"
}
}

_ssh_host_list() {
local ssh_config host_list

ssh_config=$(_parse_config_file $SSH_CONFIG_FILE)
ssh_config=$(echo $ssh_config | command grep -v -E "^\s*#[^_]")
ssh_config=$(_parse_config_file "$SSH_CONFIG_FILE")
ssh_config=$(printf "%s\n" "$ssh_config" | command grep -v -E "^\s*#[^_]")
# Ensure blank line before each Host/Match block for AWK paragraph mode (RS="")
ssh_config=$(echo $ssh_config | command awk '/^[[:space:]]*[Hh]ost[[:space:]]|^[[:space:]]*[Mm]atch[[:space:]]/{print ""} {print}')
ssh_config=$(printf "%s\n" "$ssh_config" | command awk '/^[[:space:]]*[Hh]ost[[:space:]]|^[[:space:]]*[Mm]atch[[:space:]]/{print ""} {print}')

host_list=$(echo $ssh_config | command awk '
host_list=$(printf "%s\n" "$ssh_config" | command awk '
function join(array, start, end, sep, result, i) {
# https://www.gnu.org/software/gawk/manual/html_node/Join-Function.html
if (sep == "")
Expand Down Expand Up @@ -183,7 +212,7 @@ _ssh_host_list() {
fi
host_list=$(printf "%s\n" "$host_list" | command sort -u)

echo $host_list
printf "%s\n" "$host_list"
}


Expand All @@ -201,9 +230,9 @@ Alias|->|Hostname|User|Desc
─────|──|────────|────|────
"

host_list="${header}\n${host_list}"
host_list="${header}"$'\n'"${host_list}"

echo $host_list | command column -t -s '|'
printf "%s\n" "$host_list" | command column -t -s '|'
}

_set_lbuffer() {
Expand Down