use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::failure::Failure;
use crate::rig::wire::{Account, Pid, Stamp, field};
use bash_strings::parse_array;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub build: u32,
pub status: String,
pub machine: String,
}
impl Version {
pub fn at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
(self.major, self.minor, self.patch) >= (major, minor, patch)
}
fn of(literal: &str) -> Result<Self, Failure> {
let parts = parse_array(literal).map_err(|cause| {
broken(format!(
"the version {literal:?}: {cause}"
))
})?;
let [major, minor, patch, build, status, machine] = parts.as_slice() else {
return Err(broken(format!(
"a version of {} parts",
parts.len()
)));
};
let count = |what: &str, text: &str| text.parse().map_err(|_| broken(format!("{what} {text:?}")));
Ok(Self {
major: count("a major version", major)?,
minor: count("a minor version", minor)?,
patch: count("a patch level", patch)?,
build: count("a build number", build)?,
status: status.clone(),
machine: machine.clone(),
})
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}.{}.{}({})-{}",
self.major, self.minor, self.patch, self.build, self.status
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Invocation {
pub command: Option<String>,
pub standard_input: bool,
pub interactive: bool,
}
impl Invocation {
pub fn from_a_file(&self) -> bool {
self.command.is_none() && !self.standard_input
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Flags(String);
impl Flags {
pub fn has(&self, flag: char) -> bool {
self.0.contains(flag)
}
}
impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Options {
pub flags: Flags,
pub shellopts: Vec<String>,
pub bashopts: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bash {
pub version: Version,
pub binary: PathBuf,
pub zero: String,
pub invocation: Invocation,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Shell {
pub nth: usize,
pub pid: Pid,
pub shlvl: u32,
pub subshell: u32,
pub joined: Stamp,
pub bash: Bash,
pub options: Options,
pub brought: Vec<String>,
}
impl Shell {
pub(crate) fn of(nth: usize, account: Account) -> Result<Self, Failure> {
let Account {
stamp: joined,
words,
} = account;
let word = |key: &str| {
field(&words, key)
.ok_or_else(|| broken(format!("no {key:?}")))
.map(str::to_string)
};
let count = |key: &str| -> Result<u32, Failure> {
let text = word(key)?;
text.parse().map_err(|_| broken(format!("{key} {text:?}")))
};
let split = |key: &str| -> Result<Vec<String>, Failure> {
Ok(word(key)?
.split(':')
.filter(|opt| !opt.is_empty())
.map(String::from)
.collect())
};
let flags = word("flags")?;
let command = word("command")?;
let brought = parse_array(&word("brought")?).map_err(|cause| broken(format!("the brought words: {cause}")))?;
Ok(Self {
nth,
pid: Pid(count("pid")?),
shlvl: count("shlvl")?,
subshell: count("subshell")?,
joined,
bash: Bash {
version: Version::of(&word("versinfo")?)?,
binary: PathBuf::from(word("bash")?),
zero: word("zero")?,
invocation: Invocation {
command: flags.contains('c').then_some(command),
standard_input: flags.contains('s'),
interactive: flags.contains('i'),
},
},
options: Options {
shellopts: split("shellopts")?,
bashopts: split("bashopts")?,
flags: Flags(flags),
},
brought,
})
}
}
fn broken(what: String) -> Failure {
Failure::new(
"reading what a shell said of itself",
what,
)
}