Issue with strings / interpolation

I'm new to Rust, so coming in from Python. With the following Rust code,

use shellexpand;
use std::fs;

fn main() {
    let folder_name = "some-name";
    let full_path: &str = &shellexpand::tilde("~/{}", folder_name);
    fs::create_dir_all(full_path).expect("Unable to create {} folder", folder_name);
    println!("{}", full_path);
}

Compilation fails because tilde and expect require one argument but I gave two. This works for the println! macro though - and f-strings' RFC hasn't landed yet [1].

May I know how this should work?

[1] https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html

For the tilde function, you will want to use the format! macro, and take reference to that. Note also, that it's highly unlikely that it'll return an &str with a dynamic input if it does what I think it does (returns the value of ~). Hence you'll want to annotate full_path as an owned String.

Next for the expect error, you'll need to do pattern matching to panic with a custom message. Something like this:

if let Err(_) = fs::create_dir_all(full_path) {
    panic!("Unable to create {} folder", folder_name);
}

You can also use expect and format for the result.

This will work as well:

fs::create_dir_all(full_path)
    .expect(&format("Unable to create {} folder", folder_name));

That does an unnecessary heap allocation and string formatting in the non-error case.

Edit: You know what, ignore the rest of this. @Hyeonu makes an excellent point below about newcomer confusion. See @Cerberuser's suggestion instead.

This seems like a good candidate for postfix macros (if they are ever supported) to support lazy formatting:
fs::create_dir_all(full_path)
    .expect!("Unable to create {} folder", folder_name);
which would would only format in the error case; e.g. `let a = foo.expect!("bar {}", baz);` would translate to:
let a = match foo {
  Some(f) => f,
  Err(_) => panic!("bar {}", baz),
};
1 Like

Postfix macro is not what we have now, and I don't think it will be stabilized within an year since it requires some nontrivial syntactic decisions so months of community bikeshedding would be necessary. Please don't teach newcommmers such feature, it only confuse them and may give them false signal which the Rust is an incomplete language. The Rust in its current state is pretty practical language actively used by industry.

3 Likes

AFAIK, in this case the unwrap_or_else (essentially equivalent to the if let variant presented above) is recommended, in particular by clippy.

1 Like

Thank you all for your help. I'm pretty sure @OptimisticPeach and some of you are moving me on in the right direction:

use shellexpand;
use std::fs;

fn main() {
   let folder_name = "some-name";
   let full_path = shellexpand::tilde(&format!("~/{}", folder_name)).to_string();
   if let Err(_) = fs::create_dir_all(folder_name) {
       panic!("Unable to create {} folder", folder_name)
   }
   println!("{}", full_path);
}

is what I have now, and it seems to compile successfully. More questions:

  1. Do I still use &format! in the call to shellexpand::tilde?
  2. Should I use .to_string() as suggested by Rust when I hit some errors while compiling? Is this good practice?
  3. Should I specify the type of full_path to be some form of String? I'm guessing that later versions of Rust (2018 Edition maybe?) infer this automatically.
  4. The result returned by the call to shellexpand::tilde is a growable String and not &str. I'm still not sure of the difference.
  5. Not really a question, but because full_path never gets changed, it does not need to be a &mut reference.

Question 6: Is there a equivalent of Python's os.path.sep? I think I'm hardcoding the forward slash in the call to shellexpand::tilde, or is Rust/shellexpand intelligent enough to convert that to a backslash on Windows systems?

  1. The function signature is:

    pub fn tilde<SI: ?Sized>(input: &SI) -> Cow<str> 
    where
        SI: AsRef<str>, 
    

    So, you want to provide a reference to something whose reference can become a str. String is conveniently provided, so yes, &format! is the way to go.

    • tilde returns a Cow<str>
    • fs::create_dir_all requires something which implements AsRef<Path>:
      pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()>
      
    • Cow<str> does not impl AsRef<Path>.
    • It does however, impl Deref<Target = str>.
    • &str does impl AsRef<Path>.

    So you can say the following instead of that, however chances are that the Cow was already a String instead of an str:

    let full_path = shellexpand::tilde(&format!("~/{}", folder_name));
    if let Err(_) = fs::create_dir_all(&*folder_name) {
    
  2. full_path is a Cow<str>. It may be either an &'_ str or a String.

  3. String vs str is something that's been covered extensively. However the gist of it is that a str is semantically a set of contiguous UTF-8 encoded bytes. An &str (a reference to a str) is a pointer to those bytes of dynamic length, a property which is encoded as part of the pointer. A String is a managed str. It internally is a Vec<u8> (a Vec of bytes), which it just makes sure is UTF-8.

  4. I don't quite follow what you mean. We never take a reference to full_path.

  5. Windows apis have become more modern and accept /s now, alongside \s. In any case, Rust apis in my experience have always been just fine with /s and \s in Windows. Additionally, \ is a valid file name in linux, so you should probably stick to /s when possible. There is std::path::MAIN_SEPARATOR which is what you're technically looking for, however you should really be using a PathBuf to manage your paths.

1 Like

Thank you, that's a lot to digest.

How would you suggest using PathBuf in the example above instead of String? i.e. make shellexpand accept PathBuf?

I guess using PathBuf is similar-ish to pathlib on Python.

use shellexpand;
use std::fs;
use std::path;


fn set_path(path: &path::Path) -> path::PathBuf {
    let buf = path::PathBuf::from(path);
    buf
}

fn main() {
    let folder_name = "some-name";
    let new_path_str = shellexpand::tilde(&format!("~/{}", folder_name));
    let new_path = set_path(path::Path::new(&new_path_str));
    if let Err(_) = fs::create_dir_all(&new_path) {
        panic!("Unable to create {:?} folder", &new_path)
    }
    println!("{:?}", &new_path);
}

I tried converting to use PathBuf and ended up as far as having one last error I'm unable to solve:

error[E0277]: the trait bound `std::borrow::Cow<'_, str>: std::convert::AsRef<std::ffi::OsStr>` is not satisfied
    --> src/main.rs:21:45
     |
21   |     let new_path = set_path(path::Path::new(&new_path_str));
     |                                             ^^^^^^^^^^^^^ the trait `std::convert::AsRef<std::ffi::OsStr>` is not implemented for `std::borrow::Cow<'_, str>`
     |
     = help: the following implementations were found:
               <std::borrow::Cow<'_, T> as std::convert::AsRef<T>>
               <std::borrow::Cow<'_, std::ffi::OsStr> as std::convert::AsRef<std::path::Path>>

Am I dealing with Cow or AsRef wrongly?

D'oh, I was missing .to_string() in one of the lines above:

let new_path_str = shellexpand::tilde(&format!("~/{}", folder_name)).to_string();

It then compiles successfully.