runscript/
exec.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::process::{ExitStatus, Output, Stdio};
4use std::sync::atomic::AtomicBool;
5use std::sync::mpsc::{Receiver, Sender};
6use std::sync::{mpsc, Arc};
7
8use conch_parser::ast::{
9    Arithmetic, AtomicCommandList, AtomicShellPipeableCommand, AtomicTopLevelCommand,
10    AtomicTopLevelWord, ComplexWord, ListableCommand,
11    Parameter, ParameterSubstitution, PipeableCommand, Redirect, RedirectOrCmdWord, SimpleCommand,
12    SimpleWord, Word,
13};
14use glob::{MatchOptions, Pattern, glob_with};
15use itertools::Itertools;
16
17use crate::parser::RunscriptLocation;
18use crate::{run, Script};
19
20#[derive(Clone)]
21pub struct ExecConfig<'a> {
22    /// The verbosity of output to the supplied output stream
23    pub verbosity: Verbosity,
24    /// The output stream to output to, or `None` to produce no output
25    pub output_stream: Option<Arc<termcolor::StandardStream>>,
26    /// The working directory to execute the script's commands in
27    pub working_directory: &'a Path,
28    /// Positional arguments to pass to the script.
29    ///
30    ///The first argument replaces `$1`, the second replaces `$2`, etc.
31    pub positional_args: Vec<String>,
32    /// Whether to store the text printed to stdout by the executed programs
33    pub capture_stdout: bool,
34    /// A map of environment variables to remap
35    pub env_remap: &'a HashMap<String, String>,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
39pub enum Verbosity {
40    Normal,
41    Quiet,
42    Silent,
43}
44
45#[derive(Debug)]
46pub enum ProcessExit {
47    Bool(bool),
48    Status(ExitStatus),
49}
50
51#[derive(Debug)]
52pub struct ProcessOutput {
53    pub status: ProcessExit,
54    pub stdout: Vec<u8>,
55    pub stderr: Vec<u8>,
56}
57
58#[derive(Debug)]
59pub enum CommandExecError {
60    InvalidGlob {
61        glob: String,
62        loc: RunscriptLocation,
63    },
64    NoGlobMatches {
65        glob: String,
66        loc: RunscriptLocation,
67    },
68    BadCommand {
69        err: std::io::Error,
70        loc: RunscriptLocation,
71    },
72}
73
74impl ProcessExit {
75    /// Whether the process was successful or not.
76    ///
77    /// Returns the value of a `Bool` variant, or calls the `success` function on a `Status` variant
78    pub fn success(&self) -> bool {
79        match self {
80            ProcessExit::Bool(b) => *b,
81            ProcessExit::Status(s) => s.success(),
82        }
83    }
84
85    /// The exit code the process exited with, if any.
86    ///
87    /// Coerces the `Bool` variant into `true = 0`, `false = 1`
88    pub fn code(&self) -> Option<i32> {
89        match self {
90            ProcessExit::Bool(b) => Some(!b as i32),
91            ProcessExit::Status(s) => s.code(),
92        }
93    }
94
95    /// The `ExitStatus` the process exited with.
96    ///
97    /// Panics on the `Bool` variant if on neither unix nor windows
98    pub fn status(self) -> ExitStatus {
99        match self {
100            ProcessExit::Status(s) => s,
101            ProcessExit::Bool(b) => convert_bool(b),
102        }
103    }
104}
105
106#[cfg(unix)]
107fn convert_bool(b: bool) -> ExitStatus {
108    use std::os::unix::process::ExitStatusExt;
109
110    ExitStatus::from_raw(!b as i32)
111}
112
113#[cfg(windows)]
114fn convert_bool(b: bool) -> ExitStatus {
115    use std::os::windows::process::ExitStatusExt;
116
117    ExitStatus::from_raw(!b as u32)
118}
119
120#[cfg(not(any(unix, windows)))]
121fn convert_bool(_: bool) -> ExitStatus {
122    panic!("Expected ExitStatus")
123}
124
125impl ProcessOutput {
126    pub fn new(success: bool) -> ProcessOutput {
127        ProcessOutput {
128            status: ProcessExit::Bool(success),
129            stdout: vec![],
130            stderr: vec![],
131        }
132    }
133}
134
135impl From<Output> for ProcessOutput {
136    fn from(o: Output) -> Self {
137        ProcessOutput {
138            status: ProcessExit::Status(o.status),
139            stdout: o.stdout,
140            stderr: o.stderr,
141        }
142    }
143}
144
145struct Interrupt;
146
147struct Interruptable(Receiver<Interrupt>, AtomicBool);
148struct Interrupter(Sender<Interrupt>);
149
150impl Interruptable {
151    fn was_interrupted(&self) -> bool {
152        self.1.fetch_or(
153            self.0.try_recv().is_ok(),
154            std::sync::atomic::Ordering::AcqRel,
155        )
156    }
157}
158
159impl Interrupter {
160    fn new() -> (Interrupter, Interruptable) {
161        let (sender, receiver) = mpsc::channel();
162        (
163            Interrupter(sender),
164            Interruptable(receiver, AtomicBool::new(false)),
165        )
166    }
167
168    fn interrupt(&self) {
169        self.0.send(Interrupt);
170    }
171}
172
173pub fn exec_script(
174    script: &Script,
175    config: &ExecConfig,
176) -> Result<ProcessOutput, CommandExecError> {
177    exec_script_entries(&script.commands, config, &HashMap::new())
178}
179
180fn exec_script_entries(
181    commands: &[AtomicTopLevelCommand<String>],
182    config: &ExecConfig,
183    env: &HashMap<String, String>,
184) -> Result<ProcessOutput, CommandExecError> {
185    // let mut stdout = vec![];
186    // let mut stderr = vec![];
187    crossbeam_utils::thread::scope(|s| {
188        let mut jobs = vec![];
189
190        for AtomicTopLevelCommand(command) in commands {
191            use conch_parser::ast::Command;
192
193            let (command, is_job) = match command {
194                Command::Job(job) => (job, true),
195                Command::List(list) => (list, false),
196            };
197
198            let (interrupter, interruptable) = Interrupter::new();
199
200            if is_job {
201                s.spawn(move |_| exec_andor_list(command, config, env, interruptable));
202                jobs.push(interrupter);
203            } else {
204                exec_andor_list(command, config, env, interruptable);
205            }
206        }
207
208        for job in jobs {
209            job.interrupt();
210        }
211    })
212    .unwrap();
213
214    Ok(ProcessOutput::new(true))
215}
216
217fn exec_andor_list(
218    command: &AtomicCommandList<String, AtomicTopLevelWord<String>, AtomicTopLevelCommand<String>>,
219    config: &ExecConfig,
220    env: &HashMap<String, String>,
221    interrupter: Interruptable,
222) -> Result<ProcessOutput, CommandExecError> {
223    let mut previous_output = exec_listable_command(&command.first, config, env, &interrupter);
224    //TODO: Chain
225    Ok(ProcessOutput::new(true))
226}
227
228fn exec_listable_command(
229    command: &ListableCommand<
230        AtomicShellPipeableCommand<
231            String,
232            AtomicTopLevelWord<String>,
233            AtomicTopLevelCommand<String>,
234        >,
235    >,
236    config: &ExecConfig,
237    env: &HashMap<String, String>,
238    interrupter: &Interruptable,
239) -> Result<ProcessOutput, CommandExecError> {
240    match command {
241        ListableCommand::Pipe(negate, commands) => todo!(),
242        ListableCommand::Single(command) => {
243            exec_pipeable_command(&command, config, env, interrupter)
244        }
245    }
246}
247
248fn exec_pipeable_command(
249    command: &AtomicShellPipeableCommand<
250        String,
251        AtomicTopLevelWord<String>,
252        AtomicTopLevelCommand<String>,
253    >,
254    config: &ExecConfig,
255    env: &HashMap<String, String>,
256    interrupter: &Interruptable,
257) -> Result<ProcessOutput, CommandExecError> {
258    match command {
259        PipeableCommand::Simple(command) => exec_simple_command(&command, config, env, interrupter),
260        PipeableCommand::Compound(command) => {
261            // Stuff like if, while, until, etc.
262            todo!();
263        }
264        PipeableCommand::FunctionDef(name, body) => {
265            todo!() //TODO: What to do here?? Store it in a hashmap?
266                    // Should I even support functions at all?
267                    // Probably not, since that's kinda what I'm doing anyway.
268        }
269    }
270}
271
272fn exec_simple_command(
273    command: &SimpleCommand<
274        String,
275        AtomicTopLevelWord<String>,
276        Redirect<AtomicTopLevelWord<String>>,
277    >,
278    config: &ExecConfig,
279    env: &HashMap<String, String>,
280    interrupter: &Interruptable,
281) -> Result<ProcessOutput, CommandExecError> {
282    use std::process::Command;
283
284    // TODO: Env variables
285    // TODO: Redirects
286
287    let command_words = command
288        .redirects_or_cmd_words
289        .iter()
290        .filter_map(|r| {
291            if let RedirectOrCmdWord::CmdWord(w) = r {
292                Some(evaluate_tl_word(w, config, env))
293            } else {
294                None
295            }
296        })
297        .flatten_ok()
298        .collect::<Result<Vec<_>, _>>()?;
299
300    // TODO: Print pre-evaluated command words
301    eprintln!(
302        "> {}",
303        command_words.join(" ")
304    );
305
306    // TODO: "Builtin" commands
307    // TODO: Interruption
308
309    let output = Command::new(&command_words[0])
310        .args(&command_words[1..])
311        .stdin(Stdio::inherit())
312        .stdout(if config.capture_stdout {
313            Stdio::piped()
314        } else {
315            Stdio::inherit()
316        })
317        .stderr(if config.capture_stdout {
318            Stdio::piped()
319        } else {
320            Stdio::inherit()
321        })
322        .output()
323        .unwrap();
324
325    Ok(output.into())
326}
327
328fn evaluate_tl_word(
329    AtomicTopLevelWord(word): &AtomicTopLevelWord<String>,
330    config: &ExecConfig,
331    env: &HashMap<String, String>,
332) -> Result<Vec<String>, CommandExecError> {
333    match word {
334        ComplexWord::Concat(words) => {
335            let words = words
336                .iter()
337                .map(|w| evaluate_word(w, config, env))
338                .collect::<Result<Vec<_>, _>>()?;
339            let is_glob = !words.iter().all(|w| matches!(w, GlobPart::Words(_)));
340            if is_glob {
341                let working_dir_path = {
342                    let mut path = Pattern::escape(&config.working_directory.to_string_lossy().into_owned());
343                    path.push('/');
344                    path
345                };
346                let stringified_glob = std::iter::once(working_dir_path.clone()).chain(words.iter().map(|w| match w {
347                    GlobPart::Words(words) => words.iter().map(|s| Pattern::escape(s)).join(" "),
348                    GlobPart::Star => "*".to_owned(),
349                    GlobPart::Question => "?".to_owned(),
350                    GlobPart::SquareOpen => "[".to_owned(),
351                    GlobPart::SquareClose => "]".to_owned(),
352                })).join("");
353                Ok(glob_with(&stringified_glob, MatchOptions::new()).unwrap().map(|path| path.unwrap().to_string_lossy().into_owned().strip_prefix(&working_dir_path).unwrap().to_owned()).collect::<Vec<_>>())
354            } else {
355                Ok(words.into_iter().flat_map(GlobPart::into_string).collect())
356            }
357        },
358        ComplexWord::Single(word) => evaluate_word(word, config, env).map(GlobPart::into_string),
359    }
360}
361
362fn evaluate_word(
363    word: &Word<
364        String,
365        SimpleWord<
366            String,
367            Parameter<String>,
368            Box<
369                ParameterSubstitution<
370                    Parameter<String>,
371                    AtomicTopLevelWord<String>,
372                    AtomicTopLevelCommand<String>,
373                    Arithmetic<String>,
374                >,
375            >,
376        >,
377    >,
378    config: &ExecConfig,
379    env: &HashMap<String, String>,
380) -> Result<GlobPart, CommandExecError> {
381    match word {
382        Word::SingleQuoted(literal) => Ok(vec![literal.clone()].into()),
383        Word::DoubleQuoted(words) => Ok(vec![words
384            .iter()
385            .map(|w| evaluate_simple_word(w, config, env).map(GlobPart::into_string))
386            .flatten_ok()
387            .collect::<Result<String, _>>()?].into()),
388        Word::Simple(word) => evaluate_simple_word(word, config, env),
389    }
390}
391
392fn evaluate_simple_word(
393    word: &SimpleWord<
394        String,
395        Parameter<String>,
396        Box<
397            ParameterSubstitution<
398                Parameter<String>,
399                AtomicTopLevelWord<String>,
400                AtomicTopLevelCommand<String>,
401                Arithmetic<String>,
402            >,
403        >,
404    >,
405    config: &ExecConfig,
406    env: &HashMap<String, String>,
407) -> Result<GlobPart, CommandExecError> {
408    match word {
409        SimpleWord::Literal(s) => Ok(vec![s.clone()].into()),
410        SimpleWord::Escaped(s) => Ok(vec![s.clone()].into()),
411        SimpleWord::Param(p) => Ok(evaluate_parameter(p, config, env).into()),
412        SimpleWord::Subst(p) => Ok(evaluate_param_subst(p, config, env)?.into()),
413        SimpleWord::Star => Ok(GlobPart::Star),
414        SimpleWord::Question => Ok(GlobPart::Question),
415        SimpleWord::SquareOpen => Ok(GlobPart::SquareOpen),
416        SimpleWord::SquareClose => Ok(GlobPart::SquareClose),
417        SimpleWord::Tilde => todo!(),
418        SimpleWord::Colon => Ok(vec![":".to_owned()].into()),
419    }
420}
421
422fn evaluate_param_subst(
423    param: &ParameterSubstitution<
424        Parameter<String>,
425        AtomicTopLevelWord<String>,
426        AtomicTopLevelCommand<String>,
427        Arithmetic<String>,
428    >,
429    config: &ExecConfig,
430    env: &HashMap<String, String>,
431) -> Result<Vec<String>, CommandExecError> {
432    match param {
433        ParameterSubstitution::Command(commands) => exec_script_entries(commands, config, env).map(|output| vec![String::from_utf8(output.stdout).unwrap()]),
434        ParameterSubstitution::Len(p) => Ok(vec![format!("{}", match p {
435            Parameter::At | Parameter::Star => config.positional_args.len(),
436            p => evaluate_parameter(p, config, env).into_iter().map(|s| s.len()).reduce(|acc, s| acc + s + 1).unwrap_or(0),
437        })]),
438        ParameterSubstitution::Arith(_) => todo!(),
439        ParameterSubstitution::Default(_, _, _) => todo!(),
440        ParameterSubstitution::Assign(_, _, _) => todo!(),
441        ParameterSubstitution::Error(_, _, _) => todo!(),
442        ParameterSubstitution::Alternative(_, _, _) => todo!(),
443        ParameterSubstitution::RemoveSmallestSuffix(_, _) => todo!(),
444        ParameterSubstitution::RemoveLargestSuffix(_, _) => todo!(),
445        ParameterSubstitution::RemoveSmallestPrefix(_, _) => todo!(),
446        ParameterSubstitution::RemoveLargestPrefix(_, _) => todo!(),
447    }
448}
449
450fn evaluate_parameter(
451    parameter: &Parameter<String>,
452    config: &ExecConfig,
453    env: &HashMap<String, String>,
454) -> Vec<String> {
455    match parameter {
456        Parameter::Positional(n) => config
457            .positional_args
458            .get(*n as usize)
459            .cloned()
460            .into_iter()
461            .collect(),
462        Parameter::Var(name) => env
463            .get(name)
464            .cloned()
465            .or_else(|| std::env::var(name).ok())
466            .into_iter()
467            .collect(),
468        Parameter::At => config.positional_args.clone(),
469        Parameter::Pound => vec![format!("{}", config.positional_args.len())],
470        Parameter::Dollar => vec![format!("{}", std::process::id())],
471
472        Parameter::Star => todo!(), // Like @ but runs evaluate_word on each word
473        Parameter::Question => todo!(), // Exit code of previous top-level command
474        Parameter::Dash => todo!(), // Options of current run invocation. Perhaps could be useful?
475        Parameter::Bang => todo!(), // PID of most recent job
476    }
477}
478
479enum GlobPart {
480    Words(Vec<String>),
481    Star,
482    Question,
483    SquareOpen,
484    SquareClose,
485}
486
487impl GlobPart {
488    fn into_string(self) -> Vec<String> {
489        match self {
490            GlobPart::Words(words) => words,
491            GlobPart::Star => vec!["*".to_owned()],
492            GlobPart::Question => vec!["?".to_owned()],
493            GlobPart::SquareOpen => vec!["[".to_owned()],
494            GlobPart::SquareClose => vec!["]".to_owned()],
495        }
496    }
497}
498
499impl From<Vec<String>> for GlobPart {
500    fn from(v: Vec<String>) -> Self {
501        Self::Words(v)
502    }
503}
504
505/*
506
507fn exec_script_entries(entries: &[ScriptEntry], config: &ExecConfig, env_remap: &HashMap<String, String>) -> Result<ProcessOutput, (CommandExecError, ScriptEntry)> {
508    let mut env = config.env_remap.clone();
509    let mut stdout_acc = Vec::new();
510    let mut stderr_acc = Vec::new();
511    for entry in entries {
512        match entry {
513            ScriptEntry::Command(command) => {
514                let output = exec_tl_cmd(&command, config, &env, config.capture_stdout)
515                    .map_err(|e| e.either(|e| (e, entry.clone()), |e| e))?;
516                if config.capture_stdout {
517                    stdout_acc.extend(output.stdout.into_iter());
518                    stderr_acc.extend(output.stderr.into_iter());
519                }
520                if !output.status.success() {
521                    if config.verbosity < Verbosity::Silent {
522                        match output.status.code() {
523                            Some(i) => eprintln!("=> exit {}", i),
524                            None => eprintln!("=> {}", signal(&output.status.status())),
525                        }
526                    }
527                    return Ok(ProcessOutput {
528                        status: ProcessExit::Bool(false),
529                        stdout: stdout_acc,
530                        stderr: stderr_acc,
531                    });
532                }
533            },
534            ScriptEntry::Env { var, val, loc } => {
535                if config.verbosity < Verbosity::Silent {
536                    eprintln!("> {}", entry);
537                }
538                env.insert(
539                    var.clone(),
540                    evaluate_arg(val, loc.clone(), config, &env, false).map_err(|e| (e, entry.clone()))?.join(" ")
541                );
542            }
543        }
544    }
545    Ok(ProcessOutput {
546        status: ProcessExit::Bool(true),
547        stdout: stdout_acc,
548        stderr: stderr_acc,
549    })
550}
551
552fn exec_tl_cmd(command: &script::TopLevelCommand, config: &ExecConfig, env_remap: &HashMap<String, String>, piped: bool) -> Result<ProcessOutput, Either<CommandExecError, (CommandExecError, ScriptEntry)>> {
553    match command {
554        script::TopLevelCommand::Command(c) => exec_cmd(c, config, env_remap, piped),
555        script::TopLevelCommand::BlockCommand(entries) => exec_script_entries(&entries, config, env_remap).map_err(|e| Right(e)),
556    }
557}
558
559fn exec_cmd(command: &script::Command, config: &ExecConfig, env_remap: &HashMap<String, String>, piped: bool) -> Result<ProcessOutput, Either<CommandExecError, (CommandExecError, ScriptEntry)>> {
560    let target = &evaluate_arg(&command.target, command.loc.clone(), config, env_remap, false).map_err(|e| Left(e))?[0];
561    if target == "run" {
562        if config.verbosity < Verbosity::Silent {
563            eprintln!("> {}", command);
564        }
565        let args = command.args.iter()
566            .map(|x| evaluate_arg(x, command.loc.clone(), config, env_remap, true))
567            .collect::<Result<Vec<Vec<String>>, CommandExecError>>().map_err(|e| Left(e))?
568            .into_iter()
569            .flatten()
570            .collect::<Vec<_>>();
571        //TODO: Pass env
572        return run(
573            &args.iter().map(|s| &**s).collect::<Vec<_>>(),
574            config.working_directory,
575            config.verbosity,
576            piped,
577            env_remap,
578        ).map_err(|err| Left(CommandExecError::BadCommand { err, loc: command.loc.clone() }));
579    }
580
581    let mut stdin: Option<Vec<u8>> = None;
582
583    let chained_output = match &*command.chained {
584        ChainedCommand::Pipe(c) => {
585            let h = exec_cmd(&c, config, env_remap, true)?;
586            stdin = Some(h.stdout);
587            None
588        },
589        ChainedCommand::And(c) => {
590            let h = exec_tl_cmd(&c, config, env_remap, piped)?;
591            if !h.status.success() {
592                return Ok(h);
593            }
594            Some(h.stdout)
595        },
596        ChainedCommand::Or(c) => {
597            let h = exec_tl_cmd(&c, config, env_remap, piped)?;
598            if h.status.success() {
599                return Ok(h);
600            }
601            Some(h.stdout)
602        },
603        ChainedCommand::None => None
604    };
605
606    let mut args = Vec::with_capacity(command.args.len());
607    let mut args_did_evaluate = false;
608    for arg in &command.args {
609        match arg {
610            Argument::Unquoted(ArgPart::Str(_)) | Argument::Single(_) => {}
611            Argument::Double(v) if v.iter().all(|p| matches!(p, ArgPart::Str(_))) => {}
612            _ => args_did_evaluate = true,
613        }
614        match evaluate_arg(arg, command.loc.clone(), config, env_remap, true) {
615            Ok(v) => args.extend(v),
616            Err(e) => {
617                eprintln!("> {}", command);
618                return Err(Left(e));
619            }
620        }
621    }
622
623    if config.verbosity < Verbosity::Silent {
624        if let Some(mut lock) = config.output_stream.as_deref().map(termcolor::StandardStream::lock) {
625            write!(lock, "> {}", command).expect("Failed to write");
626            if args_did_evaluate {
627                use termcolor::WriteColor;
628
629                lock.set_color(ColorSpec::new().set_italic(true)).expect("Failed to set italic");
630                write!(
631                    lock,
632                    " = {}{}",
633                    target,
634                    args.iter()
635                        .map(|arg| if arg.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
636                            arg.clone()
637                        } else {
638                            format!("'{}'", arg)
639                        })
640                        .fold("".to_owned(), |mut acc, s| { acc.push(' '); acc.push_str(&s); acc })
641                ).expect("Failed to write");
642                lock.reset().expect("Failed to reset colour");
643            }
644            writeln!(lock).expect("Failed to write");
645        }
646    }
647
648    let mut child = match Command::new(target)
649        .args(args)
650        .current_dir(config.working_directory)
651        .envs(env_remap)
652        .stdin(match stdin { Some(_) => Stdio::piped(), None => if config.verbosity >= Verbosity::Quiet { Stdio::null() } else { Stdio::inherit() } })
653        .stdout(if piped { Stdio::piped() } else if config.verbosity >= Verbosity::Quiet { Stdio::null() } else { Stdio::inherit() })
654        .stderr(if piped { Stdio::piped() } else if config.verbosity >= Verbosity::Quiet { Stdio::null() } else { Stdio::inherit() })
655        .spawn() {
656        Ok(c) => c,
657        Err(e) => {
658            return Err(Left(CommandExecError::BadCommand{
659                err: e,
660                loc: command.loc.clone(),
661            }))
662        }
663    };
664
665    if let Some(v) = stdin {
666        let buffer = &v;
667        child.stdin.as_mut().unwrap().write_all(buffer).expect("Failed to write stdin");
668    }
669
670    let mut output = ProcessOutput::from(child.wait_with_output().expect("Command was never started"));
671    if let Some(mut o) = chained_output {
672        o.append(&mut output.stdout);
673        output.stdout = o;
674    }
675
676    Ok(output)
677}
678
679fn evaluate_arg(arg: &Argument, command_loc: RunscriptLocation, config: &ExecConfig, env_override: &HashMap<String, String>, match_globs: bool) -> Result<Vec<String>, CommandExecError> {
680    match arg {
681        Argument::Unquoted(p) => match p {
682            ArgPart::Str(s) => {
683                if match_globs && s.chars().any(|c| c == '*' || c == '(' || c == '|' || c == '<' || c == '[' || c == '?') {
684                    match Glob::new(&s) {
685                        Ok(walker) => {
686                            let cwd = config.working_directory;
687                            let strings = walker.walk(cwd).into_iter()
688                                .map(|k| k.to_string_lossy().into_owned())
689                                .collect::<Vec<String>>();
690                            if strings.is_empty() {
691                                Err(CommandExecError::NoGlobMatches { glob: s.clone(), loc: command_loc })
692                            } else {
693                                Ok(strings)
694                            }
695                        },
696                        Err(err) => Err(CommandExecError::InvalidGlob { glob: s.clone(), err, loc: command_loc })
697                    }
698                } else {
699                    Ok(vec![s.clone()])
700                }
701            },
702            _ => evaluate_part(p, env_override, config),
703        },
704        Argument::Single(s) => Ok(vec![s.clone()]),
705        Argument::Double(p) => {
706            let args = p.iter().map(|x| evaluate_part(x, env_override, config)).collect::<Result<Vec<Vec<String>>, CommandExecError>>()?.iter().fold(vec![], |mut acc, x| { acc.append(&mut x.clone()); acc });
707            Ok(vec![args.iter().fold(String::new(), |acc, s| acc + &s)])
708        }
709    }
710}
711
712fn evaluate_part(part: &ArgPart, env_override: &HashMap<String, String>, config: &ExecConfig) -> Result<Vec<String>, CommandExecError> {
713    use std::env::VarError::*;
714    match part {
715        ArgPart::Str(s) => Ok(vec![s.clone()]),
716        ArgPart::Arg(n) => Ok(vec![config.positional_args.get(*n - 1).cloned().unwrap_or_else(|| "".to_owned())]),
717        ArgPart::Var(v) => Ok(vec![env_override.get(v).cloned().unwrap_or_else(|| std::env::var(v).unwrap_or_else(|err| match err { NotPresent => "".to_owned(), NotUnicode(s) => s.to_string_lossy().into_owned() }))]),
718        ArgPart::AllArgs => Ok(config.positional_args.clone()),
719        ArgPart::Cmd(c) => {
720            Ok(vec![String::from_utf8_lossy(
721                &match Command::new(&evaluate_arg(&c.target, c.loc.clone(), config, env_override, false)?[0])
722                    .args(c.args.iter().map(|x| evaluate_arg(x, c.loc.clone(), config, env_override, true)).collect::<Result<Vec<Vec<String>>, CommandExecError>>()?.iter().fold(vec![], |mut acc, x| { acc.append(&mut x.clone()); acc }))
723                    .current_dir(config.working_directory)
724                    .output() {
725                        Ok(o) => o.stdout,
726                        Err(e) => return Err(CommandExecError::BadCommand {
727                            err: e,
728                            loc: c.loc.clone(),
729                        })
730                    }
731                ).into_owned()])
732        }
733    }
734}
735
736*/
737
738#[cfg(unix)]
739use std::os::raw::{c_char, c_int};
740
741#[cfg(unix)]
742extern "C" {
743    fn strsignal(sig: c_int) -> *const c_char;
744}
745
746#[cfg(unix)]
747fn signal(status: &ExitStatus) -> String {
748    use std::ffi::CStr;
749    use std::os::unix::process::ExitStatusExt;
750
751    let signal = status.signal().expect("Expected signal");
752
753    // SAFETY: Function is guaranteed by POSIX to exist.
754    let sigstr = unsafe { CStr::from_ptr(strsignal(signal as c_int)) };
755    format!(
756        "signal {} ({})",
757        signal,
758        sigstr.to_string_lossy().to_owned()
759    )
760}
761
762#[cfg(not(unix))]
763fn signal(_: &ExitStatus) -> String {
764    panic!("Non-unix program terminated with signal");
765}