feat: better error messages
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -116,12 +116,19 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lssh"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"glob",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -8,3 +8,4 @@ edition = "2024"
|
||||
[dependencies]
|
||||
clap = { version = "4.5.55", features = ["derive"] }
|
||||
glob = "0.3.3"
|
||||
log = { version = "0.4.29", features = ["std"] }
|
||||
|
||||
50
src/logger.rs
Normal file
50
src/logger.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::io::{self, IsTerminal};
|
||||
|
||||
use log::{Level, LevelFilter, Log};
|
||||
|
||||
const CSI: &str = "\x1B[";
|
||||
|
||||
struct Logger {
|
||||
is_terminal: bool,
|
||||
}
|
||||
|
||||
pub fn init(level: LevelFilter) {
|
||||
let logger = Logger {
|
||||
is_terminal: io::stderr().is_terminal(),
|
||||
};
|
||||
|
||||
log::set_boxed_logger(Box::new(logger)).expect("init only called once");
|
||||
log::set_max_level(level);
|
||||
}
|
||||
|
||||
impl Log for Logger {
|
||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||
metadata.level() <= log::max_level()
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
|
||||
match record.level() {
|
||||
Level::Error if self.is_terminal => eprint!("{CSI}1;31merror:{CSI}0m "),
|
||||
Level::Warn if self.is_terminal => eprint!("{CSI}1;33mwarn:{CSI}0m "),
|
||||
Level::Info if self.is_terminal => eprint!("{CSI}1minfo:{CSI}0m "),
|
||||
Level::Debug if self.is_terminal => eprint!("{CSI}1mdebug:{CSI}0m "),
|
||||
Level::Trace if self.is_terminal => eprint!("{CSI}1mtrace:{CSI}0m "),
|
||||
level => eprint!("{}: ", level.as_str().to_lowercase()),
|
||||
}
|
||||
|
||||
// let module_path = record.module_path().unwrap_or("lssh");
|
||||
// if self.is_terminal {
|
||||
// eprint!("{CSI}1m{module_path}:{CSI}0m ");
|
||||
// } else {
|
||||
// eprint!("{module_path}: ");
|
||||
// }
|
||||
|
||||
eprintln!("{}", record.args());
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
97
src/main.rs
97
src/main.rs
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user