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

  1. Pass a real filename with an extension as `--output`, e.g. `export.note.svg` instead of `/` or `.`.
  2. Check the variable feeding `--output` is non-empty before invoking the CLI (`[ -n "$OUT" ] || exit 1`).
  3. Verify the path does not end in `..` or `/`; canonicalize with `realpath`/`path.canonicalize()` and confirm it points to a file-like name.
  4. 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

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


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)