Skip to content

Commit 0e67481

Browse files
committed
feat: support ${variable:?} in interpolation string
the same as in format! macro
1 parent cbb40df commit 0e67481

File tree

1 file changed

+96
-46
lines changed

1 file changed

+96
-46
lines changed

macros/src/lexer.rs

Lines changed: 96 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,130 @@ use crate::parser::{ParseArg, Parser};
22
use proc_macro2::{token_stream, Delimiter, Ident, Literal, Span, TokenStream, TokenTree};
33
use proc_macro_error2::abort;
44
use quote::quote;
5-
use std::ffi::OsString;
65
use 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
1414
pub 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+
79129
enum SepToken {
80130
Space,
81131
SemiColon,

0 commit comments

Comments
 (0)