Recognizing File Path and Error Handling

I'm writing a little console application that needs to load its configurations from a file in a neighbored directory.
Normally is not always executed from the installation folder so first I need to recognize the installation folder and from there on build the configuration folder.
So I developed this code:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=1e4434732ca1faa4097065e549562e13
I was expecting to be able to build the configuration file path as: /playground/application_default.conf
instead it was truncated to:

app cnf pth 1: '/application_default.conf'
Path of this executable is: /playground/target/debug/playground
md pth : '/playground/target/debug/playground'
md nm : 'playground'
mndir: '/playground'
wrkdir: '/playground'
cnf dir 1: '/playground'
log dir 1: '/playground'
app cnf pth 0: '/playground'
app cnf pth 1: '/application_default.conf'
config file directory: '/playground'
config file name: 'application_default.conf'

So first I want to find the correct location of the configuration file
and second I want to resolve all unwrap() calls.

My clear favorite is this one to get the name of the configuration file:

    println!(
        "config file name: '{}'",
        scnfflnm.file_name().unwrap().to_str().unwrap()
    );

I appreciate any suggestions on this topic.

When you push ./ onto the path, it doesn't do anything - just keeps it as /playground, since it's equivalent. You then change the file name from playground to application_default, and add an extension, which is why you're getting /application_default.conf. You probably want to push format!("application_{}.conf", ssvrprfx) instead.

To remove the unwrapping, you could put all the path stuff in a function that returns std::io::Result and use the question mark operator instead of unwrap, something like:

fn get_config_file_path() -> std::io::Result<PathBuf> {
    let mdpth = std::env::current_exe()?;
    let mdnm = mdpth.file_name()?;
    // ...
}

thank you for your response.
I looked again into the official documentation and could simplify the file name building sequence to this:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=94c753fd9dd79eae6a9ca697a13da0c2

    //---------------------------------
    //Load the Configuration from the File

    let mut scnfflnm = cnfdir.clone();

    if bdbg && !bqt {
        println!("app cnf pth 0: '{}'", scnfflnm.to_str().unwrap());
    }

    scnfflnm.push(format!("application_{}", ssvrprfx.as_str()));
    scnfflnm.set_extension("conf");

    if bdbg && !bqt {
        println!("app cnf pth 1: '{}'", scnfflnm.to_str().unwrap());
    }

    println!("config file directory: '{}'", cnfdir.to_str().unwrap());
    println!(
        "config file name: '{}'",
        scnfflnm.file_name().unwrap().to_str().unwrap()
    );

But still I got difficulties with this section:

    let mdpth = std::env::current_exe().unwrap();
    let mdnm = mdpth.file_name().unwrap();

    let wrkdir = mdpth.parent().unwrap();
    let mut mndir = wrkdir.to_str().unwrap();

    if let Some(ps) = mndir.find("/target/") {
        mndir = mndir.split_at(ps + 1).0;
    }

    let mndir = PathBuf::from(mndir);

    if bdbg && !bqt {
        println!("md pth : '{}'", mdpth.display());
        println!("md nm : '{}'", mdnm.to_str().unwrap());
        println!("mndir: '{}'", mndir.display());
        println!("wrkdir: '{}'", wrkdir.display());
    } //if bdbg && ! bqt

exactly in this use case of the Playground the current working directory is not the directory where the binary is located.

Now I get the expected result:

Path of this executable is: /playground/target/debug/playground
md pth : '/playground/target/debug/playground'
md nm : 'playground'
mndir: '/playground/'
wrkdir: '/playground/target/debug'
cnf dir 1: '/playground/target/debug'
log dir 1: '/playground/target/debug'
app cnf pth 0: '/playground/target/debug'
app cnf pth 1: '/playground/target/debug/application_default.conf'
config file directory: '/playground/target/debug'
config file name: 'application_default.conf'

But still I got my difficulties with the Error Handling.
Mostly when the application can't find the Configuration File its a Installation or Permission Issue so I would like to see where it was looking for the Configuration File to be able to fix it
/playground/target/debug/application_default.conf
would tell me that the config directory not exist or that the configuration file is not located correctly.
So I want to resolve the Error Handling in a constructive way.

I suspect you're having difficulties in this area because you're not doing any error handling at all. unwrap() does not handle errors, it panics when an error is present.

Trying to decipher your goal from this code is extremely challenging. It contains a lot of things that are irrelevant to that goal. It might be that there are just a lot of cross-cutting concerns involved, or lack of abstraction. In any case, I tried to rewrite the code to a) be readable and b) handle errors. Of course, handling errors is a lengthy topic of discussion, and this sample just punts the error to the caller, but it does give the caller the opportunity to handle the error, and that's what is important.

use std::env;
use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use thiserror::Error;

#[derive(Debug, Error)]
enum Error {
    #[error("Directory parsing error")]
    ParseDirectory,

    #[error("Target directory not found")]
    NoTarget,

    #[error("I/O Error")]
    Io(#[from] io::Error),
}

fn get_path(top: &Path, name: &str) -> Result<PathBuf, Error> {
    let target = Some(OsStr::new("target"));
    let dir = top
        .ancestors()
        .find(|p| p.is_dir() && p.file_name() == target)
        .ok_or(Error::NoTarget)?;

    let mut dir = dir.parent().ok_or(Error::ParseDirectory)?.to_path_buf();
    dir.push(name);

    Ok(dir)
}

fn main() -> Result<(), Error> {
    let top = env::current_exe()?;
    let top = top.parent().ok_or(Error::ParseDirectory)?;

    let config = get_path(&top, "config")?;
    let logs = get_path(&top, "logs")?;

    println!("config dir: {:?}", config);
    println!("logs dir: {:?}", logs);

    Ok(())
}

If you would like to handle an error, you might do something like this to log the error:

let logs = get_path(&dir, "logs").map_err(|e| {
    eprintln!("Unable to get logs path: {:?}", e);
    e
})?;

Or ignore the error and use a fallback directory:

let logs = get_path(&dir, "logs")
    .unwrap_or_else(|_| PathBuf::new("/tmp/logs"))?;

Whatever the case, the caller now has the opportunity to handle errors instead of panic.

Thank you for your detailed Reply. It is a very interesting approach and has many good points.
Unfortunately it does not compile:

error[E0432]: unresolved import `thiserror`
 --> src/main.rs:8:5
  |
8 | use thiserror::Error;
  |     ^^^^^^^^^ use of undeclared type or module `thiserror`

I was thinking it could be std::error::Error but this Trait doesn't work this way either:

error: cannot find derive macro `Error` in this scope
  --> src/main.rs:11:17
   |
11 | #[derive(Debug, Error)]
   |                 ^^^^^

error: cannot find attribute `error` in this scope
  --> src/main.rs:13:7
   |
13 |     #[error("Directory parsing error")]
   |       ^^^^^
error: cannot find attribute `from` in this scope
  --> src/main.rs:20:10
   |
20 |     Io(#[from] io::Error),
   |          ^^^^

Yes, actually the application should try an alternative location for the Configuration File like in

let config = get_path(&dir, "config")
    .unwrap_or_else(|_| PathBuf::from(top))?;

You need to add the thiserror crate to use it.

[dependencies]
thiserror = "1.0.19"
1 Like

I used thiserror because it offers an effortless way to derive the std::error::Error trait. anyhow is also a popular option for those that wish to be more flexible in the types of errors they accept. Both are written by the same author. :heart:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.