How to accept a file as a command-line argument in Rust

Use std::path::PathBuf instead of String as the type to provide to an Args struct for clap.

Using String as a starting point

It’s common to ask users where to save files. When defining command-line arguments with clap, it’s easy to request these as a String.

				
					use clap::Parser;

#[derive(Debug, Parser)]
struct Args {
    #[clap(long)]
    save_to: String,
}
				
			

Using a String is a good first step. If it’s a necessary argument, your users will get warned very quickly about when it’s missing

There’s a small problem, though. There’s a mismatch between Rust’s opinion of text and the operating system’s opinion about what counts as a valid path.

  • String is too restrictive. It’s UTF-8, whereas operating systems might use different encodings.
  • String is also too permissive. It allows users to pass in any text at all. File system locations have special rules though. They’re probably not allowed to contain the NULL byte, although this is perfectly legal within a Rust String

Replacing String with std::path::PathBuf

These differences described the previous section mean that our application code needs to do lots of work itself. It needs to check that the user is sending data that the operating system will understand, as well as report the error to the user if there’s a mistake.

You push that work to clap by changing the type that you accept in your Args struct:

				
					use clap::Parser;

#[derive(Debug, Parser)]
struct Args {
    #[clap(long)]
    save_to: std::path::PathBuf,
}
				
			

Tidying up the code

You’re also welcome to use imports to bring PathBuf into local scope, rather than using its full path in the body of the program.

				
					use clap::Parser;
use std::path::PathBuf;

#[derive(Debug, Parser)]
struct Args {
    #[clap(long)]
    save_to: PathBuf,
}
				
			

Testing command-line arguments in Rust

Testing command-line arguments in Rust’s clap crate is easier than it seems. Instead of calling Args::parse(), which parses the actual command-line arguments (known as “argv”, for argument vector), our test code can use Args::parse_from().

Note: I recommend Args::parse_from() rather that Args::try_parse_from() in tests, because we want to induce panics as soon as possible in test code. A panic means that the test has failed and aborting as soon as possible makes the problem easier to identify and debug.

				
					use clap::Parser;
use std::path::PathBuf;

#[derive(Debug, Parser)]
struct Args {
    #[clap(long)]
    save_to: PathBuf,
}

#[test]
fn requires_save_location() {
   // omitting the required argument generates an error
   assert!(Args::try_parse_from(&["app"]).is_err());
   
   // passing in the required argument allows Args to be constructed
   let _ = Args::parse_from(&["app", "--save-to=somewhere.db"]);
}
				
			

Contents