Skip to content

Commit cb19f05

Browse files
Feat: Substitute environment variables into config paths (#265)
* Substitute environment variables into config paths Fixes #174. * Improve env lookup error handling and reporting. --------- Co-authored-by: Karel Peeters <karel@easics.be>
1 parent 5127082 commit cb19f05

File tree

1 file changed

+118
-3
lines changed

1 file changed

+118
-3
lines changed

vhdl_lang/src/config.rs

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66

77
//! Configuration of the design hierarchy and other settings
88
9-
use crate::data::*;
10-
use fnv::FnvHashMap;
119
use std::env;
10+
use std::env::VarError;
1211
use std::fs::File;
1312
use std::io;
1413
use std::io::prelude::*;
1514
use std::path::Path;
15+
16+
use fnv::FnvHashMap;
1617
use toml::Value;
1718

19+
use crate::data::*;
20+
1821
#[derive(Clone, PartialEq, Eq, Default, Debug)]
1922
pub struct Config {
2023
// A map from library name to file name
@@ -131,6 +134,17 @@ impl Config {
131134
.as_str()
132135
.ok_or_else(|| format!("not a string {file}"))?;
133136

137+
let file = substitute_environment_variables(file, |v| std::env::var(v)).map_err(
138+
|(var, e)| match e {
139+
VarError::NotPresent => {
140+
format!("environment variable '{var}' is not defined")
141+
}
142+
VarError::NotUnicode(_) => format!(
143+
"environment variable '{var}' does not contain valid unicode data"
144+
),
145+
},
146+
)?;
147+
134148
let path = parent.join(file);
135149
let path = path
136150
.to_str()
@@ -278,6 +292,37 @@ impl Config {
278292
}
279293
}
280294

295+
fn substitute_environment_variables(
296+
s: &str,
297+
lookup: impl Fn(&str) -> Result<String, VarError>,
298+
) -> Result<String, (String, VarError)> {
299+
let mut result = String::new();
300+
301+
let mut left = s;
302+
while let Some(start) = left.find('$') {
303+
// keep non-env var piece as-is
304+
result.push_str(&left[..start]);
305+
left = &left[start + 1..];
306+
307+
// replace env var
308+
let env_name_len = left
309+
.find(|c: char| !(c == '_' || c.is_ascii_alphanumeric()))
310+
.unwrap_or(left.len());
311+
let env_name = &left[..env_name_len];
312+
313+
let replacement = lookup(env_name).map_err(|e| (env_name.to_owned(), e))?;
314+
result.push_str(&replacement);
315+
316+
// skip past env var
317+
left = &left[env_name_len..];
318+
}
319+
320+
// keep remaining string
321+
result.push_str(left);
322+
323+
Ok(result)
324+
}
325+
281326
/// Returns true if the pattern is a plain file name and not a glob pattern
282327
fn is_literal(pattern: &str) -> bool {
283328
for chr in pattern.chars() {
@@ -293,9 +338,13 @@ fn is_literal(pattern: &str) -> bool {
293338

294339
#[cfg(test)]
295340
mod tests {
296-
use super::*;
341+
use std::collections::HashMap;
342+
use std::ffi::OsString;
343+
297344
use pretty_assertions::assert_eq;
298345

346+
use super::*;
347+
299348
/// Utility function to create an empty file in parent folder
300349
fn touch(parent: &Path, file_name: &str) -> PathBuf {
301350
let path = parent.join(file_name);
@@ -501,6 +550,7 @@ lib.files = [
501550
assert_files_eq(&file_names, &[file1, file2]);
502551
assert_eq!(messages, vec![]);
503552
}
553+
504554
#[test]
505555
fn test_warning_on_emtpy_glob_pattern() {
506556
let parent = Path::new("parent_folder");
@@ -541,4 +591,69 @@ work.files = [
541591
);
542592
assert_eq!(config.expect_err("Expected erroneous config"), "The 'work' library is not a valid library.\nHint: To use a library that contains all files, use a common name for all libraries, i.e., 'defaultlib'")
543593
}
594+
595+
#[test]
596+
fn substitute() {
597+
let mut map = HashMap::new();
598+
map.insert("A".to_owned(), Ok("a".to_owned()));
599+
map.insert("ABCD".to_owned(), Ok("abcd".to_owned()));
600+
map.insert("A_0".to_owned(), Ok("a0".to_owned()));
601+
map.insert("_".to_owned(), Ok("u".to_owned()));
602+
map.insert("PATH".to_owned(), Ok("some/path".to_owned()));
603+
map.insert(
604+
"not_unicode".to_owned(),
605+
Err(VarError::NotUnicode(OsString::new())),
606+
);
607+
// note: "not_present" is not in the map
608+
609+
let lookup = |v: &str| map.get(v).unwrap_or(&Err(VarError::NotPresent)).clone();
610+
611+
// simple pattern tests
612+
assert_eq!(
613+
Ok("test".to_owned()),
614+
substitute_environment_variables("test", lookup)
615+
);
616+
assert_eq!(
617+
Ok("a".to_owned()),
618+
substitute_environment_variables("$A", lookup)
619+
);
620+
assert_eq!(
621+
Ok("abcd".to_owned()),
622+
substitute_environment_variables("$ABCD", lookup)
623+
);
624+
assert_eq!(
625+
Ok("a0".to_owned()),
626+
substitute_environment_variables("$A_0", lookup)
627+
);
628+
assert_eq!(
629+
Ok("u".to_owned()),
630+
substitute_environment_variables("$_", lookup)
631+
);
632+
assert_eq!(
633+
Ok("some/path".to_owned()),
634+
substitute_environment_variables("$PATH", lookup)
635+
);
636+
637+
// embedded in longer string
638+
assert_eq!(
639+
Ok("test/a/test".to_owned()),
640+
substitute_environment_variables("test/$A/test", lookup)
641+
);
642+
assert_eq!(
643+
Ok("test/a".to_owned()),
644+
substitute_environment_variables("test/$A", lookup)
645+
);
646+
assert_eq!(
647+
Ok("a/test".to_owned()),
648+
substitute_environment_variables("$A/test", lookup)
649+
);
650+
assert_eq!(
651+
Ok("test/some/path/test".to_owned()),
652+
substitute_environment_variables("test/$PATH/test", lookup)
653+
);
654+
655+
// error cases
656+
assert!(substitute_environment_variables("$not_present", lookup).is_err());
657+
assert!(substitute_environment_variables("$not_unicode", lookup).is_err());
658+
}
544659
}

0 commit comments

Comments
 (0)