From: Lars Ingebrigtsen Date: Sat, 13 Jul 2019 01:50:43 +0000 (+0200) Subject: Document format-spec and expand the modifiers it supports X-Git-Tag: emacs-27.0.90~1976 X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=936d074d7c58bd4504b89a0b739b370312ae141a;p=emacs.git 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). --- diff --git a/doc/lispref/text.texi b/doc/lispref/text.texi index 94b94eaba7e..df9fce066f0 100644 --- a/doc/lispref/text.texi +++ b/doc/lispref/text.texi @@ -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 diff --git a/etc/NEWS b/etc/NEWS index 7e10d132dbe..902203f0c33 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -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 diff --git a/lisp/format-spec.el b/lisp/format-spec.el index cf2d364bb28..220cecd9b05 100644 --- a/lisp/format-spec.el +++ b/lisp/format-spec.el @@ -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, diff --git a/test/lisp/format-spec-tests.el b/test/lisp/format-spec-tests.el index 6fbfaaad83a..a386e9da8ff 100644 --- a/test/lisp/format-spec-tests.el +++ b/test/lisp/format-spec-tests.el @@ -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