Run Program Asynchronously in Emacs

So, after a review of Elisp basics, we're finally ready to integrate our MS Graph with PowerShell script into Emacs.

Simply running the script and waiting for its output won't do, as the script can take a fair bit of time (especially when it needs to get a token for the first time), and this will block the editor. We'll need something asynchronous.

EmacsWiki has a good page on how to execute an external command that includes many synchronous and asynchronous approaches.

In today's approach, we're going to directly leverage the start-process function. The usage is quite simple - a friendly name that will be made unique, a buffer, the program name, and tokenized arguments. We'll set the buffer to nil to avoid creating a buffer.

The function returns a process, and there are two things we'll want to use with to it:

Extracting Output

You can simply read all the output by concatenating strings in the filter.

(setq result "")
; set a filter that will print in minibuffer and accumulate
(set-process-filter myproc
                    (lambda (process output)
                      (progn
                        (message "%s" output)
                        (setq result (concat result output)))))

If you instead want to look for specific parts of it, the string-match function will let you run a regular expression, and then you can extract the parts that you care about.

Handling the Result

The output from our PowerShell script isn't just plain text though, but is a JSON-formatted value.

Fortunately for us, ELisp has the very handy json-parse-string function, which we can use to get structured access to the results.

JSON objects are represented by hashes, and so the gethash function can be used to grab the value we care about.

Other Ideas

At some point, I played with the idea of using the clipboard to make the sign-in PIN more readily available, and to browse to the URL as needed.

However, accessing the clipboard from another process blocks the UI thread, possibly because the program is asked to flush out the contents to the clipboard - turns out a wrote a bunch about this over a decade ago, but note I'm just speculating that this is the smae mechanism.

I also played a bunch with the idea of using environment variables, and I might still use those later, but this will do for now .

All Together

Now, we can put in a little lambda to decide what we want to do with the final result, and have ourselves a nice little function that puts together everything to run our powershell script, read the JSON-formatted output, and present it to the user.

; greet someone whose name starts with 'Mar'
(let ((scopes "user.read openid profile offline_access")
      (url "https://graph.microsoft.com/v1.0/me/contacts?$filter=startswith(displayName,'Mar')")
      (callback (lambda (jresult) (message "Hello %s!" (gethash "displayName" jresult))))
      )
  (setq myproc (start-process "graphmacs" nil "powershell.exe" "-NoLogo" "-NonInteractive" "-ExecutionPolicy" "Bypass" "-File" "graphmacs.ps1"))
  (setq result "")
  ; set a filter that will print in minibuffer and accumulate
  (set-process-filter myproc
                      (lambda (process output)
                        (progn
                          (message "%s" output)
                          (setq result (concat result output)))))
  ; when finished, process the result
  (set-process-sentinel myproc
                        (lambda (process event)
                          (if (string-equal event "finished\n")
                              (progn
                                (message "%s: %s" event result)
                                (string-match "\\({.@odata.+\\)" result)
                                (setq odata (match-string 1 result))
                                (setq jresult (json-parse-string odata))
                                (funcall callback jresult)
                                )))))

Happy running!

Tags:  emacstutorial

Home