use std::time::Duration;
use clap::{Parser, Subcommand as ClapSubcommand, ValueEnum};
use crate::clock::PollClock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Shell {
Bash,
Zsh,
Fish,
Powershell,
}
#[derive(Debug, Parser)]
#[command(
name = "rusty-autossh",
bin_name = "rusty-autossh",
version,
about = "Keep an SSH tunnel alive across drops (Rust port of autossh(1))",
disable_help_subcommand = true,
disable_version_flag = true,
trailing_var_arg = true,
allow_hyphen_values = true
)]
pub struct Cli {
#[arg(short = 'M', long = "monitor-port", value_name = "PORT[:ECHO]")]
pub monitor: Option<String>,
#[arg(short = 'f', long = "background")]
pub background: bool,
#[arg(short = 'V', long = "version", action = clap::ArgAction::Version)]
pub print_version: Option<bool>,
#[arg(short = '1', long = "one-shot")]
pub one_shot: bool,
#[arg(long = "poll", value_name = "SECS")]
pub poll: Option<u64>,
#[arg(long = "first-poll", value_name = "SECS")]
pub first_poll: Option<u64>,
#[arg(long = "gate-time", value_name = "SECS")]
pub gate_time: Option<u64>,
#[arg(long = "max-start", value_name = "N", allow_negative_numbers = true)]
pub max_start: Option<i64>,
#[arg(long = "max-lifetime", value_name = "SECS")]
pub max_lifetime: Option<u64>,
#[arg(long = "ssh-path", value_name = "PATH")]
pub ssh_path: Option<std::path::PathBuf>,
#[arg(long = "pid-file", value_name = "PATH")]
pub pid_file: Option<std::path::PathBuf>,
#[arg(long = "log-file", value_name = "PATH")]
pub log_file: Option<std::path::PathBuf>,
#[arg(long = "debug")]
pub debug: bool,
#[arg(long = "log-level", value_name = "LEVEL")]
pub log_level: Option<String>,
#[arg(long = "strict", conflicts_with = "no_strict")]
pub strict: bool,
#[arg(long = "no-strict")]
pub no_strict: bool,
#[command(subcommand)]
pub command: Option<Subcommand>,
#[arg(trailing_var_arg = true)]
pub ssh_args: Vec<String>,
}
#[derive(Debug, ClapSubcommand)]
pub enum Subcommand {
Completions {
shell: Shell,
},
}
pub fn split_autossh_args(argv: &[String]) -> (Vec<String>, Vec<String>) {
let mut autossh = Vec::new();
let mut ssh = Vec::new();
let mut i = 0;
while i < argv.len() {
let tok = &argv[i];
if tok == "--" {
ssh.extend(argv[i + 1..].iter().cloned());
break;
}
if is_autossh_known_flag(tok) {
autossh.push(tok.clone());
if flag_takes_value(tok) && i + 1 < argv.len() {
autossh.push(argv[i + 1].clone());
i += 2;
continue;
}
i += 1;
continue;
}
ssh.extend(argv[i..].iter().cloned());
break;
}
(autossh, ssh)
}
fn is_autossh_known_flag(tok: &str) -> bool {
matches!(
tok,
"-M" | "-f"
| "-V"
| "-1"
| "--monitor-port"
| "--background"
| "--version"
| "--one-shot"
| "--poll"
| "--first-poll"
| "--gate-time"
| "--max-start"
| "--max-lifetime"
| "--ssh-path"
| "--pid-file"
| "--log-file"
| "--debug"
| "--log-level"
| "--strict"
| "--no-strict"
) || tok.starts_with("--monitor-port=")
|| tok.starts_with("--poll=")
|| tok.starts_with("--first-poll=")
|| tok.starts_with("--gate-time=")
|| tok.starts_with("--max-start=")
|| tok.starts_with("--max-lifetime=")
|| tok.starts_with("--ssh-path=")
|| tok.starts_with("--pid-file=")
|| tok.starts_with("--log-file=")
|| tok.starts_with("--log-level=")
|| tok.starts_with("-M") }
fn flag_takes_value(tok: &str) -> bool {
matches!(
tok,
"-M" | "--monitor-port"
| "--poll"
| "--first-poll"
| "--gate-time"
| "--max-start"
| "--max-lifetime"
| "--ssh-path"
| "--pid-file"
| "--log-file"
| "--log-level"
)
}
pub fn apply_dash_f_overrides(clock: &mut PollClock, dash_f: bool) {
if dash_f {
clock.gate_time = Duration::ZERO;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_separator_dash_dash() {
let argv = vec![
"-M".to_string(),
"20000".to_string(),
"--".to_string(),
"--strict".to_string(),
"user@host".to_string(),
];
let (a, s) = split_autossh_args(&argv);
assert_eq!(a, vec!["-M".to_string(), "20000".to_string()]);
assert_eq!(s, vec!["--strict".to_string(), "user@host".to_string()]);
}
#[test]
fn split_first_unrecognized_starts_ssh_args() {
let argv = vec![
"-f".to_string(),
"user@host".to_string(),
"-L".to_string(),
"8080:localhost:80".to_string(),
];
let (a, s) = split_autossh_args(&argv);
assert_eq!(a, vec!["-f".to_string()]);
assert_eq!(
s,
vec![
"user@host".to_string(),
"-L".to_string(),
"8080:localhost:80".to_string(),
]
);
}
#[test]
fn apply_dash_f_zeros_gate_time() {
let mut clock = PollClock::default();
apply_dash_f_overrides(&mut clock, true);
assert_eq!(clock.gate_time, Duration::ZERO);
}
}