use regex::Regex;
use std::collections::HashSet;
pub fn should_skip_line(line: &str) -> bool {
if line.trim_start().starts_with('#') {
return true;
}
if line.contains('=') && !line.contains("if [") && !line.contains("[ ") {
if let Some(eq_pos) = line.find('=') {
if let Some(first_space) = line.find(' ') {
if eq_pos < first_space {
return true; }
}
}
}
false
}
pub fn find_dollar_position(line: &str, var_start: usize) -> usize {
line[..var_start].rfind('$').unwrap_or(var_start)
}
pub fn calculate_end_column(line: &str, var_end: usize, is_braced: bool) -> usize {
if is_braced {
let after_var = &line[var_end..];
if let Some(brace_pos) = after_var.find('}') {
var_end + brace_pos + 2 } else {
var_end + 1 }
} else {
var_end + 1 }
}
pub fn is_in_arithmetic_context(line: &str, dollar_pos: usize, var_end: usize) -> bool {
let before = &line[..dollar_pos];
let after = &line[var_end..];
if before.contains("$((") && after.contains("))") {
return true;
}
if let Some(paren_pos) = before.rfind("((") {
let is_standalone = if paren_pos > 0 {
!before[..paren_pos].ends_with('$')
} else {
true
};
if is_standalone && after.contains("))") {
return true;
}
}
false
}
pub fn get_cstyle_for_loop_vars(source: &str) -> HashSet<String> {
#[allow(clippy::unwrap_used)] static CSTYLE_FOR: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\bfor\s*\(\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*=").unwrap()
});
let mut vars = HashSet::new();
for cap in CSTYLE_FOR.captures_iter(source) {
if let Some(m) = cap.get(1) {
vars.insert(m.as_str().to_string());
}
}
vars
}
pub fn is_in_double_bracket_context(line: &str, dollar_pos: usize, var_end: usize) -> bool {
let before = &line[..dollar_pos];
let after = &line[var_end..];
if let Some(open_pos) = before.rfind("[[") {
let is_double = if open_pos > 0 {
!before[..open_pos].ends_with('[')
} else {
true
};
if is_double && after.contains("]]") {
return true;
}
}
false
}
fn is_immediately_quoted(before_context: &str, after_context: &str) -> bool {
if before_context.ends_with('"') && after_context.starts_with('"') {
return true;
}
if after_context.starts_with('}') {
if let Some(brace_pos) = after_context.find('}') {
let after_brace = &after_context[brace_pos + 1..];
if before_context.ends_with('"') && after_brace.starts_with('"') {
return true;
}
}
}
false
}
fn count_unescaped_quotes(s: &str) -> usize {
let mut count = 0;
let mut escaped = false;
for ch in s.chars() {
if escaped {
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if ch == '"' {
count += 1;
}
}
count
}
fn is_inside_quoted_string(before_context: &str, after_context: &str) -> bool {
let quote_count = count_unescaped_quotes(before_context);
if quote_count.is_multiple_of(2) {
return false;
}
if after_context.starts_with('}') {
if let Some(brace_pos) = after_context.find('}') {
return after_context[brace_pos + 1..].contains('"');
}
}
after_context.contains('"')
}
pub fn is_already_quoted(line: &str, dollar_pos: usize, var_end: usize) -> bool {
let before_context = &line[..dollar_pos];
let after_context = &line[var_end..];
is_immediately_quoted(before_context, after_context)
|| is_inside_quoted_string(before_context, after_context)
}
pub fn format_var_text(var_name: &str, is_braced: bool) -> String {
if is_braced {
format!("${{{}}}", var_name)
} else {
format!("${}", var_name)
}
}
pub fn format_quoted_var(var_name: &str, is_braced: bool) -> String {
format!("\"{}\"", format_var_text(var_name, is_braced))
}
pub fn line_has_arithmetic_markers(line: &str) -> bool {
line.contains("$((") || line.contains("((")
}
pub struct UnquotedVar {
pub var_name: String,
pub col: usize,
pub end_col: usize,
pub is_braced: bool,
}
#[allow(clippy::unwrap_used)] pub fn get_var_pattern() -> Regex {
Regex::new(r#"(?m)(?P<pre>[^"']|^)\$(?:\{(?P<brace>[A-Za-z_][A-Za-z0-9_]*)\}|(?P<simple>[A-Za-z_][A-Za-z0-9_]*))"#).unwrap()
}
pub fn find_unquoted_vars(
line: &str,
pattern: &Regex,
cstyle_vars: &HashSet<String>,
) -> Vec<UnquotedVar> {
let mut result = Vec::new();
if should_skip_line(line) {
return result;
}
let is_arithmetic = line_has_arithmetic_markers(line);
for cap in pattern.captures_iter(line) {
let var_capture = match cap.name("brace").or_else(|| cap.name("simple")) {
Some(v) => v,
None => continue,
};
let var_name = var_capture.as_str();
let dollar_pos = find_dollar_position(line, var_capture.start());
let col = dollar_pos + 1;
let is_braced = cap.name("brace").is_some();
let end_col = calculate_end_column(line, var_capture.end(), is_braced);
if is_arithmetic && is_in_arithmetic_context(line, dollar_pos, var_capture.end()) {
continue;
}
if is_already_quoted(line, dollar_pos, var_capture.end()) {
continue;
}
if is_in_double_bracket_context(line, dollar_pos, var_capture.end()) {
continue;
}
if cstyle_vars.contains(var_name) {
continue;
}
result.push(UnquotedVar {
var_name: var_name.to_string(),
col,
end_col,
is_braced,
});
}
result
}
#[cfg(test)]
#[path = "sc2086_logic_tests.rs"]
mod tests;