@@ -2,80 +2,130 @@ use crate::parser::{ParseArg, Parser};
22use proc_macro2:: { token_stream, Delimiter , Ident , Literal , Span , TokenStream , TokenTree } ;
33use proc_macro_error2:: abort;
44use quote:: quote;
5- use std:: ffi:: OsString ;
65use std:: iter:: Peekable ;
6+ use std:: str:: Chars ;
77
8- // Scan string literal to tokenstream, used by most of the macros
9- //
10- // - support ${var} or $var for interpolation
11- // - to escape '$' itself, use "$$"
12- // - support normal rust character escapes:
13- // https://doc.rust-lang.org/reference/tokens.html#ascii-escapes
8+ /// Scan string literal to tokenstream, used by most of the macros
9+ ///
10+ /// - support $var, $ {var} or ${ var:?} for interpolation
11+ /// - to escape '$' itself, use "$$"
12+ /// - support normal rust character escapes:
13+ /// https://doc.rust-lang.org/reference/tokens.html#ascii-escapes
1414pub fn scan_str_lit ( lit : & Literal ) -> TokenStream {
1515 let s = lit. to_string ( ) ;
16+
17+ // If the literal is not a string (e.g., a number literal), treat it as a direct CmdString.
1618 if !s. starts_with ( '\"' ) {
1719 return quote ! ( :: cmd_lib:: CmdString :: from( #lit) ) ;
1820 }
19- let mut iter = s[ 1 ..s. len ( ) - 1 ] // To trim outside ""
20- . chars ( )
21- . peekable ( ) ;
21+
22+ // Extract the inner string by trimming the surrounding quotes.
23+ let inner_str = & s[ 1 ..s. len ( ) - 1 ] ;
24+ let mut chars = inner_str. chars ( ) . peekable ( ) ;
2225 let mut output = quote ! ( :: cmd_lib:: CmdString :: default ( ) ) ;
23- let mut last_part = OsString :: new ( ) ;
24- fn seal_last_part ( last_part : & mut OsString , output : & mut TokenStream ) {
26+ let mut current_literal_part = String :: new ( ) ;
27+
28+ // Helper function to append the accumulated literal part to the output TokenStream
29+ // and clear the current_literal_part.
30+ let seal_current_literal_part = |output : & mut TokenStream , last_part : & mut String | {
2531 if !last_part. is_empty ( ) {
26- let lit_str = format ! ( "\" {}\" " , last_part. to_str( ) . unwrap( ) ) ;
27- let l = syn:: parse_str :: < Literal > ( & lit_str) . unwrap ( ) ;
28- output. extend ( quote ! ( . append( #l) ) ) ;
32+ let lit_str = format ! ( "\" {}\" " , last_part) ;
33+ // It's safe to unwrap parse_str because we are constructing a valid string literal.
34+ let literal_token = syn:: parse_str :: < Literal > ( & lit_str) . unwrap ( ) ;
35+ output. extend ( quote ! ( . append( #literal_token) ) ) ;
2936 last_part. clear ( ) ;
3037 }
31- }
38+ } ;
3239
33- while let Some ( ch) = iter . next ( ) {
40+ while let Some ( ch) = chars . next ( ) {
3441 if ch == '$' {
35- if iter. peek ( ) == Some ( & '$' ) {
36- iter. next ( ) ;
37- last_part. push ( "$" ) ;
42+ // Handle "$$" for escaping '$'
43+ if chars. peek ( ) == Some ( & '$' ) {
44+ chars. next ( ) ; // Consume the second '$'
45+ current_literal_part. push ( '$' ) ;
3846 continue ;
3947 }
4048
41- seal_last_part ( & mut last_part, & mut output) ;
42- let mut with_brace = false ;
43- if iter. peek ( ) == Some ( & '{' ) {
44- with_brace = true ;
45- iter. next ( ) ;
46- }
47- let mut var = String :: new ( ) ;
48- while let Some ( & c) = iter. peek ( ) {
49- if !c. is_ascii_alphanumeric ( ) && c != '_' {
50- break ;
49+ // Before handling a variable, append any accumulated literal part.
50+ seal_current_literal_part ( & mut output, & mut current_literal_part) ;
51+
52+ let mut debug_format = false ; // New flag for debug formatting
53+
54+ // Check for '{' to start a braced interpolation
55+ if chars. peek ( ) == Some ( & '{' ) {
56+ chars. next ( ) ; // Consume '{'
57+
58+ let var_name = parse_variable_name ( & mut chars) ;
59+
60+ // After variable name, check for ':?' for debug formatting
61+ if chars. peek ( ) == Some ( & ':' ) {
62+ chars. next ( ) ; // Consume ':'
63+ if chars. peek ( ) == Some ( & '?' ) {
64+ chars. next ( ) ; // Consume '?'
65+ debug_format = true ;
66+ } else {
67+ // If it's ':' but not ':?', then it's a malformed substitution
68+ abort ! ( lit. span( ) , "bad substitution: expected '?' after ':'" ) ;
69+ }
5170 }
52- if var. is_empty ( ) && c. is_ascii_digit ( ) {
53- break ;
71+
72+ // Expect '}' to close the braced interpolation
73+ if chars. next ( ) != Some ( '}' ) {
74+ abort ! ( lit. span( ) , "bad substitution: expected '}'" ) ;
5475 }
55- var. push ( c) ;
56- iter. next ( ) ;
57- }
58- if with_brace {
59- if iter. peek ( ) != Some ( & '}' ) {
60- abort ! ( lit. span( ) , "bad substitution" ) ;
76+
77+ if !var_name. is_empty ( ) {
78+ let var_ident = syn:: parse_str :: < Ident > ( & var_name) . unwrap ( ) ;
79+ if debug_format {
80+ output. extend ( quote ! ( . append( format!( "{:?}" , #var_ident) ) ) ) ;
81+ } else {
82+ output. extend ( quote ! ( . append( format!( "{}" , #var_ident) ) ) ) ;
83+ }
6184 } else {
62- iter. next ( ) ;
85+ // This covers cases like "${}" or "${:?}" with empty variable name
86+ output. extend ( quote ! ( . append( "$" ) ) ) ;
6387 }
64- }
65- if !var. is_empty ( ) {
66- let var = syn:: parse_str :: < Ident > ( & var) . unwrap ( ) ;
67- output. extend ( quote ! ( . append( #var. as_os_str( ) ) ) ) ;
6888 } else {
69- output. extend ( quote ! ( . append( "$" ) ) ) ;
89+ // Handle bare $var (no braces)
90+ let var_name = parse_variable_name ( & mut chars) ;
91+ if !var_name. is_empty ( ) {
92+ let var_ident = syn:: parse_str :: < Ident > ( & var_name) . unwrap ( ) ;
93+ output. extend ( quote ! ( . append( format!( "{}" , #var_ident) ) ) ) ;
94+ } else {
95+ // If '$' is not followed by a valid variable name or a valid brace,
96+ // treat it as a literal "$".
97+ output. extend ( quote ! ( . append( "$" ) ) ) ;
98+ }
7099 }
71100 } else {
72- last_part . push ( ch. to_string ( ) ) ;
101+ current_literal_part . push ( ch) ;
73102 }
74103 }
75- seal_last_part ( & mut last_part, & mut output) ;
104+
105+ // Append any remaining literal part after the loop finishes.
106+ seal_current_literal_part ( & mut output, & mut current_literal_part) ;
76107 output
77108}
78109
110+ /// Parses a variable name from the character iterator.
111+ /// A variable name consists of alphanumeric characters and underscores,
112+ /// and cannot start with a digit.
113+ fn parse_variable_name ( chars : & mut Peekable < Chars < ' _ > > ) -> String {
114+ let mut var = String :: new ( ) ;
115+ while let Some ( & c) = chars. peek ( ) {
116+ if !( c. is_ascii_alphanumeric ( ) || c == '_' ) {
117+ break ;
118+ }
119+ if var. is_empty ( ) && c. is_ascii_digit ( ) {
120+ // Variable names cannot start with a digit
121+ break ;
122+ }
123+ var. push ( c) ;
124+ chars. next ( ) ; // Consume the character
125+ }
126+ var
127+ }
128+
79129enum SepToken {
80130 Space ,
81131 SemiColon ,
0 commit comments