8

I would like to set the environment variables for a spawned subprocess only. They are controlled by process-environment. The documentation says:

binding that variable with let is also reasonable practice.

In this example I try to unset HOME with:

(defun f()
  (let ((old-home (getenv  "HOME")))
    (let* ((process-environment process-environment)
       (process-environment (setenv-internal process-environment  "HOME" nil nil)))
      (start-process "proc" nil "notepad"))
    (string= old-home (getenv  "HOME"))))

(f)

(f) returns nil and process-environment is permanently changed (outside the let*).

Another thing, that happens when debugging, is that concurrent processes using HOME (for example Emacs autosave hooks) might fail crashing the debug session.

An alterative would be to use two setenvs before and after start-process. It still would not solve the conflict with concurrent processes (and does not explain how to use process-environment inside a let).

antonio
  • 1,762
  • 12
  • 24

3 Answers3

5

setenv-internal and setenv change the list stored in process-environment (as local or special variable) by side-effects.

It does not help if you assign the list (pointer) to a local variable process-environment. You still have only a single list for the process-environment which is just bound to two variables -- the global variable process-environment and the let-bound variable process-environment. If you modify that single list by side-effect, it effects all processes that use that list as process environment regardless to which variables the list is bound.

To get separate process environments for all processes you must create a copy of process-environment for the let-binding, e.g. with cl-copy-list.

(defun f()
  (with-temp-buffer
    (let ((old-home (getenv  "HOME"))
      (process-environment (cl-copy-list process-environment)))
      (setenv "HOME" nil)
      (start-process "proc" (current-buffer) "emacs" "--batch" "--eval" "(prin1 (concat \"proc gets \" (getenv-internal \"HOME\" initial-environment)))" "--kill")
      (message "HOME within the let: %s" (getenv  "HOME")))
    (message "HOME after the let: %s" (getenv "HOME"))
    (buffer-string)))
(f)
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • Thank you very much. I still wonder if I can have the `setenv` affecting only the given process and avoid conflicts with concurrent processes. For example, consider the autosave hook starting while `HOME` is unset/modified and the save path uses `~`. – antonio Jun 01 '18 at 07:38
  • @antonio I have added some sentences that hopefully make crystal clear that `cl-copy-list` avoid effects across processes through `process-environment`. – Tobias Jun 01 '18 at 20:00
  • 1
    `(getenv-internal \"HOME\" initial-environment)` won't really give the initial environment value of HOME on Windows prior to Emacs 25.2 (see [Bug#10980](https://debbugs.gnu.org/cgi/bugreport.cgi?bug=10980)). – npostavs Jun 01 '18 at 20:25
3

Since Emacs 28.1 you can use the with-environment-variables macro.

Example:

(with-environment-variables (("DESKTOP_SESSION" "TEST_VALUE"))
  (shell-command "echo $DESKTOP_SESSION"))
(shell-command "echo $DESKTOP_SESSION")

Output:

TEST_VALUE
ubuntu
flonic
  • 41
  • 2
2

The "let binding" way is the following (notice that contrary to Tobias's answer, this does not involve any copying or "set"ting):

(let ((process-environment
       (cons "HOME"
             (cons (concat "OLDHOME=" (getenv "HOME"))
                   process-environment))))
  (start-process "proc" (current-buffer) ...))
Stefan
  • 26,154
  • 3
  • 46
  • 84
  • 3
    It should be noted that the `(cons "HOME"` part only has the desired effect in Emacs 25.1 or newer, due to [Bug#23779](https://debbugs.gnu.org/cgi/bugreport.cgi?bug=23779). – npostavs May 31 '18 at 18:58
  • 1
    @npostavs Thanks. I had almost the same solution as Stefan (only the OLDHOME-thing was a bit different) but I discarded it because it did not work for me. Your comment disclosed why. – Tobias May 31 '18 at 19:17