├── LICENSE ├── Package.swift ├── README.md └── Sources ├── DotEnv └── DotEnv.swift └── DotEnvTests └── DotEnvTests.swift /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Rob Allen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftDotEnv", 7 | products: [ 8 | .library( 9 | name: "SwiftDotEnv", 10 | targets: ["DotEnv"]), 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .target( 15 | name: "DotEnv", 16 | dependencies: [] 17 | ), 18 | .testTarget( 19 | name: "DotEnvTests", 20 | dependencies: ["DotEnv"] 21 | ) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftDotEnv 2 | 3 | | :warning: This repository is archived as I'm no longer using Swift on Linux and so maintaining isn't appropriate. | 4 | | --- | 5 | 6 | Swift 4 library that loads environment variables from `.env` into the environment inspired by the [Ruby dotenv][1] and [PHP dotenv][2] projects. 7 | 8 | [1]: https://github.com/bkeepers/dotenv 9 | [2]: https://github.com/vlucas/phpdotenv 10 | 11 | ## Why? 12 | 13 | Storing [configuration in the environment](http://12factor.net/config) is one of the tenets of a [twelve-factor app](http://12factor.net). Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. 14 | 15 | But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. SwiftDotEnv loads variables from a `.env` file into `ENV` when the environment is bootstrapped. 16 | 17 | ## Installation 18 | 19 | Install using [Swift Package Manager](https://swift.org/package-manager/). 20 | 21 | ## Swift version 22 | 23 | Version 2 of this module has been tested with Swift `4.0`. Version 1 has been tested with Swift `3.0` and `3.1`. 24 | 25 | ## Usage 26 | 27 | ```swift 28 | import DotEnv 29 | 30 | let env = DotEnv(withFile: ".env") 31 | 32 | let host = env.get("DB_HOST") ?? "localhost" 33 | let port = env.getAsInt("DB_PORT") ?? 3306 34 | let isEnabled = env.getAsBool("IS_ENABLED") ?? true 35 | ``` 36 | 37 | An example `.env` file would be: 38 | 39 | ``` 40 | DB_HOST=test.com 41 | DB_PORT=1234 42 | IS_ENABLED=0 43 | ``` 44 | 45 | There are three getter methods: 46 | 47 | * `get()` returns a `String?` 48 | * `getAsInt()` returns an `Int?` 49 | * `getAsBool()` returns a `Bool?` where case-insensitive `"true"`, `"yes"` and `"1"` evaluate to `true` 50 | 51 | You can also use subscript access to retrieve the string version: 52 | 53 | ```swift 54 | let host = env["DB_HOST"] ?? "localhost" 55 | ``` 56 | 57 | As a convenience, you can use `env.all()` to retrieve all environment variables. 58 | 59 | Note that the `.env` file is referenced relative to the directory where the binary is executed from. 60 | 61 | 62 | ## Caveats 63 | 64 | Currently has a very naive parser of the `.env` file and so doesn't support multi-line values. 65 | 66 | 67 | ## Contribute 68 | 69 | Contributions welcome! Please put your changes in a separate branch from master and raise a PR. 70 | -------------------------------------------------------------------------------- /Sources/DotEnv/DotEnv.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(Linux) 4 | import Glibc 5 | #else 6 | import Darwin 7 | #endif 8 | 9 | 10 | public struct DotEnv { 11 | 12 | public init(withFile path: String = ".env") { 13 | loadDotEnvFile(path: path) 14 | } 15 | 16 | /// 17 | /// Load .env file and put all the variables into the environment 18 | /// 19 | public func loadDotEnvFile(path: String) { 20 | 21 | var path = path.starts(with: "/") ? path : getAbsolutePath(relativePath: "/\(path)") 22 | if let path = path, let contents = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) { 23 | 24 | let lines = String(describing: contents).split { $0 == "\n" || $0 == "\r\n" }.map(String.init) 25 | for line in lines { 26 | // ignore comments 27 | if line[line.startIndex] == "#" { 28 | continue 29 | } 30 | 31 | // ignore lines that appear empty 32 | if line.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines).isEmpty { 33 | continue 34 | } 35 | 36 | // extract key and value which are separated by an equals sign 37 | let parts = line.split(separator: "=", maxSplits: 1).map(String.init) 38 | 39 | let key = parts[0].trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) 40 | var value = parts[1].trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) 41 | 42 | // remove surrounding quotes from value & convert remove escape character before any embedded quotes 43 | if value[value.startIndex] == "\"" && value[value.index(before: value.endIndex)] == "\"" { 44 | value.remove(at: value.startIndex) 45 | value.remove(at: value.index(before: value.endIndex)) 46 | value = value.replacingOccurrences(of:"\\\"", with: "\"") 47 | } 48 | setenv(key, value, 1) 49 | } 50 | } 51 | } 52 | 53 | /// 54 | /// Return the value for `name` in the environment, returning the default if not present 55 | /// 56 | public func get(_ name: String) -> String? { 57 | guard let value = getenv(name) else { 58 | return nil 59 | } 60 | return String(validatingUTF8: value) 61 | } 62 | 63 | /// 64 | /// Return the integer value for `name` in the environment, returning default if not present 65 | /// 66 | public func getAsInt(_ name: String) -> Int? { 67 | guard let value = get(name) else { 68 | return nil 69 | } 70 | return Int(value) 71 | } 72 | 73 | /// 74 | /// Return the boolean value for `name` in the environment, returning default if not present 75 | /// 76 | /// Note that the value is lowercaed and must be "true", "yes" or "1" to be considered true. 77 | /// 78 | public func getAsBool(_ name: String) -> Bool? { 79 | guard let value = get(name) else { 80 | return nil 81 | } 82 | 83 | // is it "true"? 84 | if ["true", "yes", "1"].contains(value.lowercased()) { 85 | return true 86 | } 87 | 88 | return false 89 | } 90 | 91 | /// 92 | /// Array subscript access to environment variables as it's cleaner 93 | /// 94 | public subscript(key: String) -> String? { 95 | get { 96 | return get(key) 97 | } 98 | } 99 | 100 | 101 | // Open 102 | public func all() -> [String: String] { 103 | return ProcessInfo.processInfo.environment 104 | } 105 | 106 | /// 107 | /// Determine absolute path of the given argument relative to the current 108 | /// directory 109 | /// 110 | private func getAbsolutePath(relativePath: String) -> String? { 111 | let fileManager = FileManager.default 112 | let currentPath = fileManager.currentDirectoryPath 113 | let filePath = currentPath + relativePath 114 | if fileManager.fileExists(atPath: filePath) { 115 | return filePath 116 | } else { 117 | return nil 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/DotEnvTests/DotEnvTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import DotEnv 4 | 5 | class DotEnvTests: XCTestCase { 6 | 7 | var env: DotEnv! 8 | 9 | override func setUp() { 10 | let path = "\(FileManager.default.currentDirectoryPath)/mock.env" 11 | let mockEnv = 12 | """ 13 | # example comment 14 | 15 | MOCK_STRING=helloMom 16 | MOCK_INT=42 17 | MOCK_BOOL=true 18 | """ 19 | FileManager.default.createFile(atPath: path, contents: mockEnv.data(using: .utf8), attributes: nil) 20 | env = DotEnv(withFile: "mock.env") 21 | } 22 | 23 | func test_get_returnsString() { 24 | let actualResult = env.get("MOCK_STRING") 25 | 26 | XCTAssertNotNil(actualResult) 27 | XCTAssertEqual(actualResult!, "helloMom") 28 | } 29 | 30 | func test_getAsInt_returnsInt() { 31 | let actualResult = env.getAsInt("MOCK_INT") 32 | 33 | XCTAssertNotNil(actualResult) 34 | XCTAssertEqual(actualResult!, 42) 35 | } 36 | 37 | func test_getAsBool_returnsBool() { 38 | let actualResult = env.getAsBool("MOCK_BOOL") 39 | 40 | XCTAssertNotNil(actualResult) 41 | XCTAssertTrue(actualResult!) 42 | } 43 | 44 | func test_comments_AreStripped() { 45 | let actualResult = env.get("# example comment") 46 | 47 | XCTAssertNil(actualResult) 48 | } 49 | 50 | func test_emptyLines_AreStripped() { 51 | let actualResult = env.get("\r\n") 52 | 53 | XCTAssertNil(actualResult) 54 | } 55 | 56 | func test_all_containsTestEnv() { 57 | let actual = env.all() 58 | 59 | XCTAssertTrue(actual.contains(where: { (key, _) -> Bool in 60 | return key == "MOCK_STRING" 61 | })) 62 | XCTAssertTrue(actual.contains(where: { (key, _) -> Bool in 63 | return key == "MOCK_INT" 64 | })) 65 | XCTAssertTrue(actual.contains(where: { (key, _) -> Bool in 66 | return key == "MOCK_BOOL" 67 | })) 68 | } 69 | } 70 | --------------------------------------------------------------------------------