1use super::Command;
20use crate::common::syntax::Mode;
21use crate::common::syntax::OptionArgumentSpec;
22use crate::common::syntax::OptionSpec;
23use crate::common::syntax::parse_arguments;
24use thiserror::Error;
25use yash_env::Env;
26use yash_env::semantics::Field;
27use yash_env::source::pretty::Snippet;
28use yash_env::source::pretty::{Report, ReportType};
29
30#[derive(Clone, Debug, Eq, Error, PartialEq)]
32#[non_exhaustive]
33pub enum Error {
34 #[error(transparent)]
36 CommonError(#[from] crate::common::syntax::ParseError<'static>),
37
38 #[error("multibyte delimiter is not supported")]
40 MultibyteDelimiter { delimiter: Field },
41
42 #[error("missing operand")]
44 MissingOperand,
45
46 #[error("invalid variable name")]
48 InvalidVariableName { name: Field },
49}
50
51impl Error {
52 #[must_use]
54 pub fn to_report(&self) -> Report<'_> {
55 let snippets = match self {
56 Self::CommonError(parse_error) => return parse_error.to_report(),
57
58 Self::MultibyteDelimiter { delimiter } => Snippet::with_primary_span(
59 &delimiter.origin,
60 format!(
61 "delimiter {:?} is {}-byte long",
62 delimiter.value,
63 delimiter.value.len()
64 )
65 .into(),
66 ),
67
68 Self::MissingOperand => vec![],
69
70 Self::InvalidVariableName { name } => Snippet::with_primary_span(
71 &name.origin,
72 format!("variable name {name:?} is not valid").into(),
73 ),
74 };
75
76 let mut report = Report::new();
77 report.r#type = ReportType::Error;
78 report.title = self.to_string().into();
79 report.snippets = snippets;
80 report
81 }
82}
83
84impl<'a> From<&'a Error> for Report<'a> {
85 #[inline]
86 fn from(error: &'a Error) -> Self {
87 error.to_report()
88 }
89}
90
91const OPTION_SPECS: &[OptionSpec] = &[
92 OptionSpec::new()
93 .short('d')
94 .long("delimiter")
95 .argument(OptionArgumentSpec::Required),
96 OptionSpec::new().short('r').long("raw-mode"),
97];
98
99pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result<Command, Error> {
101 let mode = Mode::with_env(env);
102 let (options, operands) = parse_arguments(OPTION_SPECS, mode, args)?;
103
104 let mut delimiter = b'\n';
106 let mut is_raw = false;
107 for option in options {
108 match option.spec.get_short() {
109 Some('d') => {
110 let arg = option.argument.unwrap();
111 match arg.value.len() {
112 0 => delimiter = b'\0',
113 1 => delimiter = arg.value.as_bytes()[0],
114 _ => return Err(Error::MultibyteDelimiter { delimiter: arg }),
115 }
116 }
117 Some('r') => is_raw = true,
118 _ => unreachable!(),
119 }
120 }
121
122 let mut variables = validate_names(operands)?;
124 let last_variable = variables.pop().ok_or(Error::MissingOperand)?;
125
126 Ok(Command {
127 delimiter,
128 is_raw,
129 variables,
130 last_variable,
131 })
132}
133
134fn validate_names(names: Vec<Field>) -> Result<Vec<Field>, Error> {
139 match names.iter().position(|name| name.value.contains('=')) {
140 None => Ok(names),
141 Some(i) => Err(Error::InvalidVariableName {
142 name: { names }.swap_remove(i),
143 }),
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn one_operand() {
153 let env = Env::new_virtual();
154 assert_eq!(
155 parse(&env, Field::dummies(["var"])),
156 Ok(Command {
157 delimiter: b'\n',
158 is_raw: false,
159 variables: vec![],
160 last_variable: Field::dummy("var"),
161 })
162 );
163 }
164
165 #[test]
166 fn raw_mode() {
167 let env = Env::new_virtual();
168 assert_eq!(
169 parse(&env, Field::dummies(["-r", "var"])),
170 Ok(Command {
171 delimiter: b'\n',
172 is_raw: true,
173 variables: vec![],
174 last_variable: Field::dummy("var"),
175 })
176 );
177 }
178
179 #[test]
180 fn nul_delimiter() {
181 let env = Env::new_virtual();
182 assert_eq!(
183 parse(&env, Field::dummies(["-d", "", "var"])),
184 Ok(Command {
185 delimiter: b'\0',
186 is_raw: false,
187 variables: vec![],
188 last_variable: Field::dummy("var"),
189 })
190 );
191 }
192
193 #[test]
194 fn non_default_non_nul_delimiter() {
195 let env = Env::new_virtual();
196 assert_eq!(
197 parse(&env, Field::dummies(["-d", ":", "var"])),
198 Ok(Command {
199 delimiter: b':',
200 is_raw: false,
201 variables: vec![],
202 last_variable: Field::dummy("var"),
203 })
204 );
205 }
206
207 #[test]
208 fn multibyte_delimiter_is_not_supported() {
209 let env = Env::new_virtual();
210 assert_eq!(
211 parse(&env, Field::dummies(["-d", "!?", "var"])),
212 Err(Error::MultibyteDelimiter {
213 delimiter: Field::dummy("!?")
214 })
215 );
216
217 assert_eq!(
218 parse(&env, Field::dummies(["-d", "あ", "var"])),
219 Err(Error::MultibyteDelimiter {
220 delimiter: Field::dummy("あ")
221 })
222 );
223 }
224
225 #[test]
226 fn many_operands() {
227 let env = Env::new_virtual();
228 assert_eq!(
229 parse(&env, Field::dummies(["foo", "bar"])),
230 Ok(Command {
231 delimiter: b'\n',
232 is_raw: false,
233 variables: Field::dummies(["foo"]),
234 last_variable: Field::dummy("bar"),
235 })
236 );
237
238 assert_eq!(
239 parse(&env, Field::dummies(["first", "second", "third"])),
240 Ok(Command {
241 delimiter: b'\n',
242 is_raw: false,
243 variables: Field::dummies(["first", "second"]),
244 last_variable: Field::dummy("third"),
245 })
246 );
247 }
248
249 #[test]
250 fn missing_operand() {
251 let env = Env::new_virtual();
252 assert_eq!(parse(&env, vec![]), Err(Error::MissingOperand));
253 }
254
255 #[test]
256 fn operand_containing_equal() {
257 let env = Env::new_virtual();
258 assert_eq!(
259 parse(&env, Field::dummies(["="])),
260 Err(Error::InvalidVariableName {
261 name: Field::dummy("=")
262 })
263 );
264 assert_eq!(
265 parse(&env, Field::dummies(["foo", "bar=bar", "baz"])),
266 Err(Error::InvalidVariableName {
267 name: Field::dummy("bar=bar")
268 })
269 );
270 }
271}