Version: 1:3
progedit: Programmatic File Editing in Racket
(require (planet neil/progedit:1:3)) |
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:
Parse file into syntax objects.
Identify desired changes to the file, in terms of deletes, inserts, and replaces.
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
(retaining or restoring the original file contents on an error, and leaving a
backup file on success). 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.
"myfile"
(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:
#lang racket |
(require (planet neil/progedit:1) |
syntax/parse) |
|
(progedit-file |
"myfile" |
#:read |
(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") |
#\newline)) |
'())) |
(syntax-parse stx |
(((~datum assign) (~datum name) VAL) |
(if name-stx |
(raise-syntax-error |
'foo |
"name assigned multiple times" |
stx |
#f |
(list name-stx)) |
(loop #'VAL))) |
(_ (loop name-stx))))))) |
#:write |
(lambda (in out inserts replaces) |
(progedit in |
out |
#:inserts inserts |
#:replaces replaces))) |
This program will edit the above "myfile" to change "John" to "Jane", so the file becomes:
"myfile"
(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:
"myfile"
(assign honorific "Dr.") |
; (perpetually) |
(assign age 29) |
If we run our program again, it adds a new form to the end:
"myfile"
(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 | | | | | | | out | | | | | | [ | #: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.
If, during evaluation of read-proc, you decide not to modify file filename after all, you can use an escape continuation. In this case, file filename will remain unmodified, although there will still be the effects of backup-proc and possibly of modification to the file that procedure
indicated.
Note that some of the semantics of progedit-file are intentionally vague, since precise filesystem semantics are
subtle vary among different host platforms, and we don’t want to constrain the
implementation in ways that encumber future enhancements unnecessarily.
3 History
PLaneT 1:3 — 2012-06-14
PLaneT 1:2 — 2012-06-11
PLaneT 1:1 — 2012-06-11
PLaneT 1:0 — 2012-06-11
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.