unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
@ 2022-09-03  5:03 Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-03 12:26 ` Lars Ingebrigtsen
  2022-09-18 11:18 ` Michael Albinus
  0 siblings, 2 replies; 32+ messages in thread
From: Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2022-09-03  5:03 UTC (permalink / raw)
  To: 57556


This is related to bug #57370 which I reported earlier. That bug can be
closed as my reproduction instructions there were incorrect and my title
did not capture the root of the problem. The real problem only seems to
appear when loading the included library tramp-integration.

1. Create an executable script ~/test-bin/test.sh
2. emacs -Q -l tramp-integration
3. Eval the elisp snippet:
`(setenv "PATH" (concat (expand-file-name "~/test-bin") ":"  
(getenv "PATH")))`
4. Start eshell
5. test.sh
6. Observe command not found error


In GNU Emacs 28.1 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.33,  
cairo version 1.16.0)
  of 2022-06-27, modified by Debian built on kokoro-ubuntu
Windowing system distributor 'The X.Org Foundation', version 11.0.12101004
System Description: Debian GNU/Linux rodete

Configured using:
  'configure --build x86_64-linux-gnu --prefix=/usr
  --sharedstatedir=/var/lib --libexecdir=/usr/lib
  --localstatedir=/var/lib --infodir=/usr/share/info
  --mandir=/usr/share/man --enable-libsystemd --with-pop=yes
   
--enable-locallisppath=/etc/google-emacs:/usr/local/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/site-lisp:/usr/local/share/google-emacs/site-lisp:/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/site-lisp:/usr/share/google-emacs/site-lisp
  --with-sound=alsa --without-gconf --with-mailutils
  --program-prefix=google- --disable-silent-rules
  GOOGLE_VERSION=28.1+gg1+1.20220627.053402.rc213 --build
  x86_64-linux-gnu --prefix=/usr --sharedstatedir=/var/lib
  --libexecdir=/usr/lib --localstatedir=/var/lib
  --infodir=/usr/share/info --mandir=/usr/share/man --enable-libsystemd
  --with-pop=yes
   
--enable-locallisppath=/etc/google-emacs:/usr/local/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/site-lisp:/usr/local/share/google-emacs/site-lisp:/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/site-lisp:/usr/share/google-emacs/site-lisp
  --with-sound=alsa --without-gconf --with-mailutils
  --program-prefix=google- --disable-silent-rules
  GOOGLE_VERSION=28.1+gg1+1.20220627.053402.rc213 --with-cairo
  --with-x=yes --with-x-toolkit=gtk3 --with-toolkit-scroll-bars
  'CFLAGS=-g -O2
   
-ffile-prefix-map=/build/google-emacs-V4HnnR/google-emacs-28.1+gg1+1.20220627.053402.rc213=.  
-fstack-protector-strong
  -Wformat -Werror=format-security -Wall' 'CPPFLAGS=-Wdate-time
  -D_FORTIFY_SOURCE=2' LDFLAGS=-Wl,-z,relro'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
JSON LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES NOTIFY
INOTIFY PDUMPER PNG RSVG SECCOMP SOUND THREADS TIFF TOOLKIT_SCROLL_BARS
X11 XDBE XIM XPM GTK3 ZLIB

Important settings:
   value of $LANG: en_US.UTF-8
   value of $XMODIFIERS: @im=ibus
   locale-coding-system: utf-8-unix

Major mode: ELisp/l

Minor modes in effect:
   t: t
   global-git-commit-mode: t
   magit-auto-revert-mode: t
   google-tramp-direct-mode: t
   global-subword-mode: t
   subword-mode: t
   google-emacs-support-show-upgrade-mode: t
   editorconfig-mode: t
   google3-build-global-integrate-build-manipulation-mode: t
   google3-build-integrate-build-manipulation-mode: t
   google-kg-mode: t
   shell-dirtrack-mode: t
   tooltip-mode: t
   global-eldoc-mode: t
   eldoc-mode: t
   show-paren-mode: t
   electric-indent-mode: t
   mouse-wheel-mode: t
   file-name-shadow-mode: t
   global-font-lock-mode: t
   font-lock-mode: t
   blink-cursor-mode: t
   auto-composition-mode: t
   auto-encryption-mode: t
   auto-compression-mode: t
   line-number-mode: t
   indent-tabs-mode: t
   transient-mark-mode: t

Load-path shadows:
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/f/f-shortdoc  
hides  
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/f/google/f-shortdoc
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/async/smtpmail-async  
hides  
/usr/share/google-emacs/site-lisp/elpa/async-1.9.3/smtpmail-async
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/async/async  
hides  
/usr/share/google-emacs/site-lisp/elpa/async-1.9.3/async
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/async/async-bytecomp  
hides  
/usr/share/google-emacs/site-lisp/elpa/async-1.9.3/async-bytecomp
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/async/dired-async  
hides  
/usr/share/google-emacs/site-lisp/elpa/async-1.9.3/dired-async
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-clang  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-clang
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-tng  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-tng
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-oddmuse  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-oddmuse
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-bbdb  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-bbdb
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-keywords  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-keywords
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-capf  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-capf
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-dabbrev-code  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-dabbrev-code
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-tempo  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-tempo
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-ispell  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-ispell
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-dabbrev  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-dabbrev
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-abbrev  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-abbrev
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-elisp  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-elisp
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-yasnippet  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-yasnippet
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-cmake  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-cmake
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-etags  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-etags
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-template  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-template
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-files  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-files
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-gtags  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-gtags
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-nxml  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-nxml
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-css  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-css
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/company_mode/company-semantic  
hides  
/usr/share/google-emacs/site-lisp/elpa/company-0.9.13/company-semantic
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/dash/dash  
hides  
/usr/share/google-emacs/site-lisp/elpa/dash-2.19.1/dash
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/epl/epl  
hides  
/usr/share/google-emacs/site-lisp/elpa/epl-0.9/epl
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/f/f  
hides /usr/share/google-emacs/site-lisp/elpa/f-0.20.0/f
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/flycheck/flycheck  
hides  
/usr/share/google-emacs/site-lisp/elpa/flycheck-32snapshot/flycheck
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/ht/ht  
hides  
/usr/share/google-emacs/site-lisp/elpa/ht-2.3/ht
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/lv/lv  
hides  
/usr/share/google-emacs/site-lisp/elpa/lv-0.15.0/lv
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/memoize/memoize  
hides  
/usr/share/google-emacs/site-lisp/elpa/memoize-1.1/memoize
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/pkg_info/pkg-info  
hides  
/usr/share/google-emacs/site-lisp/elpa/pkg-info-0.6/pkg-info
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/popup/popup  
hides  
/usr/share/google-emacs/site-lisp/elpa/popup-0.5.8/popup
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/s/s  
hides /usr/share/google-emacs/site-lisp/elpa/s-1.12.0/s
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/spinner/spinner  
hides  
/usr/share/google-emacs/site-lisp/elpa/spinner-1.7.4/spinner
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/yasnippet/yasnippet  
hides  
/usr/share/google-emacs/site-lisp/elpa/yasnippet-0.14.0/yasnippet
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/jsonrpc/jsonrpc  
hides  
/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/lisp/jsonrpc
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/transient/lisp/transient  
hides  
/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/lisp/transient
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/flymake/flymake  
hides  
/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/lisp/progmodes/flymake
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/xref/xref  
hides  
/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/lisp/progmodes/xref
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/project/project  
hides  
/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/lisp/progmodes/project
/usr/share/google-emacs/site-lisp/elpa/let-alist-1.0.6/let-alist hides  
/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/lisp/emacs-lisp/let-alist
/usr/share/google-emacs/site-lisp/emacs-google-config/third_party/elisp/eldoc/eldoc  
hides  
/usr/share/google-emacs/28.1+gg1+1.20220627.053402.rc213/lisp/emacs-lisp/eldoc

Features:
(shadow mail-extr emacsbug rmail-spam-filter rmailedit rmailsort
undigest rmailout rmailkwd qp rmailsum semantic/symref/grep
semantic/symref semantic/util-modes semantic/util semantic semantic/tag
semantic/lex semantic/fw mode-local cedet cl-print rst google3-ffap ffap
grep apropos pcmpl-gnu tabify woman man find-dired pcmpl-unix bookmark
magit-bundle eieio-opt speedbar ezimage dframe find-func calc-alg
tramp-cache em-unix em-term term disp-table ehelp em-script em-prompt
em-ls em-hist em-pred em-glob em-cmpl em-dirs esh-var em-basic em-banner
em-alias esh-mode eshell esh-cmd esh-ext esh-opt esh-proc esh-io esh-arg
esh-module esh-groups esh-util pulse color etags fileloop calc-stuff
calc-ext calc-misc calc-menu calc calc-loaddefs rect calc-macs misearch
multi-isearch sort goto-addr vc-mtn vc-bzr vc-src vc-sccs vc-svn vc-cvs
vc-rcs bug-reference help-fns radix-tree face-remap magit-version
magit-bookmark git-rebase magit-extras magit-sparse-checkout
magit-gitignore magit-ediff ediff ediff-merg ediff-mult ediff-wind
ediff-diff ediff-help ediff-init ediff-util magit-subtree magit-patch
magit-submodule magit-obsolete magit-popup magit-blame magit-stash
magit-reflog magit-bisect magit-push magit-pull magit-fetch magit-clone
magit-remote magit-commit magit-sequence magit-notes magit-worktree
magit-tag magit-merge magit-branch magit-reset magit-files magit-refs
magit-status magit magit-repos magit-apply magit-wip magit-log
magit-diff smerge-mode diff git-commit log-edit pcvs-util add-log
magit-core magit-autorevert magit-margin magit-transient magit-process
magit-mode transient magit-git magit-base magit-section crm compat-27
compat-26 mule-util jka-compr rmailmm protobuffer which-func
reformat-file apply-ed-script message rmc puny rfc822 mml mml-sec epa
derived epg rfc6068 epg-config gnus-util rmail rmail-loaddefs mm-decode
mm-bodies mm-encode mail-parse rfc2231 mailabbrev gmm-utils mailheader
dired-aux dired dired-loaddefs autorevert filenotify vc-git vc-fig vc-hg
diff-mode vc vc-dispatcher editorconfig-core editorconfig-core-handle
editorconfig-fnmatch ido-completing-read+ memoize cus-edit pp cus-start
wid-edit minibuf-eldef google-tramp cap-words superword subword desktop
frameset cus-load edmacro kmacro google-sendgmr sendmail rfc2047 rfc2045
ietf-drums mm-util mail-prsvr mail-utils google google-emacs-support
editorconfig google-log gud url-sso google3-build-fn google-imports
google3 google-gud google-comint google-kg google-trailing-whitespace
google-coding-style python-custom sh-script smie python tramp-sh tramp
tramp-loaddefs trampver tramp-integration files-x tramp-compat
parse-time iso8601 time-date ls-lisp js imenu cc-mode cc-fonts cc-guess
cc-menus cc-cmds cc-styles cc-align cc-engine google-codemaker
google-process google-emacs-utilities with-editor cl-extra help-mode
server compat pcase f f-shortdoc shortdoc dash s aio generator
emacs-google-config-loaddefs google-paths xdg rx google-platform sql
view ess-site ess-toolbar ess-mouse mouseme ess-swv ess-noweb
ess-noweb-font-lock-mode ess-jags-d ess-bugs-l essd-els ess-xls-d
ess-vst-d ess-stata-mode ess-stata-lang cc-vars cc-defs make-regexp
ess-sp6w-d ess-sp5-d ess-sp4-d ess-sas-d ess-sas-l ess-sas-a ess-s4-d
ess-s3-d ess-omg-d ess-omg-l ess-arc-d ess-lsp-l ess-sp6-d ess-dde
ess-sp3-d ess-julia julia-mode cl ess-r-mode ess-r-flymake flymake-proc
flymake warnings thingatpt ess-r-xref xref project ess-trns
ess-r-package shell pcomplete ess-r-syntax ess-r-completion ess-roxy
ess-rd essddr noutline outline easy-mmode hideshow ess-s-lang ess-help
info ess-mode ess ess-noweb-mode ess-inf ess-tracebug advice format-spec
ess-generics compile text-property-search ess-utils ido ess-custom
executable comint ansi-color ring package browse-url url url-proxy
url-privacy url-expand url-methods url-history url-cookie url-domsuf
url-util mailcap url-handlers url-parse auth-source cl-seq eieio
eieio-core cl-macs eieio-loaddefs password-cache json subr-x map
url-vars seq byte-opt gv bytecomp byte-compile cconv cl-loaddefs cl-lib
iso-transl tooltip eldoc paren electric uniquify ediff-hook vc-hooks
lisp-float-type elisp-mode mwheel term/x-win x-win term/common-win x-dnd
tool-bar dnd fontset image regexp-opt fringe tabulated-list replace
newcomment text-mode lisp-mode prog-mode register page tab-bar menu-bar
rfn-eshadow isearch easymenu timer select scroll-bar mouse jit-lock
font-lock syntax font-core term/tty-colors frame minibuffer cl-generic
cham georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese eucjp-ms cp51932 hebrew greek romanian slovak czech
european ethiopic indian cyrillic chinese composite emoji-zwj charscript
charprop case-table epa-hook jka-cmpr-hook help simple abbrev obarray
cl-preloaded nadvice button loaddefs faces cus-face macroexp files
window text-properties overlay sha1 md5 base64 format env code-pages
mule custom widget hashtable-print-readable backquote threads dbusbind
inotify lcms2 dynamic-setting system-font-setting font-render-setting
cairo move-toolbar gtk x-toolkit x multi-tty make-network-process emacs)

Memory information:
((conses 16 614935 72899)
  (symbols 48 36853 6)
  (strings 32 163952 30759)
  (string-bytes 1 5377096)
  (vectors 16 74674)
  (vector-slots 8 1217245 548922)
  (floats 8 315 517)
  (intervals 56 17337 921)
  (buffers 992 51))





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-03  5:03 bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2022-09-03 12:26 ` Lars Ingebrigtsen
  2022-09-18 11:18 ` Michael Albinus
  1 sibling, 0 replies; 32+ messages in thread
From: Lars Ingebrigtsen @ 2022-09-03 12:26 UTC (permalink / raw)
  To: Colton Lewis; +Cc: 57556

Colton Lewis <coltonlewis@google.com> writes:

> This is related to bug #57370 which I reported earlier. That bug can be
> closed as my reproduction instructions there were incorrect and my title
> did not capture the root of the problem.

Now closed.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-03  5:03 bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-03 12:26 ` Lars Ingebrigtsen
@ 2022-09-18 11:18 ` Michael Albinus
  2022-09-18 18:54   ` Jim Porter
  1 sibling, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-09-18 11:18 UTC (permalink / raw)
  To: 57556; +Cc: coltonlewis

[-- Attachment #1: Type: text/plain, Size: 835 bytes --]

Colton Lewis via "Bug reports for GNU Emacs, the Swiss army knife of
text editors" <bug-gnu-emacs@gnu.org> writes:

Hi Colton,

> 1. Create an executable script ~/test-bin/test.sh
> 2. emacs -Q -l tramp-integration
> 3. Eval the elisp snippet:
> `(setenv "PATH" (concat (expand-file-name "~/test-bin") ":" (getenv
> "PATH")))`
> 4. Start eshell
> 5. test.sh
> 6. Observe command not found error

I've fixed this in the emacs-28 branch of the git repository, will be
merged to the master branch next days. Do yo have a chance to test? As
reference, I've appended the patch.

It isn't known yet whether there will be another Emacs 28 release, so
this change will appear in either Emacs 28.3 or 29.1. But you don't need
to wait that long, the next Tramp 2.5.3.3 release on GNU ELPA will carry
this patch as well.

Best regards, Michael.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Type: text/x-patch, Size: 907 bytes --]

*** /tmp/ediffO8DSK8	2022-09-18 13:16:26.974594555 +0200
--- /home/albinus/src/emacs-28/lisp/net/tramp-integration.el	2022-09-18 12:05:50.293303445 +0200
***************
*** 134,141 ****
    ;; Remove last element of `(exec-path)', which is `exec-directory'.
    ;; Use `path-separator' as it does eshell.
    (setq eshell-path-env
! 	(mapconcat
! 	 #'identity (butlast (tramp-compat-exec-path)) path-separator)))

  (with-eval-after-load 'esh-util
    (add-hook 'eshell-mode-hook
--- 134,143 ----
    ;; Remove last element of `(exec-path)', which is `exec-directory'.
    ;; Use `path-separator' as it does eshell.
    (setq eshell-path-env
!         (if (file-remote-p default-directory)
!             (mapconcat
! 	     #'identity (butlast (tramp-compat-exec-path)) path-separator)
!           (getenv "PATH"))))

  (with-eval-after-load 'esh-util
    (add-hook 'eshell-mode-hook

^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-18 11:18 ` Michael Albinus
@ 2022-09-18 18:54   ` Jim Porter
  2022-09-18 19:07     ` Michael Albinus
  0 siblings, 1 reply; 32+ messages in thread
From: Jim Porter @ 2022-09-18 18:54 UTC (permalink / raw)
  To: Michael Albinus, 57556; +Cc: coltonlewis

On 9/18/2022 4:18 AM, Michael Albinus wrote:
> I've fixed this in the emacs-28 branch of the git repository, will be
> merged to the master branch next days. Do yo have a chance to test? As
> reference, I've appended the patch.

For what it's worth, I've started on a more-elaborate patch for this 
that would move all this into Eshell and allow manipulating Eshell's 
PATH variable on remote hosts too. (Eshell's codebase already has some 
special handling for Tramp, so I think it would be reasonable for Eshell 
to handle Tramp integration here, rather than the other way around.)

My patch needs a couple preliminary fixes though so it'll probably take 
a few days at least until I have something ready for people to look at.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-18 18:54   ` Jim Porter
@ 2022-09-18 19:07     ` Michael Albinus
  2022-09-22 17:23       ` Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-09-18 19:07 UTC (permalink / raw)
  To: Jim Porter; +Cc: coltonlewis, 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

> For what it's worth, I've started on a more-elaborate patch for this
> that would move all this into Eshell and allow manipulating Eshell's
> PATH variable on remote hosts too. (Eshell's codebase already has some
> special handling for Tramp, so I think it would be reasonable for
> Eshell to handle Tramp integration here, rather than the other way
> around.)
>
> My patch needs a couple preliminary fixes though so it'll probably
> take a few days at least until I have something ready for people to
> look at.

Much appreciated! For possible Tramp changes please take into account,
that Tramp 2.6 should be backward compatible down to Emacs 26.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-18 19:07     ` Michael Albinus
@ 2022-09-22 17:23       ` Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-22 17:55         ` Michael Albinus
  0 siblings, 1 reply; 32+ messages in thread
From: Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2022-09-22 17:23 UTC (permalink / raw)
  To: Michael Albinus; +Cc: Jim Porter, 57556

Thank you Michael. I have tested and can confirm your patch solves my problem.

On Sun, Sep 18, 2022 at 2:07 PM Michael Albinus <michael.albinus@gmx.de> wrote:
>
> Jim Porter <jporterbugs@gmail.com> writes:
>
> Hi Jim,
>
> > For what it's worth, I've started on a more-elaborate patch for this
> > that would move all this into Eshell and allow manipulating Eshell's
> > PATH variable on remote hosts too. (Eshell's codebase already has some
> > special handling for Tramp, so I think it would be reasonable for
> > Eshell to handle Tramp integration here, rather than the other way
> > around.)
> >
> > My patch needs a couple preliminary fixes though so it'll probably
> > take a few days at least until I have something ready for people to
> > look at.
>
> Much appreciated! For possible Tramp changes please take into account,
> that Tramp 2.6 should be backward compatible down to Emacs 26.
>
> Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-22 17:23       ` Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2022-09-22 17:55         ` Michael Albinus
  2022-09-30  3:54           ` Jim Porter
  0 siblings, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-09-22 17:55 UTC (permalink / raw)
  To: Colton Lewis; +Cc: Jim Porter, 57556-done

Version: 28.3

Colton Lewis <coltonlewis@google.com> writes:

Hi Colton,

> Thank you Michael. I have tested and can confirm your patch solves my problem.

Thanks for the feedback. I'm closing the bug.

There will be another patch by Jim. But this is for Emacs 29 only I
guess, and it will override this change then w/o regression.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-22 17:55         ` Michael Albinus
@ 2022-09-30  3:54           ` Jim Porter
  2022-10-01 20:25             ` Michael Albinus
  0 siblings, 1 reply; 32+ messages in thread
From: Jim Porter @ 2022-09-30  3:54 UTC (permalink / raw)
  To: 57556, michael.albinus, coltonlewis

[-- Attachment #1: Type: text/plain, Size: 3152 bytes --]

On 9/22/2022 10:55 AM, Michael Albinus wrote:
> There will be another patch by Jim. But this is for Emacs 29 only I
> guess, and it will override this change then w/o regression.

It took a lot longer than anticipated, but here's a patch series to give 
Eshell full support for a Tramp-aware $PATH. This ended up considerably 
more elaborate than intended, mostly because of patch #3.

If people prefer, I could file a separate bug # for this set of changes, 
but I thought it would be better to track it here.

Here's a brief summary of everything I changed, and my reasoning. I'm 
still thinking about some of the changes myself, but I think this is 
complete enough for others to take a look at and provide feedback if 
they want.

Patch #2: Obsolete 'eshell/define'
----------------------------------------

I'm pretty sure this function never worked, since it adds Eshell 
variable aliases in a form that the rest of the code can't handle. If 
I'm wrong about this and someone can show how it should work, I'm happy 
to get rid of this patch and replace it with any fixes necessitated by 
my other patches.

Patch #3: Allow setting variable aliases
----------------------------------------

Since the plan is to make $PATH into a variable alias so that Eshell can 
do the right thing when changing directories to a different host, I 
wanted to be sure users can *set* variable aliases so that updating 
$PATH will be easy. This adds the ability to do that, along with a new 
"set" command in Eshell. That lets you set either environment variables 
or Lisp variables (note that "#'" is just Eshell's way of spelling "'", 
since a single-quote is used for literal strings in Eshell):

   set ENV_VAR value
   set #'lisp-var value

I debated on the name, since people might think it's more like Bash's 
"set" than Lisp's "set", but Eshell already has "setq", so I think "set" 
makes sense.

However, you can set these in other ways too:

   export ENV_VAR=value
   setq lisp-var value

Really, the "set" function is probably optional, but it seemed more 
convenient to me when I was trying these patches out, and I think it's a 
nice middle ground between Lispiness and sh-ness.

Patch #4: Make $PATH a variable alias
----------------------------------------

This stores the $PATH in an alist indexed by host, similar to 
'grep-host-defaults-alist'. For consistency, it now derives its value 
from '(exec-path)' everywhere (formerly, it used '(getenv "PATH") for 
local hosts and '(exec-path)' for Tramp).

This is likely an incompatible change for some users if they call 
(setenv "PATH" "foobar") in their init scripts, but it's easy enough to 
fix: just make the corresponding changes to 'exec-path' too. That said, 
if people think compatibility is more important, I could change this to 
use '(getenv "PATH")' for local directories.

These changes would probably be good to test on MS-Windows as well, 
since MS-Windows uses a different path-separator, so the code has to be 
pretty careful to use the right separator for the right system. I don't 
have Emacs builds set up on an MS-Windows system at the moment (though 
I've been meaning to).

[-- Attachment #2: 0001-Allow-ignoring-errors-when-calling-eshell-match-comm.patch --]
[-- Type: text/plain, Size: 3827 bytes --]

From aeb070716c41e3ee3012ae75d4554d05a203ffe4 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sat, 24 Sep 2022 18:13:03 -0700
Subject: [PATCH 1/5] ; Allow ignoring errors when calling
 'eshell-match-command-output'

* test/lisp/eshell/eshell-tests-helpers.el
(eshell-match-command-output): New argument IGNORE-ERRORS.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/last-status-var-lisp-command)
(esh-var-test/last-status-var-lisp-form)
(esh-var-test/last-status-var-lisp-form-2): Ignore errors when calling
'eshell-match-command-output'.
---
 test/lisp/eshell/esh-var-tests.el        | 15 ++++++---------
 test/lisp/eshell/eshell-tests-helpers.el | 13 ++++++++++---
 2 files changed, 16 insertions(+), 12 deletions(-)

diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
index cb5b1766bb..ad695e45d7 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -472,9 +472,8 @@ esh-var-test/last-status-var-lisp-command
                                 "t\n0\n")
    (eshell-match-command-output "zerop 1; echo $?"
                                 "0\n")
-   (let ((debug-on-error nil))
-     (eshell-match-command-output "zerop foo; echo $?"
-                                  "1\n"))))
+   (eshell-match-command-output "zerop foo; echo $?"
+                                "1\n" nil t)))
 
 (ert-deftest esh-var-test/last-status-var-lisp-form ()
   "Test using the \"last exit status\" ($?) variable with a Lisp form"
@@ -484,9 +483,8 @@ esh-var-test/last-status-var-lisp-form
                                   "t\n0\n")
      (eshell-match-command-output "(zerop 1); echo $?"
                                   "2\n")
-     (let ((debug-on-error nil))
-       (eshell-match-command-output "(zerop \"foo\"); echo $?"
-                                    "1\n")))))
+     (eshell-match-command-output "(zerop \"foo\"); echo $?"
+                                  "1\n" nil t))))
 
 (ert-deftest esh-var-test/last-status-var-lisp-form-2 ()
   "Test using the \"last exit status\" ($?) variable with a Lisp form.
@@ -497,9 +495,8 @@ esh-var-test/last-status-var-lisp-form-2
                                   "0\n")
      (eshell-match-command-output "(zerop 0); echo $?"
                                   "0\n")
-     (let ((debug-on-error nil))
-       (eshell-match-command-output "(zerop \"foo\"); echo $?"
-                                    "1\n")))))
+     (eshell-match-command-output "(zerop \"foo\"); echo $?"
+                                  "1\n" nil t))))
 
 (ert-deftest esh-var-test/last-status-var-ext-cmd ()
   "Test using the \"last exit status\" ($?) variable with an external command"
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index 73abfcbb55..e713e162ad 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -100,9 +100,16 @@ eshell-match-output--explainer
 
 (put 'eshell-match-output 'ert-explainer #'eshell-match-output--explainer)
 
-(defun eshell-match-command-output (command regexp &optional func)
-  "Insert a COMMAND at the end of the buffer and match the output with REGEXP."
-  (eshell-insert-command command func)
+(defun eshell-match-command-output (command regexp &optional func
+                                            ignore-errors)
+  "Insert a COMMAND at the end of the buffer and match the output with REGEXP.
+FUNC is the function to call after inserting the text (see
+`eshell-insert-command').
+
+If IGNORE-ERRORS is non-nil, ignore any errors signaled when
+inserting the command."
+  (let ((debug-on-error (and (not ignore-errors) debug-on-error)))
+    (eshell-insert-command command func))
   (eshell-wait-for-subprocess)
   (should (eshell-match-output regexp)))
 
-- 
2.25.1


[-- Attachment #3: 0002-Obsolete-eshell-define.patch --]
[-- Type: text/plain, Size: 1674 bytes --]

From 1e59ff312115290d4ec60adb5c8f0def613d4634 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Wed, 28 Sep 2022 09:34:38 -0700
Subject: [PATCH 2/5] ; Obsolete 'eshell/define'

* lisp/eshell/esh-var.el (eshell/define): Make obsolete, and explain
its current state.

* doc/misc/eshell.texi (Built-ins): Remove 'define'.
---
 doc/misc/eshell.texi   | 5 -----
 lisp/eshell/esh-var.el | 5 +++++
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 0ee33f2c2a..8036bbd83a 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -439,11 +439,6 @@ Built-ins
 is similar to, but slightly different from, the GNU Coreutils
 @command{date} command.
 
-@item define
-@cmindex define
-Define a variable alias.
-@xref{Variable Aliases, , , elisp, The Emacs Lisp Reference Manual}.
-
 @item diff
 @cmindex diff
 Compare files using Emacs's internal @code{diff} (not to be confused
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index 36e59cd5a4..3c09fc52fb 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -302,6 +302,11 @@ eshell-interpolate-variable
 
 (defun eshell/define (var-alias definition)
   "Define a VAR-ALIAS using DEFINITION."
+  ;; FIXME: This function doesn't work (it produces variable aliases
+  ;; in a form not recognized by other parts of the code), and likely
+  ;; hasn't worked since before its introduction into Emacs.  It
+  ;; should either be removed or fixed up.
+  (declare (obsolete nil "29.1"))
   (if (not definition)
       (setq eshell-variable-aliases-list
 	    (delq (assoc var-alias eshell-variable-aliases-list)
-- 
2.25.1


[-- Attachment #4: 0003-Allow-setting-the-values-of-variable-aliases-in-Eshe.patch --]
[-- Type: text/plain, Size: 22059 bytes --]

From d44b146b8bf114970b568ea7d71b5780a99d66d7 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
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"))
 
+\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
 
-- 
2.25.1


[-- Attachment #5: 0004-Improve-handling-of-PATH-in-Eshell-for-remote-direct.patch --]
[-- Type: text/plain, Size: 17848 bytes --]

From d00a37e47107355516497b64a9cec54b0b17cfb4 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Thu, 15 Sep 2022 12:24:37 -0700
Subject: [PATCH 4/5] Improve handling of $PATH in Eshell for remote
 directories

* lisp/eshell/esh-util.el (eshell-path-env, eshell-parse-colon-path):
Make obsolete.
(eshell-host-path-env): New variable.
(eshell-get-path-assq, eshell-set-path): New functions.
(eshell-get-path): Use 'eshell-get-path-assq'.

* lisp/eshell/esh-var.el (eshell-variable-aliases-list): Add entry for
$PATH.
(eshell-var-initialize): Add 'eshell-host-path-env' to
'eshell-subcommand-bindings'.

* lisp/eshell/esh-ext.el (eshell-search-path): Use 'file-name-concat'
instead of 'concat'.
(eshell/addpath): Use 'eshell-get-path' and 'eshell-set-path'.

* lisp/net/tramp-integration.el: Only apply Eshell hooks when
'eshell-host-path-env' is unbound.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/path-var/local-directory)
(esh-var-test/path-var/remote-directory, esh-var-test/path-var/set)
(esh-var-test/path-var/set-locally): New tests.

* test/lisp/eshell/esh-ext-tests.el: New file.

* test/lisp/eshell/eshell-tests-helpers.el
(eshell-tests-remote-accessible-p): New function.

* doc/misc/eshell.texi (Variables): Document $PATH.

* etc/NEWS: Announce this change (bug#57556).
---
 doc/misc/eshell.texi                     |  8 +++
 etc/NEWS                                 |  5 ++
 lisp/eshell/esh-ext.el                   | 25 ++++----
 lisp/eshell/esh-util.el                  | 65 ++++++++++++++++++--
 lisp/eshell/esh-var.el                   | 12 +++-
 lisp/net/tramp-integration.el            | 21 ++++---
 test/lisp/eshell/esh-ext-tests.el        | 77 ++++++++++++++++++++++++
 test/lisp/eshell/esh-var-tests.el        | 42 +++++++++++++
 test/lisp/eshell/eshell-tests-helpers.el | 10 +++
 9 files changed, 235 insertions(+), 30 deletions(-)
 create mode 100644 test/lisp/eshell/esh-ext-tests.el

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 48edee59ab..dc1af16fcf 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -939,6 +939,14 @@ Variables
 directory ring via subscripting, e.g.@: @samp{$-[1]} refers to the
 working directory @emph{before} the previous one.
 
+@vindex $PATH
+@item $PATH
+This specifies the directories to search for executable programs as a
+string, separated by @code{":"} for Unix and GNU systems, and
+@code{";"} for MS systems.  This variable is connection-aware, so when
+the current directory on a remote host, it will automatically update
+to reflect the search path on that host.
+
 @vindex $_
 @item $_
 This refers to the last argument of the last command.  With a
diff --git a/etc/NEWS b/etc/NEWS
index 76e44965ba..7d1f9bf355 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -320,6 +320,11 @@ previous 'C-x ='.
 
 ** Eshell
 
+*** Eshell's PATH is now derived from 'exec-path'.
+For consistency with remote connections, Eshell now uses 'exec-path'
+to determine the execution path on the local system, instead of using
+the PATH environment variable directly.
+
 ---
 *** 'source' and '.' no longer accept the '--help' option.
 This is for compatibility with the shell versions of these commands,
diff --git a/lisp/eshell/esh-ext.el b/lisp/eshell/esh-ext.el
index 98902fc6f2..4ec9241c22 100644
--- a/lisp/eshell/esh-ext.el
+++ b/lisp/eshell/esh-ext.el
@@ -77,7 +77,7 @@ eshell-search-path
     (let ((list (eshell-get-path))
 	  suffixes n1 n2 file)
       (while list
-	(setq n1 (concat (car list) name))
+	(setq n1 (file-name-concat (car list) name))
 	(setq suffixes eshell-binary-suffixes)
 	(while suffixes
 	  (setq n2 (concat n1 (car suffixes)))
@@ -231,6 +231,8 @@ eshell-external-command
       (eshell-gather-process-output
        (car interp) (append (cdr interp) args)))))
 
+(defvar eshell-in-subcommand-p)         ; Defined in esh-cmd.el.
+
 (defun eshell/addpath (&rest args)
   "Add a set of paths to PATH."
   (eshell-eval-using-options
@@ -239,17 +241,16 @@ eshell/addpath
      (?h "help" nil nil  "display this usage message")
      :usage "[-b] PATH
 Adds the given PATH to $PATH.")
-   (if args
-       (progn
-	 (setq eshell-path-env (getenv "PATH")
-	       args (mapconcat #'identity args path-separator)
-	       eshell-path-env
-	       (if prepend
-		   (concat args path-separator eshell-path-env)
-		 (concat eshell-path-env path-separator args)))
-	 (setenv "PATH" eshell-path-env))
-     (dolist (dir (parse-colon-path (getenv "PATH")))
-       (eshell-printn dir)))))
+   (let ((path (eshell-get-path t)))
+     (if args
+         (progn
+           (setq path (if prepend
+                          (append args path)
+                        (append path args)))
+           (eshell-set-path path eshell-in-subcommand-p)
+           (string-join path (path-separator)))
+       (dolist (dir path)
+         (eshell-printn dir))))))
 
 (put 'eshell/addpath 'eshell-no-numeric-conversions t)
 (put 'eshell/addpath 'eshell-filename-arguments t)
diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el
index 9258ca5e40..d7cb79830b 100644
--- a/lisp/eshell/esh-util.el
+++ b/lisp/eshell/esh-util.el
@@ -249,17 +249,70 @@ eshell-path-env
 It might be different from \(getenv \"PATH\"), when
 `default-directory' points to a remote host.")
 
-(defun eshell-get-path ()
+(make-obsolete-variable 'eshell-path-env 'eshell-get-path "29.1")
+
+(defvar-local eshell-host-path-env nil
+  "An alist mapping local and remote hosts to their `exec-path' values.
+These can be retrieved via `eshell-get-path' or updated via
+`eshell-set-path'.")
+
+(defun eshell-get-path-assq (host &optional initialize copy-p)
+  "Return the path association for HOST from `eshell-host-path-env'.
+If HOST is nil, use `localhost'.  If the association already
+exists, just return it; if COPY-P is non-nil, push a copy of the
+association onto the list and return that.  This is useful when
+temporarily altering the path.
+
+If the association doesn't exisst and INITIALIZE is non-nil,
+initialize it from `exec-path' first."
+  (if-let ((host-id (intern (or host "localhost")))
+           (cached-path (assq host-id eshell-host-path-env)))
+      (if copy-p
+          (car (push (copy-tree cached-path) eshell-host-path-env))
+        cached-path)
+    ;; If not already cached, get the path from `exec-path', removing
+    ;; the last element, which is `exec-directory'.
+    (car (push (cons host-id (when initialize (butlast (exec-path))))
+               eshell-host-path-env))))
+
+(defun eshell-get-path (&optional local-part)
   "Return $PATH as a list.
-Add the current directory on MS-Windows."
-  (eshell-parse-colon-path
-   (if (eshell-under-windows-p)
-       (concat "." path-separator eshell-path-env)
-     eshell-path-env)))
+If LOCAL-PART is non-nil, only return the local part of the path.
+Otherwise, return the full, possibly-remote path.
+
+On MS-Windows, add the current directory as the first directory
+in the path."
+  (let* ((remote (file-remote-p default-directory))
+         (path (cdr (eshell-get-path-assq remote t))))
+    (when (and (eshell-under-windows-p)
+               (not remote))
+      (push "." path))
+    (if (and remote (not local-part))
+        (mapcar (lambda (x) (concat remote x)) path)
+      path)))
+
+(defun eshell-set-path (path &optional copy-p)
+  "Set the Eshell $PATH to PATH.
+PATH can be either a list of directories or a string of
+directories separated by `path-separator'.
+
+If COPY-P is non-nil, set this as a new entry in
+`eshell-host-path-env'.  This is useful for temporarily altering
+the path."
+  (let* ((remote (file-remote-p default-directory))
+         (path-entry (eshell-get-path-assq remote nil copy-p)))
+    (setcdr path-entry
+            (if (listp path)
+                path
+              ;; Don't use `parse-colon-path' here, since we don't
+              ;; want the additonal translations it does on each
+              ;; element.
+              (split-string path (path-separator))))))
 
 (defun eshell-parse-colon-path (path-env)
   "Split string with `parse-colon-path'.
 Prepend remote identification of `default-directory', if any."
+  (declare (obsolete nil "29.1"))
   (let ((remote (file-remote-p default-directory)))
     (if remote
 	(mapcar
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index caf143e1a1..8a27ab747d 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -156,7 +156,14 @@ eshell-variable-aliases-list
     ("LINES" ,(lambda () (window-body-height nil 'remap)) t t)
     ("INSIDE_EMACS" eshell-inside-emacs t)
 
-    ;; for eshell-cmd.el
+    ;; for esh-ext.el
+    ("PATH" (,(lambda () (string-join (eshell-get-path t) (path-separator)))
+             . ,(lambda (_ value)
+                  (eshell-set-path value eshell-in-subcommand-p)
+                  value))
+     t t)
+
+    ;; for esh-cmd.el
     ("_" ,(lambda (indices quoted)
 	    (if (not indices)
 	        (car (last eshell-last-arguments))
@@ -249,7 +256,8 @@ eshell-var-initialize
   (setq-local eshell-subcommand-bindings
               (append
                '((process-environment (eshell-copy-environment))
-                 (eshell-variable-aliases-list eshell-variable-aliases-list))
+                 (eshell-variable-aliases-list eshell-variable-aliases-list)
+                 (eshell-host-path-env eshell-host-path-env))
                eshell-subcommand-bindings))
 
   (setq-local eshell-special-chars-inside-quoting
diff --git a/lisp/net/tramp-integration.el b/lisp/net/tramp-integration.el
index 35c0636b1c..b054c7c7d0 100644
--- a/lisp/net/tramp-integration.el
+++ b/lisp/net/tramp-integration.el
@@ -136,16 +136,17 @@ tramp-eshell-directory-change
           (getenv "PATH"))))
 
 (with-eval-after-load 'esh-util
-  (add-hook 'eshell-mode-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'eshell-directory-change-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'tramp-integration-unload-hook
-	    (lambda ()
-	      (remove-hook 'eshell-mode-hook
-			   #'tramp-eshell-directory-change)
-	      (remove-hook 'eshell-directory-change-hook
-			   #'tramp-eshell-directory-change))))
+  (unless (boundp 'eshell-host-path-env)
+    (add-hook 'eshell-mode-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'eshell-directory-change-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'tramp-integration-unload-hook
+	      (lambda ()
+	        (remove-hook 'eshell-mode-hook
+			     #'tramp-eshell-directory-change)
+	        (remove-hook 'eshell-directory-change-hook
+			     #'tramp-eshell-directory-change)))))
 
 ;;; Integration of recentf.el:
 
diff --git a/test/lisp/eshell/esh-ext-tests.el b/test/lisp/eshell/esh-ext-tests.el
new file mode 100644
index 0000000000..cf0910d797
--- /dev/null
+++ b/test/lisp/eshell/esh-ext-tests.el
@@ -0,0 +1,77 @@
+;;; esh-ext-tests.el --- esh-ext test suite  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's external command handling.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'esh-ext)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+         (expand-file-name "eshell-tests-helpers"
+                           (file-name-directory (or load-file-name
+                                                    default-directory))))
+
+;;; Tests:
+
+(ert-deftest esh-ext-test/addpath/end ()
+  "Test that \"addpath\" adds paths to the end of $PATH."
+  (with-temp-eshell
+   (let ((eshell-host-path-env '((localhost . ("/some/path" "/other/path"))))
+         (expected-path (string-join '("/some/path" "/other/path" "/new/path"
+                                       "/new/path2")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/begin ()
+  "Test that \"addpath -b\" adds paths to the beginning of $PATH."
+  (with-temp-eshell
+   (let ((eshell-host-path-env '((localhost . ("/some/path" "/other/path"))))
+         (expected-path (string-join '("/new/path" "/new/path2" "/some/path"
+                                       "/other/path")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath -b /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/set-locally ()
+  "Test adding to the path temporarily in a subcommand."
+  (let* ((original-path-list '("/some/path" "/other/path"))
+         (eshell-host-path-env `((localhost . ,original-path-list)))
+         (original-path (string-join original-path-list (path-separator)))
+         (local-path (string-join (append original-path-list '("/new/path"))
+                                  (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output
+      "{ addpath /new/path; env }"
+      (format "PATH=%s\n" (regexp-quote local-path)))
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH"
+                                  (concat original-path "\n")))))
+
+;; esh-ext-tests.el ends here
diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
index a7ac52ed24..6f4b0b9994 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -23,6 +23,7 @@
 
 ;;; Code:
 
+(require 'tramp)
 (require 'ert)
 (require 'esh-mode)
 (require 'esh-var)
@@ -610,6 +611,47 @@ esh-var-test/inside-emacs-var-split-indices
    (eshell-match-command-output "echo $INSIDE_EMACS[, 1]"
                                 "eshell")))
 
+(ert-deftest esh-var-test/path-var/local-directory ()
+  "Test using $PATH in a local directory."
+  (let ((expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path))
+     (should (equal (mapcar #'car eshell-host-path-env)
+                    '(localhost))))))
+
+(ert-deftest esh-var-test/path-var/remote-directory ()
+  "Test using $PATH in a remote directory."
+  (skip-unless (eshell-tests-remote-accessible-p))
+  (let* ((default-directory ert-remote-temporary-file-directory)
+         (expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path))
+     (should (equal (mapcar #'car eshell-host-path-env)
+                    (list (intern (file-remote-p default-directory))))))))
+
+(ert-deftest esh-var-test/path-var/set ()
+  "Test setting $PATH."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
+(ert-deftest esh-var-test/path-var/set-locally ()
+  "Test setting $PATH temporarily for a single command."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "PATH=/local/path env"
+                                  "PATH=/local/path\n")
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
 (ert-deftest esh-var-test/last-status-var-lisp-command ()
   "Test using the \"last exit status\" ($?) variable with a Lisp command"
   (with-temp-eshell
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index e713e162ad..0067216f70 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -36,6 +36,16 @@ eshell-test--max-subprocess-time
   "The maximum amount of time to wait for a subprocess to finish, in seconds.
 See `eshell-wait-for-subprocess'.")
 
+(defun eshell-tests-remote-accessible-p ()
+  "Return if a test involving remote files can proceed.
+If using this function, be sure to load `tramp' near the
+beginning of the test file."
+  (ignore-errors
+    (and
+     (file-remote-p ert-remote-temporary-file-directory)
+     (file-directory-p ert-remote-temporary-file-directory)
+     (file-writable-p ert-remote-temporary-file-directory))))
+
 (defmacro with-temp-eshell (&rest body)
   "Evaluate BODY in a temporary Eshell buffer."
   `(save-current-buffer
-- 
2.25.1


[-- Attachment #6: 0005-Print-the-correct-PATH-when-Eshell-s-which-fails-to-.patch --]
[-- Type: text/plain, Size: 1077 bytes --]

From 94a5b957ae84d14eff17204a0df751ec5a973923 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Thu, 15 Sep 2022 12:32:02 -0700
Subject: [PATCH 5/5] Print the correct $PATH when Eshell's 'which' fails to
 find a command

* lisp/eshell/esh-cmd.el (eshell/which): Use 'eshell-get-path'
(bug#20008).
---
 lisp/eshell/esh-cmd.el | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index 9a56b56458..0151bec0a2 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -1275,8 +1275,9 @@ eshell/which
                         name)
                   (eshell-search-path name)))))
       (if (not program)
-	  (eshell-error (format "which: no %s in (%s)\n"
-				name (getenv "PATH")))
+          (eshell-error (format "which: no %s in (%s)\n"
+                                name (string-join (eshell-get-path t)
+                                                  path-separator)))
 	(eshell-printn program)))))
 
 (put 'eshell/which 'eshell-no-numeric-conversions t)
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-09-30  3:54           ` Jim Porter
@ 2022-10-01 20:25             ` Michael Albinus
  2022-10-01 22:02               ` Jim Porter
  0 siblings, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-10-01 20:25 UTC (permalink / raw)
  To: Jim Porter; +Cc: coltonlewis, 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

I didn't install your patches, but I gave them a cursory review.

> Patch #3: Allow setting variable aliases
> ----------------------------------------
>
> Since the plan is to make $PATH into a variable alias so that Eshell
> can do the right thing when changing directories to a different host,
> I wanted to be sure users can *set* variable aliases so that updating
> $PATH will be easy. This adds the ability to do that, along with a new
> "set" command in Eshell. That lets you set either environment
> variables or Lisp variables (note that "#'" is just Eshell's way of
> spelling "'", since a single-quote is used for literal strings in
> Eshell):
>
>   set ENV_VAR value
>   set #'lisp-var value

Well, in Elisp the #'symbol read syntax is used for function names, see
(info "(elisp) Special Read Syntax")

So it is surprising to see it used for variable names.

> Patch #4: Make $PATH a variable alias
> ----------------------------------------
>
> This stores the $PATH in an alist indexed by host, similar to
> 'grep-host-defaults-alist'. For consistency, it now derives its value
> from '(exec-path)' everywhere (formerly, it used '(getenv "PATH") for
> local hosts and '(exec-path)' for Tramp).

Again, no possibility to use connection-local variables? You use them
already by calling (path-separator) ...

Personally I believe 'grep-host-defaults-alist' shall also be changed to
a connection-local mechanism, but likely, this would break too much code
in the wild.

> -(defun eshell-get-path ()
> +(make-obsolete-variable 'eshell-path-env 'eshell-get-path "29.1")

I guess you mean 'eshell-host-path-env' as CURRENT-NAME.

> +(defun eshell-get-path (&optional local-part)
> +  (let* ((remote (file-remote-p default-directory))
> +         (path (cdr (eshell-get-path-assq remote t))))
> +    (when (and (eshell-under-windows-p)
> +               (not remote))
> +      (push "." path))
> +    (if (and remote (not local-part))
> +        (mapcar (lambda (x) (concat remote x)) path)

Why not file-name-concat?

Otherwise, I'd say let's install the patch, and see how it goes. There
isn't too much time left until the feature freeze in November.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-01 20:25             ` Michael Albinus
@ 2022-10-01 22:02               ` Jim Porter
  2022-10-02  5:34                 ` Jim Porter
  2022-10-02  8:55                 ` Michael Albinus
  0 siblings, 2 replies; 32+ messages in thread
From: Jim Porter @ 2022-10-01 22:02 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 57556, coltonlewis

On 10/1/2022 1:25 PM, Michael Albinus wrote:
> Jim Porter <jporterbugs@gmail.com> writes:
> 
> I didn't install your patches, but I gave them a cursory review.

Thanks for taking a look.

>>    set ENV_VAR value
>>    set #'lisp-var value
> 
> Well, in Elisp the #'symbol read syntax is used for function names, see
> (info "(elisp) Special Read Syntax")
> 
> So it is surprising to see it used for variable names.

Yeah, it took me quite a while to realize that Eshell's meaning of 
#'symbol is different from Elisp's. I recently added some documentation 
in the Eshell manual to (hopefully) clarify the potential confusion.

>> Patch #4: Make $PATH a variable alias
>> ----------------------------------------
>>
>> This stores the $PATH in an alist indexed by host, similar to
>> 'grep-host-defaults-alist'. For consistency, it now derives its value
>> from '(exec-path)' everywhere (formerly, it used '(getenv "PATH") for
>> local hosts and '(exec-path)' for Tramp).
> 
> Again, no possibility to use connection-local variables? You use them
> already by calling (path-separator) ...

I'll take a look at doing that. As I understand it, connection-local 
variables are cleared if the associated connection gets cleaned up, 
right? I wonder if that would be the right thing to do. For example, if 
I cd into a remote host in Eshell, then update Eshell's $PATH for that 
host, then clean up the connection, should the $PATH be reset to the 
default for that host? I'm really not sure...

>> -(defun eshell-get-path ()
>> +(make-obsolete-variable 'eshell-path-env 'eshell-get-path "29.1")
> 
> I guess you mean 'eshell-host-path-env' as CURRENT-NAME.

I wanted to say, "Instead of using the variable 'eshell-path-env', call 
the function 'eshell-get-path'." Maybe that's not the right way to 
indicate that though.

>> +(defun eshell-get-path (&optional local-part)
>> +  (let* ((remote (file-remote-p default-directory))
>> +         (path (cdr (eshell-get-path-assq remote t))))
>> +    (when (and (eshell-under-windows-p)
>> +               (not remote))
>> +      (push "." path))
>> +    (if (and remote (not local-part))
>> +        (mapcar (lambda (x) (concat remote x)) path)
> 
> Why not file-name-concat?

Good point. I'd forgotten to update that when copying that bit out of 
'eshell-parse-colon-path'.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-01 22:02               ` Jim Porter
@ 2022-10-02  5:34                 ` Jim Porter
  2022-10-02  8:48                   ` Michael Albinus
  2022-10-02  8:55                 ` Michael Albinus
  1 sibling, 1 reply; 32+ messages in thread
From: Jim Porter @ 2022-10-02  5:34 UTC (permalink / raw)
  To: Michael Albinus; +Cc: coltonlewis, 57556

On 10/1/2022 3:02 PM, Jim Porter wrote:
> On 10/1/2022 1:25 PM, Michael Albinus wrote:
>> Again, no possibility to use connection-local variables? You use them
>> already by calling (path-separator) ...
> 
> I'll take a look at doing that. As I understand it, connection-local 
> variables are cleared if the associated connection gets cleaned up, 
> right? I wonder if that would be the right thing to do. For example, if 
> I cd into a remote host in Eshell, then update Eshell's $PATH for that 
> host, then clean up the connection, should the $PATH be reset to the 
> default for that host? I'm really not sure...

After a bit of trying, I wasn't able to get this to work. I was doing 
something along these lines:

   (defvar-local eshell-path-env-list nil)

   (connection-local-set-profile-variables
    'eshell-connection-local-profile
    '((eshell-path-env-list . nil)))

   (connection-local-set-profiles
    nil 'eshell-connection-local-profile)

   ;; When getting the $PATH...
   (with-connection-local-variables
    (or eshell-path-env-list
        (setq eshell-path-env-list (butlast (exec-path)))))

However, the next time I try to get the $PATH in the 
'with-connection-local-variables' block, the value is nil again, so it 
gets recomputed. I guess 'setq' inside 'with-connection-local-variables' 
doesn't work?

This is also made more complex by the fact that in an Eshell subcommand, 
we want to be able to modify the $PATH locally so that it's reset to the 
previous value after the subcommand. Since there are so many different 
things that can alter the $PATH value, making it connection-local seemed 
to be more complex in my experiments. The alist implementation is a bit 
more primitive, but I found it easier to reason through the logic. That 
could just be because I don't quite understand all the details of 
connection-local variables though.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-02  5:34                 ` Jim Porter
@ 2022-10-02  8:48                   ` Michael Albinus
  2022-10-07  3:19                     ` Jim Porter
  0 siblings, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-10-02  8:48 UTC (permalink / raw)
  To: Jim Porter; +Cc: coltonlewis, 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

> On 10/1/2022 3:02 PM, Jim Porter wrote:
>> On 10/1/2022 1:25 PM, Michael Albinus wrote:
>>> Again, no possibility to use connection-local variables? You use them
>>> already by calling (path-separator) ...
>> I'll take a look at doing that. As I understand it, connection-local
>> variables are cleared if the associated connection gets cleaned up,
>> right? I wonder if that would be the right thing to do. For example,
>> if I cd into a remote host in Eshell, then update Eshell's $PATH for
>> that host, then clean up the connection, should the $PATH be reset
>> to the default for that host? I'm really not sure...
>
> After a bit of trying, I wasn't able to get this to work. I was doing
> something along these lines:
>
>   (defvar-local eshell-path-env-list nil)
>
>   (connection-local-set-profile-variables
>    'eshell-connection-local-profile
>    '((eshell-path-env-list . nil)))
>
>   (connection-local-set-profiles
>    nil 'eshell-connection-local-profile)
>
>   ;; When getting the $PATH...
>   (with-connection-local-variables
>    (or eshell-path-env-list
>        (setq eshell-path-env-list (butlast (exec-path)))))
>
> However, the next time I try to get the $PATH in the
> 'with-connection-local-variables' block, the value is nil again, so it
> gets recomputed. I guess 'setq' inside
> 'with-connection-local-variables' doesn't work?

Yes. with-connection-local-variables is designed to provide those
variables inside the BODY only. And you have used nil as CRITERIA in
connection-local-set-profiles, which means you get the same variables
for every kind of default-directory, which means you don't use
connection-local variables at all :-)

What you need is a permanent setting of variables. Something like

(connection-local-set-profiles
 (connection-local-criteria-for-default-directory 'eshell)
 'eshell-connection-local-profile)

(let ((enable-connection-local-variables t)
      connection-local-variables-alist) ;; I'm not sure this is needed.
  (hack-connection-local-variables-apply
   (connection-local-criteria-for-default-directory 'eshell))
  ;; The body.
  ...)

I have used `eshell' as APPLICATION, the default application would be
`tramp'. But since you care only your eshell-* variables, you could use
an own namespace I believe. Of course, you could also use `tramp' or
anything else, it's your decision.

> This is also made more complex by the fact that in an Eshell
> subcommand, we want to be able to modify the $PATH locally so that
> it's reset to the previous value after the subcommand.

In this case, you could use the with-connection-local-variables macro
indeed. Something like

(let ((connection-local-default-application 'eshell))
  (with-connection-local-variables
  ;; Some temprary modifications.
  ...)

> Since there are so many different things that can alter the $PATH
> value, making it connection-local seemed to be more complex in my
> experiments. The alist implementation is a bit more primitive, but I
> found it easier to reason through the logic. That could just be
> because I don't quite understand all the details of connection-local
> variables though.

It is more complex to set it up, indeed. (I wish we would have made it
more friendly.) But in the long term, it will be more robust I believe.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-01 22:02               ` Jim Porter
  2022-10-02  5:34                 ` Jim Porter
@ 2022-10-02  8:55                 ` Michael Albinus
  1 sibling, 0 replies; 32+ messages in thread
From: Michael Albinus @ 2022-10-02  8:55 UTC (permalink / raw)
  To: Jim Porter; +Cc: 57556, coltonlewis

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

> I'll take a look at doing that. As I understand it, connection-local
> variables are cleared if the associated connection gets cleaned up,
> right? I wonder if that would be the right thing to do. For example,
> if I cd into a remote host in Eshell, then update Eshell's $PATH for
> that host, then clean up the connection, should the $PATH be reset to
> the default for that host? I'm really not sure...

Connection-local variables have their own meaning, they are not bound to
Tramp connections. Tramp is just one application using them (but perhaps
the major one).

Even using default-directory as indicator is not fixed. It happens, if
you use connection-local-criteria-for-default-directory as CRITERIA here
and there. But you can compose your own CRITERIA if you like.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-02  8:48                   ` Michael Albinus
@ 2022-10-07  3:19                     ` Jim Porter
  2022-10-07 18:28                       ` Michael Albinus
  0 siblings, 1 reply; 32+ messages in thread
From: Jim Porter @ 2022-10-07  3:19 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 57556

[-- Attachment #1: Type: text/plain, Size: 1556 bytes --]

On 10/2/2022 1:48 AM, Michael Albinus wrote:
> Yes. with-connection-local-variables is designed to provide those
> variables inside the BODY only. And you have used nil as CRITERIA in
> connection-local-set-profiles, which means you get the same variables
> for every kind of default-directory, which means you don't use
> connection-local variables at all :-)
> 
> What you need is a permanent setting of variables. Something like
> 
> (connection-local-set-profiles
>   (connection-local-criteria-for-default-directory 'eshell)
>   'eshell-connection-local-profile)
> 
> (let ((enable-connection-local-variables t)
>        connection-local-variables-alist) ;; I'm not sure this is needed.
>    (hack-connection-local-variables-apply
>     (connection-local-criteria-for-default-directory 'eshell))
>    ;; The body.
>    ...)

Hmm, I've tried this a few different ways, and I haven't been able to 
get it to work the way it should. Maybe I'm just missing something?

Attached is a minimal test case I've extracted to show the issue I'm 
having. It seems the problem is that, while I can update the path in 
'eshell-set-path' with no problem, when I call 'eshell-get-path' again, 
'hack-connection-local-variables-apply' resets 'eshell-path-env-list' to 
nil, so the modified path is lost.

Do you have any ideas about what I'm doing wrong? Or maybe 
connection-local variables aren't supposed to be used this way. All the 
documentation I see on them involves setting variables to constant 
values, not updating them in-place over the life of a program.

[-- Attachment #2: connection-local.el --]
[-- Type: text/plain, Size: 2779 bytes --]

;;; -*- lexical-binding:t -*-

;; Run these tests with:
;;  emacs -Q --batch -l ~/etc/emacs/connection-local.el \
;;        --eval '(ert-run-tests-batch-and-exit t)'

(require 'tramp)
(require 'ert)
(require 'ert-x)

(defvar-local eshell-path-env-list nil)

(connection-local-set-profile-variables
 'eshell-connection-local-profile
 '((eshell-path-env-list . nil)))

(connection-local-set-profiles
 (connection-local-criteria-for-default-directory 'eshell)
 'eshell-connection-local-profile)

(defun eshell-get-path ()
  "Return $PATH as a list."
  (let ((enable-connection-local-variables t)
        connection-local-variables-alist) ;; I'm not sure this is needed.
    (hack-connection-local-variables-apply
     (connection-local-criteria-for-default-directory 'eshell))
    (or eshell-path-env-list
        ;; If not already cached, get the path from `exec-path',
        ;; removing the last element, which is `exec-directory'.
        (setq eshell-path-env-list (butlast (exec-path))))))

(defun eshell-set-path (path)
  "Set the Eshell $PATH to PATH.
PATH can be either a list of directories or a string of
directories separated by `path-separator'."
  (let ((enable-connection-local-variables t)
        connection-local-variables-alist) ;; I'm not sure this is needed.
    (hack-connection-local-variables-apply
     (connection-local-criteria-for-default-directory 'eshell))
    (setq eshell-path-env-list
          (if (listp path)
              path
            ;; Don't use `parse-colon-path' here, since we don't want
            ;; the additonal translations it does on each element.
            (split-string path (path-separator))))))

(ert-deftest esh-var-test/path-var/preserve-across-hosts ()
  "Test that $PATH can be set independently on multiple hosts."
  ;; Test the initial value of the local $PATH.
  (should (equal (eshell-get-path) (butlast (exec-path))))

  ;; Set the local $PATH and make sure it retains the value we set.
  (should (equal (eshell-set-path "/local/path") '("/local/path")))
  (should (equal (eshell-get-path) '("/local/path")))      ; FAIL

  (let ((default-directory ert-remote-temporary-file-directory))
    ;; Test the initial value of the remote $PATH.
    (should (equal (eshell-get-path) (butlast (exec-path))))

    ;; Set the remote $PATH and make sure it retains the value we set.
    (should (equal (eshell-set-path "/remote/path") '("/remote/path")))
    (should (equal (eshell-get-path) '("/remote/path"))))  ; FAIL

  ;; Make sure we get the local $PATH we set above.
  (should (equal (eshell-get-path) '("/local/path")))      ; FAIL

  ;; Make sure we get the remote $PATH we set above.
  (let ((default-directory ert-remote-temporary-file-directory))
    (should (equal (eshell-get-path) '("/remote/path"))))) ; FAIL

^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-07  3:19                     ` Jim Porter
@ 2022-10-07 18:28                       ` Michael Albinus
  2022-10-08 22:09                         ` Jim Porter
  0 siblings, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-10-07 18:28 UTC (permalink / raw)
  To: Jim Porter; +Cc: 57556

[-- Attachment #1: Type: text/plain, Size: 1543 bytes --]

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

Thanks your the example file, it is a pleasure to work on something like this!

> Hmm, I've tried this a few different ways, and I haven't been able to
> get it to work the way it should. Maybe I'm just missing something?
>
> Attached is a minimal test case I've extracted to show the issue I'm
> having. It seems the problem is that, while I can update the path in
> 'eshell-set-path' with no problem, when I call 'eshell-get-path'
> again, 'hack-connection-local-variables-apply' resets
> 'eshell-path-env-list' to nil, so the modified path is lost.
>
> Do you have any ideas about what I'm doing wrong? Or maybe
> connection-local variables aren't supposed to be used this way. All
> the documentation I see on them involves setting variables to constant
> values, not updating them in-place over the life of a program.

Well, your example code has some problems:

- connection-local-criteria-for-default-directory returns nil for a
  local default directory. In order to handle this case, I have added
  eshell-connection-local-criteria-for-default-directory.

- You cannot use the same profile for different criteria, unless you
  intend the same settings. Therefore, I have added
  eshell-connection-local-profile.

- If you change settings in eshell-get-path or eshell-set-path, you must
  tell this to the connection-local variables machinery. I've extended
  both functions accordingly.

Now your ert test passes for me. Changed connection-local.el added.

Best regards, Michael.


[-- Attachment #2: connection-local.el --]
[-- Type: text/plain, Size: 3743 bytes --]

;;; -*- lexical-binding:t -*-

;; Run these tests with:
;;  emacs -Q --batch -l ~/etc/emacs/connection-local.el \
;;        --eval '(ert-run-tests-batch-and-exit t)'

(require 'tramp)
(require 'ert)
(require 'ert-x)

(defvar-local eshell-path-env-list nil)

(defsubst eshell-connection-local-criteria-for-default-directory ()
  (or (connection-local-criteria-for-default-directory 'eshell)
      '(:application eshell :machine local)))

(defsubst eshell-connection-local-profile ()
  (intern (concat "eshell-connection-local-profile-"
		  (or (file-remote-p default-directory) "local"))))

;; Initial values.
(connection-local-set-profile-variables
 'eshell-connection-local-profile
 '((eshell-path-env-list . nil)))

(connection-local-set-profiles
 '(:application eshell)
 'eshell-connection-local-profile)

(defun eshell-get-path ()
  "Return $PATH as a list."
  (let ((enable-connection-local-variables t)
        connection-local-variables-alist) ;; I'm not sure this is needed.
    (hack-connection-local-variables-apply
     (eshell-connection-local-criteria-for-default-directory))
    (prog1
	(or eshell-path-env-list
            ;; If not already cached, get the path from `exec-path',
            ;; removing the last element, which is `exec-directory'.
            (setq eshell-path-env-list (butlast (exec-path))))
      ;; Set connection-local-variable.
      (connection-local-set-profile-variables
       (eshell-connection-local-profile)
       `((eshell-path-env-list . ,eshell-path-env-list)))
      (connection-local-set-profiles
       (eshell-connection-local-criteria-for-default-directory)
       (eshell-connection-local-profile)))))

(defun eshell-set-path (path)
  "Set the Eshell $PATH to PATH.
PATH can be either a list of directories or a string of
directories separated by `path-separator'."
  (let ((enable-connection-local-variables t)
        connection-local-variables-alist) ;; I'm not sure this is needed.
    (hack-connection-local-variables-apply
     (eshell-connection-local-criteria-for-default-directory))
    (prog1
	(setq eshell-path-env-list
              (if (listp path)
		  path
		;; Don't use `parse-colon-path' here, since we don't want
		;; the additonal translations it does on each element.
		(split-string path (path-separator))))
      ;; Set connection-local-variable.
      (connection-local-set-profile-variables
       (eshell-connection-local-profile)
       `((eshell-path-env-list . ,eshell-path-env-list)))
      (connection-local-set-profiles
       (eshell-connection-local-criteria-for-default-directory)
       (eshell-connection-local-profile)))))

(ert-deftest esh-var-test/path-var/preserve-across-hosts ()
  "Test that $PATH can be set independently on multiple hosts."
  ;; Test the initial value of the local $PATH.
  (should (equal (eshell-get-path) (butlast (exec-path))))

  ;; Set the local $PATH and make sure it retains the value we set.
  (should (equal (eshell-set-path "/local/path") '("/local/path")))
  (should (equal (eshell-get-path) '("/local/path")))      ; FAIL

  (let ((default-directory ert-remote-temporary-file-directory))
    ;; Test the initial value of the remote $PATH.
    (should (equal (eshell-get-path) (butlast (exec-path))))

    ;; Set the remote $PATH and make sure it retains the value we set.
    (should (equal (eshell-set-path "/remote/path") '("/remote/path")))
    (should (equal (eshell-get-path) '("/remote/path"))))  ; FAIL

  ;; Make sure we get the local $PATH we set above.
  (should (equal (eshell-get-path) '("/local/path")))      ; FAIL

  ;; Make sure we get the remote $PATH we set above.
  (let ((default-directory ert-remote-temporary-file-directory))
    (should (equal (eshell-get-path) '("/remote/path"))))) ; FAIL

^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-07 18:28                       ` Michael Albinus
@ 2022-10-08 22:09                         ` Jim Porter
  2022-10-09 18:01                           ` Michael Albinus
  2022-10-10  9:15                           ` Michael Albinus
  0 siblings, 2 replies; 32+ messages in thread
From: Jim Porter @ 2022-10-08 22:09 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 57556

[-- Attachment #1: Type: text/plain, Size: 538 bytes --]

On 10/7/2022 11:28 AM, Michael Albinus wrote:
> Now your ert test passes for me. Changed connection-local.el added.

Thanks, this makes connection-local variables a lot clearer to me. Since 
this seems pretty tricky to get right for people who don't know much 
about connection-local variables, maybe it would make sense to add some 
helper functions for anyone who wants to do something similar?

I attached an updated version of my connection-local.el that tries to 
pull out the additions you made into some helpers. What do you think?

[-- Attachment #2: connection-local.el --]
[-- Type: text/plain, Size: 5509 bytes --]

;;; -*- lexical-binding:t -*-

;; Run these tests with:
;;  emacs -Q --batch -l ~/etc/emacs/connection-local.el \
;;        --eval '(ert-run-tests-batch-and-exit t)'

(require 'tramp)
(require 'ert)
(require 'ert-x)

;;; Possible enhancements to connection-local variables:

(defvar connection-local-criteria nil
  "The current connection-local criteria, or nil.
This is set while executing the body of
`with-connection-local-application-variables'.")

(defun connection-local-profile-name ()
  "Get a connection-local profile name.

This allows `connection-local-setq' to use this profile name when
setting variables connection-locally. In theory, a user of these
functions could locally override this function if they wanted to
change the naming scheme."
  (when connection-local-criteria
    (intern (concat
             "auto-connection-local-profile/"
             (symbol-name (plist-get connection-local-criteria :application))
             "/" (or (file-remote-p default-directory) "local")))))

(defmacro with-connection-local-application-variables (application &rest body)
  "Apply connection-local variables for APPLICATION in `default-directory'.
Execute BODY, and unwind connection-local variables.

This is just `with-connection-local-variables', plus the ability
to set an application."
  (declare (debug t) (indent 1))
  `(with-connection-local-application-variables-1
    ,application (lambda () ,@body)))

(defun with-connection-local-application-variables-1 (application body-fun)
  "Apply connection-local variables for APPLICATION in `default-directory'.
Call BODY-FUN with no args, and then unwind connection-local variables."
  (if (file-remote-p default-directory)
      (let ((enable-connection-local-variables t)
            (connection-local-criteria
             (connection-local-criteria-for-default-directory application))
            (old-buffer-local-variables (buffer-local-variables))
	    connection-local-variables-alist)
	(hack-connection-local-variables-apply connection-local-criteria)
	(unwind-protect
            (funcall body-fun)
	  ;; Cleanup.
	  (dolist (variable connection-local-variables-alist)
	    (let ((elt (assq (car variable) old-buffer-local-variables)))
	      (if elt
		  (set (make-local-variable (car elt)) (cdr elt))
		(kill-local-variable (car variable)))))))
    ;; No connection-local variables to apply.
    (funcall body-fun)))

(defmacro connection-local-setq (&rest pairs)
  "Set variables in PAIRS connection-locally.
If there's no connection-local profile to use, just set the
variables as normal.

\(fn [VARIABLE VALUE]...)"
  (let ((set-expr nil)
        (profile-vars nil))
    (while pairs
      (unless (symbolp (car pairs))
        (error "Attempting to set a non-symbol: %s" (car pairs)))
      (push `(set ',(car pairs) ,(cadr pairs)) set-expr)
      (push `(cons ',(car pairs) ,(car pairs)) profile-vars)
      (setq pairs (cddr pairs)))
    `(prog1
         ,(macroexp-progn (nreverse set-expr))
       (when-let ((profile-name (connection-local-profile-name)))
         (connection-local-set-profile-variables
          profile-name
          (list ,@(nreverse profile-vars)))
         (connection-local-set-profiles
          connection-local-criteria
          profile-name)))))

;;; Eshell code:

(defvar-local eshell-path-env-list nil)

;; Initial values.
(connection-local-set-profile-variables
 'eshell-connection-local-profile
 '((eshell-path-env-list . nil)))

(connection-local-set-profiles
 '(:application eshell)
 'eshell-connection-local-profile)

(defun eshell-get-path ()
  "Return $PATH as a list."
  (with-connection-local-application-variables 'eshell
    (or eshell-path-env-list
        ;; If not already cached, get the path from `exec-path',
        ;; removing the last element, which is `exec-directory'.
        (connection-local-setq eshell-path-env-list (butlast (exec-path))))))

(defun eshell-set-path (path)
  "Set the Eshell $PATH to PATH.
PATH can be either a list of directories or a string of
directories separated by `path-separator'."
  (with-connection-local-application-variables 'eshell
    (connection-local-setq
     eshell-path-env-list
     (if (listp path)
	 path
       ;; Don't use `parse-colon-path' here, since we don't want
       ;; the additonal translations it does on each element.
       (split-string path (path-separator))))))

;;; Eshell tests:

(ert-deftest esh-var-test/path-var/preserve-across-hosts ()
  "Test that $PATH can be set independently on multiple hosts."
  ;; Test the initial value of the local $PATH.
  (should (equal (eshell-get-path) (butlast (exec-path))))

  ;; Set the local $PATH and make sure it retains the value we set.
  (should (equal (eshell-set-path "/local/path") '("/local/path")))
  (should (equal (eshell-get-path) '("/local/path")))      ; FAIL

  (let ((default-directory ert-remote-temporary-file-directory))
    ;; Test the initial value of the remote $PATH.
    (should (equal (eshell-get-path) (butlast (exec-path))))

    ;; Set the remote $PATH and make sure it retains the value we set.
    (should (equal (eshell-set-path "/remote/path") '("/remote/path")))
    (should (equal (eshell-get-path) '("/remote/path"))))  ; FAIL

  ;; Make sure we get the local $PATH we set above.
  (should (equal (eshell-get-path) '("/local/path")))      ; FAIL

  ;; Make sure we get the remote $PATH we set above.
  (let ((default-directory ert-remote-temporary-file-directory))
    (should (equal (eshell-get-path) '("/remote/path"))))) ; FAIL

^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-08 22:09                         ` Jim Porter
@ 2022-10-09 18:01                           ` Michael Albinus
  2022-10-13  4:11                             ` Jim Porter
  2022-10-10  9:15                           ` Michael Albinus
  1 sibling, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-10-09 18:01 UTC (permalink / raw)
  To: Jim Porter; +Cc: 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

> Thanks, this makes connection-local variables a lot clearer to
> me. Since this seems pretty tricky to get right for people who don't
> know much about connection-local variables, maybe it would make sense
> to add some helper functions for anyone who wants to do something
> similar?
>
> I attached an updated version of my connection-local.el that tries to
> pull out the additions you made into some helpers. What do you think?

I gave it a short reading, and in general it looks OK (comments
below). Do you want to provide a patch for files-x.el with this?

This patch shall also extent the "Connection Local Variables" section of
the Elisp manual. This section is already quite
long (~150 lines), and speaks almost about static setting of
connection-local variables. You bring dynamic settings here, maybe a
subsection would help to structure. And feel free to restructure the
other, long text if you believe it would help.

Add an example.

Btw, do you have write access to the Emacs git repo? If not I recommend
to ask the maintainers (Eli or Lars) for this.

> (defun connection-local-profile-name ()
>   "Get a connection-local profile name.
>
> This allows `connection-local-setq' to use this profile name when
> setting variables connection-locally. In theory, a user of these
> functions could locally override this function if they wanted to
> change the naming scheme."

The profile name is derived from default-directory, I would make this
more obvious in the function name and the docstring.

>   (when connection-local-criteria
>     (intern (concat
>              "auto-connection-local-profile/"
>              (symbol-name (plist-get connection-local-criteria :application))
>              "/" (or (file-remote-p default-directory) "local")))))

It is not guaranteed that the :application property exists. See the
docstring of connection-local-criteria-alist: "All properties are
optional ...".

> (defun with-connection-local-application-variables-1 (application body-fun)
>   "Apply connection-local variables for APPLICATION in `default-directory'.
> Call BODY-FUN with no args, and then unwind connection-local variables."

Please say that APPLICATION must be a symbol, and shouldn't be nil (if
this is what you want).

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-08 22:09                         ` Jim Porter
  2022-10-09 18:01                           ` Michael Albinus
@ 2022-10-10  9:15                           ` Michael Albinus
  1 sibling, 0 replies; 32+ messages in thread
From: Michael Albinus @ 2022-10-10  9:15 UTC (permalink / raw)
  To: Jim Porter; +Cc: 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

Thinking about, I believe the following would suffice (untested):

--8<---------------cut here---------------start------------->8---
(defmacro with-connection-local-application-variables (application &rest body)
  "Apply connection-local variables for APPLICATION in `default-directory'.
Execute BODY, and unwind connection-local variables.

This is just `with-connection-local-variables', plus the ability
to set an application."
  (declare (debug t) (indent 1))
  `(let ((connection-local-default-application ,application))
    (with-connection-local-variables-1 (lambda () ,@body))))
--8<---------------cut here---------------end--------------->8---

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-09 18:01                           ` Michael Albinus
@ 2022-10-13  4:11                             ` Jim Porter
  2022-10-13  6:35                               ` Eli Zaretskii
  2022-10-14 12:27                               ` Michael Albinus
  0 siblings, 2 replies; 32+ messages in thread
From: Jim Porter @ 2022-10-13  4:11 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 57556

[-- Attachment #1: Type: text/plain, Size: 1956 bytes --]

On 10/9/2022 11:01 AM, Michael Albinus wrote:
> Jim Porter <jporterbugs@gmail.com> writes:
> 
>> I attached an updated version of my connection-local.el that tries to
>> pull out the additions you made into some helpers. What do you think?
> 
> I gave it a short reading, and in general it looks OK (comments
> below). Do you want to provide a patch for files-x.el with this?

Thanks for taking a look. I've added a separate patch (0002 in this 
series) for adding these functions (with some improvements over the 
little test script we worked on) to files-x.el. (Patch 0001 just fixes 
an issue in the docs/tests where the :application had an extra quote.)

If you think it would be easier to track, I could file a new bug and put 
patches 0001 and 0002 in there, then come back to this bug once that's 
merged. Either way is fine by me.

The other patches in this series are mostly-unchanged from before, 
except for 0006, which now uses the new 'setq-connection-local' macro.

> This patch shall also extent the "Connection Local Variables" section of
> the Elisp manual. This section is already quite
> long (~150 lines), and speaks almost about static setting of
> connection-local variables. You bring dynamic settings here, maybe a
> subsection would help to structure. And feel free to restructure the
> other, long text if you believe it would help.

I added all this to the manual (with an example), and divided the 
Connection Local Variables section into two subsections: one for how to 
initialize profiles and set criteria for them, and another for applying 
the variables. I put the 'setq-connection-local' docs in the second 
section, since it's closely related to 'with-connection-local-variables'.

> Btw, do you have write access to the Emacs git repo? If not I recommend
> to ask the maintainers (Eli or Lars) for this.

I do have commit access to the git repo now, so I'll be able to merge 
these patches on my own once they look good.

[-- Attachment #2: 0001-Remove-over-quoting-of-application-values-in-connect.patch --]
[-- Type: text/plain, Size: 3206 bytes --]

From 5ea6556bc11dc428b3cba5d746c064f652326d1d Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Wed, 12 Oct 2022 11:28:05 -0700
Subject: [PATCH 1/7] ; Remove over-quoting of :application values in
 connection-local variables

* test/lisp/files-x-tests.el (files-x-test--application)
(files-x-test--another-application):
* doc/lispref/variables.texi (Connection Local Variables): Remove
extra quotes.
---
 doc/lispref/variables.texi | 14 +++++++-------
 test/lisp/files-x-tests.el |  4 ++--
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi
index 1d891618da..2a06169b21 100644
--- a/doc/lispref/variables.texi
+++ b/doc/lispref/variables.texi
@@ -2311,13 +2311,13 @@ Connection Local Variables
 @example
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "ssh" :machine "localhost")
+  '(:application tramp :protocol "ssh" :machine "localhost")
   'remote-bash 'remote-null-device)
 @end group
 
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "sudo"
+  '(:application tramp :protocol "sudo"
     :user "root" :machine "localhost")
   'remote-ksh 'remote-null-device)
 @end group
@@ -2329,13 +2329,13 @@ Connection Local Variables
 @example
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "ssh" :machine "localhost")
+  '(:application tramp :protocol "ssh" :machine "localhost")
   'remote-bash)
 @end group
 
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "sudo"
+  '(:application tramp :protocol "sudo"
     :user "root" :machine "localhost")
   'remote-ksh)
 @end group
@@ -2365,7 +2365,7 @@ Connection Local Variables
 @example
 @group
 (hack-connection-local-variables
-  '(:application 'tramp :protocol "ssh" :machine "localhost"))
+  '(:application tramp :protocol "ssh" :machine "localhost"))
 @end group
 
 @group
@@ -2401,7 +2401,7 @@ Connection Local Variables
 
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "ssh" :machine "remotehost")
+  '(:application tramp :protocol "ssh" :machine "remotehost")
   'remote-perl)
 @end group
 
@@ -2429,7 +2429,7 @@ Connection Local Variables
 
 @group
 (connection-local-set-profiles
-  '(:application 'my-app :protocol "ssh" :machine "remotehost")
+  '(:application my-app :protocol "ssh" :machine "remotehost")
   'my-remote-perl)
 @end group
 
diff --git a/test/lisp/files-x-tests.el b/test/lisp/files-x-tests.el
index 7ee2f0c1a6..2f6d0d4a99 100644
--- a/test/lisp/files-x-tests.el
+++ b/test/lisp/files-x-tests.el
@@ -42,9 +42,9 @@ remote-null-device
 (put 'remote-shell-login-switch 'safe-local-variable #'identity)
 (put 'remote-null-device 'safe-local-variable #'identity)
 
-(defconst files-x-test--application '(:application 'my-application))
+(defconst files-x-test--application '(:application my-application))
 (defconst files-x-test--another-application
-  '(:application 'another-application))
+  '(:application another-application))
 (defconst files-x-test--protocol '(:protocol "my-protocol"))
 (defconst files-x-test--user '(:user "my-user"))
 (defconst files-x-test--machine '(:machine "my-machine"))
-- 
2.25.1


[-- Attachment #3: 0002-Add-helpers-to-dynamically-assign-connection-local-v.patch --]
[-- Type: text/plain, Size: 21257 bytes --]

From cbcd2369e50e46bf7f8a2b54fbc85626773d1234 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Tue, 11 Oct 2022 22:11:04 -0700
Subject: [PATCH 2/7] Add helpers to dynamically assign connection-local values

* lisp/files-x.el (connection-local-criteria)
(connection-local-profile-name-for-setq): New variables.
(connection-local-profile-name-for-criteria): New function.
(with-connection-local-variables-1): ... let-bind them here.
(with-connection-local-application-variables, setq-connection-local):
New macros.

* test/lisp/files-x-tests.el: Require 'tramp-integration'
(files-x-test--variable5, remote-lazy-var): New variables.
(files-x-test-hack-connection-local-variables-apply): Expand checks.
(files-x-test-with-connection-local-variables): Remove
'hack-connection-local-variables-apply' check (it belongs in the above
test), and expand some other checks.
(files-x-test--get-lazy-var, files-x-test--set-lazy-var): New
functions.
(files-x-test-setq-connection-local): New test.

* doc/lispref/variables.texi (Connection Local Variables): Split into
two subsections and document the new features.

* etc/NEWS: Announce 'setq-connection-local'.
---
 doc/lispref/variables.texi |  91 ++++++++++++++++++++++-------
 etc/NEWS                   |   7 +++
 lisp/files-x.el            |  86 +++++++++++++++++++++++++--
 test/lisp/files-x-tests.el | 117 ++++++++++++++++++++++++-------------
 4 files changed, 234 insertions(+), 67 deletions(-)

diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi
index 2a06169b21..6734e9b47c 100644
--- a/doc/lispref/variables.texi
+++ b/doc/lispref/variables.texi
@@ -2242,6 +2242,21 @@ Connection Local Variables
 variable settings in buffers with a remote connection.  They are bound
 and set depending on the remote connection a buffer is dedicated to.
 
+@menu
+* Connection Local Profiles::            Storing variable settings to
+                                         apply to connections.
+* Applying Connection Local Variables::  Using connection-local values
+                                         in your code.
+@end menu
+
+@node Connection Local Profiles
+@subsection Connection Local Profiles
+
+  Emacs uses connection-local profiles to store the variable settings
+to apply to particular connections.  You can then associate these with
+remote connections by defining the criteria when they should apply,
+using @code{connection-local-set-profiles}.
+
 @defun connection-local-set-profile-variables profile variables
 This function defines a set of variable settings for the connection
 @var{profile}, which is a symbol.  You can later assign the connection
@@ -2356,6 +2371,13 @@ Connection Local Variables
 list.
 @end deffn
 
+@node Applying Connection Local Variables
+@subsection Applying Connection Local Variables
+
+  When writing connection-aware code, you'll need to collect, and
+possibly apply, any connection-local variables.  There are several
+ways to do this, as described below.
+
 @defun hack-connection-local-variables criteria
 This function collects applicable connection-local variables
 associated with @var{criteria} in
@@ -2384,9 +2406,9 @@ Connection Local Variables
 @var{criteria}, and immediately applies them in the current buffer.
 @end defun
 
-@defmac with-connection-local-variables &rest body
-All connection-local variables, which are specified by
-@code{default-directory}, are applied.
+@defmac with-connection-local-application-variables application &rest body
+Apply all connection-local variables for @code{application}, which are
+specified by @code{default-directory}.
 
 After that, @var{body} is executed, and the connection-local variables
 are unwound.  Example:
@@ -2394,20 +2416,20 @@ Connection Local Variables
 @example
 @group
 (connection-local-set-profile-variables
-  'remote-perl
-  '((perl-command-name . "/usr/local/bin/perl")
+  'my-remote-perl
+  '((perl-command-name . "/usr/local/bin/perl5")
     (perl-command-switch . "-e %s")))
 @end group
 
 @group
 (connection-local-set-profiles
-  '(:application tramp :protocol "ssh" :machine "remotehost")
-  'remote-perl)
+  '(:application my-app :protocol "ssh" :machine "remotehost")
+  'my-remote-perl)
 @end group
 
 @group
 (let ((default-directory "/ssh:remotehost:/working/dir/"))
-  (with-connection-local-variables
+  (with-connection-local-application-variables 'my-app
     do something useful))
 @end group
 @end example
@@ -2416,30 +2438,57 @@ Connection Local Variables
 @defvar connection-local-default-application
 The default application, a symbol, to be applied in
 @code{with-connection-local-variables}.  It defaults to @code{tramp},
-but in case you want to overwrite Tramp's settings temporarily, you
-could let-bind it like
+but you can let-bind it to change the application temporarily.
+
+This variable must not be changed globally.
+@end defvar
+
+@defmac with-connection-local-variables &rest body
+This is equivalent to
+@code{with-connection-local-application-variables}, but uses
+@code{connection-local-default-application} for the application.
+@end defmac
+
+@defmac setq-connection-local [symbol form]@dots{}
+This macro sets each @var{symbol} connection-locally to the result of
+evaluating the corresponding @var{form}, using the connection-local
+profile specified in @code{connection-local-profile-name-for-setq}; if
+the profile name is @code{nil}, this macro will just set the variables
+normally, as with @code{setq}.
+
+For example, you can use this macro in combination with
+@code{with-connection-local-variables} to lazily initialize
+connection-local settings:
 
 @example
 @group
+(defvar my-app-variable nil)
+
 (connection-local-set-profile-variables
-  'my-remote-perl
-  '((perl-command-name . "/usr/local/bin/perl5")
-    (perl-command-switch . "-e %s")))
-@end group
+ 'my-app-connection-default-profile
+ '((my-app-variable . nil)))
 
-@group
 (connection-local-set-profiles
-  '(:application my-app :protocol "ssh" :machine "remotehost")
-  'my-remote-perl)
+ '(:application my-app)
+ 'my-app-connection-default-profile)
 @end group
 
 @group
-(let ((default-directory "/ssh:remotehost:/working/dir/")
-      (connection-local-default-application 'my-app))
-  (with-connection-local-variables
-    do something useful))
+(defun my-app-get-variable ()
+  (with-connection-local-application-variables 'my-app
+    (or my-app-variable
+        (setq-connection-local my-app-variable
+                               do something useful))))
 @end group
 @end example
+@end defmac
+
+@defvar connection-local-profile-name-for-setq
+The connection-local profile name, a symbol, to use when setting
+variables via @code{setq-connection-local}.  This is let-bound in the
+body of @code{with-connection-local-variables}, but you can also
+let-bind it yourself if you'd like to set variables on a different
+profile.
 
 This variable must not be changed globally.
 @end defvar
diff --git a/etc/NEWS b/etc/NEWS
index ca857056fd..a10d2438c8 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -3196,6 +3196,13 @@ TIMEOUT is the idle time after which to deactivate the transient map.
 The default timeout value can be defined by the new variable
 'set-transient-map-timeout'.
 
++++
+** New macro 'setq-connection-local'.
+This allows dynamically setting variable values for a particular
+connection within the body of 'with-connection-local-variables'.  See
+the "(elisp) Connection Local Variables" node in the Lisp Reference
+manual for more information.
+
 +++
 ** 'plist-get', 'plist-put' and 'plist-member' are no longer limited to 'eq'.
 These function now take an optional comparison predicate argument.
diff --git a/lisp/files-x.el b/lisp/files-x.el
index da1e44e250..da1f7a9088 100644
--- a/lisp/files-x.el
+++ b/lisp/files-x.el
@@ -618,6 +618,18 @@ connection-local-criteria-alist
   :group 'tramp
   :version "29.1")
 
+(defvar connection-local-criteria nil
+  "The current connection-local criteria, or nil.
+This is set while executing the body of
+`with-connection-local-variables'.")
+
+(defvar connection-local-profile-name-for-setq nil
+  "The current connection-local profile name, or nil.
+This is the name of the profile to use when setting variables via
+`setq-connection-local'.  Its value is derived from
+`connection-local-criteria' and is set while executing the body
+of `with-connection-local-variables'.")
+
 (defsubst connection-local-normalize-criteria (criteria)
   "Normalize plist CRITERIA according to properties.
 Return a reordered plist."
@@ -736,6 +748,15 @@ connection-local-criteria-for-default-directory
       :user        ,(file-remote-p default-directory 'user)
       :machine     ,(file-remote-p default-directory 'host))))
 
+(defun connection-local-profile-name-for-criteria (criteria)
+  "Get a connection-local profile name based on CRITERIA."
+  (when criteria
+    (let (print-level print-length)
+      (intern (concat
+               "autogenerated-connection-local-profile/"
+               (prin1-to-string
+                (connection-local-normalize-criteria criteria)))))))
+
 ;;;###autoload
 (defmacro with-connection-local-variables (&rest body)
   "Apply connection-local variables according to `default-directory'.
@@ -743,16 +764,28 @@ with-connection-local-variables
   (declare (debug t))
   `(with-connection-local-variables-1 (lambda () ,@body)))
 
+;;;###autoload
+(defmacro with-connection-local-application-variables (application &rest body)
+  "Apply connection-local variables for APPLICATION in `default-directory'.
+Execute BODY, and unwind connection-local variables."
+  (declare (debug t) (indent 1))
+  `(let ((connection-local-default-application ,application))
+     (with-connection-local-variables-1 (lambda () ,@body))))
+
 ;;;###autoload
 (defun with-connection-local-variables-1 (body-fun)
   "Apply connection-local variables according to `default-directory'.
 Call BODY-FUN with no args, and then unwind connection-local variables."
   (if (file-remote-p default-directory)
-      (let ((enable-connection-local-variables t)
-            (old-buffer-local-variables (buffer-local-variables))
-	    connection-local-variables-alist)
-	(hack-connection-local-variables-apply
-	 (connection-local-criteria-for-default-directory))
+      (let* ((enable-connection-local-variables t)
+             (connection-local-criteria
+              (connection-local-criteria-for-default-directory))
+             (connection-local-profile-name-for-setq
+              (connection-local-profile-name-for-criteria
+               connection-local-criteria))
+             (old-buffer-local-variables (buffer-local-variables))
+	     connection-local-variables-alist)
+	(hack-connection-local-variables-apply connection-local-criteria)
 	(unwind-protect
             (funcall body-fun)
 	  ;; Cleanup.
@@ -764,6 +797,49 @@ with-connection-local-variables-1
     ;; No connection-local variables to apply.
     (funcall body-fun)))
 
+;;;###autoload
+(defmacro setq-connection-local (&rest pairs)
+  "Set each VARIABLE connection-locally to VALUE.
+
+When `connection-local-profile-name-for-setq' is set, assign each
+variable's value on that connection profile, and set that profile
+for `connection-local-criteria'.  You can use this in combination
+with `with-connection-local-variables', as in
+
+  (with-connection-local-variables
+    (setq-connection-local VARIABLE VALUE))
+
+If there's no connection-local profile to use, just set the
+variables normally, as with `setq'.
+
+The variables are literal symbols and should not be quoted.  The
+second VALUE is not computed until after the first VARIABLE is
+set, and so on; each VALUE can use the new value of variables set
+earlier in the `setq-connection-local'.  The return value of the
+`setq-connection-local' form is the value of the last VALUE.
+
+\(fn [VARIABLE VALUE]...)"
+  (declare (debug setq))
+  (unless (zerop (mod (length pairs) 2))
+    (error "PAIRS must have an even number of variable/value members"))
+  (let ((set-expr nil)
+        (profile-vars nil))
+    (while pairs
+      (unless (symbolp (car pairs))
+        (error "Attempting to set a non-symbol: %s" (car pairs)))
+      (push `(set ',(car pairs) ,(cadr pairs)) set-expr)
+      (push `(cons ',(car pairs) ,(car pairs)) profile-vars)
+      (setq pairs (cddr pairs)))
+    `(prog1
+         ,(macroexp-progn (nreverse set-expr))
+       (when connection-local-profile-name-for-setq
+         (connection-local-set-profile-variables
+          connection-local-profile-name-for-setq
+          (list ,@(nreverse profile-vars)))
+         (connection-local-set-profiles
+          connection-local-criteria
+          connection-local-profile-name-for-setq)))))
+
 ;;;###autoload
 (defun path-separator ()
   "The connection-local value of `path-separator'."
diff --git a/test/lisp/files-x-tests.el b/test/lisp/files-x-tests.el
index 2f6d0d4a99..9499c951c5 100644
--- a/test/lisp/files-x-tests.el
+++ b/test/lisp/files-x-tests.el
@@ -23,6 +23,7 @@
 
 (require 'ert)
 (require 'files-x)
+(require 'tramp-integration)
 
 (defconst files-x-test--variables1
   '((remote-shell-file-name . "/bin/bash")
@@ -35,7 +36,10 @@ files-x-test--variables3
   '((remote-null-device . "/dev/null")))
 (defconst files-x-test--variables4
   '((remote-null-device . "null")))
+(defconst files-x-test--variables5
+  '((remote-lazy-var . nil)))
 (defvar remote-null-device)
+(defvar remote-lazy-var nil)
 (put 'remote-shell-file-name 'safe-local-variable #'identity)
 (put 'remote-shell-command-switch 'safe-local-variable #'identity)
 (put 'remote-shell-interactive-switch 'safe-local-variable #'identity)
@@ -233,9 +237,12 @@ files-x-test-hack-connection-local-variables-apply
                  (nreverse (copy-tree files-x-test--variables2)))))
         ;; The variables exist also as local variables.
         (should (local-variable-p 'remote-shell-file-name))
+        (should (local-variable-p 'remote-null-device))
         ;; The proper variable value is set.
         (should
-         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))))
+         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
+        (should
+         (string-equal (symbol-value 'remote-null-device) "/dev/null"))))
 
     ;; The third test case.  Both criteria `files-x-test--criteria1'
     ;; and `files-x-test--criteria2' apply, but there are no double
@@ -274,13 +281,11 @@ files-x-test-hack-connection-local-variables-apply
         (should-not (local-variable-p 'remote-shell-file-name))
         (should-not (boundp 'remote-shell-file-name))))))
 
-(defvar tramp-connection-local-default-shell-variables)
-(defvar tramp-connection-local-default-system-variables)
-
 (ert-deftest files-x-test-with-connection-local-variables ()
   "Test setting connection-local variables."
 
-  (let (connection-local-profile-alist connection-local-criteria-alist)
+  (let ((connection-local-profile-alist connection-local-profile-alist)
+        (connection-local-criteria-alist connection-local-criteria-alist))
     (connection-local-set-profile-variables
      'remote-bash files-x-test--variables1)
     (connection-local-set-profile-variables
@@ -291,29 +296,6 @@ files-x-test-with-connection-local-variables
     (connection-local-set-profiles
      nil 'remote-ksh 'remote-nullfile)
 
-    (with-temp-buffer
-      (let ((enable-connection-local-variables t))
-        (hack-connection-local-variables-apply nil)
-
-	;; All connection-local variables are set.  They apply in
-        ;; reverse order in `connection-local-variables-alist'.
-        (should
-         (equal connection-local-variables-alist
-		(append
-		 (nreverse (copy-tree files-x-test--variables3))
-		 (nreverse (copy-tree files-x-test--variables2)))))
-        ;; The variables exist also as local variables.
-        (should (local-variable-p 'remote-shell-file-name))
-        (should (local-variable-p 'remote-null-device))
-        ;; The proper variable values are set.
-        (should
-         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
-        (should
-         (string-equal (symbol-value 'remote-null-device) "/dev/null"))
-
-	;; A candidate connection-local variable is not bound yet.
-        (should-not (local-variable-p 'remote-shell-command-switch))))
-
     (with-temp-buffer
       ;; Use the macro.  We need a remote `default-directory'.
       (let ((enable-connection-local-variables t)
@@ -331,18 +313,18 @@ files-x-test-with-connection-local-variables
 	(with-connection-local-variables
 	 ;; All connection-local variables are set.  They apply in
 	 ;; reverse order in `connection-local-variables-alist'.
-	 ;; Since we ha a remote default directory, Tramp's settings
+	 ;; Since we have a remote default directory, Tramp's settings
 	 ;; are appended as well.
          (should
           (equal
            connection-local-variables-alist
 	   (append
-	    (nreverse (copy-tree files-x-test--variables3))
-	    (nreverse (copy-tree files-x-test--variables2))
             (nreverse
              (copy-tree tramp-connection-local-default-shell-variables))
             (nreverse
-             (copy-tree tramp-connection-local-default-system-variables)))))
+             (copy-tree tramp-connection-local-default-system-variables))
+	    (nreverse (copy-tree files-x-test--variables3))
+	    (nreverse (copy-tree files-x-test--variables2)))))
          ;; The variables exist also as local variables.
          (should (local-variable-p 'remote-shell-file-name))
          (should (local-variable-p 'remote-null-device))
@@ -352,15 +334,21 @@ files-x-test-with-connection-local-variables
          (should
           (string-equal (symbol-value 'remote-null-device) "/dev/null"))
 
-         ;; Run another instance of `with-connection-local-variables'
-         ;; with a different application.
-         (let ((connection-local-default-application (cadr files-x-test--application)))
-	   (with-connection-local-variables
-            ;; The proper variable values are set.
-            (should
-             (string-equal (symbol-value 'remote-shell-file-name) "/bin/bash"))
-            (should
-             (string-equal (symbol-value 'remote-null-device) "/dev/null"))))
+         ;; Run `with-connection-local-application-variables' to use a
+         ;; different application.
+	 (with-connection-local-application-variables
+             (cadr files-x-test--application)
+         (should
+          (equal
+           connection-local-variables-alist
+	   (append
+	    (nreverse (copy-tree files-x-test--variables3))
+	    (nreverse (copy-tree files-x-test--variables1)))))
+           ;; The proper variable values are set.
+           (should
+            (string-equal (symbol-value 'remote-shell-file-name) "/bin/bash"))
+           (should
+            (string-equal (symbol-value 'remote-null-device) "/dev/null")))
          ;; The variable values are reset.
          (should
           (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
@@ -376,5 +364,52 @@ files-x-test-with-connection-local-variables
 	(should-not (boundp 'remote-shell-file-name))
 	(should (string-equal (symbol-value 'remote-null-device) "null"))))))
 
+(defun files-x-test--get-lazy-var ()
+  "Get the connection-local value of `remote-lazy-var'.
+If it's not initialized yet, initialize it."
+  (with-connection-local-application-variables
+      (cadr files-x-test--application)
+    (or remote-lazy-var
+        (setq-connection-local remote-lazy-var
+                               (or (file-remote-p default-directory 'host)
+                                   "local")))))
+
+(defun files-x-test--set-lazy-var (value)
+  "Set the connection-local value of `remote-lazy-var'"
+  (with-connection-local-application-variables
+      (cadr files-x-test--application)
+    (setq-connection-local remote-lazy-var value)))
+
+(ert-deftest files-x-test-setq-connection-local ()
+  "Test dynamically setting connection local variables."
+  (let (connection-local-profile-alist connection-local-criteria-alist)
+    (connection-local-set-profile-variables
+     'remote-lazy files-x-test--variables5)
+    (connection-local-set-profiles
+     files-x-test--application
+     'remote-lazy)
+
+    ;; Test the initial local value.
+    (should (equal (files-x-test--get-lazy-var) "local"))
+
+    ;; Set the local value and make sure it retains the value we set.
+    (should (equal (files-x-test--set-lazy-var "here") "here"))
+    (should (equal (files-x-test--get-lazy-var) "here"))
+
+    (let ((default-directory "/method:host:"))
+      ;; Test the initial remote value.
+      (should (equal (files-x-test--get-lazy-var) "host"))
+
+      ;; Set the remote value and make sure it retains the value we set.
+      (should (equal (files-x-test--set-lazy-var "there") "there"))
+      (should (equal (files-x-test--get-lazy-var) "there")))
+
+    ;; Make sure we get the local value we set above.
+    (should (equal (files-x-test--get-lazy-var) "here"))
+
+  ;; Make sure we get the remote value we set above.
+  (let ((default-directory "/method:host:"))
+    (should (equal (files-x-test--get-lazy-var) "there")))))
+
 (provide 'files-x-tests)
 ;;; files-x-tests.el ends here
-- 
2.25.1


[-- Attachment #4: 0003-Allow-ignoring-errors-when-calling-eshell-match-comm.patch --]
[-- Type: text/plain, Size: 3827 bytes --]

From 44c465c153c318b4c4d440639353e6f9e7eec8b8 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sat, 24 Sep 2022 18:13:03 -0700
Subject: [PATCH 3/7] ; Allow ignoring errors when calling
 'eshell-match-command-output'

* test/lisp/eshell/eshell-tests-helpers.el
(eshell-match-command-output): New argument IGNORE-ERRORS.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/last-status-var-lisp-command)
(esh-var-test/last-status-var-lisp-form)
(esh-var-test/last-status-var-lisp-form-2): Ignore errors when calling
'eshell-match-command-output'.
---
 test/lisp/eshell/esh-var-tests.el        | 15 ++++++---------
 test/lisp/eshell/eshell-tests-helpers.el | 13 ++++++++++---
 2 files changed, 16 insertions(+), 12 deletions(-)

diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
index cb5b1766bb..ad695e45d7 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -472,9 +472,8 @@ esh-var-test/last-status-var-lisp-command
                                 "t\n0\n")
    (eshell-match-command-output "zerop 1; echo $?"
                                 "0\n")
-   (let ((debug-on-error nil))
-     (eshell-match-command-output "zerop foo; echo $?"
-                                  "1\n"))))
+   (eshell-match-command-output "zerop foo; echo $?"
+                                "1\n" nil t)))
 
 (ert-deftest esh-var-test/last-status-var-lisp-form ()
   "Test using the \"last exit status\" ($?) variable with a Lisp form"
@@ -484,9 +483,8 @@ esh-var-test/last-status-var-lisp-form
                                   "t\n0\n")
      (eshell-match-command-output "(zerop 1); echo $?"
                                   "2\n")
-     (let ((debug-on-error nil))
-       (eshell-match-command-output "(zerop \"foo\"); echo $?"
-                                    "1\n")))))
+     (eshell-match-command-output "(zerop \"foo\"); echo $?"
+                                  "1\n" nil t))))
 
 (ert-deftest esh-var-test/last-status-var-lisp-form-2 ()
   "Test using the \"last exit status\" ($?) variable with a Lisp form.
@@ -497,9 +495,8 @@ esh-var-test/last-status-var-lisp-form-2
                                   "0\n")
      (eshell-match-command-output "(zerop 0); echo $?"
                                   "0\n")
-     (let ((debug-on-error nil))
-       (eshell-match-command-output "(zerop \"foo\"); echo $?"
-                                    "1\n")))))
+     (eshell-match-command-output "(zerop \"foo\"); echo $?"
+                                  "1\n" nil t))))
 
 (ert-deftest esh-var-test/last-status-var-ext-cmd ()
   "Test using the \"last exit status\" ($?) variable with an external command"
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index 73abfcbb55..e713e162ad 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -100,9 +100,16 @@ eshell-match-output--explainer
 
 (put 'eshell-match-output 'ert-explainer #'eshell-match-output--explainer)
 
-(defun eshell-match-command-output (command regexp &optional func)
-  "Insert a COMMAND at the end of the buffer and match the output with REGEXP."
-  (eshell-insert-command command func)
+(defun eshell-match-command-output (command regexp &optional func
+                                            ignore-errors)
+  "Insert a COMMAND at the end of the buffer and match the output with REGEXP.
+FUNC is the function to call after inserting the text (see
+`eshell-insert-command').
+
+If IGNORE-ERRORS is non-nil, ignore any errors signaled when
+inserting the command."
+  (let ((debug-on-error (and (not ignore-errors) debug-on-error)))
+    (eshell-insert-command command func))
   (eshell-wait-for-subprocess)
   (should (eshell-match-output regexp)))
 
-- 
2.25.1


[-- Attachment #5: 0004-Obsolete-eshell-define.patch --]
[-- Type: text/plain, Size: 1674 bytes --]

From 0374d6b456d4062c326ee935b1109f45863287bc Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Wed, 28 Sep 2022 09:34:38 -0700
Subject: [PATCH 4/7] ; Obsolete 'eshell/define'

* lisp/eshell/esh-var.el (eshell/define): Make obsolete, and explain
its current state.

* doc/misc/eshell.texi (Built-ins): Remove 'define'.
---
 doc/misc/eshell.texi   | 5 -----
 lisp/eshell/esh-var.el | 5 +++++
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 0ee33f2c2a..8036bbd83a 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -439,11 +439,6 @@ Built-ins
 is similar to, but slightly different from, the GNU Coreutils
 @command{date} command.
 
-@item define
-@cmindex define
-Define a variable alias.
-@xref{Variable Aliases, , , elisp, The Emacs Lisp Reference Manual}.
-
 @item diff
 @cmindex diff
 Compare files using Emacs's internal @code{diff} (not to be confused
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index 36e59cd5a4..3c09fc52fb 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -302,6 +302,11 @@ eshell-interpolate-variable
 
 (defun eshell/define (var-alias definition)
   "Define a VAR-ALIAS using DEFINITION."
+  ;; FIXME: This function doesn't work (it produces variable aliases
+  ;; in a form not recognized by other parts of the code), and likely
+  ;; hasn't worked since before its introduction into Emacs.  It
+  ;; should either be removed or fixed up.
+  (declare (obsolete nil "29.1"))
   (if (not definition)
       (setq eshell-variable-aliases-list
 	    (delq (assoc var-alias eshell-variable-aliases-list)
-- 
2.25.1


[-- Attachment #6: 0005-Allow-setting-the-values-of-variable-aliases-in-Eshe.patch --]
[-- Type: text/plain, Size: 22059 bytes --]

From 8ed4d6a640761d07500dfabccfa100f1cc498dcc Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sun, 25 Sep 2022 21:47:26 -0700
Subject: [PATCH 5/7] 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 3f3a1616ee..c5ceb3ffd1 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"))
 
+\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
 
-- 
2.25.1


[-- Attachment #7: 0006-Improve-handling-of-PATH-in-Eshell-for-remote-direct.patch --]
[-- Type: text/plain, Size: 19579 bytes --]

From 4fe9542a5ba888f2054dee3693b61e714919e2b8 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Thu, 15 Sep 2022 12:24:37 -0700
Subject: [PATCH 6/7] Improve handling of $PATH in Eshell for remote
 directories

* lisp/eshell/esh-util.el (eshell-path-env, eshell-parse-colon-path):
Make obsolete.
(eshell-path-env-list): New variable.
(eshell-connection-default-profile): New connection-local profile.
(eshell-get-path): Reimplement using 'eshell-path-env-list'.
(eshell-set-path): New function.

* lisp/eshell/esh-var.el (eshell-variable-aliases-list): Add entry for
$PATH.
(eshell-var-initialize): Add 'eshell-path-env-list' to
'eshell-subcommand-bindings'.

* lisp/eshell/esh-ext.el (eshell-search-path): Use 'file-name-concat'
instead of 'concat'.
(eshell/addpath): Use 'eshell-get-path' and 'eshell-set-path'.

* lisp/net/tramp-integration.el: Only apply Eshell hooks when
'eshell-path-env-list' is unbound.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/path-var/local-directory)
(esh-var-test/path-var/remote-directory, esh-var-test/path-var/set)
(esh-var-test/path-var/set-locally)
(esh-var-test/path-var-preserve-across-hosts): New tests.

* test/lisp/eshell/esh-ext-tests.el: New file.

* test/lisp/eshell/eshell-tests-helpers.el
(with-temp-eshell): Set 'eshell-last-dir-ring-file-name' to nil.
(eshell-tests-remote-accessible-p, eshell-last-input)
(eshell-last-output): New functions.
(eshell-match-output, eshell-match-output--explainer): Use
'eshell-last-input' and 'eshell-last-output'.

* doc/misc/eshell.texi (Variables): Document $PATH.

* etc/NEWS: Announce this change (bug#57556).
---
 doc/misc/eshell.texi                     |  8 +++
 etc/NEWS                                 |  5 ++
 lisp/eshell/esh-ext.el                   | 23 ++++---
 lisp/eshell/esh-util.el                  | 53 +++++++++++++++--
 lisp/eshell/esh-var.el                   | 12 +++-
 lisp/net/tramp-integration.el            | 21 +++----
 test/lisp/eshell/esh-ext-tests.el        | 76 ++++++++++++++++++++++++
 test/lisp/eshell/esh-var-tests.el        | 60 +++++++++++++++++++
 test/lisp/eshell/eshell-tests-helpers.el | 32 +++++++---
 9 files changed, 253 insertions(+), 37 deletions(-)
 create mode 100644 test/lisp/eshell/esh-ext-tests.el

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 48edee59ab..dc1af16fcf 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -939,6 +939,14 @@ Variables
 directory ring via subscripting, e.g.@: @samp{$-[1]} refers to the
 working directory @emph{before} the previous one.
 
+@vindex $PATH
+@item $PATH
+This specifies the directories to search for executable programs as a
+string, separated by @code{":"} for Unix and GNU systems, and
+@code{";"} for MS systems.  This variable is connection-aware, so when
+the current directory on a remote host, it will automatically update
+to reflect the search path on that host.
+
 @vindex $_
 @item $_
 This refers to the last argument of the last command.  With a
diff --git a/etc/NEWS b/etc/NEWS
index a10d2438c8..218fbfa42a 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -356,6 +356,11 @@ previous 'C-x ='.
 
 ** Eshell
 
+*** Eshell's PATH is now derived from 'exec-path'.
+For consistency with remote connections, Eshell now uses 'exec-path'
+to determine the execution path on the local system, instead of using
+the PATH environment variable directly.
+
 ---
 *** 'source' and '.' no longer accept the '--help' option.
 This is for compatibility with the shell versions of these commands,
diff --git a/lisp/eshell/esh-ext.el b/lisp/eshell/esh-ext.el
index 98902fc6f2..d513d750d9 100644
--- a/lisp/eshell/esh-ext.el
+++ b/lisp/eshell/esh-ext.el
@@ -77,7 +77,7 @@ eshell-search-path
     (let ((list (eshell-get-path))
 	  suffixes n1 n2 file)
       (while list
-	(setq n1 (concat (car list) name))
+	(setq n1 (file-name-concat (car list) name))
 	(setq suffixes eshell-binary-suffixes)
 	(while suffixes
 	  (setq n2 (concat n1 (car suffixes)))
@@ -239,17 +239,16 @@ eshell/addpath
      (?h "help" nil nil  "display this usage message")
      :usage "[-b] PATH
 Adds the given PATH to $PATH.")
-   (if args
-       (progn
-	 (setq eshell-path-env (getenv "PATH")
-	       args (mapconcat #'identity args path-separator)
-	       eshell-path-env
-	       (if prepend
-		   (concat args path-separator eshell-path-env)
-		 (concat eshell-path-env path-separator args)))
-	 (setenv "PATH" eshell-path-env))
-     (dolist (dir (parse-colon-path (getenv "PATH")))
-       (eshell-printn dir)))))
+   (let ((path (eshell-get-path t)))
+     (if args
+         (progn
+           (setq path (if prepend
+                          (append args path)
+                        (append path args)))
+           (eshell-set-path path)
+           (string-join path (path-separator)))
+       (dolist (dir path)
+         (eshell-printn dir))))))
 
 (put 'eshell/addpath 'eshell-no-numeric-conversions t)
 (put 'eshell/addpath 'eshell-filename-arguments t)
diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el
index 9258ca5e40..55983b1feb 100644
--- a/lisp/eshell/esh-util.el
+++ b/lisp/eshell/esh-util.el
@@ -249,17 +249,58 @@ eshell-path-env
 It might be different from \(getenv \"PATH\"), when
 `default-directory' points to a remote host.")
 
-(defun eshell-get-path ()
+(make-obsolete-variable 'eshell-path-env 'eshell-get-path "29.1")
+
+(defvar-local eshell-path-env-list nil)
+
+(connection-local-set-profile-variables
+ 'eshell-connection-default-profile
+ '((eshell-path-env-list . nil)))
+
+(connection-local-set-profiles
+ '(:application eshell)
+ 'eshell-connection-default-profile)
+
+(defun eshell-get-path (&optional local-part)
   "Return $PATH as a list.
-Add the current directory on MS-Windows."
-  (eshell-parse-colon-path
-   (if (eshell-under-windows-p)
-       (concat "." path-separator eshell-path-env)
-     eshell-path-env)))
+If LOCAL-PART is non-nil, only return the local part of the path.
+Otherwise, return the full, possibly-remote path.
+
+On MS-Windows, add the current directory as the first directory
+in the path."
+  (with-connection-local-application-variables 'eshell
+    (let ((remote (file-remote-p default-directory))
+          (path
+           (or eshell-path-env-list
+               ;; If not already cached, get the path from
+               ;; `exec-path', removing the last element, which is
+               ;; `exec-directory'.
+               (setq-connection-local eshell-path-env-list
+                                      (butlast (exec-path))))))
+      (when (and (eshell-under-windows-p)
+                 (not remote))
+        (push "." path))
+      (if (and remote (not local-part))
+          (mapcar (lambda (x) (file-name-concat remote x)) path)
+        path))))
+
+(defun eshell-set-path (path)
+  "Set the Eshell $PATH to PATH.
+PATH can be either a list of directories or a string of
+directories separated by `path-separator'."
+  (with-connection-local-application-variables 'eshell
+    (setq-connection-local
+     eshell-path-env-list
+     (if (listp path)
+	 path
+       ;; Don't use `parse-colon-path' here, since we don't want
+       ;; the additonal translations it does on each element.
+       (split-string path (path-separator))))))
 
 (defun eshell-parse-colon-path (path-env)
   "Split string with `parse-colon-path'.
 Prepend remote identification of `default-directory', if any."
+  (declare (obsolete nil "29.1"))
   (let ((remote (file-remote-p default-directory)))
     (if remote
 	(mapcar
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index caf143e1a1..57ea42f493 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -156,7 +156,14 @@ eshell-variable-aliases-list
     ("LINES" ,(lambda () (window-body-height nil 'remap)) t t)
     ("INSIDE_EMACS" eshell-inside-emacs t)
 
-    ;; for eshell-cmd.el
+    ;; for esh-ext.el
+    ("PATH" (,(lambda () (string-join (eshell-get-path t) (path-separator)))
+             . ,(lambda (_ value)
+                  (eshell-set-path value)
+                  value))
+     t t)
+
+    ;; for esh-cmd.el
     ("_" ,(lambda (indices quoted)
 	    (if (not indices)
 	        (car (last eshell-last-arguments))
@@ -249,7 +256,8 @@ eshell-var-initialize
   (setq-local eshell-subcommand-bindings
               (append
                '((process-environment (eshell-copy-environment))
-                 (eshell-variable-aliases-list eshell-variable-aliases-list))
+                 (eshell-variable-aliases-list eshell-variable-aliases-list)
+                 (eshell-path-env-list eshell-path-env-list))
                eshell-subcommand-bindings))
 
   (setq-local eshell-special-chars-inside-quoting
diff --git a/lisp/net/tramp-integration.el b/lisp/net/tramp-integration.el
index 35c0636b1c..4be019edd9 100644
--- a/lisp/net/tramp-integration.el
+++ b/lisp/net/tramp-integration.el
@@ -136,16 +136,17 @@ tramp-eshell-directory-change
           (getenv "PATH"))))
 
 (with-eval-after-load 'esh-util
-  (add-hook 'eshell-mode-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'eshell-directory-change-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'tramp-integration-unload-hook
-	    (lambda ()
-	      (remove-hook 'eshell-mode-hook
-			   #'tramp-eshell-directory-change)
-	      (remove-hook 'eshell-directory-change-hook
-			   #'tramp-eshell-directory-change))))
+  (unless (boundp 'eshell-path-env-list)
+    (add-hook 'eshell-mode-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'eshell-directory-change-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'tramp-integration-unload-hook
+	      (lambda ()
+	        (remove-hook 'eshell-mode-hook
+			     #'tramp-eshell-directory-change)
+	        (remove-hook 'eshell-directory-change-hook
+			     #'tramp-eshell-directory-change)))))
 
 ;;; Integration of recentf.el:
 
diff --git a/test/lisp/eshell/esh-ext-tests.el b/test/lisp/eshell/esh-ext-tests.el
new file mode 100644
index 0000000000..54191e9409
--- /dev/null
+++ b/test/lisp/eshell/esh-ext-tests.el
@@ -0,0 +1,76 @@
+;;; esh-ext-tests.el --- esh-ext test suite  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's external command handling.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'esh-ext)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+         (expand-file-name "eshell-tests-helpers"
+                           (file-name-directory (or load-file-name
+                                                    default-directory))))
+
+;;; Tests:
+
+(ert-deftest esh-ext-test/addpath/end ()
+  "Test that \"addpath\" adds paths to the end of $PATH."
+  (with-temp-eshell
+   (let ((eshell-path-env-list '("/some/path" "/other/path"))
+         (expected-path (string-join '("/some/path" "/other/path" "/new/path"
+                                       "/new/path2")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/begin ()
+  "Test that \"addpath -b\" adds paths to the beginning of $PATH."
+  (with-temp-eshell
+   (let ((eshell-path-env-list '("/some/path" "/other/path"))
+         (expected-path (string-join '("/new/path" "/new/path2" "/some/path"
+                                       "/other/path")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath -b /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/set-locally ()
+  "Test adding to the path temporarily in a subcommand."
+  (let* ((eshell-path-env-list '("/some/path" "/other/path"))
+         (original-path (string-join eshell-path-env-list (path-separator)))
+         (local-path (string-join (append eshell-path-env-list '("/new/path"))
+                                  (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output
+      "{ addpath /new/path; env }"
+      (format "PATH=%s\n" (regexp-quote local-path)))
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH"
+                                  (concat original-path "\n")))))
+
+;; esh-ext-tests.el ends here
diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
index a7ac52ed24..31b01c5605 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -23,6 +23,7 @@
 
 ;;; Code:
 
+(require 'tramp)
 (require 'ert)
 (require 'esh-mode)
 (require 'esh-var)
@@ -610,6 +611,65 @@ esh-var-test/inside-emacs-var-split-indices
    (eshell-match-command-output "echo $INSIDE_EMACS[, 1]"
                                 "eshell")))
 
+(ert-deftest esh-var-test/path-var/local-directory ()
+  "Test using $PATH in a local directory."
+  (let ((expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path)))))
+
+(ert-deftest esh-var-test/path-var/remote-directory ()
+  "Test using $PATH in a remote directory."
+  (skip-unless (eshell-tests-remote-accessible-p))
+  (let* ((default-directory ert-remote-temporary-file-directory)
+         (expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path)))))
+
+(ert-deftest esh-var-test/path-var/set ()
+  "Test setting $PATH."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
+(ert-deftest esh-var-test/path-var/set-locally ()
+  "Test setting $PATH temporarily for a single command."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "PATH=/local/path env"
+                                  "PATH=/local/path\n")
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
+(ert-deftest esh-var-test/path-var/preserve-across-hosts ()
+  "Test that $PATH can be set independently on multiple hosts."
+  (let ((local-directory default-directory)
+        local-path remote-path)
+    (with-temp-eshell
+     ;; Set the $PATH on localhost.
+     (eshell-insert-command "set PATH /local/path")
+     (setq local-path (eshell-last-output))
+     ;; `cd' to a remote host and set the $PATH there too.
+     (eshell-insert-command
+      (format "cd %s" ert-remote-temporary-file-directory))
+     (eshell-insert-command "set PATH /remote/path")
+     (setq remote-path (eshell-last-output))
+     ;; Return to localhost and check that $PATH is the value we set
+     ;; originally.
+     (eshell-insert-command (format "cd %s" local-directory))
+     (eshell-match-command-output "echo $PATH" (regexp-quote local-path))
+     ;; ... and do the same for the remote host.
+     (eshell-insert-command
+      (format "cd %s" ert-remote-temporary-file-directory))
+     (eshell-match-command-output "echo $PATH" (regexp-quote remote-path)))))
+
 (ert-deftest esh-var-test/last-status-var-lisp-command ()
   "Test using the \"last exit status\" ($?) variable with a Lisp command"
   (with-temp-eshell
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index e713e162ad..1d9674070c 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -31,11 +31,22 @@
 (require 'eshell)
 
 (defvar eshell-history-file-name nil)
+(defvar eshell-last-dir-ring-file-name nil)
 
 (defvar eshell-test--max-subprocess-time 5
   "The maximum amount of time to wait for a subprocess to finish, in seconds.
 See `eshell-wait-for-subprocess'.")
 
+(defun eshell-tests-remote-accessible-p ()
+  "Return if a test involving remote files can proceed.
+If using this function, be sure to load `tramp' near the
+beginning of the test file."
+  (ignore-errors
+    (and
+     (file-remote-p ert-remote-temporary-file-directory)
+     (file-directory-p ert-remote-temporary-file-directory)
+     (file-writable-p ert-remote-temporary-file-directory))))
+
 (defmacro with-temp-eshell (&rest body)
   "Evaluate BODY in a temporary Eshell buffer."
   `(save-current-buffer
@@ -44,6 +55,7 @@ with-temp-eshell
               ;; back on $HISTFILE.
               (process-environment (cons "HISTFILE" process-environment))
               (eshell-history-file-name nil)
+              (eshell-last-dir-ring-file-name nil)
               (eshell-buffer (eshell t)))
          (unwind-protect
              (with-current-buffer eshell-buffer
@@ -83,19 +95,25 @@ eshell-insert-command
   (insert-and-inherit command)
   (funcall (or func 'eshell-send-input)))
 
+(defun eshell-last-input ()
+  "Return the input of the last Eshell command."
+  (buffer-substring-no-properties
+   eshell-last-input-start eshell-last-input-end))
+
+(defun eshell-last-output ()
+  "Return the output of the last Eshell command."
+  (buffer-substring-no-properties
+   (eshell-beginning-of-output) (eshell-end-of-output)))
+
 (defun eshell-match-output (regexp)
   "Test whether the output of the last command matches REGEXP."
-  (string-match-p
-    regexp (buffer-substring-no-properties
-            (eshell-beginning-of-output) (eshell-end-of-output))))
+  (string-match-p regexp (eshell-last-output)))
 
 (defun eshell-match-output--explainer (regexp)
   "Explain the result of `eshell-match-output'."
   `(mismatched-output
-    (command ,(buffer-substring-no-properties
-               eshell-last-input-start eshell-last-input-end))
-    (output ,(buffer-substring-no-properties
-              (eshell-beginning-of-output) (eshell-end-of-output)))
+    (command ,(eshell-last-input))
+    (output ,(eshell-last-output))
     (regexp ,regexp)))
 
 (put 'eshell-match-output 'ert-explainer #'eshell-match-output--explainer)
-- 
2.25.1


[-- Attachment #8: 0007-Print-the-correct-PATH-when-Eshell-s-which-fails-to-.patch --]
[-- Type: text/plain, Size: 1079 bytes --]

From 8465cb89d85be5ee492aad7f5f001892ef32b783 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Thu, 15 Sep 2022 12:32:02 -0700
Subject: [PATCH 7/7] Print the correct $PATH when Eshell's 'which' fails to
 find a command

* lisp/eshell/esh-cmd.el (eshell/which): Use 'eshell-get-path'
(bug#20008).
---
 lisp/eshell/esh-cmd.el | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index c5ceb3ffd1..4a41bbe8fa 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -1274,8 +1274,9 @@ eshell/which
                         name)
                   (eshell-search-path name)))))
       (if (not program)
-	  (eshell-error (format "which: no %s in (%s)\n"
-				name (getenv "PATH")))
+          (eshell-error (format "which: no %s in (%s)\n"
+                                name (string-join (eshell-get-path t)
+                                                  (path-separator))))
 	(eshell-printn program)))))
 
 (put 'eshell/which 'eshell-no-numeric-conversions t)
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-13  4:11                             ` Jim Porter
@ 2022-10-13  6:35                               ` Eli Zaretskii
  2022-10-14  1:29                                 ` Jim Porter
  2022-10-14 12:27                               ` Michael Albinus
  1 sibling, 1 reply; 32+ messages in thread
From: Eli Zaretskii @ 2022-10-13  6:35 UTC (permalink / raw)
  To: Jim Porter; +Cc: michael.albinus, 57556

> Cc: 57556@debbugs.gnu.org
> Date: Wed, 12 Oct 2022 21:11:47 -0700
> From: Jim Porter <jporterbugs@gmail.com>
> 
> On 10/9/2022 11:01 AM, Michael Albinus wrote:
> > Jim Porter <jporterbugs@gmail.com> writes:
> > 
> >> I attached an updated version of my connection-local.el that tries to
> >> pull out the additions you made into some helpers. What do you think?
> > 
> > I gave it a short reading, and in general it looks OK (comments
> > below). Do you want to provide a patch for files-x.el with this?
> 
> Thanks for taking a look. I've added a separate patch (0002 in this 
> series) for adding these functions (with some improvements over the 
> little test script we worked on) to files-x.el. (Patch 0001 just fixes 
> an issue in the docs/tests where the :application had an extra quote.)
> 
> If you think it would be easier to track, I could file a new bug and put 
> patches 0001 and 0002 in there, then come back to this bug once that's 
> merged. Either way is fine by me.
> 
> The other patches in this series are mostly-unchanged from before, 
> except for 0006, which now uses the new 'setq-connection-local' macro.
> 
> > This patch shall also extent the "Connection Local Variables" section of
> > the Elisp manual. This section is already quite
> > long (~150 lines), and speaks almost about static setting of
> > connection-local variables. You bring dynamic settings here, maybe a
> > subsection would help to structure. And feel free to restructure the
> > other, long text if you believe it would help.
> 
> I added all this to the manual (with an example), and divided the 
> Connection Local Variables section into two subsections: one for how to 
> initialize profiles and set criteria for them, and another for applying 
> the variables. I put the 'setq-connection-local' docs in the second 
> section, since it's closely related to 'with-connection-local-variables'.

Thanks.  Some general documentation-related comments below.

> +@node Connection Local Profiles
> +@subsection Connection Local Profiles

It is generally a good idea to have a @cindex entry for the main topic
of a section/subsection.  Usually, the @cindex entry is just the node
or section name, with all of its words down-cases.  For example, here
I'd use

  @cindex connect local profiles

And similarly for the other subsection you added.

My other general comment is to never miss an opportunity of adding a
cross-reference when you reference a term or a symbol described
elsewhere in the documentation.  Never assume the reader already knows
what all the stuff you reference is about.  For example:

> +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.

This will benefit from a cross-reference to where 'setq' is described
in the ELisp reference manual.

Please review the documentation changes for more cross-reference
opportunities like this one.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-13  6:35                               ` Eli Zaretskii
@ 2022-10-14  1:29                                 ` Jim Porter
  2022-10-14  6:17                                   ` Eli Zaretskii
  2022-10-14 12:28                                   ` Michael Albinus
  0 siblings, 2 replies; 32+ messages in thread
From: Jim Porter @ 2022-10-14  1:29 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: michael.albinus, 57556

[-- Attachment #1: Type: text/plain, Size: 1128 bytes --]

Thanks for taking a look.

On 10/12/2022 11:35 PM, Eli Zaretskii wrote:
> It is generally a good idea to have a @cindex entry for the main topic
> of a section/subsection.  Usually, the @cindex entry is just the node
> or section name, with all of its words down-cases.

Done (for both subsections).

> My other general comment is to never miss an opportunity of adding a
> cross-reference when you reference a term or a symbol described
> elsewhere in the documentation.  Never assume the reader already knows
> what all the stuff you reference is about.  For example:
[snip]
> This will benefit from a cross-reference to where 'setq' is described
> in the ELisp reference manual.

Thanks, added a cross-reference here.

> Please review the documentation changes for more cross-reference
> opportunities like this one.

I also added cross-references to the Remote Files section of the Emacs 
manual, plus to the section on let-binding. Those seemed like the most 
useful ones to me, although maybe there are some others I missed.

I attached an updated version of patch 0002 with the manual changes (the 
others are unchanged).

[-- Attachment #2: 0002-Add-helpers-to-dynamically-assign-connection-local-v.patch --]
[-- Type: text/plain, Size: 21644 bytes --]

From 0bf72dd54a034f6b36aaefcb456136f0ea349707 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Tue, 11 Oct 2022 22:11:04 -0700
Subject: [PATCH 2/7] Add helpers to dynamically assign connection-local values

* lisp/files-x.el (connection-local-criteria)
(connection-local-profile-name-for-setq): New variables.
(connection-local-profile-name-for-criteria): New function.
(with-connection-local-variables-1): ... let-bind them here.
(with-connection-local-application-variables, setq-connection-local):
New macros.

* test/lisp/files-x-tests.el: Require 'tramp-integration'
(files-x-test--variable5, remote-lazy-var): New variables.
(files-x-test-hack-connection-local-variables-apply): Expand checks.
(files-x-test-with-connection-local-variables): Remove
'hack-connection-local-variables-apply' check (it belongs in the above
test), and expand some other checks.
(files-x-test--get-lazy-var, files-x-test--set-lazy-var): New
functions.
(files-x-test-setq-connection-local): New test.

* doc/lispref/variables.texi (Connection Local Variables): Split into
two subsections and document the new features.

* etc/NEWS: Announce 'setq-connection-local'.
---
 doc/lispref/variables.texi |  97 +++++++++++++++++++++++-------
 etc/NEWS                   |   7 +++
 lisp/files-x.el            |  86 +++++++++++++++++++++++++--
 test/lisp/files-x-tests.el | 117 ++++++++++++++++++++++++-------------
 4 files changed, 239 insertions(+), 68 deletions(-)

diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi
index 2a06169b21..25736a588b 100644
--- a/doc/lispref/variables.texi
+++ b/doc/lispref/variables.texi
@@ -2239,9 +2239,26 @@ Connection Local Variables
 @cindex connection local variables
 
   Connection-local variables provide a general mechanism for different
-variable settings in buffers with a remote connection.  They are bound
+variable settings in buffers with a remote connection (@pxref{Remote
+Files,, Remote Files, emacs, The GNU Emacs Manual}).  They are bound
 and set depending on the remote connection a buffer is dedicated to.
 
+@menu
+* Connection Local Profiles::            Storing variable settings to
+                                         apply to connections.
+* Applying Connection Local Variables::  Using connection-local values
+                                         in your code.
+@end menu
+
+@node Connection Local Profiles
+@subsection Connection Local Profiles
+@cindex connection local profiles
+
+  Emacs uses connection-local profiles to store the variable settings
+to apply to particular connections.  You can then associate these with
+remote connections by defining the criteria when they should apply,
+using @code{connection-local-set-profiles}.
+
 @defun connection-local-set-profile-variables profile variables
 This function defines a set of variable settings for the connection
 @var{profile}, which is a symbol.  You can later assign the connection
@@ -2356,6 +2373,14 @@ Connection Local Variables
 list.
 @end deffn
 
+@node Applying Connection Local Variables
+@subsection Applying Connection Local Variables
+@cindex connection local variables, applying
+
+  When writing connection-aware code, you'll need to collect, and
+possibly apply, any connection-local variables.  There are several
+ways to do this, as described below.
+
 @defun hack-connection-local-variables criteria
 This function collects applicable connection-local variables
 associated with @var{criteria} in
@@ -2384,9 +2409,9 @@ Connection Local Variables
 @var{criteria}, and immediately applies them in the current buffer.
 @end defun
 
-@defmac with-connection-local-variables &rest body
-All connection-local variables, which are specified by
-@code{default-directory}, are applied.
+@defmac with-connection-local-application-variables application &rest body
+Apply all connection-local variables for @code{application}, which are
+specified by @code{default-directory}.
 
 After that, @var{body} is executed, and the connection-local variables
 are unwound.  Example:
@@ -2394,20 +2419,20 @@ Connection Local Variables
 @example
 @group
 (connection-local-set-profile-variables
-  'remote-perl
-  '((perl-command-name . "/usr/local/bin/perl")
+  'my-remote-perl
+  '((perl-command-name . "/usr/local/bin/perl5")
     (perl-command-switch . "-e %s")))
 @end group
 
 @group
 (connection-local-set-profiles
-  '(:application tramp :protocol "ssh" :machine "remotehost")
-  'remote-perl)
+  '(:application my-app :protocol "ssh" :machine "remotehost")
+  'my-remote-perl)
 @end group
 
 @group
 (let ((default-directory "/ssh:remotehost:/working/dir/"))
-  (with-connection-local-variables
+  (with-connection-local-application-variables 'my-app
     do something useful))
 @end group
 @end example
@@ -2416,30 +2441,58 @@ Connection Local Variables
 @defvar connection-local-default-application
 The default application, a symbol, to be applied in
 @code{with-connection-local-variables}.  It defaults to @code{tramp},
-but in case you want to overwrite Tramp's settings temporarily, you
-could let-bind it like
+but you can let-bind it to change the application temporarily
+(@pxref{Local Variables}).
+
+This variable must not be changed globally.
+@end defvar
+
+@defmac with-connection-local-variables &rest body
+This is equivalent to
+@code{with-connection-local-application-variables}, but uses
+@code{connection-local-default-application} for the application.
+@end defmac
+
+@defmac setq-connection-local [symbol form]@dots{}
+This macro sets each @var{symbol} connection-locally to the result of
+evaluating the corresponding @var{form}, using the connection-local
+profile specified in @code{connection-local-profile-name-for-setq}; if
+the profile name is @code{nil}, this macro will just set the variables
+normally, as with @code{setq} (@pxref{Setting Variables}).
+
+For example, you can use this macro in combination with
+@code{with-connection-local-variables} to lazily initialize
+connection-local settings:
 
 @example
 @group
+(defvar my-app-variable nil)
+
 (connection-local-set-profile-variables
-  'my-remote-perl
-  '((perl-command-name . "/usr/local/bin/perl5")
-    (perl-command-switch . "-e %s")))
-@end group
+ 'my-app-connection-default-profile
+ '((my-app-variable . nil)))
 
-@group
 (connection-local-set-profiles
-  '(:application my-app :protocol "ssh" :machine "remotehost")
-  'my-remote-perl)
+ '(:application my-app)
+ 'my-app-connection-default-profile)
 @end group
 
 @group
-(let ((default-directory "/ssh:remotehost:/working/dir/")
-      (connection-local-default-application 'my-app))
-  (with-connection-local-variables
-    do something useful))
+(defun my-app-get-variable ()
+  (with-connection-local-application-variables 'my-app
+    (or my-app-variable
+        (setq-connection-local my-app-variable
+                               do something useful))))
 @end group
 @end example
+@end defmac
+
+@defvar connection-local-profile-name-for-setq
+The connection-local profile name, a symbol, to use when setting
+variables via @code{setq-connection-local}.  This is let-bound in the
+body of @code{with-connection-local-variables}, but you can also
+let-bind it yourself if you'd like to set variables on a different
+profile.
 
 This variable must not be changed globally.
 @end defvar
diff --git a/etc/NEWS b/etc/NEWS
index ca857056fd..a10d2438c8 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -3196,6 +3196,13 @@ TIMEOUT is the idle time after which to deactivate the transient map.
 The default timeout value can be defined by the new variable
 'set-transient-map-timeout'.
 
++++
+** New macro 'setq-connection-local'.
+This allows dynamically setting variable values for a particular
+connection within the body of 'with-connection-local-variables'.  See
+the "(elisp) Connection Local Variables" node in the Lisp Reference
+manual for more information.
+
 +++
 ** 'plist-get', 'plist-put' and 'plist-member' are no longer limited to 'eq'.
 These function now take an optional comparison predicate argument.
diff --git a/lisp/files-x.el b/lisp/files-x.el
index da1e44e250..da1f7a9088 100644
--- a/lisp/files-x.el
+++ b/lisp/files-x.el
@@ -618,6 +618,18 @@ connection-local-criteria-alist
   :group 'tramp
   :version "29.1")
 
+(defvar connection-local-criteria nil
+  "The current connection-local criteria, or nil.
+This is set while executing the body of
+`with-connection-local-variables'.")
+
+(defvar connection-local-profile-name-for-setq nil
+  "The current connection-local profile name, or nil.
+This is the name of the profile to use when setting variables via
+`setq-connection-local'.  Its value is derived from
+`connection-local-criteria' and is set while executing the body
+of `with-connection-local-variables'.")
+
 (defsubst connection-local-normalize-criteria (criteria)
   "Normalize plist CRITERIA according to properties.
 Return a reordered plist."
@@ -736,6 +748,15 @@ connection-local-criteria-for-default-directory
       :user        ,(file-remote-p default-directory 'user)
       :machine     ,(file-remote-p default-directory 'host))))
 
+(defun connection-local-profile-name-for-criteria (criteria)
+  "Get a connection-local profile name based on CRITERIA."
+  (when criteria
+    (let (print-level print-length)
+      (intern (concat
+               "autogenerated-connection-local-profile/"
+               (prin1-to-string
+                (connection-local-normalize-criteria criteria)))))))
+
 ;;;###autoload
 (defmacro with-connection-local-variables (&rest body)
   "Apply connection-local variables according to `default-directory'.
@@ -743,16 +764,28 @@ with-connection-local-variables
   (declare (debug t))
   `(with-connection-local-variables-1 (lambda () ,@body)))
 
+;;;###autoload
+(defmacro with-connection-local-application-variables (application &rest body)
+  "Apply connection-local variables for APPLICATION in `default-directory'.
+Execute BODY, and unwind connection-local variables."
+  (declare (debug t) (indent 1))
+  `(let ((connection-local-default-application ,application))
+     (with-connection-local-variables-1 (lambda () ,@body))))
+
 ;;;###autoload
 (defun with-connection-local-variables-1 (body-fun)
   "Apply connection-local variables according to `default-directory'.
 Call BODY-FUN with no args, and then unwind connection-local variables."
   (if (file-remote-p default-directory)
-      (let ((enable-connection-local-variables t)
-            (old-buffer-local-variables (buffer-local-variables))
-	    connection-local-variables-alist)
-	(hack-connection-local-variables-apply
-	 (connection-local-criteria-for-default-directory))
+      (let* ((enable-connection-local-variables t)
+             (connection-local-criteria
+              (connection-local-criteria-for-default-directory))
+             (connection-local-profile-name-for-setq
+              (connection-local-profile-name-for-criteria
+               connection-local-criteria))
+             (old-buffer-local-variables (buffer-local-variables))
+	     connection-local-variables-alist)
+	(hack-connection-local-variables-apply connection-local-criteria)
 	(unwind-protect
             (funcall body-fun)
 	  ;; Cleanup.
@@ -764,6 +797,49 @@ with-connection-local-variables-1
     ;; No connection-local variables to apply.
     (funcall body-fun)))
 
+;;;###autoload
+(defmacro setq-connection-local (&rest pairs)
+  "Set each VARIABLE connection-locally to VALUE.
+
+When `connection-local-profile-name-for-setq' is set, assign each
+variable's value on that connection profile, and set that profile
+for `connection-local-criteria'.  You can use this in combination
+with `with-connection-local-variables', as in
+
+  (with-connection-local-variables
+    (setq-connection-local VARIABLE VALUE))
+
+If there's no connection-local profile to use, just set the
+variables normally, as with `setq'.
+
+The variables are literal symbols and should not be quoted.  The
+second VALUE is not computed until after the first VARIABLE is
+set, and so on; each VALUE can use the new value of variables set
+earlier in the `setq-connection-local'.  The return value of the
+`setq-connection-local' form is the value of the last VALUE.
+
+\(fn [VARIABLE VALUE]...)"
+  (declare (debug setq))
+  (unless (zerop (mod (length pairs) 2))
+    (error "PAIRS must have an even number of variable/value members"))
+  (let ((set-expr nil)
+        (profile-vars nil))
+    (while pairs
+      (unless (symbolp (car pairs))
+        (error "Attempting to set a non-symbol: %s" (car pairs)))
+      (push `(set ',(car pairs) ,(cadr pairs)) set-expr)
+      (push `(cons ',(car pairs) ,(car pairs)) profile-vars)
+      (setq pairs (cddr pairs)))
+    `(prog1
+         ,(macroexp-progn (nreverse set-expr))
+       (when connection-local-profile-name-for-setq
+         (connection-local-set-profile-variables
+          connection-local-profile-name-for-setq
+          (list ,@(nreverse profile-vars)))
+         (connection-local-set-profiles
+          connection-local-criteria
+          connection-local-profile-name-for-setq)))))
+
 ;;;###autoload
 (defun path-separator ()
   "The connection-local value of `path-separator'."
diff --git a/test/lisp/files-x-tests.el b/test/lisp/files-x-tests.el
index 2f6d0d4a99..9499c951c5 100644
--- a/test/lisp/files-x-tests.el
+++ b/test/lisp/files-x-tests.el
@@ -23,6 +23,7 @@
 
 (require 'ert)
 (require 'files-x)
+(require 'tramp-integration)
 
 (defconst files-x-test--variables1
   '((remote-shell-file-name . "/bin/bash")
@@ -35,7 +36,10 @@ files-x-test--variables3
   '((remote-null-device . "/dev/null")))
 (defconst files-x-test--variables4
   '((remote-null-device . "null")))
+(defconst files-x-test--variables5
+  '((remote-lazy-var . nil)))
 (defvar remote-null-device)
+(defvar remote-lazy-var nil)
 (put 'remote-shell-file-name 'safe-local-variable #'identity)
 (put 'remote-shell-command-switch 'safe-local-variable #'identity)
 (put 'remote-shell-interactive-switch 'safe-local-variable #'identity)
@@ -233,9 +237,12 @@ files-x-test-hack-connection-local-variables-apply
                  (nreverse (copy-tree files-x-test--variables2)))))
         ;; The variables exist also as local variables.
         (should (local-variable-p 'remote-shell-file-name))
+        (should (local-variable-p 'remote-null-device))
         ;; The proper variable value is set.
         (should
-         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))))
+         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
+        (should
+         (string-equal (symbol-value 'remote-null-device) "/dev/null"))))
 
     ;; The third test case.  Both criteria `files-x-test--criteria1'
     ;; and `files-x-test--criteria2' apply, but there are no double
@@ -274,13 +281,11 @@ files-x-test-hack-connection-local-variables-apply
         (should-not (local-variable-p 'remote-shell-file-name))
         (should-not (boundp 'remote-shell-file-name))))))
 
-(defvar tramp-connection-local-default-shell-variables)
-(defvar tramp-connection-local-default-system-variables)
-
 (ert-deftest files-x-test-with-connection-local-variables ()
   "Test setting connection-local variables."
 
-  (let (connection-local-profile-alist connection-local-criteria-alist)
+  (let ((connection-local-profile-alist connection-local-profile-alist)
+        (connection-local-criteria-alist connection-local-criteria-alist))
     (connection-local-set-profile-variables
      'remote-bash files-x-test--variables1)
     (connection-local-set-profile-variables
@@ -291,29 +296,6 @@ files-x-test-with-connection-local-variables
     (connection-local-set-profiles
      nil 'remote-ksh 'remote-nullfile)
 
-    (with-temp-buffer
-      (let ((enable-connection-local-variables t))
-        (hack-connection-local-variables-apply nil)
-
-	;; All connection-local variables are set.  They apply in
-        ;; reverse order in `connection-local-variables-alist'.
-        (should
-         (equal connection-local-variables-alist
-		(append
-		 (nreverse (copy-tree files-x-test--variables3))
-		 (nreverse (copy-tree files-x-test--variables2)))))
-        ;; The variables exist also as local variables.
-        (should (local-variable-p 'remote-shell-file-name))
-        (should (local-variable-p 'remote-null-device))
-        ;; The proper variable values are set.
-        (should
-         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
-        (should
-         (string-equal (symbol-value 'remote-null-device) "/dev/null"))
-
-	;; A candidate connection-local variable is not bound yet.
-        (should-not (local-variable-p 'remote-shell-command-switch))))
-
     (with-temp-buffer
       ;; Use the macro.  We need a remote `default-directory'.
       (let ((enable-connection-local-variables t)
@@ -331,18 +313,18 @@ files-x-test-with-connection-local-variables
 	(with-connection-local-variables
 	 ;; All connection-local variables are set.  They apply in
 	 ;; reverse order in `connection-local-variables-alist'.
-	 ;; Since we ha a remote default directory, Tramp's settings
+	 ;; Since we have a remote default directory, Tramp's settings
 	 ;; are appended as well.
          (should
           (equal
            connection-local-variables-alist
 	   (append
-	    (nreverse (copy-tree files-x-test--variables3))
-	    (nreverse (copy-tree files-x-test--variables2))
             (nreverse
              (copy-tree tramp-connection-local-default-shell-variables))
             (nreverse
-             (copy-tree tramp-connection-local-default-system-variables)))))
+             (copy-tree tramp-connection-local-default-system-variables))
+	    (nreverse (copy-tree files-x-test--variables3))
+	    (nreverse (copy-tree files-x-test--variables2)))))
          ;; The variables exist also as local variables.
          (should (local-variable-p 'remote-shell-file-name))
          (should (local-variable-p 'remote-null-device))
@@ -352,15 +334,21 @@ files-x-test-with-connection-local-variables
          (should
           (string-equal (symbol-value 'remote-null-device) "/dev/null"))
 
-         ;; Run another instance of `with-connection-local-variables'
-         ;; with a different application.
-         (let ((connection-local-default-application (cadr files-x-test--application)))
-	   (with-connection-local-variables
-            ;; The proper variable values are set.
-            (should
-             (string-equal (symbol-value 'remote-shell-file-name) "/bin/bash"))
-            (should
-             (string-equal (symbol-value 'remote-null-device) "/dev/null"))))
+         ;; Run `with-connection-local-application-variables' to use a
+         ;; different application.
+	 (with-connection-local-application-variables
+             (cadr files-x-test--application)
+         (should
+          (equal
+           connection-local-variables-alist
+	   (append
+	    (nreverse (copy-tree files-x-test--variables3))
+	    (nreverse (copy-tree files-x-test--variables1)))))
+           ;; The proper variable values are set.
+           (should
+            (string-equal (symbol-value 'remote-shell-file-name) "/bin/bash"))
+           (should
+            (string-equal (symbol-value 'remote-null-device) "/dev/null")))
          ;; The variable values are reset.
          (should
           (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
@@ -376,5 +364,52 @@ files-x-test-with-connection-local-variables
 	(should-not (boundp 'remote-shell-file-name))
 	(should (string-equal (symbol-value 'remote-null-device) "null"))))))
 
+(defun files-x-test--get-lazy-var ()
+  "Get the connection-local value of `remote-lazy-var'.
+If it's not initialized yet, initialize it."
+  (with-connection-local-application-variables
+      (cadr files-x-test--application)
+    (or remote-lazy-var
+        (setq-connection-local remote-lazy-var
+                               (or (file-remote-p default-directory 'host)
+                                   "local")))))
+
+(defun files-x-test--set-lazy-var (value)
+  "Set the connection-local value of `remote-lazy-var'"
+  (with-connection-local-application-variables
+      (cadr files-x-test--application)
+    (setq-connection-local remote-lazy-var value)))
+
+(ert-deftest files-x-test-setq-connection-local ()
+  "Test dynamically setting connection local variables."
+  (let (connection-local-profile-alist connection-local-criteria-alist)
+    (connection-local-set-profile-variables
+     'remote-lazy files-x-test--variables5)
+    (connection-local-set-profiles
+     files-x-test--application
+     'remote-lazy)
+
+    ;; Test the initial local value.
+    (should (equal (files-x-test--get-lazy-var) "local"))
+
+    ;; Set the local value and make sure it retains the value we set.
+    (should (equal (files-x-test--set-lazy-var "here") "here"))
+    (should (equal (files-x-test--get-lazy-var) "here"))
+
+    (let ((default-directory "/method:host:"))
+      ;; Test the initial remote value.
+      (should (equal (files-x-test--get-lazy-var) "host"))
+
+      ;; Set the remote value and make sure it retains the value we set.
+      (should (equal (files-x-test--set-lazy-var "there") "there"))
+      (should (equal (files-x-test--get-lazy-var) "there")))
+
+    ;; Make sure we get the local value we set above.
+    (should (equal (files-x-test--get-lazy-var) "here"))
+
+  ;; Make sure we get the remote value we set above.
+  (let ((default-directory "/method:host:"))
+    (should (equal (files-x-test--get-lazy-var) "there")))))
+
 (provide 'files-x-tests)
 ;;; files-x-tests.el ends here
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-14  1:29                                 ` Jim Porter
@ 2022-10-14  6:17                                   ` Eli Zaretskii
  2022-10-14 12:28                                   ` Michael Albinus
  1 sibling, 0 replies; 32+ messages in thread
From: Eli Zaretskii @ 2022-10-14  6:17 UTC (permalink / raw)
  To: Jim Porter; +Cc: michael.albinus, 57556

> Date: Thu, 13 Oct 2022 18:29:23 -0700
> Cc: michael.albinus@gmx.de, 57556@debbugs.gnu.org
> From: Jim Porter <jporterbugs@gmail.com>
> 
> I attached an updated version of patch 0002 with the manual changes (the 
> others are unchanged).

Thanks, LGTM.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-13  4:11                             ` Jim Porter
  2022-10-13  6:35                               ` Eli Zaretskii
@ 2022-10-14 12:27                               ` Michael Albinus
  2022-10-14 20:53                                 ` Jim Porter
  1 sibling, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-10-14 12:27 UTC (permalink / raw)
  To: Jim Porter; +Cc: 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

All patches look OK to me (patch 0002 in the newer version). Just two
minor nits:

> [6. text/plain; 0005-Allow-setting-the-values-of-variable-aliases-in-Eshe.patch]
>
> +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

@var{} produces already capital letters, so you are more consistent with
@var{name}.

> [7. text/plain; 0006-Improve-handling-of-PATH-in-Eshell-for-remote-direct.patch]
>
> +@vindex $PATH
> +@item $PATH
> +This specifies the directories to search for executable programs as a
> +string, separated by @code{":"} for Unix and GNU systems, and
> +@code{";"} for MS systems.  This variable is connection-aware, so when
> +the current directory on a remote host, it will automatically update
> +to reflect the search path on that host.

"... when the current directory is on a remote host ..."

Perhaps you could also say, that when the current directory changes back
to the local host, $PATH is also updated respectively.

Best reegards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-14  1:29                                 ` Jim Porter
  2022-10-14  6:17                                   ` Eli Zaretskii
@ 2022-10-14 12:28                                   ` Michael Albinus
  1 sibling, 0 replies; 32+ messages in thread
From: Michael Albinus @ 2022-10-14 12:28 UTC (permalink / raw)
  To: Jim Porter; +Cc: Eli Zaretskii, 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

> I attached an updated version of patch 0002 with the manual changes
> (the others are unchanged).

This looks OK to me. The patch for variables.texi didn't apply cleanly
in my env, but reading the diff gave me enough information.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-14 12:27                               ` Michael Albinus
@ 2022-10-14 20:53                                 ` Jim Porter
  2022-10-15 10:38                                   ` Michael Albinus
  2022-10-16 20:51                                   ` Richard Stallman
  0 siblings, 2 replies; 32+ messages in thread
From: Jim Porter @ 2022-10-14 20:53 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 57556

[-- Attachment #1: Type: text/plain, Size: 2481 bytes --]

On 10/14/2022 5:27 AM, Michael Albinus wrote:
> Jim Porter <jporterbugs@gmail.com> writes:
> 
> Hi Jim,
> 
> All patches look OK to me (patch 0002 in the newer version). Just two
> minor nits:
> 
>> [6. text/plain; 0005-Allow-setting-the-values-of-variable-aliases-in-Eshe.patch]
>>
>> +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
> 
> @var{} produces already capital letters, so you are more consistent with
> @var{name}.

My intent was to make that display as all-caps in the HTML documentation 
as well. In that excerpt, 'NAME' should always be an environment 
variable, so I used the capitalization conventions that env vars usually 
use. 'name', on the other hand, could be a Lisp variable or an env var.

I adjusted these docs a bit since they seemed unclear to me on a second 
reading (see attached), but kept the all-caps NAME for env vars. If you 
still think that's wrong, I'll change it to lower-case before merging.

>> [7. text/plain; 0006-Improve-handling-of-PATH-in-Eshell-for-remote-direct.patch]
>>
>> +@vindex $PATH
>> +@item $PATH
>> +This specifies the directories to search for executable programs as a
>> +string, separated by @code{":"} for Unix and GNU systems, and
>> +@code{";"} for MS systems.  This variable is connection-aware, so when
>> +the current directory on a remote host, it will automatically update
>> +to reflect the search path on that host.
> 
> "... when the current directory is on a remote host ..."
> 
> Perhaps you could also say, that when the current directory changes back
> to the local host, $PATH is also updated respectively.

I reworked this to make it clearer that the value is updated *every* 
time you 'cd' into a different host (including from one remote host to 
another) and also added a cross-reference to the Remote Files section of 
the Emacs manual.

[-- Attachment #2: 0005-Allow-setting-the-values-of-variable-aliases-in-Eshe.patch --]
[-- Type: text/plain, Size: 22139 bytes --]

From 28daaf0bd9890aa6d6df7fb03478358821aaa80e Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sun, 25 Sep 2022 21:47:26 -0700
Subject: [PATCH 5/7] 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              |  47 ++++++++--
 lisp/eshell/esh-cmd.el            |   4 +-
 lisp/eshell/esh-var.el            | 141 +++++++++++++++++++++--------
 test/lisp/eshell/esh-var-tests.el | 145 ++++++++++++++++++++++++++++++
 4 files changed, 291 insertions(+), 46 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 8036bbd83a..2deb6bdc20 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,34 @@ 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 (@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}'s value is a symbol, or an environment variable if it's
+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 3f3a1616ee..c5ceb3ffd1 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"))
 
+\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
 
-- 
2.25.1


[-- Attachment #3: 0006-Improve-handling-of-PATH-in-Eshell-for-remote-direct.patch --]
[-- Type: text/plain, Size: 19675 bytes --]

From 717df5f25828c969c998de73de114c7adf546880 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Thu, 15 Sep 2022 12:24:37 -0700
Subject: [PATCH 6/7] Improve handling of $PATH in Eshell for remote
 directories

* lisp/eshell/esh-util.el (eshell-path-env, eshell-parse-colon-path):
Make obsolete.
(eshell-path-env-list): New variable.
(eshell-connection-default-profile): New connection-local profile.
(eshell-get-path): Reimplement using 'eshell-path-env-list'.
(eshell-set-path): New function.

* lisp/eshell/esh-var.el (eshell-variable-aliases-list): Add entry for
$PATH.
(eshell-var-initialize): Add 'eshell-path-env-list' to
'eshell-subcommand-bindings'.

* lisp/eshell/esh-ext.el (eshell-search-path): Use 'file-name-concat'
instead of 'concat'.
(eshell/addpath): Use 'eshell-get-path' and 'eshell-set-path'.

* lisp/net/tramp-integration.el: Only apply Eshell hooks when
'eshell-path-env-list' is unbound.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/path-var/local-directory)
(esh-var-test/path-var/remote-directory, esh-var-test/path-var/set)
(esh-var-test/path-var/set-locally)
(esh-var-test/path-var-preserve-across-hosts): New tests.

* test/lisp/eshell/esh-ext-tests.el: New file.

* test/lisp/eshell/eshell-tests-helpers.el
(with-temp-eshell): Set 'eshell-last-dir-ring-file-name' to nil.
(eshell-tests-remote-accessible-p, eshell-last-input)
(eshell-last-output): New functions.
(eshell-match-output, eshell-match-output--explainer): Use
'eshell-last-input' and 'eshell-last-output'.

* doc/misc/eshell.texi (Variables): Document $PATH.

* etc/NEWS: Announce this change (bug#57556).
---
 doc/misc/eshell.texi                     | 10 ++++
 etc/NEWS                                 |  5 ++
 lisp/eshell/esh-ext.el                   | 23 ++++---
 lisp/eshell/esh-util.el                  | 53 +++++++++++++++--
 lisp/eshell/esh-var.el                   | 12 +++-
 lisp/net/tramp-integration.el            | 21 +++----
 test/lisp/eshell/esh-ext-tests.el        | 76 ++++++++++++++++++++++++
 test/lisp/eshell/esh-var-tests.el        | 60 +++++++++++++++++++
 test/lisp/eshell/eshell-tests-helpers.el | 32 +++++++---
 9 files changed, 255 insertions(+), 37 deletions(-)
 create mode 100644 test/lisp/eshell/esh-ext-tests.el

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 2deb6bdc20..ad09e9120b 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -940,6 +940,16 @@ Variables
 directory ring via subscripting, e.g.@: @samp{$-[1]} refers to the
 working directory @emph{before} the previous one.
 
+@vindex $PATH
+@item $PATH
+This specifies the directories to search for executable programs.  Its
+value is a string, separated by @code{":"} for Unix and GNU systems,
+and @code{";"} for MS systems.  This variable is connection-aware, so
+whenever you change the current directory to a different host
+(@pxref{Remote Files, , , emacs, The GNU Emacs Manual}),
+the value will automatically update to reflect the search path on that
+host.
+
 @vindex $_
 @item $_
 This refers to the last argument of the last command.  With a
diff --git a/etc/NEWS b/etc/NEWS
index 72b2331b81..871060148d 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -356,6 +356,11 @@ previous 'C-x ='.
 
 ** Eshell
 
+*** Eshell's PATH is now derived from 'exec-path'.
+For consistency with remote connections, Eshell now uses 'exec-path'
+to determine the execution path on the local system, instead of using
+the PATH environment variable directly.
+
 ---
 *** 'source' and '.' no longer accept the '--help' option.
 This is for compatibility with the shell versions of these commands,
diff --git a/lisp/eshell/esh-ext.el b/lisp/eshell/esh-ext.el
index 98902fc6f2..d513d750d9 100644
--- a/lisp/eshell/esh-ext.el
+++ b/lisp/eshell/esh-ext.el
@@ -77,7 +77,7 @@ eshell-search-path
     (let ((list (eshell-get-path))
 	  suffixes n1 n2 file)
       (while list
-	(setq n1 (concat (car list) name))
+	(setq n1 (file-name-concat (car list) name))
 	(setq suffixes eshell-binary-suffixes)
 	(while suffixes
 	  (setq n2 (concat n1 (car suffixes)))
@@ -239,17 +239,16 @@ eshell/addpath
      (?h "help" nil nil  "display this usage message")
      :usage "[-b] PATH
 Adds the given PATH to $PATH.")
-   (if args
-       (progn
-	 (setq eshell-path-env (getenv "PATH")
-	       args (mapconcat #'identity args path-separator)
-	       eshell-path-env
-	       (if prepend
-		   (concat args path-separator eshell-path-env)
-		 (concat eshell-path-env path-separator args)))
-	 (setenv "PATH" eshell-path-env))
-     (dolist (dir (parse-colon-path (getenv "PATH")))
-       (eshell-printn dir)))))
+   (let ((path (eshell-get-path t)))
+     (if args
+         (progn
+           (setq path (if prepend
+                          (append args path)
+                        (append path args)))
+           (eshell-set-path path)
+           (string-join path (path-separator)))
+       (dolist (dir path)
+         (eshell-printn dir))))))
 
 (put 'eshell/addpath 'eshell-no-numeric-conversions t)
 (put 'eshell/addpath 'eshell-filename-arguments t)
diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el
index 9258ca5e40..55983b1feb 100644
--- a/lisp/eshell/esh-util.el
+++ b/lisp/eshell/esh-util.el
@@ -249,17 +249,58 @@ eshell-path-env
 It might be different from \(getenv \"PATH\"), when
 `default-directory' points to a remote host.")
 
-(defun eshell-get-path ()
+(make-obsolete-variable 'eshell-path-env 'eshell-get-path "29.1")
+
+(defvar-local eshell-path-env-list nil)
+
+(connection-local-set-profile-variables
+ 'eshell-connection-default-profile
+ '((eshell-path-env-list . nil)))
+
+(connection-local-set-profiles
+ '(:application eshell)
+ 'eshell-connection-default-profile)
+
+(defun eshell-get-path (&optional local-part)
   "Return $PATH as a list.
-Add the current directory on MS-Windows."
-  (eshell-parse-colon-path
-   (if (eshell-under-windows-p)
-       (concat "." path-separator eshell-path-env)
-     eshell-path-env)))
+If LOCAL-PART is non-nil, only return the local part of the path.
+Otherwise, return the full, possibly-remote path.
+
+On MS-Windows, add the current directory as the first directory
+in the path."
+  (with-connection-local-application-variables 'eshell
+    (let ((remote (file-remote-p default-directory))
+          (path
+           (or eshell-path-env-list
+               ;; If not already cached, get the path from
+               ;; `exec-path', removing the last element, which is
+               ;; `exec-directory'.
+               (setq-connection-local eshell-path-env-list
+                                      (butlast (exec-path))))))
+      (when (and (eshell-under-windows-p)
+                 (not remote))
+        (push "." path))
+      (if (and remote (not local-part))
+          (mapcar (lambda (x) (file-name-concat remote x)) path)
+        path))))
+
+(defun eshell-set-path (path)
+  "Set the Eshell $PATH to PATH.
+PATH can be either a list of directories or a string of
+directories separated by `path-separator'."
+  (with-connection-local-application-variables 'eshell
+    (setq-connection-local
+     eshell-path-env-list
+     (if (listp path)
+	 path
+       ;; Don't use `parse-colon-path' here, since we don't want
+       ;; the additonal translations it does on each element.
+       (split-string path (path-separator))))))
 
 (defun eshell-parse-colon-path (path-env)
   "Split string with `parse-colon-path'.
 Prepend remote identification of `default-directory', if any."
+  (declare (obsolete nil "29.1"))
   (let ((remote (file-remote-p default-directory)))
     (if remote
 	(mapcar
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index caf143e1a1..57ea42f493 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -156,7 +156,14 @@ eshell-variable-aliases-list
     ("LINES" ,(lambda () (window-body-height nil 'remap)) t t)
     ("INSIDE_EMACS" eshell-inside-emacs t)
 
-    ;; for eshell-cmd.el
+    ;; for esh-ext.el
+    ("PATH" (,(lambda () (string-join (eshell-get-path t) (path-separator)))
+             . ,(lambda (_ value)
+                  (eshell-set-path value)
+                  value))
+     t t)
+
+    ;; for esh-cmd.el
     ("_" ,(lambda (indices quoted)
 	    (if (not indices)
 	        (car (last eshell-last-arguments))
@@ -249,7 +256,8 @@ eshell-var-initialize
   (setq-local eshell-subcommand-bindings
               (append
                '((process-environment (eshell-copy-environment))
-                 (eshell-variable-aliases-list eshell-variable-aliases-list))
+                 (eshell-variable-aliases-list eshell-variable-aliases-list)
+                 (eshell-path-env-list eshell-path-env-list))
                eshell-subcommand-bindings))
 
   (setq-local eshell-special-chars-inside-quoting
diff --git a/lisp/net/tramp-integration.el b/lisp/net/tramp-integration.el
index 35c0636b1c..4be019edd9 100644
--- a/lisp/net/tramp-integration.el
+++ b/lisp/net/tramp-integration.el
@@ -136,16 +136,17 @@ tramp-eshell-directory-change
           (getenv "PATH"))))
 
 (with-eval-after-load 'esh-util
-  (add-hook 'eshell-mode-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'eshell-directory-change-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'tramp-integration-unload-hook
-	    (lambda ()
-	      (remove-hook 'eshell-mode-hook
-			   #'tramp-eshell-directory-change)
-	      (remove-hook 'eshell-directory-change-hook
-			   #'tramp-eshell-directory-change))))
+  (unless (boundp 'eshell-path-env-list)
+    (add-hook 'eshell-mode-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'eshell-directory-change-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'tramp-integration-unload-hook
+	      (lambda ()
+	        (remove-hook 'eshell-mode-hook
+			     #'tramp-eshell-directory-change)
+	        (remove-hook 'eshell-directory-change-hook
+			     #'tramp-eshell-directory-change)))))
 
 ;;; Integration of recentf.el:
 
diff --git a/test/lisp/eshell/esh-ext-tests.el b/test/lisp/eshell/esh-ext-tests.el
new file mode 100644
index 0000000000..54191e9409
--- /dev/null
+++ b/test/lisp/eshell/esh-ext-tests.el
@@ -0,0 +1,76 @@
+;;; esh-ext-tests.el --- esh-ext test suite  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's external command handling.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'esh-ext)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+         (expand-file-name "eshell-tests-helpers"
+                           (file-name-directory (or load-file-name
+                                                    default-directory))))
+
+;;; Tests:
+
+(ert-deftest esh-ext-test/addpath/end ()
+  "Test that \"addpath\" adds paths to the end of $PATH."
+  (with-temp-eshell
+   (let ((eshell-path-env-list '("/some/path" "/other/path"))
+         (expected-path (string-join '("/some/path" "/other/path" "/new/path"
+                                       "/new/path2")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/begin ()
+  "Test that \"addpath -b\" adds paths to the beginning of $PATH."
+  (with-temp-eshell
+   (let ((eshell-path-env-list '("/some/path" "/other/path"))
+         (expected-path (string-join '("/new/path" "/new/path2" "/some/path"
+                                       "/other/path")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath -b /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/set-locally ()
+  "Test adding to the path temporarily in a subcommand."
+  (let* ((eshell-path-env-list '("/some/path" "/other/path"))
+         (original-path (string-join eshell-path-env-list (path-separator)))
+         (local-path (string-join (append eshell-path-env-list '("/new/path"))
+                                  (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output
+      "{ addpath /new/path; env }"
+      (format "PATH=%s\n" (regexp-quote local-path)))
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH"
+                                  (concat original-path "\n")))))
+
+;; esh-ext-tests.el ends here
diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
index a7ac52ed24..31b01c5605 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -23,6 +23,7 @@
 
 ;;; Code:
 
+(require 'tramp)
 (require 'ert)
 (require 'esh-mode)
 (require 'esh-var)
@@ -610,6 +611,65 @@ esh-var-test/inside-emacs-var-split-indices
    (eshell-match-command-output "echo $INSIDE_EMACS[, 1]"
                                 "eshell")))
 
+(ert-deftest esh-var-test/path-var/local-directory ()
+  "Test using $PATH in a local directory."
+  (let ((expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path)))))
+
+(ert-deftest esh-var-test/path-var/remote-directory ()
+  "Test using $PATH in a remote directory."
+  (skip-unless (eshell-tests-remote-accessible-p))
+  (let* ((default-directory ert-remote-temporary-file-directory)
+         (expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path)))))
+
+(ert-deftest esh-var-test/path-var/set ()
+  "Test setting $PATH."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
+(ert-deftest esh-var-test/path-var/set-locally ()
+  "Test setting $PATH temporarily for a single command."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "PATH=/local/path env"
+                                  "PATH=/local/path\n")
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
+(ert-deftest esh-var-test/path-var/preserve-across-hosts ()
+  "Test that $PATH can be set independently on multiple hosts."
+  (let ((local-directory default-directory)
+        local-path remote-path)
+    (with-temp-eshell
+     ;; Set the $PATH on localhost.
+     (eshell-insert-command "set PATH /local/path")
+     (setq local-path (eshell-last-output))
+     ;; `cd' to a remote host and set the $PATH there too.
+     (eshell-insert-command
+      (format "cd %s" ert-remote-temporary-file-directory))
+     (eshell-insert-command "set PATH /remote/path")
+     (setq remote-path (eshell-last-output))
+     ;; Return to localhost and check that $PATH is the value we set
+     ;; originally.
+     (eshell-insert-command (format "cd %s" local-directory))
+     (eshell-match-command-output "echo $PATH" (regexp-quote local-path))
+     ;; ... and do the same for the remote host.
+     (eshell-insert-command
+      (format "cd %s" ert-remote-temporary-file-directory))
+     (eshell-match-command-output "echo $PATH" (regexp-quote remote-path)))))
+
 (ert-deftest esh-var-test/last-status-var-lisp-command ()
   "Test using the \"last exit status\" ($?) variable with a Lisp command"
   (with-temp-eshell
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index e713e162ad..1d9674070c 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -31,11 +31,22 @@
 (require 'eshell)
 
 (defvar eshell-history-file-name nil)
+(defvar eshell-last-dir-ring-file-name nil)
 
 (defvar eshell-test--max-subprocess-time 5
   "The maximum amount of time to wait for a subprocess to finish, in seconds.
 See `eshell-wait-for-subprocess'.")
 
+(defun eshell-tests-remote-accessible-p ()
+  "Return if a test involving remote files can proceed.
+If using this function, be sure to load `tramp' near the
+beginning of the test file."
+  (ignore-errors
+    (and
+     (file-remote-p ert-remote-temporary-file-directory)
+     (file-directory-p ert-remote-temporary-file-directory)
+     (file-writable-p ert-remote-temporary-file-directory))))
+
 (defmacro with-temp-eshell (&rest body)
   "Evaluate BODY in a temporary Eshell buffer."
   `(save-current-buffer
@@ -44,6 +55,7 @@ with-temp-eshell
               ;; back on $HISTFILE.
               (process-environment (cons "HISTFILE" process-environment))
               (eshell-history-file-name nil)
+              (eshell-last-dir-ring-file-name nil)
               (eshell-buffer (eshell t)))
          (unwind-protect
              (with-current-buffer eshell-buffer
@@ -83,19 +95,25 @@ eshell-insert-command
   (insert-and-inherit command)
   (funcall (or func 'eshell-send-input)))
 
+(defun eshell-last-input ()
+  "Return the input of the last Eshell command."
+  (buffer-substring-no-properties
+   eshell-last-input-start eshell-last-input-end))
+
+(defun eshell-last-output ()
+  "Return the output of the last Eshell command."
+  (buffer-substring-no-properties
+   (eshell-beginning-of-output) (eshell-end-of-output)))
+
 (defun eshell-match-output (regexp)
   "Test whether the output of the last command matches REGEXP."
-  (string-match-p
-    regexp (buffer-substring-no-properties
-            (eshell-beginning-of-output) (eshell-end-of-output))))
+  (string-match-p regexp (eshell-last-output)))
 
 (defun eshell-match-output--explainer (regexp)
   "Explain the result of `eshell-match-output'."
   `(mismatched-output
-    (command ,(buffer-substring-no-properties
-               eshell-last-input-start eshell-last-input-end))
-    (output ,(buffer-substring-no-properties
-              (eshell-beginning-of-output) (eshell-end-of-output)))
+    (command ,(eshell-last-input))
+    (output ,(eshell-last-output))
     (regexp ,regexp)))
 
 (put 'eshell-match-output 'ert-explainer #'eshell-match-output--explainer)
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-14 20:53                                 ` Jim Porter
@ 2022-10-15 10:38                                   ` Michael Albinus
  2022-10-15 23:33                                     ` Jim Porter
  2022-10-16 20:51                                   ` Richard Stallman
  1 sibling, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-10-15 10:38 UTC (permalink / raw)
  To: Jim Porter; +Cc: 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

>> @var{} produces already capital letters, so you are more consistent
>> with
>> @var{name}.
>
> My intent was to make that display as all-caps in the HTML
> documentation as well. In that excerpt, 'NAME' should always be an
> environment variable, so I used the capitalization conventions that
> env vars usually use. 'name', on the other hand, could be a Lisp
> variable or an env var.

You use @var{} by side-effect6, which is always problematic. For
example, RMS is looking for a replacement of the texinfo syntax for
documentation, org syntax is a candidate. But whatever is chosen, an
automatic conversion from .texinfo to .<whatever> could be problematic
then.

Use what is offered by texinfo. Say for example

--8<---------------cut here---------------start------------->8---
the @env{PATH} environment variable
--8<---------------cut here---------------end--------------->8---

And in sample code, do not apply further formatting, but say it as you
mean it

--8<---------------cut here---------------start------------->8---
@samp{setq name value}
@samp{export NAME=value}
--8<---------------cut here---------------end--------------->8---

> I adjusted these docs a bit since they seemed unclear to me on a
> second reading (see attached), but kept the all-caps NAME for env
> vars. If you still think that's wrong, I'll change it to lower-case
> before merging.

Otherwise, it LGTM. Somewhere there is only one space after a dot
(should be two spaces), but I didn't marked this during review, and now
I'm too lazy to look for :-)

I'd say just push it to the repo, and if there's something left to do we
can stiil do it.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-15 10:38                                   ` Michael Albinus
@ 2022-10-15 23:33                                     ` Jim Porter
  2022-10-16 17:00                                       ` Michael Albinus
  0 siblings, 1 reply; 32+ messages in thread
From: Jim Porter @ 2022-10-15 23:33 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 57556

[-- Attachment #1: Type: text/plain, Size: 1921 bytes --]

On 10/15/2022 3:38 AM, Michael Albinus wrote:
> Use what is offered by texinfo. Say for example
> 
> --8<---------------cut here---------------start------------->8---
> the @env{PATH} environment variable
> --8<---------------cut here---------------end--------------->8---

Ah ha, got it.

> And in sample code, do not apply further formatting, but say it as you
> mean it
> 
> --8<---------------cut here---------------start------------->8---
> @samp{setq name value}
> @samp{export NAME=value}
> --8<---------------cut here---------------end--------------->8---

The Texinfo manual says I should wrap metasyntactic variables though[1]:

     For example,

         To delete file @var{filename},
         type @samp{rm @var{filename}}.

     produces

         To delete file filename, type ‘rm filename’.

     (Note that @var may appear inside @code, @samp, @file, etc.)


> Otherwise, it LGTM. Somewhere there is only one space after a dot
> (should be two spaces), but I didn't marked this during review, and now
> I'm too lazy to look for :-)

I think I found it. Fixed.

> I'd say just push it to the repo, and if there's something left to do we
> can stiil do it.

There's one significant problem I noticed with patch 0002 that we should 
probably fix first: 'setq-connection-local' clears all connection-local 
variables for the profile that were set elsewhere. That is, this:

     (with-connection-local-variables
      (setq-connection-local foo "foo")
      (setq-connection-local bar "bar"))

only sets 'bar' on the connection-local profile; the second 
'setq-connection-local' clears 'foo'. Attached is a fix for this (I'll 
fold it into patch 0002 before merging). I'm not sure if the new 
'connection-local-update-profile-variables' I added is 100% perfect, but 
I think it should work for more real-world situations.

[1] 
https://www.gnu.org/software/texinfo/manual/texinfo/html_node/_0040var.html

[-- Attachment #2: connection-local-update-profile-variables.diff --]
[-- Type: text/plain, Size: 4548 bytes --]

diff --git a/lisp/files-x.el b/lisp/files-x.el
index 665ae2ffa8..0640413ddd 100644
--- a/lisp/files-x.el
+++ b/lisp/files-x.el
@@ -706,6 +706,23 @@ connection-local-set-profile-variables
   (customize-set-variable
    'connection-local-profile-alist connection-local-profile-alist))
 
+;;;###autoload
+(defun connection-local-update-profile-variables (profile variables)
+  "Update the variable settings for PROFILE in-place.
+VARIABLES is a list that declares connection-local variables for
+the connection profile.  An element in VARIABLES is an alist
+whose elements are of the form (VAR . VALUE).
+
+Unlike `connection-local-set-profile-variables' (which see), this
+function preserves the values of any existing variable
+definitions that aren't listed in VARIABLES."
+  (when-let ((existing-variables
+              (nreverse (alist-get profile connection-local-profile-alist))))
+    (dolist (var variables)
+      (setf (alist-get (car var) existing-variables) (cdr var)))
+    (setq variables (nreverse existing-variables)))
+  (connection-local-set-profile-variables profile variables))
+
 (defun hack-connection-local-variables (criteria)
   "Read connection-local variables according to CRITERIA.
 Store the connection-local variables in buffer local
@@ -833,7 +850,7 @@ setq-connection-local
     `(prog1
          ,(macroexp-progn (nreverse set-expr))
        (when connection-local-profile-name-for-setq
-         (connection-local-set-profile-variables
+         (connection-local-update-profile-variables
           connection-local-profile-name-for-setq
           (list ,@(nreverse profile-vars)))
          (connection-local-set-profiles
diff --git a/test/lisp/files-x-tests.el b/test/lisp/files-x-tests.el
index 9499c951c5..274c49bb80 100644
--- a/test/lisp/files-x-tests.el
+++ b/test/lisp/files-x-tests.el
@@ -37,7 +37,8 @@ files-x-test--variables3
 (defconst files-x-test--variables4
   '((remote-null-device . "null")))
 (defconst files-x-test--variables5
-  '((remote-lazy-var . nil)))
+  '((remote-lazy-var . nil)
+    (remote-null-device . "/dev/null")))
 (defvar remote-null-device)
 (defvar remote-lazy-var nil)
 (put 'remote-shell-file-name 'safe-local-variable #'identity)
@@ -95,6 +96,28 @@ files-x-test-connection-local-set-profile-variables
       (connection-local-get-profile-variables 'remote-nullfile)
       files-x-test--variables4))))
 
+(ert-deftest files-x-test-connection-local-update-profile-variables ()
+  "Test updating connection-local profile variables."
+
+  ;; Declare (PROFILE VARIABLES) objects.
+  (let (connection-local-profile-alist connection-local-criteria-alist)
+    (connection-local-set-profile-variables
+     'remote-bash (copy-alist files-x-test--variables1))
+    (should
+     (equal
+      (connection-local-get-profile-variables 'remote-bash)
+      files-x-test--variables1))
+
+    ;; Updating overwrites only the values specified in this call, but
+    ;; retains all the other values from previous calls.
+    (connection-local-update-profile-variables
+     'remote-bash files-x-test--variables2)
+    (should
+     (equal
+      (connection-local-get-profile-variables 'remote-bash)
+      (cons (car files-x-test--variables2)
+            (cdr files-x-test--variables1))))))
+
 (ert-deftest files-x-test-connection-local-set-profiles ()
   "Test setting connection-local profiles."
 
@@ -402,14 +425,21 @@ files-x-test-setq-connection-local
 
       ;; Set the remote value and make sure it retains the value we set.
       (should (equal (files-x-test--set-lazy-var "there") "there"))
-      (should (equal (files-x-test--get-lazy-var) "there")))
+      (should (equal (files-x-test--get-lazy-var) "there"))
+      (with-connection-local-application-variables
+          (cadr files-x-test--application)
+        (setq-connection-local remote-null-device "null")))
 
     ;; Make sure we get the local value we set above.
     (should (equal (files-x-test--get-lazy-var) "here"))
+    (should-not (boundp 'remote-null-device))
 
-  ;; Make sure we get the remote value we set above.
-  (let ((default-directory "/method:host:"))
-    (should (equal (files-x-test--get-lazy-var) "there")))))
+    ;; Make sure we get the remote values we set above.
+    (let ((default-directory "/method:host:"))
+      (should (equal (files-x-test--get-lazy-var) "there"))
+      (with-connection-local-application-variables
+          (cadr files-x-test--application)
+        (should (equal remote-null-device "null"))))))
 
 (provide 'files-x-tests)
 ;;; files-x-tests.el ends here

^ permalink raw reply related	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-15 23:33                                     ` Jim Porter
@ 2022-10-16 17:00                                       ` Michael Albinus
  2022-10-16 23:01                                         ` Jim Porter
  0 siblings, 1 reply; 32+ messages in thread
From: Michael Albinus @ 2022-10-16 17:00 UTC (permalink / raw)
  To: Jim Porter; +Cc: 57556

Jim Porter <jporterbugs@gmail.com> writes:

Hi Jim,

> There's one significant problem I noticed with patch 0002 that we
> should probably fix first: 'setq-connection-local' clears all
> connection-local variables for the profile that were set
> elsewhere. That is, this:
>
>     (with-connection-local-variables
>      (setq-connection-local foo "foo")
>      (setq-connection-local bar "bar"))
>
> only sets 'bar' on the connection-local profile; the second
> 'setq-connection-local' clears 'foo'. Attached is a fix for this (I'll
> fold it into patch 0002 before merging). I'm not sure if the new
> 'connection-local-update-profile-variables' I added is 100% perfect,
> but I think it should work for more real-world situations.

I see. Yes, connection-local-update-profile-variables should do
better. But I don't understand why its implementation doesn't use the
existing connection-local-get-profile-variables.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-14 20:53                                 ` Jim Porter
  2022-10-15 10:38                                   ` Michael Albinus
@ 2022-10-16 20:51                                   ` Richard Stallman
  2022-10-16 23:07                                     ` Jim Porter
  1 sibling, 1 reply; 32+ messages in thread
From: Richard Stallman @ 2022-10-16 20:51 UTC (permalink / raw)
  To: Jim Porter; +Cc: michael.albinus, 57556

[[[ To any NSA and FBI agents reading my email: please consider    ]]]
[[[ whether defending the US Constitution against all enemies,     ]]]
[[[ foreign or domestic, requires you to follow Snowden's example. ]]]

  > My intent was to make that display as all-caps in the HTML documentation 
  > as well. In that excerpt, 'NAME' should always be an environment 
  > variable, so I used the capitalization conventions that env vars usually 
  > use. 'name', on the other hand, could be a Lisp variable or an env var.

It should be @var{name} to express that it is a metasyntactic variable
that stands for something called ``name''.

It is true that that the name it stands for will be the name of an
environment variable, but that is a secondary fact about it.  The most
crucial fact about this metasyntactic variable is that it is a
metasyntactic variable.  So we should format it like metasyntactic
variables, which is what @var does.

In formats which allow distinctions of type face, we should indicate this
with italics, and the metasyntactic variable name should be in lower case.
-- 
Dr Richard Stallman (https://stallman.org)
Chief GNUisance of the GNU Project (https://gnu.org)
Founder, Free Software Foundation (https://fsf.org)
Internet Hall-of-Famer (https://internethalloffame.org)







^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-16 17:00                                       ` Michael Albinus
@ 2022-10-16 23:01                                         ` Jim Porter
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Porter @ 2022-10-16 23:01 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 57556

On 10/16/2022 10:00 AM, Michael Albinus wrote:
> I see. Yes, connection-local-update-profile-variables should do
> better. But I don't understand why its implementation doesn't use the
> existing connection-local-get-profile-variables.

Just forgetfulness on my part. :)





^ permalink raw reply	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-16 20:51                                   ` Richard Stallman
@ 2022-10-16 23:07                                     ` Jim Porter
  2022-10-18  1:51                                       ` Jim Porter
  0 siblings, 1 reply; 32+ messages in thread
From: Jim Porter @ 2022-10-16 23:07 UTC (permalink / raw)
  To: rms; +Cc: michael.albinus, 57556

[-- Attachment #1: Type: text/plain, Size: 970 bytes --]

On 10/16/2022 1:51 PM, Richard Stallman wrote:
> [[[ To any NSA and FBI agents reading my email: please consider    ]]]
> [[[ whether defending the US Constitution against all enemies,     ]]]
> [[[ foreign or domestic, requires you to follow Snowden's example. ]]]
> 
>    > My intent was to make that display as all-caps in the HTML documentation
>    > as well. In that excerpt, 'NAME' should always be an environment
>    > variable, so I used the capitalization conventions that env vars usually
>    > use. 'name', on the other hand, could be a Lisp variable or an env var.
> 
> It should be @var{name} to express that it is a metasyntactic variable
> that stands for something called ``name''.

Thanks. I've fixed my changes in the Eshell manual to use this 
convention now, and also added some further cross-references.

I've attached (hopefully) the final version of these patches, which I'll 
merge in the next day or so, unless someone finds any other issues.

[-- Attachment #2: 0001-Remove-over-quoting-of-application-values-in-connect.patch --]
[-- Type: text/plain, Size: 3206 bytes --]

From 046d15f14fcdf5816072c5dcb844f9dd4882051c Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Wed, 12 Oct 2022 11:28:05 -0700
Subject: [PATCH 1/7] ; Remove over-quoting of :application values in
 connection-local variables

* test/lisp/files-x-tests.el (files-x-test--application)
(files-x-test--another-application):
* doc/lispref/variables.texi (Connection Local Variables): Remove
extra quotes.
---
 doc/lispref/variables.texi | 14 +++++++-------
 test/lisp/files-x-tests.el |  4 ++--
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi
index 1d891618da..2a06169b21 100644
--- a/doc/lispref/variables.texi
+++ b/doc/lispref/variables.texi
@@ -2311,13 +2311,13 @@ Connection Local Variables
 @example
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "ssh" :machine "localhost")
+  '(:application tramp :protocol "ssh" :machine "localhost")
   'remote-bash 'remote-null-device)
 @end group
 
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "sudo"
+  '(:application tramp :protocol "sudo"
     :user "root" :machine "localhost")
   'remote-ksh 'remote-null-device)
 @end group
@@ -2329,13 +2329,13 @@ Connection Local Variables
 @example
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "ssh" :machine "localhost")
+  '(:application tramp :protocol "ssh" :machine "localhost")
   'remote-bash)
 @end group
 
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "sudo"
+  '(:application tramp :protocol "sudo"
     :user "root" :machine "localhost")
   'remote-ksh)
 @end group
@@ -2365,7 +2365,7 @@ Connection Local Variables
 @example
 @group
 (hack-connection-local-variables
-  '(:application 'tramp :protocol "ssh" :machine "localhost"))
+  '(:application tramp :protocol "ssh" :machine "localhost"))
 @end group
 
 @group
@@ -2401,7 +2401,7 @@ Connection Local Variables
 
 @group
 (connection-local-set-profiles
-  '(:application 'tramp :protocol "ssh" :machine "remotehost")
+  '(:application tramp :protocol "ssh" :machine "remotehost")
   'remote-perl)
 @end group
 
@@ -2429,7 +2429,7 @@ Connection Local Variables
 
 @group
 (connection-local-set-profiles
-  '(:application 'my-app :protocol "ssh" :machine "remotehost")
+  '(:application my-app :protocol "ssh" :machine "remotehost")
   'my-remote-perl)
 @end group
 
diff --git a/test/lisp/files-x-tests.el b/test/lisp/files-x-tests.el
index 7ee2f0c1a6..2f6d0d4a99 100644
--- a/test/lisp/files-x-tests.el
+++ b/test/lisp/files-x-tests.el
@@ -42,9 +42,9 @@ remote-null-device
 (put 'remote-shell-login-switch 'safe-local-variable #'identity)
 (put 'remote-null-device 'safe-local-variable #'identity)
 
-(defconst files-x-test--application '(:application 'my-application))
+(defconst files-x-test--application '(:application my-application))
 (defconst files-x-test--another-application
-  '(:application 'another-application))
+  '(:application another-application))
 (defconst files-x-test--protocol '(:protocol "my-protocol"))
 (defconst files-x-test--user '(:user "my-user"))
 (defconst files-x-test--machine '(:machine "my-machine"))
-- 
2.25.1


[-- Attachment #3: 0002-Add-helpers-to-dynamically-assign-connection-local-v.patch --]
[-- Type: text/plain, Size: 24536 bytes --]

From 28c5a764fe9b1042ee2af64f710a4c4fdedcae43 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Tue, 11 Oct 2022 22:11:04 -0700
Subject: [PATCH 2/7] Add helpers to dynamically assign connection-local values

* lisp/files-x.el (connection-local-criteria)
(connection-local-profile-name-for-setq): New variables.
(with-connection-local-variables-1): ... let-bind them here.
(connection-local-update-profile-variables)
(connection-local-profile-name-for-criteria): New functions.
(with-connection-local-application-variables, setq-connection-local):
New macros.

* test/lisp/files-x-tests.el: Require 'tramp-integration'
(files-x-test--variable5, remote-lazy-var): New variables.
(files-x-test-hack-connection-local-variables-apply): Expand checks.
(files-x-test-with-connection-local-variables): Remove
'hack-connection-local-variables-apply' check (it belongs in the above
test), and expand some other checks.
(files-x-test--get-lazy-var, files-x-test--set-lazy-var): New
functions.
(files-x-test-connection-local-update-profile-variables)
(files-x-test-setq-connection-local): New tests.

* doc/lispref/variables.texi (Connection Local Variables): Split into
two subsections and document the new features.

* etc/NEWS: Announce 'setq-connection-local'.
---
 doc/lispref/variables.texi |  98 ++++++++++++++++++------
 etc/NEWS                   |   7 ++
 lisp/files-x.el            | 103 ++++++++++++++++++++++++--
 test/lisp/files-x-tests.el | 148 +++++++++++++++++++++++++++----------
 4 files changed, 288 insertions(+), 68 deletions(-)

diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi
index 2a06169b21..cbe276b2dc 100644
--- a/doc/lispref/variables.texi
+++ b/doc/lispref/variables.texi
@@ -2239,9 +2239,26 @@ Connection Local Variables
 @cindex connection local variables
 
   Connection-local variables provide a general mechanism for different
-variable settings in buffers with a remote connection.  They are bound
+variable settings in buffers with a remote connection (@pxref{Remote
+Files,, Remote Files, emacs, The GNU Emacs Manual}).  They are bound
 and set depending on the remote connection a buffer is dedicated to.
 
+@menu
+* Connection Local Profiles::            Storing variable settings to
+                                         apply to connections.
+* Applying Connection Local Variables::  Using connection-local values
+                                         in your code.
+@end menu
+
+@node Connection Local Profiles
+@subsection Connection Local Profiles
+@cindex connection local profiles
+
+  Emacs uses connection-local profiles to store the variable settings
+to apply to particular connections.  You can then associate these with
+remote connections by defining the criteria when they should apply,
+using @code{connection-local-set-profiles}.
+
 @defun connection-local-set-profile-variables profile variables
 This function defines a set of variable settings for the connection
 @var{profile}, which is a symbol.  You can later assign the connection
@@ -2356,6 +2373,14 @@ Connection Local Variables
 list.
 @end deffn
 
+@node Applying Connection Local Variables
+@subsection Applying Connection Local Variables
+@cindex connection local variables, applying
+
+  When writing connection-aware code, you'll need to collect, and
+possibly apply, any connection-local variables.  There are several
+ways to do this, as described below.
+
 @defun hack-connection-local-variables criteria
 This function collects applicable connection-local variables
 associated with @var{criteria} in
@@ -2384,9 +2409,9 @@ Connection Local Variables
 @var{criteria}, and immediately applies them in the current buffer.
 @end defun
 
-@defmac with-connection-local-variables &rest body
-All connection-local variables, which are specified by
-@code{default-directory}, are applied.
+@defmac with-connection-local-application-variables application &rest body
+Apply all connection-local variables for @code{application}, which are
+specified by @code{default-directory}.
 
 After that, @var{body} is executed, and the connection-local variables
 are unwound.  Example:
@@ -2394,20 +2419,20 @@ Connection Local Variables
 @example
 @group
 (connection-local-set-profile-variables
-  'remote-perl
-  '((perl-command-name . "/usr/local/bin/perl")
+  'my-remote-perl
+  '((perl-command-name . "/usr/local/bin/perl5")
     (perl-command-switch . "-e %s")))
 @end group
 
 @group
 (connection-local-set-profiles
-  '(:application tramp :protocol "ssh" :machine "remotehost")
-  'remote-perl)
+  '(:application my-app :protocol "ssh" :machine "remotehost")
+  'my-remote-perl)
 @end group
 
 @group
 (let ((default-directory "/ssh:remotehost:/working/dir/"))
-  (with-connection-local-variables
+  (with-connection-local-application-variables 'my-app
     do something useful))
 @end group
 @end example
@@ -2416,30 +2441,59 @@ Connection Local Variables
 @defvar connection-local-default-application
 The default application, a symbol, to be applied in
 @code{with-connection-local-variables}.  It defaults to @code{tramp},
-but in case you want to overwrite Tramp's settings temporarily, you
-could let-bind it like
+but you can let-bind it to change the application temporarily
+(@pxref{Local Variables}).
+
+This variable must not be changed globally.
+@end defvar
+
+@defmac with-connection-local-variables &rest body
+This is equivalent to
+@code{with-connection-local-application-variables}, but uses
+@code{connection-local-default-application} for the application.
+@end defmac
+
+@defmac setq-connection-local [symbol form]@dots{}
+This macro sets each @var{symbol} connection-locally to the result of
+evaluating the corresponding @var{form}, using the connection-local
+profile specified in @code{connection-local-profile-name-for-setq}; if
+the profile name is @code{nil}, this macro will just set the variables
+normally, as with @code{setq} (@pxref{Setting Variables}).
+
+For example, you can use this macro in combination with
+@code{with-connection-local-variables} or
+@code{with-connection-local-application-variables} to lazily
+initialize connection-local settings:
 
 @example
 @group
+(defvar my-app-variable nil)
+
 (connection-local-set-profile-variables
-  'my-remote-perl
-  '((perl-command-name . "/usr/local/bin/perl5")
-    (perl-command-switch . "-e %s")))
-@end group
+ 'my-app-connection-default-profile
+ '((my-app-variable . nil)))
 
-@group
 (connection-local-set-profiles
-  '(:application my-app :protocol "ssh" :machine "remotehost")
-  'my-remote-perl)
+ '(:application my-app)
+ 'my-app-connection-default-profile)
 @end group
 
 @group
-(let ((default-directory "/ssh:remotehost:/working/dir/")
-      (connection-local-default-application 'my-app))
-  (with-connection-local-variables
-    do something useful))
+(defun my-app-get-variable ()
+  (with-connection-local-application-variables 'my-app
+    (or my-app-variable
+        (setq-connection-local my-app-variable
+                               do something useful))))
 @end group
 @end example
+@end defmac
+
+@defvar connection-local-profile-name-for-setq
+The connection-local profile name, a symbol, to use when setting
+variables via @code{setq-connection-local}.  This is let-bound in the
+body of @code{with-connection-local-variables}, but you can also
+let-bind it yourself if you'd like to set variables on a different
+profile.
 
 This variable must not be changed globally.
 @end defvar
diff --git a/etc/NEWS b/etc/NEWS
index 9641587052..72b2331b81 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -3214,6 +3214,13 @@ TIMEOUT is the idle time after which to deactivate the transient map.
 The default timeout value can be defined by the new variable
 'set-transient-map-timeout'.
 
++++
+** New macro 'setq-connection-local'.
+This allows dynamically setting variable values for a particular
+connection within the body of 'with-connection-local-variables'.  See
+the "(elisp) Connection Local Variables" node in the Lisp Reference
+manual for more information.
+
 +++
 ** 'plist-get', 'plist-put' and 'plist-member' are no longer limited to 'eq'.
 These function now take an optional comparison predicate argument.
diff --git a/lisp/files-x.el b/lisp/files-x.el
index f6d5d6cc27..1ae6586e70 100644
--- a/lisp/files-x.el
+++ b/lisp/files-x.el
@@ -618,6 +618,18 @@ connection-local-criteria-alist
   :group 'tramp
   :version "29.1")
 
+(defvar connection-local-criteria nil
+  "The current connection-local criteria, or nil.
+This is set while executing the body of
+`with-connection-local-variables'.")
+
+(defvar connection-local-profile-name-for-setq nil
+  "The current connection-local profile name, or nil.
+This is the name of the profile to use when setting variables via
+`setq-connection-local'.  Its value is derived from
+`connection-local-criteria' and is set while executing the body
+of `with-connection-local-variables'.")
+
 (defsubst connection-local-normalize-criteria (criteria)
   "Normalize plist CRITERIA according to properties.
 Return a reordered plist."
@@ -694,6 +706,23 @@ connection-local-set-profile-variables
   (customize-set-variable
    'connection-local-profile-alist connection-local-profile-alist))
 
+;;;###autoload
+(defun connection-local-update-profile-variables (profile variables)
+  "Update the variable settings for PROFILE in-place.
+VARIABLES is a list that declares connection-local variables for
+the connection profile.  An element in VARIABLES is an alist
+whose elements are of the form (VAR . VALUE).
+
+Unlike `connection-local-set-profile-variables' (which see), this
+function preserves the values of any existing variable
+definitions that aren't listed in VARIABLES."
+  (when-let ((existing-variables
+              (nreverse (connection-local-get-profile-variables profile))))
+    (dolist (var variables)
+      (setf (alist-get (car var) existing-variables) (cdr var)))
+    (setq variables (nreverse existing-variables)))
+  (connection-local-set-profile-variables profile variables))
+
 (defun hack-connection-local-variables (criteria)
   "Read connection-local variables according to CRITERIA.
 Store the connection-local variables in buffer local
@@ -736,6 +765,15 @@ connection-local-criteria-for-default-directory
       :user        ,(file-remote-p default-directory 'user)
       :machine     ,(file-remote-p default-directory 'host))))
 
+(defun connection-local-profile-name-for-criteria (criteria)
+  "Get a connection-local profile name based on CRITERIA."
+  (when criteria
+    (let (print-level print-length)
+      (intern (concat
+               "autogenerated-connection-local-profile/"
+               (prin1-to-string
+                (connection-local-normalize-criteria criteria)))))))
+
 ;;;###autoload
 (defmacro with-connection-local-variables (&rest body)
   "Apply connection-local variables according to `default-directory'.
@@ -743,16 +781,28 @@ with-connection-local-variables
   (declare (debug t))
   `(with-connection-local-variables-1 (lambda () ,@body)))
 
+;;;###autoload
+(defmacro with-connection-local-application-variables (application &rest body)
+  "Apply connection-local variables for APPLICATION in `default-directory'.
+Execute BODY, and unwind connection-local variables."
+  (declare (debug t) (indent 1))
+  `(let ((connection-local-default-application ,application))
+     (with-connection-local-variables-1 (lambda () ,@body))))
+
 ;;;###autoload
 (defun with-connection-local-variables-1 (body-fun)
   "Apply connection-local variables according to `default-directory'.
 Call BODY-FUN with no args, and then unwind connection-local variables."
   (if (file-remote-p default-directory)
-      (let ((enable-connection-local-variables t)
-            (old-buffer-local-variables (buffer-local-variables))
-	    connection-local-variables-alist)
-	(hack-connection-local-variables-apply
-	 (connection-local-criteria-for-default-directory))
+      (let* ((enable-connection-local-variables t)
+             (connection-local-criteria
+              (connection-local-criteria-for-default-directory))
+             (connection-local-profile-name-for-setq
+              (connection-local-profile-name-for-criteria
+               connection-local-criteria))
+             (old-buffer-local-variables (buffer-local-variables))
+	     connection-local-variables-alist)
+	(hack-connection-local-variables-apply connection-local-criteria)
 	(unwind-protect
             (funcall body-fun)
 	  ;; Cleanup.
@@ -764,6 +814,49 @@ with-connection-local-variables-1
     ;; No connection-local variables to apply.
     (funcall body-fun)))
 
+;;;###autoload
+(defmacro setq-connection-local (&rest pairs)
+  "Set each VARIABLE connection-locally to VALUE.
+
+When `connection-local-profile-name-for-setq' is set, assign each
+variable's value on that connection profile, and set that profile
+for `connection-local-criteria'.  You can use this in combination
+with `with-connection-local-variables', as in
+
+  (with-connection-local-variables
+    (setq-connection-local VARIABLE VALUE))
+
+If there's no connection-local profile to use, just set the
+variables normally, as with `setq'.
+
+The variables are literal symbols and should not be quoted.  The
+second VALUE is not computed until after the first VARIABLE is
+set, and so on; each VALUE can use the new value of variables set
+earlier in the `setq-connection-local'.  The return value of the
+`setq-connection-local' form is the value of the last VALUE.
+
+\(fn [VARIABLE VALUE]...)"
+  (declare (debug setq))
+  (unless (zerop (mod (length pairs) 2))
+    (error "PAIRS must have an even number of variable/value members"))
+  (let ((set-expr nil)
+        (profile-vars nil))
+    (while pairs
+      (unless (symbolp (car pairs))
+        (error "Attempting to set a non-symbol: %s" (car pairs)))
+      (push `(set ',(car pairs) ,(cadr pairs)) set-expr)
+      (push `(cons ',(car pairs) ,(car pairs)) profile-vars)
+      (setq pairs (cddr pairs)))
+    `(prog1
+         ,(macroexp-progn (nreverse set-expr))
+       (when connection-local-profile-name-for-setq
+         (connection-local-update-profile-variables
+          connection-local-profile-name-for-setq
+          (list ,@(nreverse profile-vars)))
+         (connection-local-set-profiles
+          connection-local-criteria
+          connection-local-profile-name-for-setq)))))
+
 ;;;###autoload
 (defun path-separator ()
   "The connection-local value of `path-separator'."
diff --git a/test/lisp/files-x-tests.el b/test/lisp/files-x-tests.el
index 2f6d0d4a99..b1555a0266 100644
--- a/test/lisp/files-x-tests.el
+++ b/test/lisp/files-x-tests.el
@@ -23,6 +23,7 @@
 
 (require 'ert)
 (require 'files-x)
+(require 'tramp-integration)
 
 (defconst files-x-test--variables1
   '((remote-shell-file-name . "/bin/bash")
@@ -35,7 +36,11 @@ files-x-test--variables3
   '((remote-null-device . "/dev/null")))
 (defconst files-x-test--variables4
   '((remote-null-device . "null")))
+(defconst files-x-test--variables5
+  '((remote-lazy-var . nil)
+    (remote-null-device . "/dev/null")))
 (defvar remote-null-device)
+(defvar remote-lazy-var nil)
 (put 'remote-shell-file-name 'safe-local-variable #'identity)
 (put 'remote-shell-command-switch 'safe-local-variable #'identity)
 (put 'remote-shell-interactive-switch 'safe-local-variable #'identity)
@@ -91,6 +96,28 @@ files-x-test-connection-local-set-profile-variables
       (connection-local-get-profile-variables 'remote-nullfile)
       files-x-test--variables4))))
 
+(ert-deftest files-x-test-connection-local-update-profile-variables ()
+  "Test updating connection-local profile variables."
+
+  ;; Declare (PROFILE VARIABLES) objects.
+  (let (connection-local-profile-alist connection-local-criteria-alist)
+    (connection-local-set-profile-variables
+     'remote-bash (copy-alist files-x-test--variables1))
+    (should
+     (equal
+      (connection-local-get-profile-variables 'remote-bash)
+      files-x-test--variables1))
+
+    ;; Updating overwrites only the values specified in this call, but
+    ;; retains all the other values from previous calls.
+    (connection-local-update-profile-variables
+     'remote-bash files-x-test--variables2)
+    (should
+     (equal
+      (connection-local-get-profile-variables 'remote-bash)
+      (cons (car files-x-test--variables2)
+            (cdr files-x-test--variables1))))))
+
 (ert-deftest files-x-test-connection-local-set-profiles ()
   "Test setting connection-local profiles."
 
@@ -233,9 +260,12 @@ files-x-test-hack-connection-local-variables-apply
                  (nreverse (copy-tree files-x-test--variables2)))))
         ;; The variables exist also as local variables.
         (should (local-variable-p 'remote-shell-file-name))
+        (should (local-variable-p 'remote-null-device))
         ;; The proper variable value is set.
         (should
-         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))))
+         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
+        (should
+         (string-equal (symbol-value 'remote-null-device) "/dev/null"))))
 
     ;; The third test case.  Both criteria `files-x-test--criteria1'
     ;; and `files-x-test--criteria2' apply, but there are no double
@@ -274,13 +304,11 @@ files-x-test-hack-connection-local-variables-apply
         (should-not (local-variable-p 'remote-shell-file-name))
         (should-not (boundp 'remote-shell-file-name))))))
 
-(defvar tramp-connection-local-default-shell-variables)
-(defvar tramp-connection-local-default-system-variables)
-
 (ert-deftest files-x-test-with-connection-local-variables ()
   "Test setting connection-local variables."
 
-  (let (connection-local-profile-alist connection-local-criteria-alist)
+  (let ((connection-local-profile-alist connection-local-profile-alist)
+        (connection-local-criteria-alist connection-local-criteria-alist))
     (connection-local-set-profile-variables
      'remote-bash files-x-test--variables1)
     (connection-local-set-profile-variables
@@ -291,29 +319,6 @@ files-x-test-with-connection-local-variables
     (connection-local-set-profiles
      nil 'remote-ksh 'remote-nullfile)
 
-    (with-temp-buffer
-      (let ((enable-connection-local-variables t))
-        (hack-connection-local-variables-apply nil)
-
-	;; All connection-local variables are set.  They apply in
-        ;; reverse order in `connection-local-variables-alist'.
-        (should
-         (equal connection-local-variables-alist
-		(append
-		 (nreverse (copy-tree files-x-test--variables3))
-		 (nreverse (copy-tree files-x-test--variables2)))))
-        ;; The variables exist also as local variables.
-        (should (local-variable-p 'remote-shell-file-name))
-        (should (local-variable-p 'remote-null-device))
-        ;; The proper variable values are set.
-        (should
-         (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
-        (should
-         (string-equal (symbol-value 'remote-null-device) "/dev/null"))
-
-	;; A candidate connection-local variable is not bound yet.
-        (should-not (local-variable-p 'remote-shell-command-switch))))
-
     (with-temp-buffer
       ;; Use the macro.  We need a remote `default-directory'.
       (let ((enable-connection-local-variables t)
@@ -331,18 +336,18 @@ files-x-test-with-connection-local-variables
 	(with-connection-local-variables
 	 ;; All connection-local variables are set.  They apply in
 	 ;; reverse order in `connection-local-variables-alist'.
-	 ;; Since we ha a remote default directory, Tramp's settings
+	 ;; Since we have a remote default directory, Tramp's settings
 	 ;; are appended as well.
          (should
           (equal
            connection-local-variables-alist
 	   (append
-	    (nreverse (copy-tree files-x-test--variables3))
-	    (nreverse (copy-tree files-x-test--variables2))
             (nreverse
              (copy-tree tramp-connection-local-default-shell-variables))
             (nreverse
-             (copy-tree tramp-connection-local-default-system-variables)))))
+             (copy-tree tramp-connection-local-default-system-variables))
+	    (nreverse (copy-tree files-x-test--variables3))
+	    (nreverse (copy-tree files-x-test--variables2)))))
          ;; The variables exist also as local variables.
          (should (local-variable-p 'remote-shell-file-name))
          (should (local-variable-p 'remote-null-device))
@@ -352,15 +357,21 @@ files-x-test-with-connection-local-variables
          (should
           (string-equal (symbol-value 'remote-null-device) "/dev/null"))
 
-         ;; Run another instance of `with-connection-local-variables'
-         ;; with a different application.
-         (let ((connection-local-default-application (cadr files-x-test--application)))
-	   (with-connection-local-variables
-            ;; The proper variable values are set.
-            (should
-             (string-equal (symbol-value 'remote-shell-file-name) "/bin/bash"))
-            (should
-             (string-equal (symbol-value 'remote-null-device) "/dev/null"))))
+         ;; Run `with-connection-local-application-variables' to use a
+         ;; different application.
+	 (with-connection-local-application-variables
+             (cadr files-x-test--application)
+         (should
+          (equal
+           connection-local-variables-alist
+	   (append
+	    (nreverse (copy-tree files-x-test--variables3))
+	    (nreverse (copy-tree files-x-test--variables1)))))
+           ;; The proper variable values are set.
+           (should
+            (string-equal (symbol-value 'remote-shell-file-name) "/bin/bash"))
+           (should
+            (string-equal (symbol-value 'remote-null-device) "/dev/null")))
          ;; The variable values are reset.
          (should
           (string-equal (symbol-value 'remote-shell-file-name) "/bin/ksh"))
@@ -376,5 +387,60 @@ files-x-test-with-connection-local-variables
 	(should-not (boundp 'remote-shell-file-name))
 	(should (string-equal (symbol-value 'remote-null-device) "null"))))))
 
+(defun files-x-test--get-lazy-var ()
+  "Get the connection-local value of `remote-lazy-var'.
+If it's not initialized yet, initialize it."
+  (with-connection-local-application-variables
+      (cadr files-x-test--application)
+    (or remote-lazy-var
+        (setq-connection-local remote-lazy-var
+                               (or (file-remote-p default-directory 'host)
+                                   "local")))))
+
+(defun files-x-test--set-lazy-var (value)
+  "Set the connection-local value of `remote-lazy-var'"
+  (with-connection-local-application-variables
+      (cadr files-x-test--application)
+    (setq-connection-local remote-lazy-var value)))
+
+(ert-deftest files-x-test-setq-connection-local ()
+  "Test dynamically setting connection local variables."
+  (let (connection-local-profile-alist connection-local-criteria-alist)
+    (connection-local-set-profile-variables
+     'remote-lazy files-x-test--variables5)
+    (connection-local-set-profiles
+     files-x-test--application
+     'remote-lazy)
+
+    ;; Test the initial local value.
+    (should (equal (files-x-test--get-lazy-var) "local"))
+
+    ;; Set the local value and make sure it retains the value we set.
+    (should (equal (files-x-test--set-lazy-var "here") "here"))
+    (should (equal (files-x-test--get-lazy-var) "here"))
+
+    (let ((default-directory "/method:host:"))
+      ;; Test the initial remote value.
+      (should (equal (files-x-test--get-lazy-var) "host"))
+
+      ;; Set the remote value and make sure it retains the value we set.
+      (should (equal (files-x-test--set-lazy-var "there") "there"))
+      (should (equal (files-x-test--get-lazy-var) "there"))
+      ;; Set another connection-local variable.
+      (with-connection-local-application-variables
+          (cadr files-x-test--application)
+        (setq-connection-local remote-null-device "null")))
+
+    ;; Make sure we get the local value we set above.
+    (should (equal (files-x-test--get-lazy-var) "here"))
+    (should-not (boundp 'remote-null-device))
+
+    ;; Make sure we get the remote values we set above.
+    (let ((default-directory "/method:host:"))
+      (should (equal (files-x-test--get-lazy-var) "there"))
+      (with-connection-local-application-variables
+          (cadr files-x-test--application)
+        (should (equal remote-null-device "null"))))))
+
 (provide 'files-x-tests)
 ;;; files-x-tests.el ends here
-- 
2.25.1


[-- Attachment #4: 0003-Allow-ignoring-errors-when-calling-eshell-match-comm.patch --]
[-- Type: text/plain, Size: 3827 bytes --]

From e0224cb5c9277b7059e546a3b93fb52ad7b183d2 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sat, 24 Sep 2022 18:13:03 -0700
Subject: [PATCH 3/7] ; Allow ignoring errors when calling
 'eshell-match-command-output'

* test/lisp/eshell/eshell-tests-helpers.el
(eshell-match-command-output): New argument IGNORE-ERRORS.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/last-status-var-lisp-command)
(esh-var-test/last-status-var-lisp-form)
(esh-var-test/last-status-var-lisp-form-2): Ignore errors when calling
'eshell-match-command-output'.
---
 test/lisp/eshell/esh-var-tests.el        | 15 ++++++---------
 test/lisp/eshell/eshell-tests-helpers.el | 13 ++++++++++---
 2 files changed, 16 insertions(+), 12 deletions(-)

diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
index cb5b1766bb..ad695e45d7 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -472,9 +472,8 @@ esh-var-test/last-status-var-lisp-command
                                 "t\n0\n")
    (eshell-match-command-output "zerop 1; echo $?"
                                 "0\n")
-   (let ((debug-on-error nil))
-     (eshell-match-command-output "zerop foo; echo $?"
-                                  "1\n"))))
+   (eshell-match-command-output "zerop foo; echo $?"
+                                "1\n" nil t)))
 
 (ert-deftest esh-var-test/last-status-var-lisp-form ()
   "Test using the \"last exit status\" ($?) variable with a Lisp form"
@@ -484,9 +483,8 @@ esh-var-test/last-status-var-lisp-form
                                   "t\n0\n")
      (eshell-match-command-output "(zerop 1); echo $?"
                                   "2\n")
-     (let ((debug-on-error nil))
-       (eshell-match-command-output "(zerop \"foo\"); echo $?"
-                                    "1\n")))))
+     (eshell-match-command-output "(zerop \"foo\"); echo $?"
+                                  "1\n" nil t))))
 
 (ert-deftest esh-var-test/last-status-var-lisp-form-2 ()
   "Test using the \"last exit status\" ($?) variable with a Lisp form.
@@ -497,9 +495,8 @@ esh-var-test/last-status-var-lisp-form-2
                                   "0\n")
      (eshell-match-command-output "(zerop 0); echo $?"
                                   "0\n")
-     (let ((debug-on-error nil))
-       (eshell-match-command-output "(zerop \"foo\"); echo $?"
-                                    "1\n")))))
+     (eshell-match-command-output "(zerop \"foo\"); echo $?"
+                                  "1\n" nil t))))
 
 (ert-deftest esh-var-test/last-status-var-ext-cmd ()
   "Test using the \"last exit status\" ($?) variable with an external command"
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index 73abfcbb55..e713e162ad 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -100,9 +100,16 @@ eshell-match-output--explainer
 
 (put 'eshell-match-output 'ert-explainer #'eshell-match-output--explainer)
 
-(defun eshell-match-command-output (command regexp &optional func)
-  "Insert a COMMAND at the end of the buffer and match the output with REGEXP."
-  (eshell-insert-command command func)
+(defun eshell-match-command-output (command regexp &optional func
+                                            ignore-errors)
+  "Insert a COMMAND at the end of the buffer and match the output with REGEXP.
+FUNC is the function to call after inserting the text (see
+`eshell-insert-command').
+
+If IGNORE-ERRORS is non-nil, ignore any errors signaled when
+inserting the command."
+  (let ((debug-on-error (and (not ignore-errors) debug-on-error)))
+    (eshell-insert-command command func))
   (eshell-wait-for-subprocess)
   (should (eshell-match-output regexp)))
 
-- 
2.25.1


[-- Attachment #5: 0004-Obsolete-eshell-define.patch --]
[-- Type: text/plain, Size: 1674 bytes --]

From fbf9191ad694c9995a3985175d8b277392400757 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Wed, 28 Sep 2022 09:34:38 -0700
Subject: [PATCH 4/7] ; Obsolete 'eshell/define'

* lisp/eshell/esh-var.el (eshell/define): Make obsolete, and explain
its current state.

* doc/misc/eshell.texi (Built-ins): Remove 'define'.
---
 doc/misc/eshell.texi   | 5 -----
 lisp/eshell/esh-var.el | 5 +++++
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 0ee33f2c2a..8036bbd83a 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -439,11 +439,6 @@ Built-ins
 is similar to, but slightly different from, the GNU Coreutils
 @command{date} command.
 
-@item define
-@cmindex define
-Define a variable alias.
-@xref{Variable Aliases, , , elisp, The Emacs Lisp Reference Manual}.
-
 @item diff
 @cmindex diff
 Compare files using Emacs's internal @code{diff} (not to be confused
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index 36e59cd5a4..3c09fc52fb 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -302,6 +302,11 @@ eshell-interpolate-variable
 
 (defun eshell/define (var-alias definition)
   "Define a VAR-ALIAS using DEFINITION."
+  ;; FIXME: This function doesn't work (it produces variable aliases
+  ;; in a form not recognized by other parts of the code), and likely
+  ;; hasn't worked since before its introduction into Emacs.  It
+  ;; should either be removed or fixed up.
+  (declare (obsolete nil "29.1"))
   (if (not definition)
       (setq eshell-variable-aliases-list
 	    (delq (assoc var-alias eshell-variable-aliases-list)
-- 
2.25.1


[-- Attachment #6: 0005-Allow-setting-the-values-of-variable-aliases-in-Eshe.patch --]
[-- Type: text/plain, Size: 22178 bytes --]

From f535cc568bac5d8a90b030547aa1761f6378db9f Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sun, 25 Sep 2022 21:47:26 -0700
Subject: [PATCH 5/7] 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              |  49 ++++++++--
 lisp/eshell/esh-cmd.el            |   4 +-
 lisp/eshell/esh-var.el            | 141 +++++++++++++++++++++--------
 test/lisp/eshell/esh-var-tests.el | 145 ++++++++++++++++++++++++++++++
 4 files changed, 293 insertions(+), 46 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 8036bbd83a..21c1671a21 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -694,10 +694,18 @@ 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
+(@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 @@ 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 +891,35 @@ 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 (@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:
diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index 3f3a1616ee..c5ceb3ffd1 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"))
 
+\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
 
-- 
2.25.1


[-- Attachment #7: 0006-Improve-handling-of-PATH-in-Eshell-for-remote-direct.patch --]
[-- Type: text/plain, Size: 19675 bytes --]

From 5b915a21708332d1b25bd78d3742ae90d93473b2 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Thu, 15 Sep 2022 12:24:37 -0700
Subject: [PATCH 6/7] Improve handling of $PATH in Eshell for remote
 directories

* lisp/eshell/esh-util.el (eshell-path-env, eshell-parse-colon-path):
Make obsolete.
(eshell-path-env-list): New variable.
(eshell-connection-default-profile): New connection-local profile.
(eshell-get-path): Reimplement using 'eshell-path-env-list'.
(eshell-set-path): New function.

* lisp/eshell/esh-var.el (eshell-variable-aliases-list): Add entry for
$PATH.
(eshell-var-initialize): Add 'eshell-path-env-list' to
'eshell-subcommand-bindings'.

* lisp/eshell/esh-ext.el (eshell-search-path): Use 'file-name-concat'
instead of 'concat'.
(eshell/addpath): Use 'eshell-get-path' and 'eshell-set-path'.

* lisp/net/tramp-integration.el: Only apply Eshell hooks when
'eshell-path-env-list' is unbound.

* test/lisp/eshell/esh-var-tests.el
(esh-var-test/path-var/local-directory)
(esh-var-test/path-var/remote-directory, esh-var-test/path-var/set)
(esh-var-test/path-var/set-locally)
(esh-var-test/path-var-preserve-across-hosts): New tests.

* test/lisp/eshell/esh-ext-tests.el: New file.

* test/lisp/eshell/eshell-tests-helpers.el
(with-temp-eshell): Set 'eshell-last-dir-ring-file-name' to nil.
(eshell-tests-remote-accessible-p, eshell-last-input)
(eshell-last-output): New functions.
(eshell-match-output, eshell-match-output--explainer): Use
'eshell-last-input' and 'eshell-last-output'.

* doc/misc/eshell.texi (Variables): Document $PATH.

* etc/NEWS: Announce this change (bug#57556).
---
 doc/misc/eshell.texi                     | 10 ++++
 etc/NEWS                                 |  5 ++
 lisp/eshell/esh-ext.el                   | 23 ++++---
 lisp/eshell/esh-util.el                  | 53 +++++++++++++++--
 lisp/eshell/esh-var.el                   | 12 +++-
 lisp/net/tramp-integration.el            | 21 +++----
 test/lisp/eshell/esh-ext-tests.el        | 76 ++++++++++++++++++++++++
 test/lisp/eshell/esh-var-tests.el        | 60 +++++++++++++++++++
 test/lisp/eshell/eshell-tests-helpers.el | 32 +++++++---
 9 files changed, 255 insertions(+), 37 deletions(-)
 create mode 100644 test/lisp/eshell/esh-ext-tests.el

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 21c1671a21..d518eafd72 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -942,6 +942,16 @@ Variables
 directory ring via subscripting, e.g.@: @samp{$-[1]} refers to the
 working directory @emph{before} the previous one.
 
+@vindex $PATH
+@item $PATH
+This specifies the directories to search for executable programs.  Its
+value is a string, separated by @code{":"} for Unix and GNU systems,
+and @code{";"} for MS systems.  This variable is connection-aware, so
+whenever you change the current directory to a different host
+(@pxref{Remote Files, , , emacs, The GNU Emacs Manual}),
+the value will automatically update to reflect the search path on that
+host.
+
 @vindex $_
 @item $_
 This refers to the last argument of the last command.  With a
diff --git a/etc/NEWS b/etc/NEWS
index 72b2331b81..871060148d 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -356,6 +356,11 @@ previous 'C-x ='.
 
 ** Eshell
 
+*** Eshell's PATH is now derived from 'exec-path'.
+For consistency with remote connections, Eshell now uses 'exec-path'
+to determine the execution path on the local system, instead of using
+the PATH environment variable directly.
+
 ---
 *** 'source' and '.' no longer accept the '--help' option.
 This is for compatibility with the shell versions of these commands,
diff --git a/lisp/eshell/esh-ext.el b/lisp/eshell/esh-ext.el
index 98902fc6f2..d513d750d9 100644
--- a/lisp/eshell/esh-ext.el
+++ b/lisp/eshell/esh-ext.el
@@ -77,7 +77,7 @@ eshell-search-path
     (let ((list (eshell-get-path))
 	  suffixes n1 n2 file)
       (while list
-	(setq n1 (concat (car list) name))
+	(setq n1 (file-name-concat (car list) name))
 	(setq suffixes eshell-binary-suffixes)
 	(while suffixes
 	  (setq n2 (concat n1 (car suffixes)))
@@ -239,17 +239,16 @@ eshell/addpath
      (?h "help" nil nil  "display this usage message")
      :usage "[-b] PATH
 Adds the given PATH to $PATH.")
-   (if args
-       (progn
-	 (setq eshell-path-env (getenv "PATH")
-	       args (mapconcat #'identity args path-separator)
-	       eshell-path-env
-	       (if prepend
-		   (concat args path-separator eshell-path-env)
-		 (concat eshell-path-env path-separator args)))
-	 (setenv "PATH" eshell-path-env))
-     (dolist (dir (parse-colon-path (getenv "PATH")))
-       (eshell-printn dir)))))
+   (let ((path (eshell-get-path t)))
+     (if args
+         (progn
+           (setq path (if prepend
+                          (append args path)
+                        (append path args)))
+           (eshell-set-path path)
+           (string-join path (path-separator)))
+       (dolist (dir path)
+         (eshell-printn dir))))))
 
 (put 'eshell/addpath 'eshell-no-numeric-conversions t)
 (put 'eshell/addpath 'eshell-filename-arguments t)
diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el
index 9258ca5e40..55983b1feb 100644
--- a/lisp/eshell/esh-util.el
+++ b/lisp/eshell/esh-util.el
@@ -249,17 +249,58 @@ eshell-path-env
 It might be different from \(getenv \"PATH\"), when
 `default-directory' points to a remote host.")
 
-(defun eshell-get-path ()
+(make-obsolete-variable 'eshell-path-env 'eshell-get-path "29.1")
+
+(defvar-local eshell-path-env-list nil)
+
+(connection-local-set-profile-variables
+ 'eshell-connection-default-profile
+ '((eshell-path-env-list . nil)))
+
+(connection-local-set-profiles
+ '(:application eshell)
+ 'eshell-connection-default-profile)
+
+(defun eshell-get-path (&optional local-part)
   "Return $PATH as a list.
-Add the current directory on MS-Windows."
-  (eshell-parse-colon-path
-   (if (eshell-under-windows-p)
-       (concat "." path-separator eshell-path-env)
-     eshell-path-env)))
+If LOCAL-PART is non-nil, only return the local part of the path.
+Otherwise, return the full, possibly-remote path.
+
+On MS-Windows, add the current directory as the first directory
+in the path."
+  (with-connection-local-application-variables 'eshell
+    (let ((remote (file-remote-p default-directory))
+          (path
+           (or eshell-path-env-list
+               ;; If not already cached, get the path from
+               ;; `exec-path', removing the last element, which is
+               ;; `exec-directory'.
+               (setq-connection-local eshell-path-env-list
+                                      (butlast (exec-path))))))
+      (when (and (eshell-under-windows-p)
+                 (not remote))
+        (push "." path))
+      (if (and remote (not local-part))
+          (mapcar (lambda (x) (file-name-concat remote x)) path)
+        path))))
+
+(defun eshell-set-path (path)
+  "Set the Eshell $PATH to PATH.
+PATH can be either a list of directories or a string of
+directories separated by `path-separator'."
+  (with-connection-local-application-variables 'eshell
+    (setq-connection-local
+     eshell-path-env-list
+     (if (listp path)
+	 path
+       ;; Don't use `parse-colon-path' here, since we don't want
+       ;; the additonal translations it does on each element.
+       (split-string path (path-separator))))))
 
 (defun eshell-parse-colon-path (path-env)
   "Split string with `parse-colon-path'.
 Prepend remote identification of `default-directory', if any."
+  (declare (obsolete nil "29.1"))
   (let ((remote (file-remote-p default-directory)))
     (if remote
 	(mapcar
diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el
index caf143e1a1..57ea42f493 100644
--- a/lisp/eshell/esh-var.el
+++ b/lisp/eshell/esh-var.el
@@ -156,7 +156,14 @@ eshell-variable-aliases-list
     ("LINES" ,(lambda () (window-body-height nil 'remap)) t t)
     ("INSIDE_EMACS" eshell-inside-emacs t)
 
-    ;; for eshell-cmd.el
+    ;; for esh-ext.el
+    ("PATH" (,(lambda () (string-join (eshell-get-path t) (path-separator)))
+             . ,(lambda (_ value)
+                  (eshell-set-path value)
+                  value))
+     t t)
+
+    ;; for esh-cmd.el
     ("_" ,(lambda (indices quoted)
 	    (if (not indices)
 	        (car (last eshell-last-arguments))
@@ -249,7 +256,8 @@ eshell-var-initialize
   (setq-local eshell-subcommand-bindings
               (append
                '((process-environment (eshell-copy-environment))
-                 (eshell-variable-aliases-list eshell-variable-aliases-list))
+                 (eshell-variable-aliases-list eshell-variable-aliases-list)
+                 (eshell-path-env-list eshell-path-env-list))
                eshell-subcommand-bindings))
 
   (setq-local eshell-special-chars-inside-quoting
diff --git a/lisp/net/tramp-integration.el b/lisp/net/tramp-integration.el
index 35c0636b1c..4be019edd9 100644
--- a/lisp/net/tramp-integration.el
+++ b/lisp/net/tramp-integration.el
@@ -136,16 +136,17 @@ tramp-eshell-directory-change
           (getenv "PATH"))))
 
 (with-eval-after-load 'esh-util
-  (add-hook 'eshell-mode-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'eshell-directory-change-hook
-	    #'tramp-eshell-directory-change)
-  (add-hook 'tramp-integration-unload-hook
-	    (lambda ()
-	      (remove-hook 'eshell-mode-hook
-			   #'tramp-eshell-directory-change)
-	      (remove-hook 'eshell-directory-change-hook
-			   #'tramp-eshell-directory-change))))
+  (unless (boundp 'eshell-path-env-list)
+    (add-hook 'eshell-mode-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'eshell-directory-change-hook
+	      #'tramp-eshell-directory-change)
+    (add-hook 'tramp-integration-unload-hook
+	      (lambda ()
+	        (remove-hook 'eshell-mode-hook
+			     #'tramp-eshell-directory-change)
+	        (remove-hook 'eshell-directory-change-hook
+			     #'tramp-eshell-directory-change)))))
 
 ;;; Integration of recentf.el:
 
diff --git a/test/lisp/eshell/esh-ext-tests.el b/test/lisp/eshell/esh-ext-tests.el
new file mode 100644
index 0000000000..54191e9409
--- /dev/null
+++ b/test/lisp/eshell/esh-ext-tests.el
@@ -0,0 +1,76 @@
+;;; esh-ext-tests.el --- esh-ext test suite  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's external command handling.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'esh-ext)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+         (expand-file-name "eshell-tests-helpers"
+                           (file-name-directory (or load-file-name
+                                                    default-directory))))
+
+;;; Tests:
+
+(ert-deftest esh-ext-test/addpath/end ()
+  "Test that \"addpath\" adds paths to the end of $PATH."
+  (with-temp-eshell
+   (let ((eshell-path-env-list '("/some/path" "/other/path"))
+         (expected-path (string-join '("/some/path" "/other/path" "/new/path"
+                                       "/new/path2")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/begin ()
+  "Test that \"addpath -b\" adds paths to the beginning of $PATH."
+  (with-temp-eshell
+   (let ((eshell-path-env-list '("/some/path" "/other/path"))
+         (expected-path (string-join '("/new/path" "/new/path2" "/some/path"
+                                       "/other/path")
+                                     (path-separator))))
+     (eshell-match-command-output "addpath -b /new/path /new/path2"
+                                  (concat expected-path "\n"))
+     (eshell-match-command-output "echo $PATH"
+                                  (concat expected-path "\n")))))
+
+(ert-deftest esh-ext-test/addpath/set-locally ()
+  "Test adding to the path temporarily in a subcommand."
+  (let* ((eshell-path-env-list '("/some/path" "/other/path"))
+         (original-path (string-join eshell-path-env-list (path-separator)))
+         (local-path (string-join (append eshell-path-env-list '("/new/path"))
+                                  (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output
+      "{ addpath /new/path; env }"
+      (format "PATH=%s\n" (regexp-quote local-path)))
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH"
+                                  (concat original-path "\n")))))
+
+;; esh-ext-tests.el ends here
diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
index a7ac52ed24..31b01c5605 100644
--- a/test/lisp/eshell/esh-var-tests.el
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -23,6 +23,7 @@
 
 ;;; Code:
 
+(require 'tramp)
 (require 'ert)
 (require 'esh-mode)
 (require 'esh-var)
@@ -610,6 +611,65 @@ esh-var-test/inside-emacs-var-split-indices
    (eshell-match-command-output "echo $INSIDE_EMACS[, 1]"
                                 "eshell")))
 
+(ert-deftest esh-var-test/path-var/local-directory ()
+  "Test using $PATH in a local directory."
+  (let ((expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path)))))
+
+(ert-deftest esh-var-test/path-var/remote-directory ()
+  "Test using $PATH in a remote directory."
+  (skip-unless (eshell-tests-remote-accessible-p))
+  (let* ((default-directory ert-remote-temporary-file-directory)
+         (expected-path (string-join (eshell-get-path t) (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output "echo $PATH" (regexp-quote expected-path)))))
+
+(ert-deftest esh-var-test/path-var/set ()
+  "Test setting $PATH."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
+(ert-deftest esh-var-test/path-var/set-locally ()
+  "Test setting $PATH temporarily for a single command."
+  (let* ((path-to-set-list '("/some/path" "/other/path"))
+         (path-to-set (string-join path-to-set-list (path-separator))))
+    (with-temp-eshell
+     (eshell-match-command-output (concat "set PATH " path-to-set)
+                                  (concat path-to-set "\n"))
+     (eshell-match-command-output "PATH=/local/path env"
+                                  "PATH=/local/path\n")
+     ;; After the last command, the previous $PATH value should be restored.
+     (eshell-match-command-output "echo $PATH" (concat path-to-set "\n"))
+     (should (equal (eshell-get-path) path-to-set-list)))))
+
+(ert-deftest esh-var-test/path-var/preserve-across-hosts ()
+  "Test that $PATH can be set independently on multiple hosts."
+  (let ((local-directory default-directory)
+        local-path remote-path)
+    (with-temp-eshell
+     ;; Set the $PATH on localhost.
+     (eshell-insert-command "set PATH /local/path")
+     (setq local-path (eshell-last-output))
+     ;; `cd' to a remote host and set the $PATH there too.
+     (eshell-insert-command
+      (format "cd %s" ert-remote-temporary-file-directory))
+     (eshell-insert-command "set PATH /remote/path")
+     (setq remote-path (eshell-last-output))
+     ;; Return to localhost and check that $PATH is the value we set
+     ;; originally.
+     (eshell-insert-command (format "cd %s" local-directory))
+     (eshell-match-command-output "echo $PATH" (regexp-quote local-path))
+     ;; ... and do the same for the remote host.
+     (eshell-insert-command
+      (format "cd %s" ert-remote-temporary-file-directory))
+     (eshell-match-command-output "echo $PATH" (regexp-quote remote-path)))))
+
 (ert-deftest esh-var-test/last-status-var-lisp-command ()
   "Test using the \"last exit status\" ($?) variable with a Lisp command"
   (with-temp-eshell
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index e713e162ad..1d9674070c 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -31,11 +31,22 @@
 (require 'eshell)
 
 (defvar eshell-history-file-name nil)
+(defvar eshell-last-dir-ring-file-name nil)
 
 (defvar eshell-test--max-subprocess-time 5
   "The maximum amount of time to wait for a subprocess to finish, in seconds.
 See `eshell-wait-for-subprocess'.")
 
+(defun eshell-tests-remote-accessible-p ()
+  "Return if a test involving remote files can proceed.
+If using this function, be sure to load `tramp' near the
+beginning of the test file."
+  (ignore-errors
+    (and
+     (file-remote-p ert-remote-temporary-file-directory)
+     (file-directory-p ert-remote-temporary-file-directory)
+     (file-writable-p ert-remote-temporary-file-directory))))
+
 (defmacro with-temp-eshell (&rest body)
   "Evaluate BODY in a temporary Eshell buffer."
   `(save-current-buffer
@@ -44,6 +55,7 @@ with-temp-eshell
               ;; back on $HISTFILE.
               (process-environment (cons "HISTFILE" process-environment))
               (eshell-history-file-name nil)
+              (eshell-last-dir-ring-file-name nil)
               (eshell-buffer (eshell t)))
          (unwind-protect
              (with-current-buffer eshell-buffer
@@ -83,19 +95,25 @@ eshell-insert-command
   (insert-and-inherit command)
   (funcall (or func 'eshell-send-input)))
 
+(defun eshell-last-input ()
+  "Return the input of the last Eshell command."
+  (buffer-substring-no-properties
+   eshell-last-input-start eshell-last-input-end))
+
+(defun eshell-last-output ()
+  "Return the output of the last Eshell command."
+  (buffer-substring-no-properties
+   (eshell-beginning-of-output) (eshell-end-of-output)))
+
 (defun eshell-match-output (regexp)
   "Test whether the output of the last command matches REGEXP."
-  (string-match-p
-    regexp (buffer-substring-no-properties
-            (eshell-beginning-of-output) (eshell-end-of-output))))
+  (string-match-p regexp (eshell-last-output)))
 
 (defun eshell-match-output--explainer (regexp)
   "Explain the result of `eshell-match-output'."
   `(mismatched-output
-    (command ,(buffer-substring-no-properties
-               eshell-last-input-start eshell-last-input-end))
-    (output ,(buffer-substring-no-properties
-              (eshell-beginning-of-output) (eshell-end-of-output)))
+    (command ,(eshell-last-input))
+    (output ,(eshell-last-output))
     (regexp ,regexp)))
 
 (put 'eshell-match-output 'ert-explainer #'eshell-match-output--explainer)
-- 
2.25.1


[-- Attachment #8: 0007-Print-the-correct-PATH-when-Eshell-s-which-fails-to-.patch --]
[-- Type: text/plain, Size: 1079 bytes --]

From 95f021dede1be4b53cd789704543bbfa945442ee Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Thu, 15 Sep 2022 12:32:02 -0700
Subject: [PATCH 7/7] Print the correct $PATH when Eshell's 'which' fails to
 find a command

* lisp/eshell/esh-cmd.el (eshell/which): Use 'eshell-get-path'
(bug#20008).
---
 lisp/eshell/esh-cmd.el | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index c5ceb3ffd1..4a41bbe8fa 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -1274,8 +1274,9 @@ eshell/which
                         name)
                   (eshell-search-path name)))))
       (if (not program)
-	  (eshell-error (format "which: no %s in (%s)\n"
-				name (getenv "PATH")))
+          (eshell-error (format "which: no %s in (%s)\n"
+                                name (string-join (eshell-get-path t)
+                                                  (path-separator))))
 	(eshell-printn program)))))
 
 (put 'eshell/which 'eshell-no-numeric-conversions t)
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded
  2022-10-16 23:07                                     ` Jim Porter
@ 2022-10-18  1:51                                       ` Jim Porter
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Porter @ 2022-10-18  1:51 UTC (permalink / raw)
  To: rms; +Cc: michael.albinus, 57556

On 10/16/2022 4:07 PM, Jim Porter wrote:
> I've attached (hopefully) the final version of these patches, which I'll 
> merge in the next day or so, unless someone finds any other issues.

Merged as fd4992d356a9c4225cb518a6a5309aaa1d0f640b. I also included a 
small fix to 'eshell-get-path' to fix a failure on MS-Windows systems.





^ permalink raw reply	[flat|nested] 32+ messages in thread

end of thread, other threads:[~2022-10-18  1:51 UTC | newest]

Thread overview: 32+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-09-03  5:03 bug#57556: 28.1; Eshell not finding executables in PATH when tramp-integration loaded Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-09-03 12:26 ` Lars Ingebrigtsen
2022-09-18 11:18 ` Michael Albinus
2022-09-18 18:54   ` Jim Porter
2022-09-18 19:07     ` Michael Albinus
2022-09-22 17:23       ` Colton Lewis via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-09-22 17:55         ` Michael Albinus
2022-09-30  3:54           ` Jim Porter
2022-10-01 20:25             ` Michael Albinus
2022-10-01 22:02               ` Jim Porter
2022-10-02  5:34                 ` Jim Porter
2022-10-02  8:48                   ` Michael Albinus
2022-10-07  3:19                     ` Jim Porter
2022-10-07 18:28                       ` Michael Albinus
2022-10-08 22:09                         ` Jim Porter
2022-10-09 18:01                           ` Michael Albinus
2022-10-13  4:11                             ` Jim Porter
2022-10-13  6:35                               ` Eli Zaretskii
2022-10-14  1:29                                 ` Jim Porter
2022-10-14  6:17                                   ` Eli Zaretskii
2022-10-14 12:28                                   ` Michael Albinus
2022-10-14 12:27                               ` Michael Albinus
2022-10-14 20:53                                 ` Jim Porter
2022-10-15 10:38                                   ` Michael Albinus
2022-10-15 23:33                                     ` Jim Porter
2022-10-16 17:00                                       ` Michael Albinus
2022-10-16 23:01                                         ` Jim Porter
2022-10-16 20:51                                   ` Richard Stallman
2022-10-16 23:07                                     ` Jim Porter
2022-10-18  1:51                                       ` Jim Porter
2022-10-10  9:15                           ` Michael Albinus
2022-10-02  8:55                 ` Michael Albinus

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).