use crate::field::{FieldRange, get_string_by_field};
use crate::helper::item::strip_ansi;
use crate::item::MatchedItem;
use regex::Regex;
use std::fmt::Write as _;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::prelude::v1::*;
#[cfg(feature = "cli")]
pub fn unescape_delimiter(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('x') => {
let hex: String = chars.by_ref().take(2).collect();
if hex.len() == 2 {
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte as char);
} else {
result.push('\\');
result.push('x');
result.push_str(&hex);
}
} else {
result.push('\\');
result.push('x');
result.push_str(&hex);
}
}
Some('t') => result.push('\t'),
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('\\') | None => result.push('\\'),
Some(other) => {
result.push('\\');
result.push(other);
}
}
} else {
result.push(c);
}
}
result
}
pub fn read_file_lines(filename: &str) -> std::result::Result<Vec<String>, std::io::Error> {
let file = File::open(filename)?;
BufReader::new(file).lines().collect()
}
#[cfg(any(windows, test))]
fn quote_cmd_arg(s: &str) -> String {
let mut quoted = String::from("\"");
let mut slashes = 0;
for c in s.chars() {
if c == '"' {
quoted.extend(std::iter::repeat_n('\\', slashes + 1));
}
quoted.push(c);
slashes = if c == '\\' { slashes + 1 } else { 0 };
}
quoted.extend(std::iter::repeat_n('\\', slashes));
quoted.push('"');
let mut escaped = String::new();
for c in quoted.chars() {
if "&|<>()^%!\"".contains(c) {
escaped.push('^');
}
escaped.push(c);
}
escaped
}
fn quote_shell_arg(s: &str) -> String {
#[cfg(windows)]
{
quote_cmd_arg(s)
}
#[cfg(not(windows))]
{
format!("'{}'", s.replace('\'', "'\\''"))
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
pub fn printf<'a>(
pattern: &str,
delimiter: &Regex,
replstr: &str,
selected: &(impl Iterator<Item = &'a MatchedItem> + std::clone::Clone),
current: &Option<MatchedItem>,
query: &str,
command_query: &str,
quote_args: bool,
) -> String {
let escape_arg = |s: &str, quote: bool| {
let mut res = s.replace('\0', "\\0").clone();
if quote && quote_args {
res = quote_shell_arg(&res);
}
res
};
let item_text = current.as_ref().map(|s| strip_ansi(&s.output()).0).unwrap_or_default();
let escaped_item = escape_arg(&item_text, true);
let escaped_query = escape_arg(query, true);
let escaped_cmd_query = escape_arg(command_query, true);
let replstr_parts = pattern.split(replstr);
let mut replaced_parts = Vec::new();
for part in replstr_parts {
let mut sub = part.split('{');
let mut replaced = sub.next().unwrap_or_default().to_string();
for s in sub {
let mut inside = true;
let mut content = String::new();
for c in s.chars() {
if inside {
if c == '}' {
match content.as_str() {
"" => replaced.push_str("{}"),
"q" => replaced.push_str(&escaped_query),
"cq" => replaced.push_str(&escaped_cmd_query),
"n" if current.as_ref().is_some() => {
replaced.push_str(
current
.as_ref()
.map(|i| i.rank.index)
.unwrap_or_default()
.to_string()
.as_str(),
);
}
s if s == "+n" || s.starts_with("+n:") || s == "+" || s.starts_with("+:") => {
let is_n = s.starts_with("+n");
let accessor = if is_n {
|i: &MatchedItem| i.rank.index.to_string()
} else {
|i: &MatchedItem| strip_ansi(&i.output()).0
};
let mut quote_individually = false;
let delim = s.rsplit_once(':').map_or_else(
|| {
quote_individually = quote_args;
" "
},
|x| x.1,
);
let mut expanded = selected
.clone()
.map(|i| escape_arg(&accessor(i), quote_individually))
.reduce(|a: String, b| a.clone() + delim + b.as_str())
.unwrap_or_default();
if expanded.is_empty() {
expanded = current
.as_ref()
.map(|i| escape_arg(&accessor(i), quote_individually))
.unwrap_or_default();
}
if quote_args && !quote_individually {
replaced.push_str("e_shell_arg(&expanded));
} else {
replaced.push_str(&expanded);
}
}
s => {
let (is_plus, stripped) = match s.strip_prefix('+') {
Some(x) => (true, x),
None => (false, s),
};
if is_plus {
let mut quote_individually = false;
let (stripped, delim) = stripped.rsplit_once(':').unwrap_or_else(|| {
quote_individually = quote_args;
(stripped, " ")
});
if let Some(range) = FieldRange::from_str(stripped) {
let expanded = selected
.clone()
.map(|i| {
escape_arg(
get_string_by_field(delimiter, &strip_ansi(&i.output()).0, &range)
.unwrap_or_default(),
quote_individually,
)
})
.reduce(|a: String, b| a.clone() + delim + b.as_str())
.unwrap_or_default();
if quote_args && !quote_individually {
replaced.push_str("e_shell_arg(&expanded));
} else {
replaced.push_str(&expanded);
}
} else {
log::warn!("Failed to build multi-item field range from {content}");
let _ = write!(replaced, "{{{s}}}");
}
} else if let Some(range) = FieldRange::from_str(stripped) {
let replacement =
get_string_by_field(delimiter, &item_text, &range).unwrap_or_default();
replaced.push_str(&escape_arg(replacement, true));
} else {
log::warn!("Failed to build field range from {content}");
let _ = write!(replaced, "{{{s}}}");
}
}
}
content.clear();
inside = false;
} else {
content.push(c);
}
} else if c == '{' {
inside = true;
} else {
replaced.push(c);
}
}
}
replaced_parts.push(replaced);
}
replaced_parts
.into_iter()
.reduce(|a: String, b| a + &escaped_item + &b)
.unwrap_or_default()
}
#[cfg(test)]
#[path = "util_tests.rs"]
mod test;