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 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
@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
@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:
(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)
(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
(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)
("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:
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,
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'.
+
+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 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.
+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, return the value bound to it.
+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))
;; 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 '(?$)))
(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))
(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'."
(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)
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
(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:
(require 'ert)
(require 'esh-mode)
+(require 'esh-var)
(require 'eshell)
(require 'eshell-tests-helpers
(eshell-command-result-equal "echo \"${echo \\\"000 010 020\\\"}[0]\""
"000"))
+\f
+;; 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"))))
+
+\f
+;; 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")))))
+
\f
;; Built-in variables