GeezerVison is showing github.com — src: add `--env-file-if-exists` flag · nodejs/node@77d162a
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +16Lines changed: 16 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -818,6 +818,8 @@ in the file, the value from the environment takes precedence. | 818 | 818 | You can pass multiple `--env-file` arguments. Subsequent files override | 819 | 819 | pre-existing variables defined in previous files. | 820 | 820 |
| | 821 | +An error is thrown if the file does not exist. | | 822 | + | 821 | 823 | ```bash | 822 | 824 | node --env-file=.env --env-file=.development.env index.js | 823 | 825 | ``` | @@ -857,6 +859,9 @@ Export keyword before a key is ignored: | 857 | 859 | export USERNAME="nodejs" # will result in `nodejs` as the value. | 858 | 860 | ``` | 859 | 861 |
| | 862 | +If you want to load environment variables from a file that may not exist, you | | 863 | +can use the [`--env-file-if-exists`][] flag instead. | | 864 | + | 860 | 865 | ### `-e`, `--eval "script"` | 861 | 866 |
| 862 | 867 | <!-- YAML | @@ -1765,6 +1770,15 @@ is being linked to Node.js. Sharing the OpenSSL configuration may have unwanted | 1765 | 1770 | implications and it is recommended to use a configuration section specific to | 1766 | 1771 | Node.js which is `nodejs_conf` and is default when this option is not used. | 1767 | 1772 |
| | 1773 | +### `--env-file-if-exists=config` | | 1774 | + | | 1775 | +<!-- YAML | | 1776 | +added: REPLACEME | | 1777 | +--> | | 1778 | + | | 1779 | +Behavior is the same as [`--env-file`][], but an error is not thrown if the file | | 1780 | +does not exist. | | 1781 | + | 1768 | 1782 | ### `--pending-deprecation` | 1769 | 1783 |
| 1770 | 1784 | <!-- YAML | @@ -3581,6 +3595,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 | 3581 | 3595 | [`--build-snapshot`]: #--build-snapshot | 3582 | 3596 | [`--cpu-prof-dir`]: #--cpu-prof-dir | 3583 | 3597 | [`--diagnostic-dir`]: #--diagnostic-dirdirectory | | 3598 | +[`--env-file-if-exists`]: #--env-file-if-existsconfig | | 3599 | +[`--env-file`]: #--env-fileconfig | 3584 | 3600 | [`--experimental-default-type=module`]: #--experimental-default-typetype | 3585 | 3601 | [`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs | 3586 | 3602 | [`--experimental-strip-types`]: #--experimental-strip-types | |
Collapse file Copy file name to clipboardExpand all lines: src/node.cc +12-6Lines changed: 12 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -903,20 +903,26 @@ static ExitCode InitializeNodeWithArgsInternal( | 903 | 903 | HandleEnvOptions(per_process::cli_options->per_isolate->per_env); | 904 | 904 |
| 905 | 905 | std::string node_options; | 906 | | - auto file_paths = node::Dotenv::GetPathFromArgs(*argv); | | 906 | + auto env_files = node::Dotenv::GetDataFromArgs(*argv); | 907 | 907 |
| 908 | | - if (!file_paths.empty()) { | | 908 | + if (!env_files.empty()) { | 909 | 909 | CHECK(!per_process::v8_initialized); | 910 | 910 |
| 911 | | - for (const auto& file_path : file_paths) { | 912 | | - switch (per_process::dotenv_file.ParsePath(file_path)) { | | 911 | + for (const auto& file_data : env_files) { | | 912 | + switch (per_process::dotenv_file.ParsePath(file_data.path)) { | 913 | 913 | case Dotenv::ParseResult::Valid: | 914 | 914 | break; | 915 | 915 | case Dotenv::ParseResult::InvalidContent: | 916 | | - errors->push_back(file_path + ": invalid format"); | | 916 | + errors->push_back(file_data.path + ": invalid format"); | 917 | 917 | break; | 918 | 918 | case Dotenv::ParseResult::FileError: | 919 | | - errors->push_back(file_path + ": not found"); | | 919 | + if (file_data.is_optional) { | | 920 | + fprintf(stderr, | | 921 | + "%s not found. Continuing without it.\n", | | 922 | + file_data.path.c_str()); | | 923 | + continue; | | 924 | + } | | 925 | + errors->push_back(file_data.path + ": not found"); | 920 | 926 | break; | 921 | 927 | default: | 922 | 928 | UNREACHABLE(); | |
Collapse file Copy file name to clipboardExpand all lines: src/node_dotenv.cc +32-16Lines changed: 32 additions & 16 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -11,36 +11,52 @@ using v8::NewStringType; | 11 | 11 | using v8::Object; | 12 | 12 | using v8::String; | 13 | 13 |
| 14 | | -std::vector<std::string> Dotenv::GetPathFromArgs( | | 14 | +std::vector<Dotenv::env_file_data> Dotenv::GetDataFromArgs( | 15 | 15 | const std::vector<std::string>& args) { | | 16 | + const std::string_view optional_env_file_flag = "--env-file-if-exists"; | | 17 | + | 16 | 18 | const auto find_match = [](const std::string& arg) { | 17 | | - return arg == "--" || arg == "--env-file" || arg.starts_with("--env-file="); | | 19 | + return arg == "--" || arg == "--env-file" || | | 20 | + arg.starts_with("--env-file=") || arg == "--env-file-if-exists" || | | 21 | + arg.starts_with("--env-file-if-exists="); | 18 | 22 | }; | 19 | | - std::vector<std::string> paths; | 20 | | - auto path = std::find_if(args.begin(), args.end(), find_match); | 21 | 23 |
| 22 | | - while (path != args.end()) { | 23 | | - if (*path == "--") { | 24 | | - return paths; | | 24 | + std::vector<Dotenv::env_file_data> env_files; | | 25 | + // This will be an iterator, pointing to args.end() if no matches are found | | 26 | + auto matched_arg = std::find_if(args.begin(), args.end(), find_match); | | 27 | + | | 28 | + while (matched_arg != args.end()) { | | 29 | + if (*matched_arg == "--") { | | 30 | + return env_files; | 25 | 31 | } | 26 | | - auto equal_char = path->find('='); | 27 | 32 |
| 28 | | - if (equal_char != std::string::npos) { | 29 | | - paths.push_back(path->substr(equal_char + 1)); | | 33 | + auto equal_char_index = matched_arg->find('='); | | 34 | + | | 35 | + if (equal_char_index != std::string::npos) { | | 36 | + // `--env-file=path` | | 37 | + auto flag = matched_arg->substr(0, equal_char_index); | | 38 | + auto file_path = matched_arg->substr(equal_char_index + 1); | | 39 | + | | 40 | + struct env_file_data env_file_data = { | | 41 | + file_path, flag.starts_with(optional_env_file_flag)}; | | 42 | + env_files.push_back(env_file_data); | 30 | 43 | } else { | 31 | | - auto next_path = std::next(path); | | 44 | + // `--env-file path` | | 45 | + auto file_path = std::next(matched_arg); | 32 | 46 |
| 33 | | - if (next_path == args.end()) { | 34 | | - return paths; | | 47 | + if (file_path == args.end()) { | | 48 | + return env_files; | 35 | 49 | } | 36 | 50 |
| 37 | | - paths.push_back(*next_path); | | 51 | + struct env_file_data env_file_data = { | | 52 | + *file_path, matched_arg->starts_with(optional_env_file_flag)}; | | 53 | + env_files.push_back(env_file_data); | 38 | 54 | } | 39 | 55 |
| 40 | | - path = std::find_if(++path, args.end(), find_match); | | 56 | + matched_arg = std::find_if(++matched_arg, args.end(), find_match); | 41 | 57 | } | 42 | 58 |
| 43 | | - return paths; | | 59 | + return env_files; | 44 | 60 | } | 45 | 61 |
| 46 | 62 | void Dotenv::SetEnvironment(node::Environment* env) { | |
Collapse file Copy file name to clipboardExpand all lines: src/node_dotenv.h +5-1Lines changed: 5 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -13,6 +13,10 @@ namespace node { | 13 | 13 | class Dotenv { | 14 | 14 | public: | 15 | 15 | enum ParseResult { Valid, FileError, InvalidContent }; | | 16 | + struct env_file_data { | | 17 | + std::string path; | | 18 | + bool is_optional; | | 19 | + }; | 16 | 20 |
| 17 | 21 | Dotenv() = default; | 18 | 22 | Dotenv(const Dotenv& d) = delete; | @@ -27,7 +31,7 @@ class Dotenv { | 27 | 31 | void SetEnvironment(Environment* env); | 28 | 32 | v8::Local<v8::Object> ToObject(Environment* env) const; | 29 | 33 |
| 30 | | - static std::vector<std::string> GetPathFromArgs( | | 34 | + static std::vector<env_file_data> GetDataFromArgs( | 31 | 35 | const std::vector<std::string>& args); | 32 | 36 |
| 33 | 37 | private: | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -652,6 +652,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | 652 | 652 | "set environment variables from supplied file", | 653 | 653 | &EnvironmentOptions::env_file); | 654 | 654 | Implies("--env-file", "[has_env_file_string]"); | | 655 | + AddOption("--env-file-if-exists", | | 656 | + "set environment variables from supplied file", | | 657 | + &EnvironmentOptions::optional_env_file); | | 658 | + Implies("--env-file-if-exists", "[has_env_file_string]"); | 655 | 659 | AddOption("--test", | 656 | 660 | "launch test runner on startup", | 657 | 661 | &EnvironmentOptions::test_runner); | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -178,6 +178,7 @@ class EnvironmentOptions : public Options { | 178 | 178 | std::string redirect_warnings; | 179 | 179 | std::string diagnostic_dir; | 180 | 180 | std::string env_file; | | 181 | + std::string optional_env_file; | 181 | 182 | bool has_env_file_string = false; | 182 | 183 | bool test_runner = false; | 183 | 184 | uint64_t test_runner_concurrency = 0; | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-dotenv-edge-cases.js +45-9Lines changed: 45 additions & 9 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,30 +10,53 @@ const validEnvFilePath = '../fixtures/dotenv/valid.env'; | 10 | 10 | const nodeOptionsEnvFilePath = '../fixtures/dotenv/node-options.env'; | 11 | 11 |
| 12 | 12 | describe('.env supports edge cases', () => { | 13 | | - | 14 | | - it('supports multiple declarations', async () => { | 15 | | - // process.env.BASIC is equal to `basic` because the second .env file overrides it. | | 13 | + it('supports multiple declarations, including optional ones', async () => { | 16 | 14 | const code = ` | 17 | 15 | const assert = require('assert'); | 18 | 16 | assert.strictEqual(process.env.BASIC, 'basic'); | 19 | 17 | assert.strictEqual(process.env.NODE_NO_WARNINGS, '1'); | 20 | 18 | `.trim(); | | 19 | + const children = await Promise.all(Array.from({ length: 4 }, (_, i) => | | 20 | + common.spawnPromisified( | | 21 | + process.execPath, | | 22 | + [ | | 23 | + // Bitwise AND to create all 4 possible combinations: | | 24 | + // i & 0b01 is truthy when i has value 0bx1 (i.e. 0b01 (1) and 0b11 (3)), falsy otherwise. | | 25 | + // i & 0b10 is truthy when i has value 0b1x (i.e. 0b10 (2) and 0b11 (3)), falsy otherwise. | | 26 | + `${i & 0b01 ? '--env-file' : '--env-file-if-exists'}=${nodeOptionsEnvFilePath}`, | | 27 | + `${i & 0b10 ? '--env-file' : '--env-file-if-exists'}=${validEnvFilePath}`, | | 28 | + '--eval', code, | | 29 | + ], | | 30 | + { cwd: __dirname }, | | 31 | + ))); | | 32 | + assert.deepStrictEqual(children, Array.from({ length: 4 }, () => ({ | | 33 | + code: 0, | | 34 | + signal: null, | | 35 | + stdout: '', | | 36 | + stderr: '', | | 37 | + }))); | | 38 | + }); | | 39 | + | | 40 | + it('supports absolute paths', async () => { | | 41 | + const code = ` | | 42 | + require('assert').strictEqual(process.env.BASIC, 'basic'); | | 43 | + `.trim(); | 21 | 44 | const child = await common.spawnPromisified( | 22 | 45 | process.execPath, | 23 | | - [ `--env-file=${nodeOptionsEnvFilePath}`, `--env-file=${validEnvFilePath}`, '--eval', code ], | 24 | | - { cwd: __dirname }, | | 46 | + [ `--env-file=${path.resolve(__dirname, validEnvFilePath)}`, '--eval', code ], | 25 | 47 | ); | 26 | 48 | assert.strictEqual(child.stderr, ''); | 27 | 49 | assert.strictEqual(child.code, 0); | 28 | 50 | }); | 29 | 51 |
| 30 | | - it('supports absolute paths', async () => { | | 52 | + it('supports a space instead of \'=\' for the flag ', async () => { | 31 | 53 | const code = ` | 32 | 54 | require('assert').strictEqual(process.env.BASIC, 'basic'); | 33 | 55 | `.trim(); | 34 | 56 | const child = await common.spawnPromisified( | 35 | 57 | process.execPath, | 36 | | - [ `--env-file=${path.resolve(__dirname, validEnvFilePath)}`, '--eval', code ], | | 58 | + [ '--env-file', validEnvFilePath, '--eval', code ], | | 59 | + { cwd: __dirname }, | 37 | 60 | ); | 38 | 61 | assert.strictEqual(child.stderr, ''); | 39 | 62 | assert.strictEqual(child.code, 0); | @@ -48,10 +71,23 @@ describe('.env supports edge cases', () => { | 48 | 71 | [ '--env-file=.env', '--eval', code ], | 49 | 72 | { cwd: __dirname }, | 50 | 73 | ); | 51 | | - assert.notStrictEqual(child.stderr.toString(), ''); | | 74 | + assert.notStrictEqual(child.stderr, ''); | 52 | 75 | assert.strictEqual(child.code, 9); | 53 | 76 | }); | 54 | 77 |
| | 78 | + it('should handle non-existent optional .env file', async () => { | | 79 | + const code = ` | | 80 | + require('assert').strictEqual(1,1); | | 81 | + `.trim(); | | 82 | + const child = await common.spawnPromisified( | | 83 | + process.execPath, | | 84 | + ['--env-file-if-exists=.env', '--eval', code], | | 85 | + { cwd: __dirname }, | | 86 | + ); | | 87 | + assert.notStrictEqual(child.stderr, ''); | | 88 | + assert.strictEqual(child.code, 0); | | 89 | + }); | | 90 | + | 55 | 91 | it('should not override existing environment variables but introduce new vars', async () => { | 56 | 92 | const code = ` | 57 | 93 | require('assert').strictEqual(process.env.BASIC, 'existing'); | @@ -106,7 +142,7 @@ describe('.env supports edge cases', () => { | 106 | 142 | '--eval', `require('assert').strictEqual(process.env.BASIC, undefined);`, | 107 | 143 | '--', '--env-file', validEnvFilePath, | 108 | 144 | ], | 109 | | - { cwd: fixtures.path('dotenv') }, | | 145 | + { cwd: __dirname }, | 110 | 146 | ); | 111 | 147 | assert.strictEqual(child.stdout, ''); | 112 | 148 | assert.strictEqual(child.stderr, ''); | |
|