mirror of
https://github.com/java-james/flutter_dotenv.git
synced 2025-08-26 02:41:04 +08:00
feat: merge with platform environment
This commit is contained in:
@ -1,16 +1,28 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_dotenv/src/parser.dart';
|
||||
|
||||
import 'errors.dart';
|
||||
|
||||
/// Loads environment variables from a `.env` file.
|
||||
///
|
||||
/// ## usage
|
||||
///
|
||||
/// Once you call [load] or the factory constructor with a valid env, the top-level [env] map is available.
|
||||
/// Once you call [load], the top-level [env] map is available.
|
||||
/// You may wish to prefix the import.
|
||||
///
|
||||
/// import 'package:flutter_dotenv/flutter_dotenv.dart' show load, env;
|
||||
/// import 'package:dotenv/dotenv.dart' show load, env;
|
||||
///
|
||||
/// void main() {
|
||||
/// await DotEnv().load('.env');
|
||||
/// runApp(App());
|
||||
/// var x = DotEnv().env['foo'];
|
||||
/// load();
|
||||
/// var x = env['foo'];
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
@ -18,77 +30,46 @@
|
||||
///
|
||||
/// const _requiredEnvVars = const ['host', 'port'];
|
||||
/// bool get hasEnv => isEveryDefined(_requiredEnvVars);
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import './parser.dart';
|
||||
|
||||
///
|
||||
/// ## usage
|
||||
///
|
||||
/// Future main() async {
|
||||
/// await DotEnv().load('.env');
|
||||
/// //...runapp
|
||||
/// }
|
||||
///
|
||||
/// Verify required variables are present:
|
||||
///
|
||||
/// const _requiredEnvVars = const ['host', 'port'];
|
||||
/// bool get hasEnv => isEveryDefined(_requiredEnvVars);
|
||||
var _isInitialized = false;
|
||||
var _envMap = Map<String, String>.from(Platform.environment);
|
||||
|
||||
class DotEnv {
|
||||
Map<String, String> _env = <String, String>{};
|
||||
static DotEnv _singleton;
|
||||
|
||||
Map<String, String> get env {
|
||||
if (_env.isEmpty) {
|
||||
stderr.writeln(
|
||||
'[flutter_dotenv] No env values found. Make sure you have called DotEnv.load()');
|
||||
}
|
||||
return _env;
|
||||
/// A copy of [Platform.environment](dart:io) including variables loaded at runtime from a file.
|
||||
Map<String, String> get env {
|
||||
if(!_isInitialized) {
|
||||
throw NotInitializedError();
|
||||
}
|
||||
return _envMap;
|
||||
}
|
||||
|
||||
/// Clear [env] and optionally overwrite with a new writable copy of [Platform.environment](dart:io).
|
||||
Map clean({ bool retainPlatformEnvironment = true}) => _envMap = Map.from(retainPlatformEnvironment ? Platform.environment : {});
|
||||
|
||||
/// Loads environment variables from the env file into a map
|
||||
Future load({String fileName = '.env', Parser parser = const Parser(), bool includePlatformEnvironment = true}) async {
|
||||
clean(retainPlatformEnvironment: includePlatformEnvironment);
|
||||
final allLines = await _getEntriesFromFile(fileName);
|
||||
final envEntries = parser.parse(allLines);
|
||||
_envMap.addAll(envEntries);
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
/// True if all supplied variables have nonempty value; false otherwise.
|
||||
/// Differs from [containsKey](dart:core) by excluding null values.
|
||||
/// Note [load] should be called first.
|
||||
bool isEveryDefined(Iterable<String> vars) =>
|
||||
vars.every((k) => _envMap[k] != null && _envMap[k].isNotEmpty);
|
||||
|
||||
|
||||
Future<List<String>> _getEntriesFromFile(String filename) async {
|
||||
try {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
var envString = await rootBundle.loadString(filename);
|
||||
if (envString.isEmpty) {
|
||||
throw EmptyEnvFileError();
|
||||
}
|
||||
return envString.split('\n');
|
||||
} on FlutterError {
|
||||
throw FileNotFoundError();
|
||||
}
|
||||
|
||||
set env(Map<String, String> env) {
|
||||
_env = env;
|
||||
}
|
||||
|
||||
/// True if all supplied variables have nonempty value; false otherwise.
|
||||
/// Differs from [containsKey](dart:core) by excluding null values.
|
||||
/// Note [load] should be called first.
|
||||
bool isEveryDefined(Iterable<String> vars) =>
|
||||
vars.every((k) => env[k] != null && env[k].isNotEmpty);
|
||||
|
||||
/// Read environment variables from [filename] and add them to [env].
|
||||
/// Logs to [stderr] if [filename] does not exist.
|
||||
Future load([String filename = '.env', Parser psr = const Parser()]) async {
|
||||
var lines = await _verify(filename);
|
||||
_env.addAll(psr.parse(lines));
|
||||
}
|
||||
|
||||
Future<List<String>> _verify(String filename) async {
|
||||
try {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
var str = await rootBundle.loadString(filename);
|
||||
if (str.isNotEmpty) return str.split('\n');
|
||||
stderr.writeln('[flutter_dotenv] Load failed: file $filename was empty');
|
||||
} on FlutterError {
|
||||
stderr.writeln('[flutter_dotenv] Load failed: file not found');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
factory DotEnv({Map<String, String> env}) {
|
||||
if (_singleton == null) {
|
||||
_singleton = DotEnv._internal(env: env);
|
||||
}
|
||||
return _singleton;
|
||||
}
|
||||
|
||||
DotEnv._internal({Map<String, String> env})
|
||||
: _env = env ?? <String, String>{};
|
||||
}
|
||||
|
16
lib/src/errors.dart
Normal file
16
lib/src/errors.dart
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../flutter_dotenv.dart';
|
||||
|
||||
class NotInitializedError extends Error {}
|
||||
class ValueNotFound extends Error {}
|
||||
class FileNotFoundError extends Error {}
|
||||
class EmptyEnvFileError extends Error {}
|
@ -1,32 +1,35 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
/// Creates key-value pairs from strings formatted as environment
|
||||
/// variable definitions.
|
||||
class Parser {
|
||||
static const _singleQuot = "'";
|
||||
static const _keyword = 'export';
|
||||
|
||||
static final _comment = RegExp(r'''#.*(?:[^'"])$''');
|
||||
static final _surroundQuotes = RegExp(r'''^(['"])(.*)\1$''');
|
||||
static final _bashVar = RegExp(r'(?:\\)?(\$)(?:{)?([a-zA-Z_][\w]*)+(?:})?');
|
||||
|
||||
/// [Parser] methods are pure functions.
|
||||
const Parser();
|
||||
|
||||
/// Creates a [Map](dart:core)
|
||||
/// Creates a [Map](dart:core) suitable for merging into [Platform.environment](dart:io).
|
||||
/// Duplicate keys are silently discarded.
|
||||
Map<String, String> parse(Iterable<String> lines) {
|
||||
var out = <String, String>{};
|
||||
for (var line in lines) {
|
||||
lines.forEach((line) {
|
||||
var kv = parseOne(line, env: out);
|
||||
if (kv.isEmpty) continue;
|
||||
if (kv.isEmpty) return;
|
||||
out.putIfAbsent(kv.keys.single, () => kv.values.single);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Parses a single line into a key-value pair.
|
||||
@visibleForTesting
|
||||
Map<String, String> parseOne(String line,
|
||||
{Map<String, String> env = const {}}) {
|
||||
{Map<String, String> env: const {}}) {
|
||||
var stripped = strip(line);
|
||||
if (!_isValid(stripped)) return {};
|
||||
|
||||
@ -36,11 +39,25 @@ class Parser {
|
||||
if (k.isEmpty) return {};
|
||||
|
||||
var rhs = stripped.substring(idx + 1, stripped.length).trim();
|
||||
var quotChar = surroundingQuote(rhs);
|
||||
var v = unquote(rhs);
|
||||
|
||||
return {k: v};
|
||||
if (quotChar == _singleQuot) {
|
||||
return {k: v};
|
||||
}
|
||||
|
||||
return {k: interpolate(v, env)};
|
||||
}
|
||||
|
||||
/// Substitutes $bash_vars in [val] with values from [env].
|
||||
@visibleForTesting
|
||||
String interpolate(String val, Map<String, String> env) =>
|
||||
val.replaceAllMapped(_bashVar, (m) {
|
||||
var k = m.group(2);
|
||||
if (!_has(env, k)) return _tryPlatformEnv(k);
|
||||
return env[k];
|
||||
});
|
||||
|
||||
/// If [val] is wrapped in single or double quotes, returns the quote character.
|
||||
/// Otherwise, returns the empty string.
|
||||
@visibleForTesting
|
||||
@ -63,4 +80,13 @@ class Parser {
|
||||
String swallow(String line) => line.replaceAll(_keyword, '').trim();
|
||||
|
||||
bool _isValid(String s) => s.isNotEmpty && s.contains('=');
|
||||
}
|
||||
|
||||
/// [null] is a valid value in a Dart map, but the env var representation is empty string, not the string 'null'
|
||||
bool _has(Map<String, String> map, String key) =>
|
||||
map.containsKey(key) && map[key] != null;
|
||||
|
||||
String _tryPlatformEnv(String key) {
|
||||
if (!_has(Platform.environment, key)) return '';
|
||||
return Platform.environment[key];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user