Skip to main content

strat9_kernel/shell/
parser.rs

1//! Shell command parser with pipeline and redirection support.
2//!
3//! Supports:
4//! - Simple commands: `ls /tmp`
5//! - Pipes: `cat /tmp/foo | grep bar`
6//! - Output redirect (truncate): `ls > /tmp/out`
7//! - Output redirect (append): `ls >> /tmp/out`
8//! - Input redirect: `grep pattern < /tmp/input`
9//! - Combinations: `cat /tmp/data | grep key > /tmp/result`
10
11use alloc::{
12    string::{String, ToString},
13    vec::Vec,
14};
15
16/// Parsed command structure.
17#[derive(Debug)]
18pub struct Command {
19    /// Command name (e.g., "mem", "silo", "ps").
20    pub name: String,
21    /// Arguments (e.g., ["ls"] for "silo ls").
22    pub args: Vec<String>,
23}
24
25/// Output redirection target.
26#[derive(Debug, Clone)]
27pub enum Redirect {
28    /// Truncate and write (`>`).
29    Truncate(String),
30    /// Append (`>>`).
31    Append(String),
32}
33
34/// A single stage in a pipeline.
35#[derive(Debug)]
36pub struct PipelineStage {
37    /// The command to execute.
38    pub command: Command,
39    /// Optional output redirect (`>` or `>>`).
40    pub stdout_redirect: Option<Redirect>,
41    /// Optional input redirect (`<`).
42    pub stdin_redirect: Option<String>,
43}
44
45/// A parsed pipeline (one or more stages connected by `|`).
46#[derive(Debug)]
47pub struct Pipeline {
48    /// Ordered stages; output of stage N feeds into stage N+1.
49    pub stages: Vec<PipelineStage>,
50}
51
52/// Parse a full command line into a [`Pipeline`].
53///
54/// Returns `None` if the line is empty or whitespace-only.
55pub fn parse_pipeline(line: &str) -> Option<Pipeline> {
56    let trimmed = line.trim();
57    if trimmed.is_empty() {
58        return None;
59    }
60
61    let segments: Vec<&str> = trimmed.split('|').collect();
62    let mut stages = Vec::new();
63
64    for seg in &segments {
65        let stage = parse_stage(seg.trim())?;
66        stages.push(stage);
67    }
68
69    if stages.is_empty() {
70        return None;
71    }
72
73    Some(Pipeline { stages })
74}
75
76/// Parse a single pipeline stage (command + optional redirections).
77fn parse_stage(segment: &str) -> Option<PipelineStage> {
78    let tokens = tokenize(segment);
79    if tokens.is_empty() {
80        return None;
81    }
82
83    let mut cmd_tokens: Vec<String> = Vec::new();
84    let mut stdout_redirect: Option<Redirect> = None;
85    let mut stdin_redirect: Option<String> = None;
86
87    let mut i = 0;
88    while i < tokens.len() {
89        if tokens[i] == ">>" {
90            i += 1;
91            if i < tokens.len() {
92                stdout_redirect = Some(Redirect::Append(tokens[i].clone()));
93            }
94        } else if tokens[i] == ">" {
95            i += 1;
96            if i < tokens.len() {
97                stdout_redirect = Some(Redirect::Truncate(tokens[i].clone()));
98            }
99        } else if tokens[i] == "<" {
100            i += 1;
101            if i < tokens.len() {
102                stdin_redirect = Some(tokens[i].clone());
103            }
104        } else {
105            cmd_tokens.push(tokens[i].clone());
106        }
107        i += 1;
108    }
109
110    if cmd_tokens.is_empty() {
111        return None;
112    }
113
114    let name = cmd_tokens.remove(0);
115    Some(PipelineStage {
116        command: Command {
117            name,
118            args: cmd_tokens,
119        },
120        stdout_redirect,
121        stdin_redirect,
122    })
123}
124
125/// Tokenize a segment, keeping `>>`, `>`, `<` as distinct tokens.
126fn tokenize(input: &str) -> Vec<String> {
127    let mut tokens = Vec::new();
128    let mut current = String::new();
129    let chars: Vec<char> = input.chars().collect();
130    let mut i = 0;
131
132    while i < chars.len() {
133        match chars[i] {
134            '>' => {
135                if !current.is_empty() {
136                    tokens.push(core::mem::take(&mut current));
137                }
138                if i + 1 < chars.len() && chars[i + 1] == '>' {
139                    tokens.push(String::from(">>"));
140                    i += 2;
141                } else {
142                    tokens.push(String::from(">"));
143                    i += 1;
144                }
145            }
146            '<' => {
147                if !current.is_empty() {
148                    tokens.push(core::mem::take(&mut current));
149                }
150                tokens.push(String::from("<"));
151                i += 1;
152            }
153            ' ' | '\t' => {
154                if !current.is_empty() {
155                    tokens.push(core::mem::take(&mut current));
156                }
157                i += 1;
158            }
159            ch => {
160                current.push(ch);
161                i += 1;
162            }
163        }
164    }
165
166    if !current.is_empty() {
167        tokens.push(current);
168    }
169
170    tokens
171}
172
173/// Parse a simple command line (no pipe/redirect support).
174///
175/// Returns `None` if the line is empty or whitespace-only.
176pub fn parse(line: &str) -> Option<Command> {
177    let trimmed = line.trim();
178    if trimmed.is_empty() {
179        return None;
180    }
181
182    let mut parts = trimmed.split_whitespace();
183    let name = parts.next()?.to_string();
184    let args: Vec<String> = parts.map(|s| s.to_string()).collect();
185
186    Some(Command { name, args })
187}