├── .editorconfig ├── .github └── workflows │ ├── linux-ci.yml │ └── release-version.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── logo-small.png ├── shard.yml ├── spec ├── fixtures │ ├── app.env.test.local │ ├── load_from_path │ │ ├── .env │ │ ├── .env.local │ │ ├── .env.production.local │ │ └── .env.test │ ├── overwrite.env │ └── parse_sample.env ├── poncho │ ├── loader_spec.cr │ └── parser_spec.cr ├── poncho_spec.cr └── spec_helper.cr └── src ├── poncho.cr └── poncho ├── loader.cr ├── parser.cr └── version.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/linux-ci.yml: -------------------------------------------------------------------------------- 1 | name: Linux CI 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | pull_request: 7 | branches: "*" 8 | 9 | jobs: 10 | specs: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | crystal: [ '1.0.0', 'latest', 'nightly' ] 16 | name: Crystal ${{ matrix.crystal }} tests 17 | steps: 18 | - uses: actions/checkout@master 19 | - uses: oprypin/install-crystal@v1 20 | with: 21 | crystal: ${{ matrix.crystal }} 22 | - name: Install dependencies 23 | run: shards install 24 | - name: Run tests 25 | run: crystal spec --error-on-warnings --error-trace 26 | - name: Run code format check 27 | run: | 28 | if ! crystal tool format --check; then 29 | crystal tool format 30 | git diff 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /.github/workflows/release-version.yml: -------------------------------------------------------------------------------- 1 | name: Deploy new release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Create Release 13 | id: create_release 14 | uses: actions/create-release@v1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | with: 18 | tag_name: ${{ github.ref }} 19 | release_name: Release ${{ github.ref }} 20 | draft: false 21 | prerelease: false 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in application that uses them 9 | /shard.lock 10 | .aider* 11 | .env 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## TODO 11 | 12 | - [ ] Parser 13 | - [x] core engine 14 | - [ ] support parse to array 15 | - [ ] support parse to hash 16 | - [ ] support parse to json 17 | - [ ] support parse to yaml 18 | - [x] Loader 19 | - [x] Load to `ENV` 20 | - [ ] Writer 21 | - [ ] store to file 22 | 23 | ## [0.4.1] (2024-11-12) 24 | 25 | ### Fixed 26 | 27 | - Fix for pure empty value. [#9](https://github.com/icyleaf/poncho/pull/9) [#11](https://github.com/icyleaf/poncho/pull/11) 28 | 29 | ## [0.4.0] (2021-03-25) 30 | 31 | ### Fixed 32 | 33 | - Compatibility with Crystal 0.31 34 | 35 | ## [0.3.0] (2018-08-22) 36 | 37 | ## Added 38 | 39 | - Add variables in value support:sparkles:! [#6](https://github.com/icyleaf/poncho/pull/6) 40 | 41 | ## [0.2.0] (2018-08-20) 42 | 43 | ### Added 44 | 45 | #### Parser 46 | 47 | - Add new parameter `overwrite : Bool` to `#parse` method, by default `false` means that it sets value once with same key. 48 | - Add `#parse!` method to parse dotenv and overwrite the value with same key, sames as `#parse(true)`. 49 | 50 | #### Loader 51 | 52 | Supportd now! 53 | 54 | - Support single file with enviroment name and following searching file by orders. 55 | - Support single path and following searching file by orders. 56 | - Support multiple files (ignore searching orders and environment name). 57 | - Support overwrite existing environment variables 58 | 59 | See [#3](https://github.com/icyleaf/poncho/pull/3). 60 | 61 | ### Changed 62 | 63 | - Change behavior with `Poncho::Parser.new` it does **NOT** parse automatic, you need call `#parse` method. 64 | 65 | ### Fixed 66 | 67 | - Fix snakecase each part split by "_" do not format full key. `ABC_name` => `A_B_C_NAME` => "ABC_NAME" 68 | 69 | ## [0.1.1] (2018-07-27) 70 | 71 | ### Added 72 | 73 | - Added `#to_h` methods. 74 | 75 | ## [0.1.0] (2018-07-24) 76 | 77 | :star2:First beta version.:star2: 78 | 79 | [Unreleased]: https://github.com/icyleaf/poncho/compare/v0.4.1...HEAD 80 | [0.4.1]: https://github.com/icyleaf/poncho/compare/v0.4.0...v0.4.1 81 | [0.4.0]: https://github.com/icyleaf/poncho/compare/v0.3.0...v0.4.0 82 | [0.3.0]: https://github.com/icyleaf/poncho/compare/v0.2.0...v0.3.0 83 | [0.2.0]: https://github.com/icyleaf/poncho/compare/v0.1.1...v0.2.0 84 | [0.1.1]: https://github.com/icyleaf/poncho/compare/v0.1.0...v0.1.1 85 | [0.1.0]: https://github.com/icyleaf/poncho/compare/04d17738bcb7c15000ae56fea6c72157a96edfc4...v0.1.0 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 icyleaf 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![poncho-logo](https://github.com/icyleaf/poncho/raw/master/logo-small.png) 2 | 3 | # Poncho 4 | 5 | [![Language](https://img.shields.io/badge/language-crystal-776791.svg)](https://github.com/crystal-lang/crystal) 6 | [![Tag](https://img.shields.io/github/tag/icyleaf/poncho.svg)](https://github.com/icyleaf/poncho/blob/master/CHANGELOG.md) 7 | [![Linux CI](https://github.com/icyleaf/poncho/actions/workflows/linux-ci.yml/badge.svg?branch=master)](https://github.com/icyleaf/poncho/actions/workflows/linux-ci.yml) 8 | 9 | A .env parser/loader improved for performance. Poncho Icon by lastspark from [Noun Project](https://thenounproject.com). 10 | 11 | 12 | 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Parse](#parse) 16 | - [Rules](#rules) 17 | - [Overrides](#overrides) 18 | - [Examples](#examples) 19 | - [Load](#load) 20 | - [Orders](#orders) 21 | - [Overrides](#overrides-1) 22 | - [Examples](#examples-1) 23 | - [Best solution](#best-solution) 24 | - [Donate](#donate) 25 | - [How to Contribute](#how-to-contribute) 26 | - [You may also like](#you-may-also-like) 27 | - [License](#license) 28 | 29 | 30 | 31 | ## Installation 32 | 33 | Add this to your application's `shard.yml`: 34 | 35 | ```yaml 36 | dependencies: 37 | poncho: 38 | github: icyleaf/poncho 39 | ``` 40 | 41 | ## Usage 42 | 43 | Add your application configuration to your `.env` file in the root of your project: 44 | 45 | ```bash 46 | MYSQL_HOST=localhost 47 | MYSQL_PORT=3306 48 | MYSQL_DATABASE=poncho 49 | MYSQL_USER=poncho 50 | MYSQL_PASSWORD=74e10b72-33b1-434b-a476-cfee0faa7d75 51 | ``` 52 | 53 | Now you can parse or load it. 54 | 55 | ### Parse 56 | 57 | Poncho parses the contents of your file containing environment variables is available to use. 58 | It accepts a String or IO and will return an `Hash` with the parsed keys and values. 59 | 60 | #### Rules 61 | 62 | Poncho parser currently supports the following rules: 63 | 64 | - Skipped the empty line and comment(`#`). 65 | - Ignore the comment which after (`#`). 66 | - `ENV=development` becomes `{"ENV" => "development"}`. 67 | - Snakecase and upcase the key: `dbName` becomes `DB_NAME`, `DB_NAME` becomes `DB_NAME` 68 | - Support variables in value. `$NAME` or `${NAME}`. 69 | - Whitespace is removed from both ends of the value. `NAME = foo ` becomes`{"NAME" => "foo"}` 70 | - New lines are expanded if in double quotes. `MULTILINE="new\nline"` becomes `{"MULTILINE" => "new\nline"}` 71 | - Inner quotes are maintained (such like `Hash`/`JSON`). `JSON={"foo":"bar"}` becomes `{"JSON" => "{\"foo\":\"bar\"}"}` 72 | - Empty values become empty strings. 73 | - Single and double quoted values are escaped. 74 | - Overwrite optional (default is non-overwrite). 75 | - Support variables in value. `WELCOME="hello $NAME"` becomes `{"WELCOME" => "hello foo"}` 76 | 77 | #### Overrides 78 | 79 | By default, Poncho won't overwrite existing environment variables as dotenv assumes the deployment environment 80 | has more knowledge about configuration than the application does. 81 | To overwrite existing environment variables you can use `Poncho.parse!(string_or_io)` / 82 | `Poncho.from_file(file, overwrite: true)` and `Poncho.parse(string_or_io, overwrite: true)`. 83 | 84 | #### Examples 85 | 86 | ```crystal 87 | require "poncho" 88 | # Or only import parser 89 | require "poncho/parser" 90 | 91 | poncho = Poncho.from_file ".env" 92 | # or 93 | poncho = Poncho.parse("ENV=development\nENV=production") 94 | poncho["ENV"] # => "development" 95 | 96 | # Overwrite value with exists key 97 | poncho = Poncho.parse!("ENV=development\nENV=production") 98 | poncho["ENV"] # => "production" 99 | ``` 100 | 101 | ### Load 102 | 103 | Poncho loads the environment file is easy to use, based on parser above. 104 | 105 | It accepts both single file (or path) and multiple files. 106 | 107 | #### Orders 108 | 109 | Poncho loads **single file** supports the following order with environment name (default is `development`): 110 | 111 | - `.env` - The Original® 112 | - `.env.development` - Environment-specific settings. 113 | - `.env.local` - Local overrides. This file is loaded for all environments except `test`. 114 | - `.env.development.local` - Local overrides of environment-specific settings. 115 | 116 | > **NO** effect with multiple files, it only loads the given files. 117 | 118 | #### Overrides 119 | 120 | By default, Poncho won't overwrite existing environment variables as dotenv assumes the deployment environment 121 | has more knowledge about configuration than the application does. 122 | To overwrite existing environment variables you can use `Poncho.load!(*files)` or `Poncho.load(*files, overwrite: true)`. 123 | 124 | #### Examples 125 | 126 | ```crystal 127 | require "poncho" 128 | # Or only import loader 129 | require "poncho/loader" 130 | 131 | # Load singe file 132 | # Searching order: .env.development, .env.local, .env.development.local 133 | Poncho.load ".env" 134 | 135 | # Load from path 136 | Poncho.load "config/" 137 | 138 | # Load production file 139 | # Searching order: .env, .env.production, .env.local, .env.production.local 140 | Poncho.load ".env", env: "production" 141 | 142 | # Load multiple files and overwrite value with exists key 143 | # note: ignore enviroment name. 144 | # Searching order: .env, .env.local 145 | Poncho.load! ".env", ".env.local", env: "test" 146 | ``` 147 | 148 | ## Best solution 149 | 150 | [Totem](https://github.com/icyleaf/totem) is here to help with that. Poncho was built-in to Totem to better with configuration. 151 | Configuration file formats is always the problem, you want to focus on building awesome things. 152 | 153 | ## How to Contribute 154 | 155 | Your contributions are always welcome! Please submit a pull request or create an issue to add a new question, bug or feature to the list. 156 | 157 | All [Contributors](https://github.com/icyleaf/poncho/graphs/contributors) are on the wall. 158 | 159 | ## You may also like 160 | 161 | - [halite](https://github.com/icyleaf/halite) - Crystal HTTP Requests Client with a chainable REST API, built-in sessions and middlewares. 162 | - [totem](https://github.com/icyleaf/totem) - Load and parse a configuration file or string in JSON, YAML, dotenv formats. 163 | - [markd](https://github.com/icyleaf/markd) - Yet another markdown parser built for speed, Compliant to CommonMark specification. 164 | - [popcorn](https://github.com/icyleaf/popcorn) - Easy and Safe casting from one type to another. 165 | - [fast-crystal](https://github.com/icyleaf/fast-crystal) - 💨 Writing Fast Crystal 😍 -- Collect Common Crystal idioms. 166 | 167 | ## License 168 | 169 | [MIT License](https://github.com/icyleaf/poncho/blob/master/LICENSE) © icyleaf 170 | -------------------------------------------------------------------------------- /logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/poncho/c6412131aeb8a309c8e771a3ee6fa5f7536cb427/logo-small.png -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: poncho 2 | version: 0.4.1 3 | 4 | authors: 5 | - icyleaf 6 | 7 | crystal: ">= 0.36.1, < 2.0.0" 8 | 9 | license: MIT 10 | -------------------------------------------------------------------------------- /spec/fixtures/app.env.test.local: -------------------------------------------------------------------------------- 1 | PONCHO_NAME="poncho" 2 | PONCHO_ENV="test" -------------------------------------------------------------------------------- /spec/fixtures/load_from_path/.env: -------------------------------------------------------------------------------- 1 | PONCHO_FROM=".env" 2 | 3 | -------------------------------------------------------------------------------- /spec/fixtures/load_from_path/.env.local: -------------------------------------------------------------------------------- 1 | PONCHO_FROM=".local" 2 | PONCHO_PATH="/Users/icyleaf/workspaces/poncho" -------------------------------------------------------------------------------- /spec/fixtures/load_from_path/.env.production.local: -------------------------------------------------------------------------------- 1 | PONCHO_FROM=".production.local" 2 | PONCHO_URL="poncho.example.com" 3 | PONCHO_MYSQL_HOST='poncho.example.com' 4 | PONCHO_MYSQL_PORT=3306 5 | PONCHO_MYSQL_DATABASE=poncho_production 6 | PONCHO_MYSQL_USER=poncho 7 | PONCHO_MYSQL_PASSWORD="p@nchO" -------------------------------------------------------------------------------- /spec/fixtures/load_from_path/.env.test: -------------------------------------------------------------------------------- 1 | PONCHO_FROM=".test" 2 | PONCHO_PATH="/home/ci/github/poncho" 3 | PONCHO_URL="localhost.test" 4 | PONCHO_MYSQL_HOST=localhost.test 5 | PONCHO_MYSQL_DATABASE=poncho_test -------------------------------------------------------------------------------- /spec/fixtures/overwrite.env: -------------------------------------------------------------------------------- 1 | PONCHO_NAME="foo" 2 | PONCHO_NAME="bar" 3 | PONCHO_name="overwrite" -------------------------------------------------------------------------------- /spec/fixtures/parse_sample.env: -------------------------------------------------------------------------------- 1 | # This is comment 2 | 3 | # COMMENTS=work 4 | BLANK='' 5 | STR=foo 6 | STR_WITH_COMMENTS=bar # str with comment 7 | STR_WITH_HASH_SYMBOL="abc#123"#stick comment 8 | INT =42 9 | FLOAT= 33.3 10 | BOOL_TRUE = 1 11 | BOOL_FALSE=0 12 | PROXIED={{STR}} 13 | SINGLE_QUOTES='single_quotes' 14 | DOUBLE_QUOTES="double_quotes" 15 | EXPAND_NEWLINES="expand\nnewlines" 16 | DONT_EXPAND_NEWLINES_1=dontexpand\nnewlines 17 | DONT_EXPAND_NEWLINES_2='dontexpand\nnewlines' 18 | lower_case=lower_case 19 | camelCase=camelCase 20 | URL=https://example.com/path?query=1 21 | UNDEFINED_EXPAND=$TOTALLY_UNDEFINED_ENV_KEY 22 | EQUAL_SIGNS=equals== 23 | INCLUDE_SPACE=some spaced out string 24 | USERNAME="user@example.com" 25 | 26 | SINGLE_VARIABLE=$STR 27 | MULTIPLE_VARIABLE1=$STR$INT 28 | MULTIPLE_VARIABLE2=$STR$INT1 29 | SINGLE_BLOCK_VARIABLE=${STR}${INT} 30 | SINGLE_QUOTES_VARIABLE='hello $STR!' 31 | DOUBLE_QUOTES_VARIABLE="hello ${STR}, my email is $USERNAME" 32 | UNQUOTED_VARIABLE=Hello there! 33 | EMPTY_VARIABLE= 34 | 35 | LIST_STR='foo,bar' 36 | LIST_STR_WITH_SPACES=' foo, bar' 37 | LIST_INT=1,2,3 38 | LIST_INT_WITH_SPACES=1, 2,3 39 | DICT_STR=key1=val1, key2=val2 40 | DICT_INT=key1=1, key2=2 41 | JSON='{"foo": "bar", "baz": [1, 2, 3]}' 42 | RETAIN_INNER_QUOTES={"foo": "bar"} 43 | RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' 44 | -------------------------------------------------------------------------------- /spec/poncho/loader_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Poncho::Loader do 4 | describe "loads single file" do 5 | describe "disable overwrite" do 6 | describe "with sample file" do 7 | loader = Poncho::Loader.new 8 | loader.load fixture_path("parse_sample.env") 9 | it_equal_group ENV.to_h 10 | clean_env "parse_sample.env" 11 | end 12 | 13 | describe "with overwrite file" do 14 | loader = Poncho::Loader.new 15 | loader.load fixture_path("overwrite.env") 16 | it_equal ENV.to_h, "PONCHO_NAME", "foo" 17 | clean_env "overwrite.env" 18 | end 19 | 20 | describe "with file and test env" do 21 | loader = Poncho::Loader.new env: "test" 22 | loader.load fixture_path("app.env") 23 | it_equal ENV.to_h, "PONCHO_NAME", "poncho" 24 | clean_env "app.env.test.local" 25 | end 26 | 27 | describe "with path" do 28 | loader = Poncho::Loader.new 29 | loader.load fixture_path("load_from_path") 30 | it_equal ENV.to_h, "PONCHO_FROM", ".env" 31 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 32 | clean_env "load_from_path/.env" 33 | clean_env "load_from_path/.env.local" 34 | end 35 | 36 | describe "with path and test env" do 37 | loader = Poncho::Loader.new env: "test" 38 | loader.load fixture_path("load_from_path") 39 | it_equal ENV.to_h, "PONCHO_FROM", ".env" 40 | it_equal ENV.to_h, "PONCHO_URL", "localhost.test" 41 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "localhost.test" 42 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_test" 43 | it_equal ENV.to_h, "PONCHO_PATH", "/home/ci/github/poncho" 44 | clean_env "load_from_path/.env" 45 | clean_env "load_from_path/.env.test" 46 | end 47 | 48 | describe "with path and production env" do 49 | loader = Poncho::Loader.new env: "production" 50 | loader.load fixture_path("load_from_path") 51 | it_equal ENV.to_h, "PONCHO_FROM", ".env" 52 | it_equal ENV.to_h, "PONCHO_URL", "poncho.example.com" 53 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "poncho.example.com" 54 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_production" 55 | it_equal ENV.to_h, "PONCHO_MYSQL_USER", "poncho" 56 | it_equal ENV.to_h, "PONCHO_MYSQL_PASSWORD", "p@nchO" 57 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 58 | clean_env "load_from_path/.env" 59 | clean_env "load_from_path/.env.production.local" 60 | end 61 | end 62 | end 63 | 64 | describe "enable overwrite" do 65 | describe "with overwrite file" do 66 | loader = Poncho::Loader.new 67 | loader.load! fixture_path("overwrite.env") 68 | it_equal ENV.to_h, "PONCHO_NAME", "overwrite" 69 | clean_env "overwrite.env" 70 | end 71 | 72 | describe "with path" do 73 | loader = Poncho::Loader.new 74 | loader.load! fixture_path("load_from_path") 75 | it_equal ENV.to_h, "PONCHO_FROM", ".local" 76 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 77 | clean_env "load_from_path/.env" 78 | clean_env "load_from_path/.env.local" 79 | end 80 | 81 | describe "with path and env" do 82 | loader = Poncho::Loader.new env: "test" 83 | loader.load! fixture_path("load_from_path") 84 | it_equal ENV.to_h, "PONCHO_FROM", ".local" 85 | it_equal ENV.to_h, "PONCHO_URL", "localhost.test" 86 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "localhost.test" 87 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_test" 88 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 89 | clean_env "load_from_path/.env" 90 | clean_env "load_from_path/.env.test" 91 | end 92 | 93 | describe "with path and production env" do 94 | loader = Poncho::Loader.new env: "production" 95 | loader.load! fixture_path("load_from_path") 96 | it_equal ENV.to_h, "PONCHO_FROM", ".production.local" 97 | it_equal ENV.to_h, "PONCHO_URL", "poncho.example.com" 98 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "poncho.example.com" 99 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_production" 100 | it_equal ENV.to_h, "PONCHO_MYSQL_USER", "poncho" 101 | it_equal ENV.to_h, "PONCHO_MYSQL_PASSWORD", "p@nchO" 102 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 103 | clean_env "load_from_path/.env" 104 | clean_env "load_from_path/.env.production.local" 105 | end 106 | end 107 | 108 | describe "loads multiple files" do 109 | describe "with non-overwrite" do 110 | loader = Poncho::Loader.new env: "development" 111 | loader.load fixture_path("overwrite.env"), fixture_path("app.env.test.local") 112 | it_equal ENV.to_h, "PONCHO_NAME", "foo" 113 | it_equal ENV.to_h, "PONCHO_ENV", "test" 114 | clean_env "overwrite.env" 115 | clean_env "app.env.test.local" 116 | end 117 | 118 | describe "with overwrite" do 119 | loader = Poncho::Loader.new env: "development" 120 | loader.load! fixture_path("overwrite.env"), fixture_path("app.env.test.local") 121 | it_equal ENV.to_h, "PONCHO_NAME", "poncho" 122 | it_equal ENV.to_h, "PONCHO_ENV", "test" 123 | clean_env "overwrite.env" 124 | clean_env "app.env.test.local" 125 | end 126 | 127 | describe "with env" do 128 | loader = Poncho::Loader.new env: "production" 129 | loader.load! fixture_path("load_from_path/.env"), fixture_path("load_from_path/.env.test") 130 | it_equal ENV.to_h, "PONCHO_FROM", ".test" 131 | it_equal ENV.to_h, "PONCHO_URL", "localhost.test" 132 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "localhost.test" 133 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_test" 134 | it_equal ENV.to_h, "PONCHO_PATH", "/home/ci/github/poncho" 135 | clean_env "load_from_path/.env" 136 | clean_env "load_from_path/.env.test" 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/poncho/parser_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Poncho::Parser do 4 | describe "loads from file" do 5 | describe ".from_file" do 6 | env = Poncho::Parser.from_file fixture_path("parse_sample.env") 7 | it_equal_group env 8 | end 9 | 10 | describe "#new" do 11 | env = Poncho::Parser.new File.open(fixture_path("parse_sample.env")) 12 | env.parse 13 | it_equal_group env 14 | end 15 | end 16 | 17 | describe "loads from raw string" do 18 | env = Poncho::Parser.new load_fixture("parse_sample.env") 19 | env.parse 20 | it_equal_group env 21 | end 22 | 23 | describe "gets" do 24 | describe "non-overwrite the key" do 25 | env = Poncho::Parser.from_file fixture_path("overwrite.env") 26 | it_equal env, "PONCHO_NAME", "foo" 27 | end 28 | 29 | describe "overwrite the key" do 30 | env = Poncho::Parser.from_file fixture_path("overwrite.env"), overwrite: true 31 | it_equal env, "PONCHO_NAME", "overwrite" 32 | end 33 | 34 | describe "#to_h" do 35 | env = Poncho::Parser.from_file fixture_path("parse_sample.env") 36 | env.to_h.should be_a Hash(String, String) 37 | it_equal_group env.to_h 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/poncho_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Poncho do 4 | describe ".parse" do 5 | describe "loads from file" do 6 | describe ".from_file" do 7 | env = Poncho.from_file fixture_path("parse_sample.env") 8 | it_equal_group env 9 | end 10 | 11 | describe "#new" do 12 | env = Poncho.parse File.open(fixture_path("parse_sample.env")) 13 | env.parse 14 | it_equal_group env 15 | end 16 | end 17 | 18 | describe "loads from raw string" do 19 | env = Poncho.parse load_fixture("parse_sample.env") 20 | env.parse 21 | it_equal_group env 22 | end 23 | 24 | describe "gets" do 25 | describe "non-overwrite the key" do 26 | env = Poncho.from_file fixture_path("overwrite.env") 27 | it_equal env, "PONCHO_NAME", "foo" 28 | end 29 | 30 | describe "overwrite the key" do 31 | env = Poncho.from_file fixture_path("overwrite.env"), true 32 | it_equal env, "PONCHO_NAME", "overwrite" 33 | end 34 | 35 | describe "#to_h" do 36 | env = Poncho.from_file fixture_path("parse_sample.env") 37 | env.to_h.should be_a Hash(String, String) 38 | it_equal_group env.to_h 39 | end 40 | end 41 | end 42 | 43 | describe ".load" do 44 | describe "loads single file" do 45 | describe "disable overwrite" do 46 | describe "with sample file" do 47 | Poncho.load fixture_path("parse_sample.env") 48 | it_equal_group ENV.to_h 49 | clean_env "parse_sample.env" 50 | end 51 | 52 | describe "with overwrite file" do 53 | Poncho.load fixture_path("overwrite.env") 54 | it_equal ENV.to_h, "PONCHO_NAME", "foo" 55 | clean_env "overwrite.env" 56 | end 57 | 58 | describe "with file and test env" do 59 | Poncho.load fixture_path("app.env"), env: "test" 60 | it_equal ENV.to_h, "PONCHO_NAME", "poncho" 61 | clean_env "app.env.test.local" 62 | end 63 | 64 | describe "with path" do 65 | Poncho.load fixture_path("load_from_path") 66 | it_equal ENV.to_h, "PONCHO_FROM", ".env" 67 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 68 | clean_env "load_from_path/.env" 69 | clean_env "load_from_path/.env.local" 70 | end 71 | 72 | describe "with path and test env" do 73 | Poncho.load fixture_path("load_from_path"), env: "test" 74 | it_equal ENV.to_h, "PONCHO_FROM", ".env" 75 | it_equal ENV.to_h, "PONCHO_URL", "localhost.test" 76 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "localhost.test" 77 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_test" 78 | it_equal ENV.to_h, "PONCHO_PATH", "/home/ci/github/poncho" 79 | clean_env "load_from_path/.env" 80 | clean_env "load_from_path/.env.test" 81 | end 82 | 83 | describe "with path and production env" do 84 | Poncho.load fixture_path("load_from_path"), env: "production" 85 | it_equal ENV.to_h, "PONCHO_FROM", ".env" 86 | it_equal ENV.to_h, "PONCHO_URL", "poncho.example.com" 87 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "poncho.example.com" 88 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_production" 89 | it_equal ENV.to_h, "PONCHO_MYSQL_USER", "poncho" 90 | it_equal ENV.to_h, "PONCHO_MYSQL_PASSWORD", "p@nchO" 91 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 92 | clean_env "load_from_path/.env" 93 | clean_env "load_from_path/.env.production.local" 94 | end 95 | end 96 | end 97 | 98 | describe "enable overwrite" do 99 | describe "with overwrite file" do 100 | Poncho.load! fixture_path("overwrite.env") 101 | it_equal ENV.to_h, "PONCHO_NAME", "overwrite" 102 | clean_env "overwrite.env" 103 | end 104 | 105 | describe "with path" do 106 | Poncho.load! fixture_path("load_from_path") 107 | it_equal ENV.to_h, "PONCHO_FROM", ".local" 108 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 109 | clean_env "load_from_path/.env" 110 | clean_env "load_from_path/.env.local" 111 | end 112 | 113 | describe "with path and env" do 114 | Poncho.load! fixture_path("load_from_path"), env: "test" 115 | it_equal ENV.to_h, "PONCHO_FROM", ".local" 116 | it_equal ENV.to_h, "PONCHO_URL", "localhost.test" 117 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "localhost.test" 118 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_test" 119 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 120 | clean_env "load_from_path/.env" 121 | clean_env "load_from_path/.env.test" 122 | end 123 | 124 | describe "with path and production env" do 125 | Poncho.load! fixture_path("load_from_path"), env: "production" 126 | it_equal ENV.to_h, "PONCHO_FROM", ".production.local" 127 | it_equal ENV.to_h, "PONCHO_URL", "poncho.example.com" 128 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "poncho.example.com" 129 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_production" 130 | it_equal ENV.to_h, "PONCHO_MYSQL_USER", "poncho" 131 | it_equal ENV.to_h, "PONCHO_MYSQL_PASSWORD", "p@nchO" 132 | it_equal ENV.to_h, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho" 133 | clean_env "load_from_path/.env" 134 | clean_env "load_from_path/.env.production.local" 135 | end 136 | end 137 | 138 | describe "loads multiple files" do 139 | describe "with non-overwrite" do 140 | Poncho.load fixture_path("overwrite.env"), fixture_path("app.env.test.local"), env: "development" 141 | it_equal ENV.to_h, "PONCHO_NAME", "foo" 142 | it_equal ENV.to_h, "PONCHO_ENV", "test" 143 | clean_env "overwrite.env" 144 | clean_env "app.env.test.local" 145 | end 146 | 147 | describe "with overwrite" do 148 | Poncho.load! fixture_path("overwrite.env"), fixture_path("app.env.test.local"), env: "development" 149 | it_equal ENV.to_h, "PONCHO_NAME", "poncho" 150 | it_equal ENV.to_h, "PONCHO_ENV", "test" 151 | clean_env "overwrite.env" 152 | clean_env "app.env.test.local" 153 | end 154 | 155 | describe "with env" do 156 | Poncho.load! fixture_path("load_from_path/.env"), fixture_path("load_from_path/.env.test"), env: "production" 157 | it_equal ENV.to_h, "PONCHO_FROM", ".test" 158 | it_equal ENV.to_h, "PONCHO_URL", "localhost.test" 159 | it_equal ENV.to_h, "PONCHO_MYSQL_HOST", "localhost.test" 160 | it_equal ENV.to_h, "PONCHO_MYSQL_DATABASE", "poncho_test" 161 | it_equal ENV.to_h, "PONCHO_PATH", "/home/ci/github/poncho" 162 | clean_env "load_from_path/.env" 163 | clean_env "load_from_path/.env.test" 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/poncho" 3 | 4 | def it_equal_group(data = nil, file = __FILE__, line = __LINE__) 5 | it_equal data, "BLANK", "", file, line 6 | it_equal data, "STR", "foo", file, line 7 | it_equal data, "STR_WITH_COMMENTS", "bar", file, line 8 | it_equal data, "STR_WITH_HASH_SYMBOL", "abc#123", file, line 9 | it_equal data, "INT", "42", file, line 10 | it_equal data, "FLOAT", "33.3", file, line 11 | it_equal data, "BOOL_TRUE", "1", file, line 12 | it_equal data, "BOOL_FALSE", "0", file, line 13 | it_equal data, "PROXIED", "{{STR}}", file, line 14 | it_equal data, "SINGLE_QUOTES", "single_quotes", file, line 15 | it_equal data, "DOUBLE_QUOTES", "double_quotes", file, line 16 | it_equal data, "EXPAND_NEWLINES", "expand\nnewlines", file, line 17 | it_equal data, "DONT_EXPAND_NEWLINES_1", "dontexpand\\nnewlines", file, line 18 | it_equal data, "DONT_EXPAND_NEWLINES_2", "dontexpand\\nnewlines", file, line 19 | it_equal data, "LOWER_CASE", "lower_case", file, line 20 | it_equal data, "CAMEL_CASE", "camelCase", file, line 21 | it_equal data, "LIST_STR", "foo,bar", file, line 22 | it_equal data, "LIST_STR_WITH_SPACES", " foo, bar", file, line 23 | it_equal data, "LIST_INT", "1,2,3", file, line 24 | it_equal data, "LIST_INT_WITH_SPACES", "1, 2,3", file, line 25 | it_equal data, "DICT_STR", "key1=val1, key2=val2", file, line 26 | it_equal data, "DICT_INT", "key1=1, key2=2", file, line 27 | it_equal data, "JSON", %Q{{"foo": "bar", "baz": [1, 2, 3]}}, file, line 28 | it_equal data, "URL", "https://example.com/path?query=1", file, line 29 | it_equal data, "UNDEFINED_EXPAND", "$TOTALLY_UNDEFINED_ENV_KEY", file, line 30 | it_equal data, "EQUAL_SIGNS", "equals==", file, line 31 | it_equal data, "RETAIN_INNER_QUOTES", %Q{{"foo": "bar"}}, file, line 32 | it_equal data, "RETAIN_INNER_QUOTES_AS_STRING", %Q{{"foo": "bar"}}, file, line 33 | it_equal data, "INCLUDE_SPACE", "some spaced out string", file, line 34 | it_equal data, "USERNAME", "user@example.com", file, line 35 | it_equal data, "SINGLE_VARIABLE", "foo", file, line 36 | it_equal data, "MULTIPLE_VARIABLE1", "foo42", file, line 37 | it_equal data, "MULTIPLE_VARIABLE2", "foo$INT1", file, line 38 | it_equal data, "SINGLE_BLOCK_VARIABLE", "foo42", file, line 39 | it_equal data, "SINGLE_QUOTES_VARIABLE", "hello $STR!", file, line 40 | it_equal data, "DOUBLE_QUOTES_VARIABLE", "hello foo, my email is user@example.com", file, line 41 | it_equal data, "UNQUOTED_VARIABLE", "Hello there!", file, line 42 | it_equal data, "EMPTY_VARIABLE", "", file, line 43 | end 44 | 45 | def it_equal(data, key, expected, file = __FILE__, line = __LINE__) 46 | it "gets #{key}", file, line do 47 | data[key].should eq expected 48 | end 49 | end 50 | 51 | def clean_env(filename) 52 | data = Poncho::Parser.from_file fixture_path(filename) 53 | data.each do |key, _| 54 | ENV.delete(key) 55 | end 56 | end 57 | 58 | def load_fixture(filename : String) 59 | File.read_lines(fixture_path(filename)).join("\n") 60 | end 61 | 62 | def fixture_path 63 | File.expand_path("../fixtures/", __FILE__) 64 | end 65 | 66 | def fixture_path(filename : String) 67 | File.join(fixture_path, filename) 68 | end 69 | -------------------------------------------------------------------------------- /src/poncho.cr: -------------------------------------------------------------------------------- 1 | require "./poncho/*" 2 | 3 | # Poncho is dotenv parser and loader improved for performance 4 | # 5 | # You can only require parser or loader, that is why it has two helpers extend to it. 6 | module Poncho 7 | end 8 | -------------------------------------------------------------------------------- /src/poncho/loader.cr: -------------------------------------------------------------------------------- 1 | require "./parser" 2 | 3 | module Poncho 4 | # `Poncho::Loader` is a .env file loader 5 | # 6 | # Poncho loads the environment file is easy to use. It accepts both single file (or path) and multiple files. 7 | # 8 | # ### Orders 9 | # 10 | # Poncho loads **single file** supports the following order with environment name (default is `development`): 11 | # 12 | # - `.env` - The Original® 13 | # - `.env.development` - Environment-specific settings. 14 | # - `.env.local` - Local overrides. This file is loaded for all environments except `test`. 15 | # - `.env.development.local` - Local overrides of environment-specific settings. 16 | # 17 | # > **NO** effect with multiple files, it only loads the given files. 18 | # 19 | # ### Overrides 20 | # 21 | # By default, Poncho won't overwrite existing environment variables as dotenv assumes the deployment environment 22 | # has more knowledge about configuration than the application does. 23 | # 24 | # ### Load single file 25 | # 26 | # ``` 27 | # loader = Poncho::Loader.new env: "development" 28 | # loader.load ".env" 29 | # ``` 30 | # 31 | # ### Load multiple files 32 | # 33 | # ``` 34 | # loader = Poncho::Loader.new 35 | # loader.load ".env", ".env.local" 36 | # ``` 37 | class Loader 38 | DEFAULT_ENV_NAME = "development" 39 | 40 | def initialize(@env : String? = nil) 41 | end 42 | 43 | # Load environment variables and overwrite existing ones. 44 | # 45 | # Same as `#load`(*files, overwrite: true) 46 | def load!(*paths) 47 | load(*paths, overwrite: true) 48 | end 49 | 50 | # Load environment variables 51 | def load(*paths, overwrite = false) 52 | if paths.size > 1 && paths.last.is_a?(Bool) 53 | overwrite = paths.last.as(Bool) 54 | end 55 | 56 | paths = paths.select { |f| f.is_a?(String) && !f.as(String).empty? } 57 | if paths.size > 1 58 | # Loads passed file in multiple files mode, ignore env name. 59 | paths.each do |path| 60 | load_to_env(path, overwrite) if File.exists?(path) 61 | end 62 | else 63 | # Loads orders files in single file mode. 64 | load_files(paths.first).each do |file| 65 | load_to_env(file, overwrite) 66 | end 67 | end 68 | end 69 | 70 | private def load_to_env(file : String, overwrite : Bool) 71 | Poncho::Parser.from_file(file, overwrite).each do |key, value| 72 | key_exists = ENV.has_key?(key) 73 | ENV[key] = value if !key_exists || (key_exists && overwrite) 74 | end 75 | end 76 | 77 | private def load_files(path : String) : Array(String) 78 | local_suffix = ".local" 79 | default_dotenv_name = ".env" 80 | env_name = @env || DEFAULT_ENV_NAME 81 | 82 | path = File.expand_path(path) 83 | filepath = Dir.exists?(path) ? path : File.dirname(path) 84 | filename = Dir.exists?(path) ? default_dotenv_name : File.basename(path) 85 | is_local_env = filename.ends_with?(local_suffix) 86 | Array(String).new.tap do |files| 87 | append_existed_file(files, filepath, filename) 88 | name = is_local_env ? "#{filename.gsub(local_suffix, "")}.#{env_name}" : "#{filename}.#{env_name}" 89 | append_existed_file(files, filepath, name) 90 | append_existed_file(files, filepath, "#{filename}#{local_suffix}") unless is_local_env 91 | append_existed_file(files, filepath, "#{filename}.#{env_name}#{local_suffix}") 92 | end 93 | end 94 | 95 | private def append_existed_file(files, path, name) 96 | if file = find_file?(path, name) 97 | files << file 98 | end 99 | end 100 | 101 | private def find_file?(path, name) : String? 102 | file = File.join(path, name) 103 | return file if File.exists?(file) 104 | end 105 | end 106 | 107 | # Poncho load helper 108 | # 109 | # ``` 110 | # require "poncho/loader" 111 | # ``` 112 | module LoaderHelper 113 | # Load environment variables and overwrite existing ones. 114 | # 115 | # Same as `#load`(*files, overwrite: true) 116 | # 117 | # ``` 118 | # # Searching order: .env.development, .env.local, .env.development.local 119 | # Poncho.load! ".env" 120 | # 121 | # # Load from path 122 | # Poncho.load! "config/" 123 | # 124 | # # Load multiple files, ignore enviroment name. 125 | # Poncho.load! ".env", ".env.local", env: "test" 126 | # ``` 127 | def load!(*files, env : String? = nil) 128 | load(*files, env: env, overwrite: true) 129 | end 130 | 131 | # Load environment variables 132 | # 133 | # ``` 134 | # # Searching order: .env.development, .env.local, .env.development.local 135 | # Poncho.load ".env" 136 | # 137 | # # Load from path 138 | # Poncho.load "config/" 139 | # 140 | # # Load multiple files, ignore enviroment name. 141 | # Poncho.load ".env", ".env.local", env: "test" 142 | # ``` 143 | def load(*files, env : String? = nil, overwrite = false) 144 | loader = Loader.new(env: env) 145 | loader.load(*files, overwrite: overwrite) 146 | loader 147 | end 148 | end 149 | 150 | extend LoaderHelper 151 | end 152 | -------------------------------------------------------------------------------- /src/poncho/parser.cr: -------------------------------------------------------------------------------- 1 | module Poncho 2 | # `Poncho::Parser` is a .env file parser 3 | # 4 | # Poncho parses the contents of your file containing environment variables is available to use. 5 | # It accepts a String or IO and will return an `Hash` with the parsed keys and values. 6 | # 7 | # ### Rules 8 | # 9 | # Poncho parser currently supports the following rules: 10 | # 11 | # - Skipped the empty line and comment(`#`). 12 | # - Ignore the comment which after (`#`). 13 | # - `ENV=development` becomes `{"ENV" => "development"}`. 14 | # - Converts camelcase boundaries to underscores and upcase the key: `dbName` becomes `DB_NAME`, `DB_NAME` becomes `DB_NAME` 15 | # - Support variables in value. `$NAME` or `${NAME}`. 16 | # - Whitespace is removed from both ends of the value. `NAME = foo ` becomes`{"NAME" => "foo"} 17 | # - New lines are expanded if in double quotes. `MULTILINE="new\nline"` becomes `{"MULTILINE" => "new\nline"} 18 | # - Inner quotes are maintained (such like `Hash`/`JSON`). `JSON={"foo":"bar"}` becomes `{"JSON" => "{\"foo\":\"bar\"}"} 19 | # - Empty values become empty strings. 20 | # - Single and double quoted values are escaped. 21 | # - Overwrite optional (default is non-overwrite). 22 | # - Support variables in value. `WELCOME="hello $NAME"` becomes `{"WELCOME" => "hello foo"}` 23 | # 24 | # ### Overrides 25 | # 26 | # By default, Poncho won't overwrite existing environment variables as dotenv assumes the deployment environment 27 | # has more knowledge about configuration than the application does. 28 | # To overwrite existing environment variables you can use `Poncho.parse!(string_or_io)` / 29 | # `Poncho.from_file(file, overwrite: true)` and `Poncho.parse(string_or_io, overwrite: true)`. 30 | # 31 | # ### Parse file 32 | # 33 | # ``` 34 | # parser = Poncho::Parser.from_file(".env") 35 | # parser["ENV"] # => "development" 36 | # ``` 37 | # 38 | # ### Parse raw string 39 | # 40 | # ``` 41 | # parser = Poncho::Parser.new("ENV=development\nDB_NAME=poncho\nENV=production") 42 | # parser.parse 43 | # parser["ENV"] # => "development" 44 | # 45 | # # Overwrite the key 46 | # parser.parse! 47 | # parser["ENV"] # => "production" 48 | # ``` 49 | class Parser 50 | def self.from_file(file : String, overwrite = false) 51 | parser = new(File.open(file)) 52 | parser.parse(overwrite) 53 | parser 54 | end 55 | 56 | @env = {} of String => String 57 | @vars = {} of String => Array(String) 58 | 59 | def initialize(@raw : String | IO) 60 | end 61 | 62 | # Parse environment variables and overwrite existing ones. 63 | # 64 | # Same as `#parse`(overwrite: true) 65 | def parse! 66 | parse(true) 67 | end 68 | 69 | # Parse environment variables 70 | def parse(overwrite = false) 71 | @raw.each_line do |line| 72 | next if line.blank? || !line.includes?('=') 73 | next unless expression = extract_expression(line) 74 | key, value = extract_env(expression) 75 | set_env(key, value, overwrite) 76 | end 77 | 78 | replace_variables 79 | end 80 | 81 | # Returns this collection as a plain Hash. 82 | def to_h : Hash(String, String) 83 | @env 84 | end 85 | 86 | forward_missing_to @env 87 | 88 | private def extract_env(expression : String) 89 | search_vars = true 90 | key, value = expression.split("=", 2).map { |v| v.strip } 91 | if !value.empty? 92 | if ['\'', '"'].includes?(value[0]) && ['\'', '"'].includes?(value[-1]) 93 | if value[0] == '"' && value[-1] == '"' 94 | value = value.gsub("\\n", "\n").gsub("\\r", "\r") 95 | else 96 | search_vars = false 97 | end 98 | 99 | value = value[1..-2] 100 | end 101 | end 102 | 103 | if search_vars && (vars = find_vars(value)) 104 | @vars[env_key(key)] = vars 105 | end 106 | 107 | [key, value] 108 | end 109 | 110 | private def set_env(key : String, value : String, overwrite : Bool) 111 | env_key = env_key(key) 112 | key_existes = @env.has_key?(env_key) 113 | if !key_existes || (key_existes && overwrite) 114 | @env[env_key] = value 115 | end 116 | end 117 | 118 | private def env_key(key : String) 119 | key.underscore.upcase 120 | end 121 | 122 | private def replace_variables 123 | @vars.each do |key, vars| 124 | replaced = false 125 | 126 | value = @env[key] 127 | vars.each do |var| 128 | var_key = var[1..-1] 129 | if var_key[0] == '{' && var_key[-1] == '}' 130 | var_key = var_key[1..-2] 131 | end 132 | 133 | if var_value = @env[env_key(var_key)]? 134 | value = value.sub(var, var_value) 135 | replaced = true 136 | end 137 | end 138 | 139 | @env[key] = value if replaced 140 | end 141 | end 142 | 143 | private def find_vars(value : String) 144 | return unless starts_at = value.index('$') 145 | vars = value[(starts_at + 1)..-1].split('$') 146 | Array(String).new.tap do |obj| 147 | vars.each do |var| 148 | brace_open = false 149 | value = String.build do |io| 150 | io << '$' 151 | var.each_char do |char| 152 | brace_open = true if char == '{' 153 | io << char if var_name_valid?(char) 154 | if brace_open && char == '}' 155 | break 156 | end 157 | end 158 | end 159 | 160 | obj << value 161 | end 162 | end 163 | end 164 | 165 | private def var_name_valid?(char) 166 | ord = char.ord 167 | (ord >= 48 && ord <= 57) || 168 | (ord >= 65 && ord <= 90) || 169 | (ord >= 97 && ord <= 122) || 170 | [95, 123, 125].includes?(ord) 171 | end 172 | 173 | private def extract_expression(raw) : String? 174 | if raw.includes?('#') 175 | segments = [] of String 176 | quotes_open = false 177 | raw.split('#').each do |segment| 178 | if segment.scan("'").size == 1 || segment.scan("\"").size == 1 179 | if quotes_open 180 | quotes_open = false 181 | segments << segment 182 | else 183 | quotes_open = true 184 | end 185 | end 186 | 187 | if segments.size.zero? || quotes_open 188 | segments << segment 189 | end 190 | end 191 | 192 | line = segments.join('#') 193 | line unless line.empty? 194 | else 195 | raw 196 | end 197 | end 198 | end 199 | 200 | # Poncho parser helper 201 | # 202 | # ``` 203 | # require "poncho/parser" 204 | # ``` 205 | module ParserHelper 206 | # Parse dotenv from file 207 | def from_file(file : String, overwrite = false) 208 | Parser.from_file(file, overwrite) 209 | end 210 | 211 | # Parse raw string and overwrite the value with same key 212 | # 213 | # Same as `#parse`(raw, overwrite: true) 214 | def parse!(raw : String | IO) 215 | parse(raw, true) 216 | end 217 | 218 | # Parse raw string 219 | def parse(raw : String | IO, overwrite = false) 220 | parser = Parser.new(raw) 221 | parser.parse(overwrite) 222 | parser 223 | end 224 | end 225 | 226 | extend ParserHelper 227 | end 228 | -------------------------------------------------------------------------------- /src/poncho/version.cr: -------------------------------------------------------------------------------- 1 | module Poncho 2 | VERSION = "0.4.1" 3 | end 4 | --------------------------------------------------------------------------------