From 7f98d9c6d59472abf189ccd5a5c76415f9d1cbb3 Mon Sep 17 00:00:00 2001 From: Philipp Stephani Date: Wed, 31 May 2017 22:09:39 -0700 Subject: [PATCH 1/2] Implement field numbers in format strings A field number explicitly specifies the argument to be formatted. This is especially important for potential localization work, since grammars of various languages dictate different word orders. * src/editfns.c (Fformat): Update documentation. (styled_format): Implement field numbers. * doc/lispref/strings.texi (Formatting Strings): Document field numbers. * lisp/emacs-lisp/bytecomp.el (byte-compile-format-warn): Adapt. * test/src/editfns-tests.el (format-with-field): New unit test. --- doc/lispref/strings.texi | 31 ++++++++++++++++++++++--- etc/NEWS | 3 +++ lisp/emacs-lisp/bytecomp.el | 11 ++++++--- src/editfns.c | 55 ++++++++++++++++++++++++++++++++++++++------- test/src/editfns-tests.el | 18 +++++++++++++++ 5 files changed, 104 insertions(+), 14 deletions(-) diff --git a/doc/lispref/strings.texi b/doc/lispref/strings.texi index 9436a96..9bf52f2 100644 --- a/doc/lispref/strings.texi +++ b/doc/lispref/strings.texi @@ -864,7 +864,8 @@ Formatting Strings (format "%s" @var{arbitrary-string}) @end example - If @var{string} contains more than one format specification, the + If @var{string} contains more than one format specification and none +of the format specifications contain an explicit field number, the format specifications correspond to successive values from @var{objects}. Thus, the first format specification in @var{string} uses the first such value, the second format specification uses the @@ -961,6 +962,25 @@ Formatting Strings @end group @end example address@hidden field number + A specification can have a @dfn{field number}, which is a decimal +number after the initial @samp{%}, followed by a literal dollar sign address@hidden If you provide a field numer, then the argument to be +printed corresponds to the given field number instead of the next +argument. Field numbers start at 1. + +You can mix specifications with and without field numbers. A +specification without a field number that follows a specification with +a field number will convert the argument after the one specified by +the field number: + address@hidden +(format "First argument %2$s, then %s, then %1$s" 1 2 3) + @result{} "First argument 2, then 3, then 1" address@hidden example + +You can't use field numbers in a @samp{%%} specification. + @cindex field width @cindex padding A specification can have a @dfn{width}, which is a decimal number @@ -996,9 +1016,14 @@ Formatting Strings @end group @end example +If you want to use both a field number and a width, place the field +number before the width. For example, in @samp{%2$7s}, @samp{2} is +the field number and @samp{7} is the width. + @cindex flags in format specifications - Immediately after the @samp{%} and before the optional width -specifier, you can also put certain @dfn{flag characters}. + After the @samp{%} and before the optional width specifier, you can +also put certain @dfn{flag characters}. The flag characters need to +come directly after a potential field number. The flag @samp{+} inserts a plus sign before a positive number, so that it always has a sign. A space character as flag inserts a space diff --git a/etc/NEWS b/etc/NEWS index 43e7897..e31c0c7 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -364,6 +364,9 @@ large integers from being displayed as characters. ** Two new commands for finding the source code of Emacs Lisp libraries: 'find-library-other-window' and 'find-library-other-frame'. +** You can now provide explicit field numbers in format specifiers. +For example, '(format "%2$s %1$s" 1 2)' produces "2 1". + * Editing Changes in Emacs 26.1 diff --git a/lisp/emacs-lisp/bytecomp.el b/lisp/emacs-lisp/bytecomp.el index 12a7d4a..e5b9b47 100644 --- a/lisp/emacs-lisp/bytecomp.el +++ b/lisp/emacs-lisp/bytecomp.el @@ -1375,10 +1375,15 @@ byte-compile-format-warn (let ((nfields (with-temp-buffer (insert (nth 1 form)) (goto-char (point-min)) - (let ((n 0)) + (let ((i 0) (n 0)) (while (re-search-forward "%." nil t) - (unless (eq ?% (char-after (1+ (match-beginning 0)))) - (setq n (1+ n)))) + (backward-char) + (unless (eq ?% (char-after)) + (setq i (if (looking-at "\\([0-9]+\\)\\$") + (string-to-number (match-string 1) 10) + (1+ i)) + n (max n i))) + (forward-char)) n))) (nargs (- (length form) 2))) (unless (= nargs nfields) diff --git a/src/editfns.c b/src/editfns.c index 89a6724..44341ce 100644 --- a/src/editfns.c +++ b/src/editfns.c @@ -48,6 +48,7 @@ along with GNU Emacs. If not, see . */ #include #include +#include #include #include #include @@ -3856,7 +3857,7 @@ The first argument is a format control string. The other arguments are substituted into it to make the result, a string. The format control string may contain %-sequences meaning to substitute -the next available argument: +the next available argument, or the argument explicitly specified: %s means print a string argument. Actually, prints any object, with `princ'. %d means print as signed number in decimal. @@ -3873,13 +3874,17 @@ the next available argument: The argument used for %d, %o, %x, %e, %f, %g or %c must be a number. Use %% to put a single % into the output. -A %-sequence may contain optional flag, width, and precision -specifiers, as follows: +A %-sequence may contain optional field number, flag, width, and +precision specifiers, as follows: - %character + %character -where flags is [+ #-0]+, width is [0-9]+, and precision is a literal -period "." followed by [0-9]+ +where field is [0-9]+ followed by a literal dollar "$", flags is +[+ #-0]+, width is [0-9]+, and precision is a literal period "." +followed by [0-9]+. + +If field is given, it must be a one-based argument number; the given +argument is substituted instead of the next one. The + flag character inserts a + before any positive number, while a space inserts a space before any positive number; these flags only @@ -4032,14 +4037,19 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) { /* General format specifications look like - '%' [flags] [field-width] [precision] format + '%' [field-number] [flags] [field-width] [precision] format where + field-number ::= [0-9]+ '$' flags ::= [-+0# ]+ field-width ::= [0-9]+ precision ::= '.' [0-9]* + If a field-number is specified, it specifies the argument + number to substitute. Otherwise, the next argument is + taken. + If a field-width is specified, it specifies to which width the output should be padded with blanks, if the output string is shorter than field-width. @@ -4048,6 +4058,29 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) digits to print after the '.' for floats, or the max. number of chars to print from a string. */ + char *field_end; + uintmax_t raw_field = strtoumax (format, &field_end, 10); + bool has_field = false; + if (c_isdigit (*format) && *field_end == '$') + { + if (raw_field < 1 || raw_field >= PTRDIFF_MAX) + { + /* doprnt doesn't support %.*s, so we need to copy + the field number string. */ + ptrdiff_t length = field_end - format; + eassert (length > 0); + eassert (length < PTRDIFF_MAX); + char *field = SAFE_ALLOCA (length + 1); + memcpy (field, format, length); + field[length] = '\0'; + error ("Invalid field number `%s'", field); + } + has_field = true; + /* n is incremented below. */ + n = raw_field - 1; + format = field_end + 1; + } + bool minus_flag = false; bool plus_flag = false; bool space_flag = false; @@ -4090,7 +4123,13 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) memset (&discarded[format0 - format_start], 1, format - format0 - (conversion == '%')); if (conversion == '%') - goto copy_char; + { + if (has_field) + /* FIXME: `error' doesn't appear to support `%%'. */ + error ("Field number specified together with `%c' conversion", + '%'); + goto copy_char; + } ++n; if (! (n < nargs)) diff --git a/test/src/editfns-tests.el b/test/src/editfns-tests.el index 8019eb0..f76c6c9 100644 --- a/test/src/editfns-tests.el +++ b/test/src/editfns-tests.el @@ -177,4 +177,22 @@ transpose-test-get-byte-positions (format-time-string "%Y-%m-%d %H:%M:%S.%3N %z" nil (concat (make-string 2048 ?X) "0"))))) +(ert-deftest format-with-field () + (should (equal (format "First argument %2$s, then %s, then %1$s" 1 2 3) + "First argument 2, then 3, then 1")) + (should (equal (format "a %2$s %d %1$d %2$S %d %d b" 11 "22" 33 44) + "a 22 33 11 \"22\" 33 44 b")) + (should (equal (format "a %08$s %s b" 1 2 3 4 5 6 7 8 9) "a 8 9 b")) + (should (equal (should-error (format "a %999999$s b" 11)) + '(error "Not enough arguments for format string"))) + (should (equal (should-error (format "a %$s b" 11)) + ;; FIXME: there shouldn't be two % in the error + ;; string! + '(error "Invalid format operation %%$"))) + (should (equal (should-error (format "a %0$s b" 11)) + '(error "Invalid field number `0'"))) + (should (equal + (should-error (format "a %1$% %s b" 11)) + '(error "Field number specified together with `%' conversion")))) + ;;; editfns-tests.el ends here -- 2.9.4