edward

An extensible POSIX-compatible implementation of the ed(1) text editor

git clone https://git.8pit.net/edward.git

  1;; Command parsers are registered in the following alist through macros
  2;; provided below. These macros use the register-command procedure.
  3;; Parsers can be obtained from the alist using get-command-parsers.
  4
  5(define command-parsers '())
  6
  7;; This procedure can be used to register a new editor command. It
  8;; receives a unique command `name` and an executor procedure `proc` as
  9;; an argument. The amount of parameters passed to `proc` depends on the
 10;; associated parser combinator.
 11;;
 12;; **Warning:** Avoid calling this procedure directly and instead use
 13;; the high-level interface provided by the command definition macros
 14;; [described below](#section-defining-commands).
 15
 16(define (register-command name proc)
 17  (set! command-parsers
 18    (alist-cons name proc command-parsers)))
 19
 20(define (get-command-parsers exclude)
 21  (fold (lambda (x y)
 22          (if (member (car x) exclude)
 23            y
 24            (cons (cdr x) y)))
 25        '() command-parsers))
 26
 27;; Print commands (l, n, p) are additionally tracked in a seperated
 28;; alist. This eases implementing commands which can be suffixed
 29;; with a print command (see parse-print-cmd definition below).
 30
 31(define print-commands '())
 32
 33(define (register-print-command char proc)
 34  (set! print-commands
 35    (alist-cons char proc print-commands)))
 36
 37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 38
 39;;>| Defining Commands
 40;;>
 41;;> Conceptually, edward distinguishes the following four command types:
 42;;>
 43;;> 1. *Print commands*, e.g. the `p` command. These can be used as
 44;;>    suffixes to edit commands.
 45;;> 2. *Edit commands*, i.e. commands which modify the text editor
 46;;>    buffer in some way (e.g. `d`).
 47;;> 3. *Input-mode commands*. Like the edit commands, but read
 48;;>    additional data from input mode.
 49;;> 4. *File commands*, which perform I/O operations and cannot be
 50;;>    suffixed with a print command.
 51;;>
 52;;> Commands of the different types are defined using the abstractions
 53;;> described in this section. Every command definition requires at least
 54;;> a unique command name (a symbol) and an executor procedure which is
 55;;> passed the editor object and values returned by the command parser.
 56
 57;;> Define a new file command. Apart from the unique `name` and
 58;;> executor procedure `proc`, commands of this type require a default
 59;;> [edward address][edward ed addr] `addr`. If no default address is
 60;;> provided, it is assumed that this command doesn't expect an address.
 61;;> Furthermore, a a parser combinator definition needs to be provided in
 62;;> the `body`. The combinators defined in the `body` are expanded to a
 63;;> [parse-blanks-seq][parse-blanks-seq]. The first combinator of the
 64;;> body must be a [parse-cmd-char][parse-cmd-char]. All
 65;;> [non-ignored][parse-ignore] parser combinator return values are
 66;;> passed to `proc` as procedure arguments.
 67;;>
 68;;>    (define-file-cmd (name proc addr) body ...)
 69;;>
 70;;> [edward ed addr]: edward.ed.addr.html
 71;;> [parse-blanks-seq]: edward.parse.html#parse-blanks-seq
 72;;> [parse-ignore]: edward.parse.html#parse-ignore
 73;;> [parse-cmd-char]: #parse-cmd-char
 74
 75(define-syntax define-file-cmd
 76  (syntax-rules ()
 77    ((define-file-cmd (NAME EXECUTOR ADDR) BODY ...)
 78     (register-command (quote NAME)
 79       (parse-map
 80         (parse-blanks-seq
 81           BODY ...
 82           (parse-ignore parse-newline))
 83         (lambda (args)
 84           (make-cmd (quote NAME) ADDR EXECUTOR args)))))
 85    ((define-file-cmd (NAME EXECUTOR) BODY ...)
 86     (define-file-cmd (NAME EXECUTOR '()) BODY ...))))
 87
 88;;> Define a new edit command. These commands are conceptually similar
 89;;> to file commands. Therefore, please refer to the documentation of
 90;;> [define-file-cmd][define-file-cmd] for more information on the
 91;;> parameters.
 92;;>
 93;;> Contrary to file commands, edit commands can additionally be suffixed
 94;;> with a print command. If a print command suffix is present, this
 95;;> print command will be executed after the editor changes have been
 96;;> performed by the edit command.
 97;;>
 98;;>    (define-edit-cmd (name proc addr) body ...)
 99;;>
100;;> [define-file-cmd]: #define-file-cmd
101
102(define-syntax define-edit-cmd
103  (syntax-rules ()
104    ((define-edit-cmd (NAME EXECUTOR ADDR) BODY ...)
105     (register-command (quote NAME)
106       (parse-map
107         (parse-seq
108           (parse-blanks-seq BODY ...)
109           (parse-optional parse-print-cmd)
110           (parse-ignore parse-blanks)
111           (parse-ignore parse-newline))
112         (lambda (args)
113           (cmd-with-print (quote NAME) ADDR EXECUTOR
114                           (first args) (second args))))))
115    ((define-edit-cmd (NAME EXECUTOR) BODY ...)
116     (define-edit-cmd (NAME EXECUTOR '()) BODY ...))))
117
118;;> Define a new input command. These commands are conceptually similar
119;;> to edit commands. Similar to edit commands, input commands can also
120;;> be suffixed with a print command. Therefore, please refer to the
121;;> documentation of [define-edit-cmd][define-edit-cmd] for more
122;;> information on the parameters.
123;;>
124;;> Contrary to other command types, input commands additionally
125;;> read data using ed input mode. The received data is passed as a
126;;> list of lines as the last parameter to `proc`.
127;;>
128;;>    (define-input-cmd (name proc addr) body ...)
129;;>
130;;> [define-edit-cmd]: #define-edit-cmd
131
132(define-syntax define-input-cmd
133  (syntax-rules ()
134    ((define-input-cmd (NAME EXECUTOR ADDR) BODY ...)
135     (register-command (quote NAME)
136       (parse-map
137         (parse-seq
138           (parse-blanks-seq BODY ...)
139           (parse-optional parse-print-cmd)
140           (parse-ignore parse-blanks)
141           (parse-ignore parse-newline)
142
143           parse-input-mode
144           (parse-ignore
145             (parse-or
146               parse-end
147               (parse-seq
148                 (parse-string ".")
149                 (parse-seq parse-blanks parse-newline)))))
150         (lambda (args)
151           (cmd-with-print
152             (quote NAME)
153             ADDR
154             EXECUTOR
155             (append (first args) (list (third args)))
156             (second args))))))))
157
158;; According to POSIX.1-2008 it is invalid for more than one command to
159;; appear on a line. However, commands other than e, E, f, q, Q, r, w, and !
160;; can be suffixed by the commands l, n, or p. In this case the suffixed
161;; command is executed and then the new current line is written as
162;; defined by the l, n, or p command.
163
164(define parse-print-cmd
165  (parse-lazy ;; must be lazy, otherwise print-commands is not populated.
166    (parse-strip-blanks
167      (parse-map
168        (parse-alist print-commands)
169        (lambda (proc)
170          (make-cmd 'print-suffix (make-range) proc '()))))))
171
172;; Define a new command which can be suffixed by a print command.
173
174(define (cmd-with-print symbol def-addr executor cmd-args print-cmd)
175  (make-cmd
176    symbol
177    def-addr
178    (lambda (editor . args)
179      (if (null? def-addr) ;; If command expects address
180        (editor-exec editor #f (make-cmd symbol def-addr executor args))
181        (editor-xexec editor (car args) (make-cmd symbol def-addr executor (cdr args))))
182      (when print-cmd
183        (editor-exec editor #f print-cmd)))
184    cmd-args))
185
186;;> Define a new print command. Print commands are automatically parsed
187;;> using [parse-cmd-char](#parse-cmd-char) based on the provided
188;;> `cmd-char` character. No custom parser combinator can be supplied
189;;> for these commands. Furthermore, print commands always use the
190;;> current line as the default address. Similar to other command types,
191;;> a unique command `name` (a symbol) must be defined. The executor
192;;> procedure `proc` is always passed an editor object and the address
193;;> range which was passed to the command.
194
195(define (define-print-cmd name proc char)
196  (register-print-command char proc)
197    (register-command name
198      (parse-map
199        (parse-seq
200          (parse-blanks-seq (parse-cmd-char char))
201          (parse-ignore (parse-optional parse-print-cmd))
202          (parse-ignore parse-blanks)
203          (parse-ignore parse-newline))
204        (lambda (args)
205          (make-cmd name (make-range) proc (car args))))))
206
207;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
208
209;;>| Command Parsing
210;;>
211;;> Procedures to invoke parsers for defined editor commands.
212
213;; Parse any of the commands listed above and strip any trailing blanks
214;; as the command letter can be preceded by zero or more <blank>
215;; characters.
216;;
217;; Returns a list where car is the executor for the parsed commands and
218;; cdr are the arguments which are supposed to be passed to this
219;; executor.
220
221(define (%parse-cmd parsers)
222  (parse-map
223    (parse-seq
224      (parse-optional parse-addrs)
225      (apply
226        parse-or
227        (append parsers (list (parse-fail "unknown command")))))
228    (lambda (x)
229      (let ((cmd (last x))
230            (addr (first x)))
231        (cons addr cmd)))))
232
233;;> Parse a single, arbitrary command that was previously defined using
234;;> one of the abstractions [described above][define commands]. If no
235;;> command parser matches the input, then parsing fails with the error
236;;> message `"unknown command"`.
237;;>
238;;> [define commands]: #section-defining-commands
239
240(define (parse-cmd)
241  (%parse-cmd (get-command-parsers '())))
242
243(define (parse-global-cmd)
244  (%parse-cmd
245    ;; Filter out cmds producing undefined behaviour in global command.
246    (get-command-parsers '(%eof global interactive global-unmatched
247                           interactive-unmatched shell-escape))))
248
249(define (parse-interactive-cmd)
250  (parse-or
251    (parse-bind 'eof parse-end)
252    (parse-bind 'null-command parse-newline)
253    (parse-bind 'repeat-previous (parse-string "&\n"))
254    (%parse-cmd
255      ;; Filter out cmds not supported in interactive mode (as per POSIX).
256      (get-command-parsers '(%eof append change insert global interactive
257                             global-unmatched interactive-unmatched)))))
258
259;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
260
261;;>| Parser Utilities
262;;>
263;;> Utility parser combinators that are useful for defining editor
264;;> command parsers and, contrary to the combinators defined in
265;;> [edward parse][edward parse], are somewhat specific to ed(1).
266;;>
267;;> [edward parse]: edward.parse.html
268
269;;> Parse a command character within a [parse-seq][parse-seq] /
270;;> [parse-blanks-seq][parse-blanks-seq]. This character is ignored
271;;> in the sequence and as such not returned.
272;;>
273;;> [parse-seq]: edward.parse.html#parse-seq
274;;> [parse-blanks-seq]: edward.parse.html#parse-blanks-seq
275
276(define (parse-cmd-char ch)
277  ;; TODO: Prefix failure reason with command char that failed to parse.
278  (parse-ignore (parse-commit (parse-char ch))))
279
280;;> Read input data in the input mode format. Returns a list of parsed
281;;> lines as strings which do not include the terminating newlines.
282
283(define parse-input-mode
284  (parse-repeat
285    (parse-assert
286      parse-line
287      (lambda (line)
288        (not (equal? line "."))))))
289
290;; Parse a delimiter for a regular expression. As per POSIX, any
291;; character other then <space> and <newline> can be a delimiter.
292
293(define parse-delim-char
294  (parse-char (char-set-complement (char-set #\space #\newline))))
295
296;;> Parse RE pair for the substitute command (e.g. `/RE/replacement/`).
297;;> The given procedure is responsible for parsing the replacement, it is
298;;> passed the utilized delimiter as a single character function
299;;> argument.
300;;>
301;;> Returns triplet `(RE, replacement, print?)` where `print?` indicates
302;;> if the closing delimiter was emitted, i.e. if the resulting string
303;;> should be printed after the replacement was performed.
304
305(define (parse-re-pair delim-proc)
306  (parse-with-context
307    parse-delim-char
308    (lambda (delim)
309      (parse-seq
310        (parse-regex-lit delim)
311        (delim-proc delim)
312        (parse-or
313          (parse-bind #t parse-end-of-line)
314          (parse-bind #f (parse-char delim)))))))
315
316;;> Parses a regular expression enclosed by two matching delimiter characters.
317
318(define parse-re
319  (parse-with-context
320    parse-delim-char
321    parse-regex-lit*))
322
323;; Read lines of a command list and perform unescaping of newlines.
324;; Returns a string which can then be further processed using
325;; parse-command-list. Basically, this is a two stage parsing process.
326
327(define parse-line-continuation
328  (parse-map
329    (parse-seq
330      (parse-token (lambda (x)
331                     (and
332                       (not (char=? x #\\))
333                       (not (char=? x #\newline)))))
334      (parse-esc (parse-char #\newline)))
335    (lambda (lst)
336      (string-append (car lst) "\n"))))
337
338(define parse-last-line
339  (parse-map
340    (parse-token (lambda (x) (not (char=? x #\newline))))
341    (lambda (str)
342      (string-append str "\n"))))
343
344(define unwrap-command-list+
345  (parse-map
346    (parse-seq
347      (parse-repeat parse-line-continuation)
348      parse-last-line)
349    (lambda (lst)
350      (string-append
351        (apply string-append (first lst))
352        (second lst)))))
353
354;;> Parse a command list, as passed to the `g` and `v` command.
355
356(define unwrap-command-list
357  (parse-or
358    ;; empty command list is equivalent to the p command
359    (parse-bind "p\n" parse-end-of-line)
360    unwrap-command-list+))
361
362;; Returns list of editor command from a command list string as created
363;; by the unwrap-command-list procedure. The list can afterwards be
364;; passed to the editor-exec-cmdlist procedure.
365
366(define (parse-command-list cmdstr)
367  (call-with-parse (parse-repeat+ (parse-global-cmd))
368                   (string->parse-stream cmdstr)
369                   0
370                   (lambda (r s i fk)
371                     (if (parse-stream-end? s i)
372                       r
373                       (fk s i "incomplete command list parse")))
374                   (lambda (s i reason) (editor-raise reason))))
375
376;;> Parses a filename, which is then read/written by ed. A file name is
377;;> either a path to a file or a shell command as passed to the ed
378;;> shell escape command. The latter is recognized by a `!` character
379;;> prefix.
380
381(define parse-filename
382  (parse-atomic
383    (parse-or
384      (parse-map
385        (parse-seq
386          (parse-string "!")
387          (parse-token (lambda (x) (not (char=? x #\newline)))))
388        (lambda (lst) (apply string-append lst)))
389      (parse-token char-set:graphic))))
390
391;;> Parses a command character followed by an optional file parameter.
392;;> The parameters *must* be separated by one or more <blank>
393;;> characters.
394
395(define (parse-file-cmd ch)
396  (parse-map
397    (parse-seq
398      (parse-cmd-char ch)
399      (parse-default
400        (parse-map (parse-seq parse-blanks+ parse-filename) cadr)
401        ""))
402    car))
403
404;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
405
406;;>| Executor Utilities
407;;>
408;;> Utility procedures for common command executor operations.
409
410;;> If changes have been made to the current buffer since the last write
411;;> of the buffer to a file, then ed should warn the user before the
412;;> buffer is destroyed. Warnings must be confirmed by repeating the
413;;> command, which closes the buffer.
414;;>
415;;> This procedure expects an editor record, the symbol of the command
416;;> to be repeated and a thunk executed if the command was confirmed or
417;;> no confirmation is necessary (i.e. buffer was not modified).
418
419(define (call-when-confirmed editor cmd-sym thunk)
420  (if (or
421        (eqv? (text-editor-prevcmd editor) cmd-sym)
422        (not (text-editor-modified? editor)))
423    (thunk)
424    ;; Can't use editor-raise here as the prevcmd in the
425    ;; editor record is not updated then (see editor-start).
426    (editor-error editor "Warning: buffer modified")))
427
428;;> Parameterizable executor for substitution cases where no addressed
429;;> line matched the desired substitution. Can be overwritten using
430;;> parameterize. By default, an error is raised if no substitution
431;;> was performed.
432
433(define subst-nomatch-handler
434  (make-parameter
435    (lambda (msg)
436      (editor-raise msg))))
437
438;; Execute line-proc for each matched line for a global command.
439
440(define (each-matched-line editor lines regex match-proc line-proc)
441  (let ((bre (editor-make-regex editor regex)))
442    (for-each (lambda (line)
443                (when (match-proc bre line)
444                  ;; The executed command may perform modifications
445                  ;; which affect line numbers. As such, we find the
446                  ;; current number for the given line using pointer
447                  ;; comparision on the text editor buffer.
448                  (let ((lnum (editor-get-lnum editor line)))
449                    (when lnum ;; line has not been deleted by a preceeding command
450                      (parameterize ((subst-nomatch-handler id))
451                        (line-proc lnum line))))))
452              (editor-get-lines editor lines))))
453
454;;> Execute a command list, parsed using
455;;> [unwrap-command-list](#unwrap-command-list), for the `g` and `v`
456;;> command.
457
458(define (exec-command-list editor match-proc lines regex cmdstr)
459  (let ((cmds (parse-command-list cmdstr)))
460    (each-matched-line editor lines regex match-proc
461                       (lambda (lnum line)
462                         (editor-goto! editor lnum)
463                         (editor-exec-cmdlist editor cmds)))))
464
465;;> Like [exec-command-list](#exec-command-list) but intended to be used
466;;> for interactive commands, i.e. `G` and `V`.
467
468(define (exec-command-list-interactive editor match-proc lines regex)
469  (define previous-command '())
470  (define (get-interactive editor)
471    (let* ((cmd (editor-interactive editor (parse-interactive-cmd)))
472           (ret (case cmd
473                  ((eof) (editor-raise "unexpected end-of-file"))
474                  ((null-command) #f)
475                  ((repeat-previous)
476                   (if (null? previous-command)
477                     (editor-raise "no previous command")
478                     previous-command))
479                  (else cmd))))
480      (when ret
481        (set! previous-command ret))
482      ret))
483
484  (each-matched-line editor lines regex match-proc
485                     (lambda (lnum line)
486                       (println line)
487                       (let ((cmd-pair (get-interactive editor)))
488                         (when cmd-pair ;; not null command
489                           (editor-goto! editor lnum)
490                           (editor-exec editor (car cmd-pair) (cdr cmd-pair)))))))
491
492;;> Predicate which returns true if the given string `fn` is a file name
493;;> and not a shell command.
494
495(define (filename-cmd? fn)
496  (and
497    (not (empty-string? fn))
498    (eqv? (string-ref fn 0) #\!)))
499
500(define (filename-unwrap fn)
501  (let ((fn-cmd? (filename-cmd? fn)))
502    (if fn-cmd?
503      (values #t (string-copy fn 1))
504      (values #f fn))))
505
506;;> Write a list of `lines` (represented as a string without a terminating
507;;> newline) to a given `filename`. If `filename` starts with `!` (i.e.
508;;> is a command according to [filename-cmd?](#filename-cmd?)), write data
509;;> to standard input of given command string.
510;;>
511;;> Returns amount of bytes written to the `filename` on success and false
512;;> if an error occurred.
513
514(define (write-lines filename lines)
515  (let-values (((fn-cmd? fn) (filename-unwrap filename)))
516    (with-io-error-handler fn
517      (lambda ()
518        (let ((proc (lambda (port) (lines->port lines port))))
519          (if fn-cmd?
520            (call-with-output-pipe fn proc)
521            (call-with-output-file fn proc)))))))
522
523;;> Read data from given filename as a list of lines. If filename start
524;;> with `!` (i.e. is a command), read data from the standard output of
525;;> the given command.
526;;>
527;;> If an error occurs, returns false and prints an error message to the
528;;> current-error-port. Otherwise, returns a pair of retrieved lines and
529;;> amount of total bytes received.
530
531(define (read-from filename)
532  (let-values (((fn-cmd? fn) (filename-unwrap filename)))
533    (with-io-error-handler fn
534      (lambda ()
535        (if fn-cmd?
536          (call-with-input-pipe fn port->lines)
537          (call-with-input-file fn port->lines))))))
538
539(define (with-io-error-handler fn thunk)
540  (call-with-current-continuation
541    (lambda (k)
542      (with-exception-handler
543        (lambda (eobj)
544          (fprintln (current-error-port) fn ": "
545                    (error-object-message eobj))
546          (k #f))
547        thunk))))