Skip to content

Commit

Permalink
[Env] added the component
Browse files Browse the repository at this point in the history
  • Loading branch information
fabpot committed Jan 11, 2017
1 parent e66e6af commit efcc938
Show file tree
Hide file tree
Showing 13 changed files with 749 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -40,6 +40,7 @@
"symfony/debug-bundle": "self.version",
"symfony/doctrine-bridge": "self.version",
"symfony/dom-crawler": "self.version",
"symfony/env": "self.version",
"symfony/event-dispatcher": "self.version",
"symfony/expression-language": "self.version",
"symfony/filesystem": "self.version",
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/Env/.gitignore
@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml
7 changes: 7 additions & 0 deletions src/Symfony/Component/Env/CHANGELOG.md
@@ -0,0 +1,7 @@
CHANGELOG
=========

3.3.0
-----

* added the component
365 changes: 365 additions & 0 deletions src/Symfony/Component/Env/Dotenv.php
@@ -0,0 +1,365 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Env;

use Symfony\Component\Env\Exception\FormatException;
use Symfony\Component\Env\Exception\FormatExceptionContext;
use Symfony\Component\Env\Exception\PathException;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;

/**
* Manages .env files.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Dotenv
{
const VARNAME_REGEX = '[A-Z][A-Z0-9_]*';
const STATE_VARNAME = 0;
const STATE_VALUE = 1;

private $path;
private $cursor;
private $lineno;
private $data;
private $end;
private $state;
private $export;
private $values;

/**
* Loads one or several .env files.
*
* @param ...string A list of files to load
*
* @throws FormatException when a file has a syntax error
* @throws PathException when a file does not exist or is not readable
*/
public function load(/*...$paths*/)
{
// func_get_args() to be replaced by a variadic argument for Symfony 4.0
foreach (func_get_args() as $path) {
if (!is_readable($path)) {
throw new PathException($path);
}

$this->populate($this->parse(file_get_contents($path), $path));
}
}

/**
* Sets values as environment variables (via putenv, $_ENV, and $_SERVER).
*
* Note that existing environment variables are never overridden.
*
* @param array An array of env variables
*/
public function populate($values)
{
foreach ($values as $name => $value) {
if (getenv($name)) {
continue;
}

putenv("$name=$value");
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}

/**
* Parses the contents of an .env file.
*
* @param string $data The data to be parsed
* @param string $path The original file name where data where stored (used for more meaningful error messages)
*
* @return array An array of env variables
*
* @throws FormatException when a file has a syntax error
*/
public function parse($data, $path = '.env')
{
$this->path = $path;
$this->data = str_replace(array("\r\n", "\r"), "\n", $data);
$this->lineno = 1;
$this->cursor = 0;
$this->end = strlen($this->data);
$this->state = self::STATE_VARNAME;
$this->values = array();
$name = $value = '';

while ($this->cursor < $this->end) {
switch ($this->state) {
case self::STATE_VARNAME:
$name = $this->lexVarname();
$this->state = self::STATE_VALUE;
break;

case self::STATE_VALUE:
$this->values[$name] = $this->lexValue();
$this->state = self::STATE_VARNAME;
break;
}
}

if (self::STATE_VALUE === $this->state) {
$this->values[$name] = '';
}

return $this->values;
}

private function lexVarname()
{
$this->skipEmptyLines();
$this->skipWhitespace();

// optional export
$this->export = false;
if ('export ' === substr($this->data, $this->cursor, 7)) {
$this->export = true;
$this->cursor += 7;
$this->skipWhitespace();
}

// var name
if (!preg_match('/('.self::VARNAME_REGEX.'|\n)/Ai', $this->data, $matches, 0, $this->cursor)) {
throw new FormatException('Invalid character in variable name', $this->createFormatExceptionContext());
}
$this->moveCursor($matches[0]);

if ($this->cursor === $this->end) {
if ($this->export) {
throw new FormatException('Unable to unset an environment variable', $this->createFormatExceptionContext());
}

throw new FormatException('Missing = in the environment variable declaration', $this->createFormatExceptionContext());
}

if (' ' === $this->data[$this->cursor]) {
throw new FormatException('Whitespace are not supported after the variable name', $this->createFormatExceptionContext());
}

if ('=' !== $this->data[$this->cursor]) {
throw new FormatException('Missing = in the environment variable declaration', $this->createFormatExceptionContext());
}
++$this->cursor;

return $matches[1];
}

private function lexValue()
{
if ("\n" === $this->data[$this->cursor]) {
$this->skipEmptyLines();

return '';
}

if (preg_match('/ *(?!#.*)?(?:\n|$)/Am', $this->data, $matches, null, $this->cursor)) {
$this->moveCursor($matches[0]);

return '';
}

if (' ' === $this->data[$this->cursor]) {
$this->skipComment();

// not a problem if the value is only a comment and/or whitespace
if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor - 1]) {
return '';
}

throw new FormatException('Whitespace are not supported before the value', $this->createFormatExceptionContext());
}

$value = '';
$singleQuoted = false;
$notQuoted = false;
if ("'" === $this->data[$this->cursor]) {
$singleQuoted = true;
++$this->cursor;
while ("\n" !== $this->data[$this->cursor]) {
if ("'" === $this->data[$this->cursor]) {
if ($this->cursor + 1 === $this->end) {
break;
}
if ("'" === $this->data[$this->cursor + 1]) {
++$this->cursor;
} else {
break;
}
}
$value .= $this->data[$this->cursor];
++$this->cursor;

if ($this->cursor === $this->end) {
throw new FormatException('Missing quote to end the value', $this->createFormatExceptionContext());
}
}
if ("\n" === $this->data[$this->cursor]) {
throw new FormatException('Missing quote to end the value', $this->createFormatExceptionContext());
}
++$this->cursor;
$this->skipComment();
} elseif ('"' === $this->data[$this->cursor]) {
++$this->cursor;
while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) {
$value .= $this->data[$this->cursor];
++$this->cursor;

if ($this->cursor === $this->end) {
throw new FormatException('Missing quote to end the value', $this->createFormatExceptionContext());
}
}
if ("\n" === $this->data[$this->cursor]) {
throw new FormatException('Missing quote to end the value', $this->createFormatExceptionContext());
}
++$this->cursor;
$this->skipComment();
$value = str_replace(array('\\\\', '\\"', '\r', '\n'), array('\\', '"', "\r", "\n"), $value);
} else {
$notQuoted = true;
while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor] && !(' ' === $this->data[$this->cursor - 1] && '#' === $this->data[$this->cursor])) {
$value .= $this->data[$this->cursor];
++$this->cursor;
}
$value = rtrim($value);
$this->skipComment();
}

$this->skipEmptyLines();

$currentValue = $value;
if (!$singleQuoted) {
$value = $this->resolveVariables($value);
$value = $this->resolveCommands($value);
}

if ($notQuoted && $currentValue == $value && preg_match('/\s+/', $value)) {
throw new FormatException('A value containing spaces must be surrounded by quotes', $this->createFormatExceptionContext());
}

return $value;
}

private function skipWhitespace()
{
$this->cursor += strspn($this->data, ' ', $this->cursor);
}

private function skipEmptyLines()
{
if (preg_match('/(\n+|^#[^\n]*(\n*|$))+/Asm', $this->data, $match, null, $this->cursor)) {
$this->moveCursor($match[0]);
}
}

private function skipComment()
{
if (preg_match('/ *#[^\n]*(\n*|$)/Asm', $this->data, $match, null, $this->cursor)) {
$this->moveCursor($match[0]);
}
}

private function resolveCommands($value)
{
if (false === strpos($value, '$')) {
return $value;
}

$regex = '/
(\\\\)? # escaped with a backslash?
\$
(?<cmd>
\( # require opening parenthesis
([^()]|\g<cmd>)+ # allow any number of non-parens, or balanced parens (by nesting the <cmd> expression recursively)
\) # require closing paren
)
/x';

$env = array_replace($_ENV, $this->values);
$path = $this->path;
$lineno = $this->lineno;

return preg_replace_callback($regex, function ($matches) use ($env, $path, $lineno) {
if ('\\' === $matches[1]) {
return substr($matches[0], 1);
}

if (!class_exists('Symfony\Component\Process\Process')) {
throw new \LogicException('Resolving commands requires the Symfony Process component.');
}

$process = new Process('echo '.$matches[0], null, $env);
try {
$process->mustRun();
} catch (ProcessException $e) {
throw new FormatException(sprintf('Issue expanding a command (%s)', $process->getErrorOutput()), $this->createFormatExceptionContext());
}

return preg_replace('/[\r\n]+$/', '', $process->getOutput());
}, $value);
}

private function resolveVariables($value)
{
if (false === strpos($value, '$')) {
return $value;
}

$regex = '/
(\\\\)? # escaped with a backslash?
\$
(?!\() # no opening parenthesis
(\{)? # optional brace
('.self::VARNAME_REGEX.') # var name
(\})? # optional closing brace
/xi';

$values = $this->values;
$path = $this->path;
$lineno = $this->lineno;
$value = preg_replace_callback($regex, function ($matches) use ($values, $path, $lineno) {
if ('\\' === $matches[1]) {
return substr($matches[0], 1);
}

if ('{' === $matches[2] && !isset($matches[4])) {
throw new FormatException('Unclosed braces on variable expansion', $this->createFormatExceptionContext());
}

$value = (string) array_key_exists($matches[3], $values) ? $values[$matches[3]] : getenv($matches[3]);

if (!$matches[2] && isset($matches[4])) {
$value .= '}';
}

return $value;
}, $value);

// unescape $
return str_replace('\\$', '$', $value);
}

private function moveCursor($text)
{
$this->cursor += strlen($text);
$this->lineno += substr_count($text, "\n");
}

private function createFormatExceptionContext()
{
return new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor);
}
}

0 comments on commit efcc938

Please sign in to comment.