nu_command/conversions/into/
string.rs

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) // https://github.com/nushell/nushell/issues/7032
51            .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        // Just set the type - that should be good enough. There is no guarantee that the data
176        // within a string stream is actually valid UTF-8. But refuse to do it if it was already set
177        // to binary
178        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            // Watch out for CantConvert's argument order
239            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            // Only custom values that have a base value that can be converted to string are
258            // accepted.
259            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}