33namespace Dotenv ;
44
55use Dotenv \Exception \InvalidFileException ;
6- use Dotenv \Regex \Regex ;
76
87class Parser
98{
9+ const INITIAL_STATE = 0 ;
10+ const UNQUOTED_STATE = 1 ;
11+ const QUOTED_STATE = 2 ;
12+ const ESCAPE_STATE = 3 ;
13+ const WHITESPACE_STATE = 4 ;
14+ const COMMENT_STATE = 5 ;
15+
1016 /**
1117 * Parse the given environment variable entry into a name and value.
1218 *
13- * Takes value as passed in by developer and:
14- * - breaks up the line into a name and value,
15- * - cleaning the value of quotes,
16- * - cleaning the name of quotes.
17- *
1819 * @param string $entry
1920 *
2021 * @throws \Dotenv\Exception\InvalidFileException
@@ -25,14 +26,12 @@ public static function parse($entry)
2526 {
2627 list ($ name , $ value ) = self ::splitStringIntoParts ($ entry );
2728
28- return [self ::sanitiseName ($ name ), self ::sanitiseValue ($ value )];
29+ return [self ::parseName ($ name ), self ::parseValue ($ value )];
2930 }
3031
3132 /**
3233 * Split the compound string into parts.
3334 *
34- * If the `$line` contains an `=` sign, then we split it into 2 parts.
35- *
3635 * @param string $line
3736 *
3837 * @throws \Dotenv\Exception\InvalidFileException
@@ -66,7 +65,7 @@ private static function splitStringIntoParts($line)
6665 *
6766 * @return string
6867 */
69- private static function sanitiseName ($ name )
68+ private static function parseName ($ name )
7069 {
7170 $ name = trim (str_replace (['export ' , '\'' , '" ' ], '' , $ name ));
7271
@@ -100,71 +99,60 @@ private static function isValidName($name)
10099 *
101100 * @return string|null
102101 */
103- private static function sanitiseValue ($ value )
102+ private static function parseValue ($ value )
104103 {
105104 if ($ value === null || trim ($ value ) === '' ) {
106105 return $ value ;
107106 }
108107
109- if (self ::beginsWithAQuote ($ value )) {
110- return self ::processQuotedValue ($ value );
111- }
112-
113- // Strip comments from the left
114- $ value = explode (' # ' , $ value , 2 )[0 ];
115-
116- // Unquoted values cannot contain whitespace
117- if (preg_match ('/\s+/ ' , $ value ) > 0 ) {
118- // Check if value is a comment (usually triggered when empty value with comment)
119- if (preg_match ('/^#/ ' , $ value ) > 0 ) {
120- $ value = '' ;
121- } else {
122- throw new InvalidFileException (
123- self ::getErrorMessage ('an unexpected space ' , $ value )
124- );
108+ return array_reduce (str_split ($ value ), function ($ data , $ char ) use ($ value ) {
109+ switch ($ data [1 ]) {
110+ case self ::INITIAL_STATE :
111+ if ($ 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 === '" ' ) {
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 === '" ' || $ char === '\\' ) {
136+ return [$ data [0 ].$ char , self ::QUOTED_STATE ];
137+ } else {
138+ throw new InvalidFileException (
139+ self ::getErrorMessage ('an unexpected escape sequence ' , $ value )
140+ );
141+ }
142+ case self ::WHITESPACE_STATE :
143+ if ($ char === '# ' ) {
144+ return [$ data [0 ], self ::COMMENT_STATE ];
145+ } elseif (!ctype_space ($ char )) {
146+ throw new InvalidFileException (
147+ self ::getErrorMessage ('unexpected whitespace ' , $ value )
148+ );
149+ } else {
150+ return [$ data [0 ], self ::WHITESPACE_STATE ];
151+ }
152+ case self ::COMMENT_STATE :
153+ return [$ data [0 ], self ::COMMENT_STATE ];
125154 }
126- }
127-
128- return $ value ;
129- }
130-
131- /**
132- * Strips quotes from the environment variable value.
133- *
134- * @param string $value
135- *
136- * @return string
137- */
138- private static function processQuotedValue ($ value )
139- {
140- $ quote = $ value [0 ];
141-
142- $ pattern = sprintf (
143- '/^
144- %1$s # match a quote at the start of the value
145- ( # capturing sub-pattern used
146- (?: # we do not need to capture this
147- [^%1$s \\\\]+ # any character other than a quote or backslash
148- | \\\\\\\\ # or two backslashes together
149- | \\\\%1$s # or an escaped quote e.g \"
150- )* # as many characters that match the previous rules
151- ) # end of the capturing sub-pattern
152- %1$s # and the closing quote
153- .*$ # and discard any string after the closing quote
154- /mx ' ,
155- $ quote
156- );
157-
158- return Regex::replace ($ pattern , '$1 ' , $ value )
159- ->mapSuccess (function ($ str ) use ($ quote ) {
160- return str_replace ('\\\\' , '\\' , str_replace ("\\$ quote " , $ quote , $ str ));
161- })
162- ->mapError (function ($ err ) use ($ value ) {
163- throw new InvalidFileException (
164- self ::getErrorMessage (sprintf ('a quote parsing error (%s) ' , $ err ), $ value )
165- );
166- })
167- ->getSuccess ();
155+ }, ['' , self ::INITIAL_STATE ])[0 ];
168156 }
169157
170158 /**
@@ -183,16 +171,4 @@ private static function getErrorMessage($cause, $subject)
183171 strtok ($ subject , "\n" )
184172 );
185173 }
186-
187- /**
188- * Determine if the given string begins with a quote.
189- *
190- * @param string $value
191- *
192- * @return bool
193- */
194- private static function beginsWithAQuote ($ value )
195- {
196- return isset ($ value [0 ]) && ($ value [0 ] === '" ' || $ value [0 ] === '\'' );
197- }
198174}
0 commit comments