ghoti_exec/
builtins.rs

1use std::fmt::Write;
2use std::ops::ControlFlow;
3use std::str::FromStr;
4
5use clap::{Args, Parser};
6use owo_colors::AnsiColors;
7
8use crate::command::{self, BoxCommand, UserFunc};
9use crate::utils::validate_variable_name;
10use crate::{Error, ExecContext, ExecResult, LocateExternalCommand, Status, VarScope, Variable};
11
12macro_rules! bail {
13    ($($msg:tt)+) => {
14        return Err(crate::Error::Custom(format!($($msg)+)).into())
15    };
16}
17
18macro_rules! ensure {
19    ($cond:expr, $($msg:tt)+) => {
20        if !$cond {
21            bail!($($msg)+);
22        }
23    };
24}
25
26pub mod test;
27
28pub fn all_builtins() -> impl ExactSizeIterator<Item = (&'static str, BoxCommand)> {
29    [
30        (
31            "command",
32            Box::new(command::parsed_builtin(command)) as BoxCommand,
33        ),
34        ("set", Box::new(command::parsed_builtin(set))),
35        ("builtin", Box::new(command::parsed_builtin(builtin))),
36        ("source", Box::new(command::raw_builtin(source))),
37        ("test", Box::new(command::raw_builtin(test::test))),
38        ("functions", Box::new(command::parsed_builtin(functions))),
39        ("set_color", Box::new(command::parsed_builtin(set_color))),
40        ("echo", Box::new(command::raw_builtin(echo))),
41        ("type", Box::new(command::parsed_builtin(type_))),
42    ]
43    .into_iter()
44}
45
46#[derive(Debug, Parser)]
47pub(crate) struct FunctionOpts {
48    #[arg(long, short)]
49    pub description: Option<String>,
50}
51
52// TODO
53#[derive(Debug, Parser)]
54pub struct SetOpts {
55    #[command(flatten)]
56    op: SetOp,
57
58    #[command(flatten)]
59    scope: SetScope,
60
61    #[command(flatten)]
62    attr: SetVarAttr,
63
64    #[arg(trailing_var_arg = true)]
65    args: Vec<String>,
66}
67
68#[derive(Debug, Args)]
69#[group(required = false, multiple = false)]
70pub struct SetScope {
71    #[arg(long, short)]
72    local: bool,
73    #[arg(long, short)]
74    function: bool,
75    #[arg(long, short)]
76    global: bool,
77    #[arg(long, short = 'U')]
78    universal: bool,
79}
80
81#[derive(Debug, Args)]
82#[group(required = false, multiple = false)]
83pub struct SetOp {
84    #[arg(long, short)]
85    erase: bool,
86    #[arg(long, short)]
87    query: bool,
88}
89
90#[derive(Debug, Args)]
91#[group(required = false, multiple = false)]
92pub struct SetVarAttr {
93    #[arg(long, short = 'x')]
94    export: bool,
95    #[arg(long, short)]
96    unexport: bool,
97}
98
99pub async fn set(ctx: &mut ExecContext<'_>, args: SetOpts) -> ExecResult<Option<Status>> {
100    let scope = if args.scope.local {
101        VarScope::Local
102    } else if args.scope.function {
103        VarScope::Function
104    } else if args.scope.global {
105        VarScope::Global
106    } else if args.scope.universal {
107        VarScope::Universal
108    } else {
109        VarScope::Auto
110    };
111
112    if args.op.erase || args.op.query {
113        ensure!(
114            !args.attr.export && !args.attr.unexport,
115            "--export or --unexport can only be used for setting or listing variables",
116        );
117    }
118
119    if args.op.query {
120        let mut fail_cnt = 0usize;
121        for name in &args.args {
122            ensure!(scope == VarScope::Auto, "TODO");
123            if ctx.get_var(name).is_none() {
124                fail_cnt += 1;
125            }
126        }
127        Ok(Some(fail_cnt.into()))
128    } else if args.op.erase {
129        for name in &args.args {
130            validate_variable_name(name)?;
131        }
132        let mut fail_cnt = 0usize;
133        for name in &args.args {
134            if !ctx.remove_var(scope, name) {
135                fail_cnt += 1;
136            }
137        }
138        Ok(Some(fail_cnt.into()))
139    } else if let Some((name, vals)) = args.args.split_first() {
140        validate_variable_name(name)?;
141        ensure!(
142            !ctx.has_special_var(name),
143            "cannot modify special variable: {name:?}",
144        );
145        let mut var = Variable::new_list(vals.to_vec());
146        var.export = args.attr.export;
147        ctx.set_var(name, scope, var);
148        // Keep previous status.
149        Ok(None)
150    } else {
151        let mut buf = String::new();
152        ctx.list_vars::<()>(scope, |name, var| {
153            if args.attr.export && !var.export || args.attr.unexport && var.export {
154                return ControlFlow::Continue(());
155            }
156
157            buf.push_str(name);
158            if !var.value.is_empty() {
159                buf.push(' ');
160                for (idx, val) in var.value.iter().enumerate() {
161                    if idx != 0 {
162                        buf.push_str("  ");
163                    }
164                    write!(buf, "\"{}\"", val.escape_debug()).unwrap();
165                }
166            }
167            buf.push('\n');
168            ControlFlow::Continue(())
169        });
170        let _: ExecResult<_> = ctx.io().stdout.write_all(buf).await;
171        Ok(Some(Status::SUCCESS))
172    }
173}
174
175#[derive(Debug, Parser)]
176pub struct BuiltinArgs {
177    #[arg(long, short)]
178    names: bool,
179    #[arg(long, short)]
180    query: bool,
181
182    #[arg(trailing_var_arg = true)]
183    args: Vec<String>,
184}
185
186pub async fn builtin(ctx: &mut ExecContext<'_>, args: BuiltinArgs) -> ExecResult<Option<Status>> {
187    ensure!(
188        !(args.names && args.query),
189        "--names and --query are mutually exclusive"
190    );
191    if args.names {
192        let mut names = ctx.builtins().map(|(name, _)| name).collect::<Vec<_>>();
193        names.sort_unstable();
194        let out = names.iter().flat_map(|&s| [s, "\n"]).collect::<String>();
195        ctx.io().stdout.write_all(out).await?;
196        Ok(Some(Status::SUCCESS))
197    } else if args.query {
198        let ok = args.args.iter().any(|name| ctx.get_builtin(name).is_some());
199        Ok(Some(ok.into()))
200    } else {
201        ensure!(!args.args.is_empty(), "missing builtin name");
202        let name = &args.args[0];
203        let cmd = ctx
204            .get_builtin(name)
205            .ok_or_else(|| Error::CommandNotFound(name.into()))?;
206        cmd.exec(ctx, &args.args).await;
207        Ok(None)
208    }
209}
210
211#[derive(Debug, Parser)]
212pub struct CommandArgs {
213    #[arg(long, short)]
214    pub query: bool,
215    #[arg(long, short)]
216    pub search: bool,
217
218    #[arg(trailing_var_arg = true)]
219    pub args: Vec<String>,
220}
221
222pub async fn command(ctx: &mut ExecContext<'_>, args: CommandArgs) -> ExecResult<Status> {
223    if args.query {
224        let mut found = false;
225        for name in &args.args {
226            if let LocateExternalCommand::ExecFile(_) = ctx.locate_external_command(name) {
227                found = true;
228                break;
229            }
230        }
231        Ok(found.into())
232    } else if args.search {
233        let mut found = true;
234        let mut buf = String::new();
235        for name in &args.args {
236            if let LocateExternalCommand::ExecFile(path) = ctx.locate_external_command(name) {
237                found = true;
238                writeln!(buf, "{}", path.display()).unwrap();
239            }
240        }
241        let _: ExecResult<_> = ctx.io().stdout.write_all(buf).await;
242        Ok(found.into())
243    } else {
244        let cmd = args.args.first().ok_or(Error::EmptyCommand)?;
245        let exe_path = match ctx.locate_external_command(cmd) {
246            LocateExternalCommand::ExecFile(path) => path,
247            LocateExternalCommand::NotExecFile(path) | LocateExternalCommand::Dir(path) => {
248                return Err(Error::CommandNotFound(format!(
249                    "{} (candidate {} is not an executable file)",
250                    cmd,
251                    path.display(),
252                )));
253            }
254            LocateExternalCommand::NotFound => {
255                return Err(Error::CommandNotFound(cmd.into()));
256            }
257        };
258        ctx.exec_external_command(&exe_path, &args.args).await
259    }
260}
261
262pub async fn source(ctx: &mut ExecContext<'_>, args: &[String]) -> ExecResult<()> {
263    let (path, text, args) = match args[1..].split_first() {
264        Some((path, args)) => {
265            let text = tokio::task::spawn_blocking({
266                let path = path.clone();
267                move || std::fs::read_to_string(path)
268            })
269            .await
270            .expect("no panic")
271            .map_err(Error::ReadWrite)?;
272            (path.clone(), text, args)
273        }
274        None => {
275            let text = ctx.io().stdin.read_to_string().await?;
276            ("<stdin>".into(), text, &[][..])
277        }
278    };
279
280    let scope = &mut **ctx.enter_local_scope();
281    scope.set_var("argv", VarScope::Local, args.to_vec());
282    scope.exec_source(Some(path), text).await;
283    Ok(())
284}
285
286#[derive(Debug, Parser)]
287pub struct FunctionsOpts {
288    #[arg(long, short)]
289    pub erase: bool,
290    #[arg(long, short)]
291    pub query: bool,
292    #[arg(long, short)]
293    pub all: bool,
294
295    pub funcs: Vec<String>,
296}
297
298pub async fn functions(ctx: &mut ExecContext<'_>, args: FunctionsOpts) -> ExecResult {
299    ensure!(
300        args.erase as u8 + args.query as u8 <= 1,
301        "--erase and --query are mutually exclusive",
302    );
303    ensure!(
304        !args.all || args.funcs.is_empty(),
305        "--all can only be used without positional arguments"
306    );
307
308    if args.erase {
309        let fail_cnt = args
310            .funcs
311            .iter()
312            .map(|name| !ctx.remove_global_func(name) as usize)
313            .sum::<usize>();
314        Ok(fail_cnt.into())
315    } else if args.query {
316        let mut fail_cnt = 0usize;
317        for name in &args.funcs {
318            if ctx.get_or_autoload_func(name).await.is_none() {
319                fail_cnt += 1;
320            }
321        }
322        Ok(fail_cnt.into())
323    } else if args.funcs.is_empty() {
324        let mut buf = String::new();
325        ctx.list_funcs::<()>(|name, _cmd| {
326            if args.all || !name.starts_with("_") {
327                writeln!(buf, "{name}").unwrap();
328            }
329            ControlFlow::Continue(())
330        })
331        .await;
332        let _: ExecResult<_> = ctx.io().stdout.write_all(buf).await;
333        Ok(Status::SUCCESS)
334    } else {
335        let mut buf = String::new();
336        let mut fail_cnt = 0usize;
337        for name in &args.funcs {
338            if let Some(cmd) = ctx.get_or_autoload_func(name).await {
339                let func = cmd.as_any().downcast_ref::<UserFunc>().unwrap();
340                writeln!(buf, "{:#?}", func.stmt()).unwrap();
341            } else {
342                fail_cnt += 1;
343            }
344        }
345        let _: ExecResult<_> = ctx.io().stdout.write_all(buf).await;
346        Ok(fail_cnt.into())
347    }
348}
349
350#[derive(Debug, Parser)]
351pub struct SetColorOpts {
352    #[arg(long, short)]
353    background: Option<SetColor>,
354
355    #[arg(long, short = 'c')]
356    print_colors: Option<SetColor>,
357
358    #[arg(long, short = 'o')]
359    bold: bool,
360
361    #[arg(long, short)]
362    dim: bool,
363
364    #[arg(long, short)]
365    italics: bool,
366
367    #[arg(long, short)]
368    reverse: bool,
369
370    #[arg(long, short)]
371    underline: bool,
372
373    color: Option<SetColor>,
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum SetColor {
378    Normal,
379    Ansi(AnsiColors),
380    Rgb(u8, u8, u8),
381}
382
383impl FromStr for SetColor {
384    type Err = String;
385
386    fn from_str(s: &str) -> Result<Self, Self::Err> {
387        let err = || format!("invalid color: {s:?}");
388        Ok(Self::Ansi(match s {
389            "normal" => return Ok(Self::Normal),
390
391            "black" => AnsiColors::Black,
392            "red" => AnsiColors::Red,
393            "green" => AnsiColors::Green,
394            "yellow" => AnsiColors::Yellow,
395            "blue" => AnsiColors::Blue,
396            "magenta" => AnsiColors::Magenta,
397            "cyan" => AnsiColors::Cyan,
398            "white" => AnsiColors::White,
399            "brblack" => AnsiColors::BrightBlack,
400            "brred" => AnsiColors::BrightRed,
401            "brgreen" => AnsiColors::BrightGreen,
402            "bryellow" => AnsiColors::BrightYellow,
403            "brblue" => AnsiColors::BrightBlue,
404            "brmagenta" => AnsiColors::BrightMagenta,
405            "brcyan" => AnsiColors::BrightCyan,
406            "brwhite" => AnsiColors::BrightWhite,
407
408            s if s.len() == 3 => {
409                let v = u32::from_str_radix(s, 16).map_err(|_| err())?;
410                let f = |x: u32| x as u8 * 11;
411                return Ok(Self::Rgb(f(v >> 8), f((v >> 4) & 0xF), f(v & 0xF)));
412            }
413            s if s.len() == 6 => {
414                let v = u32::from_str_radix(s, 16).map_err(|_| err())?;
415                let f = |x: u32| x as u8;
416                return Ok(Self::Rgb(f(v >> 16), f((v >> 8) & 0xFF), f(v & 0xFF)));
417            }
418            _ => return Err(err()),
419        }))
420    }
421}
422
423pub async fn set_color(ctx: &mut ExecContext<'_>, args: SetColorOpts) -> ExecResult {
424    let mut s = owo_colors::Style::new();
425    match args.background {
426        Some(SetColor::Normal) => s = s.on_default_color(),
427        Some(SetColor::Ansi(c)) => s = s.on_color(c),
428        Some(SetColor::Rgb(r, g, b)) => s = s.on_truecolor(r, g, b),
429        None => {}
430    }
431    if args.bold {
432        s = s.bold();
433    }
434    if args.dim {
435        s = s.dimmed();
436    }
437    if args.italics {
438        s = s.italic();
439    }
440    if args.reverse {
441        s = s.reversed();
442    }
443    if args.underline {
444        s = s.underline();
445    }
446
447    match args.color {
448        Some(SetColor::Normal) => s = s.default_color(),
449        Some(SetColor::Ansi(c)) => s = s.color(c),
450        Some(SetColor::Rgb(r, g, b)) => s = s.truecolor(r, g, b),
451        None => {}
452    }
453
454    let reset = if args.color == Some(SetColor::Normal) || args.background == Some(SetColor::Normal)
455    {
456        "\x1B[m"
457    } else {
458        ""
459    };
460
461    let s = format!("{reset}{}", s.prefix_formatter());
462    if !s.is_empty() {
463        let _: ExecResult<_> = ctx.io().stdout.write_all(s).await;
464        Ok(Status::SUCCESS)
465    } else {
466        Ok(Status::FAILURE)
467    }
468}
469
470pub async fn echo(ctx: &mut ExecContext<'_>, args: &[String]) -> ExecResult {
471    let mut newline = true;
472    let mut unescape = false;
473    let mut space = true;
474
475    let mut iter = args[1..].iter();
476    let mut buf = String::new();
477    let mut first = true;
478    for el in iter.by_ref() {
479        match &**el {
480            "-n" => newline = false,
481            "-s" => space = false,
482            "-E" => unescape = false,
483            "-e" => unescape = true,
484            "--" => break,
485            _ => {
486                buf.push_str(el);
487                first = false;
488                break;
489            }
490        }
491    }
492
493    for el in iter {
494        if first {
495            first = false;
496        } else if space {
497            buf.push(' ');
498        }
499        buf.push_str(el);
500    }
501    if newline {
502        buf.push('\n');
503    }
504
505    ensure!(!unescape, "TODO");
506
507    ctx.io().stdout.write_all(buf).await
508}
509
510#[derive(Debug, Parser)]
511pub struct TypeArgs {
512    pub names: Vec<String>,
513}
514
515pub async fn type_(ctx: &mut ExecContext<'_>, args: TypeArgs) -> ExecResult {
516    let mut buf = String::new();
517    let mut failed = 0usize;
518    for name in &args.names {
519        if ctx.get_global_func(name).is_some() {
520            // TODO: function source
521            writeln!(buf, "{name} is a function").unwrap();
522        } else if ctx.get_builtin(name).is_some() {
523            writeln!(buf, "{name} is a builtin").unwrap();
524        } else {
525            match ctx.locate_external_command(name) {
526                LocateExternalCommand::ExecFile(path) => {
527                    writeln!(buf, "{name} is {}", path.display()).unwrap();
528                }
529                LocateExternalCommand::NotExecFile(_)
530                | LocateExternalCommand::Dir(_)
531                | LocateExternalCommand::NotFound => {
532                    writeln!(buf, "type: cannot find {name:?}").unwrap();
533                    failed += 1;
534                }
535            }
536        }
537    }
538
539    let _: ExecResult<_> = ctx.io().stdout.write_all(buf).await;
540    Ok(failed.into())
541}