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::{anyhow, Result},
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    pub fn string() -> Pipe {
129        Pipe::String(None)
130    }
131}
132
133//--------------------------------------------------------------------------------------------------
134
135/// Create a [`Style`] from a [`&str`] specification
136pub fn style(s: &str) -> Result<Style> {
137    let mut r = Style::new();
138    for i in s.split('+') {
139        if let Some(color) = i.strip_prefix('#') {
140            r = r.color(html(color)?);
141        } else if let Some(color) = i.strip_prefix("on-#") {
142            r = r.on_color(html(color)?);
143        } else {
144            match i {
145                "black" => r = r.black(),
146                "red" => r = r.red(),
147                "green" => r = r.green(),
148                "yellow" => r = r.yellow(),
149                "blue" => r = r.blue(),
150                "magenta" => r = r.magenta(),
151                "purple" => r = r.purple(),
152                "cyan" => r = r.cyan(),
153                "white" => r = r.white(),
154                //---
155                "bold" => r = r.bold(),
156                "italic" => r = r.italic(),
157                "dimmed" => r = r.dimmed(),
158                "underline" => r = r.underline(),
159                "blink" => r = r.blink(),
160                "blink_fast" => r = r.blink_fast(),
161                "reversed" => r = r.reversed(),
162                "hidden" => r = r.hidden(),
163                "strikethrough" => r = r.strikethrough(),
164                //---
165                "bright-black" => r = r.bright_black(),
166                "bright-red" => r = r.bright_red(),
167                "bright-green" => r = r.bright_green(),
168                "bright-yellow" => r = r.bright_yellow(),
169                "bright-blue" => r = r.bright_blue(),
170                "bright-magenta" => r = r.bright_magenta(),
171                "bright-purple" => r = r.bright_purple(),
172                "bright-cyan" => r = r.bright_cyan(),
173                "bright-white" => r = r.bright_white(),
174                //---
175                "on-black" => r = r.on_black(),
176                "on-red" => r = r.on_red(),
177                "on-green" => r = r.on_green(),
178                "on-yellow" => r = r.on_yellow(),
179                "on-blue" => r = r.on_blue(),
180                "on-magenta" => r = r.on_magenta(),
181                "on-purple" => r = r.on_purple(),
182                "on-cyan" => r = r.on_cyan(),
183                "on-white" => r = r.on_white(),
184                //---
185                "on-bright-black" => r = r.on_bright_black(),
186                "on-bright-red" => r = r.on_bright_red(),
187                "on-bright-green" => r = r.on_bright_green(),
188                "on-bright-yellow" => r = r.on_bright_yellow(),
189                "on-bright-blue" => r = r.on_bright_blue(),
190                "on-bright-magenta" => r = r.on_bright_magenta(),
191                "on-bright-purple" => r = r.on_bright_purple(),
192                "on-bright-cyan" => r = r.on_bright_cyan(),
193                "on-bright-white" => r = r.on_bright_white(),
194                //---
195                _ => return Err(anyhow!("Invalid style spec: {s:?}!")),
196            }
197        }
198    }
199    Ok(r)
200}
201
202fn html(rrggbb: &str) -> Result<Rgb> {
203    let r = u8::from_str_radix(&rrggbb[0..2], 16)?;
204    let g = u8::from_str_radix(&rrggbb[2..4], 16)?;
205    let b = u8::from_str_radix(&rrggbb[4..6], 16)?;
206    Ok(Rgb(r, g, b))
207}
208
209#[derive(Clone, Debug, Default, ValueEnum)]
210pub enum ColorOverride {
211    #[default]
212    Auto,
213    Always,
214    Never,
215}
216
217impl ColorOverride {
218    pub fn init(&self) {
219        match self {
220            ColorOverride::Always => anstream::ColorChoice::Always.write_global(),
221            ColorOverride::Never => anstream::ColorChoice::Never.write_global(),
222            ColorOverride::Auto => {}
223        }
224    }
225}
226
227//--------------------------------------------------------------------------------------------------
228
229struct Prefix {
230    style: Style,
231}
232
233impl std::fmt::Display for Prefix {
234    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
235        self.style.fmt_prefix(f)
236    }
237}
238
239fn print_prefix(style: Style) {
240    print!("{}", Prefix { style });
241}
242
243//--------------------------------------------------------------------------------------------------
244
245struct Suffix {
246    style: Style,
247}
248
249impl std::fmt::Display for Suffix {
250    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
251        self.style.fmt_suffix(f)
252    }
253}
254
255fn print_suffix(style: Style) {
256    print!("{}", Suffix { style });
257}
258
259//--------------------------------------------------------------------------------------------------
260
261/**
262Command runner
263
264*Please see also the module-level documentation for a high-level description and examples.*
265
266```
267use sprint::*;
268
269// Use the default configuration:
270
271let shell = Shell::default();
272
273// Or a custom configuration:
274
275let shell = Shell {
276    shell: Some(String::from("sh -c")),
277    //shell: Some(String::from("bash -c")), // Use bash
278    //shell: Some(String::from("bash -xeo pipefail -c")), // Use bash w/ options
279    //shell: None, // Run directly instead of a shell
280
281    dry_run: false,
282    sync: true,
283    print: true,
284    color: ColorOverride::default(),
285
286    fence: String::from("```"),
287    info: String::from("text"),
288    prompt: String::from("$ "),
289
290    fence_style: style("#555555").expect("style"),
291    info_style: style("#555555").expect("style"),
292    prompt_style: style("#555555").expect("style"),
293    command_style: style("#00ffff+bold").expect("style"),
294    error_style: style("#ff0000+bold+italic").expect("style"),
295};
296
297// Or modify it on the fly:
298
299let mut shell = Shell::default();
300
301shell.shell = None;
302shell.sync = false;
303
304// ...
305```
306*/
307#[derive(Clone, Debug)]
308pub struct Shell {
309    pub shell: Option<String>,
310
311    pub dry_run: bool,
312    pub sync: bool,
313    pub print: bool,
314    pub color: ColorOverride,
315
316    pub fence: String,
317    pub info: String,
318    pub prompt: String,
319
320    pub fence_style: Style,
321    pub info_style: Style,
322    pub prompt_style: Style,
323    pub command_style: Style,
324    pub error_style: Style,
325}
326
327impl Default for Shell {
328    /// Default [`Shell`]
329    fn default() -> Shell {
330        Shell {
331            shell: Some(String::from("sh -c")),
332
333            dry_run: false,
334            sync: true,
335            print: true,
336            color: ColorOverride::default(),
337
338            fence: String::from("```"),
339            info: String::from("text"),
340            prompt: String::from("$ "),
341
342            fence_style: style("#555555").expect("style"),
343            info_style: style("#555555").expect("style"),
344            prompt_style: style("#555555").expect("style"),
345            command_style: style("#00ffff+bold").expect("style"),
346            error_style: style("#ff0000+bold+italic").expect("style"),
347        }
348    }
349}
350
351impl Shell {
352    /// Run command(s)
353    pub fn run(&self, commands: &[Command]) -> Vec<Command> {
354        if self.sync {
355            if self.print {
356                self.print_fence(0);
357                println!("{}", self.info.style(self.info_style));
358            }
359
360            let mut r = vec![];
361            let mut error = None;
362
363            for (i, command) in commands.iter().enumerate() {
364                if i > 0 && self.print && !self.dry_run {
365                    println!();
366                }
367
368                let result = self.run1(command);
369
370                if let Some(code) = &result.code {
371                    if !result.codes.contains(code) {
372                        error = Some(format!(
373                            "**Command `{}` exited with code: `{code}`!**",
374                            result.command,
375                        ));
376                    }
377                } else if !self.dry_run {
378                    error = Some(format!(
379                        "**Command `{}` was killed by a signal!**",
380                        result.command,
381                    ));
382                }
383
384                r.push(result);
385
386                if error.is_some() {
387                    break;
388                }
389            }
390
391            if self.print {
392                self.print_fence(2);
393
394                if let Some(error) = error {
395                    println!("{}\n", error.style(self.error_style));
396                }
397            }
398
399            r
400        } else {
401            commands
402                .par_iter()
403                .map(|command| self.run1(command))
404                .collect()
405        }
406    }
407
408    /// Run a single command
409    pub fn run1(&self, command: &Command) -> Command {
410        if self.print {
411            if !self.dry_run {
412                print!("{}", self.prompt.style(self.prompt_style));
413            }
414
415            println!(
416                "{}",
417                command
418                    .command
419                    .replace(" && ", " \\\n&& ")
420                    .replace(" || ", " \\\n|| ")
421                    .replace("; ", "; \\\n")
422                    .style(self.command_style),
423            );
424        }
425
426        if self.dry_run {
427            return command.clone();
428        }
429
430        self.core(command)
431    }
432
433    /// Pipe a single command
434    pub fn pipe1(&self, command: &str) -> String {
435        let command = Command {
436            command: command.to_string(),
437            stdout: Pipe::string(),
438            ..Default::default()
439        };
440
441        let result = self.core(&command);
442
443        if let Pipe::String(Some(stdout)) = &result.stdout {
444            stdout.to_string()
445        } else {
446            String::new()
447        }
448    }
449
450    /// Run a command in a child process
451    pub fn run1_async(&self, command: &Command) -> std::process::Child {
452        let (prog, args) = self.prepare(&command.command);
453
454        let mut cmd = std::process::Command::new(prog);
455        cmd.args(&args);
456
457        if matches!(command.stdin, Pipe::String(_)) {
458            cmd.stdin(std::process::Stdio::piped());
459        }
460
461        if matches!(command.stdout, Pipe::String(_) | Pipe::Null) {
462            cmd.stdout(std::process::Stdio::piped());
463        }
464
465        if matches!(command.stderr, Pipe::String(_) | Pipe::Null) {
466            cmd.stderr(std::process::Stdio::piped());
467        }
468
469        if self.print {
470            if let Pipe::String(Some(s)) = &command.stdin {
471                self.print_fence(0);
472                println!("{}", command.command.style(self.info_style));
473                println!("{s}");
474                self.print_fence(2);
475                self.print_fence(0);
476                println!("{}", self.info.style(self.info_style));
477            }
478        }
479
480        let mut child = cmd.spawn().unwrap();
481
482        if let Pipe::String(Some(s)) = &command.stdin {
483            let mut stdin = child.stdin.take().unwrap();
484            stdin.write_all(s.as_bytes()).unwrap();
485        }
486
487        child
488    }
489
490    /// Core part to run/pipe a command
491    pub fn core(&self, command: &Command) -> Command {
492        let mut child = self.run1_async(command);
493
494        let mut r = command.clone();
495
496        r.code = match child.wait() {
497            Ok(status) => status.code(),
498            Err(_e) => None,
499        };
500
501        if matches!(command.stdout, Pipe::String(_)) {
502            let mut stdout = String::new();
503            child.stdout.unwrap().read_to_string(&mut stdout).unwrap();
504            r.stdout = Pipe::String(Some(stdout));
505        }
506
507        if matches!(command.stderr, Pipe::String(_)) {
508            let mut stderr = String::new();
509            child.stderr.unwrap().read_to_string(&mut stderr).unwrap();
510            r.stderr = Pipe::String(Some(stderr));
511        }
512
513        if self.print {
514            if let Pipe::String(Some(_s)) = &command.stdin {
515                self.print_fence(2);
516            }
517        }
518
519        r
520    }
521
522    /// Prepare the command
523    fn prepare(&self, command: &str) -> (String, Vec<String>) {
524        if let Some(s) = &self.shell {
525            let mut args = shlex::split(s).unwrap();
526            let prog = args.remove(0);
527            args.push(command.to_string());
528            (prog, args)
529        } else {
530            // Shell disabled; run command directly
531            let mut args = shlex::split(command).unwrap();
532            let prog = args.remove(0);
533            (prog, args)
534        }
535    }
536
537    /// Print the fence
538    pub fn print_fence(&self, newlines: usize) {
539        print!(
540            "{}{}",
541            self.fence.style(self.fence_style),
542            "\n".repeat(newlines),
543        );
544    }
545
546    /// Print the interactive prompt
547    pub fn interactive_prompt(&self, previous: bool) {
548        if previous {
549            self.print_fence(2);
550        }
551
552        self.print_fence(0);
553        println!("{}", self.info.style(self.info_style));
554        print!("{}", self.prompt.style(self.prompt_style));
555
556        // Set the command style
557        print_prefix(self.command_style);
558        std::io::stdout().flush().expect("flush");
559    }
560
561    /// Clear the command style
562    pub fn interactive_prompt_reset(&self) {
563        print_suffix(self.command_style);
564        std::io::stdout().flush().expect("flush");
565    }
566
567    /// Simpler interface to run command(s)
568    pub fn run_str(&self, commands: &[&str]) -> Vec<Command> {
569        self.run(&commands.iter().map(|x| Command::new(x)).collect::<Vec<_>>())
570    }
571}
572
573//--------------------------------------------------------------------------------------------------
574
575#[derive(Clone, Debug, PartialEq, Eq)]
576pub struct Command {
577    pub command: String,
578    pub stdin: Pipe,
579    pub codes: Vec<i32>,
580    pub stdout: Pipe,
581    pub stderr: Pipe,
582    pub code: Option<i32>,
583}
584
585impl Default for Command {
586    fn default() -> Command {
587        Command {
588            command: Default::default(),
589            stdin: Pipe::Null,
590            codes: vec![0],
591            stdout: Pipe::Stdout,
592            stderr: Pipe::Stderr,
593            code: Default::default(),
594        }
595    }
596}
597
598impl Command {
599    pub fn new(command: &str) -> Command {
600        Command {
601            command: command.to_string(),
602            ..Default::default()
603        }
604    }
605}