Document format-spec and expand the modifiers it supports

* doc/lispref/text.texi (Interpolated Strings): New section.
* lisp/format-spec.el (format-spec--parse-modifiers)
(format-spec--pad): New functions.
(format-spec): Support more format modifiers (bug#32931).
This commit is contained in:
Lars Ingebrigtsen 2019-07-13 03:50:43 +02:00
parent 74579d3d2b
commit 936d074d7c
4 changed files with 157 additions and 7 deletions

View file

@ -58,6 +58,7 @@ the character after point.
of another buffer.
* Decompression:: Dealing with compressed data.
* Base 64:: Conversion to or from base 64 encoding.
* Interpolated Strings:: Formatting Customizable Strings.
* Checksum/Hash:: Computing cryptographic hashes.
* GnuTLS Cryptography:: Cryptographic algorithms imported from GnuTLS.
* Parsing HTML/XML:: Parsing HTML and XML.
@ -4626,6 +4627,72 @@ If optional argument @var{base64url} is is non-@code{nil}, then padding
is optional, and the URL variant of base 64 encoding is used.
@end defun
@node Interpolated Strings
@section Formatting Customizable Strings
It is, in some circumstances, useful to present users with a string to
be customized that can then be expanded programmatically. For
instance, @code{erc-header-line-format} is @code{"%n on %t (%m,%l)
%o"}, and each of those characters after the percent signs are
expanded when the header line is computed. To do this, the
@code{format-spec} function is used:
@defun format-spec format specification &optional only-present
@var{format} is the format specification string as in the example
above. @var{specification} is an alist that has elements where the
@code{car} is a character and the @code{cdr} is the substitution.
If @code{ONLY-PRESENT} is @code{nil}, errors will be signalled if a
format character has been used that's not present in
@var{specification}. If it's non-@code{nil}, that format
specification is left verbatim in the result.
@end defun
Here's a trivial example:
@example
(format-spec "su - %u %l"
`((?u . ,(user-login-name))
(?l . "ls")))
=> "su - foo ls"
@end example
In addition to allowing padding/limiting to a certain length, the
following modifiers are can be used:
@table @asis
@item @samp{0}
Use zero padding.
@item @samp{@ }
User space padding.
@item @samp{-}
Pad to the right.
@item @samp{^}
Use upper case.
@item @samp{_}
Use lower case.
@item @samp{<}
If the length needs to limited, remove characters from the left.
@item @samp{>}
Same as previous, but remove characters from the right.
@end table
If contradictory modifiers are used (for instance, both upper- and
lower case), then what happens is undefined.
As an example, @samp{"%<010b"} means ``insert the @samp{b} expansion,
but pad with leading zeroes if it's less than ten characters, and if
it's more than ten characters, shorten by removing characters from the
left''.
@node Checksum/Hash
@section Checksum/Hash
@cindex MD5 checksum

View file

@ -2293,6 +2293,13 @@ argument is 'iec' and the empty string otherwise. We recomment a
space or non-breaking space as third argument, and "B" as fourth
argument, circumstances allowing.
+++
** `format-spec' has been expanded with several modifiers to allow
greater flexibility when customizing variables. The modifiers include
zero-padding, upper- and lower-casing, and limiting the length of the
interpolated strings. The function has now also been documented in
the Emacs Lisp manual.
* Changes in Emacs 27.1 on Non-Free Operating Systems

View file

@ -24,6 +24,8 @@
;;; Code:
(require 'subr-x)
(defun format-spec (format specification &optional only-present)
"Return a string based on FORMAT and SPECIFICATION.
FORMAT is a string containing `format'-like specs like \"su - %u %k\",
@ -32,9 +34,22 @@ to values.
For instance:
(format-spec \"su - %u %k\"
(format-spec \"su - %u %l\"
`((?u . ,(user-login-name))
(?k . \"ls\")))
(?l . \"ls\")))
Each format spec can have modifiers, where \"%<010b\" means \"if
the expansion is shorter than ten characters, zero-pad it, and if
it's longer, chop off characters from the left size\".
The following modifiers are allowed:
* 0: Use zero-padding.
* -: Pad to the right.
* ^: Upper-case the expansion.
* _: Lower-case the expansion.
* <: Limit the length by removing chars from the left.
* >: Limit the length by removing chars from the right.
Any text properties on a %-spec itself are propagated to the text
that it generates.
@ -52,16 +67,31 @@ where they are, including \"%%\" strings."
(unless only-present
(delete-char 1)))
;; Valid format spec.
((looking-at "\\([-0-9.]*\\)\\([a-zA-Z]\\)")
(let* ((num (match-string 1))
(spec (string-to-char (match-string 2)))
((looking-at "\\([-0 _^<>]*\\)\\([0-9.]*\\)\\([a-zA-Z]\\)")
(let* ((modifiers (match-string 1))
(num (match-string 2))
(spec (string-to-char (match-string 3)))
(val (assq spec specification)))
(if (not val)
(unless only-present
(error "Invalid format character: `%%%c'" spec))
(setq val (cdr val))
(setq val (cdr val)
modifiers (format-spec--parse-modifiers modifiers))
;; Pad result to desired length.
(let ((text (format (concat "%" num "s") val)))
(let ((text (format "%s" val)))
(when num
(setq num (string-to-number num))
(setq text (format-spec--pad text num modifiers))
(when (> (length text) num)
(cond
((memq :chop-left modifiers)
(setq text (substring text (- (length text) num))))
((memq :chop-right modifiers)
(setq text (substring text 0 num))))))
(when (memq :uppercase modifiers)
(setq text (upcase text)))
(when (memq :lowercase modifiers)
(setq text (downcase text)))
;; Insert first, to preserve text properties.
(insert-and-inherit text)
;; Delete the specifier body.
@ -75,6 +105,34 @@ where they are, including \"%%\" strings."
(error "Invalid format string")))))
(buffer-string)))
(defun format-spec--pad (text total-length modifiers)
(if (> (length text) total-length)
;; The text is longer than the specified length; do nothing.
text
(let ((padding (make-string (- total-length (length text))
(if (memq :zero-pad modifiers)
?0
?\s))))
(if (memq :right-pad modifiers)
(concat text padding)
(concat padding text)))))
(defun format-spec--parse-modifiers (modifiers)
(let ((elems nil))
(mapc (lambda (char)
(when-let ((modifier
(pcase char
(?0 :zero-pad)
(?\s :space-pad)
(?^ :uppercase)
(?_ :lowercase)
(?- :right-pad)
(?< :chop-left)
(?> :chop-right))))
(push modifier elems)))
modifiers)
elems))
(defun format-spec-make (&rest pairs)
"Return an alist suitable for use in `format-spec' based on PAIRS.
PAIRS is a list where every other element is a character and a value,

View file

@ -37,4 +37,22 @@
(should (equal (format-spec "foo %b %z %% zot" '((?b . "bar")) t)
"foo bar %z %% zot")))
(ert-deftest test-format-modifiers ()
(should (equal (format-spec "foo %10b zot" '((?b . "bar")))
"foo bar zot"))
(should (equal (format-spec "foo % 10b zot" '((?b . "bar")))
"foo bar zot"))
(should (equal (format-spec "foo %-010b zot" '((?b . "bar")))
"foo bar0000000 zot"))
(should (equal (format-spec "foo %0-10b zot" '((?b . "bar")))
"foo bar0000000 zot"))
(should (equal (format-spec "foo %^10b zot" '((?b . "bar")))
"foo BAR zot"))
(should (equal (format-spec "foo %_10b zot" '((?b . "BAR")))
"foo bar zot"))
(should (equal (format-spec "foo %<4b zot" '((?b . "longbar")))
"foo gbar zot"))
(should (equal (format-spec "foo %>4b zot" '((?b . "longbar")))
"foo long zot")))
;;; format-spec-tests.el ends here