1 Introduction
2 Interface
3 History
4 Legal
Version: 1:1

progedit: Programmatic File Editing in Racket

Neil Van Dyke

 (require (planet neil/progedit:1:1))

1 Introduction

The progedit package is for programmatic editing of files via Racket programs. For an example of programmatic editing, progedit was originally written so that the McFly Tools package could modify a user’s "info.rkt", such as by adding define forms that were missing, without modifying anything else in the file.
We expect that progedit will usually be used mostly with syntax objects, in this pattern:
  1. Parse file into syntax objects.

  2. Identify desired changes to the file, in terms of deletes, inserts, and replaces.

  3. Apply all the desired changes to the file in a single read-write pass.

The progedit-file procedure provides a framework for the reading and writing (maintaining a backup file, and restoring the original file on error). The progedit procedure accepts a language for the changes, encoded as dynamically generated lists, and applies the changes to the file.
For example, let’s say we have a file in a language like in file "myfile" below.


(assign honorific "Dr.")
(assign name "John")
; (perpetually)
(assign age 29)
And let’s say we want to write a program that accepts a file in this language, and makes sure that any name variable in the file is set to "Jane". Specifically, if it finds a (assign name value) form in the file, it replaces value with "Jane"; and if it doesn’t find that form in the file, it adds a new (assign name "Jane") form to the file. Here’s such a program, using progedit-file and progedit:
 (lambda (in)
   (let loop ((name-stx #f))
     (let ((stx (read-syntax #f in)))
       (if (eof-object? stx)
           (if name-stx
               (values '()
                       `((,name-stx ,#'"Jane")))
               (values `((#f #\newline
                             ,#'(assign name "Jane")
           (syntax-parse stx
             (((~datum assign) (~datum name) VAL)
              (if name-stx
                   "name assigned multiple times"
                   (list name-stx))
                  (loop #'VAL)))
             (_ (loop name-stx)))))))
 (lambda (in out inserts replaces)
   (progedit in
             #:inserts  inserts
             #:replaces replaces)))
This program will edit the above "myfile" to change "John" to "Jane", so the file becomes:


(assign honorific "Dr.")
(assign name "Jane")
; (perpetually)
(assign age 29)
Now, if we manually edit "myfile" to remove the (assign name value) form altogether, it looks like:


(assign honorific "Dr.")
; (perpetually)
(assign age 29)
If we run our program again, it adds a new form to the end:


(assign honorific "Dr.")
; ; (perpetually)
(assign age 29)
(assign name "Jane")
Notice that, although this particular program parses the file using read-syntax, which doesn’t even see the comment in the file, the comment remains intact. progedit changes only the parts of the file it’s told to, and leaves every other character in the file intact.

2 Interface

The main engine for progedit is the progedit procedure. progedit will often be used in conjunction with the progedit-file procedure.
(default-progedit-write-stx stx out)  void
  stx : syntax?
  out : output-port?
Used as the default for the optional #:write-stx argument of progedit, this procedure writes stx as if it were in the Racket programming language.
(progedit in    
  [#:deletes deletes    
  #:inserts inserts    
  #:replaces replaces    
  #:write-stx write-stx])  void?
  in : input-port?
  out : output-port?
  deletes : list? = '()
  inserts : list? = '()
  replaces : list? = '()
  write-stx : (-> syntax? output-port? any)
   = default-progedit-write-stx
Performs a programmatic editing of the input read from in, writing the edited result to out. This is usually used in conjunction with progedit-file, which supplies the input and output ports.
The edits are specified by the language of the deletes, inserts, and replacements arguments. A BNF-like grammar for this language is:
  deletes = (delete ...)
  inserts = (insert ...)
  replaces = (replace ...)
  delete = syntax
  | (position . position)
  insert = (position . content)
  replace = (delete . content)
  position = exact-positive-integer
  | #f
  | (before . syntax)
  | (after  . syntax)
  | (before syntax)
  | (after  syntax)
  content = syntax
  | string
  | byte-string
  | character
  | input-port
  | procedure
  | (content . content)
  | ()
In general, you usually want to specify position either by a syntax object taken from a parse of the input, or by #f, meaning the end of the input. You can also use a number for the character position, with the characters being numbered starting with 1.
For content, syntax objects are written as Racket code. Strings, byte strings, and characters are written verbatim. Input ports are written by copying their content to the output. Procedures are written by applying the procedure with out as an argument. Pairs are written by recursively writing their CAR and CDR.
(default-progedit-file-backup path)  path?
  path : path-string?
This procedure is used as the default for the optional #:backup argument of progedit-file. It returns (path-add-suffix path ".bak") after deleting any such existing file.
(progedit-file filename    
  #:read read-proc    
  #:write write-proc    
  [#:backup backup-proc])  any
  filename : path-string?
  read-proc : (-> input-port? any)
  write-proc : procedure?
  backup-proc : (-> path-string? path-string?)
   = default-progedit-file-backup
Applies read-proc and write-proc to read and then write file filename, creating a backup file named by calling the backup procedure argument with an argument of filename. Any error results in the file’s contents either being left unchanged or being restored. Changes to the file during write-proc, such as another program modifying the file, results in an error.
Symbolic links are followed, so the actual file is edited, and any symbolic link remains unmodified.
read-proc is called with an argument of an input port on the contents of the file. This is not necessarily on the file itself; it might be on a copy of the file. The value or values returned by read-proc are appended to the arguments when write-proc is called.
write-proc is called with two arguments — an input port and an output-port – with an additional argument for each value returned by read-proc. Normally this will be one additional argument, unless a multiple-value return is used by write-proc. This provides a functional way to communicate information from read-proc to write-proc. The input port will be on the contents of the file before editing, and the write port will be for the contents of the file after editing. Normally, write-proc will use procedure progedit to handle these two ports.

3 History

4 Legal

Copyright 2012 Neil Van Dyke. This program is Free Software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. See http://www.gnu.org/licenses/ for details. For other licenses and consulting, please contact the author.