1;;; em-dirs.el --- directory navigation commands 2 3;; Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 4;; 2005, 2006, 2007 Free Software Foundation, Inc. 5 6;; Author: John Wiegley <johnw@gnu.org> 7 8;; This file is part of GNU Emacs. 9 10;; GNU Emacs is free software; you can redistribute it and/or modify 11;; it under the terms of the GNU General Public License as published by 12;; the Free Software Foundation; either version 2, or (at your option) 13;; any later version. 14 15;; GNU Emacs is distributed in the hope that it will be useful, 16;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18;; GNU General Public License for more details. 19 20;; You should have received a copy of the GNU General Public License 21;; along with GNU Emacs; see the file COPYING. If not, write to the 22;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 23;; Boston, MA 02110-1301, USA. 24 25(provide 'em-dirs) 26 27(eval-when-compile (require 'esh-maint)) 28(require 'eshell) 29 30(defgroup eshell-dirs nil 31 "Directory navigation involves changing directories, examining the 32current directory, maintaining a directory stack, and also keeping 33track of a history of the last directory locations the user was in. 34Emacs does provide standard Lisp definitions of `pwd' and `cd', but 35they lack somewhat in feel from the typical shell equivalents." 36 :tag "Directory navigation" 37 :group 'eshell-module) 38 39;;; Commentary: 40 41;; The only special feature that Eshell offers in the last-dir-ring. 42;; To view the ring, enter: 43;; 44;; cd = 45;; 46;; Changing to an index within the ring is done using: 47;; 48;; cd - ; same as cd -0 49;; cd -4 50;; 51;; Or, it is possible to change the first member in the ring which 52;; matches a regexp: 53;; 54;; cd =bcc ; change to the last directory visited containing "bcc" 55;; 56;; This ring is maintained automatically, and is persisted across 57;; Eshell sessions. It is a separate mechanism from `pushd' and 58;; `popd', and the two may be used at the same time. 59 60(require 'ring) 61(require 'esh-opt) 62 63;;; User Variables: 64 65(defcustom eshell-dirs-load-hook '(eshell-dirs-initialize) 66 "*A hook that gets run when `eshell-dirs' is loaded." 67 :type 'hook 68 :group 'eshell-dirs) 69 70(defcustom eshell-pwd-convert-function (if (eshell-under-windows-p) 71 'expand-file-name 72 'identity) 73 "*The function used to normalize the value of Eshell's `pwd'. 74The value returned by `pwd' is also used when recording the 75last-visited directory in the last-dir-ring, so it will affect the 76form of the list used by 'cd ='." 77 :type '(radio (function-item file-truename) 78 (function-item expand-file-name) 79 (function-item identity) 80 (function :tag "Other")) 81 :group 'eshell-dirs) 82 83(defcustom eshell-ask-to-save-last-dir 'always 84 "*Determine if the last-dir-ring should be automatically saved. 85The last-dir-ring is always preserved when exiting an Eshell buffer. 86However, when Emacs is being shut down, this variable determines 87whether to prompt the user, or just save the ring. 88If set to nil, it means never ask whether to save the last-dir-ring. 89If set to t, always ask if any Eshell buffers are open at exit time. 90If set to `always', the list-dir-ring will always be saved, silently." 91 :type '(choice (const :tag "Never" nil) 92 (const :tag "Ask" t) 93 (const :tag "Always save" always)) 94 :group 'eshell-dirs) 95 96(defcustom eshell-cd-shows-directory nil 97 "*If non-nil, using `cd' will report the directory it changes to." 98 :type 'boolean 99 :group 'eshell-dirs) 100 101(defcustom eshell-cd-on-directory t 102 "*If non-nil, do a cd if a directory is in command position." 103 :type 'boolean 104 :group 'eshell-dirs) 105 106(defcustom eshell-directory-change-hook nil 107 "*A hook to run when the current directory changes." 108 :type 'hook 109 :group 'eshell-dirs) 110 111(defcustom eshell-list-files-after-cd nil 112 "*If non-nil, call \"ls\" with any remaining args after doing a cd. 113This is provided for convenience, since the same effect is easily 114achieved by adding a function to `eshell-directory-change-hook' that 115calls \"ls\" and references `eshell-last-arguments'." 116 :type 'boolean 117 :group 'eshell-dirs) 118 119(defcustom eshell-pushd-tohome nil 120 "*If non-nil, make pushd with no arg behave as 'pushd ~' (like `cd'). 121This mirrors the optional behavior of tcsh." 122 :type 'boolean 123 :group 'eshell-dirs) 124 125(defcustom eshell-pushd-dextract nil 126 "*If non-nil, make \"pushd +n\" pop the nth dir to the stack top. 127This mirrors the optional behavior of tcsh." 128 :type 'boolean 129 :group 'eshell-dirs) 130 131(defcustom eshell-pushd-dunique nil 132 "*If non-nil, make pushd only add unique directories to the stack. 133This mirrors the optional behavior of tcsh." 134 :type 'boolean 135 :group 'eshell-dirs) 136 137(defcustom eshell-dirtrack-verbose t 138 "*If non-nil, show the directory stack following directory change. 139This is effective only if directory tracking is enabled." 140 :type 'boolean 141 :group 'eshell-dirs) 142 143(defcustom eshell-last-dir-ring-file-name 144 (concat eshell-directory-name "lastdir") 145 "*If non-nil, name of the file to read/write the last-dir-ring. 146See also `eshell-read-last-dir-ring' and `eshell-write-last-dir-ring'. 147If it is nil, the last-dir-ring will not be written to disk." 148 :type 'file 149 :group 'eshell-dirs) 150 151(defcustom eshell-last-dir-ring-size 32 152 "*If non-nil, the size of the directory history ring. 153This ring is added to every time `cd' or `pushd' is used. It simply 154stores the most recent directory locations Eshell has been in. To 155return to the most recent entry, use 'cd -' (equivalent to 'cd -0'). 156To return to an older entry, use 'cd -N', where N is an integer less 157than `eshell-last-dir-ring-size'. To return to the last directory 158matching a particular regexp, use 'cd =REGEXP'. To display the 159directory history list, use 'cd ='. 160 161This mechanism is very similar to that provided by `pushd', except 162it's far more automatic. `pushd' allows the user to decide which 163directories gets pushed, and its size is unlimited. 164 165`eshell-last-dir-ring' is meant for users who don't use `pushd' 166explicity very much, but every once in a while would like to return to 167a previously visited directory without having to type in the whole 168thing again." 169 :type 'integer 170 :group 'eshell-dirs) 171 172(defcustom eshell-last-dir-unique t 173 "*If non-nil, `eshell-last-dir-ring' contains only unique entries." 174 :type 'boolean 175 :group 'eshell-dirs) 176 177;;; Internal Variables: 178 179(defvar eshell-dirstack nil 180 "List of directories saved by pushd in the Eshell buffer. 181Thus, this does not include the current directory.") 182 183(defvar eshell-last-dir-ring nil 184 "The last directory that eshell was in.") 185 186;;; Functions: 187 188(defun eshell-dirs-initialize () 189 "Initialize the builtin functions for Eshell." 190 (make-local-variable 'eshell-variable-aliases-list) 191 (setq eshell-variable-aliases-list 192 (append 193 eshell-variable-aliases-list 194 '(("-" (lambda (indices) 195 (if (not indices) 196 (unless (ring-empty-p eshell-last-dir-ring) 197 (expand-file-name 198 (ring-ref eshell-last-dir-ring 0))) 199 (expand-file-name 200 (eshell-apply-indices eshell-last-dir-ring indices))))) 201 ("+" "PWD") 202 ("PWD" (lambda (indices) 203 (expand-file-name (eshell/pwd))) t) 204 ("OLDPWD" (lambda (indices) 205 (unless (ring-empty-p eshell-last-dir-ring) 206 (expand-file-name 207 (ring-ref eshell-last-dir-ring 0)))) t)))) 208 209 (when eshell-cd-on-directory 210 (make-local-variable 'eshell-interpreter-alist) 211 (setq eshell-interpreter-alist 212 (cons (cons 'eshell-lone-directory-p 213 'eshell-dirs-substitute-cd) 214 eshell-interpreter-alist))) 215 216 (add-hook 'eshell-parse-argument-hook 217 'eshell-parse-user-reference nil t) 218 (if (eshell-under-windows-p) 219 (add-hook 'eshell-parse-argument-hook 220 'eshell-parse-drive-letter nil t)) 221 222 (when (eshell-using-module 'eshell-cmpl) 223 (add-hook 'pcomplete-try-first-hook 224 'eshell-complete-user-reference nil t)) 225 226 (make-local-variable 'eshell-dirstack) 227 (make-local-variable 'eshell-last-dir-ring) 228 229 (if eshell-last-dir-ring-file-name 230 (eshell-read-last-dir-ring)) 231 (unless eshell-last-dir-ring 232 (setq eshell-last-dir-ring (make-ring eshell-last-dir-ring-size))) 233 234 (add-hook 'eshell-exit-hook 'eshell-write-last-dir-ring nil t) 235 236 (add-hook 'kill-emacs-hook 'eshell-save-some-last-dir)) 237 238(defun eshell-save-some-last-dir () 239 "Save the list-dir-ring for any open Eshell buffers." 240 (eshell-for buf (buffer-list) 241 (if (buffer-live-p buf) 242 (with-current-buffer buf 243 (if (and eshell-mode 244 eshell-ask-to-save-last-dir 245 (or (eq eshell-ask-to-save-last-dir 'always) 246 (y-or-n-p 247 (format "Save last dir ring for Eshell buffer `%s'? " 248 (buffer-name buf))))) 249 (eshell-write-last-dir-ring)))))) 250 251(defun eshell-lone-directory-p (file) 252 "Test whether FILE is just a directory name, and not a command name." 253 (and (file-directory-p file) 254 (or (file-name-directory file) 255 (not (eshell-search-path file))))) 256 257(defun eshell-dirs-substitute-cd (&rest args) 258 "Substitute the given command for a call to `cd' on that name." 259 (if (> (length args) 1) 260 (error "%s: command not found" (car args)) 261 (throw 'eshell-replace-command 262 (eshell-parse-command "cd" (eshell-flatten-list args))))) 263 264(defun eshell-parse-user-reference () 265 "An argument beginning with ~ is a filename to be expanded." 266 (when (and (not eshell-current-argument) 267 (eq (char-after) ?~)) 268 (add-to-list 'eshell-current-modifiers 'expand-file-name) 269 (forward-char) 270 (char-to-string (char-before)))) 271 272(defun eshell-parse-drive-letter () 273 "An argument beginning X:[^/] is a drive letter reference." 274 (when (and (not eshell-current-argument) 275 (looking-at "\\([A-Za-z]:\\)\\([^/\\\\]\\|\\'\\)")) 276 (goto-char (match-end 1)) 277 (let* ((letter (match-string 1)) 278 (regexp (concat "\\`" letter)) 279 (path (eshell-find-previous-directory regexp))) 280 (concat (or path letter) "/")))) 281 282(defun eshell-complete-user-reference () 283 "If there is a user reference, complete it." 284 (let ((arg (pcomplete-actual-arg))) 285 (when (string-match "\\`~[a-z]*\\'" arg) 286 (setq pcomplete-stub (substring arg 1) 287 pcomplete-last-completion-raw t) 288 (throw 'pcomplete-completions 289 (progn 290 (eshell-read-user-names) 291 (pcomplete-uniqify-list 292 (mapcar 293 (function 294 (lambda (user) 295 (file-name-as-directory (cdr user)))) 296 eshell-user-names))))))) 297 298(defun eshell/pwd (&rest args) 299 "Change output from `pwd` to be cleaner." 300 (let* ((path default-directory) 301 (len (length path))) 302 (if (and (> len 1) 303 (eq (aref path (1- len)) ?/) 304 (not (and (eshell-under-windows-p) 305 (string-match "\\`[A-Za-z]:[\\\\/]\\'" path)))) 306 (setq path (substring path 0 (1- (length path))))) 307 (if eshell-pwd-convert-function 308 (funcall eshell-pwd-convert-function path) 309 path))) 310 311(defun eshell-expand-multiple-dots (path) 312 "Convert '...' to '../..', '....' to '../../..', etc.. 313 314With the following piece of advice, you can make this functionality 315available in most of Emacs, with the exception of filename completion 316in the minibuffer: 317 318 (defadvice expand-file-name 319 (before translate-multiple-dots 320 (filename &optional directory) activate) 321 (setq filename (eshell-expand-multiple-dots filename)))" 322 (while (string-match "\\.\\.\\(\\.+\\)" path) 323 (let* ((extra-dots (match-string 1 path)) 324 (len (length extra-dots)) 325 replace-text) 326 (while (> len 0) 327 (setq replace-text (concat replace-text "/..") 328 len (1- len))) 329 (setq path 330 (replace-match replace-text t t path 1)))) 331 path) 332 333(defun eshell-find-previous-directory (regexp) 334 "Find the most recent last-dir matching REGEXP." 335 (let ((index 0) 336 (len (ring-length eshell-last-dir-ring)) 337 oldpath) 338 (if (> (length regexp) 0) 339 (while (< index len) 340 (setq oldpath (ring-ref eshell-last-dir-ring index)) 341 (if (string-match regexp oldpath) 342 (setq index len) 343 (setq oldpath nil 344 index (1+ index))))) 345 oldpath)) 346 347(eval-when-compile 348 (defvar dired-directory)) 349 350(defun eshell/cd (&rest args) ; all but first ignored 351 "Alias to extend the behavior of `cd'." 352 (setq args (eshell-flatten-list args)) 353 (let ((path (car args)) 354 (subpath (car (cdr args))) 355 (case-fold-search (eshell-under-windows-p)) 356 handled) 357 (if (numberp path) 358 (setq path (number-to-string path))) 359 (if (numberp subpath) 360 (setq subpath (number-to-string subpath))) 361 (cond 362 (subpath 363 (let ((curdir (eshell/pwd))) 364 (if (string-match path curdir) 365 (setq path (replace-match subpath nil nil curdir)) 366 (error "Path substring '%s' not found" path)))) 367 ((and path (string-match "^-\\([0-9]*\\)$" path)) 368 (let ((index (match-string 1 path))) 369 (setq path 370 (ring-remove eshell-last-dir-ring 371 (if index 372 (string-to-number index) 373 0))))) 374 ((and path (string-match "^=\\(.*\\)$" path)) 375 (let ((oldpath (eshell-find-previous-directory 376 (match-string 1 path)))) 377 (if oldpath 378 (setq path oldpath) 379 (let ((len (ring-length eshell-last-dir-ring)) 380 (index 0)) 381 (if (= len 0) 382 (error "Directory ring empty")) 383 (eshell-init-print-buffer) 384 (while (< index len) 385 (eshell-buffered-print 386 (concat (number-to-string index) ": " 387 (ring-ref eshell-last-dir-ring index) "\n")) 388 (setq index (1+ index))) 389 (eshell-flush) 390 (setq handled t))))) 391 (path 392 (setq path (eshell-expand-multiple-dots path)))) 393 (unless handled 394 (setq dired-directory (or path "~")) 395 (let ((curdir (eshell/pwd))) 396 (unless (equal curdir dired-directory) 397 (eshell-add-to-dir-ring curdir)) 398 (let ((result (cd dired-directory))) 399 (and eshell-cd-shows-directory 400 (eshell-printn result))) 401 (run-hooks 'eshell-directory-change-hook) 402 (if eshell-list-files-after-cd 403 (throw 'eshell-replace-command 404 (eshell-parse-command "ls" (cdr args)))) 405 nil)))) 406 407(put 'eshell/cd 'eshell-no-numeric-conversions t) 408 409(defun eshell-add-to-dir-ring (path) 410 "Add PATH to the last-dir-ring, if applicable." 411 (unless (and (not (ring-empty-p eshell-last-dir-ring)) 412 (equal path (ring-ref eshell-last-dir-ring 0))) 413 (if eshell-last-dir-unique 414 (let ((index 0) 415 (len (ring-length eshell-last-dir-ring))) 416 (while (< index len) 417 (if (equal (ring-ref eshell-last-dir-ring index) path) 418 (ring-remove eshell-last-dir-ring index) 419 (setq index (1+ index)))))) 420 (ring-insert eshell-last-dir-ring path))) 421 422;;; pushd [+n | dir] 423(defun eshell/pushd (&rest args) ; all but first ignored 424 "Implementation of pushd in Lisp." 425 (let ((path (car args))) 426 (cond 427 ((null path) 428 ;; no arg -- swap pwd and car of stack unless eshell-pushd-tohome 429 (cond (eshell-pushd-tohome 430 (eshell/pushd "~")) 431 (eshell-dirstack 432 (let ((old (eshell/pwd))) 433 (eshell/cd (car eshell-dirstack)) 434 (setq eshell-dirstack (cons old (cdr eshell-dirstack))) 435 (eshell/dirs t))) 436 (t 437 (error "pushd: No other directory")))) 438 ((string-match "^\\+\\([0-9]\\)" path) 439 ;; pushd +n 440 (setq path (string-to-number (match-string 1 path))) 441 (cond ((> path (length eshell-dirstack)) 442 (error "Directory stack not that deep")) 443 ((= path 0) 444 (error "Couldn't cd")) 445 (eshell-pushd-dextract 446 (let ((dir (nth (1- path) eshell-dirstack))) 447 (eshell/popd path) 448 (eshell/pushd (eshell/pwd)) 449 (eshell/cd dir) 450 (eshell/dirs t))) 451 (t 452 (let* ((ds (cons (eshell/pwd) eshell-dirstack)) 453 (dslen (length ds)) 454 (front (nthcdr path ds)) 455 (back (nreverse (nthcdr (- dslen path) (reverse ds)))) 456 (new-ds (append front back))) 457 (eshell/cd (car new-ds)) 458 (setq eshell-dirstack (cdr new-ds)) 459 (eshell/dirs t))))) 460 (t 461 ;; pushd <dir> 462 (let ((old-wd (eshell/pwd))) 463 (eshell/cd path) 464 (if (or (null eshell-pushd-dunique) 465 (not (member old-wd eshell-dirstack))) 466 (setq eshell-dirstack (cons old-wd eshell-dirstack))) 467 (eshell/dirs t))))) 468 nil) 469 470(put 'eshell/pushd 'eshell-no-numeric-conversions t) 471 472;;; popd [+n] 473(defun eshell/popd (&rest args) 474 "Implementation of popd in Lisp." 475 (let ((ref (or (car args) "+0"))) 476 (unless (and (stringp ref) 477 (string-match "\\`\\([+-][0-9]+\\)\\'" ref)) 478 (error "popd: bad arg `%s'" ref)) 479 (setq ref (string-to-number (match-string 1 ref))) 480 (cond ((= ref 0) 481 (unless eshell-dirstack 482 (error "popd: Directory stack empty")) 483 (eshell/cd (car eshell-dirstack)) 484 (setq eshell-dirstack (cdr eshell-dirstack)) 485 (eshell/dirs t)) 486 ((<= (abs ref) (length eshell-dirstack)) 487 (let* ((ds (cons nil eshell-dirstack)) 488 (cell (nthcdr (if (> ref 0) 489 (1- ref) 490 (+ (length eshell-dirstack) ref)) ds)) 491 (dir (cadr cell))) 492 (eshell/cd dir) 493 (setcdr cell (cdr (cdr cell))) 494 (setq eshell-dirstack (cdr ds)) 495 (eshell/dirs t))) 496 (t 497 (error "Couldn't popd")))) 498 nil) 499 500(put 'eshell/popd 'eshell-no-numeric-conversions t) 501 502(defun eshell/dirs (&optional if-verbose) 503 "Implementation of dirs in Lisp." 504 (when (or (not if-verbose) eshell-dirtrack-verbose) 505 (let* ((msg "") 506 (ds (cons (eshell/pwd) eshell-dirstack)) 507 (home (expand-file-name "~/")) 508 (homelen (length home))) 509 (while ds 510 (let ((dir (car ds))) 511 (and (>= (length dir) homelen) 512 (string= home (substring dir 0 homelen)) 513 (setq dir (concat "~/" (substring dir homelen)))) 514 (setq msg (concat msg (directory-file-name dir) " ")) 515 (setq ds (cdr ds)))) 516 msg))) 517 518(defun eshell-read-last-dir-ring () 519 "Sets the buffer's `eshell-last-dir-ring' from a history file." 520 (let ((file eshell-last-dir-ring-file-name)) 521 (cond 522 ((or (null file) 523 (equal file "") 524 (not (file-readable-p file))) 525 nil) 526 (t 527 (let* ((count 0) 528 (size eshell-last-dir-ring-size) 529 (ring (make-ring size))) 530 (with-temp-buffer 531 (insert-file-contents file) 532 ;; Save restriction in case file is already visited... 533 ;; Watch for those date stamps in history files! 534 (goto-char (point-max)) 535 (while (and (< count size) 536 (re-search-backward "^\\([^\n].*\\)$" nil t)) 537 (ring-insert-at-beginning ring (match-string 1)) 538 (setq count (1+ count))) 539 ;; never allow the top element to equal the current 540 ;; directory 541 (while (and (not (ring-empty-p ring)) 542 (equal (ring-ref ring 0) (eshell/pwd))) 543 (ring-remove ring 0))) 544 (setq eshell-last-dir-ring ring)))))) 545 546(defun eshell-write-last-dir-ring () 547 "Writes the buffer's `eshell-last-dir-ring' to a history file." 548 (let ((file eshell-last-dir-ring-file-name)) 549 (cond 550 ((or (null file) 551 (equal file "") 552 (null eshell-last-dir-ring) 553 (ring-empty-p eshell-last-dir-ring)) 554 nil) 555 ((not (file-writable-p file)) 556 (message "Cannot write last-dir-ring file %s" file)) 557 (t 558 (let* ((ring eshell-last-dir-ring) 559 (index (ring-length ring))) 560 (with-temp-buffer 561 (while (> index 0) 562 (setq index (1- index)) 563 (insert (ring-ref ring index) ?\n)) 564 (insert (eshell/pwd) ?\n) 565 (eshell-with-private-file-modes 566 (write-region (point-min) (point-max) file nil 567 'no-message)))))))) 568 569;;; Code: 570 571;;; arch-tag: 1e9c5a95-f1bd-45f8-ad36-55aac706e787 572;;; em-dirs.el ends here 573