Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions lib/dotenv.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
/// 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<String, String> parse(Iterable<String> 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'];
/// // ...
/// }
///
/// Verify required variables are present:
///
/// 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;
91 changes: 51 additions & 40 deletions lib/src/dotenv.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,76 @@ import 'dart:io';

import 'package:meta/meta.dart';

import 'parser.dart';
part 'parser.dart';

final _env = <String, String>{
...Platform.environment,
};

@visibleForTesting
Map<String, String> get loadedEnv => _env;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the loaded environment Map fully available (even if just read only) would help to not have to update any existing types in apps.
Since Platform.environment is a plain old Map<String, String> It would be preferred if we can get access to the raw map


/// Loads key-value pairs from a file into a [Map<String, String>].
///
/// 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 = <String, String>{};
/// 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<String, String> parse(Iterable<String> 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<String, String> get map => _map;
final DefaultParser _parser;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be of type 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<String, String> other) => _map.addAll(other);
void addAll(Map<String, String> 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],
/// and the value is non-empty. See [isDefined].
///
/// 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.
///
Expand All @@ -68,23 +84,18 @@ class DotEnv {
/// to the underlying [Map].
///
/// Logs to [stderr] if any file does not exist; see [quiet].
void load(
[Iterable<String> filenames = const ['.env'],
Parser psr = const Parser()]) {
void load([
Iterable<String> 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<String> _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();
}
}
13 changes: 8 additions & 5 deletions lib/src/parser.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'dart:io';
part of './dotenv.dart';

import 'package:meta/meta.dart';
abstract class Parser {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this serve as an interface. So it could be abstract interface class Parser

/// Parses .env file contents into a [Map<String, String>].
Map<String, String> parse(Iterable<String> 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';

Expand All @@ -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
Expand Down