Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support variables in values #6

Merged
merged 3 commits into from
Aug 22, 2018
Merged
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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,15 @@ Poncho parser currently supports the following rules:
- Skipped the empty line and comment(`#`).
- Ignore the comment which after (`#`).
- `ENV=development` becomes `{"ENV" => "development"}`.
- Snakecase and upcase the key: `dbName` becomes `DB_NAME`, `DB_NAME` becomes `DB_NAME`.
- Snakecase and upcase the key: `dbName` becomes `DB_NAME`, `DB_NAME` becomes `DB_NAME`
- Support variables in value. `$NAME` or `${NAME}`.
- Whitespace is removed from both ends of the value. `NAME = foo ` becomes`{"NAME" => "foo"}
- New lines are expanded if in double quotes. `MULTILINE="new\nline"` becomes `{"MULTILINE" => "new\nline"}
- Inner quotes are maintained (such like `Hash`/`JSON`). `JSON={"foo":"bar"}` becomes `{"JSON" => "{\"foo\":\"bar\"}"}
- Empty values become empty strings.
- Whitespace is removed from both starts and ends of the value.
- Single and double quoted values are escaped.
- New lines are expanded if in double quotes.
- Inner quotes are maintained (such like json).
- Overwrite optional (default is non-overwrite).
- Only accpets string type value.
- Support variables in value. `WELCOME="hello $NAME"` becomes `{"WELCOME" => "hello foo"}`

#### Overrides

Expand Down
7 changes: 7 additions & 0 deletions spec/fixtures/parse_sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ EQUAL_SIGNS=equals==
INCLUDE_SPACE=some spaced out string
USERNAME="[email protected]"

SINGLE_VARIABLE=$STR
MULTIPLE_VARIABLE1=$STR$INT
MULTIPLE_VARIABLE2=$STR$INT1
SINGLE_BLOCK_VARIABLE=${STR}${INT}
SINGLE_QUOTES_VARIABLE='hello $STR!'
DOUBLE_QUOTES_VARIABLE="hello ${STR}, my email is $USERNAME"

LIST_STR='foo,bar'
LIST_STR_WITH_SPACES=' foo, bar'
LIST_INT=1,2,3
Expand Down
26 changes: 13 additions & 13 deletions spec/poncho_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -44,33 +44,33 @@ describe Poncho do
describe "loads single file" do
describe "disable overwrite" do
describe "with sample file" do
loader = Poncho.load fixture_path("parse_sample.env")
Poncho.load fixture_path("parse_sample.env")
it_equal_group ENV
clean_env "parse_sample.env"
end

describe "with overwrite file" do
loader = Poncho.load fixture_path("overwrite.env")
Poncho.load fixture_path("overwrite.env")
it_equal ENV, "PONCHO_NAME", "foo"
clean_env "overwrite.env"
end

describe "with file and test env" do
loader = Poncho.load fixture_path("app.env"), env: "test"
Poncho.load fixture_path("app.env"), env: "test"
it_equal ENV, "PONCHO_NAME", "poncho"
clean_env "app.env.test.local"
end

describe "with path" do
loader = Poncho.load fixture_path("load_from_path")
Poncho.load fixture_path("load_from_path")
it_equal ENV, "PONCHO_FROM", ".env"
it_equal ENV, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho"
clean_env "load_from_path/.env"
clean_env "load_from_path/.env.local"
end

describe "with path and test env" do
loader = Poncho.load fixture_path("load_from_path"), env: "test"
Poncho.load fixture_path("load_from_path"), env: "test"
it_equal ENV, "PONCHO_FROM", ".env"
it_equal ENV, "PONCHO_URL", "localhost.test"
it_equal ENV, "PONCHO_MYSQL_HOST", "localhost.test"
Expand All @@ -81,7 +81,7 @@ describe Poncho do
end

describe "with path and production env" do
loader = Poncho.load fixture_path("load_from_path"), env: "production"
Poncho.load fixture_path("load_from_path"), env: "production"
it_equal ENV, "PONCHO_FROM", ".env"
it_equal ENV, "PONCHO_URL", "poncho.example.com"
it_equal ENV, "PONCHO_MYSQL_HOST", "poncho.example.com"
Expand All @@ -97,21 +97,21 @@ describe Poncho do

describe "enable overwrite" do
describe "with overwrite file" do
loader = Poncho.load! fixture_path("overwrite.env")
Poncho.load! fixture_path("overwrite.env")
it_equal ENV, "PONCHO_NAME", "overwrite"
clean_env "overwrite.env"
end

describe "with path" do
loader = Poncho.load! fixture_path("load_from_path")
Poncho.load! fixture_path("load_from_path")
it_equal ENV, "PONCHO_FROM", ".local"
it_equal ENV, "PONCHO_PATH", "/Users/icyleaf/workspaces/poncho"
clean_env "load_from_path/.env"
clean_env "load_from_path/.env.local"
end

describe "with path and env" do
loader = Poncho.load! fixture_path("load_from_path"), env: "test"
Poncho.load! fixture_path("load_from_path"), env: "test"
it_equal ENV, "PONCHO_FROM", ".local"
it_equal ENV, "PONCHO_URL", "localhost.test"
it_equal ENV, "PONCHO_MYSQL_HOST", "localhost.test"
Expand All @@ -122,7 +122,7 @@ describe Poncho do
end

describe "with path and production env" do
loader = Poncho.load! fixture_path("load_from_path"), env: "production"
Poncho.load! fixture_path("load_from_path"), env: "production"
it_equal ENV, "PONCHO_FROM", ".production.local"
it_equal ENV, "PONCHO_URL", "poncho.example.com"
it_equal ENV, "PONCHO_MYSQL_HOST", "poncho.example.com"
Expand All @@ -137,23 +137,23 @@ describe Poncho do

describe "loads multiple files" do
describe "with non-overwrite" do
loader = Poncho.load fixture_path("overwrite.env"), fixture_path("app.env.test.local"), env: "development"
Poncho.load fixture_path("overwrite.env"), fixture_path("app.env.test.local"), env: "development"
it_equal ENV, "PONCHO_NAME", "foo"
it_equal ENV, "PONCHO_ENV", "test"
clean_env "overwrite.env"
clean_env "app.env.test.local"
end

describe "with overwrite" do
loader = Poncho.load! fixture_path("overwrite.env"), fixture_path("app.env.test.local"), env: "development"
Poncho.load! fixture_path("overwrite.env"), fixture_path("app.env.test.local"), env: "development"
it_equal ENV, "PONCHO_NAME", "poncho"
it_equal ENV, "PONCHO_ENV", "test"
clean_env "overwrite.env"
clean_env "app.env.test.local"
end

describe "with env" do
loader = Poncho.load! fixture_path("load_from_path/.env"), fixture_path("load_from_path/.env.test"), env: "production"
Poncho.load! fixture_path("load_from_path/.env"), fixture_path("load_from_path/.env.test"), env: "production"
it_equal ENV, "PONCHO_FROM", ".test"
it_equal ENV, "PONCHO_URL", "localhost.test"
it_equal ENV, "PONCHO_MYSQL_HOST", "localhost.test"
Expand Down
8 changes: 7 additions & 1 deletion spec/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ require "spec"
require "../src/poncho"

def fixture_path
path = File.expand_path("../fixtures/", __FILE__)
File.expand_path("../fixtures/", __FILE__)
end

def fixture_path(filename : String)
Expand Down Expand Up @@ -50,6 +50,12 @@ def it_equal_group(env)
it_equal env, "RETAIN_INNER_QUOTES_AS_STRING", %Q{{"foo": "bar"}}
it_equal env, "INCLUDE_SPACE", "some spaced out string"
it_equal env, "USERNAME", "[email protected]"
it_equal env, "SINGLE_VARIABLE", "foo"
it_equal env, "MULTIPLE_VARIABLE1", "foo42"
it_equal env, "MULTIPLE_VARIABLE2", "foo$INT1"
it_equal env, "SINGLE_BLOCK_VARIABLE", "foo42"
it_equal env, "SINGLE_QUOTES_VARIABLE", "hello $STR!"
it_equal env, "DOUBLE_QUOTES_VARIABLE", "hello foo, my email is [email protected]"
end

def clean_env(filename)
Expand Down
113 changes: 92 additions & 21 deletions src/poncho/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ module Poncho
#
# - Skipped the empty line and comment(`#`).
# - Ignore the comment which after (`#`).
# - `NAME=foo` becomes `{"NAME" => "foo"}`.
# - `ENV=development` becomes `{"ENV" => "development"}`.
# - Snakecase and upcase the key: `dbName` becomes `DB_NAME`, `DB_NAME` becomes `DB_NAME`
# - Support variables in value. `$NAME` or `${NAME}`.
# - Whitespace is removed from both ends of the value. `NAME = foo ` becomes`{"NAME" => "foo"}
# - New lines are expanded if in double quotes. `MULTILINE="new\nline"` becomes `{"MULTILINE" => "new\nline"}
# - Inner quotes are maintained (such like `Hash`/`JSON`). `JSON={"foo":"bar"}` becomes `{"JSON" => "{\"foo\":\"bar\"}"}
# - Empty values become empty strings.
# - Whirespace is removed from right ends of the value.
# - Single and Double quoted values are escaped.
# - New lines are expanded if in double quotes.
# - Inner quotes are maintained (such like json).
# - Single and double quoted values are escaped.
# - Overwrite optional (default is non-overwrite).
# - Only accpets string type value.
# - Support variables in values. `WELCOME="hello $NAME"` becomes `{"WELCOME" => "hello foo"}`
#
# ### Overrides
#
Expand Down Expand Up @@ -52,6 +54,7 @@ module Poncho
end

@env = {} of String => String
@vars = {} of String => Array(String)

def initialize(@raw : String | IO)
end
Expand All @@ -68,22 +71,11 @@ module Poncho
@raw.each_line do |line|
next if line.blank? || !line.includes?('=')
next unless expression = extract_expression(line)

key, value = expression.split("=", 2).map { |v| v.strip }
if ['\'', '"'].includes?(value[0]) && ['\'', '"'].includes?(value[-1])
if value[0] == '"' && value[-1] == '"'
value = value.gsub("\\n", "\n").gsub("\\r", "\r")
end

value = value[1..-2]
end

env_key = env_key(key)
key_existes = @env.has_key?(env_key)
@env[env_key] = value if !key_existes || (key_existes && overwrite)
key, value = extract_env(expression)
set_env(key, value, overwrite)
end

nil
replace_variables
end

# Returns this collection as a plain Hash.
Expand All @@ -93,6 +85,34 @@ module Poncho

forward_missing_to @env

private def extract_env(expression : String)
search_vars = true
key, value = expression.split("=", 2).map { |v| v.strip }
if ['\'', '"'].includes?(value[0]) && ['\'', '"'].includes?(value[-1])
if value[0] == '"' && value[-1] == '"'
value = value.gsub("\\n", "\n").gsub("\\r", "\r")
else
search_vars = false
end

value = value[1..-2]
end

if search_vars && (vars = find_vars(value))
@vars[env_key(key)] = vars
end

[key, value]
end

private def set_env(key : String, value : String, overwrite : Bool)
env_key = env_key(key)
key_existes = @env.has_key?(env_key)
if !key_existes || (key_existes && overwrite)
@env[env_key] = value
end
end

private def env_key(key : String)
unless key.includes?("_")
# example: dbName => DB_NAME
Expand All @@ -105,7 +125,58 @@ module Poncho
end.join("_").upcase
end

private def extract_expression(raw)
private def replace_variables
@vars.each do |key, vars|
replaced = false

value = @env[key]
vars.each do |var|
var_key = var[1..-1]
if var_key[0] == '{' && var_key[-1] == '}'
var_key = var_key[1..-2]
end

if var_value = @env[env_key(var_key)]?
value = value.sub(var, var_value)
replaced = true
end
end

@env[key] = value if replaced
end
end

private def find_vars(value : String)
return unless starts_at = value.index('$')
vars = value[(starts_at + 1)..-1].split('$')
Array(String).new.tap do |obj|
vars.each do |var|
brace_open = false
value = String.build do |io|
io << '$'
var.each_char do |char|
brace_open = true if char == '{'
io << char if var_name_valid?(char)
if brace_open && char == '}'
break
end
end
end

obj << value
end
end
end

def var_name_valid?(char)
ord = char.ord
(ord >= 48 && ord <= 57) ||
(ord >= 65 && ord <= 90) ||
(ord >= 97 && ord <= 122) ||
[95, 123, 125].includes?(ord)
end

private def extract_expression(raw) : String?
if raw.includes?('#')
segments = [] of String
quotes_open = false
Expand Down