use std::ffi::{OsStr, OsString};
use std::fmt;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::PathBuf;
const SSH_VALUE_OPTS: &[u8] = b"BbcDEeFIiJLlmOoPpQRSWw";
const SSH_FLAG_OPTS: &[u8] = b"1246AaCfGgKkMNnqsTtVvXxYy";
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Invocation {
pub monitor: Option<OsString>,
pub monitor_long: Option<OsString>,
pub background: bool,
pub version: bool,
pub help: bool,
pub man: bool,
pub dry_run: bool,
pub list: bool,
pub session: Option<String>,
pub config: Option<PathBuf>,
pub ssh_args: Vec<OsString>,
pub inject_at: usize,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
MissingValue(String),
UnknownLongOption(String),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingValue(o) => write!(f, "option {o} requires an argument"),
Self::UnknownLongOption(o) => write!(f, "unknown option {o}"),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse<I, S>(argv: I) -> Result<Invocation, ParseError>
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
let argv: Vec<OsString> = argv.into_iter().map(Into::into).collect();
let mut inv = Invocation::default();
let mut saw_monitor = false;
let mut after_dashdash = false;
let mut i = 0;
while i < argv.len() {
let bytes = argv[i].as_bytes();
if after_dashdash {
inv.ssh_args.push(argv[i].clone());
i += 1;
continue;
}
if bytes == b"--" {
after_dashdash = true;
i += 1;
continue;
}
if bytes.starts_with(b"--") {
i += parse_long(&mut inv, &argv, i)?;
continue;
}
if bytes.len() < 2 || bytes[0] != b'-' {
inv.ssh_args.push(argv[i].clone());
i += 1;
continue;
}
let mut kept: Vec<u8> = vec![b'-'];
let mut detached: Option<OsString> = None;
let mut consumed_next = false;
let mut j = 1;
while j < bytes.len() {
let c = bytes[j];
j += 1;
match c {
b'M' => {
if !saw_monitor {
saw_monitor = true;
inv.inject_at = inv.ssh_args.len();
}
let rest = &bytes[j..];
if rest.is_empty() {
let v = argv
.get(i + 1)
.ok_or_else(|| ParseError::MissingValue("-M".into()))?;
inv.monitor = Some(v.clone());
consumed_next = true;
} else {
inv.monitor = Some(OsString::from_vec(rest.to_vec()));
}
break;
}
b'f' => inv.background = true,
b'V' => inv.version = true,
c if SSH_VALUE_OPTS.contains(&c) => {
kept.push(c);
let rest = &bytes[j..];
if rest.is_empty() {
if let Some(v) = argv.get(i + 1) {
detached = Some(v.clone());
consumed_next = true;
}
} else {
kept.extend_from_slice(rest);
}
break;
}
c if SSH_FLAG_OPTS.contains(&c) => kept.push(c),
_ => {
kept.extend_from_slice(&bytes[j - 1..]);
break;
}
}
}
if kept.len() > 1 {
inv.ssh_args.push(OsString::from_vec(kept));
}
if let Some(v) = detached {
inv.ssh_args.push(v);
}
i += 1 + usize::from(consumed_next);
}
Ok(inv)
}
fn parse_long(inv: &mut Invocation, argv: &[OsString], i: usize) -> Result<usize, ParseError> {
let body = &argv[i].as_bytes()[2..];
let (name, inline) = match body.iter().position(|&b| b == b'=') {
Some(p) => (&body[..p], Some(OsString::from_vec(body[p + 1..].to_vec()))),
None => (body, None),
};
let name = String::from_utf8_lossy(name).into_owned();
match name.as_str() {
"help" => {
inv.help = true;
Ok(1)
}
"version" => {
inv.version = true;
Ok(1)
}
"man" => {
inv.man = true;
Ok(1)
}
"dry-run" => {
inv.dry_run = true;
Ok(1)
}
"list" => {
inv.list = true;
Ok(1)
}
"monitor" => {
let (v, step) = value_for(&name, inline, argv, i)?;
inv.monitor_long = Some(v);
Ok(step)
}
"session" => {
let (v, step) = value_for(&name, inline, argv, i)?;
inv.session = Some(v.to_string_lossy().into_owned());
Ok(step)
}
"config" => {
let (v, step) = value_for(&name, inline, argv, i)?;
inv.config = Some(PathBuf::from(v));
Ok(step)
}
_ => Err(ParseError::UnknownLongOption(format!("--{name}"))),
}
}
fn value_for(
name: &str,
inline: Option<OsString>,
argv: &[OsString],
i: usize,
) -> Result<(OsString, usize), ParseError> {
match inline {
Some(v) => Ok((v, 1)),
None => argv
.get(i + 1)
.map(|v| (v.clone(), 2))
.ok_or_else(|| ParseError::MissingValue(format!("--{name}"))),
}
}
pub fn splice_forwards(args: &mut Vec<OsString>, at: usize, forwards: Vec<OsString>) {
let at = at.min(args.len());
args.splice(at..at, forwards);
}
pub fn quote(arg: &OsStr) -> String {
let s = arg.to_string_lossy();
if !s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"@%+=:,./-_".contains(&b))
{
s.into_owned()
} else {
format!("'{}'", s.replace('\'', r"'\''"))
}
}