@@ -137,33 +137,51 @@ defmodule Envious.Parser do
137137 ?] .. ?~
138138 ] )
139139
140- # Content inside double quotes: either escape sequences or regular characters
140+ # Variable interpolation: $VAR or ${VAR}
141+ # Returns the variable name tagged for later interpolation
142+ var_interpolation =
143+ choice ( [
144+ # ${VAR} format - capture just the variable name
145+ ignore ( string ( "${" ) )
146+ |> concat ( var_name )
147+ |> ignore ( string ( "}" ) )
148+ |> tag ( :var_interp ) ,
149+ # $VAR format - capture just the variable name
150+ ignore ( string ( "$" ) )
151+ |> concat ( var_name )
152+ |> tag ( :var_interp )
153+ ] )
154+
155+ # Content inside double quotes: escape sequences, variable interpolation, or regular characters
156+ # Double quotes allow variable interpolation (like shell behavior)
141157 double_quoted_content =
142158 choice ( [
159+ var_interpolation ,
143160 escape_sequence ,
144161 double_quoted_regular_char
145162 ] )
146163
147164 # Content inside single quotes: either escape sequences or regular characters
165+ # Single quotes do NOT allow variable interpolation (like shell behavior)
148166 single_quoted_content =
149167 choice ( [
150168 escape_sequence ,
151169 single_quoted_regular_char
152170 ] )
153171
154- # Double-quoted value: "value with spaces"
155- # - Handles escape sequences and regular characters
156- # - Post-processes to convert escape sequences to actual characters
172+ # Double-quoted value: "value with spaces and $VAR interpolation "
173+ # - Handles escape sequences, variable interpolation, and regular characters
174+ # - Post-processes to build string with interpolations resolved
157175 double_quoted_value =
158176 ignore ( double_quote )
159177 |> times ( double_quoted_content , min: 0 )
160178 |> ignore ( double_quote )
161- |> reduce ( { List , :to_string , [ ] } )
162- |> post_traverse ( :process_escape_sequences )
179+ |> post_traverse ( :build_quoted_value )
163180
164- # Single-quoted value: 'value with spaces'
181+ # Single-quoted value: 'value with spaces, no interpolation '
165182 # - Handles escape sequences and regular characters
166183 # - Post-processes to convert escape sequences to actual characters
184+ # - Does NOT support variable interpolation (like shell behavior)
167185 single_quoted_value =
168186 ignore ( single_quote )
169187 |> times ( single_quoted_content , min: 0 )
@@ -175,29 +193,39 @@ defmodule Envious.Parser do
175193 # - Newline (\n) and carriage return (\r) - these end the value
176194 # - Hash (#) - this starts an inline comment
177195 # - Quotes (" and ') - these start quoted values
196+ # - Dollar ($) - this starts variable interpolation
197+ # - Brace ({, }) - reserved for ${VAR} syntax
178198 #
179199 # Character ranges:
180200 # - ?\s..?! is space (0x20) through exclamation (0x21), excluding double-quote (0x22)
181- # - ?$..?& is dollar through ampersand, excluding hash (0x23) and single-quote (0x27)
182- # - ?(..?~ is open-paren through tilde - includes alphanumeric and symbols
201+ # - ?%..?& is percent through ampersand, excluding hash (0x23) and single-quote (0x27)
202+ # - ?(..?z is open-paren through lowercase z, excluding braces ({ and })
203+ # - ?|..?~ is pipe through tilde
183204 unquoted_value_char =
184205 utf8_char ( [
185206 # Space (32) through exclamation (33), which excludes double-quote (34)
186207 ?\s .. ?! ,
187- # Dollar (36) through ampersand (38), which excludes hash (35) and single-quote (39)
188- ?$ .. ?& ,
189- # Open-paren (40) through tilde (126) - includes all alphanumeric and symbols
190- ?( .. ?~
208+ # Percent (37) through ampersand (38), which excludes hash (35), dollar (36), and single-quote (39)
209+ ?% .. ?& ,
210+ # Open-paren (40) through lowercase z (122), which excludes left-brace (123)
211+ ?( .. ?z ,
212+ # Pipe (124) through tilde (126), which excludes right-brace (125)
213+ ?| .. ?~
191214 ] )
192215
193- # Unquoted value: traditional unquoted values
194- # - Collect 0 or more value characters (allows empty values like KEY=)
195- # - Convert the character list to a string
196- # - Trim whitespace from both ends (handles inline comments: "value # comment")
216+ # Unquoted value content: either variable interpolation or regular characters
217+ unquoted_value_content =
218+ choice ( [
219+ var_interpolation ,
220+ unquoted_value_char
221+ ] )
222+
223+ # Unquoted value: supports variable interpolation
224+ # - Collect 0 or more value parts (allows empty values like KEY=)
225+ # - Post-process to trim whitespace (handles inline comments: "value # comment")
197226 unquoted_value =
198- times ( unquoted_value_char , min: 0 )
199- |> reduce ( { List , :to_string , [ ] } )
200- |> post_traverse ( :trim_value )
227+ times ( unquoted_value_content , min: 0 )
228+ |> post_traverse ( :build_unquoted_value )
201229
202230 # Parse the value portion after the = sign
203231 # Values can be:
@@ -274,28 +302,73 @@ defmodule Envious.Parser do
274302 defp not_line_terminator ( << ?\r , _ :: binary >> , context , _ , _ ) , do: { :halt , context }
275303 defp not_line_terminator ( _ , context , _ , _ ) , do: { :cont , context }
276304
277- # Post-traversal callback to trim whitespace from parsed values
305+ # Post-traversal callback to build a quoted value with variable interpolation support
278306 #
279- # This is used to remove trailing whitespace before inline comments.
280- # Example: "value # comment" becomes "value"
307+ # Takes the accumulated tokens (which may include :var_interp tags and character codes)
308+ # and builds a single string value. Variable interpolations are left as markers
309+ # to be resolved later by the main Envious module.
281310 #
282311 # Parameters:
283312 # - rest: Remaining input after parsing
284- # - [value]: The parsed value (as a single-element list from the accumulator)
313+ # - acc: Accumulated tokens from the quoted value
285314 # - context: Parser context
286315 # - _line, _offset: Position information (unused)
287316 #
288- # Returns: {rest, [trimmed_value], context}
317+ # Returns: {rest, [string_value], context}
318+ defp build_quoted_value ( rest , acc , context , _line , _offset ) do
319+ # Reverse the accumulator and build the string
320+ value = acc |> Enum . reverse ( ) |> build_value_string ( [ ] )
321+ { rest , [ value ] , context }
322+ end
323+
324+ # Post-traversal callback to build an unquoted value with variable interpolation support
325+ #
326+ # Similar to build_quoted_value but also trims whitespace.
289327 #
290- # Note: We must return `rest` (not an empty list) to allow the parser to
291- # continue processing remaining input. Returning [] would consume all input.
292- defp trim_value ( rest , [ value ] , context , _line , _offset ) when is_binary ( value ) do
293- { rest , [ String . trim ( value ) ] , context }
328+ # Parameters:
329+ # - rest: Remaining input after parsing
330+ # - acc: Accumulated tokens from the unquoted value
331+ # - context: Parser context
332+ # - _line, _offset: Position information (unused)
333+ #
334+ # Returns: {rest, [string_value], context}
335+ defp build_unquoted_value ( rest , acc , context , _line , _offset ) do
336+ # Reverse the accumulator and build the string, then trim
337+ value = acc |> Enum . reverse ( ) |> build_value_string ( [ ] ) |> String . trim ( )
338+ { rest , [ value ] , context }
294339 end
295340
296- # Fallback clause for trim_value when accumulator doesn't match expected pattern
297- defp trim_value ( rest , acc , context , _line , _offset ) do
298- { rest , acc , context }
341+ # Build a string from a list of tokens, handling variable interpolations
342+ # Tokens can be:
343+ # - Integers (character codes)
344+ # - {:var_interp, [var_name]} tuples representing variable interpolations
345+ # Returns a string with interpolation markers using a special syntax that won't conflict with user input
346+ defp build_value_string ( [ ] , acc ) do
347+ acc
348+ |> Enum . reverse ( )
349+ |> IO . iodata_to_binary ( )
350+ |> process_escapes_in_string ( [ ] )
351+ |> IO . iodata_to_binary ( )
352+ end
353+
354+ defp build_value_string ( [ { :var_interp , [ var_name ] } | rest ] , acc ) when is_binary ( var_name ) do
355+ # Use a special marker that won't conflict with normal .env content
356+ # Format: __ENVIOUS_VAR__[varname]__
357+ build_value_string ( rest , [ "__ENVIOUS_VAR__[#{ var_name } ]__" | acc ] )
358+ end
359+
360+ defp build_value_string ( [ char | rest ] , acc ) when is_integer ( char ) do
361+ build_value_string ( rest , [ << char :: utf8 >> | acc ] )
362+ end
363+
364+ defp build_value_string ( [ other | rest ] , acc ) do
365+ # Handle any other tokens (shouldn't normally happen)
366+ build_value_string ( rest , [ to_string ( other ) | acc ] )
367+ end
368+
369+ # Process escape sequences in a string (for values that support them)
370+ defp process_escapes_in_string ( binary , acc ) when is_binary ( binary ) do
371+ process_escapes ( binary , acc )
299372 end
300373
301374 # Post-traversal callback to convert [value, key] list to {key, value} tuple
0 commit comments