# HG changeset patch # User Christian Brabandt # Date 1691943304 -7200 # Node ID 2d5e8c46508b3c5f65cabf3fab8e3c934e21415e # Parent 1b2730ece70eef4d7620530c77e0d481ae86912e patch 9.0.1704: Cannot use positional arguments for printf() Commit: https://github.com/vim/vim/commit/0c6181fec4c362eb9682d5af583341eb20cb1af5 Author: Christ van Willegen Date: Sun Aug 13 18:03:14 2023 +0200 patch 9.0.1704: Cannot use positional arguments for printf() Problem: Cannot use positional arguments for printf() Solution: Support positional arguments in string formatting closes: #12140 Signed-off-by: Christian Brabandt Co-authored-by: Christ van Willegen diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -6590,7 +6590,11 @@ printf({fmt}, {expr1} ...) *printf()* The "%" starts a conversion specification. The following arguments appear in sequence: - % [flags] [field-width] [.precision] type + % [pos-argument] [flags] [field-width] [.precision] type + + pos-argument + At most one positional argument specifier. These + take the form {n$}, where n is >= 1. flags Zero or more of the following flags: @@ -6662,6 +6666,13 @@ printf({fmt}, {expr1} ...) *printf()* < This limits the length of the text used from "line" to "width" bytes. + If the argument to be formatted is specified using a posional + argument specifier, and a '*' is used to indicate that a + number argument is to be used to specify the width or + precision, the argument(s) to be used must also be specified + using a {n$} positional argument specifier. See |printf-$|. + + The conversion specifiers and their meanings are: *printf-d* *printf-b* *printf-B* *printf-o* @@ -6751,6 +6762,103 @@ printf({fmt}, {expr1} ...) *printf()* of "%" items. If there are not sufficient or too many arguments an error is given. Up to 18 arguments can be used. + *printf-$* + In certain languages, error and informative messages are + more readable when the order of words is different from the + corresponding message in English. To accomodate translations + having a different word order, positional arguments may be + used to indicate this. For instance: > + + #, c-format + msgid "%s returning %s" + msgstr "waarde %2$s komt terug van %1$s" +< + In this example, the sentence has its 2 string arguments reversed + in the output. > + + echo printf( + "In The Netherlands, vim's creator's name is: %1$s %2$s", + "Bram", "Moolenaar") +< In The Netherlands, vim's creator's name is: Bram Moolenaar > + + echo printf( + "In Belgium, vim's creator's name is: %2$s %1$s", + "Bram", "Moolenaar") +< In Belgium, vim's creator's name is: Moolenaar Bram + + Width (and precision) can be specified using the '*' specifier. + In this case, you must specify the field width position in the + argument list. > + + echo printf("%1$*2$.*3$d", 1, 2, 3) +< 001 > + echo printf("%2$*3$.*1$d", 1, 2, 3) +< 2 > + echo printf("%3$*1$.*2$d", 1, 2, 3) +< 03 > + echo printf("%1$*2$.*3$g", 1.4142, 2, 3) +< 1.414 + + You can mix specifying the width and/or precision directly + and via positional arguments: > + + echo printf("%1$4.*2$f", 1.4142135, 6) +< 1.414214 > + echo printf("%1$*2$.4f", 1.4142135, 6) +< 1.4142 > + echo printf("%1$*2$.*3$f", 1.4142135, 6, 2) +< 1.41 + + *E1400* + You cannot mix positional and non-positional arguments: > + echo printf("%s%1$s", "One", "Two") +< E1400: Cannot mix positional and non-positional + arguments: %s%1$s + + *E1401* + You cannot skip a positional argument in a format string: > + echo printf("%3$s%1$s", "One", "Two", "Three") +< E1401: format argument 2 unused in $-style + format: %3$s%1$s + + *E1402* + You can re-use a [field-width] (or [precision]) argument: > + echo printf("%1$d at width %2$d is: %01$*2$d", 1, 2) +< 1 at width 2 is: 01 + + However, you can't use it as a different type: > + echo printf("%1$d at width %2$ld is: %01$*2$d", 1, 2) +< E1402: Positional argument 2 used as field + width reused as different type: long int/int + + *E1403* + When a positional argument is used, but not the correct number + or arguments is given, an error is raised: > + echo printf("%1$d at width %2$d is: %01$*2$.*3$d", 1, 2) +< E1403: Positional argument 3 out of bounds: + %1$d at width %2$d is: %01$*2$.*3$d + + Only the first error is reported: > + echo printf("%01$*2$.*3$d %4$d", 1, 2) +< E1403: Positional argument 3 out of bounds: + %01$*2$.*3$d %4$d + + *E1404* + A positional argument can be used more than once: > + echo printf("%1$s %2$s %1$s", "One", "Two") +< One Two One + + However, you can't use a different type the second time: > + echo printf("%1$s %2$s %1$d", "One", "Two") +< E1404: Positional argument 1 type used + inconsistently: int/string + + *E1405* + Various other errors that lead to a format string being + wrongly formatted lead to: > + echo printf("%1$d at width %2$d is: %01$*2$.3$d", 1, 2) +< E1405: Invalid format specifier: + %1$d at width %2$d is: %01$*2$.3$d prompt_getprompt({buf}) *prompt_getprompt()* Returns the effective prompt text for buffer {buf}. {buf} can diff --git a/src/errors.h b/src/errors.h --- a/src/errors.h +++ b/src/errors.h @@ -3477,3 +3477,17 @@ EXTERN char e_incomplete_type[] #endif EXTERN char e_warning_pointer_block_corrupted[] INIT(= N_("E1364: Warning: Pointer block corrupted")); +EXTERN char e_cannot_mix_positional_and_non_positional_str[] + INIT(= N_("E1400: Cannot mix positional and non-positional arguments: %s")); +EXTERN char e_fmt_arg_nr_unused_str[] + INIT(= N_("E1401: format argument %d unused in $-style format: %s")); +EXTERN char e_positional_num_field_spec_reused_str_str[] + INIT(= N_("E1402: Positional argument %d used as field width reused as different type: %s/%s")); +EXTERN char e_positional_nr_out_of_bounds_str[] + INIT(= N_("E1403: Positional argument %d out of bounds: %s")); +EXTERN char e_positional_arg_num_type_inconsistent_str_str[] + INIT(= N_("E1404: Positional argument %d type used inconsistently: %s/%s")); +EXTERN char e_invalid_format_specifier_str[] + INIT(= N_("E1405: Invalid format specifier: %s")); + +// E1365 - E1399 unused diff --git a/src/globals.h b/src/globals.h --- a/src/globals.h +++ b/src/globals.h @@ -1674,6 +1674,19 @@ EXTERN int cmdwin_result INIT(= 0); // r EXTERN char_u no_lines_msg[] INIT(= N_("--No lines in buffer--")); +EXTERN char typename_unknown[] INIT(= N_("unknown")); +EXTERN char typename_int[] INIT(= N_("int")); +EXTERN char typename_longint[] INIT(= N_("long int")); +EXTERN char typename_longlongint[] INIT(= N_("long long int")); +EXTERN char typename_unsignedint[] INIT(= N_("unsigned int")); +EXTERN char typename_unsignedlongint[] INIT(= N_("unsigned long int")); +EXTERN char typename_unsignedlonglongint[] INIT(= N_("unsigned long long int")); +EXTERN char typename_pointer[] INIT(= N_("pointer")); +EXTERN char typename_percent[] INIT(= N_("percent")); +EXTERN char typename_char[] INIT(= N_("char")); +EXTERN char typename_string[] INIT(= N_("string")); +EXTERN char typename_float[] INIT(= N_("float")); + /* * When ":global" is used to number of substitutions and changed lines is * accumulated until it's finished. diff --git a/src/message_test.c b/src/message_test.c --- a/src/message_test.c +++ b/src/message_test.c @@ -39,6 +39,9 @@ char *fmt_012p = "%012p"; char *fmt_5S = "%5S"; char *fmt_06b = "%06b"; +char *fmt_06pb = "%1$0.*2$b"; +char *fmt_212s = "%2$s %1$s %2$s"; +char *fmt_21s = "%2$s %1$s"; /* * Test trunc_string(). @@ -181,6 +184,11 @@ test_vim_snprintf(void) // buffer and its content should then never be used. char *buf = malloc(bsize); + n = vim_snprintf(buf, bsize, "%.8g", 10000000.1); + assert(n == 12); + assert(bsize == 0 || STRNCMP(buf, "1.00000001e7", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + n = vim_snprintf(buf, bsize, "%d", 1234567); assert(n == 7); assert(bsize == 0 || STRNCMP(buf, "1234567", bsize_int) == 0); @@ -211,6 +219,12 @@ test_vim_snprintf(void) assert(bsize == 0 || STRNCMP(buf, "001100", bsize_int) == 0); assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + n = vim_snprintf(buf, bsize, "%s %s", "one", "two"); + assert(n == 7); + assert(bsize == 0 || STRNCMP(buf, "one two", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + +#ifdef FEAT_FLOAT n = vim_snprintf(buf, bsize, "%f", 1.234); assert(n == 8); assert(bsize == 0 || STRNCMP(buf, "1.234000", bsize_int) == 0); @@ -240,6 +254,7 @@ test_vim_snprintf(void) assert(n == 9); assert(bsize == 0 || STRNCMP(buf, "-0.000000", bsize_int) == 0); assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); +#endif n = vim_snprintf(buf, bsize, "%s", "漢語"); assert(n == 6); @@ -304,6 +319,190 @@ test_vim_snprintf(void) } } +/* + * Test vim_snprintf() with a focus on checking that positional + * arguments are correctly applied and skipped + */ + static void +test_vim_snprintf_positional(void) +{ + int n; + size_t bsize; + int bsize_int; + + // Loop on various buffer sizes to make sure that truncation of + // vim_snprintf() is correct. + for (bsize = 0; bsize < 25; ++bsize) + { + bsize_int = (int)bsize - 1; + + // buf is the heap rather than in the stack + // so valgrind can detect buffer overflows if any. + // Use malloc() rather than alloc() as test checks with 0-size + // buffer and its content should then never be used. + char *buf = malloc(bsize); + + n = vim_snprintf(buf, bsize, "%1$*2$ld", 1234567L, -9); + assert(n == 9); + assert(bsize == 0 || STRNCMP(buf, "1234567 ", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$*2$.*3$ld", 1234567L, -9, 5); + assert(n == 9); + assert(bsize == 0 || STRNCMP(buf, "1234567 ", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$*3$.*2$ld", 1234567L, 5, -9); + assert(n == 9); + assert(bsize == 0 || STRNCMP(buf, "1234567 ", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%3$*1$.*2$ld", -9, 5, 1234567L); + assert(n == 9); + assert(bsize == 0 || STRNCMP(buf, "1234567 ", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$ld", 1234567L); + assert(n == 7); + assert(bsize == 0 || STRNCMP(buf, "1234567", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$*2$ld", 1234567L, 9); + assert(n == 9); + assert(bsize == 0 || STRNCMP(buf, " 1234567", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$ld %1$d %3$lu", 12345, 9L, 7654321UL); + assert(n == 15); + assert(bsize == 0 || STRNCMP(buf, "9 12345 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$d %1$ld %3$lu", 1234567L, 9, 7654321UL); + assert(n == 17); + assert(bsize == 0 || STRNCMP(buf, "9 1234567 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$d %1$lld %3$lu", 1234567LL, 9, 7654321UL); + assert(n == 17); + assert(bsize == 0 || STRNCMP(buf, "9 1234567 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$ld %1$u %3$lu", 12345U, 9L, 7654321UL); + assert(n == 15); + assert(bsize == 0 || STRNCMP(buf, "9 12345 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$d %1$lu %3$lu", 1234567UL, 9, 7654321UL); + assert(n == 17); + assert(bsize == 0 || STRNCMP(buf, "9 1234567 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$d %1$llu %3$lu", 1234567LLU, 9, 7654321UL); + assert(n == 17); + assert(bsize == 0 || STRNCMP(buf, "9 1234567 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$d %1$llu %3$lu", 1234567LLU, 9, 7654321UL); + assert(n == 17); + assert(bsize == 0 || STRNCMP(buf, "9 1234567 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$d %1$x %3$lu", 0xdeadbeef, 9, 7654321UL); + assert(n == 18); + assert(bsize == 0 || STRNCMP(buf, "9 deadbeef 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$ld %1$c %3$lu", 'c', 9L, 7654321UL); + assert(n == 11); + assert(bsize == 0 || STRNCMP(buf, "9 c 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$ld %1$s %3$lu", "hi", 9L, 7654321UL); + assert(n == 12); + assert(bsize == 0 || STRNCMP(buf, "9 hi 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%2$ld %1$e %3$lu", 0.0, 9L, 7654321UL); + assert(n == 22); + assert(bsize == 0 || STRNCMP(buf, "9 0.000000e+00 7654321", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, fmt_212s, "one", "two", "three"); + assert(n == 11); + assert(bsize == 0 || STRNCMP(buf, "two one two", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%3$s %1$s %2$s", "one", "two", "three"); + assert(n == 13); + assert(bsize == 0 || STRNCMP(buf, "three one two", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$d", 1234567); + assert(n == 7); + assert(bsize == 0 || STRNCMP(buf, "1234567", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$x", 0xdeadbeef); + assert(n == 8); + assert(bsize == 0 || STRNCMP(buf, "deadbeef", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, fmt_06pb, (uvarnumber_T)12, 6); + assert(n == 6); + assert(bsize == 0 || STRNCMP(buf, "001100", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$s %2$s", "one", "two"); + assert(n == 7); + assert(bsize == 0 || STRNCMP(buf, "one two", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, fmt_06b, (uvarnumber_T)12); + assert(n == 6); + assert(bsize == 0 || STRNCMP(buf, "001100", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, fmt_21s, "one", "two", "three"); + assert(n == 7); + assert(bsize == 0 || STRNCMP(buf, "two one", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + +#ifdef FEAT_FLOAT + n = vim_snprintf(buf, bsize, "%1$f", 1.234); + assert(n == 8); + assert(bsize == 0 || STRNCMP(buf, "1.234000", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$e", 1.234); + assert(n == 12); + assert(bsize == 0 || STRNCMP(buf, "1.234000e+00", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$f", 0.0/0.0); + assert(n == 3); + assert(bsize == 0 || STRNCMP(buf, "nan", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$f", 1.0/0.0); + assert(n == 3); + assert(bsize == 0 || STRNCMP(buf, "inf", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$f", -1.0/0.0); + assert(n == 4); + assert(bsize == 0 || STRNCMP(buf, "-inf", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); + + n = vim_snprintf(buf, bsize, "%1$f", -0.0); + assert(n == 9); + assert(bsize == 0 || STRNCMP(buf, "-0.000000", bsize_int) == 0); + assert(bsize == 0 || buf[MIN(n, bsize_int)] == '\0'); +#endif + + free(buf); + } +} + int main(int argc, char **argv) { @@ -317,11 +516,13 @@ main(int argc, char **argv) test_trunc_string(); test_trunc_string_mbyte(); test_vim_snprintf(); + test_vim_snprintf_positional(); set_option_value_give_err((char_u *)"encoding", 0, (char_u *)"latin1", 0); init_chartab(); test_trunc_string(); test_vim_snprintf(); + test_vim_snprintf_positional(); return 0; } diff --git a/src/po/check.vim b/src/po/check.vim --- a/src/po/check.vim +++ b/src/po/check.vim @@ -30,8 +30,15 @@ func! GetMline() " remove '%' used for plural forms. let idline = substitute(idline, '\\nPlural-Forms: .\+;\\n', '', '') + " remove duplicate positional format arguments + let idline2 = "" + while idline2 != idline + let idline2 = idline + let idline = substitute(idline, '%\([1-9][0-9]*\)\$\([-+ #''.*]*[0-9]*l\=[dsuxXpoc%]\)\(.*\)%\1$\([-+ #''.*]*\)\(l\=[dsuxXpoc%]\)', '%\1$\2\3\4', 'g') + endwhile + " remove everything but % items. - return substitute(idline, '[^%]*\(%[-+ #''.0-9*]*l\=[dsuxXpoc%]\)\=', '\1', 'g') + return substitute(idline, '[^%]*\(%([1-9][0-9]*\$)\=[-+ #''.0-9*]*l\=[dsuxXpoc%]\)\=', '\1', 'g') endfunc " This only works when 'wrapscan' is not set. diff --git a/src/strings.c b/src/strings.c --- a/src/strings.c +++ b/src/strings.c @@ -2242,17 +2242,665 @@ vim_vsnprintf( return vim_vsnprintf_typval(str, str_m, fmt, ap, NULL); } +enum +{ + TYPE_UNKNOWN = -1, + TYPE_INT, + TYPE_LONGINT, + TYPE_LONGLONGINT, + TYPE_UNSIGNEDINT, + TYPE_UNSIGNEDLONGINT, + TYPE_UNSIGNEDLONGLONGINT, + TYPE_POINTER, + TYPE_PERCENT, + TYPE_CHAR, + TYPE_STRING, + TYPE_FLOAT +}; + +/* Types that can be used in a format string + */ + int +format_typeof( + const char *type, + int usetvs UNUSED) +{ + // allowed values: \0, h, l, L + char length_modifier = '\0'; + + // current conversion specifier character + char fmt_spec = '\0'; + + // parse 'h', 'l' and 'll' length modifiers + if (*type == 'h' || *type == 'l') + { + length_modifier = *type; + type++; + if (length_modifier == 'l' && *type == 'l') + { + // double l = __int64 / varnumber_T + length_modifier = 'L'; + type++; + } + } + fmt_spec = *type; + + // common synonyms: + switch (fmt_spec) + { + case 'i': fmt_spec = 'd'; break; + case '*': fmt_spec = 'd'; length_modifier = 'h'; break; + case 'D': fmt_spec = 'd'; length_modifier = 'l'; break; + case 'U': fmt_spec = 'u'; length_modifier = 'l'; break; + case 'O': fmt_spec = 'o'; length_modifier = 'l'; break; + default: break; + } + +# if defined(FEAT_EVAL) + if (usetvs) + { + switch (fmt_spec) + { + case 'd': case 'u': case 'o': case 'x': case 'X': + if (length_modifier == '\0') + length_modifier = 'L'; + } + } +# endif + + // get parameter value, do initial processing + switch (fmt_spec) + { + // '%' and 'c' behave similar to 's' regarding flags and field + // widths + case '%': + return TYPE_PERCENT; + + case 'c': + return TYPE_CHAR; + + case 's': + case 'S': + return TYPE_STRING; + + case 'd': case 'u': + case 'b': case 'B': + case 'o': + case 'x': case 'X': + case 'p': + { + // NOTE: the u, b, o, x, X and p conversion specifiers + // imply the value is unsigned; d implies a signed + // value + + // 0 if numeric argument is zero (or if pointer is + // NULL for 'p'), +1 if greater than zero (or nonzero + // for unsigned arguments), -1 if negative (unsigned + // argument is never negative) + + if (fmt_spec == 'p') + return TYPE_POINTER; + else if (fmt_spec == 'b' || fmt_spec == 'B') + return TYPE_UNSIGNEDINT; + else if (fmt_spec == 'd') + { + // signed + switch (length_modifier) + { + case '\0': + case 'h': + // char and short arguments are passed as int. + return TYPE_INT; + case 'l': + return TYPE_LONGINT; + case 'L': + return TYPE_LONGLONGINT; + } + } + else + { + // unsigned + switch (length_modifier) + { + case '\0': + case 'h': + return TYPE_UNSIGNEDINT; + case 'l': + return TYPE_UNSIGNEDLONGINT; + case 'L': + return TYPE_UNSIGNEDLONGLONGINT; + } + } + } + break; + + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + return TYPE_FLOAT; + } + + return TYPE_UNKNOWN; +} + + char * +format_typename( + const char *type) +{ + switch (format_typeof(type, FALSE)) + { + case TYPE_INT: + return _(typename_int); + + case TYPE_LONGINT: + return _(typename_longint); + + case TYPE_LONGLONGINT: + return _(typename_longlongint); + + case TYPE_UNSIGNEDINT: + return _(typename_unsignedint); + + case TYPE_UNSIGNEDLONGINT: + return _(typename_unsignedlongint); + + case TYPE_UNSIGNEDLONGLONGINT: + return _(typename_unsignedlonglongint); + + case TYPE_POINTER: + return _(typename_pointer); + + case TYPE_PERCENT: + return _(typename_percent); + + case TYPE_CHAR: + return _(typename_char); + + case TYPE_STRING: + return _(typename_string); + + case TYPE_FLOAT: + return _(typename_float); + } + + return _(typename_unknown); +} + + int +adjust_types( + const char ***ap_types, + int arg, + int *num_posarg, + const char *type) +{ + if (*ap_types == NULL || *num_posarg < arg) + { + int idx; + const char **new_types; + + if (*ap_types == NULL) + new_types = ALLOC_CLEAR_MULT(const char *, arg); + else + new_types = vim_realloc(*ap_types, arg * sizeof(const char *)); + + if (new_types == NULL) + return FAIL; + + for (idx = *num_posarg; idx < arg; ++idx) + new_types[idx] = NULL; + + *ap_types = new_types; + *num_posarg = arg; + } + + if ((*ap_types)[arg - 1] != NULL) + { + if ((*ap_types)[arg - 1][0] == '*' || type[0] == '*') + { + const char *pt = type; + if (pt[0] == '*') + pt = (*ap_types)[arg - 1]; + + if (pt[0] != '*') + { + switch (pt[0]) + { + case 'd': case 'i': break; + default: + semsg(_(e_positional_num_field_spec_reused_str_str), arg, format_typename((*ap_types)[arg - 1]), format_typename(type)); + return FAIL; + } + } + } + else + { + if (format_typeof(type, FALSE) != format_typeof((*ap_types)[arg - 1], FALSE)) + { + semsg(_( e_positional_arg_num_type_inconsistent_str_str), arg, format_typename(type), format_typename((*ap_types)[arg - 1])); + return FAIL; + } + } + } + + (*ap_types)[arg - 1] = type; + + return OK; +} + + int +parse_fmt_types( + const char ***ap_types, + int *num_posarg, + const char *fmt, + typval_T *tvs UNUSED + ) +{ + const char *p = fmt; + const char *arg = NULL; + + int any_pos = 0; + int any_arg = 0; + int arg_idx; + +#define CHECK_POS_ARG do { \ + if (any_pos && any_arg) \ + { \ + semsg(_( e_cannot_mix_positional_and_non_positional_str), fmt); \ + goto error; \ + } \ +} while (0); + + if (p == NULL) + return OK; + + while (*p != NUL) + { + if (*p != '%') + { + char *q = strchr(p + 1, '%'); + size_t n = (q == NULL) ? STRLEN(p) : (size_t)(q - p); + + p += n; + } + else + { + // allowed values: \0, h, l, L + char length_modifier = '\0'; + + // variable for positional arg + int pos_arg = -1; + const char *ptype = NULL; + + p++; // skip '%' + + // First check to see if we find a positional + // argument specifier + ptype = p; + + while (VIM_ISDIGIT(*ptype)) + ++ptype; + + if (*ptype == '$') + { + if (*p == '0') + { + // 0 flag at the wrong place + semsg(_( e_invalid_format_specifier_str), fmt); + goto error; + } + + // Positional argument + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + pos_arg = uj; + + any_pos = 1; + CHECK_POS_ARG; + + ++p; + } + + // parse flags + while (*p == '0' || *p == '-' || *p == '+' || *p == ' ' + || *p == '#' || *p == '\'') + { + switch (*p) + { + case '0': break; + case '-': break; + case '+': break; + case ' ': // If both the ' ' and '+' flags appear, the ' ' + // flag should be ignored + break; + case '#': break; + case '\'': break; + } + p++; + } + // If the '0' and '-' flags both appear, the '0' flag should be + // ignored. + + // parse field width + if (*(arg = p) == '*') + { + p++; + + if (VIM_ISDIGIT((int)(*p))) + { + // Positional argument field width + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + + if (*p != '$') + { + semsg(_( e_invalid_format_specifier_str), fmt); + goto error; + } + else + { + ++p; + any_pos = 1; + CHECK_POS_ARG; + + if (adjust_types(ap_types, uj, num_posarg, arg) == FAIL) + goto error; + } + } + else + { + any_arg = 1; + CHECK_POS_ARG; + } + } + else if (VIM_ISDIGIT((int)(*(arg = p)))) + { + // size_t could be wider than unsigned int; make sure we treat + // argument like common implementations do + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + + if (*p == '$') + { + semsg(_( e_invalid_format_specifier_str), fmt); + goto error; + } + } + + // parse precision + if (*p == '.') + { + p++; + + if (*(arg = p) == '*') + { + p++; + + if (VIM_ISDIGIT((int)(*p))) + { + // Parse precision + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + + if (*p == '$') + { + any_pos = 1; + CHECK_POS_ARG; + + ++p; + + if (adjust_types(ap_types, uj, num_posarg, arg) == FAIL) + goto error; + } + else + { + semsg(_( e_invalid_format_specifier_str), fmt); + goto error; + } + } + else + { + any_arg = 1; + CHECK_POS_ARG; + } + } + else if (VIM_ISDIGIT((int)(*(arg = p)))) + { + // size_t could be wider than unsigned int; make sure we + // treat argument like common implementations do + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + + if (*p == '$') + { + semsg(_( e_invalid_format_specifier_str), fmt); + goto error; + } + } + } + + if (pos_arg != -1) + { + any_pos = 1; + CHECK_POS_ARG; + + ptype = p; + } + + // parse 'h', 'l' and 'll' length modifiers + if (*p == 'h' || *p == 'l') + { + length_modifier = *p; + p++; + if (length_modifier == 'l' && *p == 'l') + { + // double l = __int64 / varnumber_T + length_modifier = 'L'; + p++; + } + } + + switch (*p) + { + // Check for known format specifiers. % is special! + case 'i': + case '*': + case 'd': + case 'u': + case 'o': + case 'D': + case 'U': + case 'O': + case 'x': + case 'X': + case 'b': + case 'B': + case 'c': + case 's': + case 'S': + case 'p': + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + if (pos_arg != -1) + { + if (adjust_types(ap_types, pos_arg, num_posarg, ptype) == FAIL) + goto error; + } + else + { + any_arg = 1; + CHECK_POS_ARG; + } + break; + + default: + if (pos_arg != -1) + { + semsg(_( e_cannot_mix_positional_and_non_positional_str), fmt); + goto error; + } + } + + if (*p != NUL) + p++; // step over the just processed conversion specifier + } + } + + for (arg_idx = 0; arg_idx < *num_posarg; ++arg_idx) + { + if ((*ap_types)[arg_idx] == NULL) + { + semsg(_(e_fmt_arg_nr_unused_str), arg_idx + 1, fmt); + goto error; + } + +# if defined(FEAT_EVAL) + if (tvs != NULL && tvs[arg_idx].v_type == VAR_UNKNOWN) + { + semsg(_(e_positional_nr_out_of_bounds_str), arg_idx + 1, fmt); + goto error; + } +# endif + } + + return OK; + +error: + vim_free(*ap_types); + *ap_types = NULL; + *num_posarg = 0; + return FAIL; +} + + void +skip_to_arg( + const char **ap_types, + va_list ap_start, + va_list *ap, + int *arg_idx, + int *arg_cur) +{ + int arg_min = 0; + + if (*arg_cur + 1 == *arg_idx) + { + ++*arg_cur; + ++*arg_idx; + return; + } + + if (*arg_cur >= *arg_idx) + { + // Reset ap to ap_start and skip arg_idx - 1 types + va_end(*ap); + va_copy(*ap, ap_start); + } + else + { + // Skip over any we should skip + arg_min = *arg_cur; + } + + for (*arg_cur = arg_min; *arg_cur < *arg_idx - 1; ++*arg_cur) + { + const char *p = ap_types[*arg_cur]; + + int fmt_type = format_typeof(p, TRUE); + + // get parameter value, do initial processing + switch (fmt_type) + { + case TYPE_PERCENT: + case TYPE_UNKNOWN: + break; + + case TYPE_CHAR: + va_arg(*ap, int); + break; + + case TYPE_STRING: + va_arg(*ap, char *); + break; + + case TYPE_POINTER: + va_arg(*ap, void *); + break; + + case TYPE_INT: + va_arg(*ap, int); + break; + + case TYPE_LONGINT: + va_arg(*ap, long int); + break; + + case TYPE_LONGLONGINT: + va_arg(*ap, varnumber_T); + break; + + case TYPE_UNSIGNEDINT: + va_arg(*ap, unsigned int); + break; + + case TYPE_UNSIGNEDLONGINT: + va_arg(*ap, unsigned long int); + break; + + case TYPE_UNSIGNEDLONGLONGINT: + va_arg(*ap, uvarnumber_T); + break; + + case TYPE_FLOAT: + va_arg(*ap, double); + break; + } + } + + // Because we know that after we return from this call, + // a va_arg() call is made, we can pre-emptively + // increment the current argument index. + ++*arg_cur; + ++*arg_idx; + + return; +} + int vim_vsnprintf_typval( char *str, size_t str_m, const char *fmt, - va_list ap, + va_list ap_start, typval_T *tvs) { size_t str_l = 0; const char *p = fmt; + int arg_cur = 0; + int num_posarg = 0; int arg_idx = 1; + va_list ap; + const char **ap_types = NULL; + + if (parse_fmt_types(&ap_types, &num_posarg, fmt, tvs) == FAIL) + return 0; + + va_copy(ap, ap_start); if (p == NULL) p = ""; @@ -2316,9 +2964,32 @@ vim_vsnprintf_typval( // buffer for 's' and 'S' specs char_u *tofree = NULL; + // variables for positional arg + int pos_arg = -1; + const char *ptype; + p++; // skip '%' + // First check to see if we find a positional + // argument specifier + ptype = p; + + while (VIM_ISDIGIT(*ptype)) + ++ptype; + + if (*ptype == '$') + { + // Positional argument + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + pos_arg = uj; + + ++p; + } + // parse flags while (*p == '0' || *p == '-' || *p == '+' || *p == ' ' || *p == '#' || *p == '\'') @@ -2346,11 +3017,26 @@ vim_vsnprintf_typval( int j; p++; + + if (VIM_ISDIGIT((int)(*p))) + { + // Positional argument field width + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + arg_idx = uj; + + ++p; + } + j = # if defined(FEAT_EVAL) tvs != NULL ? tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, int); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, int)); + if (j >= 0) min_field_width = j; else @@ -2375,25 +3061,8 @@ vim_vsnprintf_typval( { p++; precision_specified = 1; - if (*p == '*') - { - int j; - - j = -# if defined(FEAT_EVAL) - tvs != NULL ? tv_nr(tvs, &arg_idx) : -# endif - va_arg(ap, int); - p++; - if (j >= 0) - precision = j; - else - { - precision_specified = 0; - precision = 0; - } - } - else if (VIM_ISDIGIT((int)(*p))) + + if (VIM_ISDIGIT((int)(*p))) { // size_t could be wider than unsigned int; make sure we // treat argument like common implementations do @@ -2403,6 +3072,39 @@ vim_vsnprintf_typval( uj = 10 * uj + (unsigned int)(*p++ - '0'); precision = uj; } + else if (*p == '*') + { + int j; + + p++; + + if (VIM_ISDIGIT((int)(*p))) + { + // positional argument + unsigned int uj = *p++ - '0'; + + while (VIM_ISDIGIT((int)(*p))) + uj = 10 * uj + (unsigned int)(*p++ - '0'); + arg_idx = uj; + + ++p; + } + + j = +# if defined(FEAT_EVAL) + tvs != NULL ? tv_nr(tvs, &arg_idx) : +# endif + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, int)); + + if (j >= 0) + precision = j; + else + { + precision_specified = 0; + precision = 0; + } + } } // parse 'h', 'l' and 'll' length modifiers @@ -2438,6 +3140,9 @@ vim_vsnprintf_typval( } # endif + if (pos_arg != -1) + arg_idx = pos_arg; + // get parameter value, do initial processing switch (fmt_spec) { @@ -2462,7 +3167,9 @@ vim_vsnprintf_typval( # if defined(FEAT_EVAL) tvs != NULL ? tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, int); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, int)); + // standard demands unsigned char uchar_arg = (unsigned char)j; str_arg = (char *)&uchar_arg; @@ -2475,7 +3182,9 @@ vim_vsnprintf_typval( # if defined(FEAT_EVAL) tvs != NULL ? tv_str(tvs, &arg_idx, &tofree) : # endif - va_arg(ap, char *); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, char *)); + if (str_arg == NULL) { str_arg = "[NULL]"; @@ -2570,7 +3279,9 @@ vim_vsnprintf_typval( tvs != NULL ? (void *)tv_str(tvs, &arg_idx, NULL) : # endif - va_arg(ap, void *); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, void *)); + if (ptr_arg != NULL) arg_sign = 1; } @@ -2581,7 +3292,9 @@ vim_vsnprintf_typval( tvs != NULL ? (uvarnumber_T)tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, uvarnumber_T); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, uvarnumber_T)); + if (bin_arg != 0) arg_sign = 1; } @@ -2597,7 +3310,9 @@ vim_vsnprintf_typval( # if defined(FEAT_EVAL) tvs != NULL ? tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, int); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, int)); + if (int_arg > 0) arg_sign = 1; else if (int_arg < 0) @@ -2608,7 +3323,9 @@ vim_vsnprintf_typval( # if defined(FEAT_EVAL) tvs != NULL ? tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, long int); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, long int)); + if (long_arg > 0) arg_sign = 1; else if (long_arg < 0) @@ -2619,7 +3336,9 @@ vim_vsnprintf_typval( # if defined(FEAT_EVAL) tvs != NULL ? tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, varnumber_T); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, varnumber_T)); + if (llong_arg > 0) arg_sign = 1; else if (llong_arg < 0) @@ -2639,7 +3358,9 @@ vim_vsnprintf_typval( tvs != NULL ? (unsigned) tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, unsigned int); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, unsigned int)); + if (uint_arg != 0) arg_sign = 1; break; @@ -2649,7 +3370,9 @@ vim_vsnprintf_typval( tvs != NULL ? (unsigned long) tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, unsigned long int); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, unsigned long int)); + if (ulong_arg != 0) arg_sign = 1; break; @@ -2659,7 +3382,9 @@ vim_vsnprintf_typval( tvs != NULL ? (uvarnumber_T) tv_nr(tvs, &arg_idx) : # endif - va_arg(ap, uvarnumber_T); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, uvarnumber_T)); + if (ullong_arg != 0) arg_sign = 1; break; @@ -2859,7 +3584,9 @@ vim_vsnprintf_typval( # if defined(FEAT_EVAL) tvs != NULL ? tv_float(tvs, &arg_idx) : # endif - va_arg(ap, double); + (skip_to_arg(ap_types, ap_start, &ap, &arg_idx, &arg_cur), + va_arg(ap, double)); + abs_f = f < 0 ? -f : f; if (fmt_spec == 'g' || fmt_spec == 'G') @@ -3143,9 +3870,12 @@ vim_vsnprintf_typval( str[str_l <= str_m - 1 ? str_l : str_m - 1] = '\0'; } - if (tvs != NULL && tvs[arg_idx - 1].v_type != VAR_UNKNOWN) + if (tvs != NULL && tvs[num_posarg != 0 ? num_posarg : arg_idx - 1].v_type != VAR_UNKNOWN) emsg(_(e_too_many_arguments_to_printf)); + vim_free(ap_types); + va_end(ap); + // Return the number of characters formatted (excluding trailing nul // character), that is, the number of characters that would have been // written to the buffer if it were large enough. diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -150,6 +150,7 @@ NEW_TESTS = \ test_fnameescape \ test_fnamemodify \ test_fold \ + test_format \ test_functions \ test_function_lists \ test_ga \ diff --git a/src/testdir/test_expr.vim b/src/testdir/test_expr.vim --- a/src/testdir/test_expr.vim +++ b/src/testdir/test_expr.vim @@ -291,6 +291,8 @@ func Test_printf_misc() let lines =<< trim END call assert_equal('123', printf('123')) + call assert_equal('', printf('%')) + call assert_equal('', printf('%.0d', 0)) call assert_equal('123', printf('%d', 123)) call assert_equal('123', printf('%i', 123)) call assert_equal('123', printf('%D', 123)) diff --git a/src/testdir/test_format.vim b/src/testdir/test_format.vim new file mode 100644 --- /dev/null +++ b/src/testdir/test_format.vim @@ -0,0 +1,361 @@ +" Tests for expressions. + +source check.vim +import './vim9.vim' as v9 + +func Test_printf_pos_misc() + let lines =<< trim END + call assert_equal('123', printf('%1$d', 123)) + call assert_equal('', printf('%1$.0d', 0)) + call assert_equal('00005', printf('%1$5.5d', 5)) + call assert_equal('00005', printf('%1$*1$.5d', 5)) + call assert_equal('00005', printf('%1$5.*1$d', 5)) + call assert_equal('00005', printf('%1$*1$.*1$d', 5)) + call assert_equal('00005', printf('%1$*10$.5d%2$.0d%3$.0d%4$.0d%5$.0d%6$.0d%7$.0d%8$.0d%9$.0d', 5, 0, 0, 0, 0, 0, 0, 0, 0, 5)) + call assert_equal('00005', printf('%1$5.*10$d%2$.0d%3$.0d%4$.0d%5$.0d%6$.0d%7$.0d%8$.0d%9$.0d', 5, 0, 0, 0, 0, 0, 0, 0, 0, 5)) + call assert_equal('123', printf('%1$i', 123)) + call assert_equal('123', printf('%1$D', 123)) + call assert_equal('123', printf('%1$U', 123)) + call assert_equal('173', printf('%1$o', 123)) + call assert_equal('173', printf('%1$O', 123)) + call assert_equal('7b', printf('%1$x', 123)) + call assert_equal('7B', printf('%1$X', 123)) + call assert_equal('Printing 1 at width 1 gives: 1', 1->printf("Printing %1$d at width %1$d gives: %1$*1$d")) + call assert_equal('Printing 2 at width 2 gives: 2', 2->printf("Printing %1$d at width %1$d gives: %1$*1$d")) + call assert_equal('Printing 3 at width 3 gives: 3', 3->printf("Printing %1$d at width %1$d gives: %1$*1$d")) + call assert_equal('Printing 1 at width/precision 1.1 gives: 1', 1->printf("Printing %1$d at width/precision %1$d.%1$d gives: %1$*1$.*1$d")) + call assert_equal('Printing 2 at width/precision 2.2 gives: 02', 2->printf("Printing %1$d at width/precision %1$d.%1$d gives: %1$*1$.*1$d")) + call assert_equal('Printing 3 at width/precision 3.3 gives: 003', 3->printf("Printing %1$d at width/precision %1$d.%1$d gives: %1$*1$.*1$d")) + + call assert_equal('123', printf('%1$hd', 123)) + call assert_equal('-123', printf('%1$hd', -123)) + call assert_equal('-1', printf('%1$hd', 0xFFFF)) + call assert_equal('-1', printf('%1$hd', 0x1FFFFF)) + + call assert_equal('123', printf('%1$hu', 123)) + call assert_equal('65413', printf('%1$hu', -123)) + call assert_equal('65535', printf('%1$hu', 0xFFFF)) + call assert_equal('65535', printf('%1$hu', 0x1FFFFF)) + + call assert_equal('123', printf('%1$ld', 123)) + call assert_equal('-123', printf('%1$ld', -123)) + call assert_equal('65535', printf('%1$ld', 0xFFFF)) + call assert_equal('131071', printf('%1$ld', 0x1FFFF)) + + call assert_equal('{', printf('%1$c', 123)) + call assert_equal('abc', printf('%1$s', 'abc')) + call assert_equal('abc', printf('%1$S', 'abc')) + + call assert_equal('+123', printf('%1$+d', 123)) + call assert_equal('-123', printf('%1$+d', -123)) + call assert_equal('+123', printf('%1$+ d', 123)) + call assert_equal(' 123', printf('%1$ d', 123)) + call assert_equal(' 123', printf('%1$ d', 123)) + call assert_equal('-123', printf('%1$ d', -123)) + + call assert_equal(' 123', printf('%2$*1$d', 5, 123)) + call assert_equal('123 ', printf('%2$*1$d', -5, 123)) + call assert_equal('00123', printf('%2$.*1$d', 5, 123)) + call assert_equal(' 123', printf('%2$ *1$d', 5, 123)) + call assert_equal(' +123', printf('%2$+ *1$d', 5, 123)) + + call assert_equal(' 123', printf('%1$*2$d', 123, 5)) + call assert_equal('123 ', printf('%1$*2$d', 123, -5)) + call assert_equal('00123', printf('%1$.*2$d', 123, 5)) + call assert_equal(' 123', printf('%1$ *2$d', 123, 5)) + call assert_equal(' +123', printf('%1$+ *2$d', 123, 5)) + + call assert_equal('foobar', printf('%2$.*1$s', 9, 'foobar')) + call assert_equal('foo', printf('%2$.*1$s', 3, 'foobar')) + call assert_equal('', printf('%2$.*1$s', 0, 'foobar')) + call assert_equal('foobar', printf('%2$.*1$s', -1, 'foobar')) + + #" Unrecognized format specifier kept as-is. + call assert_equal('_123', printf("%_%1$d", 123)) + + #" Test alternate forms. + call assert_equal('0x7b', printf('%1$#x', 123)) + call assert_equal('0X7B', printf('%1$#X', 123)) + call assert_equal('0173', printf('%1$#o', 123)) + call assert_equal('0173', printf('%1$#O', 123)) + call assert_equal('abc', printf('%1$#s', 'abc')) + call assert_equal('abc', printf('%1$#S', 'abc')) + + call assert_equal('1%', printf('%1$d%%', 1)) + call assert_notequal('', printf('%1$p', "abc")) + call assert_notequal('', printf('%2$d %1$p %3$s', "abc", 2, "abc")) + + #" Try argument re-use and argument swapping + call assert_equal('one two one', printf('%1$s %2$s %1$s', "one", "two")) + call assert_equal('Screen height: 400', printf('%1$s height: %2$d', "Screen", 400)) + call assert_equal('400 is: Screen height', printf('%2$d is: %1$s height', "Screen", 400)) + + #" Try out lots of combinations of argument types to skip + call assert_equal('9 12345 7654321', printf('%2$ld %1$d %3$lu', 12345, 9, 7654321)) + call assert_equal('9 1234567 7654321', printf('%2$d %1$ld %3$lu', 1234567, 9, 7654321)) + call assert_equal('9 1234567 7654321', printf('%2$d %1$lld %3$lu', 1234567, 9, 7654321)) + call assert_equal('9 12345 7654321', printf('%2$ld %1$u %3$lu', 12345, 9, 7654321)) + call assert_equal('9 1234567 7654321', printf('%2$d %1$lu %3$lu', 1234567, 9, 7654321)) + call assert_equal('9 1234567 7654321', printf('%2$d %1$llu %3$lu', 1234567, 9, 7654321)) + call assert_equal('9 1234567 7654321', printf('%2$d %1$llu %3$lu', 1234567, 9, 7654321)) + call assert_equal('9 deadbeef 7654321', printf('%2$d %1$x %3$lu', 0xdeadbeef, 9, 7654321)) + call assert_equal('9 c 7654321', printf('%2$ld %1$c %3$lu', 99, 9, 7654321)) + call assert_equal('9 hi 7654321', printf('%2$ld %1$s %3$lu', "hi", 9, 7654321)) + call assert_equal('9 0.000000e+00 7654321', printf('%2$ld %1$e %3$lu', 0.0, 9, 7654321)) + END + call v9.CheckLegacyAndVim9Success(lines) + + call v9.CheckLegacyAndVim9Failure(["call printf('%1$d%2$d', 1, 3, 4)"], "E767:") + + call v9.CheckLegacyAndVim9Failure(["call printf('%2$d%d', 1, 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%d%2$d', 1, 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%2$*1$d%d', 1, 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%d%2$*1$d', 1, 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%2$.*1$d%d', 1, 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%d%2$.*1$d', 1, 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$%')"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$')"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$_')"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*3$.*d', 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*.*2$d', 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*.*d', 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%*.*1$d', 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%*1$.*d', 3)"], "E1400:") + call v9.CheckLegacyAndVim9Failure(["call printf('%*1$.*1$d', 3)"], "E1400:") + + call v9.CheckLegacyAndVim9Failure(["call printf('%2$d', 3, 3)"], "E1401:") + + call v9.CheckLegacyAndVim9Failure(["call printf('%2$*1$d %1$ld', 3, 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$s %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$p %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$f %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$lud %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$llud %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$lld %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$s %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$c %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$ld %1$*1$d', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$ld %2$*1$d', 3, 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*1$ld', 3)"], "E1402:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*1$.*1$ld', 3)"], "E1402:") + + call v9.CheckLegacyAndVim9Failure(["call printf('%1$d%2$d', 3)"], "E1403:") + + call v9.CheckLegacyAndVim9Failure(["call printf('%1$d %1$s', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$ld %1$s', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$ud %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$s %1$f', 3.0)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*1$d %1$ld', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$s %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$p %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$f %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$lud %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$llud %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$lld %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$s %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$c %1$d', 3)"], "E1404:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$ld %1$d', 3)"], "E1404:") + + call v9.CheckLegacyAndVim9Failure(["call printf('%1$.2$d', 3)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%01$d', 3)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%01$0d', 3)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*2d', 3)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*3.*2$d', 3)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*3$.2$d', 3)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$*3$.*2d', 3)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$1$.5d', 5)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$5.1$d', 5)"], "E1405:") + call v9.CheckLegacyAndVim9Failure(["call printf('%1$1$.1$d', 5)"], "E1405:") +endfunc + +func Test_printf_pos_float() + let lines =<< trim END + call assert_equal('1.000000', printf('%1$f', 1)) + call assert_equal('1.230000', printf('%1$f', 1.23)) + call assert_equal('1.230000', printf('%1$F', 1.23)) + call assert_equal('9999999.9', printf('%1$g', 9999999.9)) + call assert_equal('9999999.9', printf('%1$G', 9999999.9)) + call assert_equal('1.230000e+00', printf('%1$e', 1.23)) + call assert_equal('1.230000E+00', printf('%1$E', 1.23)) + call assert_equal('1.200000e-02', printf('%1$e', 0.012)) + call assert_equal('-1.200000e-02', printf('%1$e', -0.012)) + call assert_equal('0.33', printf('%1$.2f', 1.0 / 3.0)) + + #" When precision is 0, the dot should be omitted. + call assert_equal(' 2', printf('%1$*2$.f', 7.0 / 3.0, 3)) + call assert_equal(' 2', printf('%2$*1$.f', 3, 7.0 / 3.0)) + call assert_equal(' 2', printf('%1$*2$.g', 7.0 / 3.0, 3)) + call assert_equal(' 2', printf('%2$*1$.g', 3, 7.0 / 3.0)) + call assert_equal(' 2e+00', printf('%1$*2$.e', 7.0 / 3.0, 7)) + call assert_equal(' 2e+00', printf('%2$*1$.e', 7, 7.0 / 3.0)) + + #" Float zero can be signed. + call assert_equal('+0.000000', printf('%1$+f', 0.0)) + call assert_equal('0.000000', printf('%1$f', 1.0 / (1.0 / 0.0))) + call assert_equal('-0.000000', printf('%1$f', 1.0 / (-1.0 / 0.0))) + call assert_equal('0.0', printf('%1$s', 1.0 / (1.0 / 0.0))) + call assert_equal('-0.0', printf('%1$s', 1.0 / (-1.0 / 0.0))) + call assert_equal('0.0', printf('%1$S', 1.0 / (1.0 / 0.0))) + call assert_equal('-0.0', printf('%1$S', 1.0 / (-1.0 / 0.0))) + + #" Float infinity can be signed. + call assert_equal('inf', printf('%1$f', 1.0 / 0.0)) + call assert_equal('-inf', printf('%1$f', -1.0 / 0.0)) + call assert_equal('inf', printf('%1$g', 1.0 / 0.0)) + call assert_equal('-inf', printf('%1$g', -1.0 / 0.0)) + call assert_equal('inf', printf('%1$e', 1.0 / 0.0)) + call assert_equal('-inf', printf('%1$e', -1.0 / 0.0)) + call assert_equal('INF', printf('%1$F', 1.0 / 0.0)) + call assert_equal('-INF', printf('%1$F', -1.0 / 0.0)) + call assert_equal('INF', printf('%1$E', 1.0 / 0.0)) + call assert_equal('-INF', printf('%1$E', -1.0 / 0.0)) + call assert_equal('INF', printf('%1$E', 1.0 / 0.0)) + call assert_equal('-INF', printf('%1$G', -1.0 / 0.0)) + call assert_equal('+inf', printf('%1$+f', 1.0 / 0.0)) + call assert_equal('-inf', printf('%1$+f', -1.0 / 0.0)) + call assert_equal(' inf', printf('%1$ f', 1.0 / 0.0)) + call assert_equal(' inf', printf('%1$*2$f', 1.0 / 0.0, 6)) + call assert_equal(' -inf', printf('%1$*2$f', -1.0 / 0.0, 6)) + call assert_equal(' inf', printf('%1$*2$g', 1.0 / 0.0, 6)) + call assert_equal(' -inf', printf('%1$*2$g', -1.0 / 0.0, 6)) + call assert_equal(' +inf', printf('%1$+*2$f', 1.0 / 0.0, 6)) + call assert_equal(' inf', printf('%1$ *2$f', 1.0 / 0.0, 6)) + call assert_equal(' +inf', printf('%1$+0*2$f', 1.0 / 0.0, 6)) + call assert_equal('inf ', printf('%1$-*2$f', 1.0 / 0.0, 6)) + call assert_equal('-inf ', printf('%1$-*2$f', -1.0 / 0.0, 6)) + call assert_equal('+inf ', printf('%1$-+*2$f', 1.0 / 0.0, 6)) + call assert_equal(' inf ', printf('%1$- *2$f', 1.0 / 0.0, 6)) + call assert_equal('-INF ', printf('%1$-*2$F', -1.0 / 0.0, 6)) + call assert_equal('+INF ', printf('%1$-+*2$F', 1.0 / 0.0, 6)) + call assert_equal(' INF ', printf('%1$- *2$F', 1.0 / 0.0, 6)) + call assert_equal('INF ', printf('%1$-*2$G', 1.0 / 0.0, 6)) + call assert_equal('-INF ', printf('%1$-*2$G', -1.0 / 0.0, 6)) + call assert_equal('INF ', printf('%1$-*2$E', 1.0 / 0.0, 6)) + call assert_equal('-INF ', printf('%1$-*2$E', -1.0 / 0.0, 6)) + call assert_equal(' inf', printf('%2$*1$f', 6, 1.0 / 0.0)) + call assert_equal(' -inf', printf('%2$*1$f', 6, -1.0 / 0.0)) + call assert_equal(' inf', printf('%2$*1$g', 6, 1.0 / 0.0)) + call assert_equal(' -inf', printf('%2$*1$g', 6, -1.0 / 0.0)) + call assert_equal(' +inf', printf('%2$+*1$f', 6, 1.0 / 0.0)) + call assert_equal(' inf', printf('%2$ *1$f', 6, 1.0 / 0.0)) + call assert_equal(' +inf', printf('%2$+0*1$f', 6, 1.0 / 0.0)) + call assert_equal('inf ', printf('%2$-*1$f', 6, 1.0 / 0.0)) + call assert_equal('-inf ', printf('%2$-*1$f', 6, -1.0 / 0.0)) + call assert_equal('+inf ', printf('%2$-+*1$f', 6, 1.0 / 0.0)) + call assert_equal(' inf ', printf('%2$- *1$f', 6, 1.0 / 0.0)) + call assert_equal('-INF ', printf('%2$-*1$F', 6, -1.0 / 0.0)) + call assert_equal('+INF ', printf('%2$-+*1$F', 6, 1.0 / 0.0)) + call assert_equal(' INF ', printf('%2$- *1$F', 6, 1.0 / 0.0)) + call assert_equal('INF ', printf('%2$-*1$G', 6, 1.0 / 0.0)) + call assert_equal('-INF ', printf('%2$-*1$G', 6, -1.0 / 0.0)) + call assert_equal('INF ', printf('%2$-*1$E', 6, 1.0 / 0.0)) + call assert_equal('-INF ', printf('%2$-*1$E', 6, -1.0 / 0.0)) + call assert_equal('inf', printf('%1$s', 1.0 / 0.0)) + call assert_equal('-inf', printf('%1$s', -1.0 / 0.0)) + + #" Test special case where max precision is truncated at 340. + call assert_equal('1.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', printf('%1$.*2$f', 1.0, 330)) + call assert_equal('1.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', printf('%2$.*1$f', 330, 1.0)) + call assert_equal('1.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', printf('%1$.*2$f', 1.0, 340)) + call assert_equal('1.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', printf('%2$.*1$f', 340, 1.0)) + call assert_equal('1.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', printf('%1$.*2$f', 1.0, 350)) + call assert_equal('1.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', printf('%2$.*1$f', 350, 1.0)) + + #" Float nan (not a number) has no sign. + call assert_equal('nan', printf('%1$f', sqrt(-1.0))) + call assert_equal('nan', printf('%1$f', 0.0 / 0.0)) + call assert_equal('nan', printf('%1$f', -0.0 / 0.0)) + call assert_equal('nan', printf('%1$g', 0.0 / 0.0)) + call assert_equal('nan', printf('%1$e', 0.0 / 0.0)) + call assert_equal('NAN', printf('%1$F', 0.0 / 0.0)) + call assert_equal('NAN', printf('%1$G', 0.0 / 0.0)) + call assert_equal('NAN', printf('%1$E', 0.0 / 0.0)) + call assert_equal('NAN', printf('%1$F', -0.0 / 0.0)) + call assert_equal('NAN', printf('%1$G', -0.0 / 0.0)) + call assert_equal('NAN', printf('%1$E', -0.0 / 0.0)) + call assert_equal(' nan', printf('%1$*2$f', 0.0 / 0.0, 6)) + call assert_equal(' nan', printf('%1$0*2$f', 0.0 / 0.0, 6)) + call assert_equal('nan ', printf('%1$-*2$f', 0.0 / 0.0, 6)) + call assert_equal('nan ', printf('%1$- *2$f', 0.0 / 0.0, 6)) + call assert_equal(' nan', printf('%2$*1$f', 6, 0.0 / 0.0)) + call assert_equal(' nan', printf('%2$0*1$f', 6, 0.0 / 0.0)) + call assert_equal('nan ', printf('%2$-*1$f', 6, 0.0 / 0.0)) + call assert_equal('nan ', printf('%2$- *1$f', 6, 0.0 / 0.0)) + call assert_equal('nan', printf('%1$s', 0.0 / 0.0)) + call assert_equal('nan', printf('%1$s', -0.0 / 0.0)) + call assert_equal('nan', printf('%1$S', 0.0 / 0.0)) + call assert_equal('nan', printf('%1$S', -0.0 / 0.0)) + END + call v9.CheckLegacyAndVim9Success(lines) + + call v9.CheckLegacyAndVim9Failure(['echo printf("%f", "a")'], 'E807:') +endfunc + +func Test_printf_pos_errors() + call v9.CheckLegacyAndVim9Failure(['echo printf("%1$d", {})'], 'E728:') + call v9.CheckLegacyAndVim9Failure(['echo printf("%1$d", [])'], 'E745:') + call v9.CheckLegacyAndVim9Failure(['echo printf("%1$d", 1, 2)'], 'E767:') + call v9.CheckLegacyAndVim9Failure(['echo printf("%*d", 1)'], 'E766:') + call v9.CheckLegacyAndVim9Failure(['echo printf("%1$s")'], 'E1403:') + call v9.CheckLegacyAndVim9Failure(['echo printf("%1$d", 1.2)'], 'E805:') + call v9.CheckLegacyAndVim9Failure(['echo printf("%1$f")'], 'E1403:') +endfunc + +func Test_printf_pos_64bit() + let lines =<< trim END + call assert_equal("123456789012345", printf('%1$d', 123456789012345)) + END + call v9.CheckLegacyAndVim9Success(lines) +endfunc + +func Test_printf_pos_spec_s() + let lines =<< trim END + #" number + call assert_equal("1234567890", printf('%1$s', 1234567890)) + + #" string + call assert_equal("abcdefgi", printf('%1$s', "abcdefgi")) + + #" float + call assert_equal("1.23", printf('%1$s', 1.23)) + + #" list + VAR lvalue = [1, 'two', ['three', 4]] + call assert_equal(string(lvalue), printf('%1$s', lvalue)) + + #" dict + VAR dvalue = {'key1': 'value1', 'key2': ['list', 'lvalue'], 'key3': {'dict': 'lvalue'}} + call assert_equal(string(dvalue), printf('%1$s', dvalue)) + + #" funcref + call assert_equal('printf', printf('%1$s', 'printf'->function())) + + #" partial + call assert_equal(string(function('printf', ['%1$s'])), printf('%1$s', function('printf', ['%1$s']))) + END + call v9.CheckLegacyAndVim9Success(lines) +endfunc + +func Test_printf_pos_spec_b() + let lines =<< trim END + call assert_equal("0", printf('%1$b', 0)) + call assert_equal("00001100", printf('%1$0*2$b', 12, 8)) + call assert_equal("11111111", printf('%1$0*2$b', 0xff, 8)) + call assert_equal(" 1111011", printf('%1$*2$b', 123, 10)) + call assert_equal("0001111011", printf('%1$0*2$b', 123, 10)) + call assert_equal(" 0b1111011", printf('%1$#*2$b', 123, 10)) + call assert_equal("0B01111011", printf('%1$#0*2$B', 123, 10)) + call assert_equal("00001100", printf('%2$0*1$b', 8, 12)) + call assert_equal("11111111", printf('%2$0*1$b', 8, 0xff)) + call assert_equal(" 1111011", printf('%2$*1$b', 10, 123)) + call assert_equal("0001111011", printf('%2$0*1$b', 10, 123)) + call assert_equal(" 0b1111011", printf('%2$#*1$b', 10, 123)) + call assert_equal("0B01111011", printf('%2$#0*1$B', 10, 123)) + call assert_equal("1001001100101100000001011010010", printf('%1$b', 1234567890)) + call assert_equal("11100000100100010000110000011011101111101111001", printf('%1$b', 123456789012345)) + call assert_equal("1111111111111111111111111111111111111111111111111111111111111111", printf('%1$b', -1)) + END + call v9.CheckLegacyAndVim9Success(lines) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -696,6 +696,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 1704, +/**/ 1703, /**/ 1702,