sprint/
lib.rs

1/*!
2# About
3
4The `sprint` crate provides the [`Shell`] struct which represents a shell
5session in your library or CLI code and can be used for running commands:
6
7* [Show the output](#run-commands-and-show-the-output)
8* [Return the output](#run-commands-and-return-the-output)
9
10[`Shell`] exposes its properties so you can easily
11[create a custom shell](#customize) or [modify an existing shell](#modify) with
12the settings you want.
13
14[`Shell`]: https://docs.rs/sprint/latest/sprint/struct.Shell.html
15
16# Examples
17
18## Run command(s) and show the output
19
20~~~rust
21use sprint::*;
22
23let shell = Shell::default();
24
25shell.run(&[Command::new("ls"), Command::new("ls -l")]);
26
27// or equivalently:
28//shell.run_str(&["ls", "ls -l"]);
29~~~
30
31## Run command(s) and return the output
32
33~~~rust
34use sprint::*;
35
36let shell = Shell::default();
37
38let results = shell.run(&[Command {
39    command: String::from("ls"),
40    stdout: Pipe::string(),
41    codes: vec![0],
42    ..Default::default()
43}]);
44
45assert_eq!(
46    results[0].stdout,
47    Pipe::String(Some(String::from("\
48Cargo.lock
49Cargo.toml
50CHANGELOG.md
51Makefile.md
52README.md
53src
54t
55target
56tests
57\
58    "))),
59);
60~~~
61
62## Customize
63
64~~~rust
65use sprint::*;
66
67let shell = Shell {
68    shell: Some(String::from("sh -c")),
69
70    dry_run: false,
71    sync: true,
72    print: true,
73    color: ColorOverride::Auto,
74
75    fence: String::from("```"),
76    info: String::from("text"),
77    prompt: String::from("$ "),
78
79    fence_style: style("#555555").expect("style"),
80    info_style: style("#555555").expect("style"),
81    prompt_style: style("#555555").expect("style"),
82    command_style: style("#00ffff+bold").expect("style"),
83    error_style: style("#ff0000+bold+italic").expect("style"),
84};
85
86shell.run(&[Command::new("ls"), Command::new("ls -l")]);
87~~~
88
89## Modify
90
91~~~rust
92use sprint::*;
93
94let mut shell = Shell::default();
95
96shell.shell = None;
97
98shell.run(&[Command::new("ls"), Command::new("ls -l")]);
99
100shell.sync = false;
101
102shell.run(&[Command::new("ls"), Command::new("ls -l")]);
103~~~
104*/
105
106//--------------------------------------------------------------------------------------------------
107
108use {
109    anstream::{print, println},
110    anyhow::{Result, anyhow},
111    clap::ValueEnum,
112    owo_colors::{OwoColorize, Rgb, Style},
113    rayon::prelude::*,
114    std::io::{Read, Write},
115};
116
117//--------------------------------------------------------------------------------------------------
118
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum Pipe {
121    Null,
122    Stdout,
123    Stderr,
124    String(Option<String>),
125}
126
127impl Pipe {
128    #[must_use]
129    pub fn string() -> Pipe {
130        Pipe::String(None)
131    }
132}
133
134//--------------------------------------------------------------------------------------------------
135
136/**
137Create a [`Style`] from a [`&str`] specification
138
139# Errors
140
141Returns an error if not able to parse the given `&str` as a style specification
142*/
143pub fn style(s: &str) -> Result<Style> {
144    let mut r = Style::new();
145    for i in s.split('+') {
146        if let Some(color) = i.strip_prefix('#') {
147            r = r.color(html(color)?);
148        } else if let Some(color) = i.strip_prefix("on-#") {
149            r = r.on_color(html(color)?);
150        } else {
151            match i {
152                "black" => r = r.black(),
153                "red" => r = r.red(),
154                "green" => r = r.green(),
155                "yellow" => r = r.yellow(),
156                "blue" => r = r.blue(),
157                "magenta" => r = r.magenta(),
158                "purple" => r = r.purple(),
159                "cyan" => r = r.cyan(),
160                "white" => r = r.white(),
161                //---
162                "bold" => r = r.bold(),
163                "italic" => r = r.italic(),
164                "dimmed" => r = r.dimmed(),
165                "underline" => r = r.underline(),
166                "blink" => r = r.blink(),
167                "blink_fast" => r = r.blink_fast(),
168                "reversed" => r = r.reversed(),
169                "hidden" => r = r.hidden(),
170                "strikethrough" => r = r.strikethrough(),
171                //---
172                "bright-black" => r = r.bright_black(),
173                "bright-red" => r = r.bright_red(),
174                "bright-green" => r = r.bright_green(),
175                "bright-yellow" => r = r.bright_yellow(),
176                "bright-blue" => r = r.bright_blue(),
177                "bright-magenta" => r = r.bright_magenta(),
178                "bright-purple" => r = r.bright_purple(),
179                "bright-cyan" => r = r.bright_cyan(),
180                "bright-white" => r = r.bright_white(),
181                //---
182                "on-black" => r = r.on_black(),
183                "on-red" => r = r.on_red(),
184                "on-green" => r = r.on_green(),
185                "on-yellow" => r = r.on_yellow(),
186                "on-blue" => r = r.on_blue(),
187                "on-magenta" => r = r.on_magenta(),
188                "on-purple" => r = r.on_purple(),
189                "on-cyan" => r = r.on_cyan(),
190                "on-white" => r = r.on_white(),
191                //---
192                "on-bright-black" => r = r.on_bright_black(),
193                "on-bright-red" => r = r.on_bright_red(),
194                "on-bright-green" => r = r.on_bright_green(),
195                "on-bright-yellow" => r = r.on_bright_yellow(),
196                "on-bright-blue" => r = r.on_bright_blue(),
197                "on-bright-magenta" => r = r.on_bright_magenta(),
198                "on-bright-purple" => r = r.on_bright_purple(),
199                "on-bright-cyan" => r = r.on_bright_cyan(),
200                "on-bright-white" => r = r.on_bright_white(),
201                //---
202                _ => return Err(anyhow!("Invalid style spec: {s:?}!")),
203            }
204        }
205    }
206    Ok(r)
207}
208
209fn html(rrggbb: &str) -> Result<Rgb> {
210    let r = u8::from_str_radix(&rrggbb[0..2], 16)?;
211    let g = u8::from_str_radix(&rrggbb[2..4], 16)?;
212    let b = u8::from_str_radix(&rrggbb[4..6], 16)?;
213    Ok(Rgb(r, g, b))
214}
215
216#[derive(Clone, Debug, Default, ValueEnum)]
217pub enum ColorOverride {
218    #[default]
219    Auto,
220    Always,
221    Never,
222}
223
224impl ColorOverride {
225    pub fn init(&self) {
226        match self {
227            ColorOverride::Always => anstream::ColorChoice::Always.write_global(),
228            ColorOverride::Never => anstream::ColorChoice::Never.write_global(),
229            ColorOverride::Auto => {}
230        }
231    }
232}
233
234//--------------------------------------------------------------------------------------------------
235
236struct Prefix {
237    style: Style,
238}
239
240impl std::fmt::Display for Prefix {
241    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
242        self.style.fmt_prefix(f)
243    }
244}
245
246fn print_prefix(style: Style) {
247    print!("{}", Prefix { style });
248}
249
250//--------------------------------------------------------------------------------------------------
251
252struct Suffix {
253    style: Style,
254}
255
256impl std::fmt::Display for Suffix {
257    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
258        self.style.fmt_suffix(f)
259    }
260}
261
262fn print_suffix(style: Style) {
263    print!("{}", Suffix { style });
264}
265
266//--------------------------------------------------------------------------------------------------
267
268/**
269Command runner
270
271*Please see also the module-level documentation for a high-level description and examples.*
272
273```
274use sprint::*;
275
276// Use the default configuration:
277
278let shell = Shell::default();
279
280// Or a custom configuration:
281
282let shell = Shell {
283    shell: Some(String::from("sh -c")),
284    //shell: Some(String::from("bash -c")), // Use bash
285    //shell: Some(String::from("bash -xeo pipefail -c")), // Use bash w/ options
286    //shell: None, // Run directly instead of a shell
287
288    dry_run: false,
289    sync: true,
290    print: true,
291    color: ColorOverride::default(),
292
293    fence: String::from("```"),
294    info: String::from("text"),
295    prompt: String::from("$ "),
296
297    fence_style: style("#555555").expect("style"),
298    info_style: style("#555555").expect("style"),
299    prompt_style: style("#555555").expect("style"),
300    command_style: style("#00ffff+bold").expect("style"),
301    error_style: style("#ff0000+bold+italic").expect("style"),
302};
303
304// Or modify it on the fly:
305
306let mut shell = Shell::default();
307
308shell.shell = None;
309shell.sync = false;
310
311// ...
312```
313*/
314#[derive(Clone, Debug)]
315pub struct Shell {
316    pub shell: Option<String>,
317
318    pub dry_run: bool,
319    pub sync: bool,
320    pub print: bool,
321    pub color: ColorOverride,
322
323    pub fence: String,
324    pub info: String,
325    pub prompt: String,
326
327    pub fence_style: Style,
328    pub info_style: Style,
329    pub prompt_style: Style,
330    pub command_style: Style,
331    pub error_style: Style,
332}
333
334impl Default for Shell {
335    /// Default [`Shell`]
336    fn default() -> Shell {
337        Shell {
338            shell: Some(String::from("sh -c")),
339
340            dry_run: false,
341            sync: true,
342            print: true,
343            color: ColorOverride::default(),
344
345            fence: String::from("```"),
346            info: String::from("text"),
347            prompt: String::from("$ "),
348
349            fence_style: style("#555555").expect("style"),
350            info_style: style("#555555").expect("style"),
351            prompt_style: style("#555555").expect("style"),
352            command_style: style("#00ffff+bold").expect("style"),
353            error_style: style("#ff0000+bold+italic").expect("style"),
354        }
355    }
356}
357
358impl Shell {
359    /// Run command(s)
360    #[must_use]
361    pub fn run(&self, commands: &[Command]) -> Vec<Command> {
362        if self.sync {
363            if self.print {
364                self.print_fence(0);
365                println!("{}", self.info.style(self.info_style));
366            }
367
368            let mut r = vec![];
369            let mut error = None;
370
371            for (i, command) in commands.iter().enumerate() {
372                if i > 0 && self.print && !self.dry_run {
373                    println!();
374                }
375
376                let result = self.run1(command);
377
378                if let Some(code) = &result.code {
379                    if !result.codes.contains(code) {
380                        error = Some(format!(
381                            "**Command `{}` exited with code: `{code}`!**",
382                            result.command,
383                        ));
384                    }
385                } else if !self.dry_run {
386                    error = Some(format!(
387                        "**Command `{}` was killed by a signal!**",
388                        result.command,
389                    ));
390                }
391
392                r.push(result);
393
394                if error.is_some() {
395                    break;
396                }
397            }
398
399            if self.print {
400                self.print_fence(2);
401
402                if let Some(error) = error {
403                    println!("{}\n", error.style(self.error_style));
404                }
405            }
406
407            r
408        } else {
409            commands
410                .par_iter()
411                .map(|command| self.run1(command))
412                .collect()
413        }
414    }
415
416    /// Run a single command
417    #[must_use]
418    pub fn run1(&self, command: &Command) -> Command {
419        if self.print {
420            if !self.dry_run {
421                print!("{}", self.prompt.style(self.prompt_style));
422            }
423
424            println!(
425                "{}",
426                command
427                    .command
428                    .replace(" && ", " \\\n&& ")
429                    .replace(" || ", " \\\n|| ")
430                    .replace("; ", "; \\\n")
431                    .style(self.command_style),
432            );
433        }
434
435        if self.dry_run {
436            return command.clone();
437        }
438
439        self.core(command)
440    }
441
442    /// Pipe a single command
443    #[must_use]
444    pub fn pipe1(&self, command: &str) -> String {
445        let command = Command {
446            command: command.to_string(),
447            stdout: Pipe::string(),
448            ..Default::default()
449        };
450
451        let result = self.core(&command);
452
453        if let Pipe::String(Some(stdout)) = &result.stdout {
454            stdout.clone()
455        } else {
456            String::new()
457        }
458    }
459
460    /**
461    Run a command in a child process
462
463    # Panics
464
465    Panics if not able to spawn the child process
466    */
467    #[must_use]
468    pub fn run1_async(&self, command: &Command) -> std::process::Child {
469        let (prog, args) = self.prepare(&command.command);
470
471        let mut cmd = std::process::Command::new(prog);
472        cmd.args(&args);
473
474        if matches!(command.stdin, Pipe::String(_)) {
475            cmd.stdin(std::process::Stdio::piped());
476        }
477
478        if matches!(command.stdout, Pipe::String(_) | Pipe::Null) {
479            cmd.stdout(std::process::Stdio::piped());
480        }
481
482        if matches!(command.stderr, Pipe::String(_) | Pipe::Null) {
483            cmd.stderr(std::process::Stdio::piped());
484        }
485
486        if self.print
487            && let Pipe::String(Some(s)) = &command.stdin
488        {
489            self.print_fence(0);
490            println!("{}", command.command.style(self.info_style));
491            println!("{s}");
492            self.print_fence(2);
493            self.print_fence(0);
494            println!("{}", self.info.style(self.info_style));
495        }
496
497        let mut child = cmd.spawn().unwrap();
498
499        if let Pipe::String(Some(s)) = &command.stdin {
500            let mut stdin = child.stdin.take().unwrap();
501            stdin.write_all(s.as_bytes()).unwrap();
502        }
503
504        child
505    }
506
507    /**
508    Core part to run/pipe a command
509
510    # Panics
511
512    Panics if not able to spawn the child process
513    */
514    #[must_use]
515    pub fn core(&self, command: &Command) -> Command {
516        let mut child = self.run1_async(command);
517
518        let mut r = command.clone();
519
520        r.code = match child.wait() {
521            Ok(status) => status.code(),
522            Err(_e) => None,
523        };
524
525        if matches!(command.stdout, Pipe::String(_)) {
526            let mut stdout = String::new();
527            child.stdout.unwrap().read_to_string(&mut stdout).unwrap();
528            r.stdout = Pipe::String(Some(stdout));
529        }
530
531        if matches!(command.stderr, Pipe::String(_)) {
532            let mut stderr = String::new();
533            child.stderr.unwrap().read_to_string(&mut stderr).unwrap();
534            r.stderr = Pipe::String(Some(stderr));
535        }
536
537        if self.print
538            && let Pipe::String(Some(_s)) = &command.stdin
539        {
540            self.print_fence(2);
541        }
542
543        r
544    }
545
546    /// Prepare the command
547    fn prepare(&self, command: &str) -> (String, Vec<String>) {
548        if let Some(s) = &self.shell {
549            let mut args = shlex::split(s).unwrap();
550            let prog = args.remove(0);
551            args.push(command.to_string());
552            (prog, args)
553        } else {
554            // Shell disabled; run command directly
555            let mut args = shlex::split(command).unwrap();
556            let prog = args.remove(0);
557            (prog, args)
558        }
559    }
560
561    /// Print the fence
562    pub fn print_fence(&self, newlines: usize) {
563        print!(
564            "{}{}",
565            self.fence.style(self.fence_style),
566            "\n".repeat(newlines),
567        );
568    }
569
570    /**
571    Print the interactive prompt
572
573    # Panics
574
575    Panics if not able to flush stdout
576    */
577    pub fn interactive_prompt(&self, previous: bool) {
578        if previous {
579            self.print_fence(2);
580        }
581
582        self.print_fence(0);
583        println!("{}", self.info.style(self.info_style));
584        print!("{}", self.prompt.style(self.prompt_style));
585
586        // Set the command style
587        print_prefix(self.command_style);
588        std::io::stdout().flush().expect("flush");
589    }
590
591    /**
592    Clear the command style
593
594    # Panics
595
596    Panics if not able to flush stdout
597    */
598    pub fn interactive_prompt_reset(&self) {
599        print_suffix(self.command_style);
600        std::io::stdout().flush().expect("flush");
601    }
602
603    /// Simpler interface to run command(s)
604    #[must_use]
605    pub fn run_str(&self, commands: &[&str]) -> Vec<Command> {
606        self.run(&commands.iter().map(|x| Command::new(x)).collect::<Vec<_>>())
607    }
608}
609
610//--------------------------------------------------------------------------------------------------
611
612#[derive(Clone, Debug, PartialEq, Eq)]
613pub struct Command {
614    pub command: String,
615    pub stdin: Pipe,
616    pub codes: Vec<i32>,
617    pub stdout: Pipe,
618    pub stderr: Pipe,
619    pub code: Option<i32>,
620}
621
622impl Default for Command {
623    fn default() -> Command {
624        Command {
625            command: String::default(),
626            stdin: Pipe::Null,
627            codes: vec![0],
628            stdout: Pipe::Stdout,
629            stderr: Pipe::Stderr,
630            code: Option::default(),
631        }
632    }
633}
634
635impl Command {
636    #[must_use]
637    pub fn new(command: &str) -> Command {
638        Command {
639            command: command.to_string(),
640            ..Default::default()
641        }
642    }
643}