Skip to content

5. The Basics of make-process

In the previous lesson, we explored how to send a request to OpenAI with a simple prompt, "Hello!", using the curl command in the terminal. Now, we aim to replicate this functionality in Emacs using an Emacs Lisp program, specifically focusing on executing the curl command asynchronously.

To achieve this, we will utilize the make-process function, which is designed for creating and managing asynchronous processes within Emacs. Our objective in this lesson is to understand the fundamentals of the make-process function and how to leverage it for our curl command execution.

Let's delve into the details of using make-process for this purpose.

Executing Commands with make-process

To execute the following command echo foo bar baz with the function make-process

$ echo foo bar baz
foo bar baz

we can evaluate the following expression

(make-process
 :name "foo-name"
 :buffer "foo-buff"
 :command (list "echo" "foo" "bar" "baz"))

which starts the program echo in a subprocess passing it the arguments foo, bar, baz and appends its standard output to the buffer foo-buff creating it if it doesn't exist:

foo bar baz

Process foo-name finished

Note that Process foo-name finished has been added by the default process sentinel function. We'll talk more about this later.

The Process Object

This section details functions that we can apply to process objects created by the make-process function.

First, we define the variable foo-proc to hold the process object returned by make-process, which executes the command echo foo bar baz.

(setq foo-proc
      (make-process
       :name "foo-name"
       :buffer "foo-buff"
       :command (list "echo" "foo" "bar" "baz")))

Next, we verify that foo-proc is indeed of the process type and has the name foo-name:

(type-of foo-proc) ;; process
(process-name foo-proc) ;; "foo-name"

We can retrieve the buffer associated with the foo-proc process using the process-buffer function, and check whether foo-proc is still alive with the process-live-p function:

(process-buffer foo-proc) ;; #<buffer foo-buff>
(process-live-p foo-proc) ;; nil

By following these steps, we can effectively manage and query the status of processes in Emacs Lisp.

Executing Commands with Pipes Using make-process

To run a command line that includes a pipe with the make-process function, we need to adapt our approach since pipe syntax is interpreted by the command shell, not as arguments to the program.

For example, this command outputs foo to stdout and returns after 2 seconds:

$ sleep 2 | echo foo
foo

However, using make-process, we cannot directly specify both sleep and echo as separate programs because the :command keyword only accepts a single program file.

Instead, we can leverage the -c option of the sh command, which allows us to execute the next argument as a command. We can format our command like this:

$ sh -c 'sleep 2 | echo foo'
foo

This effectively runs the original command by interpreting it as a complete shell command.

Finally, we can use make-process with that command like this:

(make-process
 :name "foo-name"
 :buffer "foo-buff"
 :command (list "sh" "-c" "sleep 2 | echo foo"))

When we evaluate the above expression, it produces the following output in the foo-buff buffer after 2 seconds:

foo

Process foo-name finished

This setup enables us to run complex command lines with pipes using make-process effectively.

Process Sentinel Overview

Finally, in the following 4 sections we will see how to transfer the contents of a process buffer into another buffer once the process finishes. This is achieved using process sentinel functions attached to the initiated process.

A sentinel function is triggered whenever there is a status change in the process. The first argument passed to the sentinel is the process itself, while the second argument describes the event that caused the status change.

For instance, consider the following code snippet:

(make-process
 :name "foo-name"
 :buffer "foo-buff"
 :command (list "sh" "-c" "sleep 1 | echo foo")
 :sentinel (lambda (process event)
             (message "%S - %S" process event)))

When this expression is evaluated, it will display the following message in the echo area after 1 second:

#<process foo-name> - "finished\n"

To observe a different event type, we can utilize the kill-process function, which terminates the process initiated by make-process. Here's how we can apply the same sentinel function in this context:

(kill-process
 (make-process
  :name "foo-name"
  :buffer "foo-buff"
  :command (list "sh" "-c" "sleep 1 | echo foo")
  :sentinel (lambda (process event)
              (message "%S - %S" process event))))

After evaluating this expression, we see the following output in the echo area after 1 second:

#<process foo-name> - "killed\n"

Branching on the Event Types in the Process Sentinel

We will now enhance the sentinel function to differentiate between successful process completion and unexpected terminations. If the process status changes to "finished\n" we print OK in the echo area. Conversely, for any other status, we display Error:

(make-process
 :name "foo-name"
 :buffer "foo-buff"
 :command (list "sh" "-c" "sleep 1 | echo foo")
 :sentinel (lambda (process event)
             (if (string= event "finished\n")
                 (message "OK")
               (message "Error"))))

Printing the Process Buffer Content in the Echo Area

Now, we can modify the sentinel function to capture the process buffer's content and display it in the echo area:

(make-process
 :name "foo-name"
 :buffer "foo-buff"
 :command (list "sh" "-c" "sleep 1 | echo foo")
 :sentinel
 (lambda (process event)
   (if (not (string= event "finished\n"))
       (message "Error")
     (let ((stdout (with-current-buffer (process-buffer process)
                     (buffer-string))))
       (message "%s" stdout)))))

Within the let binding, we use with-current-buffer to switch to the process's output buffer. This allows us to read its contents. buffer-string extracts the complete string output, which contains the string foo. The final message function outputs the captured string to the echo area, indicating the process's output.

Redirecting Process Buffer Content to Another Buffer

Finally we modify the previous code snippet to redirect the content of the process buffer into a new buffer named bar, rather than displaying it in the echo area. To achieve this, we utilize the get-buffer-create function to create the bar buffer. We then use the insert function to place the output from the process buffer into the bar buffer:

(make-process
 :name "foo-name"
 :buffer "foo-buff"
 :command (list "sh" "-c" "sleep 1 | echo foo")
 :sentinel
 (lambda (process event)
   (if (not (string= event "finished\n"))
       (message "Error")
     (let ((stdout (with-current-buffer (process-buffer process)
                     (buffer-string))))
       (with-current-buffer (get-buffer-create "bar")
         (insert stdout))))))