Skip to content

Feature suggestion: Optional tilde expansion support (~/path/home/user/path) #41

@DK26

Description

@DK26

Summary

Add optional shellexpand feature for automatic Unix shell-style path expansion in soft_canonicalize().

Motivation

Users frequently pass shell-style paths from configs, CLI args, and user input. Currently requires manual expansion before calling soft_canonicalize.

Proposed API

[dependencies]
soft-canonicalize = { version = "0.4", features = ["shellexpand"] }
// With feature enabled, automatic expansion:
let path = soft_canonicalize("~/Documents/$PROJECT/file.txt")?;
// → /home/user/Documents/my-project/file.txt

// Without feature: tilde treated as literal path component

Supported Expansions (Unix only)

  • ~/home/user
  • ~/path/home/user/path
  • ~user/path/home/user/path
  • $VAR, ${VAR} → environment variables
  • ~+$PWD
  • ~-$OLDPWD

Critical Design Question: Non-Existing Users

Our library works with non-existing filesystem paths. Should it work with non-existing users?

soft_canonicalize("~alice/file.txt")
// What if user "alice" doesn't exist in /etc/passwd?

Options

Option 1: Error on unknown users

  • Contradicts "works with non-existing paths" philosophy
  • Less useful for planning future multi-user systems

Option 2: Silent literal fallback ⚠️

  • ~alice./~alice/file.txt if user doesn't exist
  • Too magical, hard to debug

Option 3: Current user only 😐

  • Expand ~ but not ~alice
  • Inconsistent, feels incomplete

Option 4: Best-effort with heuristicsRecommended

  • Existing user → authoritative lookup
  • Non-existing user → platform convention (/home/alice or /Users/alice)
  • Aligns with library philosophy: path may not exist, that's OK
  • Clear documented behavior

Option 5: Configurable policy 🔧

  • Add ExpansionPolicy enum
  • Maximum flexibility, more complex API

Option 6: Don't implement 🚫

  • Users compose with shellexpand crate themselves
  • Maintains clear separation of concerns

Implementation Notes (Option 4)

fn expand_tilde_user(username: &str, rest: &str) -> io::Result<PathBuf> {
    // Try system lookup first
    if let Some(home) = lookup_user_home(username) {
        return Ok(home.join(rest));  // Authoritative
    }
    
    // Fallback: platform conventions
    #[cfg(target_os = "macos")]
    let home = PathBuf::from("/Users").join(username);
    
    #[cfg(not(target_os = "macos"))]
    let home = PathBuf::from("/home").join(username);
    
    Ok(home.join(rest))
}

Dependencies

With feature enabled:

  • shellexpand (~20KB) - may need fork/patch for best-effort behavior
    • dirs - home directory detection
    • bstr - byte string utilities

Total: ~3 small, well-maintained crates

Questions for Discussion

  1. Which option for non-existing users? (Recommendation: Option 4)
  2. Is best-effort expansion acceptable? Will /home/alice assumption work?
  3. Implementation approach: DIY, fork shellexpand, or use as-is?
  4. Environment variables: Same policy as users or stricter?
  5. Windows: Feature doesn't compile on Windows (cfg-gated)?
  6. Should this be in the library at all? Or keep as separate concern?

Feedback needed: The non-existing user dilemma is critical. Which approach makes most sense for this library's philosophy?

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions