feat: better error messages

This commit is contained in:
2026-02-01 16:58:23 +01:00
parent 6108b3f481
commit 40fa301afe
4 changed files with 127 additions and 28 deletions

View File

@@ -11,6 +11,7 @@
//! [ssh(1)]: <https://manpages.debian.org/testing/manpages-de/ssh.1.de.html>
//! [ssh_config(5)]: <https://manpages.debian.org/bullseye/openssh-client/ssh_config.5.en.html>
mod logger;
mod ssh;
use std::{
@@ -24,6 +25,7 @@ use std::{
use clap::Parser;
use glob::glob;
use log::LevelFilter;
use crate::ssh::{SSH_SYSTEM_CONFIG, SSH_USER_CONFIG, Words, expand_inlude_args, is_host_pattern};
@@ -33,11 +35,48 @@ struct Cli {
/// This emulates the behaviour of the ssh(1) -F option
#[arg(short = 'F')]
file: Option<PathBuf>,
/// Increase logging verbosity
#[arg(short = 'v', action = clap::ArgAction::Count)]
verbose: u8,
}
macro_rules! continue_if_error {
($l:tt: $e:expr) => {
match $e {
Ok(v) => v,
Err(e) => {
log::$l!("{e}");
continue;
}
}
};
}
macro_rules! io_error {
($kind:ident) => {
Err(std::io::Error::new(
std::io::ErrorKind::$kind,
format!("{}", std::io::ErrorKind::$kind),
))
};
($kind:ident, $prefix:expr) => {
Err(io::Error::new(
std::io::ErrorKind::$kind,
format!("{}: {}", $prefix, std::io::ErrorKind::$kind),
))
};
}
fn main() {
let cli = Cli::parse();
logger::init(match cli.verbose {
0 => LevelFilter::Error,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
3.. => LevelFilter::Trace,
});
let filenames = match &cli.file {
Some(path) if path == "none" => vec![],
Some(path) => vec![path.clone()],
@@ -70,8 +109,14 @@ fn main() {
}
fn find_hosts(filename: &Path) -> io::Result<HashSet<String>> {
if !filename.is_file() {
return Err(io::Error::other("no such file"));
log::info!("reading config file: {}", filename.display());
if !filename.exists() {
return io_error!(NotFound, filename.display());
}
if filename.is_dir() {
return io_error!(IsADirectory, filename.display());
}
let mut reader = {
@@ -81,6 +126,7 @@ fn find_hosts(filename: &Path) -> io::Result<HashSet<String>> {
let mut hosts = HashSet::new();
let mut buf = String::new();
loop {
buf.clear();
let n = (&mut reader).take(1024 * 16).read_line(&mut buf)?;
@@ -97,60 +143,55 @@ fn find_hosts(filename: &Path) -> io::Result<HashSet<String>> {
};
match keyword {
k if k.starts_with('#') => {}
k if k.eq_ignore_ascii_case("include") => {
find_hosts_in_include_directive(&mut hosts, words);
}
k if k.eq_ignore_ascii_case("host") => {
find_hosts_in_host_directive(&mut hosts, words);
}
_ => continue,
k => {
log::trace!("skip unrelated keyword: '{k}'");
}
}
}
}
fn find_hosts_in_include_directive(hosts: &mut HashSet<String>, words: Words<'_>) {
for arg in words {
let Ok(arg) = arg else {
// TODO: print warning
continue;
};
let arg = continue_if_error!(warn: arg);
// Expanding env vars before globbing is what ssh does as well.
let Ok(pattern) = expand_inlude_args(arg) else {
// TODO: print warning
continue;
};
let pattern = continue_if_error!(warn: expand_inlude_args(arg));
let Ok(paths) = glob(&pattern) else {
// TODO: print warning
continue;
};
if arg != pattern {
log::debug!("expanded include '{arg}' -> '{pattern}'");
}
let paths = continue_if_error!(warn: glob(&pattern));
let mut matches = 0;
for path in paths {
// TODO: print warning
let Ok(path) = path else {
// TODO: print warning
continue;
};
matches += 1;
let Ok(ihosts) = find_hosts(&path) else {
// TODO: print warning
continue;
};
let path = continue_if_error!(error: path);
let ihosts = continue_if_error!(error: find_hosts(&path));
hosts.extend(ihosts);
}
if matches < 1 {
log::warn!("include {pattern} matched no files");
}
}
}
fn find_hosts_in_host_directive(hosts: &mut HashSet<String>, words: Words<'_>) {
for word in words {
let Ok(word) = word else {
// TODO: print warning
continue;
};
let word = continue_if_error!(warn: word);
if is_host_pattern(word) {
log::debug!("skip host pattern: '{word}'");
continue;
}