@@ -2,80 +2,130 @@ use crate::parser::{ParseArg, Parser};
2
2
use proc_macro2:: { token_stream, Delimiter , Ident , Literal , Span , TokenStream , TokenTree } ;
3
3
use proc_macro_error2:: abort;
4
4
use quote:: quote;
5
- use std:: ffi:: OsString ;
6
5
use std:: iter:: Peekable ;
6
+ use std:: str:: Chars ;
7
7
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
14
14
pub fn scan_str_lit ( lit : & Literal ) -> TokenStream {
15
15
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.
16
18
if !s. starts_with ( '\"' ) {
17
19
return quote ! ( :: cmd_lib:: CmdString :: from( #lit) ) ;
18
20
}
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 ( ) ;
22
25
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 | {
25
31
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) ) ) ;
29
36
last_part. clear ( ) ;
30
37
}
31
- }
38
+ } ;
32
39
33
- while let Some ( ch) = iter . next ( ) {
40
+ while let Some ( ch) = chars . next ( ) {
34
41
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 ( '$' ) ;
38
46
continue ;
39
47
}
40
48
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
+ }
51
70
}
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 '}'" ) ;
54
75
}
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
+ }
61
84
} else {
62
- iter. next ( ) ;
85
+ // This covers cases like "${}" or "${:?}" with empty variable name
86
+ output. extend ( quote ! ( . append( "$" ) ) ) ;
63
87
}
64
- }
65
- if !var. is_empty ( ) {
66
- let var = syn:: parse_str :: < Ident > ( & var) . unwrap ( ) ;
67
- output. extend ( quote ! ( . append( #var. as_os_str( ) ) ) ) ;
68
88
} 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
+ }
70
99
}
71
100
} else {
72
- last_part . push ( ch. to_string ( ) ) ;
101
+ current_literal_part . push ( ch) ;
73
102
}
74
103
}
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) ;
76
107
output
77
108
}
78
109
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
+
79
129
enum SepToken {
80
130
Space ,
81
131
SemiColon ,
0 commit comments