flxzt/rnote · error
Failed to get file stem
Error message
Failed to get file stem
What it means
During the `Suffix` conflict-resolution path, `file_conflict_prompt_action` (crates/rnote-cli/src/export.rs:516) calls `Path::file_stem()` on the conflicting output path to generate `<stem>_1.<ext>` style names. If the path yields no file stem — it is a root (`/`), empty, or ends in `..`-like components so it has no ordinary final component — the `let ... else` returns this error and aborts the export.
Solutions
- Pass a real filename with an extension as `--output`, e.g. `export.note.svg` instead of `/` or `.`.
- Check the variable feeding `--output` is non-empty before invoking the CLI (`[ -n "$OUT" ] || exit 1`).
- Verify the path does not end in `..` or `/`; canonicalize with `realpath`/`path.canonicalize()` and confirm it points to a file-like name.
- Library fix: validate the output path has a file stem up front and emit an error naming the offending path instead of the bare "Failed to get file stem".
Example fix
// before
let output = std::env::var("OUT").unwrap_or_default(); // may be empty
cli_export(&output, OnConflict::Suffix)?;
// after
let output = std::env::var("OUT")?;
let out_path = std::path::Path::new(&output);
if out_path.file_stem().is_none() {
anyhow::bail!("--output must include a file name, got: {output:?}");
}
cli_export(out_path, OnConflict::Suffix)?; Defensive patterns
Strategy: validation
Validate before calling
// Before calling the CLI/export API with --on-conflict suffix:
fn output_path_is_valid(p: &Path) -> bool {
p.file_stem().is_some() && !p.as_os_str().is_empty()
}
// usage
if !output_path_is_valid(&output_file) {
anyhow::bail!("output path must name a file, got {:?}", output_file);
} Type guard
fn has_file_stem(p: &std::path::Path) -> Option<std::string::String> {
p.file_stem().map(|s| s.to_string_lossy().into_owned())
} Try / catch
match export_to_file(...).await {
Err(e) if e.to_string() == "Failed to get file stem" => {
anyhow::bail!("--output must be a file path, not a directory/root/empty value");
}
other => other,
} Prevention
- Always pass a filename with an extension to --output.
- Quote and default-check shell variables used for --output.
- Reject paths ending in '/' or '..' before invoking the export.
- Improve the library error to include the offending path for diagnosability.
When it happens
Trigger: Passing an output path with no normal final component to the export commands: `--output /`, `--output .`, `--output ""`, a path terminating in `..` (e.g. `dir/..`), or a bare directory where a filename was expected, combined with `--on-conflict suffix`/`always-suffix` and the file already existing.
Common situations: Shell variables that expand to empty string (`--output "$OUT"` with OUT unset); building paths with `PathBuf::from("..")` concatenation; typos like `--output .`; Windows/Unix root paths used as a target file.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- Failed to init audioplayer. file
- Failed to get file name from output-file
- Failed to get file stem from rnote file
- The filepath does not have a parent directory
- file of source path ' ' does not have a file stem.
AI-assisted analysis of flxzt/rnote@bbc5354502 (2026-09-08).
Data as JSON: /api/errors/da9ba6f4b6a0065b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/rnote-cli/src/export.rs:516
on_conflict = OnConflict::Suffix;
*on_conflict_overwrite = Some(on_conflict);
}
OnConflict::Overwrite | OnConflict::Skip | OnConflict::Suffix => (),
}
match on_conflict {
OnConflict::Ask => Err(anyhow::anyhow!(
"on-conflict behaviour is still Ask after prompting the user."
)),
OnConflict::Overwrite => Ok(None),
OnConflict::Skip => Err(anyhow::anyhow!("Skipped {}", output_file.display())),
OnConflict::Suffix => {
let mut i = 0;
let mut new_path = output_file.to_path_buf();
let Some(file_stem) = new_path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
else {
return Err(anyhow::anyhow!("Failed to get file stem"));
};
let ext = new_path
.extension()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
while new_path.exists() {
i += 1;
new_path.set_file_name(format!("{file_stem}_{i}.{ext}"))
}
Ok(Some(new_path))
}
OnConflict::AlwaysOverwrite | OnConflict::AlwaysSkip | OnConflict::AlwaysSuffix => {
Err(anyhow::anyhow!(
"on-conflict behaviour is still {on_conflict} after applying overwrite."
))
}
}
}View on GitHub (pinned to bbc5354502)