Skip to content

Commit e1778ab

Browse files
[4.0] Re-worked parser to support escaping dollars (#380)
* Re-worked parser to support escaping dollars * Use legacy test methods * Avoid reserved word in php 5 * Updated phpdoc * Delete password.env
1 parent afb1f21 commit e1778ab

File tree

13 files changed

+363
-134
lines changed

13 files changed

+363
-134
lines changed

src/Loader.php

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Dotenv\Environment\FactoryInterface;
66
use Dotenv\Exception\InvalidPathException;
7-
use Dotenv\Regex\Regex;
87
use PhpOption\Option;
98

109
/**
@@ -180,27 +179,39 @@ private function processEntries(array $entries)
180179
* Look for ${varname} patterns in the variable value and replace with an
181180
* existing environment variable.
182181
*
183-
* @param string|null $value
182+
* @param \Dotenv\Value|null $value
184183
*
185184
* @return string|null
186185
*/
187-
private function resolveNestedVariables($value = null)
186+
private function resolveNestedVariables(Value $value = null)
188187
{
189188
return Option::fromValue($value)
190-
->filter(function ($str) {
191-
return strpos($str, '$') !== false;
192-
})
193-
->flatMap(function ($str) {
194-
return Regex::replaceCallback(
195-
'/\${([a-zA-Z0-9_.]+)}/',
196-
function (array $matches) {
197-
return Option::fromValue($this->getEnvironmentVariable($matches[1]))
198-
->getOrElse($matches[0]);
199-
},
200-
$str
201-
)->success();
189+
->map(function ($v) {
190+
return array_reduce($v->getVars(), function ($s, $i) {
191+
return substr($s, 0, $i).$this->resolveNestedVariable(substr($s, $i));
192+
}, $v->getChars());
202193
})
203-
->getOrElse($value);
194+
->getOrElse(null);
195+
}
196+
197+
/**
198+
* Resolve a single nested variable.
199+
*
200+
* @param string $str
201+
*
202+
* @return string
203+
*/
204+
private function resolveNestedVariable($str)
205+
{
206+
return Regex::replaceCallback(
207+
'/\A\${([a-zA-Z0-9_.]+)}/',
208+
function (array $matches) {
209+
return Option::fromValue($this->getEnvironmentVariable($matches[1]))
210+
->getOrElse($matches[0]);
211+
},
212+
$str,
213+
1
214+
)->success()->getOrElse($str);
204215
}
205216

206217
/**

src/Parser.php

Lines changed: 94 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
namespace Dotenv;
44

55
use Dotenv\Exception\InvalidFileException;
6+
use Dotenv\Result\Error;
7+
use Dotenv\Result\Success;
68

79
class Parser
810
{
911
const INITIAL_STATE = 0;
1012
const UNQUOTED_STATE = 1;
11-
const QUOTED_STATE = 2;
12-
const ESCAPE_STATE = 3;
13-
const WHITESPACE_STATE = 4;
14-
const COMMENT_STATE = 5;
13+
const SINGLE_QUOTED_STATE = 2;
14+
const DOUBLE_QUOTED_STATE = 3;
15+
const ESCAPE_SEQUENCE_STATE = 4;
16+
const WHITESPACE_STATE = 5;
17+
const COMMENT_STATE = 6;
1518

1619
/**
1720
* Parse the given environment variable entry into a name and value.
@@ -97,64 +100,99 @@ private static function isValidName($name)
97100
*
98101
* @throws \Dotenv\Exception\InvalidFileException
99102
*
100-
* @return string|null
103+
* @return \Dotenv\Value|null
101104
*/
102105
private static function parseValue($value)
103106
{
104-
if ($value === null || trim($value) === '') {
105-
return $value;
107+
if ($value === null) {
108+
return null;
109+
}
110+
111+
if (trim($value) === '') {
112+
return Value::blank();
106113
}
107114

108115
return array_reduce(str_split($value), function ($data, $char) use ($value) {
109-
switch ($data[1]) {
110-
case self::INITIAL_STATE:
111-
if ($char === '"' || $char === '\'') {
112-
return [$data[0], self::QUOTED_STATE];
113-
} elseif ($char === '#') {
114-
return [$data[0], self::COMMENT_STATE];
115-
} else {
116-
return [$data[0].$char, self::UNQUOTED_STATE];
117-
}
118-
case self::UNQUOTED_STATE:
119-
if ($char === '#') {
120-
return [$data[0], self::COMMENT_STATE];
121-
} elseif (ctype_space($char)) {
122-
return [$data[0], self::WHITESPACE_STATE];
123-
} else {
124-
return [$data[0].$char, self::UNQUOTED_STATE];
125-
}
126-
case self::QUOTED_STATE:
127-
if ($char === $value[0]) {
128-
return [$data[0], self::WHITESPACE_STATE];
129-
} elseif ($char === '\\') {
130-
return [$data[0], self::ESCAPE_STATE];
131-
} else {
132-
return [$data[0].$char, self::QUOTED_STATE];
133-
}
134-
case self::ESCAPE_STATE:
135-
if ($char === $value[0] || $char === '\\') {
136-
return [$data[0].$char, self::QUOTED_STATE];
137-
} elseif (in_array($char, ['f', 'n', 'r', 't', 'v'], true)) {
138-
return [$data[0].stripcslashes('\\' . $char), self::QUOTED_STATE];
139-
} else {
140-
throw new InvalidFileException(
141-
self::getErrorMessage('an unexpected escape sequence', $value)
142-
);
143-
}
144-
case self::WHITESPACE_STATE:
145-
if ($char === '#') {
146-
return [$data[0], self::COMMENT_STATE];
147-
} elseif (!ctype_space($char)) {
148-
throw new InvalidFileException(
149-
self::getErrorMessage('unexpected whitespace', $value)
150-
);
151-
} else {
152-
return [$data[0], self::WHITESPACE_STATE];
153-
}
154-
case self::COMMENT_STATE:
155-
return [$data[0], self::COMMENT_STATE];
156-
}
157-
}, ['', self::INITIAL_STATE])[0];
116+
return self::processChar($data[1], $char)->mapError(function ($err) use ($value) {
117+
throw new InvalidFileException(
118+
self::getErrorMessage($err, $value)
119+
);
120+
})->mapSuccess(function ($val) use ($data) {
121+
return [$data[0]->append($val[0], $val[1]), $val[2]];
122+
})->getSuccess();
123+
}, [Value::blank(), self::INITIAL_STATE])[0];
124+
}
125+
126+
/**
127+
* Process the given character.
128+
*
129+
* @param int $state
130+
* @param string $char
131+
*
132+
* @return array
133+
*/
134+
private static function processChar($state, $char)
135+
{
136+
switch ($state) {
137+
case self::INITIAL_STATE:
138+
if ($char === '\'') {
139+
return Success::create(['', false, self::SINGLE_QUOTED_STATE]);
140+
} elseif ($char === '"') {
141+
return Success::create(['', false, self::DOUBLE_QUOTED_STATE]);
142+
} elseif ($char === '#') {
143+
return Success::create(['', false, self::COMMENT_STATE]);
144+
} elseif ($char === '$') {
145+
return Success::create([$char, true, self::UNQUOTED_STATE]);
146+
} else {
147+
return Success::create([$char, false, self::UNQUOTED_STATE]);
148+
}
149+
case self::UNQUOTED_STATE:
150+
if ($char === '#') {
151+
return Success::create(['', false, self::COMMENT_STATE]);
152+
} elseif (ctype_space($char)) {
153+
return Success::create(['', false, self::WHITESPACE_STATE]);
154+
} elseif ($char === '$') {
155+
return Success::create([$char, true, self::UNQUOTED_STATE]);
156+
} else {
157+
return Success::create([$char, false, self::UNQUOTED_STATE]);
158+
}
159+
case self::SINGLE_QUOTED_STATE:
160+
if ($char === '\'') {
161+
return Success::create(['', false, self::WHITESPACE_STATE]);
162+
} else {
163+
return Success::create([$char, false, self::SINGLE_QUOTED_STATE]);
164+
}
165+
case self::DOUBLE_QUOTED_STATE:
166+
if ($char === '"') {
167+
return Success::create(['', false, self::WHITESPACE_STATE]);
168+
} elseif ($char === '\\') {
169+
return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]);
170+
} elseif ($char === '$') {
171+
return Success::create([$char, true, self::DOUBLE_QUOTED_STATE]);
172+
} else {
173+
return Success::create([$char, false, self::DOUBLE_QUOTED_STATE]);
174+
}
175+
case self::ESCAPE_SEQUENCE_STATE:
176+
if ($char === '"' || $char === '\\') {
177+
return Success::create([$char, false, self::DOUBLE_QUOTED_STATE]);
178+
} elseif ($char === '$') {
179+
return Success::create([$char, false, self::DOUBLE_QUOTED_STATE]);
180+
} elseif (in_array($char, ['f', 'n', 'r', 't', 'v'], true)) {
181+
return Success::create([stripcslashes('\\' . $char), false, self::DOUBLE_QUOTED_STATE]);
182+
} else {
183+
return Error::create('an unexpected escape sequence');
184+
}
185+
case self::WHITESPACE_STATE:
186+
if ($char === '#') {
187+
return Success::create(['', false, self::COMMENT_STATE]);
188+
} elseif (!ctype_space($char)) {
189+
return Error::create('unexpected whitespace');
190+
} else {
191+
return Success::create(['', false, self::WHITESPACE_STATE]);
192+
}
193+
case self::COMMENT_STATE:
194+
return Success::create(['', false, self::COMMENT_STATE]);
195+
}
158196
}
159197

160198
/**

src/Regex/Regex.php renamed to src/Regex.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<?php
22

3-
namespace Dotenv\Regex;
3+
namespace Dotenv;
44

5+
use Dotenv\Result\Error;
6+
use Dotenv\Result\Result;
7+
use Dotenv\Result\Success;
58
use PhpOption\Option;
69

710
class Regex
@@ -12,7 +15,7 @@ class Regex
1215
* @param string $pattern
1316
* @param string $subject
1417
*
15-
* @return \Dotenv\Regex\Result
18+
* @return \Dotenv\Result\Result
1619
*/
1720
public static function match($pattern, $subject)
1821
{
@@ -24,16 +27,17 @@ public static function match($pattern, $subject)
2427
/**
2528
* Perform a preg replace, wrapping up the result.
2629
*
27-
* @param string $pattern
28-
* @param string $replacement
29-
* @param string $subject
30+
* @param string $pattern
31+
* @param string $replacement
32+
* @param string $subject
33+
* @param int|null $limit
3034
*
31-
* @return \Dotenv\Regex\Result
35+
* @return \Dotenv\Result\Result
3236
*/
33-
public static function replace($pattern, $replacement, $subject)
37+
public static function replace($pattern, $replacement, $subject, $limit = null)
3438
{
35-
return self::pregAndWrap(function ($subject) use ($pattern, $replacement) {
36-
return (string) @preg_replace($pattern, $replacement, $subject);
39+
return self::pregAndWrap(function ($subject) use ($pattern, $replacement, $limit) {
40+
return (string) @preg_replace($pattern, $replacement, $subject, $limit === null ? -1 : $limit);
3741
}, $subject);
3842
}
3943

@@ -43,13 +47,14 @@ public static function replace($pattern, $replacement, $subject)
4347
* @param string $pattern
4448
* @param callable $callback
4549
* @param string $subject
50+
* @param int|null $limit
4651
*
47-
* @return \Dotenv\Regex\Result
52+
* @return \Dotenv\Result\Result
4853
*/
49-
public static function replaceCallback($pattern, callable $callback, $subject)
54+
public static function replaceCallback($pattern, callable $callback, $subject, $limit = null)
5055
{
5156
return self::pregAndWrap(function ($subject) use ($pattern, $callback) {
52-
return (string) @preg_replace_callback($pattern, $callback, $subject);
57+
return (string) @preg_replace_callback($pattern, $callback, $subject, $limit === null ? -1 : $limit);
5358
}, $subject);
5459
}
5560

@@ -59,7 +64,7 @@ public static function replaceCallback($pattern, callable $callback, $subject)
5964
* @param callable $operation
6065
* @param string $subject
6166
*
62-
* @return \Dotenv\Regex\Result
67+
* @return \Dotenv\Result\Result
6368
*/
6469
private static function pregAndWrap(callable $operation, $subject)
6570
{

src/Regex/Error.php renamed to src/Result/Error.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
<?php
22

3-
namespace Dotenv\Regex;
3+
namespace Dotenv\Result;
44

55
use PhpOption\None;
66
use PhpOption\Some;
77

88
class Error extends Result
99
{
1010
/**
11-
* @var string
11+
* @var mixed
1212
*/
1313
private $value;
1414

1515
/**
1616
* Internal constructor for an error value.
1717
*
18-
* @param string $value
18+
* @param mixed $value
1919
*
2020
* @return void
2121
*/
@@ -27,9 +27,9 @@ private function __construct($value)
2727
/**
2828
* Create a new error value.
2929
*
30-
* @param string $value
30+
* @param mixed $value
3131
*
32-
* @return \Dotenv\Regex\Result
32+
* @return \Dotenv\Result\Result
3333
*/
3434
public static function create($value)
3535
{
@@ -51,7 +51,7 @@ public function success()
5151
*
5252
* @param callable $f
5353
*
54-
* @return \Dotenv\Regex\Result
54+
* @return \Dotenv\Result\Result
5555
*/
5656
public function mapSuccess(callable $f)
5757
{
@@ -73,7 +73,7 @@ public function error()
7373
*
7474
* @param callable $f
7575
*
76-
* @return \Dotenv\Regex\Result
76+
* @return \Dotenv\Result\Result
7777
*/
7878
public function mapError(callable $f)
7979
{

0 commit comments

Comments
 (0)