diff options
| author | Lars Ingebrigtsen | 2019-07-13 03:50:43 +0200 |
|---|---|---|
| committer | Lars Ingebrigtsen | 2019-07-13 03:50:50 +0200 |
| commit | 936d074d7c58bd4504b89a0b739b370312ae141a (patch) | |
| tree | 254c2d3e4e2ae15aff37479bc7451e6fb9a54282 | |
| parent | 74579d3d2bb82f300a6f2d81b7b559f0a24061db (diff) | |
| download | emacs-936d074d7c58bd4504b89a0b739b370312ae141a.tar.gz emacs-936d074d7c58bd4504b89a0b739b370312ae141a.zip | |
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).
| -rw-r--r-- | doc/lispref/text.texi | 67 | ||||
| -rw-r--r-- | etc/NEWS | 7 | ||||
| -rw-r--r-- | lisp/format-spec.el | 72 | ||||
| -rw-r--r-- | test/lisp/format-spec-tests.el | 18 |
4 files changed, 157 insertions, 7 deletions
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. | |||
| 58 | of another buffer. | 58 | of another buffer. |
| 59 | * Decompression:: Dealing with compressed data. | 59 | * Decompression:: Dealing with compressed data. |
| 60 | * Base 64:: Conversion to or from base 64 encoding. | 60 | * Base 64:: Conversion to or from base 64 encoding. |
| 61 | * Interpolated Strings:: Formatting Customizable Strings. | ||
| 61 | * Checksum/Hash:: Computing cryptographic hashes. | 62 | * Checksum/Hash:: Computing cryptographic hashes. |
| 62 | * GnuTLS Cryptography:: Cryptographic algorithms imported from GnuTLS. | 63 | * GnuTLS Cryptography:: Cryptographic algorithms imported from GnuTLS. |
| 63 | * Parsing HTML/XML:: Parsing HTML and XML. | 64 | * Parsing HTML/XML:: Parsing HTML and XML. |
| @@ -4626,6 +4627,72 @@ If optional argument @var{base64url} is is non-@code{nil}, then padding | |||
| 4626 | is optional, and the URL variant of base 64 encoding is used. | 4627 | is optional, and the URL variant of base 64 encoding is used. |
| 4627 | @end defun | 4628 | @end defun |
| 4628 | 4629 | ||
| 4630 | |||
| 4631 | @node Interpolated Strings | ||
| 4632 | @section Formatting Customizable Strings | ||
| 4633 | |||
| 4634 | It is, in some circumstances, useful to present users with a string to | ||
| 4635 | be customized that can then be expanded programmatically. For | ||
| 4636 | instance, @code{erc-header-line-format} is @code{"%n on %t (%m,%l) | ||
| 4637 | %o"}, and each of those characters after the percent signs are | ||
| 4638 | expanded when the header line is computed. To do this, the | ||
| 4639 | @code{format-spec} function is used: | ||
| 4640 | |||
| 4641 | @defun format-spec format specification &optional only-present | ||
| 4642 | @var{format} is the format specification string as in the example | ||
| 4643 | above. @var{specification} is an alist that has elements where the | ||
| 4644 | @code{car} is a character and the @code{cdr} is the substitution. | ||
| 4645 | |||
| 4646 | If @code{ONLY-PRESENT} is @code{nil}, errors will be signalled if a | ||
| 4647 | format character has been used that's not present in | ||
| 4648 | @var{specification}. If it's non-@code{nil}, that format | ||
| 4649 | specification is left verbatim in the result. | ||
| 4650 | @end defun | ||
| 4651 | |||
| 4652 | Here's a trivial example: | ||
| 4653 | |||
| 4654 | @example | ||
| 4655 | (format-spec "su - %u %l" | ||
| 4656 | `((?u . ,(user-login-name)) | ||
| 4657 | (?l . "ls"))) | ||
| 4658 | => "su - foo ls" | ||
| 4659 | @end example | ||
| 4660 | |||
| 4661 | In addition to allowing padding/limiting to a certain length, the | ||
| 4662 | following modifiers are can be used: | ||
| 4663 | |||
| 4664 | @table @asis | ||
| 4665 | @item @samp{0} | ||
| 4666 | Use zero padding. | ||
| 4667 | |||
| 4668 | @item @samp{@ } | ||
| 4669 | User space padding. | ||
| 4670 | |||
| 4671 | @item @samp{-} | ||
| 4672 | Pad to the right. | ||
| 4673 | |||
| 4674 | @item @samp{^} | ||
| 4675 | Use upper case. | ||
| 4676 | |||
| 4677 | @item @samp{_} | ||
| 4678 | Use lower case. | ||
| 4679 | |||
| 4680 | @item @samp{<} | ||
| 4681 | If the length needs to limited, remove characters from the left. | ||
| 4682 | |||
| 4683 | @item @samp{>} | ||
| 4684 | Same as previous, but remove characters from the right. | ||
| 4685 | @end table | ||
| 4686 | |||
| 4687 | If contradictory modifiers are used (for instance, both upper- and | ||
| 4688 | lower case), then what happens is undefined. | ||
| 4689 | |||
| 4690 | As an example, @samp{"%<010b"} means ``insert the @samp{b} expansion, | ||
| 4691 | but pad with leading zeroes if it's less than ten characters, and if | ||
| 4692 | it's more than ten characters, shorten by removing characters from the | ||
| 4693 | left''. | ||
| 4694 | |||
| 4695 | |||
| 4629 | @node Checksum/Hash | 4696 | @node Checksum/Hash |
| 4630 | @section Checksum/Hash | 4697 | @section Checksum/Hash |
| 4631 | @cindex MD5 checksum | 4698 | @cindex MD5 checksum |
| @@ -2293,6 +2293,13 @@ argument is 'iec' and the empty string otherwise. We recomment a | |||
| 2293 | space or non-breaking space as third argument, and "B" as fourth | 2293 | space or non-breaking space as third argument, and "B" as fourth |
| 2294 | argument, circumstances allowing. | 2294 | argument, circumstances allowing. |
| 2295 | 2295 | ||
| 2296 | +++ | ||
| 2297 | ** `format-spec' has been expanded with several modifiers to allow | ||
| 2298 | greater flexibility when customizing variables. The modifiers include | ||
| 2299 | zero-padding, upper- and lower-casing, and limiting the length of the | ||
| 2300 | interpolated strings. The function has now also been documented in | ||
| 2301 | the Emacs Lisp manual. | ||
| 2302 | |||
| 2296 | 2303 | ||
| 2297 | * Changes in Emacs 27.1 on Non-Free Operating Systems | 2304 | * Changes in Emacs 27.1 on Non-Free Operating Systems |
| 2298 | 2305 | ||
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 @@ | |||
| 24 | 24 | ||
| 25 | ;;; Code: | 25 | ;;; Code: |
| 26 | 26 | ||
| 27 | (require 'subr-x) | ||
| 28 | |||
| 27 | (defun format-spec (format specification &optional only-present) | 29 | (defun format-spec (format specification &optional only-present) |
| 28 | "Return a string based on FORMAT and SPECIFICATION. | 30 | "Return a string based on FORMAT and SPECIFICATION. |
| 29 | FORMAT is a string containing `format'-like specs like \"su - %u %k\", | 31 | FORMAT is a string containing `format'-like specs like \"su - %u %k\", |
| @@ -32,9 +34,22 @@ to values. | |||
| 32 | 34 | ||
| 33 | For instance: | 35 | For instance: |
| 34 | 36 | ||
| 35 | (format-spec \"su - %u %k\" | 37 | (format-spec \"su - %u %l\" |
| 36 | `((?u . ,(user-login-name)) | 38 | `((?u . ,(user-login-name)) |
| 37 | (?k . \"ls\"))) | 39 | (?l . \"ls\"))) |
| 40 | |||
| 41 | Each format spec can have modifiers, where \"%<010b\" means \"if | ||
| 42 | the expansion is shorter than ten characters, zero-pad it, and if | ||
| 43 | it's longer, chop off characters from the left size\". | ||
| 44 | |||
| 45 | The following modifiers are allowed: | ||
| 46 | |||
| 47 | * 0: Use zero-padding. | ||
| 48 | * -: Pad to the right. | ||
| 49 | * ^: Upper-case the expansion. | ||
| 50 | * _: Lower-case the expansion. | ||
| 51 | * <: Limit the length by removing chars from the left. | ||
| 52 | * >: Limit the length by removing chars from the right. | ||
| 38 | 53 | ||
| 39 | Any text properties on a %-spec itself are propagated to the text | 54 | Any text properties on a %-spec itself are propagated to the text |
| 40 | that it generates. | 55 | that it generates. |
| @@ -52,16 +67,31 @@ where they are, including \"%%\" strings." | |||
| 52 | (unless only-present | 67 | (unless only-present |
| 53 | (delete-char 1))) | 68 | (delete-char 1))) |
| 54 | ;; Valid format spec. | 69 | ;; Valid format spec. |
| 55 | ((looking-at "\\([-0-9.]*\\)\\([a-zA-Z]\\)") | 70 | ((looking-at "\\([-0 _^<>]*\\)\\([0-9.]*\\)\\([a-zA-Z]\\)") |
| 56 | (let* ((num (match-string 1)) | 71 | (let* ((modifiers (match-string 1)) |
| 57 | (spec (string-to-char (match-string 2))) | 72 | (num (match-string 2)) |
| 73 | (spec (string-to-char (match-string 3))) | ||
| 58 | (val (assq spec specification))) | 74 | (val (assq spec specification))) |
| 59 | (if (not val) | 75 | (if (not val) |
| 60 | (unless only-present | 76 | (unless only-present |
| 61 | (error "Invalid format character: `%%%c'" spec)) | 77 | (error "Invalid format character: `%%%c'" spec)) |
| 62 | (setq val (cdr val)) | 78 | (setq val (cdr val) |
| 79 | modifiers (format-spec--parse-modifiers modifiers)) | ||
| 63 | ;; Pad result to desired length. | 80 | ;; Pad result to desired length. |
| 64 | (let ((text (format (concat "%" num "s") val))) | 81 | (let ((text (format "%s" val))) |
| 82 | (when num | ||
| 83 | (setq num (string-to-number num)) | ||
| 84 | (setq text (format-spec--pad text num modifiers)) | ||
| 85 | (when (> (length text) num) | ||
| 86 | (cond | ||
| 87 | ((memq :chop-left modifiers) | ||
| 88 | (setq text (substring text (- (length text) num)))) | ||
| 89 | ((memq :chop-right modifiers) | ||
| 90 | (setq text (substring text 0 num)))))) | ||
| 91 | (when (memq :uppercase modifiers) | ||
| 92 | (setq text (upcase text))) | ||
| 93 | (when (memq :lowercase modifiers) | ||
| 94 | (setq text (downcase text))) | ||
| 65 | ;; Insert first, to preserve text properties. | 95 | ;; Insert first, to preserve text properties. |
| 66 | (insert-and-inherit text) | 96 | (insert-and-inherit text) |
| 67 | ;; Delete the specifier body. | 97 | ;; Delete the specifier body. |
| @@ -75,6 +105,34 @@ where they are, including \"%%\" strings." | |||
| 75 | (error "Invalid format string"))))) | 105 | (error "Invalid format string"))))) |
| 76 | (buffer-string))) | 106 | (buffer-string))) |
| 77 | 107 | ||
| 108 | (defun format-spec--pad (text total-length modifiers) | ||
| 109 | (if (> (length text) total-length) | ||
| 110 | ;; The text is longer than the specified length; do nothing. | ||
| 111 | text | ||
| 112 | (let ((padding (make-string (- total-length (length text)) | ||
| 113 | (if (memq :zero-pad modifiers) | ||
| 114 | ?0 | ||
| 115 | ?\s)))) | ||
| 116 | (if (memq :right-pad modifiers) | ||
| 117 | (concat text padding) | ||
| 118 | (concat padding text))))) | ||
| 119 | |||
| 120 | (defun format-spec--parse-modifiers (modifiers) | ||
| 121 | (let ((elems nil)) | ||
| 122 | (mapc (lambda (char) | ||
| 123 | (when-let ((modifier | ||
| 124 | (pcase char | ||
| 125 | (?0 :zero-pad) | ||
| 126 | (?\s :space-pad) | ||
| 127 | (?^ :uppercase) | ||
| 128 | (?_ :lowercase) | ||
| 129 | (?- :right-pad) | ||
| 130 | (?< :chop-left) | ||
| 131 | (?> :chop-right)))) | ||
| 132 | (push modifier elems))) | ||
| 133 | modifiers) | ||
| 134 | elems)) | ||
| 135 | |||
| 78 | (defun format-spec-make (&rest pairs) | 136 | (defun format-spec-make (&rest pairs) |
| 79 | "Return an alist suitable for use in `format-spec' based on PAIRS. | 137 | "Return an alist suitable for use in `format-spec' based on PAIRS. |
| 80 | PAIRS is a list where every other element is a character and a value, | 138 | 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 @@ | |||
| 37 | (should (equal (format-spec "foo %b %z %% zot" '((?b . "bar")) t) | 37 | (should (equal (format-spec "foo %b %z %% zot" '((?b . "bar")) t) |
| 38 | "foo bar %z %% zot"))) | 38 | "foo bar %z %% zot"))) |
| 39 | 39 | ||
| 40 | (ert-deftest test-format-modifiers () | ||
| 41 | (should (equal (format-spec "foo %10b zot" '((?b . "bar"))) | ||
| 42 | "foo bar zot")) | ||
| 43 | (should (equal (format-spec "foo % 10b zot" '((?b . "bar"))) | ||
| 44 | "foo bar zot")) | ||
| 45 | (should (equal (format-spec "foo %-010b zot" '((?b . "bar"))) | ||
| 46 | "foo bar0000000 zot")) | ||
| 47 | (should (equal (format-spec "foo %0-10b zot" '((?b . "bar"))) | ||
| 48 | "foo bar0000000 zot")) | ||
| 49 | (should (equal (format-spec "foo %^10b zot" '((?b . "bar"))) | ||
| 50 | "foo BAR zot")) | ||
| 51 | (should (equal (format-spec "foo %_10b zot" '((?b . "BAR"))) | ||
| 52 | "foo bar zot")) | ||
| 53 | (should (equal (format-spec "foo %<4b zot" '((?b . "longbar"))) | ||
| 54 | "foo gbar zot")) | ||
| 55 | (should (equal (format-spec "foo %>4b zot" '((?b . "longbar"))) | ||
| 56 | "foo long zot"))) | ||
| 57 | |||
| 40 | ;;; format-spec-tests.el ends here | 58 | ;;; format-spec-tests.el ends here |