Cant return a `Vec<A>`

Forgive me if my english seems a bit wrong, english is not my first language

Anyway, I'm writing something that requires me to make command-line arguments,
and was stuck with this very tacky and tedious solution:

    fn installer_args<'a, I>(&self, install_dir: I, game_version: String) -> Vec<Cow<'a, str>>
    where
        I: AsRef<Path> + 'a,
    {
        match self {
            Self::Forge => vec![
                Cow::from("--installServer"),
                Cow::from(install_dir.as_ref().to_string_lossy().to_string()),
            ],
            Self::Neoforge => vec![
                Cow::from("--installServer"),
                Cow::from(install_dir.as_ref().to_string_lossy().to_string()),
            ],
            Self::Quilt => vec![
                Cow::from("install"),
                Cow::from("server"),
                Cow::from(game_version),
                Cow::from(format!(
                    "--install-dir={}",
                    install_dir.as_ref().to_string_lossy()
                )),
                Cow::from("--create-scripts"),
                Cow::from("--download-server"),
            ],
            Self::Fabric => vec![
                Cow::from("server"),
                Cow::from("-dir"),
                Cow::from(install_dir.as_ref().to_string_lossy().to_string()),
                Cow::from("-mcversion"),
                Cow::from(game_version),
                Cow::from("-downloadMinecraft"),
            ],
            Self::Glowstone => todo!(), // TODO: Also this
        }
    }

I really dont like it, and I only require these arguments to be passable in Command::args.

I originally thought of using Vec<A>, where A: ToString to no avail

Is it possible to make my code better?

I'd pass your arguments as string slices and just return a Vec<&str>:

enum Mode {
    Forge,
    Neoforge,
    Quilt,
    Fabric,
    Glowstone,
}

impl Mode {
    fn installer_args<'a>(&self, install_dir: &'a str, game_version: &'a str) -> Vec<&'a str>
    {
        match self {
            Self::Forge => vec![
                "--installServer",
                install_dir,
            ],
            Self::Neoforge => vec![
                "--installServer",
                install_dir,
            ],
            _ => todo!(),
        }
    }
}

fn main() {
    let args = Mode::Forge.installer_args("/tmp", "0.1.0");
    
    for arg in args {
        println!("{:?}", arg);
    }
}

Playground.

2 Likes

Since you are passing these args to Command which takes OsStr, you should return a Vec<&OsStr> or Vec<OsString>. You can convert &Path to &OsStr losslessly. Playground

This is cheaper (no UTF-8 validation required) and more correct (will handle non-UTF-8 paths) than converting to str or String or Cow<str>.

2 Likes

Imho using .into() instead of Cow::from would be a bit easier to read. All types are fully specified, so type inference wouldn't be an issue. I'd also extract the repeating conversions for game_version and install_dir to local variables.

I wouldn't do it here, but for larger functions of that kind I'd also consider writing a small local wrapper macro over vec! which would allow to use items of different type, while internally applying the conversion to a common enum.

2 Likes

I tried combining @afetisov and @mbrubeck's responses, and with the help of this SO post, i managed to create this:

impl<P: AsRef<OsStr>> InstallerArg for P {
    fn as_arg(&self) -> &OsStr {
        self.as_ref()
    }
}

macro_rules! args {
    ($($arg:expr),+) => {
        vec![$(InstallerArg::as_arg($arg),+]
    };
}

And tried to test it using this:

    let install_name = String::from("test");
    let args = args!["a", "b", "c", install_name];

but for some reason i get errors like:

error[E0277]: the trait bound `str: InstallerArg` is not satisfied

From my understanding, InstallerArg should be implemented for every type who also implements AsRef<OsStr>. And if str implements AsRef<OsStr>, shouldnt it also implement InstallerArg?

Please post the whole error message you get from running cargo check in your console. IDEs are notorious for removing all the helpfulness from rustc's (usually) quite elaborate and useful error messages.

The essence of your problem is that you pass &self by-reference to InstallerArg::as_arg. &str does implement AsRef<OsStr>, but str (not behind a reference) does not[1]. Calling

let s: &str = "foo";
InstallerArg::as_arg(foo);

translates to

let s: &str = "foo";
<str as InstallerArg>::as_arg(s);

because as_arg takes &self as argument, not self. To solve this you can pass a &&str to as_arg, which would be like calling

let s: &&str = &"foo";
<&str as InstallerArg>::as_arg(s);

This works as &str does implement AsRef<OsStr> and thanks to your generic implementation InstallerArg as well.

Adding an additional reference to your macro should suffice to get your example to work:

- vec![$(InstallerArg::as_arg($arg),+]
+ vec![$(InstallerArg::as_arg(&$arg),)+]

  1. Redacted, see answer below. Answer still remains valid as you need to pass String by reference, too. You can either add the reference to the macro directly like I did or pass Strings by reference to your args! macro: args!["a", "b", "c", &install_name]. ↩ī¸Ž

Because all type parameters have an implicit Sized bound, your impl does not apply to dynamically-sized types like str. You can fix it by removing the Sized bound:

-impl<P: AsRef<OsStr>> InstallerArg for P {
+impl<P: AsRef<OsStr> + ?Sized> InstallerArg for P {
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.