CLI --export-svg Error Handling with pico-args
We resolved two issues when running turtle programs with `--export-svg`:
1. **Missing filename**: Previously, running `--export-svg` without a
filename caused the CLI parser to ignore the flag and open the GUI
window. Now, it emits `Error: --export-svg: option requires an
argument` and terminates with exit code 1.
2. **Missing `svg` feature**: Previously, running `--export-svg` without
the `svg` feature enabled ignored the flag if no filename was passed
and opened the window. Now, any invocation of `--export-svg` without
`--features svg` emits `Error: SVG export feature is not enabled.
Please rebuild with --features svg` and terminates with exit code 1.
---
- Added [`pico-args = { version = "0.5", features = ["eq-separator"]
}`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/Cargo.toml)
under `[dependencies]`.
- In
[`turtle-lib/src/export.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export.rs):
- Added `MissingFeature(String)` to `ExportError` for clean error
display.
- Implemented `parse_svg_export_from_args(args: &mut
pico_args::Arguments) -> Result<Option<String>, pico_args::Error>`
to parse `--export-svg <file>` and `--export-svg=<file>`, treating
empty or flag arguments as missing values (`OptionWithoutAValue`).
- Updated `parse_svg_export_arg() -> Option<String>` for backward
compatibility.
- In `handle_svg_export`:
- Under `#[cfg(not(feature = "svg"))]`: detects any presence of
`--export-svg`, outputs `Error: SVG export feature is not enabled.
Please rebuild with --features svg`, and exits with code 1.
- Under `#[cfg(feature = "svg")]`: handles missing arguments with
`Error: --export-svg: option requires an argument` (exit 1), valid
exports (exit 0), and leaves unknown arguments untouched so
Macroquad or user flags continue to work.
- Added unit tests covering all flag parsing variants and error
displays.
- In
[`turtle-lib-macros/src/lib.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib-macros/src/lib.rs):
- Simplified the macro expansion in `turtle_main` to delegate directly
to `turtle_lib::export::handle_svg_export(&mut build_commands);`.
- Updated documentation comments to match.
---
```bash
cargo test --package turtle-lib
cargo test --package turtle-lib --features svg
cargo test --package turtle-lib-macros
cargo clippy --package turtle-lib -- -Wclippy::pedantic
-Aclippy::cast_precision_loss -Aclippy::cast_sign_loss
-Aclippy::cast_possible_truncation
cargo clippy --package turtle-lib --features svg -- -Wclippy::pedantic
-Aclippy::cast_precision_loss -Aclippy::cast_sign_loss
-Aclippy::cast_possible_truncation
cargo clippy --package turtle-lib-macros -- -Wclippy::pedantic
```
- **Unit tests**: All 29 unit tests in `turtle-lib` and 18 unit tests in
`turtle-lib-macros` passed.
- **Doctests**: All 34 doctests passed.
- **Clippy**: Passed with zero warnings across all crates and feature
configurations.
| Scenario | Command | Result | Exit Code | Window Opened? |
|---|---|---|---|---|
| Missing feature, missing filename | `cargo run --example hello_turtle
-- --export-svg` | `Error: SVG export feature is not enabled. Please
rebuild with --features svg` | 1 | No |
| Missing feature, with filename | `cargo run --example hello_turtle --
--export-svg test.svg` | `Error: SVG export feature is not enabled.
Please rebuild with --features svg` | 1 | No |
| Feature enabled, missing filename | `cargo run --example hello_turtle
--features svg -- --export-svg` | `Error: --export-svg: option requires
an argument` | 1 | No |
| Feature enabled, with filename | `cargo run --example hello_turtle
--features svg -- --export-svg /tmp/pico_test.svg` | `SVG exported
successfully to: /tmp/pico_test.svg` | 0 | No |
| Feature enabled, equals syntax | `cargo run --example hello_turtle
--features svg -- --export-svg=/tmp/pico_test_eq.svg` | `SVG exported
successfully to: /tmp/pico_test_eq.svg` | 0 | No |
| Feature enabled, extra flags | `cargo run --example hello_turtle
--features svg -- --export-svg /tmp/pico_test_extra.svg --custom-flag` |
`SVG exported successfully to: /tmp/pico_test_extra.svg` | 0 | No |
| Normal run (no flag) | `cargo run --example hello_turtle` |
Interactive window opens normally | 0 | Yes |
This commit is contained in:
@@ -66,17 +66,11 @@ use syn::ItemFn;
|
|||||||
/// use turtle_lib::*;
|
/// use turtle_lib::*;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
/// // Handle optional SVG export headlessly without opening a window
|
|
||||||
/// if let Some(filename) = turtle_lib::export::parse_svg_export_arg() {
|
|
||||||
/// let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| {
|
/// let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| {
|
||||||
/// my_drawing(turtle);
|
/// my_drawing(turtle);
|
||||||
/// };
|
/// };
|
||||||
/// if let Err(e) = turtle_lib::export::run_headless_svg_export(&mut build_commands, &filename) {
|
/// // Handle optional SVG export headlessly without opening a window
|
||||||
/// eprintln!("Error exporting SVG: {:?}", e);
|
/// turtle_lib::export::handle_svg_export(&mut build_commands);
|
||||||
/// std::process::exit(1);
|
|
||||||
/// }
|
|
||||||
/// return;
|
|
||||||
/// }
|
|
||||||
///
|
///
|
||||||
/// // Normal interactive GUI mode with window
|
/// // Normal interactive GUI mode with window
|
||||||
/// turtle_lib::macroquad::Window::new("My Turtle Drawing", async {
|
/// turtle_lib::macroquad::Window::new("My Turtle Drawing", async {
|
||||||
@@ -252,18 +246,7 @@ fn turtle_main_impl(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If --export-svg flag is present, export headlessly without opening a window
|
// If --export-svg flag is present, export headlessly without opening a window
|
||||||
if let Some(filename) = turtle_lib::export::parse_svg_export_arg() {
|
turtle_lib::export::handle_svg_export(&mut build_commands);
|
||||||
match turtle_lib::export::run_headless_svg_export(&mut build_commands, &filename) {
|
|
||||||
Ok(()) => {
|
|
||||||
println!("SVG exported successfully to: {}", filename);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error exporting SVG: {}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal rendering mode (interactive window)
|
// Normal rendering mode (interactive window)
|
||||||
turtle_lib::macroquad::Window::new(#window_title, async {
|
turtle_lib::macroquad::Window::new(#window_title, async {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ tracing = { version = "0.1", features = [
|
|||||||
], default-features = false }
|
], default-features = false }
|
||||||
turtle-lib-macros = { path = "../turtle-lib-macros" }
|
turtle-lib-macros = { path = "../turtle-lib-macros" }
|
||||||
crossbeam = "0.8"
|
crossbeam = "0.8"
|
||||||
|
pico-args = { version = "0.5", features = ["eq-separator"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# For examples and testing
|
# For examples and testing
|
||||||
|
|||||||
+125
-13
@@ -8,6 +8,7 @@ pub enum ExportError {
|
|||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Format(String),
|
Format(String),
|
||||||
Execution(String),
|
Execution(String),
|
||||||
|
MissingFeature(String),
|
||||||
// Additional formats can be added here.
|
// Additional formats can be added here.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ impl std::fmt::Display for ExportError {
|
|||||||
Self::Io(err) => write!(f, "I/O error: {err}"),
|
Self::Io(err) => write!(f, "I/O error: {err}"),
|
||||||
Self::Format(msg) => write!(f, "format error: {msg}"),
|
Self::Format(msg) => write!(f, "format error: {msg}"),
|
||||||
Self::Execution(msg) => write!(f, "execution error: {msg}"),
|
Self::Execution(msg) => write!(f, "execution error: {msg}"),
|
||||||
|
Self::MissingFeature(msg) => write!(f, "{msg}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +27,7 @@ impl std::error::Error for ExportError {
|
|||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
Self::Io(err) => Some(err),
|
Self::Io(err) => Some(err),
|
||||||
Self::Format(_) | Self::Execution(_) => None,
|
Self::Format(_) | Self::Execution(_) | Self::MissingFeature(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,18 +49,27 @@ pub(crate) trait DrawingExporter {
|
|||||||
fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError>;
|
fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse the `--export-svg` parameter from a `pico_args::Arguments` instance.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns `pico_args::Error` if `--export-svg` is provided without a valid filename.
|
||||||
|
pub fn parse_svg_export_from_args(
|
||||||
|
args: &mut pico_args::Arguments,
|
||||||
|
) -> Result<Option<String>, pico_args::Error> {
|
||||||
|
match args.opt_value_from_str::<_, String>("--export-svg") {
|
||||||
|
Ok(Some(s)) if s.trim().is_empty() || s.starts_with('-') => {
|
||||||
|
Err(pico_args::Error::OptionWithoutAValue("--export-svg"))
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check command-line arguments for the `--export-svg <filename>` flag.
|
/// Check command-line arguments for the `--export-svg <filename>` flag.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn parse_svg_export_arg() -> Option<String> {
|
pub fn parse_svg_export_arg() -> Option<String> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let mut args = pico_args::Arguments::from_env();
|
||||||
let mut i = 1;
|
parse_svg_export_from_args(&mut args).ok().flatten()
|
||||||
while i < args.len() {
|
|
||||||
if args[i] == "--export-svg" && i + 1 < args.len() {
|
|
||||||
return Some(args[i + 1].clone());
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
@@ -166,7 +177,7 @@ where
|
|||||||
{
|
{
|
||||||
let _ = &mut build_commands;
|
let _ = &mut build_commands;
|
||||||
let _ = filename;
|
let _ = filename;
|
||||||
Err(ExportError::Format(
|
Err(ExportError::MissingFeature(
|
||||||
"SVG export feature is not enabled. Please rebuild with --features svg".to_string(),
|
"SVG export feature is not enabled. Please rebuild with --features svg".to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -179,16 +190,117 @@ pub fn handle_svg_export<F>(build_commands: F)
|
|||||||
where
|
where
|
||||||
F: FnMut(&mut TurtlePlan),
|
F: FnMut(&mut TurtlePlan),
|
||||||
{
|
{
|
||||||
if let Some(filename) = parse_svg_export_arg() {
|
let mut args = pico_args::Arguments::from_env();
|
||||||
|
let export_arg = parse_svg_export_from_args(&mut args);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "svg"))]
|
||||||
|
{
|
||||||
|
let _ = build_commands;
|
||||||
|
if export_arg.is_err() || matches!(export_arg, Ok(Some(_))) {
|
||||||
|
eprintln!("Error: SVG export feature is not enabled. Please rebuild with --features svg");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
{
|
||||||
|
match export_arg {
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Error: --export-svg: option requires an argument");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Ok(Some(filename)) => {
|
||||||
match run_headless_svg_export(build_commands, &filename) {
|
match run_headless_svg_export(build_commands, &filename) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
println!("SVG exported successfully to: {filename}");
|
println!("SVG exported successfully to: {filename}");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error exporting SVG: {e}");
|
eprintln!("Error: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
|
||||||
|
fn make_args(args: &[&str]) -> pico_args::Arguments {
|
||||||
|
let os_args: Vec<OsString> = args.iter().map(OsString::from).collect();
|
||||||
|
pico_args::Arguments::from_vec(os_args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_with_filename() {
|
||||||
|
let mut args = make_args(&["--export-svg", "output.svg"]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert_eq!(res.unwrap(), Some("output.svg".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_with_equals() {
|
||||||
|
let mut args = make_args(&["--export-svg=output.svg"]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert_eq!(res.unwrap(), Some("output.svg".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_missing_argument_at_end() {
|
||||||
|
let mut args = make_args(&["--export-svg"]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_missing_argument_followed_by_flag() {
|
||||||
|
let mut args = make_args(&["--export-svg", "--verbose"]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_empty_string() {
|
||||||
|
let mut args = make_args(&["--export-svg", ""]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_empty_equals() {
|
||||||
|
let mut args = make_args(&["--export-svg="]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_not_present() {
|
||||||
|
let mut args = make_args(&["--verbose", "input.txt"]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert_eq!(res.unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_svg_export_preserves_unknown_args() {
|
||||||
|
let mut args = make_args(&["--other-flag", "--export-svg", "out.svg", "positional"]);
|
||||||
|
let res = parse_svg_export_from_args(&mut args);
|
||||||
|
assert_eq!(res.unwrap(), Some("out.svg".to_string()));
|
||||||
|
assert!(args.contains("--other-flag"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_export_error_display_missing_feature() {
|
||||||
|
let err = ExportError::MissingFeature(
|
||||||
|
"SVG export feature is not enabled. Please rebuild with --features svg".to_string(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"SVG export feature is not enabled. Please rebuild with --features svg"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user