Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle multiple dotenv files #880

Merged
merged 14 commits into from
Dec 8, 2023
2 changes: 2 additions & 0 deletions docs/integrations/dotenv.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ If you have a `.env`, you'll see instructions how to enable integration:
# Optionally, you can choose which filename to load.
#
# dotenv.filename = ".env.production";
# or
# dotenv.filename = [ ".env.production" ".env.development" ]
}
```

Expand Down
6 changes: 2 additions & 4 deletions docs/reference/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,6 @@ Disable the hint that are printed when the dotenv module is not enabled, but .en




*Type:*
boolean

Expand All @@ -541,13 +540,12 @@ boolean

## dotenv.filename

The name of the dotenv file to load.

The name of the dotenv file to load, or a list of dotenv files to load in order of precedence.



*Type:*
string
string or list of string



Expand Down
1 change: 1 addition & 0 deletions examples/dotenv/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
FOO=1
BAR=2
BAZ=3
3 changes: 2 additions & 1 deletion examples/dotenv/.test.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
env | grep FOO=1
env | grep BAR=1
env | grep BAR=1
env | grep BAZ=5
1 change: 1 addition & 0 deletions examples/dotenv/bar.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BAZ=5
1 change: 1 addition & 0 deletions examples/dotenv/devenv.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{ pkgs, ... }: {
dotenv.enable = true;
dotenv.filename = [ ".env" "bar.env" ];

env.BAR = "1";
}
73 changes: 36 additions & 37 deletions src/modules/integrations/dotenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
let
cfg = config.dotenv;

dotenvPath = config.devenv.root + "/" + cfg.filename;
normalizeFilenames = filenames: if lib.isList filenames then filenames else [ filenames ];
dotenvFiles = normalizeFilenames cfg.filename;
dotenvPaths = map (filename: config.devenv.root + "/" + filename) dotenvFiles;

dotenvFound = lib.pathExists dotenvPath;
parseLine = line:
let
parts = builtins.match "(.+) *= *(.+)" line;
Expand All @@ -16,17 +17,30 @@ let
null;

parseEnvFile = content: builtins.listToAttrs (lib.filter (x: !builtins.isNull x) (map parseLine (lib.splitString "\n" content)));

mergeEnvFiles = files: lib.foldl' (acc: file: lib.recursiveUpdate acc (if lib.pathExists file then parseEnvFile (builtins.readFile file) else { })) { } files;

createMissingFileMessage = file:
let
exampleExists = builtins.pathExists (file + ".example");
in
lib.optionalString (!lib.pathExists file) ''
echo "💡 The dotenv file '${file}' was not found."
${lib.optionalString exampleExists ''
echo " To create this file, you can copy the example file:"
echo " $ cp ${file}.example ${file}"
''}
'';

in
{
options.dotenv = {
enable = lib.mkEnableOption ".env integration, doesn't support comments or multiline values.";

filename = lib.mkOption {
type = lib.types.str;
type = lib.types.either lib.types.str (lib.types.listOf lib.types.str);
default = ".env";
description = ''
The name of the dotenv file to load.
'';
description = "The name of the dotenv file to load, or a list of dotenv files to load in order of precedence.";
};

resolved = lib.mkOption {
Expand All @@ -37,46 +51,31 @@ in
disableHint = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Disable the hint that are printed when the dotenv module is not enabled, but .env is present.
'';
description = "Disable the hint that are printed when the dotenv module is not enabled, but .env is present.";
};
};

config = lib.mkMerge [
(lib.mkIf (cfg.enable && builtins.pathExists dotenvPath) {
(lib.mkIf cfg.enable {
env = lib.mapAttrs (name: value: lib.mkDefault value) config.dotenv.resolved;
dotenv.resolved = parseEnvFile (builtins.readFile dotenvPath);
dotenv.resolved = mergeEnvFiles dotenvPaths;
})
(lib.mkIf (cfg.enable && !builtins.pathExists dotenvPath) (
let
exampleExists = builtins.pathExists (dotenvPath + ".example");
in
{
enterShell = ''
echo "💡 A ${cfg.filename} file was not found, while dotenv integration is enabled."
echo
${lib.optionalString exampleExists ''
echo " To create .env, you can copy the example file:"
echo
echo " $ cp ${dotenvPath}.example ${dotenvPath}";
echo
''}
echo " To disable it, add \`dotenv.enable = false;\` to your devenv.nix file.";
(lib.mkIf (cfg.enable) {
enterShell = lib.concatStringsSep "\n" (map createMissingFileMessage dotenvPaths);
})
(lib.mkIf (!cfg.enable && !cfg.disableHint) {
enterShell =
let
dotenvFound = lib.any (file: lib.pathExists file) dotenvPaths;
in
lib.optionalString dotenvFound ''
echo "💡 A dotenv file was found, while dotenv integration is currently not enabled."
echo
echo " To enable it, add \`dotenv.enable = true;\` to your devenv.nix file.";
echo " To disable this hint, add \`dotenv.disableHint = true;\` to your devenv.nix file.";
echo
echo "See https://devenv.sh/integrations/dotenv/ for more information.";
'';
}
))
(lib.mkIf (!cfg.enable && !cfg.disableHint) {
enterShell = lib.optionalString dotenvFound ''
echo "💡 A ${cfg.filename} file found, while dotenv integration is currently not enabled."
echo
echo " To enable it, add \`dotenv.enable = true;\` to your devenv.nix file.";
echo " To disable this hint, add \`dotenv.disableHint = true;\` to your devenv.nix file.";
echo
echo "See https://devenv.sh/integrations/dotenv/ for more information.";
'';
})
];
}