Skip to main content

nu_command/conversions/
fill.rs

1use nu_cmd_base::input_handler::{CmdArgument, operate};
2use nu_engine::command_prelude::*;
3
4use print_positions::print_positions;
5
6#[derive(Clone)]
7pub struct Fill;
8
9struct Arguments {
10    width: usize,
11    alignment: FillAlignment,
12    character: String,
13    cell_paths: Option<Vec<CellPath>>,
14}
15
16impl CmdArgument for Arguments {
17    fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
18        self.cell_paths.take()
19    }
20}
21
22#[derive(Clone, Copy)]
23enum FillAlignment {
24    Left,
25    Right,
26    Middle,
27    MiddleRight,
28}
29
30impl Command for Fill {
31    fn name(&self) -> &str {
32        "fill"
33    }
34
35    fn description(&self) -> &str {
36        "Fill and align text in columns."
37    }
38
39    fn signature(&self) -> nu_protocol::Signature {
40        Signature::build("fill")
41            .input_output_types(vec![
42                (Type::Int, Type::String),
43                (Type::Float, Type::String),
44                (Type::String, Type::String),
45                (Type::Filesize, Type::String),
46                (
47                    Type::List(Box::new(Type::Int)),
48                    Type::List(Box::new(Type::String)),
49                ),
50                (
51                    Type::List(Box::new(Type::Float)),
52                    Type::List(Box::new(Type::String)),
53                ),
54                (
55                    Type::List(Box::new(Type::String)),
56                    Type::List(Box::new(Type::String)),
57                ),
58                (
59                    Type::List(Box::new(Type::Filesize)),
60                    Type::List(Box::new(Type::String)),
61                ),
62                // General case for heterogeneous lists
63                (
64                    Type::List(Box::new(Type::Any)),
65                    Type::List(Box::new(Type::String)),
66                ),
67            ])
68            .allow_variants_without_examples(true)
69            .named(
70                "width",
71                SyntaxShape::Int,
72                "The width of the output. Defaults to 1.",
73                Some('w'),
74            )
75            .param(
76                Flag::new("alignment")
77                    .short('a')
78                    .arg(SyntaxShape::String)
79                    .desc(
80                        "The alignment of the output. Defaults to Left (Left(l), Right(r), Center(c/m), MiddleRight(cr/mr)).",
81                    )
82                    .completion(Completion::new_list(&[
83                        "left",
84                        "right",
85                        "middle",
86                        "middleright",
87                    ])),
88            )
89            .named(
90                "character",
91                SyntaxShape::String,
92                "The character to fill with. Defaults to ' ' (space).",
93                Some('c'),
94            )
95            .category(Category::Conversions)
96    }
97
98    fn search_terms(&self) -> Vec<&str> {
99        vec!["display", "render", "format", "pad", "align", "repeat"]
100    }
101
102    fn examples(&self) -> Vec<Example<'_>> {
103        vec![
104            Example {
105                description: "Fill a string on the left side to a width of 15 with the character '─'.",
106                example: "'nushell' | fill --alignment l --character '─' --width 15",
107                result: Some(Value::string("nushell────────", Span::test_data())),
108            },
109            Example {
110                description: "Fill a string on the right side to a width of 15 with the character '─'.",
111                example: "'nushell' | fill --alignment r --character '─' --width 15",
112                result: Some(Value::string("────────nushell", Span::test_data())),
113            },
114            Example {
115                description: "Fill an empty string with 10 '─' characters.",
116                example: "'' | fill --character '─' --width 10",
117                result: Some(Value::string("──────────", Span::test_data())),
118            },
119            Example {
120                description: "Fill a number on the left side to a width of 5 with the character '0'.",
121                example: "1 | fill --alignment right --character '0' --width 5",
122                result: Some(Value::string("00001", Span::test_data())),
123            },
124            Example {
125                description: "Fill a number on both sides to a width of 5 with the character '0'.",
126                example: "1.1 | fill --alignment center --character '0' --width 5",
127                result: Some(Value::string("01.10", Span::test_data())),
128            },
129            Example {
130                description: "Fill a filesize on both sides to a width of 10 with the character '0'.",
131                example: "1kib | fill --alignment middle --character '0' --width 10",
132                result: Some(Value::string("0001024000", Span::test_data())),
133            },
134        ]
135    }
136
137    fn run(
138        &self,
139        engine_state: &EngineState,
140        stack: &mut Stack,
141        call: &Call,
142        input: PipelineData,
143    ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
144        fill(engine_state, stack, call, input)
145    }
146}
147
148fn fill(
149    engine_state: &EngineState,
150    stack: &mut Stack,
151    call: &Call,
152    input: PipelineData,
153) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
154    let width_arg: Option<usize> = call.get_flag(engine_state, stack, "width")?;
155    let alignment_arg: Option<String> = call.get_flag(engine_state, stack, "alignment")?;
156    let character_arg: Option<String> = call.get_flag(engine_state, stack, "character")?;
157    let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
158    let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
159
160    let alignment = if let Some(arg) = alignment_arg {
161        match arg.to_ascii_lowercase().as_str() {
162            "l" | "left" => FillAlignment::Left,
163            "r" | "right" => FillAlignment::Right,
164            "c" | "center" | "m" | "middle" => FillAlignment::Middle,
165            "cr" | "centerright" | "mr" | "middleright" => FillAlignment::MiddleRight,
166            _ => FillAlignment::Left,
167        }
168    } else {
169        FillAlignment::Left
170    };
171
172    let width = width_arg.unwrap_or(1);
173
174    let character = character_arg.unwrap_or_else(|| " ".to_string());
175
176    let arg = Arguments {
177        width,
178        alignment,
179        character,
180        cell_paths,
181    };
182
183    operate(action, arg, input, call.head, engine_state.signals())
184}
185
186fn action(input: &Value, args: &Arguments, span: Span) -> Value {
187    match input {
188        Value::Int { val, .. } => fill_int(*val, args, span),
189        Value::Filesize { val, .. } => fill_int(val.get(), args, span),
190        Value::Float { val, .. } => fill_float(*val, args, span),
191        Value::String { val, .. } => fill_string(val, args, span),
192        // Propagate errors by explicitly matching them before the final case.
193        Value::Error { .. } => input.clone(),
194        other => Value::error(
195            ShellError::OnlySupportsThisInputType {
196                exp_input_type: "int, filesize, float, string".into(),
197                wrong_type: other.get_type().to_string(),
198                dst_span: span,
199                src_span: other.span(),
200            },
201            span,
202        ),
203    }
204}
205
206fn fill_float(num: f64, args: &Arguments, span: Span) -> Value {
207    let s = num.to_string();
208    let out_str = pad(&s, args.width, &args.character, args.alignment, false);
209
210    Value::string(out_str, span)
211}
212fn fill_int(num: i64, args: &Arguments, span: Span) -> Value {
213    let s = num.to_string();
214    let out_str = pad(&s, args.width, &args.character, args.alignment, false);
215
216    Value::string(out_str, span)
217}
218fn fill_string(s: &str, args: &Arguments, span: Span) -> Value {
219    let out_str = pad(s, args.width, &args.character, args.alignment, false);
220
221    Value::string(out_str, span)
222}
223
224fn pad(s: &str, width: usize, pad_char: &str, alignment: FillAlignment, truncate: bool) -> String {
225    // Attribution: Most of this function was taken from https://github.com/ogham/rust-pad and tweaked. Thank you!
226    // Use width instead of len for graphical display
227
228    let cols = print_positions(s).count();
229
230    if cols >= width {
231        if truncate {
232            return s[..width].to_string();
233        } else {
234            return s.to_string();
235        }
236    }
237
238    let diff = width - cols;
239
240    let (left_pad, right_pad) = match alignment {
241        FillAlignment::Left => (0, diff),
242        FillAlignment::Right => (diff, 0),
243        FillAlignment::Middle => (diff / 2, diff - diff / 2),
244        FillAlignment::MiddleRight => (diff - diff / 2, diff / 2),
245    };
246
247    let mut new_str = String::new();
248    for _ in 0..left_pad {
249        new_str.push_str(pad_char)
250    }
251    new_str.push_str(s);
252    for _ in 0..right_pad {
253        new_str.push_str(pad_char)
254    }
255    new_str
256}
257
258#[cfg(test)]
259mod test {
260    use super::*;
261
262    #[test]
263    fn test_examples() {
264        use crate::test_examples;
265
266        test_examples(Fill {})
267    }
268}