yash_builtin/command/
syntax.rs1use super::Command;
20use super::Identify;
21use super::Invoke;
22use super::Search;
23use crate::common::syntax::Mode;
24use crate::common::syntax::OptionOccurrence;
25use crate::common::syntax::OptionSpec;
26use crate::common::syntax::ParseError;
27use crate::common::syntax::parse_arguments;
28use thiserror::Error;
29use yash_env::Env;
30use yash_env::semantics::Field;
31use yash_env::source::pretty::Report;
32
33#[derive(Clone, Debug, Eq, Error, PartialEq)]
35#[non_exhaustive]
36pub enum Error {
37 #[error(transparent)]
39 CommonError(#[from] ParseError<'static>),
40 }
44
45impl Error {
46 #[must_use]
48 pub fn to_report(&self) -> Report<'_> {
49 match self {
50 Self::CommonError(e) => e.to_report(),
51 }
52 }
53}
54
55impl<'a> From<&'a Error> for Report<'a> {
56 #[inline]
57 fn from(error: &'a Error) -> Self {
58 error.to_report()
59 }
60}
61
62const OPTION_SPECS: &[OptionSpec] = &[
63 OptionSpec::new().short('p').long("path"),
64 OptionSpec::new().short('v').long("identify"),
65 OptionSpec::new().short('V').long("verbose-identify"),
66];
67
68pub fn interpret(
72 options: Vec<OptionOccurrence<'_>>,
73 operands: Vec<Field>,
74) -> Result<Command, Error> {
75 let mut standard_path = false;
77 let mut verbose_identify = None;
78 for option in options {
79 match option.spec.get_short() {
80 Some('p') => standard_path = true,
81 Some('v') => verbose_identify = Some(false),
82 Some('V') => verbose_identify = Some(true),
83 _ => unreachable!("unhandled option: {:?}", option),
84 }
85 }
86
87 if let Some(verbose) = verbose_identify {
89 let mut search = Search::default_for_identify();
90 search.standard_path = standard_path;
91 let identify = Identify {
92 names: operands,
93 search,
94 verbose,
95 };
96 Ok(identify.into())
97 } else {
98 let mut search = Search::default_for_invoke();
99 search.standard_path = standard_path;
100 let fields = operands;
101 let invoke = Invoke { fields, search };
102 Ok(invoke.into())
103 }
104}
105
106pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result<Command, Error> {
108 let (options, operands) = parse_arguments(OPTION_SPECS, Mode::with_env(env), args)?;
109 interpret(options, operands)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::command::Category;
116 use assert_matches::assert_matches;
117 use enumset::EnumSet;
118
119 #[test]
120 fn invoke_without_options() {
121 let env = Env::new_virtual();
122 let result = parse(&env, Field::dummies(["foo", "bar", "baz"]));
123
124 assert_matches!(result, Ok(Command::Invoke(invoke)) => {
125 assert_eq!(invoke.fields, Field::dummies(["foo", "bar", "baz"]));
126 assert_eq!(
127 invoke.search,
128 Search {
129 standard_path: false,
130 categories: Category::Builtin | Category::ExternalUtility
131 }
132 );
133 });
134 }
135
136 #[test]
137 fn invoke_with_p_option() {
138 let env = Env::new_virtual();
139 let result = parse(&env, Field::dummies(["-p", "foo"]));
140
141 assert_matches!(result, Ok(Command::Invoke(invoke)) => {
142 assert_eq!(invoke.fields, Field::dummies(["foo"]));
143 assert_eq!(
144 invoke.search,
145 Search {
146 standard_path: true,
147 categories: Category::Builtin | Category::ExternalUtility
148 }
149 );
150 });
151 }
152
153 #[test]
154 fn identify_without_options() {
155 let env = Env::new_virtual();
156 let result = parse(&env, Field::dummies(["-v", "foo"]));
157
158 assert_matches!(result, Ok(Command::Identify(identify)) => {
159 assert_eq!(identify.names, Field::dummies(["foo"]));
160 assert_eq!(
161 identify.search,
162 Search {
163 standard_path: false,
164 categories: EnumSet::all()
165 }
166 );
167 assert!(!identify.verbose);
168 });
169 }
170
171 #[test]
172 fn identify_with_p_option() {
173 let env = Env::new_virtual();
174 let result = parse(&env, Field::dummies(["-v", "-p", "foo"]));
175
176 assert_matches!(result, Ok(Command::Identify(identify)) => {
177 assert_eq!(identify.names, Field::dummies(["foo"]));
178 assert_eq!(
179 identify.search,
180 Search {
181 standard_path: true,
182 categories: EnumSet::all()
183 }
184 );
185 assert!(!identify.verbose);
186 });
187 }
188
189 #[test]
190 fn verbosely_identify_without_options() {
191 let env = Env::new_virtual();
192 let result = parse(&env, Field::dummies(["-V", "bar"]));
193
194 assert_matches!(result, Ok(Command::Identify(identify)) => {
195 assert_eq!(identify.names, Field::dummies(["bar"]));
196 assert_eq!(
197 identify.search,
198 Search {
199 standard_path: false,
200 categories: EnumSet::all()
201 }
202 );
203 assert!(identify.verbose);
204 });
205 }
206
207 #[test]
210 #[allow(non_snake_case)]
211 fn last_specified_option_wins_between_v_and_V() {
212 let env = Env::new_virtual();
213
214 let result = parse(&env, Field::dummies(["-V", "-v", "baz"]));
215 assert_matches!(result, Ok(Command::Identify(identify)) => {
216 assert!(!identify.verbose);
217 });
218
219 let result = parse(&env, Field::dummies(["-v", "-V", "baz"]));
220 assert_matches!(result, Ok(Command::Identify(identify)) => {
221 assert!(identify.verbose);
222 });
223 }
224}