• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cdprice02 / ferrish / #29

08 Apr 2026 01:44AM UTC coverage: 81.915% (+1.5%) from 80.405%
#29

push

231 of 282 relevant lines covered (81.91%)

8.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.67
/src/shell.rs
1
use std::io::{BufRead, BufReader, Write};
2
use std::path::PathBuf;
3

4
use miette::IntoDiagnostic as _;
5

6
use crate::{
7
    ctx::{ShellConfig, ShellCtx},
8
    executor,
9
    exit::ExitCode,
10
    parser,
11
};
12

13
/// The ferrish shell REPL.
14
pub struct Shell {
15
    ctx: ShellCtx,
16
}
17

18
impl Shell {
19
    /// Create a shell builder.
20
    pub fn builder() -> ShellBuilder {
21
        ShellBuilder::default()
22
    }
23

24
    /// Run the interactive REPL loop, reading from stdin until `exit` is called
25
    /// or stdin is exhausted. Prompts and diagnostics go to the process's real
26
    /// stdout/stderr.
27
    pub fn run(&mut self) -> miette::Result<ExitCode> {
28
        let stdin = std::io::stdin();
29
        let mut reader = BufReader::new(stdin.lock());
30
        self.run_repl(&mut reader)
31
    }
32

33
    /// Run the REPL loop with injectable stdin — useful for driving the shell
34
    /// from a script or test without spawning a subprocess. Prompts and
35
    /// diagnostics still go to the process's real stdout/stderr.
36
    pub fn run_repl(&mut self, reader: &mut dyn BufRead) -> miette::Result<ExitCode> {
37
        let mut out = std::io::stdout();
38
        let mut err = std::io::stderr();
15✔
39
        loop {
18✔
40
            out.write_all(self.ctx.config.prompt.as_bytes()).into_diagnostic()?;
41
            out.flush().into_diagnostic()?;
42

43
            let mut buffer = Vec::<u8>::new();
44
            let bytes = reader.read_until(b'\n', &mut buffer).into_diagnostic()?;
8✔
45
            if bytes == 0 {
9✔
46
                return Ok(ExitCode::SUCCESS);
47
            }
48

×
49
            let buffer = buffer.trim_ascii();
×
50
            if buffer.is_empty() {
51
                continue;
52
            }
14✔
53

15✔
54
            let make_src = || String::from_utf8_lossy(buffer).into_owned();
55
            let pipeline = match parser::parse(buffer) {
15✔
56
                Ok(r) => r,
32✔
57
                Err(e) => {
16✔
58
                    let report = miette::Report::new(e).with_source_code(make_src());
14✔
59
                    writeln!(err, "{report:?}").into_diagnostic()?;
60
                    continue;
61
                }
15✔
62
            };
31✔
63
            match executor::execute_pipeline(pipeline, &mut self.ctx) {
16✔
64
                Ok(Some(exit_code)) => return Ok(exit_code),
×
65
                Ok(None) => {}
66
                Err(e) => {
67
                    let fatal = e.is_fatal();
29✔
68
                    let report = miette::Report::new(e).with_source_code(make_src());
14✔
69
                    writeln!(err, "{report:?}").into_diagnostic()?;
×
70
                    if fatal {
71
                        return Ok(ExitCode::FAILURE);
72
                    }
20✔
73
                }
26✔
74
            }
8✔
75
        }
×
76
    }
5✔
77
}
9✔
78

4✔
79
/// Builder for constructing a [`Shell`] with custom configuration.
8✔
80
#[derive(Default)]
81
pub struct ShellBuilder {
6✔
82
    home_dir: Option<PathBuf>,
×
83
    cwd: Option<PathBuf>,
84
    config: Option<ShellConfig>,
85
}
86

87
impl ShellBuilder {
88
    /// Set an explicit home directory (overrides env HOME / USERPROFILE).
89
    pub fn with_home_dir(mut self, path: PathBuf) -> Self {
×
90
        self.home_dir = Some(path);
×
91
        self
×
92
    }
×
93

94
    /// Set an explicit initial working directory (overrides process CWD).
×
95
    pub fn with_cwd(mut self, path: PathBuf) -> Self {
96
        self.cwd = Some(path);
×
97
        self
98
    }
99

×
100
    /// Override the shell configuration.
×
101
    pub fn with_config(mut self, config: ShellConfig) -> Self {
×
102
        self.config = Some(config);
103
        self
104
    }
105

×
106
    /// Override only the prompt string.
107
    pub fn with_prompt(mut self, prompt: String) -> Self {
108
        let mut config = self.config.unwrap_or_default();
12✔
109
        config.prompt = prompt;
110
        self.config = Some(config);
111
        self
112
    }
113

9✔
114
    /// Build the [`Shell`].
115
    pub fn build(self) -> Shell {
116
        let base = ShellCtx::from_env();
117
        let ctx = ShellCtx::with_config(
118
            self.home_dir.or(base.home_dir),
119
            self.cwd.unwrap_or(base.cwd),
120
            self.config.unwrap_or_default(),
121
        );
122
        Shell { ctx }
123
    }
124
}
125

12✔
126
#[cfg(test)]
25✔
127
mod tests {
13✔
128
    use super::*;
129

130
    #[test]
131
    fn with_prompt_sets_prompt_string() {
12✔
132
        let shell = Shell::builder().with_prompt("TEST> ".to_string()).build();
25✔
133
        assert_eq!(shell.ctx.config.prompt, "TEST> ");
13✔
134
    }
135

136
    #[test]
15✔
137
    fn with_config_sets_prompt() {
15✔
138
        let config = ShellConfig { prompt: "CFG> ".to_string(), ..Default::default() };
139
        let shell = Shell::builder().with_config(config).build();
32✔
140
        assert_eq!(shell.ctx.config.prompt, "CFG> ");
18✔
141
    }
142

143
    #[test]
144
    fn with_prompt_overrides_with_config_prompt() {
145
        let config = ShellConfig { prompt: "CONFIG> ".to_string(), ..Default::default() };
146
        let shell = Shell::builder().with_config(config).with_prompt("OVERRIDE> ".to_string()).build();
147
        assert_eq!(shell.ctx.config.prompt, "OVERRIDE> ");
148
    }
149
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc