1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-26 08:25:50 +03:00

add wezterm.enumerate_ssh_hosts() helper

This helper extracts the concrete set of hosts and their configurations
from the ssh config, and arranges to reload the wezterm config if they
are changed.

This is useful when constructing ssh domain configs.

refs: https://github.com/wez/wezterm/discussions/1731
This commit is contained in:
Wez Furlong 2022-03-18 07:42:05 -07:00
parent 9db419a3f8
commit 29995c7cb3
6 changed files with 275 additions and 19 deletions

1
Cargo.lock generated
View File

@ -687,6 +687,7 @@ dependencies = [
"unicode-segmentation",
"wezterm-bidi",
"wezterm-input-types",
"wezterm-ssh",
"wezterm-term",
"winapi 0.3.9",
]

View File

@ -42,6 +42,7 @@ umask = { path = "../umask" }
unicode-segmentation = "1.8"
wezterm-bidi = { path = "../bidi", features=["use_serde"] }
wezterm-input-types = { path = "../wezterm-input-types" }
wezterm-ssh = { path = "../wezterm-ssh" }
wezterm-term = { path = "../term", features=["use_serde"] }
[target."cfg(windows)".dependencies]

View File

@ -8,6 +8,7 @@ pub use luahelper::*;
use mlua::{FromLua, Lua, Table, ToLua, ToLuaMulti, Value, Variadic};
use serde::*;
use smol::prelude::*;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::path::Path;
use termwiz::cell::{grapheme_column_width, unicode_column_width, AttributeChange, CellAttributes};
@ -92,12 +93,7 @@ pub fn make_lua_context(config_file: &Path) -> anyhow::Result<Lua> {
lua.set_named_registry_value("wezterm-watch-paths", Vec::<String>::new())?;
wezterm_mod.set(
"add_to_config_reload_watch_list",
lua.create_function(|lua, args: Variadic<String>| {
let mut watch_paths: Vec<String> =
lua.named_registry_value("wezterm-watch-paths")?;
watch_paths.extend_from_slice(&args);
lua.set_named_registry_value("wezterm-watch-paths", watch_paths)
})?,
lua.create_function(add_to_config_reload_watch_list)?,
)?;
wezterm_mod.set("target_triple", crate::wezterm_target_triple())?;
@ -241,6 +237,10 @@ pub fn make_lua_context(config_file: &Path) -> anyhow::Result<Lua> {
wezterm_mod.set("strftime", lua.create_function(strftime)?)?;
wezterm_mod.set("battery_info", lua.create_function(battery_info)?)?;
wezterm_mod.set("gradient_colors", lua.create_function(gradient_colors)?)?;
wezterm_mod.set(
"enumerate_ssh_hosts",
lua.create_function(enumerate_ssh_hosts)?,
)?;
package.set("path", path_array.join(";"))?;
@ -921,6 +921,43 @@ fn gradient_colors<'lua>(
.collect())
}
fn add_to_config_reload_watch_list<'lua>(
lua: &'lua Lua,
args: Variadic<String>,
) -> mlua::Result<()> {
let mut watch_paths: Vec<String> = lua.named_registry_value("wezterm-watch-paths")?;
watch_paths.extend_from_slice(&args);
lua.set_named_registry_value("wezterm-watch-paths", watch_paths)?;
Ok(())
}
fn enumerate_ssh_hosts<'lua>(
lua: &'lua Lua,
config_files: Variadic<String>,
) -> mlua::Result<HashMap<String, wezterm_ssh::ConfigMap>> {
let mut config = wezterm_ssh::Config::new();
for file in config_files {
config.add_config_file(file);
}
config.add_default_config_files();
// Trigger a config reload if any of the parsed ssh config files change
let files: Variadic<String> = config
.loaded_config_files()
.into_iter()
.filter_map(|p| p.to_str().map(|s| s.to_string()))
.collect();
add_to_config_reload_watch_list(lua, files)?;
let mut map = HashMap::new();
for host in config.enumerate_hosts() {
let host_config = config.for_host(&host);
map.insert(host, host_config);
}
Ok(map)
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -30,6 +30,7 @@ As features stabilize some brief notes about them will accumulate here.
* Primary selection is now supported on Wayland systems that implement [primary-selection-unstable-v1](https://wayland.app/protocols/primary-selection-unstable-v1) or the older Gtk primary selection protocol. Thanks to [@lunaryorn](https://github.com/lunaryorn)! [#1423](https://github.com/wez/wezterm/issues/1423)
* [pane:has_unseen_output()](config/lua/pane/has_unseen_output.md) and [PaneInformation.has_unseen_output](config/lua/PaneInformation.md) allow coloring or marking up tabs based on unseen output. [#796](https://github.com/wez/wezterm/discussions/796)
* Context menu extension for Nautilus. Thanks to [@lunaryorn](https://github.com/lunaryorn)! [#1092](https://github.com/wez/wezterm/issues/1092)
* [wezterm.enumerate_ssh_hosts()](config/lua/wezterm/enumerate_ssh_hosts.md) function that can be used to auto-generate ssh domain configuration
#### Changed

View File

@ -0,0 +1,101 @@
# wezterm.enumerate_ssh_hosts(\[ssh_config_file_name, ...\])
*Since: nightly builds only*
This function will parse your ssh configuration file(s) and extract from them
the set of literal (non-pattern, non-negated) host names that are specified in
`Host` and `Match` stanzas contained in those configuration files and return a
mapping from the hostname to the effective ssh config options for that host.
You may optionally pass a list of ssh configuration files that should be read,
in case you have a special configuration.
The files you specify (if any) will be parsed first, and then the default
locations for your system will be parsed.
All files read by a call to this function, and any `include` statements
processed from those ssh config files, will be added to the config reload watch
list as though
[wezterm.add_to_config_reload_watch_list()](add_to_config_reload_watch_list.md)
was called on them. Note that only concrete path names are watched: if your
config uses `include` to include glob patterns in a directory then, for
example, newly created files in that directory will not cause a config reload
event in wezterm.
This example shows how to use this function to automatically configure ssh
multiplexing domains for the hosts configured in your `~/.ssh/config` file:
```lua
local wezterm = require 'wezterm'
local ssh_domains = {}
for host, config in pairs(wezterm.enumerate_ssh_hosts()) do
table.insert(ssh_domains, {
-- the name can be anything you want; we're just using the hostname
name = host,
-- remote_address must be set to `host` for the ssh config to apply to it
remote_address = host,
-- if you don't have wezterm's mux server installed on the remote
-- host, you may wish to set multiplexing = "None" to use a direct
-- ssh connection that supports multiple panes/tabs which will close
-- when the connection is dropped.
-- multiplexing = "None",
-- if you know that the remote host has a posix/unix environment,
-- setting assume_shell = "Posix" will result in new panes respecting
-- the remote current directory when multiplexing = "None".
assume_shell = "Posix",
})
end
return {
ssh_domains = ssh_domains,
}
```
This shows the structure of the returned data, by evaluating the function in the [debug overlay](../keyassignment/ShowDebugOverlay.md) (`CTRL-SHIFT-L`):
```
> wezterm.enumerate_ssh_hosts()
{
"aur.archlinux.org": {
"hostname": "aur.archlinux.org",
"identityagent": "/run/user/1000/keyring/ssh",
"identityfile": "/home/wez/.ssh/aur",
"port": "22",
"user": "aur",
"userknownhostsfile": "/home/wez/.ssh/known_hosts /home/wez/.ssh/known_hosts2",
},
"woot": {
"hostname": "localhost",
"identityagent": "/run/user/1000/keyring/ssh",
"identityfile": "/home/wez/.ssh/id_dsa /home/wez/.ssh/id_ecdsa /home/wez/.ssh/id_ed25519 /home/wez/.ssh/id_rsa",
"port": "22",
"user": "someone",
"userknownhostsfile": "/home/wez/.ssh/known_hosts /home/wez/.ssh/known_hosts2",
},
}
```
the corresponding `~/.ssh/config` file for the above is shown below: note host
the `Host` group with a wildcard is not returned by the function because it
doesn't have a concrete host name:
```
Host aur.archlinux.org
IdentityFile ~/.ssh/aur
User aur
Host 192.168.1.*
ForwardAgent yes
ForwardX11 yes
Host woot
User someone
Hostname localhost
```

View File

@ -1,7 +1,7 @@
//! Parse an ssh_config(5) formatted config file
use regex::{Captures, Regex};
use std::collections::BTreeMap;
use std::path::Path;
use std::path::{Path, PathBuf};
pub type ConfigMap = BTreeMap<String, String>;
@ -10,24 +10,29 @@ pub type ConfigMap = BTreeMap<String, String>;
struct Pattern {
negated: bool,
pattern: String,
original: String,
is_literal: bool,
}
/// Compile a glob style pattern string into a regex pattern string
fn wildcard_to_pattern(s: &str) -> String {
fn wildcard_to_pattern(s: &str) -> (String, bool) {
let mut pattern = String::new();
let mut is_literal = true;
pattern.push('^');
for c in s.chars() {
if c == '*' {
pattern.push_str(".*");
is_literal = false;
} else if c == '?' {
pattern.push('.');
is_literal = false;
} else {
let s = regex::escape(&c.to_string());
pattern.push_str(&s);
}
}
pattern.push('$');
pattern
(pattern, is_literal)
}
impl Pattern {
@ -41,9 +46,12 @@ impl Pattern {
}
fn new(text: &str, negated: bool) -> Self {
let (pattern, is_literal) = wildcard_to_pattern(text);
Self {
pattern: wildcard_to_pattern(text),
pattern,
is_literal,
negated,
original: text.to_string(),
}
}
@ -137,16 +145,27 @@ struct ParsedConfigFile {
options: ConfigMap,
/// options inside a `Host` stanza
groups: Vec<MatchGroup>,
/// list of loaded file names
loaded_files: Vec<PathBuf>,
}
impl ParsedConfigFile {
fn parse(s: &str, cwd: Option<&Path>) -> Self {
fn parse(s: &str, cwd: Option<&Path>, source_file: Option<&Path>) -> Self {
let mut options = ConfigMap::new();
let mut groups = vec![];
let mut loaded_files = vec![];
Self::parse_impl(s, cwd, &mut options, &mut groups);
if let Some(source) = source_file {
loaded_files.push(source.to_path_buf());
}
Self { options, groups }
Self::parse_impl(s, cwd, &mut options, &mut groups, &mut loaded_files);
Self {
options,
groups,
loaded_files,
}
}
fn do_include(
@ -154,6 +173,7 @@ impl ParsedConfigFile {
cwd: Option<&Path>,
options: &mut ConfigMap,
groups: &mut Vec<MatchGroup>,
loaded_files: &mut Vec<PathBuf>,
) {
match filenamegen::Glob::new(&pattern) {
Ok(g) => {
@ -171,7 +191,14 @@ impl ParsedConfigFile {
};
match std::fs::read_to_string(&path) {
Ok(data) => {
Self::parse_impl(&data, Some(&cwd), options, groups);
loaded_files.push(path.clone());
Self::parse_impl(
&data,
Some(&cwd),
options,
groups,
loaded_files,
);
}
Err(err) => {
log::error!(
@ -203,6 +230,7 @@ impl ParsedConfigFile {
cwd: Option<&Path>,
options: &mut ConfigMap,
groups: &mut Vec<MatchGroup>,
loaded_files: &mut Vec<PathBuf>,
) {
for line in s.lines() {
let line = line.trim();
@ -250,7 +278,7 @@ impl ParsedConfigFile {
}
if k == "include" {
Self::do_include(v, cwd, options, groups);
Self::do_include(v, cwd, options, groups, loaded_files);
continue;
}
@ -422,15 +450,18 @@ impl Config {
/// and add that to the list of configs.
pub fn add_config_string(&mut self, config_string: &str) {
self.config_files
.push(ParsedConfigFile::parse(config_string, None));
.push(ParsedConfigFile::parse(config_string, None, None));
}
/// Open `path`, read its contents and parse it as an `ssh_config` file,
/// adding that to the list of configs
pub fn add_config_file<P: AsRef<Path>>(&mut self, path: P) {
if let Ok(data) = std::fs::read_to_string(path.as_ref()) {
self.config_files
.push(ParsedConfigFile::parse(&data, path.as_ref().parent()));
self.config_files.push(ParsedConfigFile::parse(
&data,
path.as_ref().parent(),
Some(path.as_ref()),
));
}
}
@ -484,7 +515,7 @@ impl Config {
}
if needs_reparse {
log::warn!(
log::debug!(
"ssh configuration uses options that require two-phase \
parsing, which isn't supported"
);
@ -637,6 +668,47 @@ impl Config {
})
.to_string();
}
/// Returns the list of file names that were loaded as part of parsing
/// the ssh config
pub fn loaded_config_files(&self) -> Vec<PathBuf> {
let mut files = vec![];
for config in &self.config_files {
for file in &config.loaded_files {
if !files.contains(file) {
files.push(file.to_path_buf());
}
}
}
files
}
/// Returns the list of host names that have defined ssh config entries.
/// The host names are literal (non-pattern), non-negated hosts extracted
/// from `Host` and `Match` stanzas in the ssh config.
pub fn enumerate_hosts(&self) -> Vec<String> {
let mut hosts = vec![];
for config in &self.config_files {
for group in &config.groups {
for c in &group.criteria {
if let Criteria::Host(patterns) = c {
for pattern in patterns {
if pattern.is_literal && !pattern.negated {
if !hosts.contains(&pattern.original) {
hosts.push(pattern.original.clone());
}
}
}
}
}
}
}
hosts
}
}
#[cfg(test)]
@ -677,6 +749,8 @@ Config {
Pattern {
negated: false,
pattern: "^foo$",
original: "foo",
is_literal: true,
},
],
),
@ -689,6 +763,7 @@ Config {
},
},
],
loaded_files: [],
},
],
options: {},
@ -834,10 +909,14 @@ Config {
Pattern {
negated: false,
pattern: "^192\\.168\\.1\\.8$",
original: "192.168.1.8",
is_literal: true,
},
Pattern {
negated: false,
pattern: "^wopr$",
original: "wopr",
is_literal: true,
},
],
),
@ -855,10 +934,14 @@ Config {
Pattern {
negated: true,
pattern: "^a\\.b$",
original: "a.b",
is_literal: true,
},
Pattern {
negated: false,
pattern: "^.*\\.b$",
original: "*.b",
is_literal: false,
},
],
),
@ -867,6 +950,8 @@ Config {
Pattern {
negated: false,
pattern: "^fred$",
original: "fred",
is_literal: true,
},
],
),
@ -884,10 +969,14 @@ Config {
Pattern {
negated: true,
pattern: "^a\\.b$",
original: "a.b",
is_literal: true,
},
Pattern {
negated: false,
pattern: "^.*\\.b$",
original: "*.b",
is_literal: false,
},
],
),
@ -896,6 +985,8 @@ Config {
Pattern {
negated: false,
pattern: "^me$",
original: "me",
is_literal: true,
},
],
),
@ -913,6 +1004,8 @@ Config {
Pattern {
negated: false,
pattern: "^.*$",
original: "*",
is_literal: false,
},
],
),
@ -923,6 +1016,7 @@ Config {
},
},
],
loaded_files: [],
},
],
options: {},
@ -937,6 +1031,16 @@ Config {
"#
);
snapshot!(
config.enumerate_hosts(),
r#"
[
"192.168.1.8",
"wopr",
]
"#
);
let opts = config.for_host("random");
snapshot!(
opts,
@ -1068,10 +1172,14 @@ Config {
Pattern {
negated: false,
pattern: "^192\\.168\\.1\\.8$",
original: "192.168.1.8",
is_literal: true,
},
Pattern {
negated: false,
pattern: "^wopr$",
original: "wopr",
is_literal: true,
},
],
),
@ -1089,10 +1197,14 @@ Config {
Pattern {
negated: true,
pattern: "^a\\.b$",
original: "a.b",
is_literal: true,
},
Pattern {
negated: false,
pattern: "^.*\\.b$",
original: "*.b",
is_literal: false,
},
],
),
@ -1110,6 +1222,8 @@ Config {
Pattern {
negated: false,
pattern: "^.*$",
original: "*",
is_literal: false,
},
],
),
@ -1120,6 +1234,7 @@ Config {
},
},
],
loaded_files: [],
},
],
options: {},