1use nu_cmd_base::input_handler::{CmdArgument, operate};
2use nu_engine::command_prelude::*;
3use nu_protocol::Config;
4use nu_utils::get_system_locale;
5use num_format::ToFormattedString;
6use std::sync::Arc;
7
8struct Arguments {
9 decimals_value: Option<i64>,
10 cell_paths: Option<Vec<CellPath>>,
11 config: Arc<Config>,
12 group_digits: bool,
13}
14
15impl CmdArgument for Arguments {
16 fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
17 self.cell_paths.take()
18 }
19}
20
21#[derive(Clone)]
22pub struct IntoString;
23
24impl Command for IntoString {
25 fn name(&self) -> &str {
26 "into string"
27 }
28
29 fn signature(&self) -> Signature {
30 Signature::build("into string")
31 .input_output_types(vec![
32 (Type::Binary, Type::String),
33 (Type::Int, Type::String),
34 (Type::Number, Type::String),
35 (Type::String, Type::String),
36 (Type::Glob, Type::String),
37 (Type::Bool, Type::String),
38 (Type::Filesize, Type::String),
39 (Type::Date, Type::String),
40 (Type::Duration, Type::String),
41 (Type::CellPath, Type::String),
42 (Type::Range, Type::String),
43 (
44 Type::List(Box::new(Type::Any)),
45 Type::List(Box::new(Type::String)),
46 ),
47 (Type::table(), Type::table()),
48 (Type::record(), Type::record()),
49 ])
50 .allow_variants_without_examples(true) .rest(
52 "rest",
53 SyntaxShape::CellPath,
54 "For a data structure input, convert data at the given cell paths.",
55 )
56 .switch(
57 "group-digits",
58 "group digits together by the locale specific thousands separator",
59 Some('g'),
60 )
61 .named(
62 "decimals",
63 SyntaxShape::Int,
64 "decimal digits to which to round",
65 Some('d'),
66 )
67 .category(Category::Conversions)
68 }
69
70 fn description(&self) -> &str {
71 "Convert value to string."
72 }
73
74 fn search_terms(&self) -> Vec<&str> {
75 vec!["convert", "text"]
76 }
77
78 fn run(
79 &self,
80 engine_state: &EngineState,
81 stack: &mut Stack,
82 call: &Call,
83 input: PipelineData,
84 ) -> Result<PipelineData, ShellError> {
85 string_helper(engine_state, stack, call, input)
86 }
87
88 fn examples(&self) -> Vec<Example<'_>> {
89 vec![
90 Example {
91 description: "convert int to string and append three decimal places",
92 example: "5 | into string --decimals 3",
93 result: Some(Value::test_string("5.000")),
94 },
95 Example {
96 description: "convert float to string and round to nearest integer",
97 example: "1.7 | into string --decimals 0",
98 result: Some(Value::test_string("2")),
99 },
100 Example {
101 description: "convert float to string",
102 example: "1.7 | into string --decimals 1",
103 result: Some(Value::test_string("1.7")),
104 },
105 Example {
106 description: "convert float to string and limit to 2 decimals",
107 example: "1.734 | into string --decimals 2",
108 result: Some(Value::test_string("1.73")),
109 },
110 Example {
111 description: "convert float to string",
112 example: "4.3 | into string",
113 result: Some(Value::test_string("4.3")),
114 },
115 Example {
116 description: "convert string to string",
117 example: "'1234' | into string",
118 result: Some(Value::test_string("1234")),
119 },
120 Example {
121 description: "convert boolean to string",
122 example: "true | into string",
123 result: Some(Value::test_string("true")),
124 },
125 Example {
126 description: "convert date to string",
127 example: "'2020-10-10 10:00:00 +02:00' | into datetime | into string",
128 result: Some(Value::test_string("Sat Oct 10 10:00:00 2020")),
129 },
130 Example {
131 description: "convert filepath to string",
132 example: "ls Cargo.toml | get name | into string",
133 result: None,
134 },
135 Example {
136 description: "convert filesize to string",
137 example: "1kB | into string",
138 result: Some(Value::test_string("1.0 kB")),
139 },
140 Example {
141 description: "convert duration to string",
142 example: "9day | into string",
143 result: Some(Value::test_string("1wk 2day")),
144 },
145 Example {
146 description: "convert cell-path to string",
147 example: "$.name | into string",
148 result: Some(Value::test_string("$.name")),
149 },
150 ]
151 }
152}
153
154fn string_helper(
155 engine_state: &EngineState,
156 stack: &mut Stack,
157 call: &Call,
158 input: PipelineData,
159) -> Result<PipelineData, ShellError> {
160 let head = call.head;
161 let decimals_value: Option<i64> = call.get_flag(engine_state, stack, "decimals")?;
162 let group_digits = call.has_flag(engine_state, stack, "group-digits")?;
163 if let Some(decimal_val) = decimals_value
164 && decimal_val.is_negative()
165 {
166 return Err(ShellError::TypeMismatch {
167 err_message: "Cannot accept negative integers for decimals arguments".to_string(),
168 span: head,
169 });
170 }
171 let cell_paths = call.rest(engine_state, stack, 0)?;
172 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
173
174 if let PipelineData::ByteStream(stream, metadata) = input {
175 if stream.type_().is_string_coercible() {
179 Ok(PipelineData::byte_stream(
180 stream.with_type(ByteStreamType::String),
181 metadata,
182 ))
183 } else {
184 Err(ShellError::CantConvert {
185 to_type: "string".into(),
186 from_type: "binary".into(),
187 span: stream.span(),
188 help: Some("try using the `decode` command".into()),
189 })
190 }
191 } else {
192 let config = stack.get_config(engine_state);
193 let args = Arguments {
194 decimals_value,
195 cell_paths,
196 config,
197 group_digits,
198 };
199 operate(action, args, input, head, engine_state.signals())
200 }
201}
202
203fn action(input: &Value, args: &Arguments, span: Span) -> Value {
204 let digits = args.decimals_value;
205 let config = &args.config;
206 let group_digits = args.group_digits;
207
208 match input {
209 Value::Int { val, .. } => {
210 let decimal_value = digits.unwrap_or(0) as usize;
211 let res = format_int(*val, group_digits, decimal_value);
212 Value::string(res, span)
213 }
214 Value::Float { val, .. } => {
215 if let Some(decimal_value) = digits {
216 let decimal_value = decimal_value as usize;
217 Value::string(format!("{val:.decimal_value$}"), span)
218 } else {
219 Value::string(val.to_string(), span)
220 }
221 }
222 Value::Bool { val, .. } => Value::string(val.to_string(), span),
223 Value::Date { val, .. } => Value::string(val.format("%c").to_string(), span),
224 Value::String { val, .. } => Value::string(val, span),
225 Value::Glob { val, .. } => Value::string(val, span),
226 Value::CellPath { val, .. } => Value::string(val.to_string(), span),
227 Value::Filesize { val, .. } => {
228 if group_digits {
229 let decimal_value = digits.unwrap_or(0) as usize;
230 Value::string(format_int(val.get(), group_digits, decimal_value), span)
231 } else {
232 Value::string(input.to_expanded_string(", ", config), span)
233 }
234 }
235 Value::Duration { val: _, .. } => Value::string(input.to_expanded_string("", config), span),
236 Value::Nothing { .. } => Value::string("".to_string(), span),
237 Value::Record { .. } => Value::error(
238 ShellError::CantConvert {
240 to_type: "string".into(),
241 from_type: "record".into(),
242 span,
243 help: Some("try using the `to nuon` command".into()),
244 },
245 span,
246 ),
247 Value::Binary { .. } => Value::error(
248 ShellError::CantConvert {
249 to_type: "string".into(),
250 from_type: "binary".into(),
251 span,
252 help: Some("try using the `decode` command".into()),
253 },
254 span,
255 ),
256 Value::Custom { val, .. } => {
257 val.to_base_value(input.span())
260 .and_then(|base_value| match action(&base_value, args, span) {
261 Value::Error { .. } => Err(ShellError::CantConvert {
262 to_type: String::from("string"),
263 from_type: val.type_name(),
264 span,
265 help: Some("this custom value can't be represented as a string".into()),
266 }),
267 success => Ok(success),
268 })
269 .unwrap_or_else(|err| Value::error(err, span))
270 }
271 Value::Error { .. } => input.clone(),
272 x => Value::error(
273 ShellError::CantConvert {
274 to_type: String::from("string"),
275 from_type: x.get_type().to_string(),
276 span,
277 help: None,
278 },
279 span,
280 ),
281 }
282}
283
284fn format_int(int: i64, group_digits: bool, decimals: usize) -> String {
285 let locale = get_system_locale();
286
287 let str = if group_digits {
288 int.to_formatted_string(&locale)
289 } else {
290 int.to_string()
291 };
292
293 if decimals > 0 {
294 let decimal_point = locale.decimal();
295
296 format!(
297 "{}{decimal_point}{dummy:0<decimals$}",
298 str,
299 decimal_point = decimal_point,
300 dummy = "",
301 decimals = decimals
302 )
303 } else {
304 str
305 }
306}
307
308#[cfg(test)]
309mod test {
310 use super::*;
311
312 #[test]
313 fn test_examples() {
314 use crate::test_examples;
315
316 test_examples(IntoString {})
317 }
318}