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