diff --git a/lib/lib.nix b/lib/lib.nix index 34c137f0..0291b0d0 100644 --- a/lib/lib.nix +++ b/lib/lib.nix @@ -682,6 +682,44 @@ in */ toKdl = import ./toKdl.nix { inherit lib wlib; }; + /** + Sanitize a string into a valid environment variable name. + + This function sanitizes all characters that are not allowed in typical + POSIX environment variable names (`[A-Za-z0-9_]`), and ensures the + resulting string starts with a valid leading character (`[A-Za-z_]`). + + Behavior: + - All invalid characters are replaced with underscore characters (`_`) + + Examples: + ``` + sanitizeEnvVarName "FOO-BAR" => "FOO_BAR" + sanitizeEnvVarName "123.abc" => "_23_abc" + sanitizeEnvVarName "!@#" => "___" + sanitizeEnvVarName "hello, world!" => "hello__world_" + ``` + + Notes: + - Only ASCII characters are considered; all other characters are removed + - This does not guarantee uniqueness across multiple inputs + */ + sanitizeEnvVarName = + s: + let + isUpper = c: c >= "A" && c <= "Z"; + isLower = c: c >= "a" && c <= "z"; + isDigit = c: c >= "0" && c <= "9"; + + valid = + i: c: + if i == 0 then + isUpper c || isLower c || c == "_" + else + isUpper c || isLower c || isDigit c || c == "_"; + in + lib.concatStrings (lib.imap0 (i: c: if valid i c then c else "_") (lib.stringToCharacters s)); + /** Placeholder value used when overriding a non-main field of a spec type. diff --git a/modules/constructFiles/module.nix b/modules/constructFiles/module.nix index 3518a961..4f598a2b 100644 --- a/modules/constructFiles/module.nix +++ b/modules/constructFiles/module.nix @@ -36,6 +36,7 @@ then that means you should set this value to something which is a valid shell variable name. ''; + apply = wlib.sanitizeEnvVarName; }; content = lib.mkOption { type = lib.types.lines; @@ -85,16 +86,36 @@ config.drv = lib.mkIf (config.constructFiles != { }) ( let files = builtins.attrValues config.constructFiles; + mkUnique = + attrs: base: + let + try = + i: + let + candidate = if i == null then base else "${base}_${toString i}"; + in + if attrs ? ${candidate} then try (if i == null then 0 else i + 1) else candidate; + in + try null; result = builtins.foldl' - (acc: v: { - attrs = acc.attrs // { - ${v.key} = v.content; - }; - passAsFile = acc.passAsFile ++ [ v.key ]; - }) + ( + acc: v: + let + key = mkUnique acc.attrs v.key; + in + { + attrs = acc.attrs // { + ${key} = v.content; + }; + passAsFile = acc.passAsFile ++ [ key ]; + } + ) { - attrs = { }; + attrs = { + # prevents something from being named this + passAsFile = [ ]; + }; passAsFile = [ ]; } files;