From 97e175ed4281585eee561af54ba14668091b04d0 Mon Sep 17 00:00:00 2001 From: Andrei Lesnitsky Date: Sun, 5 May 2024 13:52:09 +0200 Subject: [PATCH] v5 proposal --- lib/dotenv.dart | 32 +++++++++++----- lib/src/dotenv.dart | 91 +++++++++++++++++++++++++-------------------- lib/src/parser.dart | 13 ++++--- 3 files changed, 82 insertions(+), 54 deletions(-) diff --git a/lib/dotenv.dart b/lib/dotenv.dart index ef29cc9..ecd1f9b 100644 --- a/lib/dotenv.dart +++ b/lib/dotenv.dart @@ -1,17 +1,31 @@ -/// Loads environment variables from a `.env` file. +/// Loads environment variables from a `.env` file and merges with +/// `Platform.environment`. /// -/// ## usage +/// ## usages /// -/// Call [DotEnv.load] to parse the file(s). -/// Read variables from the underlying [Map] using the `[]` operator. +/// Use the default instance of [DotEnv] to read env vars: +/// +/// import 'package:dotenv/dotenv.dart'; +/// +/// void main() { +/// final myVar = DotEnv.instance['MY_VAR']; +/// } +/// +/// Use `DotEnv(parser)..load()` to create a `DotEnv` instance +/// with a custom parser: /// /// import 'package:dotenv/dotenv.dart'; /// +/// class CustomParser implements Parser { +/// Map parse(Iterable lines) { +/// // custom implementation +/// } +/// } +/// /// void main() { -/// var env = DotEnv(includePlatformEnvironment: true) -/// ..load('path/to/my/.env'); -/// var foo = env['foo']; -/// var homeDir = env['HOME']; +/// final env = DotEnv(CustomParser())..load(); +/// final foo = env['foo']; +/// final homeDir = env['HOME']; /// // ... /// } /// @@ -19,4 +33,4 @@ /// /// const _requiredEnvVars = ['host', 'port']; /// bool get hasEnv => env.isEveryDefined(_requiredEnvVars); -export 'package:dotenv/src/dotenv.dart'; +export 'package:dotenv/src/dotenv.dart' show DotEnv, Parser; diff --git a/lib/src/dotenv.dart b/lib/src/dotenv.dart index bfac82e..11646b7 100644 --- a/lib/src/dotenv.dart +++ b/lib/src/dotenv.dart @@ -4,47 +4,63 @@ import 'dart:io'; import 'package:meta/meta.dart'; -import 'parser.dart'; +part 'parser.dart'; + +final _env = { + ...Platform.environment, +}; + +@visibleForTesting +Map get loadedEnv => _env; /// Loads key-value pairs from a file into a [Map]. /// /// The parser will attempt to handle simple variable substitution, /// respect single- vs. double-quotes, and ignore `#comments` or the `export` keyword. class DotEnv { - /// If true, the underlying map will contain the entries of [Platform.environment], - /// even after calling [clear]. - /// Otherwise, it will be empty until populated by [load]. - final bool includePlatformEnvironment; + static late DotEnv _instance; + static bool _isInitialized = false; - /// If true, suppress "file not found" messages on [stderr] during [load]. - final bool quiet; - - final _map = {}; + /// Returns an instance of [DotEnv] with default [DefaultParser]. + /// + /// ```dart + /// final myVar = DotEnv.instance['MY_VAR']; + /// ``` + /// + /// If you need a custom .env file parser, create a new instance of [DotEnv]. + /// + /// ```dart + /// class CustomParser implements Parser { + /// Map parse(Iterable lines) { + /// // custom implementation + /// } + /// } + /// + /// final env = DotEnv(CustomParser())..load(); + static DotEnv get instance { + if (!_isInitialized) { + _instance = const DotEnv()..load(); + _isInitialized = true; + } - DotEnv({this.includePlatformEnvironment = false, this.quiet = false}) { - if (includePlatformEnvironment) _addPlatformEnvironment(); + return _instance; } - /// Provides access to the underlying [Map]. - /// - /// Prefer using [operator[]] to read values. - @visibleForTesting - Map get map => _map; + final DefaultParser _parser; + + const DotEnv([this._parser = const DefaultParser()]); /// Reads the value for [key] from the underlying map. /// /// Returns `null` if [key] is absent. See [isDefined]. - String? operator [](String key) => _map[key]; + String? operator [](String key) => _env[key]; /// Calls [Map.addAll] on the underlying map. - void addAll(Map other) => _map.addAll(other); + void addAll(Map other) => _env.addAll(other); /// Calls [Map.clear] on the underlying map. - /// - /// If [includePlatformEnvironment] is true, the entries of [Platform.environment] will be reinserted. void clear() { - _map.clear(); - if (includePlatformEnvironment) _addPlatformEnvironment(); + _env.clear(); } /// Equivalent to [operator []] when the underlying map contains [key], @@ -52,12 +68,12 @@ class DotEnv { /// /// Otherwise, calls [orElse] and returns the value. String getOrElse(String key, String Function() orElse) => - isDefined(key) ? _map[key]! : orElse(); + isDefined(key) ? _env[key]! : orElse(); /// True if [key] has a nonempty value in the underlying map. /// /// Differs from [Map.containsKey] by excluding empty values. - bool isDefined(String key) => _map[key]?.isNotEmpty ?? false; + bool isDefined(String key) => _env[key]?.isNotEmpty ?? false; /// True if all supplied [vars] have nonempty value; false otherwise. /// @@ -68,23 +84,18 @@ class DotEnv { /// to the underlying [Map]. /// /// Logs to [stderr] if any file does not exist; see [quiet]. - void load( - [Iterable filenames = const ['.env'], - Parser psr = const Parser()]) { + void load([ + Iterable filenames = const ['.env'], + ]) { for (var filename in filenames) { - var f = File.fromUri(Uri.file(filename)); - var lines = _verify(f); - _map.addAll(psr.parse(lines)); - } - } - - void _addPlatformEnvironment() => _map.addAll(Platform.environment); - - List _verify(File f) { - if (!f.existsSync()) { - if (!quiet) stderr.writeln('[dotenv] Load failed: file not found: $f'); - return []; + final uri = Uri.file(filename); + final f = File.fromUri(uri); + + if (f.existsSync()) { + final content = f.readAsLinesSync(); + final parsed = _parser.parse(content); + _env.addAll(parsed); + } } - return f.readAsLinesSync(); } } diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 21d4930..2899811 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -1,10 +1,13 @@ -import 'dart:io'; +part of './dotenv.dart'; -import 'package:meta/meta.dart'; +abstract class Parser { + /// Parses .env file contents into a [Map]. + Map parse(Iterable lines); +} /// Creates key-value pairs from strings formatted as environment /// variable definitions. -class Parser { +class DefaultParser implements Parser { static const _singleQuot = "'"; static const _keyword = 'export'; @@ -13,8 +16,8 @@ class Parser { static final _bashVar = new RegExp(r'(?:\\)?(\$)(?:{)?([a-zA-Z_][\w]*)+(?:})?'); - /// [Parser] methods are pure functions. - const Parser(); + /// [DefaultParser] methods are pure functions. + const DefaultParser(); /// Substitutes $bash_vars in [val] with values from [env]. @visibleForTesting