use super::JobId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobSpec<'a> {
Current,
Previous,
Numeric(JobId),
Prefix(&'a str),
Substring(&'a str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobSpecError {
Malformed,
NoSuchJob,
Ambiguous,
}
pub fn parse_job_spec(s: &str) -> Result<JobSpec<'_>, JobSpecError> {
let rest = s.strip_prefix('%').ok_or(JobSpecError::Malformed)?;
match rest {
"" => Err(JobSpecError::Malformed),
"%" | "+" => Ok(JobSpec::Current),
"-" => Ok(JobSpec::Previous),
_ => {
if rest.bytes().all(|b| b.is_ascii_digit()) {
return rest
.parse::<JobId>()
.map(JobSpec::Numeric)
.map_err(|_| JobSpecError::Malformed);
}
if let Some(sub) = rest.strip_prefix('?') {
if sub.is_empty() {
return Err(JobSpecError::Malformed);
}
return Ok(JobSpec::Substring(sub));
}
Ok(JobSpec::Prefix(rest))
}
}
}
impl super::JobTable {
pub fn resolve_job_spec(&self, spec: &str) -> Result<JobId, JobSpecError> {
self.resolve(parse_job_spec(spec)?)
}
pub fn resolve(&self, spec: JobSpec<'_>) -> Result<JobId, JobSpecError> {
match spec {
JobSpec::Current => self.current.ok_or(JobSpecError::NoSuchJob),
JobSpec::Previous => self.previous.ok_or(JobSpecError::NoSuchJob),
JobSpec::Numeric(n) => {
if self.jobs.contains_key(&n) {
Ok(n)
} else {
Err(JobSpecError::NoSuchJob)
}
}
JobSpec::Prefix(s) => self.resolve_by(|cmd| cmd.starts_with(s)),
JobSpec::Substring(s) => self.resolve_by(|cmd| cmd.contains(s)),
}
}
fn resolve_by<F>(&self, mut pred: F) -> Result<JobId, JobSpecError>
where
F: FnMut(&str) -> bool,
{
let mut matched: Option<JobId> = None;
for job in self.jobs.values() {
if pred(&job.command) {
if matched.is_some() {
return Err(JobSpecError::Ambiguous);
}
matched = Some(job.id);
}
}
matched.ok_or(JobSpecError::NoSuchJob)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::env::jobs::JobTable;
use nix::unistd::Pid;
fn pid(n: i32) -> Pid {
Pid::from_raw(n)
}
#[test]
fn test_resolve_job_spec_numeric() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve_job_spec("%1"), Ok(id));
}
#[test]
fn test_resolve_job_spec_percent_percent() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve_job_spec("%%"), Ok(id));
}
#[test]
fn test_resolve_job_spec_plus() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve_job_spec("%+"), Ok(id));
}
#[test]
fn test_resolve_job_spec_minus() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(1), vec![pid(1)], "a", false);
let _id2 = table.add_job(pid(2), vec![pid(2)], "b", false);
assert_eq!(table.resolve_job_spec("%-"), Ok(id1));
}
#[test]
fn test_resolve_job_spec_invalid() {
let table = JobTable::default();
assert_eq!(table.resolve_job_spec("%99"), Err(JobSpecError::NoSuchJob));
assert_eq!(table.resolve_job_spec("foo"), Err(JobSpecError::Malformed));
assert_eq!(table.resolve_job_spec("%abc"), Err(JobSpecError::NoSuchJob));
}
#[test]
fn test_resolve_job_spec_ambiguous() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 10", false);
table.add_job(pid(2), vec![pid(2)], "sleep 20", false);
assert_eq!(
table.resolve_job_spec("%sleep"),
Err(JobSpecError::Ambiguous)
);
}
#[test]
fn test_parse_current_percent() {
assert_eq!(parse_job_spec("%%"), Ok(JobSpec::Current));
}
#[test]
fn test_parse_current_plus() {
assert_eq!(parse_job_spec("%+"), Ok(JobSpec::Current));
}
#[test]
fn test_parse_previous() {
assert_eq!(parse_job_spec("%-"), Ok(JobSpec::Previous));
}
#[test]
fn test_parse_numeric() {
assert_eq!(parse_job_spec("%1"), Ok(JobSpec::Numeric(1)));
assert_eq!(parse_job_spec("%42"), Ok(JobSpec::Numeric(42)));
}
#[test]
fn test_parse_numeric_overflow() {
assert_eq!(
parse_job_spec("%99999999999999999999"),
Err(JobSpecError::Malformed)
);
}
#[test]
fn test_parse_prefix() {
assert_eq!(parse_job_spec("%foo"), Ok(JobSpec::Prefix("foo")));
assert_eq!(parse_job_spec("%vim"), Ok(JobSpec::Prefix("vim")));
}
#[test]
fn test_parse_substring() {
assert_eq!(parse_job_spec("%?bar"), Ok(JobSpec::Substring("bar")));
assert_eq!(parse_job_spec("%?READ"), Ok(JobSpec::Substring("READ")));
}
#[test]
fn test_parse_prefix_hyphen() {
assert_eq!(parse_job_spec("%-foo"), Ok(JobSpec::Prefix("-foo")));
}
#[test]
fn test_parse_prefix_double_percent() {
assert_eq!(parse_job_spec("%%foo"), Ok(JobSpec::Prefix("%foo")));
}
#[test]
fn test_parse_malformed_empty() {
assert_eq!(parse_job_spec(""), Err(JobSpecError::Malformed));
}
#[test]
fn test_parse_malformed_bare_percent() {
assert_eq!(parse_job_spec("%"), Err(JobSpecError::Malformed));
}
#[test]
fn test_parse_malformed_bare_question() {
assert_eq!(parse_job_spec("%?"), Err(JobSpecError::Malformed));
}
#[test]
fn test_parse_malformed_no_percent() {
assert_eq!(parse_job_spec("foo"), Err(JobSpecError::Malformed));
assert_eq!(parse_job_spec("1"), Err(JobSpecError::Malformed));
}
#[test]
fn test_resolve_current() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve(JobSpec::Current), Ok(id));
}
#[test]
fn test_resolve_current_unset() {
let table = JobTable::default();
assert_eq!(
table.resolve(JobSpec::Current),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_previous() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(1), vec![pid(1)], "a", false);
let _id2 = table.add_job(pid(2), vec![pid(2)], "b", false);
assert_eq!(table.resolve(JobSpec::Previous), Ok(id1));
}
#[test]
fn test_resolve_previous_unset() {
let mut table = JobTable::default();
let _id = table.add_job(pid(1), vec![pid(1)], "a", false);
assert_eq!(
table.resolve(JobSpec::Previous),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_numeric_hit() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve(JobSpec::Numeric(id)), Ok(id));
}
#[test]
fn test_resolve_numeric_miss() {
let table = JobTable::default();
assert_eq!(
table.resolve(JobSpec::Numeric(99)),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_prefix_single() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "vim README.md", false);
table.add_job(pid(2), vec![pid(2)], "sleep 30", false);
assert_eq!(table.resolve(JobSpec::Prefix("vim")), Ok(id));
}
#[test]
fn test_resolve_prefix_none() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 30", false);
assert_eq!(
table.resolve(JobSpec::Prefix("vim")),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_prefix_ambiguous() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 10", false);
table.add_job(pid(2), vec![pid(2)], "sleep 20", false);
assert_eq!(
table.resolve(JobSpec::Prefix("sleep")),
Err(JobSpecError::Ambiguous)
);
}
#[test]
fn test_resolve_substring_single() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "vim README.md", false);
table.add_job(pid(2), vec![pid(2)], "sleep 30", false);
assert_eq!(table.resolve(JobSpec::Substring("EADME")), Ok(id));
}
#[test]
fn test_resolve_substring_none() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 30", false);
assert_eq!(
table.resolve(JobSpec::Substring("vim")),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_substring_ambiguous() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "cat foo", false);
table.add_job(pid(2), vec![pid(2)], "grep foo", false);
assert_eq!(
table.resolve(JobSpec::Substring("foo")),
Err(JobSpecError::Ambiguous)
);
}
#[test]
fn test_resolve_prefix_matches_done_job() {
use crate::env::jobs::JobStatus;
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "vim foo", false);
if let Some(job) = table.get_mut(id) {
job.status = JobStatus::Done(0);
}
assert_eq!(table.resolve(JobSpec::Prefix("vim")), Ok(id));
}
}