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:
parent
9db419a3f8
commit
29995c7cb3
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -687,6 +687,7 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
"wezterm-bidi",
|
||||
"wezterm-input-types",
|
||||
"wezterm-ssh",
|
||||
"wezterm-term",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
@ -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]
|
||||
|
@ -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::*;
|
||||
|
@ -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
|
||||
|
||||
|
101
docs/config/lua/wezterm/enumerate_ssh_hosts.md
Normal file
101
docs/config/lua/wezterm/enumerate_ssh_hosts.md
Normal 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
|
||||
```
|
@ -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: {},
|
||||
|
Loading…
Reference in New Issue
Block a user