1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
    format_filesize, Category, Example, IntoPipelineData, PipelineData, PipelineMetadata,
    ShellError, Signature, Span, SyntaxShape, Value,
};
use std::iter;

#[derive(Clone)]
pub struct FileSize;

impl Command for FileSize {
    fn name(&self) -> &str {
        "format filesize"
    }

    fn signature(&self) -> Signature {
        Signature::build("format filesize")
            .required(
                "field",
                SyntaxShape::String,
                "the name of the column to update",
            )
            .required(
                "format value",
                SyntaxShape::String,
                "the format into which convert the filesizes",
            )
            .category(Category::Strings)
    }

    fn usage(&self) -> &str {
        "Converts a column of filesizes to some specified format"
    }

    fn search_terms(&self) -> Vec<&str> {
        vec!["convert", "display", "pattern", "human readable"]
    }

    fn run(
        &self,
        engine_state: &EngineState,
        stack: &mut Stack,
        call: &Call,
        input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let field = call.req::<Value>(engine_state, stack, 0)?.as_string()?;
        let format_value = call
            .req::<Value>(engine_state, stack, 1)?
            .as_string()?
            .to_ascii_lowercase();
        let span = call.head;
        let input_metadata = input.metadata();
        let data_as_value = input.into_value(span);

        // Something need to consider:
        // 1. what if input data type is not table?  For now just output nothing.
        // 2. what if value is not a FileSize type?  For now just return nothing too for the value.
        match data_as_value {
            Value::List { vals, span } => {
                format_impl(vals, field, format_value, span, input_metadata)
            }
            _ => Ok(Value::Nothing { span }.into_pipeline_data()),
        }
    }

    fn examples(&self) -> Vec<Example> {
        vec![
            Example {
                description: "Convert the size row to KB",
                example: "ls | format filesize size KB",
                result: None,
            },
            Example {
                description: "Convert the apparent row to B",
                example: "du | format filesize apparent B",
                result: None,
            },
        ]
    }
}

fn format_impl(
    vals: Vec<Value>,
    field: String,
    format_value: String,
    input_span: Span,
    input_metadata: Option<PipelineMetadata>,
) -> Result<PipelineData, ShellError> {
    let records: Vec<Value> = vals
        .into_iter()
        .map(|rec| {
            let record_span = rec.span();
            match rec {
                Value::Record { cols, vals, span } => {
                    let mut new_cols = vec![];
                    let mut new_vals = vec![];
                    for (c, v) in iter::zip(cols, vals) {
                        // find column to format, try format the value.
                        if c == field {
                            new_vals.push(format_value_impl(v, &format_value, span));
                        } else {
                            new_vals.push(v);
                        }
                        new_cols.push(c);
                    }
                    Value::Record {
                        cols: new_cols,
                        vals: new_vals,
                        span,
                    }
                }
                _ => Value::Nothing {
                    span: match record_span {
                        Ok(s) => s,
                        Err(_) => input_span,
                    },
                },
            }
        })
        .collect();

    let result = Value::List {
        vals: records,
        span: input_span,
    }
    .into_pipeline_data();
    Ok(result.set_metadata(input_metadata))
}

fn format_value_impl(val: Value, format_value: &str, span: Span) -> Value {
    match val {
        Value::Filesize { val, span } => Value::String {
            // don't need to concern about metric, we just format units by what user input.
            val: format_filesize(val, format_value, false),
            span,
        },
        _ => Value::Nothing { span },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_examples() {
        use crate::test_examples;

        test_examples(FileSize)
    }
}