www.howardism.org
Babblings of an aging geek in love with the Absurd, his family, and his own hubris.... oh, and Lisp.

Death to the Shell

The following is the transcript of a talk I attempted to give at EmacsConf 2019. With the technical difficulties, my talk came out more disjointed than I expected, and figured that I would type up what I tried to say, and will re-record.

Introduction

I hate the shell

Really I just included this slide as click-bait for gray-hairs to get up-voted on HackerNews or something like that.

In reality, I bring this subject and my project to start a discussion, and HackerNews is actually the last place to have that conversation.

Because in reality, the shell, like a biological virus, has subversively embedded part of its DNA into my DNA, and it has shaped the way I do.

Emacsians relationship with shells is, uhm... complicated.

As Emacsians, we have a significant love/hate relationship with shells. Just look at all the terminal emulators we have available, like shell, term, ansi-term, eshell, and vterm. (If you still haven’t embedded terminal inside your Emacs workflow, you probably haven’t seen vterm, as that seems to be an answer for you).

But like magit and dired, we have better options for just about anything the shell can do.

My agenda involves scratching two itches in how we use the shell, first part talks about improvements to how we use the shell interactively, and the second part talks about improvements to automated scripts

This talk covers both ways we use the shell, interactively and programmatically.

Warning!

I have created a project to address this, but I want to get a warning: This project is for tinkerers only (at least, at this point).

Part 1: Addressing Interactive Shell Work

Allow me to begin discussing the first idea by relating a story that happened the other day. Someone asked me to jump on a development system and restart the OpenStack service. Well, OpenStack is not a single service, but a bunch of Python scripts supporting web interfaces, and I can’t remember all of their names.

A slide where I explain all the content below.

Since all of the services start with the word, openstack, I could quickly type this out into the shell:

for S in $(systemctl --all | grep openstack | sed 's/\.service.*//' | cut -c3-)
do
    systemctl restart $S
done

If you believe that I just banged that out, you’d be deluded.

Notice the subshell section. Converting data by chaining small executables is incredibly flexible, but it has a dark side…

The data through pipes is inscrutable.

While I suppose one could call the tee command, but typically the flow of data through pipes isn’t viewable. Yeah, threading data through pipes like this is similar to function calls, but in the shell, we don’t have a debugger to expose each step.

If our problem is large, we typically skirt the issue by writing the initial data to a file, editing it in a descent enough editor, and then use that file for the final component.

This gave me an idea…

I could just use Emacs for all of it.

The shell’s flexibility makes it powerful, but anything the shell can do, I’d claim that Emacs can do better.

So my first idea is to eschew transforming data through commands like grep and sed to transforming a buffer of data using Emacs functions.

Sending data to commands is not as straight-forward as transforming the data in an Emacs buffer.

Once I can transform the data, I would like to send that data to some command in flexible ways like the shell can. I mean, there is only one way to get textual data out of a command, but lots of ways to get data in to those commands. I’ve come up with five specific use cases that if I could answer sufficiently, that should address 80 to 90% of use:

  • Data piped in through a command’s standard in is the biggest, and easiest to address, think of Emacs’ function, shell-command-on-region does this.
  • The xargs command converts a stream of lines as command line parameters.
  • I write for loops when I want to run command repeated with each line as some parameter somewhere on the command line call.
  • Often I need to copy data to clipboard so that I can paste this into my bug tracking software or some chat program, etc.
  • Finally, I may want visually inspect it.

I suppose everything I’ve state has been too esoteric, so it is time for a demonstration. What I’m about to show is primariy just a user interface, that is a thin wrapper around existing Emacs functionality. Very little is unique.

Demonstration Time!  This is more difficult so I may need to link to the video I have online, but essentially, I run a remote command and the results show up in a buffer with a Hydra of functions to call.  I use both the shell commands and regular Emacs functions to transform the data until I have a list of Openstack services.  These I sends back to the remote host, but with a command that looks for the status of each servce given.

I’d like to reproduce my previous story in real time by going through the following steps:

  • Get a list of all services: systemctl --all
  • Filter to just the openstack service names.
  • Get the status of each service.

Oh, and let’s do this all remotely, shall we?

Part 2: Addressing Programmatic Shell Work

Remember how I mention that we use shell in two distinct ways, interactively and programmatically. I’ve shown my idea for the first, let’s chat about the second.

While I’ve been thinking about this for many years, not long ago, I came across a Reddit Question:

Lisp people: What's your go-to language for when you want to write a quick script.  Python seems to be the default for me, but I do genuinely find Lisp joyful to write in.

To which I responded that as a Lisp, Emacs Lisp is great as integrating any good idea that comes along, and stealing good ideas has made this system from the twentieth century still useful and almost modern.

After many years of shell and Perl scripts that grew in complexity, I switch to writing scripts where it is easiest to call, Emacs Lisp.  With modern libraries and completing libraries like Helm and Ivy, I just found a nice go to language.
I skipped showing a couple of slides that I just described.  This slide is titled, Emacs as a Go To Scripting Language

The old adage that Emacs is an operating system is pretty true, it is just an operating system of functions, and as such, it really good for the hacker. You want to either overwrite or just advice every function. Global variables, normally a bad thing when you are sharing code with others, turns out to be really easy to prototype ideas when it is just you.

But think about all the really great libraries and support that we have that are new. I didn’t have these in the 1980s:

But I think the most compelling argument for using Emacs functions instead of shell scripts is the improved user interface. I’m serious. Think about all those command line options to each command you use, and how no amount of tab completion helps. In emacs, we can easily bind functions to keys, use two great completing systems (Ivy or Helm) that can give you history of the arguments, just not the command.

Finally, think of using Hydra and Magit’s interface, Transient, to really have a flexible approach to running commands.

Shell scripts are both good and bad

Shell scripts, especially small ones can be really readable, and they are good as running commands and connecting them with streams, however, any script that grows to any size seems to become completely unreadable. The other issue is that since everything is a string, we can have conversion issues into numbers or needing to escape white space. Sure, modern versions of the shell have arrays, but they’re pretty kludgy at best.

So, can we take the best of both Lisp and Shell scripts? If we could, this could be a win for us using Emacs, as calling a function is better than a script.

Maybe we could bring the good parts of shell scripts into Emacs.

What should we bring over from shells? The format function is fairly good, but the shell, along with most modern languages, have variable substitution within strings. This seems more readable. Also, since the shell has embedded itself in my DNA, I am comfortable with mkdir and cp. Besides, since Emacs doesn’t have namespaces, our functions tend to be pretty lengthy, which often makes our code is little more difficult to read.

But when we do, we can get a slew of Lispy goodness, like data structures, better iterations, logical variable scoping, and more functions than you can shake a stick.

Desired Features

The magic starts with a couple of macros that will convert the forms within to be more script-like. I have five specific goals for how this macro should behave. Let me explain each of these in details.

Shell Executables should just Work

Should be easy to call a shell command, using the shell form:

(piper-script
 (shell "ls /tmp"))

This should also work well with files that have spaces:

(piper-script
 (shell "ls" "~/Google Drive File Stream/My Drive/"))

Give the shell form function multiple strings, and it will assume that all strings are parameters, and not do any parsing, but this function will split single strings on spaces.

How about making an alias for the shell function name?

(piper-script
 ($ "ls /tmp"))

Looks like a prompt character.

Pipes and Data Transformations

I use the same technique described earlier of using a temporary buffer, and running shell commands such that the contents of the buffer become standard in to a shell command, and the results of the command replace the contents of the buffer for the next command. This allows an obvious pipe-like approach:

(piper-script
 ($ "ls" "-1" "/tmp")
 (| "grep Temp")
 (| "tr [a-z] [A-Z]"))

Keep in mind you can use any Emacs function that works with buffers.

Wildcard Expansion

File expansion should work as expected:

(piper-script
 ($ "ls" "~/.emacs.d/*.el"))

Keep in mind that the piper-script looks at every string, so to make this work, if an expansion fails to find matching files, I just return the original string.

Embedded Strings

While format is fine, string-variable substitution is more readable, plus I want to be able to insert both Emacs variables as well as environment variables:

(let ((some-var "fooey"))
  (piper-script
   (let ((nudder-var "chop"))
     (echo "$HOME/projects/${nudder-var}/${some-var}"))))

Should return:

"/home/howard/projects/chop/fooey"
Shell-Like Commands?

I’m not convinced, but perhaps when converted shell script should have shorter, more shell-like function names. In others words, I could use the function grep instead of keep-lines. In this case, if grep were in quotes and passed to the shell function it would call out to the executable, and if not, to the Emacs function, so maybe this would be more confusing.

However, I do need to write some functions, like sudo is a let wrapper around a modification to convert the default-directory variable to reference Tramp.

Let me run through a couple of examples, and for each, I want to show my original shell script, and how I converted it.

First Example

First, let’s see if I can get the resident memory size of two running browsers.

First example's Original Script

While the ps has a lot of command line options, I’m going to use grep to look for the top-level process, that is, the ones where the parent process is 1, and I want to ignore all the extra service frameworks that Chrome starts:

ps -u $USER -o ppid,rss,command | egrep 'Chrome|Firefox' | grep '^ *1' | grep -v Frameworks
New Piper Script for First example

The most prominent advantage is the use of Emacs’ rx macro for the regular expression (which I pass to the keep-lines alias):

(let ((browsers      (rx (or "Chrome" "Firefox")))
      (session-leads (rx line-start (one-or-more blank) "1")))

  (piper-script
   ($ "ps" "-u $USER" "-o ppid,rss,command")
   (grep browsers)
   (grep session-leads)
   (grep-v "Frameworks"))))
Second Example

I’ve installed Ubuntu on an old Macbook laptop, and I noticed that after closing the lid, the computer would repeated turn itself on and off. The reason is that some devices are set to wake up the system, and magic process entry can be used to both read the state of all these devices, but if you write the name of the device back to this process entry, it will turn it off.

Why yes, this does seem like a job for a script, eh?

Second example's original script

My original script uses cat to expose all the entries, which text I can massage with a little pipe sequence. Eventually storing each enabled device in a variable, and writing the value of that variable back into the process entry.

New Piper Script for Second example

The piper-script version starts by looking a lot like the original, but I use a Lisp-looking looping macro I call for and a function that converts all the lines in this temporary buffer into a list of strings for it to consume:

(piper-script
 (sudo
  (cat "/proc/acpi/wakeup")
  (grep "enabled")
  (replace-regexp " .*" "")
  (for (device (read-all-lines))
    (write-into device "/proc/acpi/wakeup"))))
Show us the Magic

The magic is obviously a nifty macro. I simply use a function from the dash library to analyze and possibly change every element I find in the forms given to it, without changing the structure of those forms:

(defmacro piper-script (&rest forms)
  "Runs the FORMS in a shell-like DSL."
  (cons 'with-temp-buffer
        (-tree-map #'piper--script-transform forms)))

Since all command works with a buffer, we create one, run through the converted forms, and not shown here, but we return the contents of the buffer as a string.

Piper Script Transform

The followup magic happens with a function that returns a potentially changed version of the form. It is just a lengthy cond statement, but you can see that at first, I change strings, so that I can expand any wildcards or variable substitution. Then I just swap out various function names for their alias. The way it is currently written, I can’t use any variables with matching names as I don’t currently distinguish their position. Did I mention that this was a proof of concept?

(defun piper--script-transform (element)
  "Helper for `piper-script' to convert forms and strings. "
  (cond
   ((stringp element) `(piper-script-get-files ,element))

   ((eq element '$) 'piper-script-shell)
   ((eq element '|) 'piper-script-shell)

   ((eq element 'echo) 'piper-script-echo)
   ((eq element 'ifsh) 'piper-script-shell-if)
   ((eq element 'for) 'piper-script-loop)

   ((eq element 'cat) 'insert-file-contents)
   ((eq element 'touch) 'f-touch)
   ((eq element 'ln-s) 'f-symlink)   ; Perhaps make-symbolic-link
   ((eq element 'mkdir) 'f-mkdir)    ; ...

   ((eq element 'write-into) 'piper-script-write-into)
   ((eq element 'to-clipboard) 'piper-script-to-clipboard)
   ((eq element 'read-all-lines) 'piper-script-read-all-lines)
   ;; ...
   (t element)))

Summary

A fun little project that I’m ready to show the world, but remember that this won’t be helpful unless you are ready to help me hack on the Lisp code right now. However, it does potentially show how Emacs can improve both ways we use the shell, by interactively loading data into a buffer that can be easily manipulated and in an improved way, sent to other commands.

Summary Slide

I personally think that Emacs Lisp is better than any shell script, and modern libraries help. Some features, like global variables, are condemned, but in your own environment, make Emacs hackable. However, a simple macro can create a DSL within Emacs that makes converting shell scripts easier.

If you want to check out the code base, pop over to emacs-piper project on Gitlab.