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.
This commit is contained in:
parent
7ebcb4b6f2
commit
db745f37ae
4 changed files with 297 additions and 68 deletions
|
@ -694,6 +694,126 @@ Print the current user. This Eshell version of @command{whoami}
|
|||
supports Tramp.
|
||||
@end table
|
||||
|
||||
@subsection Defining new built-in commands
|
||||
While Eshell can run Lisp functions directly as commands, it may be
|
||||
more convenient to provide a special built-in command for
|
||||
Eshell. Built-in commands are just ordinary Lisp functions designed
|
||||
to be called from Eshell. When defining an Eshell-specific version of
|
||||
an existing function, you can give that function a name starting with
|
||||
@code{eshell/} so that Eshell knows to use it.
|
||||
|
||||
@defmac eshell-eval-using-options name macro-args options body@dots{}
|
||||
This macro processes a list of @var{macro-args} for the command
|
||||
@var{name} using a set of command line @var{options}. If the
|
||||
arguments are parsed successfully, it will store the resulting values
|
||||
in local symbols and execute @var{body}; any remaining arguments will
|
||||
be available in the locally let-bound variable @code{args}. The
|
||||
return value is the value of the last form in @var{body}.
|
||||
|
||||
If an unknown option was passed in @var{macro-args} and an external
|
||||
command was specified (see below), this macro will start a process for
|
||||
that command and throw the tag @code{eshell-external} with the new
|
||||
process as its value.
|
||||
|
||||
@var{options} should be a list beginning with one or more elements of
|
||||
the following form, with each element representing a particular
|
||||
command-line switch:
|
||||
|
||||
@example
|
||||
(@var{short} @var{long} @var{value} @var{symbol} @var{help-string})
|
||||
@end example
|
||||
|
||||
@table @var
|
||||
@item short
|
||||
This element, if non-nil, should be a character to be used as a short
|
||||
switch, like @code{-@var{short}}. At least one of this element and
|
||||
@var{long} must be non-nil.
|
||||
|
||||
@item long
|
||||
This element, if non-nil, should be a string to be used as a long
|
||||
switch, like @code{--@var{long}}.
|
||||
|
||||
@item value
|
||||
This element is the value associated with the option. It can be
|
||||
either:
|
||||
|
||||
@table @asis
|
||||
@item @code{t}
|
||||
The option needs a value to be specified after the switch.
|
||||
|
||||
@item @code{nil}
|
||||
The option is given the value @code{t}.
|
||||
|
||||
@item anything else
|
||||
The option is given the specified value.
|
||||
@end table
|
||||
|
||||
@item symbol
|
||||
This element is the Lisp symbol that will be bound to @var{value}. If
|
||||
@var{symbol} is @code{nil}, specifying this switch will instead call
|
||||
@code{eshell-show-usage}, and so is appropriate for an option like
|
||||
@code{--help}.
|
||||
|
||||
@item help-string
|
||||
This element is a documentation string for the option, which will be
|
||||
displayed when @code{eshell-show-usage} is invoked.
|
||||
@end table
|
||||
|
||||
After the list of command-line switch elements, @var{options} can
|
||||
include additional keyword arguments to control how
|
||||
@code{eshell-eval-using-options} behaves. Some of these take
|
||||
arguments, while others don't. The recognized keywords are:
|
||||
|
||||
@table @code
|
||||
@item :external @var{string}
|
||||
Specify @var{string} as an external command to run if there are
|
||||
unknown switches in @var{macro-args}.
|
||||
|
||||
@item :usage @var{string}
|
||||
Set @var{string} as the initial part of the command's documentation
|
||||
string. It appears before the options are listed.
|
||||
|
||||
@item :post-usage @var{string}
|
||||
Set @var{string} to be the (optional) trailing part of the command's
|
||||
documentation string. It appears after the list of options, but
|
||||
before the final part of the documentation about the associated
|
||||
external command, if there is one.
|
||||
|
||||
@item :show-usage
|
||||
If present, then show the usage message if the command is called with
|
||||
no arguments.
|
||||
|
||||
@item :preserve-args
|
||||
Normally, @code{eshell-eval-using-options} flattens the list of
|
||||
arguments in @var{macro-args} and converts each to a string. If this
|
||||
keyword is present, avoid doing that, instead preserving the original
|
||||
arguments. This is useful for commands which want to accept arbitrary
|
||||
Lisp objects.
|
||||
|
||||
@item :parse-leading-options-only
|
||||
If present, do not parse dash or switch arguments after the first
|
||||
positional argument. Instead, treat them as positional arguments
|
||||
themselves.
|
||||
@end table
|
||||
|
||||
For example, you could handle a subset of the options for the
|
||||
@code{ls} command like this:
|
||||
|
||||
@example
|
||||
(eshell-eval-using-options
|
||||
"ls" macro-args
|
||||
'((?a nil nil show-all "show all files")
|
||||
(?I "ignore" t ignore-pattern "ignore files matching pattern")
|
||||
(nil "help" nil nil "show this help message")
|
||||
:external "ls"
|
||||
:usage "[OPTION]... [FILE]...
|
||||
List information about FILEs (the current directory by default).")
|
||||
;; List the files in ARGS somehow...
|
||||
)
|
||||
@end example
|
||||
|
||||
@end defmac
|
||||
|
||||
@subsection Built-in variables
|
||||
Eshell knows a few built-in variables:
|
||||
|
||||
|
|
6
etc/NEWS
6
etc/NEWS
|
@ -1090,6 +1090,12 @@ dimensions.
|
|||
Specifying a cons as the from argument allows to start measuring text
|
||||
from a specified amount of pixels above or below a position.
|
||||
|
||||
---
|
||||
** 'eshell-eval-using-options' now follows POSIX/GNU argument syntax conventions.
|
||||
Built-in commands in Eshell now accept command-line options with
|
||||
values passed as a single token, such as '-oVALUE' or
|
||||
'--option=VALUE'.
|
||||
|
||||
** XDG support
|
||||
|
||||
*** New function 'xdg-state-home' returns 'XDG_STATE_HOME' environment variable.
|
||||
|
|
|
@ -187,49 +187,82 @@ passed to this command, the external version `%s'
|
|||
will be called instead." extcmd)))))
|
||||
(throw 'eshell-usage usage)))
|
||||
|
||||
(defun eshell--set-option (name ai opt options opt-vals)
|
||||
(defun eshell--split-switch (switch kind)
|
||||
"Split SWITCH into its option name and potential value, if any.
|
||||
KIND should be the integer 0 if SWITCH is a short option, or 1 if it's
|
||||
a long option."
|
||||
(if (eq kind 0)
|
||||
;; Short option
|
||||
(cons (aref switch 0)
|
||||
(and (> (length switch) 1) (substring switch 1)))
|
||||
;; Long option
|
||||
(save-match-data
|
||||
(string-match "\\([^=]*\\)\\(?:=\\(.*\\)\\)?" switch)
|
||||
(cons (match-string 1 switch) (match-string 2 switch)))))
|
||||
|
||||
(defun eshell--set-option (name ai opt value options opt-vals)
|
||||
"Using NAME's remaining args (index AI), set the OPT within OPTIONS.
|
||||
If the option consumes an argument for its value, the argument list
|
||||
will be modified."
|
||||
VALUE is the potential value of the OPT, coming from args like
|
||||
\"-fVALUE\" or \"--foo=VALUE\", or nil if no value was supplied. If
|
||||
OPT doesn't consume a value, return VALUE unchanged so that it can be
|
||||
processed later; otherwsie, return nil.
|
||||
|
||||
If the OPT consumes an argument for its value and VALUE is nil, the
|
||||
argument list will be modified."
|
||||
(if (not (nth 3 opt))
|
||||
(eshell-show-usage name options)
|
||||
(setcdr (assq (nth 3 opt) opt-vals)
|
||||
(if (eq (nth 2 opt) t)
|
||||
(if (> ai (length eshell--args))
|
||||
(error "%s: missing option argument" name)
|
||||
(pop (nthcdr ai eshell--args)))
|
||||
(or (nth 2 opt) t)))))
|
||||
(if (eq (nth 2 opt) t)
|
||||
(progn
|
||||
(setcdr (assq (nth 3 opt) opt-vals)
|
||||
(or value
|
||||
(if (> ai (length eshell--args))
|
||||
(error "%s: missing option argument" name)
|
||||
(pop (nthcdr ai eshell--args)))))
|
||||
nil)
|
||||
(setcdr (assq (nth 3 opt) opt-vals)
|
||||
(or (nth 2 opt) t))
|
||||
value)))
|
||||
|
||||
(defun eshell--process-option (name switch kind ai options opt-vals)
|
||||
"For NAME, process SWITCH (of type KIND), from args at index AI.
|
||||
The SWITCH will be looked up in the set of OPTIONS.
|
||||
|
||||
SWITCH should be either a string or character. KIND should be the
|
||||
integer 0 if it's a character, or 1 if it's a string.
|
||||
SWITCH should be a string starting with the option to process,
|
||||
possibly followed by its value, e.g. \"u\" or \"uUSER\". KIND should
|
||||
be the integer 0 if it's a short option, or 1 if it's a long option.
|
||||
|
||||
The SWITCH is then be matched against OPTIONS. If no matching handler
|
||||
is found, and an :external command is defined (and available), it will
|
||||
be called; otherwise, an error will be triggered to say that the
|
||||
switch is unrecognized."
|
||||
(let* ((opts options)
|
||||
found)
|
||||
The SWITCH is then be matched against OPTIONS. If KIND is 0 and the
|
||||
SWITCH matches an option that doesn't take a value, return the
|
||||
remaining characters in SWITCH to be processed later as further short
|
||||
options.
|
||||
|
||||
If no matching handler is found, and an :external command is defined
|
||||
(and available), it will be called; otherwise, an error will be
|
||||
triggered to say that the switch is unrecognized."
|
||||
(let ((switch (eshell--split-switch switch kind))
|
||||
(opts options)
|
||||
found remaining)
|
||||
(while opts
|
||||
(if (and (listp (car opts))
|
||||
(nth kind (car opts))
|
||||
(equal switch (nth kind (car opts))))
|
||||
(equal (car switch) (nth kind (car opts))))
|
||||
(progn
|
||||
(eshell--set-option name ai (car opts) options opt-vals)
|
||||
(setq remaining (eshell--set-option name ai (car opts)
|
||||
(cdr switch) options opt-vals))
|
||||
(when (and remaining (eq kind 1))
|
||||
(error "%s: option --%s doesn't allow an argument"
|
||||
name (car switch)))
|
||||
(setq found t opts nil))
|
||||
(setq opts (cdr opts))))
|
||||
(unless found
|
||||
(if found
|
||||
remaining
|
||||
(let ((extcmd (memq ':external options)))
|
||||
(when extcmd
|
||||
(setq extcmd (eshell-search-path (cadr extcmd)))
|
||||
(if extcmd
|
||||
(throw 'eshell-ext-command extcmd)
|
||||
(error (if (characterp switch) "%s: unrecognized option -%c"
|
||||
(error (if (characterp (car switch)) "%s: unrecognized option -%c"
|
||||
"%s: unrecognized option --%s")
|
||||
name switch)))))))
|
||||
name (car switch))))))))
|
||||
|
||||
(defun eshell--process-args (name args options)
|
||||
"Process the given ARGS using OPTIONS."
|
||||
|
@ -262,12 +295,9 @@ switch is unrecognized."
|
|||
(if (> (length switch) 0)
|
||||
(eshell--process-option name switch 1 ai options opt-vals)
|
||||
(setq ai (length eshell--args)))
|
||||
(let ((len (length switch))
|
||||
(index 0))
|
||||
(while (< index len)
|
||||
(eshell--process-option name (aref switch index)
|
||||
0 ai options opt-vals)
|
||||
(setq index (1+ index))))))))
|
||||
(while (> (length switch) 0)
|
||||
(setq switch (eshell--process-option name switch 0
|
||||
ai options opt-vals)))))))
|
||||
(nconc (mapcar #'cdr opt-vals) eshell--args)))
|
||||
|
||||
(provide 'esh-opt)
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
'((?u "user" t user "execute a command as another USER")
|
||||
:parse-leading-options-only))))
|
||||
(should
|
||||
(equal '("world" "emerge")
|
||||
(equal '("DN" "emerge" "world")
|
||||
(eshell--process-args
|
||||
"sudo"
|
||||
'("-u" "root" "emerge" "-uDN" "world")
|
||||
|
@ -65,59 +65,132 @@
|
|||
|
||||
(ert-deftest test-eshell-eval-using-options ()
|
||||
"Tests for `eshell-eval-using-options'."
|
||||
;; Test short options.
|
||||
(eshell-eval-using-options
|
||||
"ls" '("-a" "/some/path")
|
||||
'((?a "all" nil show-all
|
||||
"do not ignore entries starting with ."))
|
||||
(should (eq show-all t))
|
||||
(should (equal args '("/some/path"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/some/path")
|
||||
'((?a "all" nil show-all
|
||||
"do not ignore entries starting with ."))
|
||||
(should (eq show-all nil))
|
||||
(should (equal args '("/some/path"))))
|
||||
|
||||
;; Test long options.
|
||||
(eshell-eval-using-options
|
||||
"ls" '("--all" "/some/path")
|
||||
'((?a "all" nil show-all
|
||||
"do not ignore entries starting with ."))
|
||||
(should (eq show-all t))
|
||||
(should (equal args '("/some/path"))))
|
||||
|
||||
;; Test options with constant values.
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/some/path" "-h")
|
||||
'((?h "human-readable" 1024 human-readable
|
||||
"print sizes in human readable format"))
|
||||
(should (eql human-readable 1024))
|
||||
(should (equal args '("/some/path"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/some/path" "--human-readable")
|
||||
'((?h "human-readable" 1024 human-readable
|
||||
"print sizes in human readable format"))
|
||||
(should (eql human-readable 1024))
|
||||
(should (equal args '("/some/path"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/some/path")
|
||||
'((?h "human-readable" 1024 human-readable
|
||||
"print sizes in human readable format"))
|
||||
(should (eq human-readable nil))
|
||||
(should (equal args '("/some/path"))))
|
||||
|
||||
;; Test options with user-specified values.
|
||||
(eshell-eval-using-options
|
||||
"ls" '("-I" "*.txt" "/some/path")
|
||||
'((?I "ignore" t ignore-pattern
|
||||
"do not list implied entries matching pattern"))
|
||||
(should (equal ignore-pattern "*.txt"))
|
||||
(should (equal args '("/some/path"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("-I*.txt" "/some/path")
|
||||
'((?I "ignore" t ignore-pattern
|
||||
"do not list implied entries matching pattern"))
|
||||
(should (equal ignore-pattern "*.txt"))
|
||||
(should (equal args '("/some/path"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("--ignore" "*.txt" "/some/path")
|
||||
'((?I "ignore" t ignore-pattern
|
||||
"do not list implied entries matching pattern"))
|
||||
(should (equal ignore-pattern "*.txt"))
|
||||
(should (equal args '("/some/path"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("--ignore=*.txt" "/some/path")
|
||||
'((?I "ignore" t ignore-pattern
|
||||
"do not list implied entries matching pattern"))
|
||||
(should (equal ignore-pattern "*.txt"))
|
||||
(should (equal args '("/some/path"))))
|
||||
|
||||
;; Test multiple short options in a single token.
|
||||
(eshell-eval-using-options
|
||||
"ls" '("-al" "/some/path")
|
||||
'((?a "all" nil show-all
|
||||
"do not ignore entries starting with .")
|
||||
(?l nil long-listing listing-style
|
||||
"use a long listing format"))
|
||||
(should (eq t show-all))
|
||||
(should (eql listing-style 'long-listing))
|
||||
(should (equal args '("/some/path"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("-aI*.txt" "/some/path")
|
||||
'((?a "all" nil show-all
|
||||
"do not ignore entries starting with .")
|
||||
(?I "ignore" t ignore-pattern
|
||||
"do not list implied entries matching pattern"))
|
||||
(should (eq t show-all))
|
||||
(should (equal ignore-pattern "*.txt"))
|
||||
(should (equal args '("/some/path"))))
|
||||
|
||||
;; Test that "--" terminates options.
|
||||
(eshell-eval-using-options
|
||||
"ls" '("--" "-a")
|
||||
'((?a "all" nil show-all
|
||||
"do not ignore entries starting with ."))
|
||||
(should (eq show-all nil))
|
||||
(should (equal args '("-a"))))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("--" "--all")
|
||||
'((?a "all" nil show-all
|
||||
"do not ignore entries starting with ."))
|
||||
(should (eq show-all nil))
|
||||
(should (equal args '("--all"))))
|
||||
|
||||
;; Test :parse-leading-options-only.
|
||||
(eshell-eval-using-options
|
||||
"sudo" '("-u" "root" "whoami")
|
||||
'((?u "user" t user "execute a command as another USER")
|
||||
:parse-leading-options-only)
|
||||
(should (equal user "root")))
|
||||
(should (equal user "root"))
|
||||
(should (equal args '("whoami"))))
|
||||
(eshell-eval-using-options
|
||||
"sudo" '("--user" "root" "whoami")
|
||||
'((?u "user" t user "execute a command as another USER")
|
||||
:parse-leading-options-only)
|
||||
(should (equal user "root")))
|
||||
|
||||
(should (equal user "root"))
|
||||
(should (equal args '("whoami"))))
|
||||
(eshell-eval-using-options
|
||||
"sudo" '("emerge" "-uDN" "world")
|
||||
'((?u "user" t user "execute a command as another USER"))
|
||||
(should (equal user "world")))
|
||||
(should (equal user "DN"))
|
||||
(should (equal args '("emerge" "world"))))
|
||||
(eshell-eval-using-options
|
||||
"sudo" '("emerge" "-uDN" "world")
|
||||
'((?u "user" t user "execute a command as another USER")
|
||||
:parse-leading-options-only)
|
||||
(should (eq user nil)))
|
||||
|
||||
(eshell-eval-using-options
|
||||
"ls" '("-I" "*.txt" "/dev/null")
|
||||
'((?I "ignore" t ignore-pattern
|
||||
"do not list implied entries matching pattern"))
|
||||
(should (equal ignore-pattern "*.txt")))
|
||||
|
||||
(eshell-eval-using-options
|
||||
"ls" '("-l" "/dev/null")
|
||||
'((?l nil long-listing listing-style
|
||||
"use a long listing format"))
|
||||
(should (eql listing-style 'long-listing)))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/dev/null")
|
||||
'((?l nil long-listing listing-style
|
||||
"use a long listing format"))
|
||||
(should (eq listing-style nil)))
|
||||
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/dev/null" "-h")
|
||||
'((?h "human-readable" 1024 human-readable
|
||||
"print sizes in human readable format"))
|
||||
(should (eql human-readable 1024)))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/dev/null" "--human-readable")
|
||||
'((?h "human-readable" 1024 human-readable
|
||||
"print sizes in human readable format"))
|
||||
(should (eql human-readable 1024)))
|
||||
(eshell-eval-using-options
|
||||
"ls" '("/dev/null")
|
||||
'((?h "human-readable" 1024 human-readable
|
||||
"print sizes in human readable format"))
|
||||
(should (eq human-readable nil))))
|
||||
(should (eq user nil))
|
||||
(should (equal args '("emerge" "-uDN" "world")))))
|
||||
|
||||
(provide 'esh-opt-tests)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue