[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] master 30ba444 5/5: Merge multishell 1.0.9
From: |
ken manheimer |
Subject: |
[elpa] master 30ba444 5/5: Merge multishell 1.0.9 |
Date: |
Sat, 30 Jan 2016 07:15:42 +0000 |
branch: master
commit 30ba44404311276d2650664001c5261e52d3008f
Merge: 8f6fe36 46bdd13
Author: Ken Manheimer <address@hidden>
Commit: Ken Manheimer <address@hidden>
Merge multishell 1.0.9
---
packages/multishell/multishell-list.el | 137 ++++++++++++++++
packages/multishell/multishell.el | 271 +++++++++++++++++++-------------
2 files changed, 296 insertions(+), 112 deletions(-)
diff --git a/packages/multishell/multishell-list.el
b/packages/multishell/multishell-list.el
new file mode 100644
index 0000000..ae72f92
--- /dev/null
+++ b/packages/multishell/multishell-list.el
@@ -0,0 +1,137 @@
+;;; multishell-list.el --- tabulated-list-mode for multishell shell buffers
+
+;; Copyright (C) 2016 Free Software Foundation, Inc. and Ken Manheimer
+
+;; Author: Ken Manheimer <address@hidden>
+;; Version: 1.0.9
+;; Created: 2016 -- first public availability
+;; Keywords: processes
+;; URL: https://github.com/kenmanheimer/EmacsMultishell
+
+(require 'tabulated-list)
+
+(defun multishell-list-open-pop ()
+ "Pop to current entry's shell, and refresh the listing buffer."
+ (interactive)
+ (multishell-pop-to-shell nil (tabulated-list-get-id)))
+(defun multishell-list-open-here ()
+ "Switch to current entry's shell buffer."
+ (interactive)
+ (multishell-pop-to-shell nil (tabulated-list-get-id) 'here))
+
+(defun multishell-list-delete ()
+ "Remove current environment variable value."
+ (interactive)
+ (let* ((where (save-excursion (beginning-of-line) (point)))
+ (entry (tabulated-list-get-id))
+ (name (multishell-name-from-entry entry))
+ (name-bracketed (multishell-bracket name))
+ (buffer (get-buffer name-bracketed)))
+ (when (multishell-delete-history-name name)
+ (and buffer
+ ;; If the process is live, let shell-mode get confirmation:
+ (or (comint-check-proc (current-buffer))
+ (y-or-n-p (format "Kill buffer %s? " name-bracketed)))
+ (kill-buffer name-bracketed)))
+ (revert-buffer)
+ (if (not tabulated-list-sort-key)
+ (revert-buffer))
+ (goto-char where)))
+
+(defun multishell-list-edit-entry ()
+ "Edit the value of current shell entry."
+ (interactive)
+ (let* ((where (save-excursion (beginning-of-line) (point)))
+ (entry (tabulated-list-get-id))
+ (name (multishell-name-from-entry entry))
+ (revised (multishell-read-unbracketed-entry
+ (format "Edit shell spec for %s: " name)
+ nil
+ entry))
+ (revised-pair (when revised (multishell-split-entry revised))))
+ (when revised-pair
+ (multishell-delete-history-name name)
+ (multishell-register-name-to-path (car revised-pair) (cadr revised-pair))
+ (revert-buffer)
+ (if (not tabulated-list-sort-key)
+ (revert-buffer))
+ (goto-char where))))
+
+(defun multishell-list-placeholder (value default)
+ "Return VALUE if non-empty string, else DEFAULT."
+ (if (or (not value) (string= value ""))
+ default
+ value))
+(defun multishell-list-entries ()
+ "Generate multishell name/path entries list for tabulated-list."
+ (let ((recency 0))
+ (mapcar #'(lambda (entry)
+ (setq recency (1+ recency))
+ (let* ((splat (multishell-split-entry entry))
+ (name (car splat))
+ (buffer (and name
+ (get-buffer
+ (multishell-bracket name))))
+ (status (cond ((not buffer) "x")
+ ((comint-check-proc buffer) "+")
+ (t ".")))
+ (rest (cadr splat))
+ (dissected (and rest (file-remote-p rest)
+ (tramp-dissect-file-name rest t)))
+ (path (or (and dissected (aref dissected 3))
+ rest))
+ (hops (and dissected
+ (substring
+ rest 0 (- (length rest) (length path))))))
+ (when (not name)
+ (setq name (multishell-name-from-entry entry)))
+ (list entry
+ (vector (format "%d" recency)
+ status
+ name
+ (multishell-list-placeholder hops "-")
+ (multishell-list-placeholder path ":")))))
+ (multishell-all-entries))))
+
+(defun compare-strings-as-numbers (a b)
+ (let ((a (aref (cadr a) 0))
+ (b (aref (cadr b) 0)))
+ (> (string-to-number a) (string-to-number b))))
+(define-derived-mode multishell-list-mode
+ tabulated-list-mode "Shells"
+ "Major mode for listing current and historically registered shells..
+\\{multishell-list-mode-map\}"
+ (setq tabulated-list-format [("#" 0 compare-strings-as-numbers)
+ ("! " 1 t)
+ ("Name" 15 t)
+ ("Hops" 30 t)
+ ("Path" 30 t)]
+ tabulated-list-sort-key nil
+ tabulated-list-entries #'multishell-list-entries)
+ (tabulated-list-init-header))
+
+(define-key multishell-list-mode-map (kbd "d") 'multishell-list-delete)
+(define-key multishell-list-mode-map (kbd "e") 'multishell-list-edit-entry)
+(define-key multishell-list-mode-map (kbd "o") 'multishell-list-open-pop)
+(define-key multishell-list-mode-map
+ (kbd "<return>") 'multishell-list-open-here)
+
+;;;###autoload
+(defun multishell-list ()
+ "Edit your current and historic list of shell buffers.
+
+Hit ? for a list of commands.
+
+You can get to this shell listing manager by
+recursively invoking \\[multishell-pop-to-shell] at either of the
+`multishell-pop-to-shell' universal argument prompts."
+ (interactive)
+ (let ((buffer (get-buffer-create "*Shells*")))
+ (pop-to-buffer buffer)
+ (multishell-list-mode)
+ (tabulated-list-print)))
+
+(provide 'multishell-list)
+(require 'multishell)
+
+;;; multishell-list.el ends here
diff --git a/packages/multishell/multishell.el
b/packages/multishell/multishell.el
index 2f43752..7aa4b46 100644
--- a/packages/multishell/multishell.el
+++ b/packages/multishell/multishell.el
@@ -3,7 +3,7 @@
;; Copyright (C) 1999-2016 Free Software Foundation, Inc. and Ken Manheimer
;; Author: Ken Manheimer <address@hidden>
-;; Version: 1.0.8
+;; Version: 1.0.9
;; Created: 1999 -- first public availability
;; Keywords: processes
;; URL: https://github.com/kenmanheimer/EmacsMultishell
@@ -43,7 +43,9 @@
;; * Thanks to tramp, file visits from the shell will seamlessly be in
;; the auspices of the target account, and relative to the current
;; directory, on the host where the shell is running.
-;;
+;;
+;; * Manage your list of shells, current and past, as a collection.
+;;
;; See the `multishell-pop-to-shell` docstring for details.
;;
;; Customize-group `multishell' to select and activate a keybinding and set
@@ -57,17 +59,19 @@
;;
;; Change Log:
;;
-;; * XXX 1.0.9 Ken Manheimer:
-;; - Allow existing shell buffers names as completions, even though they
-;; duplicate the names with paths. The different behavior for entries
-;; with existing buffers is actually useful. And in accord with actual
-;; behavior, where changing path for existing shells doesn't, actually.
-;; - Add paths to buffers started without one, if multishell history dir
+;; * 2016-01-30 1.0.9 Ken Manheimer:
+;; - Add multishell-list for managing the collection of current and
+;; history-registered shells: edit, delete, and switch/pop to entries.
+;; Easy access by invoking `multishell-pop-to-shell' from in the
+;; `multishell-pop-to-shell' universal arg prompts.
+;; - Duplicate existing shell buffer names in completions, for distinction.
+;; - Add paths to buffers started without one, when multishell history dir
;; tracking is enabled.
-;; - Substantial code cleanup:
-;; - simplify multishell-start-shell-in-buffer, in particular using
+;; - Major code cleanup:
+;; - Simplify multishell-start-shell-in-buffer, in particular using
;; shell function, rather than unnecessarily going underneath it.
-;; - fallback to eval-after-load in emacs, that lack
+;; - Establish multishell-name-from-entry as canonical name resolver.
+;; - Fallback to eval-after-load in emacs versions that lack
;; with-eval-after-load (eg, emacs 23).
;; - save-match-data, where match-string is used
;; - resituate some helpers
@@ -104,43 +108,28 @@
;;
;; TODO and Known Issues:
;;
-;; * Find suitable, internally consistent ways to tidy completions, eg:
-;; - first list completions for active shells, then present but inactive,
-;; then historical
-;; - some way for user to toggle between presenting just buffer names vs
-;; full buffer/path
-;; - without cutting user off from easy editing of path
-;; - maybe use keybindings that wrap minibuffer completion keys
-;; - minibuffer-local-completion-map, minibuffer-local-must-match-map
-;; - setup minibuffer with these vars just before doing completions
-;; - minibuffer exit reverts these vars, if necessary
-;; - toggles between name and name/path if repeat count provided
-;; - and an instruction about toggling in the completion buffer
-;; - eventually? "multishell-list-all", based on tabulated-list-mode
-;; - list-environment package is small, tidy, may be an easy template?
-;; - sort based on existing vs just historical
-;; - launch
-;; - rename, change path, and remove history entries
-;; - could we use it as the transient completions help window?
+;; * Add custom shell launch prep actions
+;; - for, eg, port knocking, interface activations
+;; - shell commands to execute when shell name or path matches a regexp
+;; - list of (regexp, which - name, path, or both, command)
+;; * Adapt multishell-list facilities for all-completions
+;; - See info on minibuffer-completion-help, display-completion-list
+;; - implement markup for mouse selection
;; * Investigate whether we can recognize and provide for failed hops.
;; - Tramp doesn't provide useful reactions for any hop but the first
;; - Might be stuff we can do to detect and convey failures?
;; - Might be no recourse but to seek tramp changes.
-;; * Add custom shell launch prep actions
-;; - shell commands to execute when shell name or path matches a regexp
-;; - list of [regexp, which (name, path, or both), command]
-;; - for, eg, knock commands or interface activations, whatever
;; * Try minibuffer field boundary at beginning of tramp path, to see whether
;; the field boundary magically enables tramp path completion.
-;; * Assess whether deletion of history entry via kill-buffer is sufficient.
;;; Code:
(require 'comint)
(require 'shell)
(require 'savehist)
+(require 'multishell-list)
-(defvar multishell-version "1.0.8")
+(defvar multishell-version "1.0.9")
(defun multishell-version (&optional here)
"Return string describing the loaded multishell version."
(interactive "P")
@@ -271,25 +260,31 @@ Promote added/changed entry to the front of the list."
(defun multishell-history-entries (name)
"Return `multishell-history' entry that starts with NAME, or nil if none."
- (save-match-data
- (let ((match-expr (concat "^" name "\\\(/.*$\\\)?$"))
- got)
- (dolist (entry multishell-history)
- (when (and (string-match match-expr entry)
- (not (member entry got)))
- (setq got (cons entry got))))
- got)))
-
-(defun multishell-pop-to-shell (&optional arg)
+ (let (got)
+ (dolist (entry multishell-history)
+ (when (and (string-equal name (multishell-name-from-entry entry))
+ (not (member entry got)))
+ (setq got (cons entry got))))
+ got))
+
+;;;###autoload
+(defun multishell-pop-to-shell (&optional arg name here)
"Easily navigate to and within multiple shell buffers, local and remote.
-Use universal arguments to launch and choose between alternate
-shell buffers and to select which is default. Append a path to
-a new shell name to launch a shell in that directory, and use
-Emacs tramp syntax to launch a remote shell.
+Use a single `universal-argument' (\\[universal-argument]) to launch and
choose between
+nalternate shell buffers, and a doubled universal argument to also set your
+choice as the ongoing default. Append a path to a new shell name to launch
+a shell in that directory, and use Emacs tramp syntax to launch a remote
+shell. There is a shortcut to manage your list of current and
+historical shells, collectively, using `multishell-list' - see below.
Customize-group `multishell' to set up a key binding and tweak behaviors.
+Manage your collection of current and historical shells by
+recursively invoking \\[multishell-pop-to-shell] at either of the
+`multishell-pop-to-shell' universal argument prompts, or at any time via
+\\[multishell-list]. Hit ? in the listing buffer for editing commands.
+
==== Basic operation:
- If the current buffer is shell-mode (or shell-mode derived)
@@ -317,8 +312,9 @@ was disconnected or otherwise stopped, it's resumed.
===== Universal arg to start and select between named shell buffers:
-You can name alternate shell buffers to create or return to using
-single or doubled universal arguments:
+You can name alternate shell buffers to create or return to, by
+prefixing your \\[multishell-pop-to-shell] invocation with single or double
+`universal-argument', \\[universal-argument]:
- With a single universal argument, prompt for the buffer name
to use (without the asterisks that shell mode will put around
@@ -336,6 +332,12 @@ single or doubled universal arguments:
See `multishell-primary-name' for info about preserving the
setting across emacs restarts.
+ - Manage your collection of current and historical shells by
+ recursively invoking \\[multishell-pop-to-shell] at either of the
+ `multishell-pop-to-shell' universal argument prompts, or at any
+ time via \\[multishell-list]. Hit ? in the listing buffer for
+ editing commands.
+
===== Select starting directory and remote host:
The shell buffer name you give to the prompt for a universal arg
@@ -379,9 +381,19 @@ customize the savehist group to activate savehist."
(interactive "P")
+ (let ((token '(token)))
+ (if (window-minibuffer-p)
+ (throw 'multishell-do-list token)
+ (if (equal token
+ (catch 'multishell-do-list
+ (multishell-pop-to-shell-worker arg name here)))
+ (multishell-list)))))
+
+(defun multishell-pop-to-shell-worker (&optional arg name here)
+ "Do real work of `multishell-pop-to-shell', which see."
(let* ((from-buffer (current-buffer))
(from-buffer-is-shell (derived-mode-p 'shell-mode))
- (primary-name-unbracketed (multishell-unbracket-asterisks
+ (primary-name-unbracketed (multishell-unbracket
multishell-primary-name))
(fallthrough-name (if from-buffer-is-shell
(buffer-name from-buffer)
@@ -389,14 +401,15 @@ customize the savehist group to activate savehist."
(doublearg (equal arg '(16)))
(target-name-and-path
(multishell-resolve-target-name-and-path
- (if arg
- (or (multishell-read-bare-shell-buffer-name
- (format "Shell buffer name [%s]%s "
- primary-name-unbracketed
- (if doublearg " <==" ":"))
- primary-name-unbracketed)
- primary-name-unbracketed)
- fallthrough-name)))
+ (cond (name name)
+ (arg
+ (or (multishell-read-unbracketed-entry
+ (format "Shell buffer name [%s]%s "
+ primary-name-unbracketed
+ (if doublearg " <==" ":"))
+ primary-name-unbracketed)
+ primary-name-unbracketed))
+ (t fallthrough-name))))
(use-path (cadr target-name-and-path))
(target-shell-buffer-name (car target-name-and-path))
(target-buffer (get-buffer target-shell-buffer-name))
@@ -405,7 +418,7 @@ customize the savehist group to activate savehist."
already-there)
;; Register early so the entry is pushed to the front:
- (multishell-register-name-to-path (multishell-unbracket-asterisks
+ (multishell-register-name-to-path (multishell-unbracket
target-shell-buffer-name)
use-path)
@@ -433,7 +446,9 @@ customize the savehist group to activate savehist."
;; No preexisting shell buffer, or not in a visible window:
(when (not (get-buffer target-shell-buffer-name))
(message "Creating new shell buffer '%s'" target-shell-buffer-name))
- (pop-to-buffer target-shell-buffer-name pop-up-windows))
+ (if here
+ (switch-to-buffer target-shell-buffer-name)
+ (pop-to-buffer target-shell-buffer-name pop-up-windows)))
;; Buffer exists and already has a window - jump to it:
(t (if (and multishell-pop-to-frame
@@ -443,7 +458,9 @@ customize the savehist group to activate savehist."
(select-frame-set-input-focus (window-frame inwin)))
(if (not (string= (buffer-name (current-buffer))
target-shell-buffer-name))
- (pop-to-buffer target-shell-buffer-name t))))
+ (if here
+ (switch-to-buffer target-shell-buffer-name)
+ (pop-to-buffer target-shell-buffer-name t)))))
;; We're in the buffer. Activate:
@@ -462,6 +479,21 @@ customize the savehist group to activate savehist."
(and (get-buffer-process from-buffer)
(goto-char (process-mark (get-buffer-process from-buffer)))))))
+(defun multishell-delete-history-name (name &optional ask)
+ "Remove all multishell history entries for NAME.
+
+if optional ask is non-nil (default nil), ask before each deletion.
+
+Return the last entry deleted."
+ (let (got)
+ (dolist (entry (multishell-history-entries name) got)
+ (when (and entry
+ (or (not ask)
+ (y-or-n-p (format "Remove multishell history entry `%s'? "
+ entry))))
+ (setq got entry
+ multishell-history (delete entry multishell-history))))))
+
(defun multishell-kill-buffer-query-function ()
"Offer to remove multishell-history entry for buffer."
;; Removal choice is crucial, so users can, eg, kill a shell with huge
@@ -478,14 +510,10 @@ customize the savehist group to activate savehist."
;; (Use condition-case to avoid inadvertant disruption of kill-buffer
;; activity. kill-buffer happens behind the scenes a whole lot.)
(condition-case err
- (dolist (entry (and (derived-mode-p 'shell-mode)
- (multishell-history-entries
- (multishell-unbracket-asterisks (buffer-name)))))
- (when (and entry
- (y-or-n-p (format "Remove multishell history entry `%s'? "
- entry)))
- (setq multishell-history
- (delete entry multishell-history))))
+ (and (derived-mode-p 'shell-mode)
+ (multishell-delete-history-name
+ (multishell-unbracket (buffer-name))
+ t))
(error
(message "multishell-kill-buffer-query-function error: %s" err)))
t)
@@ -505,25 +533,40 @@ customize the savehist group to activate savehist."
nil 'visible)
nil))
-(defun multishell-read-bare-shell-buffer-name (prompt default)
+(defun multishell-all-entries (&optional active-duplicated)
+ "Return multishell history, with active buffers listed first.
+
+Optional ACTIVE-DUPLICATED will return a copy of
+`multishell-history' with unbracketed names of active buffers,
+sans paths, appended to the list, so they have short and long
+completions."
+ ;; Reorder so active lead present lead historical entries:
+ (let (active-entries active-names present past splat name path buffer)
+ (dolist (entry multishell-history)
+ (setq splat (multishell-split-entry entry)
+ name (car splat)
+ path (cadr splat)
+ buffer (and name (get-buffer (multishell-bracket name))))
+ (if (buffer-live-p buffer)
+ (if (comint-check-proc buffer)
+ (setq active-entries (push entry active-entries)
+ active-names (push name active-names))
+ (setq present (push entry present)))
+ (setq past (push entry past))))
+ (setq multishell-history (append active-entries present past))
+ (if active-duplicated
+ (append multishell-history active-names)
+ multishell-history)))
+
+(defun multishell-read-unbracketed-entry (prompt default &optional initial)
"PROMPT for shell buffer name, sans asterisks. Indicate DEFAULT in prompt.
-Return the supplied name, if provided, else return nil."
- (let* ((candidates
- (append
- ;; Plain shell buffer names appended with names from name/path hist:
- (remq nil
- (mapcar (lambda (buffer)
- (let* ((name (multishell-unbracket-asterisks
- (buffer-name buffer))))
- (and (buffer-live-p buffer)
- (with-current-buffer buffer
- ;; Shell mode buffers.
- (and (derived-mode-p 'shell-mode)
- (comint-check-proc (current-buffer))))
- name)))
- (buffer-list)))
- multishell-history))
+Optional INITIAL is preliminary value to be edited.
+
+Input and completion can include associated path, if any.
+
+Return what's provided, if anything, else nil."
+ (let* ((candidates (multishell-all-entries 'active-duplicated))
(got (completing-read prompt
;; COLLECTION:
(reverse candidates)
@@ -532,7 +575,7 @@ Return the supplied name, if provided, else return nil."
;; REQUIRE-MATCH:
'confirm
;; INITIAL-INPUT
- nil
+ initial
;; HIST:
'multishell-history)))
(if (not (string= got ""))
@@ -551,32 +594,37 @@ is used.
Return them as a list (name path), with name asterisk-bracketed
and path nil if none resolved."
(let* ((splat (multishell-split-entry (or path-ish "")))
- (name (car splat))
- (path (cadr splat)))
- (if path
- (if (not name)
- (setq name
- (if (file-remote-p path)
- (let ((vec (tramp-dissect-file-name path)))
- (or (tramp-file-name-host vec)
- (tramp-file-name-domain vec)
- (tramp-file-name-localname vec)
- system-name))
- multishell-primary-name)))
- ;; No path - get one from history, if present.
- (when (not name)
- (setq name multishell-primary-name))
+ (path (cadr splat))
+ (name (or (car splat) (multishell-name-from-entry path))))
+ (when (not path)
+ ;; Get path from history, if present.
(mapcar #'(lambda (entry)
(when (or (not path) (string= path ""))
(setq path (cadr (multishell-split-entry entry)))))
(multishell-history-entries
- (multishell-unbracket-asterisks name))))
- (list (multishell-bracket-asterisks name) path)))
+ (multishell-unbracket name))))
+ (list (multishell-bracket name) path)))
+
+(defun multishell-name-from-entry (entry)
+ "Derive a name for a shell buffer according to ENTRY."
+ (if (not entry)
+ (multishell-unbracket multishell-primary-name)
+ (let* ((splat (multishell-split-entry entry))
+ (name (car splat))
+ (path (cadr splat)))
+ (or name
+ (if (file-remote-p path)
+ (let ((vec (tramp-dissect-file-name path)))
+ (or (tramp-file-name-host vec)
+ (tramp-file-name-domain vec)
+ (tramp-file-name-localname vec)
+ system-name))
+ (multishell-unbracket multishell-primary-name))))))
(defun multishell-start-shell-in-buffer (buffer-name path)
"Start, restart, or continue a shell in BUFFER-NAME on PATH."
(let* ((buffer (get-buffer buffer-name))
- is-remote is-active)
+ is-active)
(set-buffer buffer)
(setq is-active (comint-check-proc buffer))
@@ -589,7 +637,7 @@ and path nil if none resolved."
(tramp-dissect-file-name default-directory 'noexpand)
'keep-debug 'keep-password))
- (message "Connecting to %s" path)
+ (when (file-remote-p path) (message "Connecting to %s" path))
(cd path))
(shell buffer)))
@@ -645,8 +693,7 @@ and path nil if none resolved."
(tramp-dissect-file-name default-directory))
default-directory)))
(when (not (string= curdir (or multishell-was-default-directory "")))
- (multishell-track-dirchange (multishell-unbracket-asterisks
- (buffer-name))
+ (multishell-track-dirchange (multishell-unbracket (buffer-name))
curdir))
(setq multishell-was-default-directory curdir)))
;; To avoid disruption as a pervasive hook function, swallow all errors:
@@ -665,14 +712,14 @@ Returns nil for empty parts, rather than the empty
string."
(and (string= name "") (setq name nil))
(and (string= path "") (setq path nil))
(list name path))))
-(defun multishell-bracket-asterisks (name)
+(defun multishell-bracket (name)
"Return a copy of name, ensuring it has an asterisk at the beginning and
end."
(if (not (string= (substring name 0 1) "*"))
(setq name (concat "*" name)))
(if (not (string= (substring name -1) "*"))
(setq name (concat name "*")))
name)
-(defun multishell-unbracket-asterisks (name)
+(defun multishell-unbracket (name)
"Return a copy of name, removing asterisks, if any, at beginning and end."
(if (string= (substring name 0 1) "*")
(setq name (substring name 1)))
- [elpa] master updated (8f6fe36 -> 30ba444), ken manheimer, 2016/01/30
- [elpa] master 7b19a19 3/5: multishell - whoops, changed commentary while editing commit message., ken manheimer, 2016/01/30
- [elpa] master 04a0dec 1/5: multishell - start multishell-list-shells tabulated list functionality., ken manheimer, 2016/01/30
- [elpa] master 46bdd13 4/5: multishell - list active the present then historical buffers, ken manheimer, 2016/01/30
- [elpa] master 59cb26c 2/5: multishell - add multishell-list, solidify facilities, ken manheimer, 2016/01/30
- [elpa] master 30ba444 5/5: Merge multishell 1.0.9,
ken manheimer <=