From d44b146b8bf114970b568ea7d71b5780a99d66d7 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Sun, 25 Sep 2022 21:47:26 -0700 Subject: [PATCH 3/5] 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. --- doc/misc/eshell.texi | 46 ++++++++-- lisp/eshell/esh-cmd.el | 4 +- lisp/eshell/esh-var.el | 141 +++++++++++++++++++++-------- test/lisp/eshell/esh-var-tests.el | 145 ++++++++++++++++++++++++++++++ 4 files changed, 290 insertions(+), 46 deletions(-) diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index 8036bbd83a..48edee59ab 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -694,10 +694,17 @@ Built-ins 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. + @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 +750,9 @@ Built-ins @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 +890,33 @@ Built-ins @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. To set an environment +variable, use @samp{export @var{NAME}=@var{value}}. You can also use +@samp{set @var{name} @var{value}}, which sets a Lisp variable if +@var{name} is a symbol, or an environment variable if @var{name} is a +string. 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 +@{ + set @var{NAME} @var{value} + @var{command} @dots{} +@} +@end example @subsection Built-in variables Eshell knows a few built-in variables: diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el index 413336e3eb..9a56b56458 100644 --- a/lisp/eshell/esh-cmd.el +++ b/lisp/eshell/esh-cmd.el @@ -261,9 +261,9 @@ eshell-deferrable-commands (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) diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el index 3c09fc52fb..caf143e1a1 100644 --- a/lisp/eshell/esh-var.el +++ b/lisp/eshell/esh-var.el @@ -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 @@ eshell-variable-aliases-list (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 @@ eshell-variable-aliases-list ("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 @@ eshell-variable-aliases-list 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 @@ eshell-variable-aliases-list 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)) @@ -234,6 +246,11 @@ eshell-var-initialize ;; 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 @@ eshell-handle-local-variables (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 @@ eshell/define (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 @@ pcomplete/eshell-mode/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) @@ -566,18 +594,21 @@ eshell-get-variable 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 @@ eshell-get-variable (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: diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el index ad695e45d7..a7ac52ed24 100644 --- a/test/lisp/eshell/esh-var-tests.el +++ b/test/lisp/eshell/esh-var-tests.el @@ -25,6 +25,7 @@ (require 'ert) (require 'esh-mode) +(require 'esh-var) (require 'eshell) (require 'eshell-tests-helpers @@ -439,6 +440,150 @@ esh-var-test/quoted-interp-convert-cmd-split-indices (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 -- 2.25.1