use std::ffi::{CStr, CString};
use std::ptr;
use std::str::FromStr;
use std::sync::LazyLock;
use bitflags::bitflags;
use itertools::Itertools;
use crate::error::{Error, ok_or_error};
use crate::{ExecStatus, bash};
bitflags! {
#[derive(Debug, Default)]
pub struct Flags: u32 {
const NONE = 0;
const WANT_SUBSHELL = bash::CMD_WANT_SUBSHELL;
const FORCE_SUBSHELL = bash::CMD_FORCE_SUBSHELL;
const INVERT_RETURN = bash::CMD_INVERT_RETURN;
const IGNORE_RETURN = bash::CMD_IGNORE_RETURN;
const NO_FUNCTIONS = bash::CMD_NO_FUNCTIONS;
const INHIBIT_EXPANSION = bash::CMD_INHIBIT_EXPANSION;
const NO_FORK = bash::CMD_NO_FORK;
}
}
pub fn current_command_string() -> crate::Result<String> {
unsafe {
bash::CURRENT_COMMAND_STRING
.as_ref()
.map(|p| CStr::from_ptr(p).to_string_lossy().to_string())
.ok_or_else(|| Error::Base("no running command".to_string()))
}
}
pub fn current_command_name() -> crate::Result<String> {
unsafe {
bash::CURRENT_COMMAND_NAME
.as_ref()
.map(|p| CStr::from_ptr(p).to_string_lossy().to_string())
.ok_or_else(|| Error::Base("no running command".to_string()))
}
}
#[derive(Debug, Default)]
pub struct Command {
pub args: Vec<String>,
flags: Flags,
}
impl Command {
pub fn new<S: std::fmt::Display>(program: S) -> Self {
Self {
args: vec![program.to_string()],
..Default::default()
}
}
pub fn args<I>(&mut self, args: I) -> &mut Self
where
I: IntoIterator,
I::Item: Into<String>,
{
self.args.extend(args.into_iter().map(Into::into));
self
}
pub fn subshell(&mut self, value: bool) -> &mut Self {
if value {
self.flags = Flags::FORCE_SUBSHELL;
}
self
}
pub fn invert(&mut self, value: bool) -> &mut Self {
if value {
self.flags = Flags::INVERT_RETURN;
}
self
}
pub fn execute(&self) -> crate::Result<ExecStatus> {
let cmd: BashCommand = self.try_into()?;
cmd.execute()
}
}
impl FromStr for Command {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
args: s.split(' ').map(Into::into).collect(),
..Default::default()
})
}
}
impl TryFrom<&Command> for BashCommand {
type Error = Error;
fn try_from(value: &Command) -> crate::Result<Self> {
let cmd: Self = value.args.iter().join(" ").parse()?;
unsafe { (*cmd.0).flags |= value.flags.bits() as i32 };
Ok(cmd)
}
}
#[derive(Debug)]
struct BashCommand(*mut bash::Command);
impl BashCommand {
fn execute(&self) -> crate::Result<ExecStatus> {
ok_or_error(|| match unsafe { bash::scallop_execute_command(self.0) } {
0 => Ok(ExecStatus::Success),
n => Err(Error::Status(n)),
})
}
}
impl FromStr for BashCommand {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cmd_str = CString::new(s).unwrap();
let cmd_ptr = cmd_str.as_ptr() as *mut _;
let name_ptr = COMMAND_MARKER.as_ptr();
unsafe {
bash::push_stream(1);
bash::with_input_from_string(cmd_ptr, name_ptr);
let cmd = match bash::parse_command() {
0 => bash::copy_command(bash::GLOBAL_COMMAND),
_ => return Err(Error::Base(format!("failed parsing: {s}"))),
};
(*(*cmd).value.Simple).line = 0;
bash::dispose_command(bash::GLOBAL_COMMAND);
bash::GLOBAL_COMMAND = ptr::null_mut();
bash::pop_stream();
Ok(Self(cmd))
}
}
}
impl Drop for BashCommand {
fn drop(&mut self) {
unsafe { bash::dispose_command(self.0) };
}
}
static COMMAND_MARKER: LazyLock<CString> =
LazyLock::new(|| CString::new("Command::from_str").unwrap());
#[cfg(test)]
mod tests {
use crate::source;
use crate::test::assert_err_re;
use crate::variables::optional;
use super::*;
#[test]
fn new_and_execute() {
let cmd: Command = "VAR=0".parse().unwrap();
cmd.execute().unwrap();
assert_eq!(optional("VAR").unwrap(), "0");
let mut cmd: Command = "VAR=1".parse().unwrap();
cmd.subshell(true).execute().unwrap();
assert_eq!(optional("VAR").unwrap(), "0");
let mut cmd: Command = "VAR=1".parse().unwrap();
assert!(cmd.invert(true).execute().is_err());
assert_eq!(optional("VAR").unwrap(), "1");
let cmd: Command = "exit 1".parse().unwrap();
assert!(cmd.execute().is_err());
}
#[test]
fn no_line_in_error() {
let r = source::string("enable nonexistent");
assert_err_re!(r, "^line 1: enable: nonexistent: not a shell builtin$");
let cmd: Command = "enable nonexistent".parse().unwrap();
let r = cmd.execute();
assert_err_re!(r, "^enable: nonexistent: not a shell builtin$");
}
#[test]
fn current() {
let r = current_command_string();
assert_err_re!(r, "no running command");
let r = current_command_name();
assert_err_re!(r, "no running command");
}
#[test]
fn invalid() {
assert!(BashCommand::from_str("|| {").is_err());
}
}