How do I use include_str! for multiple files or an entire directory?

余生长醉 提交于 2020-05-16 04:10:01

问题


I would like to copy an entire directory to a location in a user's $HOME. Individually copying files to that directory is straightforward:

let contents = include_str!("resources/profiles/default.json");
let fpath = dpath.join(&fname);
fs::write(fpath, contents).expect(&format!("failed to create profile: {}", n));

I haven't found a way to adapt this to multiple files:

for n in ["default"] {
    let fname = format!("{}{}", n, ".json");
    let x = format!("resources/profiles/{}", fname).as_str();
    let contents = include_str!(x);
    let fpath = dpath.join(&fname);
    fs::write(fpath, contents).expect(&format!("failed to create profile: {}", n));
}

...the compiler complains that x must be a string literal.

As far as I know, there are two options:

  1. Write a custom macro.
  2. Replicate the first code for each file I want to copy.

What is the best way of doing this?


回答1:


I would create a build script that iterates through a directory, building up an array of tuples containing the name and another macro call to include the raw data:

use std::{
    env,
    error::Error,
    fs::{self, File},
    io::Write,
    path::Path,
};

const SOURCE_DIR: &str = "some/path/to/include";

fn main() -> Result<(), Box<dyn Error>> {
    let out_dir = env::var("OUT_DIR")?;
    let dest_path = Path::new(&out_dir).join("all_the_files.rs");
    let mut all_the_files = File::create(&dest_path)?;

    writeln!(&mut all_the_files, r##"["##,)?;

    for f in fs::read_dir(SOURCE_DIR)? {
        let f = f?;

        if !f.file_type()?.is_file() {
            continue;
        }

        writeln!(
            &mut all_the_files,
            r##"("{name}", include_bytes!(r#"{name}"#)),"##,
            name = f.path().display(),
        )?;
    }

    writeln!(&mut all_the_files, r##"]"##,)?;

    Ok(())
}

This has some weaknesses, namely that it requires the path to be expressible as a &str. Since you were already using include_string!, I don't think that's an extra requirement. This also means that the generated string has to be a valid Rust string. We use raw strings inside the generated file, but this can still fail if a filename were to contain the string "#. A better solution would probably use str::escape_default.

Since we are including files, I used include_bytes! instead of include_str!, but if you really needed to you can switch back. The raw bytes skips performing UTF-8 validation at compile time, so it's a small win.

Using it involves importing the generated value:

const ALL_THE_FILES: &[(&str, &[u8])] = &include!(concat!(env!("OUT_DIR"), "/all_the_files.rs"));

fn main() {
    for (name, data) in ALL_THE_FILES {
        println!("File {} is {} bytes", name, data.len());
    }
}

See also:

  • How can I locate resources for testing with Cargo?



回答2:


Using a macro:

macro_rules! incl_profiles {
    ( $( $x:expr ),* ) => {
        {
            let mut profs = Vec::new();
            $(
                profs.push(($x, include_str!(concat!("resources/profiles/", $x, ".json"))));
            )*

            profs
        }
    };
}

...

let prof_tups: Vec<(&str, &str)> = incl_profiles!("default", "python");

for (prof_name, prof_str) in prof_tups {
    let fname = format!("{}{}", prof_name, ".json");
    let fpath = dpath.join(&fname);
    fs::write(fpath, prof_str).expect(&format!("failed to create profile: {}", prof_name));
}

Note: This is not dynamic. The files ("default" and "python") are specified in the call to the macro.

Updated: Use Vec instead of HashMap.



来源:https://stackoverflow.com/questions/50553370/how-do-i-use-include-str-for-multiple-files-or-an-entire-directory

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!