1use super::*;
2
3#[derive(Debug)]
4pub enum Error<'src> {
5 AmbiguousModuleFile {
6 module: Name<'src>,
7 found: Vec<PathBuf>,
8 },
9 ArgumentCountMismatch {
10 recipe: &'src str,
11 parameters: Vec<Parameter<'src>>,
12 found: usize,
13 min: usize,
14 max: usize,
15 },
16 Assert {
17 message: String,
18 },
19 Backtick {
20 token: Token<'src>,
21 output_error: OutputError,
22 },
23 RuntimeDirIo {
24 io_error: io::Error,
25 path: PathBuf,
26 },
27 ChooserInvoke {
28 shell_binary: String,
29 shell_arguments: String,
30 chooser: OsString,
31 io_error: io::Error,
32 },
33 ChooserRead {
34 chooser: OsString,
35 io_error: io::Error,
36 },
37 ChooserStatus {
38 chooser: OsString,
39 status: ExitStatus,
40 },
41 ChooserWrite {
42 chooser: OsString,
43 io_error: io::Error,
44 },
45 CircularImport {
46 current: PathBuf,
47 import: PathBuf,
48 },
49 Code {
50 recipe: &'src str,
51 line_number: Option<usize>,
52 code: i32,
53 print_message: bool,
54 },
55 CommandInvoke {
56 binary: OsString,
57 arguments: Vec<OsString>,
58 io_error: io::Error,
59 },
60 CommandStatus {
61 binary: OsString,
62 arguments: Vec<OsString>,
63 status: ExitStatus,
64 },
65 Compile {
66 compile_error: CompileError<'src>,
67 },
68 Config {
69 config_error: ConfigError,
70 },
71 Cygpath {
72 recipe: &'src str,
73 output_error: OutputError,
74 },
75 DefaultRecipeRequiresArguments {
76 recipe: &'src str,
77 min_arguments: usize,
78 },
79 Dotenv {
80 dotenv_error: dotenvy::Error,
81 },
82 DotenvRequired,
83 DumpJson {
84 serde_json_error: serde_json::Error,
85 },
86 EditorInvoke {
87 editor: OsString,
88 io_error: io::Error,
89 },
90 EditorStatus {
91 editor: OsString,
92 status: ExitStatus,
93 },
94 EvalUnknownVariable {
95 variable: String,
96 suggestion: Option<Suggestion<'src>>,
97 },
98 ExcessInvocations {
99 invocations: usize,
100 },
101 ExpectedSubmoduleButFoundRecipe {
102 path: String,
103 },
104 FormatCheckFoundDiff,
105 FunctionCall {
106 function: Name<'src>,
107 message: String,
108 },
109 GetConfirmation {
110 io_error: io::Error,
111 },
112 Homedir,
113 InitExists {
114 justfile: PathBuf,
115 },
116 Internal {
117 message: String,
118 },
119 Io {
120 recipe: &'src str,
121 io_error: io::Error,
122 },
123 Load {
124 path: PathBuf,
125 io_error: io::Error,
126 },
127 MissingImportFile {
128 path: Token<'src>,
129 },
130 MissingModuleFile {
131 module: Name<'src>,
132 },
133 NoChoosableRecipes,
134 NoDefaultRecipe,
135 NoRecipes,
136 NotConfirmed {
137 recipe: &'src str,
138 },
139 RegexCompile {
140 source: regex::Error,
141 },
142 Script {
143 command: String,
144 io_error: io::Error,
145 recipe: &'src str,
146 },
147 Search {
148 search_error: SearchError,
149 },
150 Shebang {
151 argument: Option<String>,
152 command: String,
153 io_error: io::Error,
154 recipe: &'src str,
155 },
156 Signal {
157 recipe: &'src str,
158 line_number: Option<usize>,
159 signal: i32,
160 },
161 StdoutIo {
162 io_error: io::Error,
163 },
164 TempdirIo {
165 recipe: &'src str,
166 io_error: io::Error,
167 },
168 TempfileIo {
169 io_error: io::Error,
170 },
171 Unknown {
172 recipe: &'src str,
173 line_number: Option<usize>,
174 },
175 UnknownSubmodule {
176 path: String,
177 },
178 UnknownOverrides {
179 overrides: Vec<String>,
180 },
181 UnknownRecipe {
182 recipe: String,
183 suggestion: Option<Suggestion<'src>>,
184 },
185 UnstableFeature {
186 unstable_feature: UnstableFeature,
187 },
188 WriteJustfile {
189 justfile: PathBuf,
190 io_error: io::Error,
191 },
192}
193
194impl<'src> Error<'src> {
195 pub fn code(&self) -> Option<i32> {
196 match self {
197 Self::Code { code, .. }
198 | Self::Backtick {
199 output_error: OutputError::Code(code),
200 ..
201 } => Some(*code),
202 Self::ChooserStatus { status, .. } | Self::EditorStatus { status, .. } => status.code(),
203 _ => None,
204 }
205 }
206
207 fn context(&self) -> Option<Token<'src>> {
208 match self {
209 Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => {
210 Some(module.token)
211 }
212 Self::Backtick { token, .. } => Some(*token),
213 Self::Compile { compile_error } => Some(compile_error.context()),
214 Self::FunctionCall { function, .. } => Some(function.token),
215 Self::MissingImportFile { path } => Some(*path),
216 _ => None,
217 }
218 }
219
220 pub fn internal(message: impl Into<String>) -> Self {
221 Self::Internal {
222 message: message.into(),
223 }
224 }
225
226 pub fn print_message(&self) -> bool {
227 !matches!(
228 self,
229 Error::Code {
230 print_message: false,
231 ..
232 }
233 )
234 }
235}
236
237impl<'src> From<CompileError<'src>> for Error<'src> {
238 fn from(compile_error: CompileError<'src>) -> Self {
239 Self::Compile { compile_error }
240 }
241}
242
243impl<'src> From<ConfigError> for Error<'src> {
244 fn from(config_error: ConfigError) -> Self {
245 Self::Config { config_error }
246 }
247}
248
249impl<'src> From<dotenvy::Error> for Error<'src> {
250 fn from(dotenv_error: dotenvy::Error) -> Error<'src> {
251 Self::Dotenv { dotenv_error }
252 }
253}
254
255impl<'src> From<SearchError> for Error<'src> {
256 fn from(search_error: SearchError) -> Self {
257 Self::Search { search_error }
258 }
259}
260
261impl<'src> ColorDisplay for Error<'src> {
262 fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
263 use Error::*;
264
265 let error = color.error().paint("error");
266 let message = color.message().prefix();
267 write!(f, "{error}: {message}")?;
268
269 match self {
270 AmbiguousModuleFile { module, found } =>
271 write!(f,
272 "Found multiple source files for module `{module}`: {}",
273 List::and_ticked(found.iter().map(|path| path.display())),
274 )?,
275 ArgumentCountMismatch { recipe, found, min, max, .. } => {
276 let count = Count("argument", *found);
277 if min == max {
278 let expected = min;
279 let only = if expected < found { "only " } else { "" };
280 write!(f, "Recipe `{recipe}` got {found} {count} but {only}takes {expected}")?;
281 } else if found < min {
282 write!(f, "Recipe `{recipe}` got {found} {count} but takes at least {min}")?;
283 } else if found > max {
284 write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?;
285 }
286 }
287 Assert { message }=> {
288 write!(f, "Assert failed: {message}")?;
289 }
290 Backtick { output_error, .. } => match output_error {
291 OutputError::Code(code) => write!(f, "Backtick failed with exit code {code}")?,
292 OutputError::Signal(signal) => write!(f, "Backtick was terminated by signal {signal}")?,
293 OutputError::Unknown => write!(f, "Backtick failed for an unknown reason")?,
294 OutputError::Io(io_error) => match io_error.kind() {
295 io::ErrorKind::NotFound => write!(f, "Backtick could not be run because just could not find the shell:\n{io_error}"),
296 io::ErrorKind::PermissionDenied => write!(f, "Backtick could not be run because just could not run the shell:\n{io_error}"),
297 _ => write!(f, "Backtick could not be run because of an IO error while launching the shell:\n{io_error}"),
298 }?,
299 OutputError::Utf8(utf8_error) => write!(f, "Backtick succeeded but stdout was not utf8: {utf8_error}")?,
300 }
301 ChooserInvoke { shell_binary, shell_arguments, chooser, io_error} => {
302 let chooser = chooser.to_string_lossy();
303 write!(f, "Chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}")?;
304 }
305 ChooserRead { chooser, io_error } => {
306 let chooser = chooser.to_string_lossy();
307 write!(f, "Failed to read output from chooser `{chooser}`: {io_error}")?;
308 }
309 ChooserStatus { chooser, status } => {
310 let chooser = chooser.to_string_lossy();
311 write!(f, "Chooser `{chooser}` failed: {status}")?;
312 }
313 ChooserWrite { chooser, io_error } => {
314 let chooser = chooser.to_string_lossy();
315 write!(f, "Failed to write to chooser `{chooser}`: {io_error}")?;
316 }
317 CircularImport { current, import } => {
318 let import = import.display();
319 let current = current.display();
320 write!(f, "Import `{import}` in `{current}` is circular")?;
321 }
322 Code { recipe, line_number, code, .. } => {
323 if let Some(n) = line_number {
324 write!(f, "Recipe `{recipe}` failed on line {n} with exit code {code}")?;
325 } else {
326 write!(f, "Recipe `{recipe}` failed with exit code {code}")?;
327 }
328 }
329 CommandInvoke { binary, arguments, io_error } => {
330 let cmd = format_cmd(binary, arguments);
331 write!(f, "Failed to invoke {cmd}: {io_error}")?;
332 }
333 CommandStatus { binary, arguments, status} => {
334 let cmd = format_cmd(binary, arguments);
335 write!(f, "Command {cmd} failed: {status}")?;
336 }
337 Compile { compile_error } => Display::fmt(compile_error, f)?,
338 Config { config_error } => Display::fmt(config_error, f)?,
339 Cygpath { recipe, output_error} => match output_error {
340 OutputError::Code(code) => write!(f, "Cygpath failed with exit code {code} while translating recipe `{recipe}` shebang interpreter path")?,
341 OutputError::Signal(signal) => write!(f, "Cygpath terminated by signal {signal} while translating recipe `{recipe}` shebang interpreter path")?,
342 OutputError::Unknown => write!(f, "Cygpath experienced an unknown failure while translating recipe `{recipe}` shebang interpreter path")?,
343 OutputError::Io(io_error) => {
344 match io_error.kind() {
345 io::ErrorKind::NotFound => write!(f, "Could not find `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}"),
346 io::ErrorKind::PermissionDenied => write!(f, "Could not run `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}"),
347 _ => write!(f, "Could not run `cygpath` executable:\n{io_error}"),
348 }?;
349 }
350 OutputError::Utf8(utf8_error) => write!(f, "Cygpath successfully translated recipe `{recipe}` shebang interpreter path, but output was not utf8: {utf8_error}")?,
351 }
352 DefaultRecipeRequiresArguments { recipe, min_arguments} => {
353 let count = Count("argument", *min_arguments);
354 write!(f, "Recipe `{recipe}` cannot be used as default recipe since it requires at least {min_arguments} {count}.")?;
355 }
356 Dotenv { dotenv_error } => {
357 write!(f, "Failed to load environment file: {dotenv_error}")?;
358 }
359 DotenvRequired => {
360 write!(f, "Dotenv file not found")?;
361 }
362 DumpJson { serde_json_error } => {
363 write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?;
364 }
365 EditorInvoke { editor, io_error } => {
366 let editor = editor.to_string_lossy();
367 write!(f, "Editor `{editor}` invocation failed: {io_error}")?;
368 }
369 EditorStatus { editor, status } => {
370 let editor = editor.to_string_lossy();
371 write!(f, "Editor `{editor}` failed: {status}")?;
372 }
373 EvalUnknownVariable { variable, suggestion} => {
374 write!(f, "Justfile does not contain variable `{variable}`.")?;
375 if let Some(suggestion) = suggestion {
376 write!(f, "\n{suggestion}")?;
377 }
378 }
379 ExcessInvocations { invocations } => {
380 write!(f, "Expected 1 command-line recipe invocation but found {invocations}.")?;
381 },
382 ExpectedSubmoduleButFoundRecipe { path } => {
383 write!(f, "Expected submodule at `{path}` but found recipe.")?;
384 },
385 FormatCheckFoundDiff => {
386 write!(f, "Formatted justfile differs from original.")?;
387 }
388 FunctionCall { function, message } => {
389 let function = function.lexeme();
390 write!(f, "Call to function `{function}` failed: {message}")?;
391 }
392 GetConfirmation { io_error } => {
393 write!(f, "Failed to read confirmation from stdin: {io_error}")?;
394 }
395 Homedir => {
396 write!(f, "Failed to get homedir")?;
397 }
398 InitExists { justfile } => {
399 write!(f, "Justfile `{}` already exists", justfile.display())?;
400 }
401 Internal { message } => {
402 write!(f, "Internal runtime error, this may indicate a bug in just: {message} \
403 consider filing an issue: https://github.com/casey/just/issues/new")?;
404 }
405 Io { recipe, io_error } => {
406 match io_error.kind() {
407 io::ErrorKind::NotFound => write!(f, "Recipe `{recipe}` could not be run because just could not find the shell: {io_error}"),
408 io::ErrorKind::PermissionDenied => write!(f, "Recipe `{recipe}` could not be run because just could not run the shell: {io_error}"),
409 _ => write!(f, "Recipe `{recipe}` could not be run because of an IO error while launching the shell: {io_error}"),
410 }?;
411 }
412 Load { io_error, path } => {
413 write!(f, "Failed to read justfile at `{}`: {io_error}", path.display())?;
414 }
415 MissingImportFile { .. } => write!(f, "Could not find source file for import.")?,
416 MissingModuleFile { module } => write!(f, "Could not find source file for module `{module}`.")?,
417 NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
418 NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?,
419 NoRecipes => write!(f, "Justfile contains no recipes.")?,
420 NotConfirmed { recipe } => {
421 write!(f, "Recipe `{recipe}` was not confirmed")?;
422 }
423 RegexCompile { source } => write!(f, "{source}")?,
424 RuntimeDirIo { io_error, path } => {
425 write!(f, "I/O error in runtime dir `{}`: {io_error}", path.display())?;
426 }
427 Script { command, io_error, recipe } => {
428 write!(f, "Recipe `{recipe}` with command `{command}` execution error: {io_error}")?;
429 }
430 Search { search_error } => Display::fmt(search_error, f)?,
431 Shebang { recipe, command, argument, io_error} => {
432 if let Some(argument) = argument {
433 write!(f, "Recipe `{recipe}` with shebang `#!{command} {argument}` execution error: {io_error}")?;
434 } else {
435 write!(f, "Recipe `{recipe}` with shebang `#!{command}` execution error: {io_error}")?;
436 }
437 }
438 Signal { recipe, line_number, signal } => {
439 if let Some(n) = line_number {
440 write!(f, "Recipe `{recipe}` was terminated on line {n} by signal {signal}")?;
441 } else {
442 write!(f, "Recipe `{recipe}` was terminated by signal {signal}")?;
443 }
444 }
445 StdoutIo { io_error } => {
446 write!(f, "I/O error writing to stdout: {io_error}?")?;
447 }
448 TempdirIo { recipe, io_error } => {
449 write!(f, "Recipe `{recipe}` could not be run because of an IO error while trying to create a temporary \
450 directory or write a file to that directory: {io_error}")?;
451 }
452 TempfileIo { io_error } => {
453 write!(f, "Tempfile I/O error: {io_error}")?;
454 }
455 Unknown { recipe, line_number} => {
456 if let Some(n) = line_number {
457 write!(f, "Recipe `{recipe}` failed on line {n} for an unknown reason")?;
458 } else {
459 write!(f, "Recipe `{recipe}` failed for an unknown reason")?;
460 }
461 }
462 UnknownSubmodule { path } => {
463 write!(f, "Justfile does not contain submodule `{path}`")?;
464 }
465 UnknownOverrides { overrides } => {
466 let count = Count("Variable", overrides.len());
467 let overrides = List::and_ticked(overrides);
468 write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
469 }
470 UnknownRecipe { recipe, suggestion } => {
471 write!(f, "Justfile does not contain recipe `{recipe}`.")?;
472 if let Some(suggestion) = suggestion {
473 write!(f, "\n{suggestion}")?;
474 }
475 }
476 UnstableFeature { unstable_feature } => {
477 write!(f, "{unstable_feature} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
478 }
479 WriteJustfile { justfile, io_error } => {
480 let justfile = justfile.display();
481 write!(f, "Failed to write justfile to `{justfile}`: {io_error}")?;
482 }
483 }
484
485 write!(f, "{}", color.message().suffix())?;
486
487 if let ArgumentCountMismatch {
488 recipe, parameters, ..
489 } = self
490 {
491 writeln!(f)?;
492 write!(f, "{}:\n just {recipe}", color.message().paint("usage"))?;
493 for param in parameters {
494 write!(f, " {}", param.color_display(color))?;
495 }
496 }
497
498 if let Some(token) = self.context() {
499 writeln!(f)?;
500 write!(f, "{}", token.color_display(color.error()))?;
501 }
502
503 Ok(())
504 }
505}
506
507fn format_cmd(binary: &OsString, arguments: &Vec<OsString>) -> String {
508 iter::once(binary)
509 .chain(arguments)
510 .map(|value| Enclosure::tick(value.to_string_lossy()).to_string())
511 .collect::<Vec<String>>()
512 .join(" ")
513}