Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f448581

Browse filesBrowse files
committed
[Env] added the component
1 parent e66e6af commit f448581
Copy full SHA for f448581

13 files changed

+749
-0
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"symfony/debug-bundle": "self.version",
4141
"symfony/doctrine-bridge": "self.version",
4242
"symfony/dom-crawler": "self.version",
43+
"symfony/env": "self.version",
4344
"symfony/event-dispatcher": "self.version",
4445
"symfony/expression-language": "self.version",
4546
"symfony/filesystem": "self.version",
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
3.3.0
5+
-----
6+
7+
* added the component
+365Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dotenv;
13+
14+
use Symfony\Component\Dotenv\Exception\FormatException;
15+
use Symfony\Component\Dotenv\Exception\FormatExceptionContext;
16+
use Symfony\Component\Dotenv\Exception\PathException;
17+
use Symfony\Component\Process\Process;
18+
use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;
19+
20+
/**
21+
* Manages .env files.
22+
*
23+
* @author Fabien Potencier <fabien@symfony.com>
24+
*/
25+
final class Dotenv
26+
{
27+
const VARNAME_REGEX = '[A-Z][A-Z0-9_]*';
28+
const STATE_VARNAME = 0;
29+
const STATE_VALUE = 1;
30+
31+
private $path;
32+
private $cursor;
33+
private $lineno;
34+
private $data;
35+
private $end;
36+
private $state;
37+
private $export;
38+
private $values;
39+
40+
/**
41+
* Loads one or several .env files.
42+
*
43+
* @param ...string A list of files to load
44+
*
45+
* @throws FormatException when a file has a syntax error
46+
* @throws PathException when a file does not exist or is not readable
47+
*/
48+
public function load(/*...$paths*/)
49+
{
50+
// func_get_args() to be replaced by a variadic argument for Symfony 4.0
51+
foreach (func_get_args() as $path) {
52+
if (!is_readable($path)) {
53+
throw new PathException($path);
54+
}
55+
56+
$this->populate($this->parse(file_get_contents($path), $path));
57+
}
58+
}
59+
60+
/**
61+
* Sets values as environment variables (via putenv, $_ENV, and $_SERVER).
62+
*
63+
* Note that existing environment variables are never overridden.
64+
*
65+
* @param array An array of env variables
66+
*/
67+
public function populate($values)
68+
{
69+
foreach ($values as $name => $value) {
70+
if (isset($_ENV[$name]) || false !== getenv($name)) {
71+
continue;
72+
}
73+
74+
putenv("$name=$value");
75+
$_ENV[$name] = $value;
76+
$_SERVER[$name] = $value;
77+
}
78+
}
79+
80+
/**
81+
* Parses the contents of an .env file.
82+
*
83+
* @param string $data The data to be parsed
84+
* @param string $path The original file name where data where stored (used for more meaningful error messages)
85+
*
86+
* @return array An array of env variables
87+
*
88+
* @throws FormatException when a file has a syntax error
89+
*/
90+
public function parse($data, $path = '.env')
91+
{
92+
$this->path = $path;
93+
$this->data = str_replace(array("\r\n", "\r"), "\n", $data);
94+
$this->lineno = 1;
95+
$this->cursor = 0;
96+
$this->end = strlen($this->data);
97+
$this->state = self::STATE_VARNAME;
98+
$this->values = array();
99+
$name = $value = '';
100+
101+
while ($this->cursor < $this->end) {
102+
switch ($this->state) {
103+
case self::STATE_VARNAME:
104+
$name = $this->lexVarname();
105+
$this->state = self::STATE_VALUE;
106+
break;
107+
108+
case self::STATE_VALUE:
109+
$this->values[$name] = $this->lexValue();
110+
$this->state = self::STATE_VARNAME;
111+
break;
112+
}
113+
}
114+
115+
if (self::STATE_VALUE === $this->state) {
116+
$this->values[$name] = '';
117+
}
118+
119+
return $this->values;
120+
}
121+
122+
private function lexVarname()
123+
{
124+
$this->skipEmptyLines();
125+
$this->skipWhitespace();
126+
127+
// optional export
128+
$this->export = false;
129+
if ($this->cursor === strpos($this->data, 'export ', $this->cursor)) {
130+
$this->export = true;
131+
$this->cursor += 7;
132+
$this->skipWhitespace();
133+
}
134+
135+
// var name
136+
if (!preg_match('/('.self::VARNAME_REGEX.'|\n)/Ai', $this->data, $matches, 0, $this->cursor)) {
137+
throw $this->createFormatException('Invalid character in variable name');
138+
}
139+
$this->moveCursor($matches[0]);
140+
141+
if ($this->cursor === $this->end) {
142+
if ($this->export) {
143+
throw $this->createFormatException('Unable to unset an environment variable');
144+
}
145+
146+
throw $this->createFormatException('Missing = in the environment variable declaration');
147+
}
148+
149+
if (' ' === $this->data[$this->cursor]) {
150+
throw $this->createFormatException('Whitespace are not supported after the variable name');
151+
}
152+
153+
if ('=' !== $this->data[$this->cursor]) {
154+
throw $this->createFormatException('Missing = in the environment variable declaration');
155+
}
156+
++$this->cursor;
157+
158+
return $matches[1];
159+
}
160+
161+
private function lexValue()
162+
{
163+
if ("\n" === $this->data[$this->cursor]) {
164+
$this->skipEmptyLines();
165+
166+
return '';
167+
}
168+
169+
if (preg_match('/ *(?!#.*)?(?:\n|$)/Am', $this->data, $matches, null, $this->cursor)) {
170+
$this->moveCursor($matches[0]);
171+
172+
return '';
173+
}
174+
175+
if (' ' === $this->data[$this->cursor]) {
176+
$this->skipComment();
177+
178+
// not a problem if the value is only a comment and/or whitespace
179+
if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor - 1]) {
180+
return '';
181+
}
182+
183+
throw $this->createFormatException('Whitespace are not supported before the value');
184+
}
185+
186+
$value = '';
187+
$singleQuoted = false;
188+
$notQuoted = false;
189+
if ("'" === $this->data[$this->cursor]) {
190+
$singleQuoted = true;
191+
++$this->cursor;
192+
while ("\n" !== $this->data[$this->cursor]) {
193+
if ("'" === $this->data[$this->cursor]) {
194+
if ($this->cursor + 1 === $this->end) {
195+
break;
196+
}
197+
if ("'" !== $this->data[$this->cursor + 1]) {
198+
break;
199+
}
200+
201+
++$this->cursor;
202+
}
203+
$value .= $this->data[$this->cursor];
204+
++$this->cursor;
205+
206+
if ($this->cursor === $this->end) {
207+
throw $this->createFormatException('Missing quote to end the value');
208+
}
209+
}
210+
if ("\n" === $this->data[$this->cursor]) {
211+
throw $this->createFormatException('Missing quote to end the value');
212+
}
213+
++$this->cursor;
214+
$this->skipComment();
215+
} elseif ('"' === $this->data[$this->cursor]) {
216+
++$this->cursor;
217+
while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) {
218+
$value .= $this->data[$this->cursor];
219+
++$this->cursor;
220+
221+
if ($this->cursor === $this->end) {
222+
throw $this->createFormatException('Missing quote to end the value');
223+
}
224+
}
225+
if ("\n" === $this->data[$this->cursor]) {
226+
throw $this->createFormatException('Missing quote to end the value');
227+
}
228+
++$this->cursor;
229+
$this->skipComment();
230+
$value = str_replace(array('\\\\', '\\"', '\r', '\n'), array('\\', '"', "\r", "\n"), $value);
231+
} else {
232+
$notQuoted = true;
233+
while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor] && !(' ' === $this->data[$this->cursor - 1] && '#' === $this->data[$this->cursor])) {
234+
$value .= $this->data[$this->cursor];
235+
++$this->cursor;
236+
}
237+
$value = rtrim($value);
238+
$this->skipComment();
239+
}
240+
241+
$this->skipEmptyLines();
242+
243+
$currentValue = $value;
244+
if (!$singleQuoted) {
245+
$value = $this->resolveVariables($value);
246+
$value = $this->resolveCommands($value);
247+
}
248+
249+
if ($notQuoted && $currentValue == $value && preg_match('/\s+/', $value)) {
250+
throw $this->createFormatException('A value containing spaces must be surrounded by quotes');
251+
}
252+
253+
return $value;
254+
}
255+
256+
private function skipWhitespace()
257+
{
258+
$this->cursor += strspn($this->data, ' ', $this->cursor);
259+
}
260+
261+
private function skipEmptyLines()
262+
{
263+
if (preg_match('/(\n+|^#[^\n]*(\n*|$))+/Asm', $this->data, $match, null, $this->cursor)) {
264+
$this->moveCursor($match[0]);
265+
}
266+
}
267+
268+
private function skipComment()
269+
{
270+
if (preg_match('/ *#[^\n]*(\n*|$)/Asm', $this->data, $match, null, $this->cursor)) {
271+
$this->moveCursor($match[0]);
272+
}
273+
}
274+
275+
private function resolveCommands($value)
276+
{
277+
// commands are not supported on Windows
278+
if ('\\' === DIRECTORY_SEPARATOR) {
279+
return $value;
280+
}
281+
282+
if (false === strpos($value, '$')) {
283+
return $value;
284+
}
285+
286+
$regex = '/
287+
(\\\\)? # escaped with a backslash?
288+
\$
289+
(?<cmd>
290+
\( # require opening parenthesis
291+
([^()]|\g<cmd>)+ # allow any number of non-parens, or balanced parens (by nesting the <cmd> expression recursively)
292+
\) # require closing paren
293+
)
294+
/x';
295+
296+
return preg_replace_callback($regex, function ($matches) {
297+
if ('\\' === $matches[1]) {
298+
return substr($matches[0], 1);
299+
}
300+
301+
if (!class_exists(Process::class)) {
302+
throw new \LogicException('Resolving commands requires the Symfony Process component.');
303+
}
304+
305+
$process = new Process('echo '.$matches[0]);
306+
$process->inheritEnvironmentVariables(true);
307+
$process->setEnv($this->values);
308+
try {
309+
$process->mustRun();
310+
} catch (ProcessException $e) {
311+
throw $this->createFormatException(sprintf('Issue expanding a command (%s)', $process->getErrorOutput()));
312+
}
313+
314+
return preg_replace('/[\r\n]+$/', '', $process->getOutput());
315+
}, $value);
316+
}
317+
318+
private function resolveVariables($value)
319+
{
320+
if (false === strpos($value, '$')) {
321+
return $value;
322+
}
323+
324+
$regex = '/
325+
(\\\\)? # escaped with a backslash?
326+
\$
327+
(?!\() # no opening parenthesis
328+
(\{)? # optional brace
329+
('.self::VARNAME_REGEX.') # var name
330+
(\})? # optional closing brace
331+
/xi';
332+
333+
$value = preg_replace_callback($regex, function ($matches) {
334+
if ('\\' === $matches[1]) {
335+
return substr($matches[0], 1);
336+
}
337+
338+
if ('{' === $matches[2] && !isset($matches[4])) {
339+
throw $this->createFormatException('Unclosed braces on variable expansion');
340+
}
341+
342+
$value = (string) array_key_exists($matches[3], $this->values) ? $this->values[$matches[3]] : getenv($matches[3]);
343+
344+
if (!$matches[2] && isset($matches[4])) {
345+
$value .= '}';
346+
}
347+
348+
return $value;
349+
}, $value);
350+
351+
// unescape $
352+
return str_replace('\\$', '$', $value);
353+
}
354+
355+
private function moveCursor($text)
356+
{
357+
$this->cursor += strlen($text);
358+
$this->lineno += substr_count($text, "\n");
359+
}
360+
361+
private function createFormatException($message)
362+
{
363+
return new FormatException($message, new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor));
364+
}
365+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.