1use crate::error::ExpressionError;
8use crate::function_library::EvalContext;
9use crate::value::ExprValue;
10
11type R = Result<ExprValue, ExpressionError>;
12type Ctx<'a> = &'a mut dyn EvalContext;
13
14fn repr_string_len(a: &ExprValue) -> usize {
15 match a {
16 ExprValue::String(s) => s.len(),
17 ExprValue::Path { value, .. } => value.len(),
18 _ => 0,
19 }
20}
21
22pub fn repr_py_fn(ctx: Ctx, a: &[ExprValue]) -> R {
23 ctx.count_string_ops(repr_string_len(&a[0]))?;
24 Ok(ExprValue::String(repr_py(&a[0])))
25}
26
27pub fn repr_json_fn(ctx: Ctx, a: &[ExprValue]) -> R {
28 ctx.count_string_ops(repr_string_len(&a[0]))?;
29 Ok(ExprValue::String(repr_json(&a[0])))
30}
31
32pub fn repr_sh_fn(ctx: Ctx, a: &[ExprValue]) -> R {
33 if a[0].is_list() {
34 ctx.count_ops(a[0].list_len().unwrap_or(0))?;
35 }
36 ctx.count_string_ops(repr_string_len(&a[0]))?;
37 repr_sh(&a[0]).map(ExprValue::String)
38}
39
40pub fn repr_cmd_fn(ctx: Ctx, a: &[ExprValue]) -> R {
41 ctx.count_string_ops(repr_string_len(&a[0]))?;
42 Ok(ExprValue::String(repr_cmd(&a[0])))
43}
44
45pub fn repr_pwsh_fn(ctx: Ctx, a: &[ExprValue]) -> R {
46 ctx.count_string_ops(repr_string_len(&a[0]))?;
47 Ok(ExprValue::String(repr_pwsh(&a[0])))
48}
49
50fn repr_py(val: &ExprValue) -> String {
51 match val {
52 ExprValue::String(s) => format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
53 ExprValue::Bool(b) => if *b { "True" } else { "False" }.to_string(),
54 ExprValue::Null => "None".to_string(),
55 ExprValue::Int(i) => i.to_string(),
56 ExprValue::Float(f) => {
57 if f.value().fract() == 0.0 {
58 format!("{:.1}", f)
59 } else {
60 f.to_string()
61 }
62 }
63 val if val.is_list() => {
64 let iter = val.list_iter().expect("is_list() was true");
65 format!(
66 "[{}]",
67 iter.map(|e| repr_py(&e)).collect::<Vec<_>>().join(", ")
68 )
69 }
70 ExprValue::Path { value, .. } => {
71 format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'"))
72 }
73 ExprValue::RangeExpr(r) => format!("'{r}'"),
74 _ => val.to_display_string(),
75 }
76}
77
78fn json_escape(s: &str) -> String {
81 use std::fmt::Write;
82 let mut out = String::with_capacity(s.len());
83 for c in s.chars() {
84 match c {
85 '"' => out.push_str("\\\""),
86 '\\' => out.push_str("\\\\"),
87 '\x08' => out.push_str("\\b"),
88 '\x0c' => out.push_str("\\f"),
89 '\n' => out.push_str("\\n"),
90 '\r' => out.push_str("\\r"),
91 '\t' => out.push_str("\\t"),
92 c if (c as u32) < 0x20 => {
93 let _ = write!(out, "\\u{:04x}", c as u32);
94 }
95 c if c.is_ascii() => out.push(c),
96 c => {
97 let mut buf = [0u16; 2];
98 for unit in c.encode_utf16(&mut buf) {
99 let _ = write!(out, "\\u{:04x}", unit);
100 }
101 }
102 }
103 }
104 out
105}
106
107fn repr_json(val: &ExprValue) -> String {
108 match val {
109 ExprValue::String(s) => format!("\"{}\"", json_escape(s)),
110 ExprValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
111 ExprValue::Null => "null".to_string(),
112 ExprValue::Int(i) => i.to_string(),
113 ExprValue::Float(f) => {
114 if f.value().fract() == 0.0 {
115 format!("{:.1}", f)
116 } else {
117 f.to_string()
118 }
119 }
120 val if val.is_list() => {
121 let iter = val.list_iter().expect("guard ensures list");
122 format!(
123 "[{}]",
124 iter.map(|e| repr_json(&e)).collect::<Vec<_>>().join(", ")
125 )
126 }
127 ExprValue::Path { value, .. } => format!("\"{}\"", json_escape(value)),
128 ExprValue::RangeExpr(r) => format!("\"{r}\""),
129 _ => val.to_display_string(),
130 }
131}
132
133fn repr_sh(val: &ExprValue) -> Result<String, ExpressionError> {
134 match val {
135 ExprValue::String(s) => shlex_quote(s),
136 ExprValue::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
137 ExprValue::Null => Ok("''".to_string()),
138 ExprValue::Int(i) => Ok(i.to_string()),
139 ExprValue::Float(f) => Ok(if f.value().fract() == 0.0 {
140 format!("{:.1}", f)
141 } else {
142 f.to_string()
143 }),
144 ExprValue::Path { value, .. } => shlex_quote(value),
145 val if val.is_list() => {
146 let strs: Vec<String> = val
147 .list_iter()
148 .expect("guard ensures list")
149 .map(|e| match &e {
150 ExprValue::String(s) | ExprValue::Path { value: s, .. } => s.clone(),
151 other => other.to_display_string(),
152 })
153 .collect();
154 shlex::try_join(strs.iter().map(|s| s.as_str()))
155 .map_err(|e| ExpressionError::new(format!("Cannot shell-quote list: {e}")))
156 }
157 _ => Ok(val.to_display_string()),
158 }
159}
160
161fn shlex_quote(s: &str) -> Result<String, ExpressionError> {
163 shlex::try_quote(s)
164 .map(|c| c.into_owned())
165 .map_err(|e| ExpressionError::new(format!("Cannot shell-quote string: {e}")))
166}
167
168fn repr_cmd(val: &ExprValue) -> String {
169 match val {
170 ExprValue::String(s) => cmd_quote(s),
171 ExprValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
172 ExprValue::Null => "\"\"".to_string(),
173 ExprValue::Int(i) => i.to_string(),
174 ExprValue::Float(f) => {
175 if f.value().fract() == 0.0 {
176 format!("{:.1}", f)
177 } else {
178 f.to_string()
179 }
180 }
181 ExprValue::Path { value, .. } => cmd_quote(value),
182 val if val.is_list() => val
183 .list_iter()
184 .expect("guard ensures list")
185 .map(|e| match &e {
186 ExprValue::String(s) | ExprValue::Path { value: s, .. } => cmd_quote(s),
187 other => other.to_display_string(),
188 })
189 .collect::<Vec<_>>()
190 .join(" "),
191 _ => val.to_display_string(),
192 }
193}
194
195fn repr_pwsh(val: &ExprValue) -> String {
196 match val {
197 ExprValue::String(s) => format!("'{}'", s.replace('\'', "''")),
198 ExprValue::Bool(b) => if *b { "$true" } else { "$false" }.to_string(),
199 ExprValue::Null => "$null".to_string(),
200 ExprValue::Int(i) => i.to_string(),
201 ExprValue::Float(f) => {
202 if f.value().fract() == 0.0 {
203 format!("{:.1}", f)
204 } else {
205 f.to_string()
206 }
207 }
208 ExprValue::Path { value, .. } => format!("'{}'", value.replace('\'', "''")),
209 val if val.is_list() => {
210 let items: Vec<String> = val
211 .list_iter()
212 .expect("guard ensures list")
213 .map(|e| match &e {
214 ExprValue::String(s) | ExprValue::Path { value: s, .. } => {
215 format!("'{}'", s.replace('\'', "''"))
216 }
217 ExprValue::Bool(b) => if *b { "$true" } else { "$false" }.to_string(),
218 ExprValue::Null => "$null".to_string(),
219 ExprValue::Int(i) => i.to_string(),
220 other => other.to_display_string(),
221 })
222 .collect();
223 format!("@({})", items.join(", "))
224 }
225 ExprValue::RangeExpr(r) => format!("'{r}'"),
226 _ => val.to_display_string(),
227 }
228}
229
230fn cmd_quote(s: &str) -> String {
231 let stripped: String = s.chars().filter(|c| *c != '\n' && *c != '\r').collect();
235 const NEEDS_QUOTING: &str = " \t&|<>^\"()%!";
236 if !stripped.is_empty() && !stripped.chars().any(|c| NEEDS_QUOTING.contains(c)) {
237 return stripped;
238 }
239 let mut escaped = String::with_capacity(stripped.len());
240 for c in stripped.chars() {
241 match c {
242 '^' | '"' => {
243 escaped.push('^');
244 escaped.push(c);
245 }
246 '%' => escaped.push_str("%%"),
247 '!' => escaped.push_str("^^!"),
250 c => escaped.push(c),
251 }
252 }
253 format!("\"{escaped}\"")
254}