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