shrs_lang 0.0.6

parser and lexer for shrs posix shell
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
use std::{
    fs::File,
    io::{BufRead, BufReader},
    path::Path,
    process::{Child, Stdio},
};

use lazy_static::lazy_static;
use nix::unistd::Pid;
use shrs_core::{
    hooks::{AfterCommandCtx, BeforeCommandCtx, JobExitCtx},
    prelude::{Context, Lang, Runtime, Shell},
};
use thiserror::Error;

use crate::{ast, parser, process::ExitStatus, Lexer, Parser, PosixError};

// TODO function signature is very ugly
// TODO maybe make this a method of Command
pub fn run_external_command(
    sh: &Shell,
    ctx: &mut Context,
    rt: &mut Runtime,
    cmd: &str,
    args: &[String],
    stdin: Stdio,
    stdout: Stdio,
    pgid: Option<i32>,
    assigns: &Vec<ast::Assign>,
) -> anyhow::Result<ExitStatus> {
    use std::process::Command;

    let envs = assigns.iter().map(|word| (&word.var, &word.val));

    // TODO might need to do subst on cmd too
    let child = Command::new(cmd)
        .args(args)
        .stdin(stdin)
        .stdout(stdout)
        // .process_group(pgid.unwrap_or(0)) // pgid of 0 means use own pid as pgid
        .current_dir(rt.working_dir.to_str().unwrap())
        .envs(envs)
        .spawn()?;

    Ok(ExitStatus::Running(Pid::from_raw(child.id() as i32)))
}

fn dummy_child() -> ExitStatus {
    ExitStatus::Exited(0)
}

pub fn eval_command(
    sh: &Shell,
    ctx: &mut Context,
    rt: &mut Runtime,
    cmd: &ast::Command,
    stdin: Stdio,
    stdout: Stdio,
    _pgid: Option<i32>,
) -> anyhow::Result<ExitStatus> {
    match cmd {
        ast::Command::Simple {
            assigns,
            args,
            redirects,
        } => {
            let mut it = args.iter();

            // Retrieve command name or return immediately (empty command)
            let cmd_name = match it.next() {
                Some(cmd_name) => cmd_name,
                None => return Ok(dummy_child()),
            };
            let args = it
                .map(|a| -> String {
                    if a.len() > 1 {
                        let mut chars = a.chars();

                        let first = chars.next().unwrap();
                        let last = chars.next_back().unwrap();
                        if first == '\'' || first == '\"' {
                            if first == last {
                                return a[1..a.len() - 1].into();
                            }
                        }
                    }
                    (*a).clone()
                })
                .collect::<Vec<_>>();

            // println!("redirects {:?}", redirects);
            // println!("assigns {:?}", assigns);

            // file redirections
            // TODO: current behavior, only one read and write operation is allowed, the latter ones will override the behavior of earlier ones
            let mut cur_stdin = stdin;
            let mut cur_stdout = stdout;
            for redirect in redirects {
                let filename = Path::new(&*redirect.file);
                // TODO not making use of file descriptor at all right now
                let _n = match &redirect.n {
                    Some(n) => *n,
                    None => 1,
                };
                match redirect.mode {
                    ast::RedirectMode::Read => {
                        let file_handle = File::options()
                            .read(true)
                            .open(filename)
                            .map_err(PosixError::Redirect)?;
                        cur_stdin = Stdio::from(file_handle);
                    },
                    ast::RedirectMode::Write => {
                        let file_handle = File::options()
                            .write(true)
                            .create_new(true)
                            .open(filename)
                            .map_err(PosixError::Redirect)?;
                        cur_stdout = Stdio::from(file_handle);
                    },
                    ast::RedirectMode::ReadAppend => {
                        let file_handle = File::options()
                            .read(true)
                            .append(true)
                            .open(filename)
                            .map_err(PosixError::Redirect)?;
                        cur_stdin = Stdio::from(file_handle);
                    },
                    ast::RedirectMode::WriteAppend => {
                        let file_handle = File::options()
                            .write(true)
                            .append(true)
                            .create_new(true)
                            .open(filename)
                            .map_err(PosixError::Redirect)?;
                        cur_stdout = Stdio::from(file_handle);
                    },
                    ast::RedirectMode::ReadDup => {
                        unimplemented!()
                    },
                    ast::RedirectMode::WriteDup => {
                        unimplemented!()
                    },
                    ast::RedirectMode::ReadWrite => {
                        let file_handle = File::options()
                            .read(true)
                            .write(true)
                            .create_new(true)
                            .open(filename)
                            .map_err(PosixError::Redirect)?;
                        cur_stdin = Stdio::from(file_handle.try_clone().unwrap());
                        cur_stdout = Stdio::from(file_handle);
                    },
                };
            }

            // TODO which stdin var to use?, previous command or from file redirection?

            // TODO doing args subst here is a waste if we evaluating function body
            let subst_args = args.iter().map(|x| envsubst(rt, x)).collect::<Vec<_>>();
            for (builtin_name, builtin_cmd) in sh.builtins.iter() {
                if builtin_name == &cmd_name.as_str() {
                    // TODO actually return the output of builtin
                    let builtin_output = builtin_cmd.run(sh, ctx, rt, &subst_args)?;
                    return Ok(dummy_child());
                }
            }

            // otherwise look for defined functions

            // TODO disabling functions for now
            // let cmd_body = rt.functions.get(cmd_name.as_str()).cloned();

            let cmd_body = None;
            match cmd_body {
                Some(ref cmd_body) => eval_command(
                    sh,
                    ctx,
                    rt,
                    cmd_body,
                    Stdio::inherit(),
                    Stdio::piped(),
                    None,
                ),
                None => run_external_command(
                    sh,
                    ctx,
                    rt,
                    cmd_name,
                    &subst_args,
                    cur_stdin,
                    cur_stdout,
                    None,
                    assigns,
                ),
            }
        },
        /*
        ast::Command::Pipeline(a_cmd, b_cmd) => {
            // TODO double check that pgid works properly for pipelines that are longer than one pipe (left recursiveness of parser might mess this up)
            let mut a_cmd_handle = eval_command(sh, ctx, rt, a_cmd, stdin, Stdio::piped(), None)?;
            let piped_stdin = Stdio::from(a_cmd_handle.stdout.take().unwrap());
            let pgid = a_cmd_handle.id();
            let b_cmd_handle =
                eval_command(sh, ctx, rt, b_cmd, piped_stdin, stdout, Some(pgid as i32))?;
            Ok(b_cmd_handle)
        },
        */
        ast::Command::Or(a_cmd, b_cmd) | ast::Command::And(a_cmd, b_cmd) => {
            let negate = match cmd {
                ast::Command::Or { .. } => false,
                ast::Command::And { .. } => true,
                _ => unreachable!(),
            };
            // TODO double check if these stdin and stdou params are correct
            let mut a_cmd_handle =
                eval_command(sh, ctx, rt, a_cmd, Stdio::inherit(), Stdio::piped(), None)?;
            if (a_cmd_handle == ExitStatus::Exited(0)) ^ negate {
                // TODO return something better (indicate that command failed with exit code)
                return Ok(dummy_child());
            }
            let b_cmd_handle =
                eval_command(sh, ctx, rt, b_cmd, Stdio::inherit(), Stdio::piped(), None)?;
            Ok(b_cmd_handle)
        },
        ast::Command::Not(cmd) => {
            // TODO exit status negate
            let cmd_handle = eval_command(sh, ctx, rt, cmd, stdin, stdout, None)?;
            Ok(cmd_handle)
        },
        ast::Command::AsyncList(a_cmd, b_cmd) => {
            let a_cmd_handle =
                eval_command(sh, ctx, rt, a_cmd, Stdio::inherit(), Stdio::piped(), None)?;

            match b_cmd {
                None => {
                    // TODO might need a Command display trait implementation
                    // TODO create the job
                    // ctx.jobs.push(a_cmd_handle, String::new());
                    Ok(dummy_child())
                },
                Some(b_cmd) => {
                    let b_cmd_handle =
                        eval_command(sh, ctx, rt, b_cmd, Stdio::inherit(), Stdio::piped(), None)?;
                    Ok(b_cmd_handle)
                },
            }
        },
        ast::Command::SeqList(a_cmd, b_cmd) => {
            // TODO very similar to AsyncList
            let mut a_cmd_handle =
                eval_command(sh, ctx, rt, a_cmd, Stdio::inherit(), Stdio::piped(), None)?;

            match b_cmd {
                None => Ok(a_cmd_handle),
                Some(b_cmd) => {
                    let b_cmd_handle =
                        eval_command(sh, ctx, rt, b_cmd, Stdio::inherit(), Stdio::piped(), None)?;
                    Ok(b_cmd_handle)
                },
            }
        },
        ast::Command::Subshell(cmd) => {
            // TODO rn history is being copied too, history (and also alias?) really should be global
            // maybe separate out global context and runtime context into two structs?
            let mut new_rt = rt.clone();
            let cmd_handle = eval_command(
                sh,
                ctx,
                &mut new_rt,
                cmd,
                Stdio::inherit(),
                Stdio::piped(),
                None,
            )?;
            Ok(cmd_handle)
        },
        ast::Command::If { conds, else_part } => {
            // TODO throw proper error here
            assert!(!conds.is_empty());

            for ast::Condition { cond, body } in conds {
                let mut cond_handle =
                    eval_command(sh, ctx, rt, cond, Stdio::inherit(), Stdio::piped(), None)?;
                // TODO sorta similar to and statements

                if cond_handle == ExitStatus::Exited(0) {
                    let body_handle =
                        eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
                    return Ok(body_handle);
                }
            }

            if let Some(else_part) = else_part {
                let else_handle = eval_command(
                    sh,
                    ctx,
                    rt,
                    else_part,
                    Stdio::inherit(),
                    Stdio::piped(),
                    None,
                )?;
                return Ok(else_handle);
            }

            Ok(dummy_child())
        },
        ast::Command::While { cond, body } | ast::Command::Until { cond, body } => {
            let negate = match cmd {
                ast::Command::While { .. } => false,
                ast::Command::Until { .. } => true,
                _ => unreachable!(),
            };

            loop {
                let mut cond_handle =
                    eval_command(sh, ctx, rt, cond, Stdio::inherit(), Stdio::piped(), None)?;
                if (cond_handle == ExitStatus::Exited(0)) ^ negate {
                    let mut body_handle =
                        eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
                } else {
                    break; // TODO not sure if there should be break here
                }
            }
            Ok(dummy_child())
        },
        ast::Command::For {
            name,
            wordlist,
            body,
        } => {
            // expand wordlist
            let mut expanded = vec![];
            for word in wordlist {
                // TODO use IFS variable for this
                for subword in word.split(' ') {
                    expanded.push(subword);
                }
            }

            // execute body
            for word in expanded {
                // TODO should have separate variable struct instead of env
                rt.env.set(name, word); // TODO unset the var after the loop?
                let mut body_handle =
                    eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
            }

            Ok(dummy_child())
        },
        ast::Command::Case { word, arms } => {
            // println!("word {:?}, arms {:?}", word, arms);

            let subst_word = envsubst(rt, word);

            for ast::CaseArm { pattern, body } in arms {
                if pattern.iter().any(|x| x == &subst_word) {
                    let mut body_handle =
                        eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
                    // TODO should we break? (should multiple match arms be matched?)
                }
            }

            Ok(dummy_child())
        },
        ast::Command::Fn { fname, body } => {
            // TODO disabling functions for now since it is technically command language dependent
            // feature
            /*
            if RESERVED_WORDS.contains(&fname.as_str()) {
                eprintln!("function name cannot be a reserved keyword");
                return Ok(dummy_child()); // TODO come up with better return value
            }

            // TODO hook for redefining function?
            rt.functions.insert(fname.to_string(), body.to_owned());

            Ok(dummy_child())
            */
            todo!()
        },
        ast::Command::None => Ok(dummy_child()),
        _ => Ok(dummy_child()),
    }
}

/// Performs environment substitution on a string
// TODO regex replace might not be the best way. could also recognize the env var during parsing
// TODO handle escaped characters
fn envsubst(rt: &mut Runtime, arg: &str) -> String {
    use regex::Regex;

    lazy_static! {
        static ref R_0: Regex = Regex::new(r"\$(?P<env>[a-zA-Z_]+)").unwrap(); // no braces
        static ref R_1: Regex = Regex::new(r"\$\{(?P<env>[a-zA-Z_]+)\}").unwrap(); // with braces
        static ref R_2: Regex = Regex::new(r"~").unwrap(); // tilde
    }

    let mut subst = arg.to_string();

    // substitute special parameters first
    subst = subst.as_str().replace("$?", &rt.exit_status.to_string());
    subst = subst.as_str().replace("$#", &rt.args.len().to_string());
    subst = subst.as_str().replace("$0", &rt.name);

    for cap in R_0.captures_iter(arg) {
        // look up env var
        let var = &cap["env"];
        // TODO stupid code
        let val = match rt.env.get(var) {
            Ok(val) => val.clone(),
            Err(_) => String::new(),
        };
        let fmt_env = format!("${var}"); // format $VAR
        subst = subst.as_str().replace(&fmt_env, &val);
    }

    // TODO this is dumb stupid and bad repeated code
    for cap in R_1.captures_iter(arg) {
        let var = &cap["env"];
        let val = match rt.env.get(var) {
            Ok(val) => val.clone(),
            Err(_) => String::new(),
        };
        let fmt_env = format!("${{{var}}}"); // format ${VAR}
        subst = subst.as_str().replace(&fmt_env, &val);
    }

    // tilde substitution
    let home = match rt.env.get("HOME") {
        Ok(home) => home.as_str(),
        Err(_) => "",
    };
    let subst = R_2.replace_all(&subst, home).to_string();

    subst
}

/// Small wrapper that outputs command output if exists
pub fn command_output(
    sh: &Shell,
    ctx: &mut Context,
    rt: &mut Runtime,
    cmd_handle: &mut Child,
) -> anyhow::Result<ExitStatus> {
    // TODO also handle stderr
    let output = if let Some(out) = cmd_handle.stdout.take() {
        let reader = BufReader::new(out);
        reader
            .lines()
            .map(|line| {
                let line = line.unwrap();
                println!("{}", line);
                line
            })
            .collect::<Vec<_>>()
            .join("\n")
    } else {
        String::new()
    };

    // Fetch output status
    let exit_status = cmd_handle.wait().unwrap().code().unwrap();
    rt.exit_status = exit_status;

    // Call hook
    // TODO update this hook
    /*
    let hook_ctx = AfterCommandCtx {
        exit_code: exit_status,
        cmd_time: 0.0,
        cmd_output: output,
    };
    sh.hooks.run::<AfterCommandCtx>(sh, ctx, rt, hook_ctx)?;
    */

    Ok(ExitStatus::Exited(exit_status))
}

/*
#[cfg(test)]
mod tests {
    use super::{envsubst, Runtime};

    // #[test]
    // fn envsubst_test() {
    //     let mut rt = Runtime::default();
    //     rt.env.set("EDITOR", "vim");
    //     rt.env.set("SHELL", "/bin/shrs");
    //     let text = "$SHELL ${EDITOR}";
    //     let subst = envsubst(&mut rt, text);
    //     assert_eq!(subst, String::from("/bin/shrs vim"));
    // }

    // #[test]
    // fn path_execs_test() {
    //     println!("{:?}", find_executables_in_path("/usr/bin:/usr/local/bin"));
    // }
}
*/