use std::borrow::Cow;
use std::collections::HashMap;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use futures::future::LocalBoxFuture;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::shell::fs_util;
use super::commands::builtin_commands;
use super::commands::ShellCommand;
#[derive(Clone)]
pub struct ShellState {
env_vars: HashMap<String, String>,
shell_vars: HashMap<String, String>,
cwd: PathBuf,
commands: Arc<HashMap<String, Box<dyn ShellCommand>>>,
token: CancellationToken,
}
impl ShellState {
pub fn new(
env_vars: HashMap<String, String>,
cwd: &Path,
custom_commands: HashMap<String, Box<dyn ShellCommand>>,
) -> Self {
assert!(cwd.is_absolute());
let mut commands = builtin_commands();
commands.extend(custom_commands);
let mut result = Self {
env_vars: Default::default(),
shell_vars: Default::default(),
cwd: PathBuf::new(),
commands: Arc::new(commands),
token: CancellationToken::default(),
};
for (name, value) in env_vars {
result.apply_env_var(&name, &value);
}
result.set_cwd(cwd);
result
}
pub fn cwd(&self) -> &PathBuf {
&self.cwd
}
pub fn env_vars(&self) -> &HashMap<String, String> {
&self.env_vars
}
pub fn get_var(&self, name: &str) -> Option<&String> {
let name = if cfg!(windows) {
Cow::Owned(name.to_uppercase())
} else {
Cow::Borrowed(name)
};
self
.env_vars
.get(name.as_ref())
.or_else(|| self.shell_vars.get(name.as_ref()))
}
pub fn set_cwd(&mut self, cwd: &Path) {
self.cwd = cwd.to_path_buf();
self
.env_vars
.insert("PWD".to_string(), self.cwd.display().to_string());
}
pub fn apply_changes(&mut self, changes: &[EnvChange]) {
for change in changes {
self.apply_change(change);
}
}
pub fn apply_change(&mut self, change: &EnvChange) {
match change {
EnvChange::SetEnvVar(name, value) => self.apply_env_var(name, value),
EnvChange::SetShellVar(name, value) => {
if self.env_vars.contains_key(name) {
self.apply_env_var(name, value);
} else {
self.shell_vars.insert(name.to_string(), value.to_string());
}
}
EnvChange::Cd(new_dir) => {
self.cwd = new_dir.clone();
}
}
}
pub fn apply_env_var(&mut self, name: &str, value: &str) {
let name = if cfg!(windows) {
name.to_uppercase()
} else {
name.to_string()
};
if name == "PWD" {
let cwd = PathBuf::from(value);
if cwd.is_absolute() {
if let Ok(cwd) = fs_util::canonicalize_path(&cwd) {
self.set_cwd(&cwd);
}
}
} else {
self.shell_vars.remove(&name);
if value.is_empty() {
self.env_vars.remove(&name);
} else {
self.env_vars.insert(name, value.to_string());
}
}
}
pub fn token(&self) -> CancellationToken {
self.token.clone()
}
pub fn resolve_command(&self, name: &str) -> Option<&dyn ShellCommand> {
self.commands.get(name).map(|c| &**c)
}
pub fn with_child_token(&self) -> ShellState {
let mut state = self.clone();
state.token = self.token.child_token();
state
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum EnvChange {
SetEnvVar(String, String),
SetShellVar(String, String),
Cd(PathBuf),
}
pub type FutureExecuteResult = LocalBoxFuture<'static, ExecuteResult>;
pub const CANCELLATION_EXIT_CODE: i32 = 130;
#[derive(Debug)]
pub enum ExecuteResult {
Exit(i32, Vec<JoinHandle<i32>>),
Continue(i32, Vec<EnvChange>, Vec<JoinHandle<i32>>),
}
impl ExecuteResult {
pub fn for_cancellation() -> ExecuteResult {
ExecuteResult::Exit(CANCELLATION_EXIT_CODE, Vec::new())
}
pub fn from_exit_code(exit_code: i32) -> ExecuteResult {
ExecuteResult::Continue(exit_code, Vec::new(), Vec::new())
}
pub fn into_exit_code_and_handles(self) -> (i32, Vec<JoinHandle<i32>>) {
match self {
ExecuteResult::Exit(code, handles) => (code, handles),
ExecuteResult::Continue(code, _, handles) => (code, handles),
}
}
pub fn into_handles(self) -> Vec<JoinHandle<i32>> {
self.into_exit_code_and_handles().1
}
}
pub struct ShellPipeReader(os_pipe::PipeReader);
impl Clone for ShellPipeReader {
fn clone(&self) -> Self {
Self(self.0.try_clone().unwrap())
}
}
impl ShellPipeReader {
pub fn stdin() -> ShellPipeReader {
ShellPipeReader::from_raw(os_pipe::dup_stdin().unwrap())
}
pub fn from_raw(reader: os_pipe::PipeReader) -> Self {
Self(reader)
}
pub fn into_stdio(self) -> std::process::Stdio {
self.0.into()
}
pub fn pipe_to(self, writer: &mut dyn Write) -> Result<()> {
self.pipe_to_inner(writer, false)
}
fn pipe_to_with_flushing(self, writer: &mut dyn Write) -> Result<()> {
self.pipe_to_inner(writer, true)
}
fn pipe_to_inner(
mut self,
writer: &mut dyn Write,
flush: bool,
) -> Result<()> {
loop {
let mut buffer = [0; 512]; let size = self.0.read(&mut buffer)?;
if size == 0 {
break;
}
writer.write_all(&buffer[0..size])?;
if flush {
writer.flush()?;
}
}
Ok(())
}
pub fn pipe_to_sender(self, mut sender: ShellPipeWriter) -> Result<()> {
match &mut sender {
ShellPipeWriter::OsPipe(pipe) => self.pipe_to(pipe),
ShellPipeWriter::StdFile(file) => self.pipe_to(file),
ShellPipeWriter::Stdout => {
self.pipe_to_with_flushing(&mut std::io::stdout())
}
ShellPipeWriter::Stderr => {
self.pipe_to_with_flushing(&mut std::io::stderr())
}
ShellPipeWriter::Null => Ok(()),
}
}
}
pub enum ShellPipeWriter {
OsPipe(os_pipe::PipeWriter),
StdFile(std::fs::File),
Stdout,
Stderr,
Null,
}
impl Clone for ShellPipeWriter {
fn clone(&self) -> Self {
match self {
Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()),
Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()),
Self::Stdout => Self::Stdout,
Self::Stderr => Self::Stderr,
Self::Null => Self::Null,
}
}
}
impl ShellPipeWriter {
pub fn stdout() -> Self {
Self::Stdout
}
pub fn stderr() -> Self {
Self::Stderr
}
pub fn null() -> Self {
Self::Null
}
pub fn from_std(std_file: std::fs::File) -> Self {
Self::StdFile(std_file)
}
pub fn into_stdio(self) -> std::process::Stdio {
match self {
Self::OsPipe(pipe) => pipe.into(),
Self::StdFile(file) => file.into(),
Self::Stdout => std::process::Stdio::inherit(),
Self::Stderr => std::process::Stdio::inherit(),
Self::Null => std::process::Stdio::null(),
}
}
pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
match self {
Self::OsPipe(pipe) => pipe.write_all(bytes)?,
Self::StdFile(file) => file.write_all(bytes)?,
Self::Stdout => {
let mut stdout = std::io::stdout().lock();
stdout.write_all(bytes)?;
stdout.flush()?;
}
Self::Stderr => {
let mut stderr = std::io::stderr().lock();
stderr.write_all(bytes)?;
stderr.flush()?;
}
Self::Null => {}
}
Ok(())
}
pub fn write_line(&mut self, line: &str) -> Result<()> {
let bytes = format!("{line}\n");
self.write_all(bytes.as_bytes())
}
}
pub fn pipe() -> (ShellPipeReader, ShellPipeWriter) {
let (reader, writer) = os_pipe::pipe().unwrap();
(ShellPipeReader(reader), ShellPipeWriter::OsPipe(writer))
}