1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2026-01-30 12:21:25 -08:00

Allow setting the values of variable aliases in Eshell

This makes commands like "COLUMNS=40 some-command" work as expected.

* lisp/eshell/esh-cmd.el (eshell-subcommand-bindings): Remove
'process-environment' from here...

* lisp/eshell/esh-var.el (eshell-var-initialize): ... and add to here,
along with 'eshell-variable-aliases-list'.
(eshell-inside-emacs): Convert to a 'defvar-local' to make it settable
in a particular Eshell buffer.
(eshell-variable-aliases-list): Make $?, $$, and $* read-only and
update docstring.
(eshell-set-variable): New function...
(eshell-handle-local-variables, eshell/export, eshell/unset): ... use
it.
(eshell/set, pcomplete/eshell-mode/set): New functions.
(eshell-get-variable): Get the variable alias's getter function when
appropriate and use a safer method for checking function arity.

* test/lisp/eshell/esh-var-tests.el (esh-var-test/set/env-var)
(esh-var-test/set/symbol, esh-var-test/unset/env-var)
(esh-var-test/unset/symbol, esh-var-test/setq, esh-var-test/export)
(esh-var-test/local-variables, esh-var-test/alias/function)
(esh-var-test/alias/function-pair, esh-var-test/alias/string)
(esh-var-test/alias/string/prefer-lisp, esh-var-test/alias/symbol)
(esh-var-test/alias/symbol-pair, esh-var-test/alias/export)
(esh-var-test/alias/local-variables): New tests.

* doc/misc/eshell.texi (Built-ins): Add 'set' and update 'unset'
documentation.
(Variables): Expand documentation of how to get/set variables.
This commit is contained in:
Jim Porter 2022-09-25 21:47:26 -07:00
parent f1caa10f04
commit 7c41016fca
4 changed files with 293 additions and 46 deletions

View file

@ -694,10 +694,18 @@ used for comparing lists of strings.
This command can be loaded as part of the eshell-xtra module, which is
disabled by default.
@item set
@cmindex set
Set variable values, using the function @code{set} like a command
(@pxref{Setting Variables,,, elisp, GNU Emacs Lisp Reference Manual}).
A variable name can be a symbol, in which case it refers to a Lisp
variable, or a string, referring to an environment variable
(@pxref{Arguments}).
@item setq
@cmindex setq
Set variable values, using the function @code{setq} like a command.
@xref{Setting Variables,,, elisp, GNU Emacs Lisp Reference Manual}.
Set variable values, using the function @code{setq} like a command
(@pxref{Setting Variables,,, elisp, GNU Emacs Lisp Reference Manual}).
@item source
@cmindex source
@ -743,7 +751,9 @@ disabled by default.
@item unset
@cmindex unset
Unset an environment variable.
Unset one or more variables. As with @command{set}, a variable name
can be a symbol, in which case it refers to a Lisp variable, or a
string, referring to an environment variable.
@item wait
@cmindex wait
@ -881,12 +891,35 @@ For example, you could handle a subset of the options for the
@node Variables
@section Variables
Since Eshell is just an Emacs @acronym{REPL}@footnote{
@vindex eshell-prefer-lisp-variables
Since Eshell is a combination of an Emacs @acronym{REPL}@footnote{
Short for ``Read-Eval-Print Loop''.
}
, it does not have its own scope, and simply stores variables the same
you would in an Elisp program. Eshell provides a command version of
@code{setq} for convenience.
} and a command shell, it can refer to variables from two different
sources: ordinary Emacs Lisp variables, as well as environment
variables. By default, when using a variable in Eshell, it will first
look in the list of built-in variables, then in the list of
environment variables, and finally in the list of Lisp variables. If
you would prefer to use Lisp variables over environment variables, you
can set @code{eshell-prefer-lisp-variables} to @code{t}.
You can set variables in a few different ways. To set a Lisp
variable, you can use the command @samp{setq @var{name} @var{value}},
which works much like its Lisp counterpart (@pxref{Setting Variables,
, , elisp, The Emacs Lisp Reference Manual}). To set an environment
variable, use @samp{export @var{name}=@var{value}}. You can also use
@samp{set @var{variable} @var{value}}, which sets a Lisp variable if
@var{variable} is a symbol, or an environment variable if it's a
string (@pxref{Arguments}). Finally, you can temporarily set
environment variables for a single command with
@samp{@var{name}=@var{value} @var{command} @dots{}}. This is
equivalent to:
@example
@{
export @var{name}=@var{value}
@var{command} @dots{}
@}
@end example
@subsection Built-in variables
Eshell knows a few built-in variables:

View file

@ -261,9 +261,9 @@ the command."
(defcustom eshell-subcommand-bindings
'((eshell-in-subcommand-p t)
(eshell-in-pipeline-p nil)
(default-directory default-directory)
(process-environment (eshell-copy-environment)))
(default-directory default-directory))
"A list of `let' bindings for subcommand environments."
:version "29.1" ; removed `process-environment'
:type 'sexp
:risky t)

View file

@ -113,7 +113,7 @@
(require 'pcomplete)
(require 'ring)
(defconst eshell-inside-emacs (format "%s,eshell" emacs-version)
(defvar-local eshell-inside-emacs (format "%s,eshell" emacs-version)
"Value for the `INSIDE_EMACS' environment variable.")
(defgroup eshell-var nil
@ -162,8 +162,8 @@ if they are quoted with a backslash."
(car (last eshell-last-arguments))
(eshell-apply-indices eshell-last-arguments
indices quoted))))
("?" eshell-last-command-status)
("$" eshell-last-command-result)
("?" (eshell-last-command-status . nil))
("$" (eshell-last-command-result . nil))
;; for em-alias.el and em-script.el
("0" eshell-command-name)
@ -176,7 +176,7 @@ if they are quoted with a backslash."
("7" ,(lambda () (nth 6 eshell-command-arguments)) nil t)
("8" ,(lambda () (nth 7 eshell-command-arguments)) nil t)
("9" ,(lambda () (nth 8 eshell-command-arguments)) nil t)
("*" eshell-command-arguments))
("*" (eshell-command-arguments . nil)))
"This list provides aliasing for variable references.
Each member is of the following form:
@ -186,6 +186,11 @@ NAME defines the name of the variable, VALUE is a Lisp value used to
compute the string value that will be returned when the variable is
accessed via the syntax `$NAME'.
If VALUE is a cons (GET . SET), then variable references to NAME
will use GET to get the value, and SET to set it. GET and SET
can be one of the forms described below. If SET is nil, the
variable is read-only.
If VALUE is a function, its behavior depends on the value of
SIMPLE-FUNCTION. If SIMPLE-FUNCTION is nil, call VALUE with two
arguments: the list of the indices that were used in the reference,
@ -193,23 +198,30 @@ and either t or nil depending on whether or not the variable was
quoted with double quotes. For example, if `NAME' were aliased
to a function, a reference of `$NAME[10][20]' would result in that
function being called with the arguments `((\"10\") (\"20\"))' and
nil.
If SIMPLE-FUNCTION is non-nil, call the function with no arguments
and then pass its return value to `eshell-apply-indices'.
nil. If SIMPLE-FUNCTION is non-nil, call the function with no
arguments and then pass its return value to `eshell-apply-indices'.
If VALUE is a string, return the value for the variable with that
name in the current environment. If no variable with that name exists
in the environment, but if a symbol with that same name exists and has
a value bound to it, return that symbol's value instead. You can
prefer symbol values over environment values by setting the value
of `eshell-prefer-lisp-variables' to t.
When VALUE is a function, it's read-only by default. To make it
writeable, use the (GET . SET) form described above. If SET is a
function, it takes two arguments: a list of indices (currently
always nil, but reserved for future enhancement), and the new
value to set.
If VALUE is a symbol, return the value bound to it.
If VALUE is a string, get/set the value for the variable with
that name in the current environment. When getting the value, if
no variable with that name exists in the environment, but if a
symbol with that same name exists and has a value bound to it,
return that symbol's value instead. You can prefer symbol values
over environment values by setting the value of
`eshell-prefer-lisp-variables' to t.
If VALUE is a symbol, get/set the value bound to it.
If VALUE has any other type, signal an error.
Additionally, if COPY-TO-ENVIRONMENT is non-nil, the alias should be
copied (a.k.a. \"exported\") to the environment of created subprocesses."
:version "29.1"
:type '(repeat (list string sexp
(choice (const :tag "Copy to environment" t)
(const :tag "Use only in Eshell" nil))
@ -234,6 +246,11 @@ copied (a.k.a. \"exported\") to the environment of created subprocesses."
;; changing a variable will affect all of Emacs.
(unless eshell-modify-global-environment
(setq-local process-environment (eshell-copy-environment)))
(setq-local eshell-subcommand-bindings
(append
'((process-environment (eshell-copy-environment))
(eshell-variable-aliases-list eshell-variable-aliases-list))
eshell-subcommand-bindings))
(setq-local eshell-special-chars-inside-quoting
(append eshell-special-chars-inside-quoting '(?$)))
@ -282,9 +299,9 @@ copied (a.k.a. \"exported\") to the environment of created subprocesses."
(while (string-match setvar command)
(nconc
l (list
(list 'setenv (match-string 1 command)
(match-string 2 command)
(= (length (match-string 2 command)) 0))))
(list 'eshell-set-variable
(match-string 1 command)
(match-string 2 command))))
(setq command (eshell-stringify (car args))
args (cdr args)))
(cdr l))
@ -328,12 +345,11 @@ This function is explicit for adding to `eshell-parse-argument-hook'."
(defun eshell/export (&rest sets)
"This alias allows the `export' command to act as bash users expect."
(while sets
(if (and (stringp (car sets))
(string-match "^\\([^=]+\\)=\\(.*\\)" (car sets)))
(setenv (match-string 1 (car sets))
(match-string 2 (car sets))))
(setq sets (cdr sets))))
(dolist (set sets)
(when (and (stringp set)
(string-match "^\\([^=]+\\)=\\(.*\\)" set))
(eshell-set-variable (match-string 1 set)
(match-string 2 set)))))
(defun pcomplete/eshell-mode/export ()
"Completion function for Eshell's `export'."
@ -343,16 +359,28 @@ This function is explicit for adding to `eshell-parse-argument-hook'."
(eshell-envvar-names)))))
(defun eshell/unset (&rest args)
"Unset an environment variable."
(while args
(if (stringp (car args))
(setenv (car args) nil t))
(setq args (cdr args))))
"Unset one or more variables.
This is equivalent to calling `eshell/set' for all of ARGS with
the values of nil for each."
(dolist (arg args)
(eshell-set-variable arg nil)))
(defun pcomplete/eshell-mode/unset ()
"Completion function for Eshell's `unset'."
(while (pcomplete-here (eshell-envvar-names))))
(defun eshell/set (&rest args)
"Allow command-ish use of `set'."
(let (last-value)
(while args
(setq last-value (eshell-set-variable (car args) (cadr args))
args (cddr args)))
last-value))
(defun pcomplete/eshell-mode/set ()
"Completion function for Eshell's `set'."
(while (pcomplete-here (eshell-envvar-names))))
(defun eshell/setq (&rest args)
"Allow command-ish use of `setq'."
(let (last-value)
@ -566,18 +594,21 @@ INDICES is a list of index-lists (see `eshell-parse-indices').
If QUOTED is non-nil, this was invoked inside double-quotes."
(if-let ((alias (assoc name eshell-variable-aliases-list)))
(let ((target (nth 1 alias)))
(when (and (not (functionp target))
(consp target))
(setq target (car target)))
(cond
((functionp target)
(if (nth 3 alias)
(eshell-apply-indices (funcall target) indices quoted)
(condition-case nil
(funcall target indices quoted)
(wrong-number-of-arguments
(display-warning
:warning (concat "Function for `eshell-variable-aliases-list' "
"entry should accept two arguments: INDICES "
"and QUOTED.'"))
(funcall target indices)))))
(let ((max-arity (cdr (func-arity target))))
(if (or (eq max-arity 'many) (>= max-arity 2))
(funcall target indices quoted)
(display-warning
:warning (concat "Function for `eshell-variable-aliases-list' "
"entry should accept two arguments: INDICES "
"and QUOTED.'"))
(funcall target indices)))))
((symbolp target)
(eshell-apply-indices (symbol-value target) indices quoted))
(t
@ -594,6 +625,44 @@ If QUOTED is non-nil, this was invoked inside double-quotes."
(getenv name)))
indices quoted)))
(defun eshell-set-variable (name value)
"Set the variable named NAME to VALUE.
NAME can be a string (in which case it refers to an environment
variable or variable alias) or a symbol (in which case it refers
to a Lisp variable)."
(if-let ((alias (assoc name eshell-variable-aliases-list)))
(let ((target (nth 1 alias)))
(cond
((functionp target)
(setq target nil))
((consp target)
(setq target (cdr target))))
(cond
((functionp target)
(funcall target nil value))
((null target)
(unless eshell-in-subcommand-p
(error "Variable `%s' is not settable" (eshell-stringify name)))
(push `(,name ,(lambda () value) t t)
eshell-variable-aliases-list)
value)
;; Since getting a variable alias with a string target and
;; `eshell-prefer-lisp-variables' non-nil gets the
;; corresponding Lisp variable, make sure setting does the
;; same.
((and eshell-prefer-lisp-variables
(stringp target))
(eshell-set-variable (intern target) value))
(t
(eshell-set-variable target value))))
(cond
((stringp name)
(setenv name value))
((symbolp name)
(set name value))
(t
(error "Unknown variable `%s'" (eshell-stringify name))))))
(defun eshell-apply-indices (value indices &optional quoted)
"Apply to VALUE all of the given INDICES, returning the sub-result.
The format of INDICES is:

View file

@ -25,6 +25,7 @@
(require 'ert)
(require 'esh-mode)
(require 'esh-var)
(require 'eshell)
(require 'eshell-tests-helpers
@ -439,6 +440,150 @@ inside double-quotes"
(eshell-command-result-equal "echo \"${echo \\\"000 010 020\\\"}[0]\""
"000"))
;; Variable-related commands
(ert-deftest esh-var-test/set/env-var ()
"Test that `set' with a string variable name sets an environment variable."
(with-temp-eshell
(eshell-match-command-output "set VAR hello" "hello\n")
(should (equal (getenv "VAR") "hello")))
(should-not (equal (getenv "VAR") "hello")))
(ert-deftest esh-var-test/set/symbol ()
"Test that `set' with a symbol variable name sets a Lisp variable."
(let (eshell-test-value)
(eshell-command-result-equal "set #'eshell-test-value hello"
"hello")
(should (equal eshell-test-value "hello"))))
(ert-deftest esh-var-test/unset/env-var ()
"Test that `unset' with a string variable name unsets an env var."
(let ((process-environment (cons "VAR=value" process-environment)))
(with-temp-eshell
(eshell-match-command-output "unset VAR" "\\`\\'")
(should (equal (getenv "VAR") nil)))
(should (equal (getenv "VAR") "value"))))
(ert-deftest esh-var-test/unset/symbol ()
"Test that `unset' with a symbol variable name unsets a Lisp variable."
(let ((eshell-test-value "value"))
(eshell-command-result-equal "unset #'eshell-test-value" nil)
(should (equal eshell-test-value nil))))
(ert-deftest esh-var-test/setq ()
"Test that `setq' sets Lisp variables."
(let (eshell-test-value)
(eshell-command-result-equal "setq eshell-test-value hello"
"hello")
(should (equal eshell-test-value "hello"))))
(ert-deftest esh-var-test/export ()
"Test that `export' sets environment variables."
(with-temp-eshell
(eshell-match-command-output "export VAR=hello" "\\`\\'")
(should (equal (getenv "VAR") "hello"))))
(ert-deftest esh-var-test/local-variables ()
"Test that \"VAR=value command\" temporarily sets variables."
(with-temp-eshell
(push "VAR=value" process-environment)
(eshell-match-command-output "VAR=hello env" "VAR=hello\n")
(should (equal (getenv "VAR") "value"))))
;; Variable aliases
(ert-deftest esh-var-test/alias/function ()
"Test using a variable alias defined as a function."
(with-temp-eshell
(push `("ALIAS" ,(lambda () "value") nil t) eshell-variable-aliases-list)
(eshell-match-command-output "echo $ALIAS" "value\n")
(eshell-match-command-output "set ALIAS hello"
"Variable `ALIAS' is not settable\n"
nil t)))
(ert-deftest esh-var-test/alias/function-pair ()
"Test using a variable alias defined as a pair of getter/setter functions."
(with-temp-eshell
(let ((eshell-test-value "value"))
(push `("ALIAS" (,(lambda () eshell-test-value)
. (lambda (_ value)
(setq eshell-test-value (upcase value))))
nil t)
eshell-variable-aliases-list)
(eshell-match-command-output "echo $ALIAS" "value\n")
(eshell-match-command-output "set ALIAS hello" "HELLO\n")
(should (equal eshell-test-value "HELLO")))))
(ert-deftest esh-var-test/alias/string ()
"Test using a variable alias defined as a string.
This should get/set the aliased environment variable."
(with-temp-eshell
(let ((eshell-test-value "lisp-value"))
(push "eshell-test-value=env-value" process-environment)
(push `("ALIAS" "eshell-test-value") eshell-variable-aliases-list)
(eshell-match-command-output "echo $ALIAS" "env-value\n")
(eshell-match-command-output "set ALIAS hello" "hello\n")
(should (equal (getenv "eshell-test-value") "hello"))
(should (equal eshell-test-value "lisp-value")))))
(ert-deftest esh-var-test/alias/string/prefer-lisp ()
"Test using a variable alias defined as a string.
This sets `eshell-prefer-lisp-variables' to t and should get/set
the aliased Lisp variable."
(with-temp-eshell
(let ((eshell-test-value "lisp-value")
(eshell-prefer-lisp-variables t))
(push "eshell-test-value=env-value" process-environment)
(push `("ALIAS" "eshell-test-value") eshell-variable-aliases-list)
(eshell-match-command-output "echo $ALIAS" "lisp-value\n")
(eshell-match-command-output "set ALIAS hello" "hello\n")
(should (equal (car process-environment) "eshell-test-value=env-value"))
(should (equal eshell-test-value "hello")))))
(ert-deftest esh-var-test/alias/symbol ()
"Test using a variable alias defined as a symbol.
This should get/set the value bound to the symbol."
(with-temp-eshell
(let ((eshell-test-value "value"))
(push '("ALIAS" eshell-test-value) eshell-variable-aliases-list)
(eshell-match-command-output "echo $ALIAS" "value\n")
(eshell-match-command-output "set ALIAS hello" "hello\n")
(should (equal eshell-test-value "hello")))))
(ert-deftest esh-var-test/alias/symbol-pair ()
"Test using a variable alias defined as a pair of symbols.
This should get the value bound to the symbol, but fail to set
it, since the setter is nil."
(with-temp-eshell
(let ((eshell-test-value "value"))
(push '("ALIAS" (eshell-test-value . nil)) eshell-variable-aliases-list)
(eshell-match-command-output "echo $ALIAS" "value\n")
(eshell-match-command-output "set ALIAS hello"
"Variable `ALIAS' is not settable\n"
nil t))))
(ert-deftest esh-var-test/alias/export ()
"Test that `export' properly sets variable aliases."
(with-temp-eshell
(let ((eshell-test-value "value"))
(push `("ALIAS" (,(lambda () eshell-test-value)
. (lambda (_ value) (setq eshell-test-value value)))
nil t)
eshell-variable-aliases-list)
(eshell-match-command-output "export ALIAS=hello" "\\`\\'")
(should (equal eshell-test-value "hello")))))
(ert-deftest esh-var-test/alias/local-variables ()
"Test that \"VAR=value cmd\" temporarily sets read-only variable aliases."
(with-temp-eshell
(let ((eshell-test-value "value"))
(push `("ALIAS" ,(lambda () eshell-test-value) t t)
eshell-variable-aliases-list)
(eshell-match-command-output "ALIAS=hello env" "ALIAS=hello\n")
(should (equal eshell-test-value "value")))))
;; Built-in variables