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