Close

October 2, 2019

Emacs external code formatting in Python

One of the nice things with Emacs is its extensibility. You can write some Emacs Lisp to add some new functionality, if it does not already exist in MELPA. There are times however when writing an extension in another language might be useful (eg: Using C, C++, Perl, Lua etc).

Recently I was writing some User RPL code in Emacs for my trusty HP 50g, and decided to write a simple formatter to illustrate how to call external code that modifies the current Emacs buffer you are working on.

The Elisp program makes use of shell-command-on-region to call out to an external python program that reads from stdin and outputs the formatted text to stdout. We can indicate to Emacs to replace the text in the current buffer with the output from the external command.

An extract of the interesting parts of the Emacs extension is shown here.

;;; fs-userrpl-tools.el  --- Some tools to make UserRPL programming a better experience

 ;;; Code:
 (defgroup fs-userrpl nil
   "Tools for UserRPL programming."
   :prefix "fs-userrpl-"
   :group 'applications)
 (defcustom fs-userrpl-formatter-program "/home/frank/repos/userrpl_tools/userrpl_format.py"
   "External program that formats UserRPL code."
   :type '(string)
   :group 'fs-userrpl)
 (defun fs-userrpl-format-on-buffer ()
   "Format the current buffer using UserRPL formatting."
   (interactive)
   (save-excursion
     (shell-command-on-region (point-min) (point-max) fs-userrpl-formatter-program t t)
     (whitespace-cleanup)
     ))
 (provide 'fs-userrpl-tools)
 ;;; fs-userrpl-tools.el ends here

Basically this will allow you to type M-x fs-userrpl-format-on-buffer that calls our external Python script userrpl_format.py to format input text passed to it from Emacs. The python script simply reads from stdin and outputs modified text to stdout. It is shown here.

#!/usr/bin/env python3
#
# Copyright (C) Frank Singleton b17flyboy@gmail.com
#
# Simple UserRPL code formatter. Called from emacs
#
# For now '\<<' triggers indent increase, and '\>>' triggers indent decrease
# All lines are stripped of excess whitespace via strip() also
#

import sys


def do_main():
    """Main function"""

    # defines local indent style
    indent = 0
    indent_inc = 4
    indent_string = ' '

    for line in sys.stdin:
        line = line.strip()
        if line.startswith(r'\<<'):
            # print line then increase indent
            print('{}{}'.format(indent_string * indent, line))
            indent += indent_inc
        elif line.startswith(r'\>>'):
            # decrease indent then print line
            indent -= indent_inc
            print('{}{}'.format(indent_string * indent, line))
        else:
            # print at current indent
            print('{}{}'.format(indent_string * indent, line))


if __name__ == '__main__':
    do_main()

So, invoking M-x fs-userrpl-format-on-buffer will format existing code like this

@
@ Simple prototype to support syncing time from remote PC.
@
@ Copyright (C) Frank Singleton b17flyboy at gmail.com
@
@ Wait for 9 characters to be received from serial port.
@ eg: 21.152185 (HH.MMSSss) and set TIME on calculator
@
@ For example, use hptimesync.py.
   @
@ 1. Press SYNCT on calculator
@ 2. Run hptimesync.py on laptop (eg: Fedora VM)
@
@ Note: received time string is placed on stack as string
 @ so you can see what was received.
 @
 @

   'SYNCT' PURGE
 \<<
       MEM DROP @ force garbage collection
       9600 BAUD
           CLEAR
       ERR0   @ clear last error status
    @ init IO, clear buffers/errors
  CLOSEIO
  OPENIO

  @ wait for 9 characters. simple protocol
         @ no error checking for now.
    9 SRECV DROP
       DUP STR\-> \->TIME
    \>>


'SYNCT' STO

as something more pleasing, like this.

@
@ Simple prototype to support syncing time from remote PC.
@
@ Copyright (C) Frank Singleton b17flyboy at gmail.com
@
@ Wait for 9 characters to be received from serial port.
@ eg: 21.152185 (HH.MMSSss) and set TIME on calculator
@
@ For example, use hptimesync.py.
@
@ 1. Press SYNCT on calculator
@ 2. Run hptimesync.py on laptop (eg: Fedora VM)
@
@ Note: received time string is placed on stack as string
@ so you can see what was received.
@
@

'SYNCT' PURGE
\<<
    MEM DROP @ force garbage collection
    9600 BAUD
    CLEAR
    ERR0   @ clear last error status
    @ init IO, clear buffers/errors
    CLOSEIO
    OPENIO

    @ wait for 9 characters. simple protocol
    @ no error checking for now.
    9 SRECV DROP
    DUP STR\-> \->TIME
\>>


'SYNCT' STO

So go check out the project on Bitbucket to see how shell-command-on-region works and how  userrpl_format.py implements a simple formatter.

Cheers …