aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJim Porter2022-01-04 12:58:38 -0800
committerEli Zaretskii2022-01-12 16:58:37 +0200
commitdb745f37aec2adc44ec4b2eae0720e0365ed0ca9 (patch)
tree1c2b405ad2bc81d34fd2ae140c18855d78460665
parent7ebcb4b6f2f4531ebc893bb3b2f74d6298bf9b41 (diff)
downloademacs-db745f37aec2adc44ec4b2eae0720e0365ed0ca9.tar.gz
emacs-db745f37aec2adc44ec4b2eae0720e0365ed0ca9.zip
Follow POSIX/GNU argument conventions for 'eshell-eval-using-options'
* lisp/eshell/esh-opt.el (eshell--split-switch): New function. (eshell-set-option): Allow setting a supplied value instead of always consuming from 'eshell--args'. (eshell--process-option): Support consuming option values specified as a single token. (eshell--process-args): For short options, pass full switch token to 'eshell--process-option'. * test/lisp/eshell/esh-opt-tests.el (esh-opt-process-args-test): Fix test. (test-eshell-eval-using-options): Add tests for various types of options. * doc/misc/eshell.texi (Defining new built-in commands): New subsection, describe how to use 'eshell-eval-using-options'. * etc/NEWS: Announce the change.
-rw-r--r--doc/misc/eshell.texi120
-rw-r--r--etc/NEWS6
-rw-r--r--lisp/eshell/esh-opt.el90
-rw-r--r--test/lisp/eshell/esh-opt-tests.el151
4 files changed, 298 insertions, 69 deletions
diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 83d324c7e1b..f1d7c638056 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -694,6 +694,126 @@ Print the current user. This Eshell version of @command{whoami}
694supports Tramp. 694supports Tramp.
695@end table 695@end table
696 696
697@subsection Defining new built-in commands
698While Eshell can run Lisp functions directly as commands, it may be
699more convenient to provide a special built-in command for
700Eshell. Built-in commands are just ordinary Lisp functions designed
701to be called from Eshell. When defining an Eshell-specific version of
702an existing function, you can give that function a name starting with
703@code{eshell/} so that Eshell knows to use it.
704
705@defmac eshell-eval-using-options name macro-args options body@dots{}
706This macro processes a list of @var{macro-args} for the command
707@var{name} using a set of command line @var{options}. If the
708arguments are parsed successfully, it will store the resulting values
709in local symbols and execute @var{body}; any remaining arguments will
710be available in the locally let-bound variable @code{args}. The
711return value is the value of the last form in @var{body}.
712
713If an unknown option was passed in @var{macro-args} and an external
714command was specified (see below), this macro will start a process for
715that command and throw the tag @code{eshell-external} with the new
716process as its value.
717
718@var{options} should be a list beginning with one or more elements of
719the following form, with each element representing a particular
720command-line switch:
721
722@example
723(@var{short} @var{long} @var{value} @var{symbol} @var{help-string})
724@end example
725
726@table @var
727@item short
728This element, if non-nil, should be a character to be used as a short
729switch, like @code{-@var{short}}. At least one of this element and
730@var{long} must be non-nil.
731
732@item long
733This element, if non-nil, should be a string to be used as a long
734switch, like @code{--@var{long}}.
735
736@item value
737This element is the value associated with the option. It can be
738either:
739
740@table @asis
741@item @code{t}
742The option needs a value to be specified after the switch.
743
744@item @code{nil}
745The option is given the value @code{t}.
746
747@item anything else
748The option is given the specified value.
749@end table
750
751@item symbol
752This element is the Lisp symbol that will be bound to @var{value}. If
753@var{symbol} is @code{nil}, specifying this switch will instead call
754@code{eshell-show-usage}, and so is appropriate for an option like
755@code{--help}.
756
757@item help-string
758This element is a documentation string for the option, which will be
759displayed when @code{eshell-show-usage} is invoked.
760@end table
761
762After the list of command-line switch elements, @var{options} can
763include additional keyword arguments to control how
764@code{eshell-eval-using-options} behaves. Some of these take
765arguments, while others don't. The recognized keywords are:
766
767@table @code
768@item :external @var{string}
769Specify @var{string} as an external command to run if there are
770unknown switches in @var{macro-args}.
771
772@item :usage @var{string}
773Set @var{string} as the initial part of the command's documentation
774string. It appears before the options are listed.
775
776@item :post-usage @var{string}
777Set @var{string} to be the (optional) trailing part of the command's
778documentation string. It appears after the list of options, but
779before the final part of the documentation about the associated
780external command, if there is one.
781
782@item :show-usage
783If present, then show the usage message if the command is called with
784no arguments.
785
786@item :preserve-args
787Normally, @code{eshell-eval-using-options} flattens the list of
788arguments in @var{macro-args} and converts each to a string. If this
789keyword is present, avoid doing that, instead preserving the original
790arguments. This is useful for commands which want to accept arbitrary
791Lisp objects.
792
793@item :parse-leading-options-only
794If present, do not parse dash or switch arguments after the first
795positional argument. Instead, treat them as positional arguments
796themselves.
797@end table
798
799For example, you could handle a subset of the options for the
800@code{ls} command like this:
801
802@example
803(eshell-eval-using-options
804 "ls" macro-args
805 '((?a nil nil show-all "show all files")
806 (?I "ignore" t ignore-pattern "ignore files matching pattern")
807 (nil "help" nil nil "show this help message")
808 :external "ls"
809 :usage "[OPTION]... [FILE]...
810 List information about FILEs (the current directory by default).")
811 ;; List the files in ARGS somehow...
812 )
813@end example
814
815@end defmac
816
697@subsection Built-in variables 817@subsection Built-in variables
698Eshell knows a few built-in variables: 818Eshell knows a few built-in variables:
699 819
diff --git a/etc/NEWS b/etc/NEWS
index d7281467c68..9ad8354d119 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1090,6 +1090,12 @@ dimensions.
1090Specifying a cons as the from argument allows to start measuring text 1090Specifying a cons as the from argument allows to start measuring text
1091from a specified amount of pixels above or below a position. 1091from a specified amount of pixels above or below a position.
1092 1092
1093---
1094** 'eshell-eval-using-options' now follows POSIX/GNU argument syntax conventions.
1095Built-in commands in Eshell now accept command-line options with
1096values passed as a single token, such as '-oVALUE' or
1097'--option=VALUE'.
1098
1093** XDG support 1099** XDG support
1094 1100
1095*** New function 'xdg-state-home' returns 'XDG_STATE_HOME' environment variable. 1101*** New function 'xdg-state-home' returns 'XDG_STATE_HOME' environment variable.
diff --git a/lisp/eshell/esh-opt.el b/lisp/eshell/esh-opt.el
index d96b77ddd37..bba1c4ad25d 100644
--- a/lisp/eshell/esh-opt.el
+++ b/lisp/eshell/esh-opt.el
@@ -187,49 +187,82 @@ passed to this command, the external version `%s'
187will be called instead." extcmd))))) 187will be called instead." extcmd)))))
188 (throw 'eshell-usage usage))) 188 (throw 'eshell-usage usage)))
189 189
190(defun eshell--set-option (name ai opt options opt-vals) 190(defun eshell--split-switch (switch kind)
191 "Split SWITCH into its option name and potential value, if any.
192KIND should be the integer 0 if SWITCH is a short option, or 1 if it's
193a long option."
194 (if (eq kind 0)
195 ;; Short option
196 (cons (aref switch 0)
197 (and (> (length switch) 1) (substring switch 1)))
198 ;; Long option
199 (save-match-data
200 (string-match "\\([^=]*\\)\\(?:=\\(.*\\)\\)?" switch)
201 (cons (match-string 1 switch) (match-string 2 switch)))))
202
203(defun eshell--set-option (name ai opt value options opt-vals)
191 "Using NAME's remaining args (index AI), set the OPT within OPTIONS. 204 "Using NAME's remaining args (index AI), set the OPT within OPTIONS.
192If the option consumes an argument for its value, the argument list 205VALUE is the potential value of the OPT, coming from args like
193will be modified." 206\"-fVALUE\" or \"--foo=VALUE\", or nil if no value was supplied. If
207OPT doesn't consume a value, return VALUE unchanged so that it can be
208processed later; otherwsie, return nil.
209
210If the OPT consumes an argument for its value and VALUE is nil, the
211argument list will be modified."
194 (if (not (nth 3 opt)) 212 (if (not (nth 3 opt))
195 (eshell-show-usage name options) 213 (eshell-show-usage name options)
196 (setcdr (assq (nth 3 opt) opt-vals) 214 (if (eq (nth 2 opt) t)
197 (if (eq (nth 2 opt) t) 215 (progn
198 (if (> ai (length eshell--args)) 216 (setcdr (assq (nth 3 opt) opt-vals)
199 (error "%s: missing option argument" name) 217 (or value
200 (pop (nthcdr ai eshell--args))) 218 (if (> ai (length eshell--args))
201 (or (nth 2 opt) t))))) 219 (error "%s: missing option argument" name)
220 (pop (nthcdr ai eshell--args)))))
221 nil)
222 (setcdr (assq (nth 3 opt) opt-vals)
223 (or (nth 2 opt) t))
224 value)))
202 225
203(defun eshell--process-option (name switch kind ai options opt-vals) 226(defun eshell--process-option (name switch kind ai options opt-vals)
204 "For NAME, process SWITCH (of type KIND), from args at index AI. 227 "For NAME, process SWITCH (of type KIND), from args at index AI.
205The SWITCH will be looked up in the set of OPTIONS. 228The SWITCH will be looked up in the set of OPTIONS.
206 229
207SWITCH should be either a string or character. KIND should be the 230SWITCH should be a string starting with the option to process,
208integer 0 if it's a character, or 1 if it's a string. 231possibly followed by its value, e.g. \"u\" or \"uUSER\". KIND should
209 232be the integer 0 if it's a short option, or 1 if it's a long option.
210The SWITCH is then be matched against OPTIONS. If no matching handler 233
211is found, and an :external command is defined (and available), it will 234The SWITCH is then be matched against OPTIONS. If KIND is 0 and the
212be called; otherwise, an error will be triggered to say that the 235SWITCH matches an option that doesn't take a value, return the
213switch is unrecognized." 236remaining characters in SWITCH to be processed later as further short
214 (let* ((opts options) 237options.
215 found) 238
239If no matching handler is found, and an :external command is defined
240(and available), it will be called; otherwise, an error will be
241triggered to say that the switch is unrecognized."
242 (let ((switch (eshell--split-switch switch kind))
243 (opts options)
244 found remaining)
216 (while opts 245 (while opts
217 (if (and (listp (car opts)) 246 (if (and (listp (car opts))
218 (nth kind (car opts)) 247 (equal (car switch) (nth kind (car opts))))
219 (equal switch (nth kind (car opts))))
220 (progn 248 (progn
221 (eshell--set-option name ai (car opts) options opt-vals) 249 (setq remaining (eshell--set-option name ai (car opts)
250 (cdr switch) options opt-vals))
251 (when (and remaining (eq kind 1))
252 (error "%s: option --%s doesn't allow an argument"
253 name (car switch)))
222 (setq found t opts nil)) 254 (setq found t opts nil))
223 (setq opts (cdr opts)))) 255 (setq opts (cdr opts))))
224 (unless found 256 (if found
257 remaining
225 (let ((extcmd (memq ':external options))) 258 (let ((extcmd (memq ':external options)))
226 (when extcmd 259 (when extcmd
227 (setq extcmd (eshell-search-path (cadr extcmd))) 260 (setq extcmd (eshell-search-path (cadr extcmd)))
228 (if extcmd 261 (if extcmd
229 (throw 'eshell-ext-command extcmd) 262 (throw 'eshell-ext-command extcmd)
230 (error (if (characterp switch) "%s: unrecognized option -%c" 263 (error (if (characterp (car switch)) "%s: unrecognized option -%c"
231 "%s: unrecognized option --%s") 264 "%s: unrecognized option --%s")
232 name switch))))))) 265 name (car switch))))))))
233 266
234(defun eshell--process-args (name args options) 267(defun eshell--process-args (name args options)
235 "Process the given ARGS using OPTIONS." 268 "Process the given ARGS using OPTIONS."
@@ -262,12 +295,9 @@ switch is unrecognized."
262 (if (> (length switch) 0) 295 (if (> (length switch) 0)
263 (eshell--process-option name switch 1 ai options opt-vals) 296 (eshell--process-option name switch 1 ai options opt-vals)
264 (setq ai (length eshell--args))) 297 (setq ai (length eshell--args)))
265 (let ((len (length switch)) 298 (while (> (length switch) 0)
266 (index 0)) 299 (setq switch (eshell--process-option name switch 0
267 (while (< index len) 300 ai options opt-vals)))))))
268 (eshell--process-option name (aref switch index)
269 0 ai options opt-vals)
270 (setq index (1+ index))))))))
271 (nconc (mapcar #'cdr opt-vals) eshell--args))) 301 (nconc (mapcar #'cdr opt-vals) eshell--args)))
272 302
273(provide 'esh-opt) 303(provide 'esh-opt)
diff --git a/test/lisp/eshell/esh-opt-tests.el b/test/lisp/eshell/esh-opt-tests.el
index 532adfb733a..255768635b1 100644
--- a/test/lisp/eshell/esh-opt-tests.el
+++ b/test/lisp/eshell/esh-opt-tests.el
@@ -57,7 +57,7 @@
57 '((?u "user" t user "execute a command as another USER") 57 '((?u "user" t user "execute a command as another USER")
58 :parse-leading-options-only)))) 58 :parse-leading-options-only))))
59 (should 59 (should
60 (equal '("world" "emerge") 60 (equal '("DN" "emerge" "world")
61 (eshell--process-args 61 (eshell--process-args
62 "sudo" 62 "sudo"
63 '("-u" "root" "emerge" "-uDN" "world") 63 '("-u" "root" "emerge" "-uDN" "world")
@@ -65,59 +65,132 @@
65 65
66(ert-deftest test-eshell-eval-using-options () 66(ert-deftest test-eshell-eval-using-options ()
67 "Tests for `eshell-eval-using-options'." 67 "Tests for `eshell-eval-using-options'."
68 ;; Test short options.
68 (eshell-eval-using-options 69 (eshell-eval-using-options
69 "sudo" '("-u" "root" "whoami") 70 "ls" '("-a" "/some/path")
70 '((?u "user" t user "execute a command as another USER") 71 '((?a "all" nil show-all
71 :parse-leading-options-only) 72 "do not ignore entries starting with ."))
72 (should (equal user "root"))) 73 (should (eq show-all t))
74 (should (equal args '("/some/path"))))
73 (eshell-eval-using-options 75 (eshell-eval-using-options
74 "sudo" '("--user" "root" "whoami") 76 "ls" '("/some/path")
75 '((?u "user" t user "execute a command as another USER") 77 '((?a "all" nil show-all
76 :parse-leading-options-only) 78 "do not ignore entries starting with ."))
77 (should (equal user "root"))) 79 (should (eq show-all nil))
80 (should (equal args '("/some/path"))))
78 81
82 ;; Test long options.
79 (eshell-eval-using-options 83 (eshell-eval-using-options
80 "sudo" '("emerge" "-uDN" "world") 84 "ls" '("--all" "/some/path")
81 '((?u "user" t user "execute a command as another USER")) 85 '((?a "all" nil show-all
82 (should (equal user "world"))) 86 "do not ignore entries starting with ."))
87 (should (eq show-all t))
88 (should (equal args '("/some/path"))))
89
90 ;; Test options with constant values.
83 (eshell-eval-using-options 91 (eshell-eval-using-options
84 "sudo" '("emerge" "-uDN" "world") 92 "ls" '("/some/path" "-h")
85 '((?u "user" t user "execute a command as another USER") 93 '((?h "human-readable" 1024 human-readable
86 :parse-leading-options-only) 94 "print sizes in human readable format"))
87 (should (eq user nil))) 95 (should (eql human-readable 1024))
96 (should (equal args '("/some/path"))))
97 (eshell-eval-using-options
98 "ls" '("/some/path" "--human-readable")
99 '((?h "human-readable" 1024 human-readable
100 "print sizes in human readable format"))
101 (should (eql human-readable 1024))
102 (should (equal args '("/some/path"))))
103 (eshell-eval-using-options
104 "ls" '("/some/path")
105 '((?h "human-readable" 1024 human-readable
106 "print sizes in human readable format"))
107 (should (eq human-readable nil))
108 (should (equal args '("/some/path"))))
88 109
110 ;; Test options with user-specified values.
111 (eshell-eval-using-options
112 "ls" '("-I" "*.txt" "/some/path")
113 '((?I "ignore" t ignore-pattern
114 "do not list implied entries matching pattern"))
115 (should (equal ignore-pattern "*.txt"))
116 (should (equal args '("/some/path"))))
117 (eshell-eval-using-options
118 "ls" '("-I*.txt" "/some/path")
119 '((?I "ignore" t ignore-pattern
120 "do not list implied entries matching pattern"))
121 (should (equal ignore-pattern "*.txt"))
122 (should (equal args '("/some/path"))))
89 (eshell-eval-using-options 123 (eshell-eval-using-options
90 "ls" '("-I" "*.txt" "/dev/null") 124 "ls" '("--ignore" "*.txt" "/some/path")
91 '((?I "ignore" t ignore-pattern 125 '((?I "ignore" t ignore-pattern
92 "do not list implied entries matching pattern")) 126 "do not list implied entries matching pattern"))
93 (should (equal ignore-pattern "*.txt"))) 127 (should (equal ignore-pattern "*.txt"))
128 (should (equal args '("/some/path"))))
129 (eshell-eval-using-options
130 "ls" '("--ignore=*.txt" "/some/path")
131 '((?I "ignore" t ignore-pattern
132 "do not list implied entries matching pattern"))
133 (should (equal ignore-pattern "*.txt"))
134 (should (equal args '("/some/path"))))
94 135
136 ;; Test multiple short options in a single token.
95 (eshell-eval-using-options 137 (eshell-eval-using-options
96 "ls" '("-l" "/dev/null") 138 "ls" '("-al" "/some/path")
97 '((?l nil long-listing listing-style 139 '((?a "all" nil show-all
98 "use a long listing format")) 140 "do not ignore entries starting with .")
99 (should (eql listing-style 'long-listing))) 141 (?l nil long-listing listing-style
142 "use a long listing format"))
143 (should (eq t show-all))
144 (should (eql listing-style 'long-listing))
145 (should (equal args '("/some/path"))))
100 (eshell-eval-using-options 146 (eshell-eval-using-options
101 "ls" '("/dev/null") 147 "ls" '("-aI*.txt" "/some/path")
102 '((?l nil long-listing listing-style 148 '((?a "all" nil show-all
103 "use a long listing format")) 149 "do not ignore entries starting with .")
104 (should (eq listing-style nil))) 150 (?I "ignore" t ignore-pattern
151 "do not list implied entries matching pattern"))
152 (should (eq t show-all))
153 (should (equal ignore-pattern "*.txt"))
154 (should (equal args '("/some/path"))))
105 155
156 ;; Test that "--" terminates options.
106 (eshell-eval-using-options 157 (eshell-eval-using-options
107 "ls" '("/dev/null" "-h") 158 "ls" '("--" "-a")
108 '((?h "human-readable" 1024 human-readable 159 '((?a "all" nil show-all
109 "print sizes in human readable format")) 160 "do not ignore entries starting with ."))
110 (should (eql human-readable 1024))) 161 (should (eq show-all nil))
162 (should (equal args '("-a"))))
111 (eshell-eval-using-options 163 (eshell-eval-using-options
112 "ls" '("/dev/null" "--human-readable") 164 "ls" '("--" "--all")
113 '((?h "human-readable" 1024 human-readable 165 '((?a "all" nil show-all
114 "print sizes in human readable format")) 166 "do not ignore entries starting with ."))
115 (should (eql human-readable 1024))) 167 (should (eq show-all nil))
168 (should (equal args '("--all"))))
169
170 ;; Test :parse-leading-options-only.
116 (eshell-eval-using-options 171 (eshell-eval-using-options
117 "ls" '("/dev/null") 172 "sudo" '("-u" "root" "whoami")
118 '((?h "human-readable" 1024 human-readable 173 '((?u "user" t user "execute a command as another USER")
119 "print sizes in human readable format")) 174 :parse-leading-options-only)
120 (should (eq human-readable nil)))) 175 (should (equal user "root"))
176 (should (equal args '("whoami"))))
177 (eshell-eval-using-options
178 "sudo" '("--user" "root" "whoami")
179 '((?u "user" t user "execute a command as another USER")
180 :parse-leading-options-only)
181 (should (equal user "root"))
182 (should (equal args '("whoami"))))
183 (eshell-eval-using-options
184 "sudo" '("emerge" "-uDN" "world")
185 '((?u "user" t user "execute a command as another USER"))
186 (should (equal user "DN"))
187 (should (equal args '("emerge" "world"))))
188 (eshell-eval-using-options
189 "sudo" '("emerge" "-uDN" "world")
190 '((?u "user" t user "execute a command as another USER")
191 :parse-leading-options-only)
192 (should (eq user nil))
193 (should (equal args '("emerge" "-uDN" "world")))))
121 194
122(provide 'esh-opt-tests) 195(provide 'esh-opt-tests)
123 196