shx-for-emacs icon indicating copy to clipboard operation
shx-for-emacs copied to clipboard

An Emacs shell-mode (and comint-mode) extension that enables displaying small plots and graphics and lets users write shell commands in Emacs Lisp.

#+TITLE: shx for Emacs #+OPTIONS: toc:3 author:t creator:nil num:nil #+AUTHOR: Chris Rayner #+EMAIL: [email protected]

[[https://melpa.org/#/shx][https://melpa.org/packages/shx-badge.svg]] [[https://stable.melpa.org/#/shx][https://stable.melpa.org/packages/shx-badge.svg]] [[https://github.com/riscy/shx-for-emacs/actions][https://github.com/riscy/shx-for-emacs/workflows/test/badge.svg]]

[[file:img/screenshot.png]]

  • Table of Contents :TOC_3_gh:noexport:
  • [[#description][Description]]
  • [[#install][Install]]
    • [[#from-melpa][From MELPA]]
    • [[#from-gnu-guix][From GNU Guix]]
    • [[#manually][Manually]]
  • [[#setup][Setup]]
    • [[#quick-start][Quick-start]]
    • [[#enable-automatically][Enable automatically]]
    • [[#customize][Customize]]
  • [[#key-bindings][Key bindings]]
  • [[#markup-in-the-shell][Markup in the shell]]
  • [[#extra-shell-commands][Extra shell commands]]
    • [[#general-commands][General commands]]
    • [[#graphical-commands][Graphical commands]]
    • [[#asynchronous-commands][Asynchronous commands]]
    • [[#adding-new-commands][Adding new commands]]
  • [[#related][Related]]
  • Description /shx/ or "shell-extras" extends comint-mode in Emacs (e.g. =M-x shell=).

    It's compatible with any underlying REPL (zsh, bash, psql, ipython, etc.).

    It parses the output stream in a few useful ways:

    • Display graphics and plots in the shell with a simple markup language (e.g. ==)
    • Add event-driven and timed behaviors to any shell session
    • Open any filename or URL by arrowing up to it and pressing =RET= (shx will even try to guess the correct directory)
    • Yank any line to the prompt by arrowing up to it and pressing =C-RET=
    • Check the time a command was run by mousing over its prompt

    shx makes it easy to add new shell commands written in elisp. Some are already built in:

    • =:clear= clears the buffer (like =clear= or =Command-K= on macOS)
    • =:e filename.txt= opens a file for editing
    • =:ssh user@host:port= starts a remote shell session using tramp
    • =:view image_file.png= embeds an image in the shell
    • =:plotline data_file.txt= embeds a line plot
    • etc.

    It also extends =shell-mode='s syntax highlighting, recenters and highlights content for better viewing when you run commands like ~comint-previous-prompt~ and ~comint-kill-input~, and improves compatibility with evil-mode by anticipating when to switch to insert mode.

    Use =M-x shx RET= to start a new shell session with ~shx-mode~ enabled.

    /This version is tested with Emacs 26.1/. Check out the [[https://github.com/riscy/shx-for-emacs/releases][release log]].

  • Install *** From MELPA =M-x package-install RET shx RET= to install =shx= from [[https://melpa.org/][MELPA]]. *** From GNU Guix =guix install emacs-shx= to install =shx= from [[https://guix.gnu.org/][GNU Guix]]. *** Manually Add the following to your =.emacs=: #+begin_src elisp (add-to-list 'load-path "~/path/to/shx/") ; add shx.el's directory to the load-path (require 'shx) ; load shell-extras #+end_src

  • Setup *** Quick-start Type =M-x shx RET=. Try out the following commands:

    1. =:e ~/.bashrc= to edit your =.bashrc= (for example)
    2. =:man ls= to display the man page for =ls=
    3. =:help= to a start a completing read for other =shx= commands

*** Enable automatically If you like shx-mode, you can enable it everywhere:

#+begin_src elisp
(shx-global-mode 1)  ; toggle shx-mode on globally
#+end_src

Now shx will run automatically in any =comint-mode= buffer.  If you don't want
shx to run in every comint-mode buffer, you can use =M-x shx-mode= on a
case-by-case basis, or just add hooks to the mode in question, for example:

#+begin_src elisp
(add-hook 'inferior-python-mode-hook #'shx-mode)
#+end_src

*** Customize Use =M-x customize-group RET shx RET= to see shx's many customization options. Here's an example customization using ~setq~: #+begin_src elisp (setq ;; resync the shell's default-directory with Emacs on "z" commands: shx-directory-tracker-regexp "^z " ;; vastly improve display performance by breaking up long output lines shx-max-output 1024 ;; prevent input longer than macOS's typeahead buffer from going through shx-max-input 1024 ;; prefer inlined images and plots to have a height of 250 pixels shx-img-height 250 ;; don't show any incidental hint messages about how to use shx shx-show-hints nil ;; flash the previous comint prompt for a full second when using C-c C-p shx-flash-prompt-time 1.0 ;; use #' to prefix shx commands instead of the default :' shx-leader "#") #+end_src

  • Key bindings | Key binding | Description | |-------------+--------------------------------------------------------------------------| | =C-RET= | If the cursor is not on the prompt, paste the current line to the input | | =RET= | If the cursor is on a filename or a URL, try to open it | | =SPC= | If the prompt is =:=, send =SPC= straight through to the process | | =q= | If the prompt is =:=, send =q= straight through to the process |

    Note the prompt will be =:= when reading through the output of =less= or a =man= page if you run the following: #+begin_src elisp (setenv "LESS" "--dumb --prompt=s") #+end_src

  • Markup in the shell shx's markup can enhance basic command-line applications and drive other events.

    If the output ever contains == on a line by itself, then a scaled rendering of =mountains.png= will be inlined within the text in the shell. This works because =view= is a shx command. shx will execute any (safe) shx command that appears with the following syntax: #+begin_src xml <command arg1 arg2 ...> #+end_src where ~command~ is a shx command and ~arg1 ... argn~ is a space-separated list of arguments. Arguments don't need to be surrounded by quotes -- the command will figure out how to parse them.

    You can use this markup to create a barplot (=:plotbar=) after collecting some stats, or generate an =:alert= when a task is finished, and so forth.

  • Extra shell commands shx's 'extra' commands are invoked by typing a =:= followed by the command's name. (You can change the =:= prefix by customizing the ~shx-leader~ variable.) These commands are written in elisp and so can access all of Emacs' facilities. Type =:help= to see a complete listing of shx commands.

    One command I use frequently is the =:edit= (shorthand =:e=) command: #+begin_src bash

    edit the .emacs file:

    :edit ~/.emacs

    use tramp to edit .emacs on a remote host through ssh:

    :e /ssh:remote-host.com:~/.emacs

    use tramp to edit .bashrc on a running docker container:

    :e /docker:02fbc948e009:~/.bashrc

    edit a local file as root

    :sedit /etc/passwd #+end_src

    Thanks to [[https://github.com/CeleritasCelery][CeleritasCelery]] it's also possible to use environment variables in the argument list: #+begin_src bash :e $HOME/.emacs.d #+end_src (To see an environment variable's value, use ~(getenv "")~.)

    The =:ssh= and =:docker= commands are popular for opening "remote" shells: #+begin_src bash

    open a shell on a remote host:

    :ssh [email protected]

    connect to a running docker container

    :docker 8a8335d63ff3

    reopen the shell on the localhost:

    :ssh #+end_src [[https://github.com/p3r7][Jordan Besly]] points out that you can customize the default interpreter for each "remote" using [[https://www.gnu.org/software/emacs/manual/html_node/tramp/Remote-processes.html][connection-profile-set-local-variables]].

    I also use the =:kept= and =:keep= commands frequently: #+begin_src bash

    write a complicated command:

    wget https://bootstrap.pypa.io/get-pip.py && python get-pip.py

    save the last command:

    :keep

    search for commands having to do with pip:

    :kept pip #+end_src

    Because these commands are written in elisp, shx gives =M-x shell= a lot of the same advantages as =eshell=. You can even evaluate elisp code directly in the buffer (see =:help eval=).

*** General commands | Command | Description | |----------------------+-------------------------------------------------------| | =:alert= | Reveal the buffer with an alert. Useful for markup | | =:clear= | Clear the buffer | | =:date= | Show the date (even when the process is blocked) | | =:diff file1 file2= | Launch an Emacs diff between two files | | =:edit file= | Edit a file. Shortcut: =:e = | | =:eval (elisp-sexp)= | Evaluate some elisp code. Example: =:eval (pwd)= | | =:find = | Run a fuzzy-find for | | =:goto-url = | Completing-read for a URL | | =:header New header= | Change the current ~header-line-format~ | | =:kept regexp= | Show a list of your 'kept' commands matching regexp | | =:keep= | Add the previous command to the list of kept commands | | =:man topic= | Invoke the Emacs man page browser on a topic | | =:ssh = | Restart the shell on the specified host |

There are more than this -- type =:help= for a listing of all user commands.

*** Graphical commands | Command | Description | |------------------------------+------------------------| | =:view image_file.jpg= | Display an image | | =:plotbar data_file.txt= | Display a bar plot | | =:plotline data_file.txt= | Display a line plot | | =:plotmatrix data_file.txt= | Display a heatmap | | =:plotscatter data_file.txt= | Display a scatter plot | | =:plot3d data_file.txt= | Display a 3D plot |

These are for displaying inline graphics and plots in the shell buffer.  You
can control how much vertical space an inline image occupies by customizing
the ~shx-img-height~ variable.

Note =convert= (i.e. ImageMagick) and =gnuplot= need to be installed.  If
the binaries are installed but these commands aren't working, customize the
~shx-path-to-convert~ and ~shx-path-to-gnuplot~ variables to point to the
binaries.  Also note these graphical commands aren't yet compatible with
shells launched on remote hosts (e.g. over ssh or in a Docker container).

*** Asynchronous commands | Command | Description | |-----------------------------------+---------------------------------------------------| | =:delay = | Run a shell command after a specific delay | | =:pulse = | Repeat a shell command forever with a given delay | | =:repeat = | Repeat a shell command ~~ times | | =:stop = | Cancel a repeating or delayed command |

Use these to delay, pulse, or repeat a command a specific number of times.
Unfortunately these only support your typical shell commands, and not shx's
extra (colon-prefixed) commands.  So this possible:
#+begin_src bash
# Run the 'pwd' command 10 seconds from now:
:delay 10 pwd
#+end_src
But this is not possible:
#+begin_src bash
# Run the 'pwd' shx command 10 seconds from now (DOES NOT WORK)
:delay 10 :pwd
#+end_src

*** Adding new commands New shx commands are written by defining single-argument elisp functions named ~shx-cmd-COMMAND-NAME~, where ~COMMAND-NAME~ is what the user would type to invoke it. ***** Example: a command to rename the buffer If you evaluate the following (or add it to your ~.emacs~), #+begin_src elisp (defun shx-cmd-rename (name) "(SAFE) Rename the current buffer to NAME." (if (not (ignore-errors (rename-buffer name))) (shx-insert 'error "Can't rename buffer.") (shx-insert "Renaming buffer to " name "\n") (shx--hint "Emacs won't save buffers starting with *"))) #+end_src then each shx buffer will immediately have access to the =:rename= command. When it's invoked, shx will also display a hint about buffer names.

Note the importance of defining a docstring.  This documents the
command so that typing =:help rename= will give the user information on what
the command does.  Further, since the docstring begins with =(SAFE)=,
it becomes part of shx's markup language.  So in this case if:
#+begin_src xml
<rename A new name for the buffer>
#+end_src
appears on a line by itself in the output, the buffer will try to
automatically rename itself.

***** Example: invoking ediff from the shell A command similar to this one is built into shx: #+begin_src elisp (defun shx-cmd-diff (files) "(SAFE) Launch an Emacs `ediff' between FILES." (setq files (shx-tokenize files)) (if (not (eq (length files) 2)) (shx-insert 'error "diff \n") (shx-insert "invoking ediff...\n") (shx--asynch-funcall #'ediff (mapcar #'expand-file-name files)))) #+end_src Note that ~files~ is supplied as a string, but it's immediately parsed into a list of strings using ~shx-tokenize~. Helpfully, this function is able to parse various styles of quoting and escaping, for example ~(shx-tokenize "'file one' file\ two")~ evaluates to ~("file one" "file two")~. ***** Example: a command to browse URLs If you execute the following, #+begin_src elisp (defun shx-cmd-browse (url) "Browse the supplied URL." (shx-insert "Browsing " 'font-lock-keyword-face url) (browse-url url)) #+end_src then each shx buffer will have access to the =:browse= command.

  Note the docstring does not specify that this command is =SAFE=.
  This means =<browse url>= will not become part of shx's markup.  That
  makes sense in this case, since you wouldn't want to give a process the
  power to open arbitrary URLs without prompting.
  • Related If you're here, these might be interesting:

    • [[https://www.masteringemacs.org/article/shell-comint-secrets-history-commands][Shell & Comint Secrets: History commands]]
    • [[https://www.masteringemacs.org/article/pcomplete-context-sensitive-completion-emacs][PComplete: Context-Sensitive Completion in Emacs]]
    • [[https://dev.to/_darrenburns/10-tools-to-power-up-your-command-line-4id4][10 tools to power up your command line]]
    • [[https://www.booleanworld.com/customizing-coloring-bash-prompt/][Creating dynamic bash prompts]]
    • [[https://github.com/Orkohunter/keep][The Keep Utility]] inspired the =kept= and =keep= commands
    • [[https://terminalsare.sexy/]["Terminals Are Sexy"]] (portal)
    • [[https://github.com/riscy/command_line_lint][Command-Line Lint]], another project I maintain
    • [[http://ohmyz.sh/][oh my zsh]], a community-driven zsh configuration
    • [[https://github.com/Bash-it/bash-it][bash-it]], a community driven bash configuration

    And if running a =dumb= terminal in Emacs isn't for you, here are some alternatives:

    • [[https://leanpub.com/the-tao-of-tmux/read][The Tao of tmux]], re: working in the terminal with tmux
    • [[https://github.com/zsh-users/zsh-syntax-highlighting][zsh-syntax-highlighting]]
    • [[https://hackernoon.com/macbook-my-command-line-utilities-f8a121c3b019#.clz934ly3][Shell configuration tips]] from Vitaly Belman
    • [[http://www.iterm2.com/documentation-shell-integration.html][Shell integration]] for iTerm2 on macOS
    • [[https://getbitbar.com/][BitBar]] adds program output to menus on macOS