From 92511b09c4e33d8173b5185be202888c4cddca47 Mon Sep 17 00:00:00 2001 From: Andrew Prokhorenkov Date: Sun, 27 Sep 2020 19:12:42 +0200 Subject: [PATCH] New command "string pad" to pad text to a given width (#7340) Pads text to a given width, or the maximum width of all inputs. --- doc_src/cmds/string-pad.rst | 50 ++++++++++++++++++++ doc_src/cmds/string.rst | 16 +++++++ src/builtin_string.cpp | 91 +++++++++++++++++++++++++++++++++++-- tests/checks/string.fish | 33 ++++++++++++-- 4 files changed, 180 insertions(+), 10 deletions(-) create mode 100644 doc_src/cmds/string-pad.rst diff --git a/doc_src/cmds/string-pad.rst b/doc_src/cmds/string-pad.rst new file mode 100644 index 000000000..42fdcf11a --- /dev/null +++ b/doc_src/cmds/string-pad.rst @@ -0,0 +1,50 @@ +string-pad - pad characters before and after string +=================================================== + +Synopsis +-------- + +.. BEGIN SYNOPSIS + +:: + + string pad [(-r | --right)] [(-c | --char) CHAR] [(-w | --width) INTEGER] [STRING...] + +.. END SYNOPSIS + +Description +----------- + +.. BEGIN DESCRIPTION + +``string pad`` pads each STRING with CHAR to the given width. + +The default behavior is left padding with spaces and default width is the length of string (hence, no padding). + +If ``-r`` or ``--right`` is given, only pad after string. + +The ``-c`` or ``--char`` switch causes padding with the character CHAR instead of default whitespace character. + +If ``-w`` or ``--width`` is given, pad the string to given width. Width less than the string width will result in an unchanged string. + +.. END DESCRIPTION + +Examples +-------- + +.. BEGIN EXAMPLES + +:: + + >_ string pad -w 10 -c ' ' 'abc' + abc + + >_ string pad --right --width 12 --char=z foo barbaz + foozzzzzzzzz + barbazzzzzzz + + >_ string pad -w 6 --char=- foo | string pad --right -w 9 --char=- + ---foo--- + + +.. END EXAMPLES diff --git a/doc_src/cmds/string.rst b/doc_src/cmds/string.rst index ceea76f6b..f5f521b77 100644 --- a/doc_src/cmds/string.rst +++ b/doc_src/cmds/string.rst @@ -15,6 +15,7 @@ Synopsis string length [(-q | --quiet)] [STRING...] string lower [(-q | --quiet)] [STRING...] string match [(-a | --all)] [(-e | --entire)] [(-i | --ignore-case)] [(-r | --regex)] [(-n | --index)] [(-q | --quiet)] [(-v | --invert)] PATTERN [STRING...] + string pad [(-r | --right)] [(-c | --char) CHAR] [(-w | --width) INTEGER] [STRING...] string repeat [(-n | --count) COUNT] [(-m | --max) MAX] [(-N | --no-newline)] [(-q | --quiet)] [STRING...] string replace [(-a | --all)] [(-f | --filter)] [(-i | --ignore-case)] [(-r | --regex)] [(-q | --quiet)] PATTERN REPLACEMENT [STRING...] string split [(-m | --max) MAX] [(-n | --no-empty)] [(-q | --quiet)] [(-r | --right)] SEP [STRING...] @@ -141,6 +142,21 @@ Examples :start-after: BEGIN EXAMPLES :end-before: END EXAMPLES +"pad" subcommand +------------------ + +.. include:: string-pad.rst + :start-after: BEGIN SYNOPSIS + :end-before: END SYNOPSIS + +.. include:: string-pad.rst + :start-after: BEGIN DESCRIPTION + :end-before: END DESCRIPTION + +.. include:: string-pad.rst + :start-after: BEGIN EXAMPLES + :end-before: END EXAMPLES + "repeat" subcommand ------------------- diff --git a/src/builtin_string.cpp b/src/builtin_string.cpp index 330808129..8634a15ce 100644 --- a/src/builtin_string.cpp +++ b/src/builtin_string.cpp @@ -135,7 +135,8 @@ class arg_iterator_t { // valid and get the result of parsing the command for flags. using options_t = struct options_t { //!OCLINT(too many fields) bool all_valid = false; - bool chars_valid = false; + bool char_to_pad_valid = false; + bool chars_to_trim_valid = false; bool count_valid = false; bool entire_valid = false; bool filter_valid = false; @@ -157,6 +158,7 @@ using options_t = struct options_t { //!OCLINT(too many fields) bool no_trim_newlines_valid = false; bool fields_valid = false; bool allow_empty_valid = false; + bool width_valid = false; bool all = false; bool entire = false; @@ -179,6 +181,9 @@ using options_t = struct options_t { //!OCLINT(too many fields) long max = 0; long start = 0; long end = 0; + size_t width = 0; + + wchar_t char_to_pad = ' '; std::vector fields; @@ -242,9 +247,16 @@ static int handle_flag_a(wchar_t **argv, parser_t &parser, io_streams_t &streams static int handle_flag_c(wchar_t **argv, parser_t &parser, io_streams_t &streams, const wgetopter_t &w, options_t *opts) { - if (opts->chars_valid) { + if (opts->chars_to_trim_valid) { opts->chars_to_trim = w.woptarg; return STATUS_CMD_OK; + } else if (opts->char_to_pad_valid) { + if (wcslen(w.woptarg) != 1) { + string_error(streams, _(L"%ls: Padding should be a character '%ls'\n"), argv[0], w.woptarg); + return STATUS_INVALID_ARGS; + } + opts->char_to_pad = w.woptarg[0]; + return STATUS_CMD_OK; } string_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); return STATUS_INVALID_ARGS; @@ -451,13 +463,33 @@ static int handle_flag_v(wchar_t **argv, parser_t &parser, io_streams_t &streams return STATUS_INVALID_ARGS; } +static int handle_flag_w(wchar_t **argv, parser_t &parser, io_streams_t &streams, + const wgetopter_t &w, options_t *opts) { + long width = 0; + if (opts->width_valid) { + width = fish_wcstol(w.woptarg); + if (width < 0) { + string_error(streams, _(L"%ls: Invalid width value '%ls'\n"), argv[0], w.woptarg); + return STATUS_INVALID_ARGS; + } else if (errno) { + string_error(streams, BUILTIN_ERR_NOT_NUMBER, argv[0], w.woptarg); + return STATUS_INVALID_ARGS; + } + opts->width = static_cast(width); + return STATUS_CMD_OK; + } + string_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; +} + /// This constructs the wgetopt() short options string based on which arguments are valid for the /// subcommand. We have to do this because many short flags have multiple meanings and may or may /// not require an argument depending on the meaning. static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath complexity) wcstring short_opts(L":"); if (opts->all_valid) short_opts.append(L"a"); - if (opts->chars_valid) short_opts.append(L"c:"); + if (opts->char_to_pad_valid) short_opts.append(L"c:"); + if (opts->chars_to_trim_valid) short_opts.append(L"c:"); if (opts->count_valid) short_opts.append(L"n:"); if (opts->entire_valid) short_opts.append(L"e"); if (opts->filter_valid) short_opts.append(L"f"); @@ -478,6 +510,7 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co if (opts->no_trim_newlines_valid) short_opts.append(L"N"); if (opts->fields_valid) short_opts.append(L"f:"); if (opts->allow_empty_valid) short_opts.append(L"a"); + if (opts->width_valid) short_opts.append(L"w:"); return short_opts; } @@ -507,13 +540,14 @@ static const struct woption long_options[] = {{L"all", no_argument, nullptr, 'a' {L"no-trim-newlines", no_argument, nullptr, 'N'}, {L"fields", required_argument, nullptr, 'f'}, {L"allow-empty", no_argument, nullptr, 'a'}, + {L"width", required_argument, nullptr, 'w'}, {nullptr, 0, nullptr, 0}}; static const std::unordered_map flag_to_function = { {'N', handle_flag_N}, {'a', handle_flag_a}, {'c', handle_flag_c}, {'e', handle_flag_e}, {'f', handle_flag_f}, {'i', handle_flag_i}, {'l', handle_flag_l}, {'m', handle_flag_m}, {'n', handle_flag_n}, {'q', handle_flag_q}, {'r', handle_flag_r}, {'s', handle_flag_s}, - {'v', handle_flag_v}, {1, handle_flag_1}}; + {'v', handle_flag_v}, {'w', handle_flag_w}, {1, handle_flag_1}}; /// Parse the arguments for flags recognized by a specific string subcommand. static int parse_opts(options_t *opts, int *optind, int n_req_args, int argc, wchar_t **argv, @@ -937,6 +971,52 @@ static int string_match(parser_t &parser, io_streams_t &streams, int argc, wchar return matcher->match_count() > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR; } +static int string_pad(parser_t &parser, io_streams_t &streams, int argc, wchar_t **argv) { + options_t opts; + opts.char_to_pad_valid = true; + opts.right_valid = true; + opts.width_valid = true; + int optind; + int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + // Pad left by default + if (!opts.right) { + opts.left = true; + } + + // Find max width of strings and keep the inputs + size_t max_width = 0; + std::vector all_inputs; + + arg_iterator_t aiter_width(argv, optind, streams); + while (const wcstring *arg = aiter_width.nextstr()) { + wcstring input_string = *arg; + size_t width = fish_wcswidth(input_string); + if (width > max_width) max_width = width; + all_inputs.push_back(input_string); + } + + size_t pad_width = max_width > opts.width ? max_width : opts.width; + for (auto &input : all_inputs) { + wcstring padded = input; + size_t padded_width = fish_wcswidth(padded); + if (pad_width >= padded_width) { + size_t pad = pad_width - padded_width; + if (opts.left) { + padded.insert(0, pad, opts.char_to_pad); + } + if (opts.right) { + padded.append(pad, opts.char_to_pad); + } + } + streams.out.append(padded); + streams.out.append(L'\n'); + } + + return STATUS_CMD_OK; +} + class string_replacer_t { protected: const wchar_t *argv0; @@ -1368,7 +1448,7 @@ static int string_sub(parser_t &parser, io_streams_t &streams, int argc, wchar_t static int string_trim(parser_t &parser, io_streams_t &streams, int argc, wchar_t **argv) { options_t opts; - opts.chars_valid = true; + opts.chars_to_trim_valid = true; opts.left_valid = true; opts.right_valid = true; opts.quiet_valid = true; @@ -1453,6 +1533,7 @@ string_subcommands[] = { {L"split", &string_split}, {L"split0", &string_split0}, {L"sub", &string_sub}, {L"trim", &string_trim}, {L"lower", &string_lower}, {L"upper", &string_upper}, {L"repeat", &string_repeat}, {L"unescape", &string_unescape}, {L"collect", &string_collect}, + {L"pad", &string_pad}, {nullptr, nullptr}}; /// The string builtin, for manipulating strings. diff --git a/tests/checks/string.fish b/tests/checks/string.fish index 9d5c03acb..6efc705bb 100644 --- a/tests/checks/string.fish +++ b/tests/checks/string.fish @@ -42,6 +42,29 @@ string length "hello, world" string length -q ""; and echo not zero length; or echo zero length # CHECK: zero length +string pad foo +# CHECK: foo + +string pad -r -w 4 foo +# CHECK: foo + +string pad -r -w 7 -c '-' foo +# CHECK: foo---- + +string pad --width 7 -c '=' foo +# CHECK: ====foo + +echo \|(string pad --width 10 --right foo)\| +# CHECK: |foo | + +string pad -w 4 -c . 🐟 +# CHECK: ..🐟 + +string pad -c . long longer longest +# CHECK: ...long +# CHECK: .longer +# CHECK: longest + string sub --length 2 abcde # CHECK: ab @@ -192,7 +215,7 @@ string unescape --style=url (string escape --style=url 'a b#c"\'d') # CHECK: a b#c"'d string unescape --style=url (string escape --style=url \na\nb%c~d\n) -# CHECK: +# CHECK: # CHECK: a # CHECK: b%c~d @@ -260,7 +283,7 @@ string replace -a " " _ "spaces to underscores" # CHECK: spaces_to_underscores string replace -r -a "[^\d.]+" " " "0 one two 3.14 four 5x" -# CHECK: 0 3.14 5 +# CHECK: 0 3.14 5 string replace -r "(\w+)\s+(\w+)" "\$2 \$1 \$\$" "left right" # CHECK: right left $ @@ -295,7 +318,7 @@ and echo Unexpected exit status at line (status --current-line-number) # 'string match -r with empty capture groups' string match -r '^([ugoa]*)([=+-]?)([rwx]*)$' '=r' #CHECK: =r -#CHECK: +#CHECK: #CHECK: = #CHECK: r @@ -575,13 +598,13 @@ printf '[%s]\n' (string collect one\n\n two\n) # CHECK: [two] printf '[%s]\n' (string collect -N one\n\n two\n) # CHECK: [one -# CHECK: +# CHECK: # CHECK: ] # CHECK: [two # CHECK: ] printf '[%s]\n' (string collect --no-trim-newlines one\n\n two\n) # CHECK: [one -# CHECK: +# CHECK: # CHECK: ] # CHECK: [two # CHECK: ]