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-cmd101102(define-syntax define-edit-cmd103 (syntax-rules ()104 ((define-edit-cmd (NAME EXECUTOR ADDR) BODY ...)105 (register-command (quote NAME)106 (parse-map107 (parse-seq108 (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 EXECUTOR114 (first args) (second args))))))115 ((define-edit-cmd (NAME EXECUTOR) BODY ...)116 (define-edit-cmd (NAME EXECUTOR '()) BODY ...))))117118;;> Define a new input command. These commands are conceptually similar119;;> to edit commands. Similar to edit commands, input commands can also120;;> be suffixed with a print command. Therefore, please refer to the121;;> documentation of [define-edit-cmd][define-edit-cmd] for more122;;> information on the parameters.123;;>124;;> Contrary to other command types, input commands additionally125;;> read data using ed input mode. The received data is passed as a126;;> 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-cmd131132(define-syntax define-input-cmd133 (syntax-rules ()134 ((define-input-cmd (NAME EXECUTOR ADDR) BODY ...)135 (register-command (quote NAME)136 (parse-map137 (parse-seq138 (parse-blanks-seq BODY ...)139 (parse-optional parse-print-cmd)140 (parse-ignore parse-blanks)141 (parse-ignore parse-newline)142143 parse-input-mode144 (parse-ignore145 (parse-or146 parse-end147 (parse-seq148 (parse-string ".")149 (parse-seq parse-blanks parse-newline)))))150 (lambda (args)151 (cmd-with-print152 (quote NAME)153 ADDR154 EXECUTOR155 (append (first args) (list (third args)))156 (second args))))))))157158;; According to POSIX.1-2008 it is invalid for more than one command to159;; 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 suffixed161;; command is executed and then the new current line is written as162;; defined by the l, n, or p command.163164(define parse-print-cmd165 (parse-lazy ;; must be lazy, otherwise print-commands is not populated.166 (parse-strip-blanks167 (parse-map168 (parse-alist print-commands)169 (lambda (proc)170 (make-cmd 'print-suffix (make-range) proc '()))))))171172;; Define a new command which can be suffixed by a print command.173174(define (cmd-with-print symbol def-addr executor cmd-args print-cmd)175 (make-cmd176 symbol177 def-addr178 (lambda (editor . args)179 (if (null? def-addr) ;; If command expects address180 (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-cmd183 (editor-exec editor #f print-cmd)))184 cmd-args))185186;;> Define a new print command. Print commands are automatically parsed187;;> using [parse-cmd-char](#parse-cmd-char) based on the provided188;;> `cmd-char` character. No custom parser combinator can be supplied189;;> for these commands. Furthermore, print commands always use the190;;> current line as the default address. Similar to other command types,191;;> a unique command `name` (a symbol) must be defined. The executor192;;> procedure `proc` is always passed an editor object and the address193;;> range which was passed to the command.194195(define (define-print-cmd name proc char)196 (register-print-command char proc)197 (register-command name198 (parse-map199 (parse-seq200 (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))))))206207;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;208209;;>| Command Parsing210;;>211;;> Procedures to invoke parsers for defined editor commands.212213;; Parse any of the commands listed above and strip any trailing blanks214;; 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 and218;; cdr are the arguments which are supposed to be passed to this219;; executor.220221(define (%parse-cmd parsers)222 (parse-map223 (parse-seq224 (parse-optional parse-addrs)225 (apply226 parse-or227 (append parsers (list (parse-fail "unknown command")))))228 (lambda (x)229 (let ((cmd (last x))230 (addr (first x)))231 (cons addr cmd)))))232233;;> Parse a single, arbitrary command that was previously defined using234;;> one of the abstractions [described above][define commands]. If no235;;> command parser matches the input, then parsing fails with the error236;;> message `"unknown command"`.237;;>238;;> [define commands]: #section-defining-commands239240(define (parse-cmd)241 (%parse-cmd (get-command-parsers '())))242243(define (parse-global-cmd)244 (%parse-cmd245 ;; Filter out cmds producing undefined behaviour in global command.246 (get-command-parsers '(%eof global interactive global-unmatched247 interactive-unmatched shell-escape))))248249(define (parse-interactive-cmd)250 (parse-or251 (parse-bind 'eof parse-end)252 (parse-bind 'null-command parse-newline)253 (parse-bind 'repeat-previous (parse-string "&\n"))254 (%parse-cmd255 ;; Filter out cmds not supported in interactive mode (as per POSIX).256 (get-command-parsers '(%eof append change insert global interactive257 global-unmatched interactive-unmatched)))))258259;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;260261;;>| Parser Utilities262;;>263;;> Utility parser combinators that are useful for defining editor264;;> command parsers and, contrary to the combinators defined in265;;> [edward parse][edward parse], are somewhat specific to ed(1).266;;>267;;> [edward parse]: edward.parse.html268269;;> Parse a command character within a [parse-seq][parse-seq] /270;;> [parse-blanks-seq][parse-blanks-seq]. This character is ignored271;;> in the sequence and as such not returned.272;;>273;;> [parse-seq]: edward.parse.html#parse-seq274;;> [parse-blanks-seq]: edward.parse.html#parse-blanks-seq275276(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))))279280;;> Read input data in the input mode format. Returns a list of parsed281;;> lines as strings which do not include the terminating newlines.282283(define parse-input-mode284 (parse-repeat285 (parse-assert286 parse-line287 (lambda (line)288 (not (equal? line "."))))))289290;; Parse a delimiter for a regular expression. As per POSIX, any291;; character other then <space> and <newline> can be a delimiter.292293(define parse-delim-char294 (parse-char (char-set-complement (char-set #\space #\newline))))295296;;> Parse RE pair for the substitute command (e.g. `/RE/replacement/`).297;;> The given procedure is responsible for parsing the replacement, it is298;;> passed the utilized delimiter as a single character function299;;> argument.300;;>301;;> Returns triplet `(RE, replacement, print?)` where `print?` indicates302;;> if the closing delimiter was emitted, i.e. if the resulting string303;;> should be printed after the replacement was performed.304305(define (parse-re-pair delim-proc)306 (parse-with-context307 parse-delim-char308 (lambda (delim)309 (parse-seq310 (parse-regex-lit delim)311 (delim-proc delim)312 (parse-or313 (parse-bind #t parse-end-of-line)314 (parse-bind #f (parse-char delim)))))))315316;;> Parses a regular expression enclosed by two matching delimiter characters.317318(define parse-re319 (parse-with-context320 parse-delim-char321 parse-regex-lit*))322323;; Read lines of a command list and perform unescaping of newlines.324;; Returns a string which can then be further processed using325;; parse-command-list. Basically, this is a two stage parsing process.326327(define parse-line-continuation328 (parse-map329 (parse-seq330 (parse-token (lambda (x)331 (and332 (not (char=? x #\\))333 (not (char=? x #\newline)))))334 (parse-esc (parse-char #\newline)))335 (lambda (lst)336 (string-append (car lst) "\n"))))337338(define parse-last-line339 (parse-map340 (parse-token (lambda (x) (not (char=? x #\newline))))341 (lambda (str)342 (string-append str "\n"))))343344(define unwrap-command-list+345 (parse-map346 (parse-seq347 (parse-repeat parse-line-continuation)348 parse-last-line)349 (lambda (lst)350 (string-append351 (apply string-append (first lst))352 (second lst)))))353354;;> Parse a command list, as passed to the `g` and `v` command.355356(define unwrap-command-list357 (parse-or358 ;; empty command list is equivalent to the p command359 (parse-bind "p\n" parse-end-of-line)360 unwrap-command-list+))361362;; Returns list of editor command from a command list string as created363;; by the unwrap-command-list procedure. The list can afterwards be364;; passed to the editor-exec-cmdlist procedure.365366(define (parse-command-list cmdstr)367 (call-with-parse (parse-repeat+ (parse-global-cmd))368 (string->parse-stream cmdstr)369 0370 (lambda (r s i fk)371 (if (parse-stream-end? s i)372 r373 (fk s i "incomplete command list parse")))374 (lambda (s i reason) (editor-raise reason))))375376;;> Parses a filename, which is then read/written by ed. A file name is377;;> either a path to a file or a shell command as passed to the ed378;;> shell escape command. The latter is recognized by a `!` character379;;> prefix.380381(define parse-filename382 (parse-atomic383 (parse-or384 (parse-map385 (parse-seq386 (parse-string "!")387 (parse-token (lambda (x) (not (char=? x #\newline)))))388 (lambda (lst) (apply string-append lst)))389 (parse-token char-set:graphic))))390391;;> Parses a command character followed by an optional file parameter.392;;> The parameters *must* be separated by one or more <blank>393;;> characters.394395(define (parse-file-cmd ch)396 (parse-map397 (parse-seq398 (parse-cmd-char ch)399 (parse-default400 (parse-map (parse-seq parse-blanks+ parse-filename) cadr)401 ""))402 car))403404;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;405406;;>| Executor Utilities407;;>408;;> Utility procedures for common command executor operations.409410;;> If changes have been made to the current buffer since the last write411;;> of the buffer to a file, then ed should warn the user before the412;;> buffer is destroyed. Warnings must be confirmed by repeating the413;;> command, which closes the buffer.414;;>415;;> This procedure expects an editor record, the symbol of the command416;;> to be repeated and a thunk executed if the command was confirmed or417;;> no confirmation is necessary (i.e. buffer was not modified).418419(define (call-when-confirmed editor cmd-sym thunk)420 (if (or421 (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 the425 ;; editor record is not updated then (see editor-start).426 (editor-error editor "Warning: buffer modified")))427428;;> Parameterizable executor for substitution cases where no addressed429;;> line matched the desired substitution. Can be overwritten using430;;> parameterize. By default, an error is raised if no substitution431;;> was performed.432433(define subst-nomatch-handler434 (make-parameter435 (lambda (msg)436 (editor-raise msg))))437438;; Execute line-proc for each matched line for a global command.439440(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 modifications445 ;; which affect line numbers. As such, we find the446 ;; current number for the given line using pointer447 ;; 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 command450 (parameterize ((subst-nomatch-handler id))451 (line-proc lnum line))))))452 (editor-get-lines editor lines))))453454;;> Execute a command list, parsed using455;;> [unwrap-command-list](#unwrap-command-list), for the `g` and `v`456;;> command.457458(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-proc461 (lambda (lnum line)462 (editor-goto! editor lnum)463 (editor-exec-cmdlist editor cmds)))))464465;;> Like [exec-command-list](#exec-command-list) but intended to be used466;;> for interactive commands, i.e. `G` and `V`.467468(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 cmd473 ((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 ret481 (set! previous-command ret))482 ret))483484 (each-matched-line editor lines regex match-proc485 (lambda (lnum line)486 (println line)487 (let ((cmd-pair (get-interactive editor)))488 (when cmd-pair ;; not null command489 (editor-goto! editor lnum)490 (editor-exec editor (car cmd-pair) (cdr cmd-pair)))))))491492;;> Predicate which returns true if the given string `fn` is a file name493;;> and not a shell command.494495(define (filename-cmd? fn)496 (and497 (not (empty-string? fn))498 (eqv? (string-ref fn 0) #\!)))499500(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))))505506;;> Write a list of `lines` (represented as a string without a terminating507;;> newline) to a given `filename`. If `filename` starts with `!` (i.e.508;;> is a command according to [filename-cmd?](#filename-cmd?)), write data509;;> to standard input of given command string.510;;>511;;> Returns amount of bytes written to the `filename` on success and false512;;> if an error occurred.513514(define (write-lines filename lines)515 (let-values (((fn-cmd? fn) (filename-unwrap filename)))516 (with-io-error-handler fn517 (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)))))))522523;;> Read data from given filename as a list of lines. If filename start524;;> with `!` (i.e. is a command), read data from the standard output of525;;> the given command.526;;>527;;> If an error occurs, returns false and prints an error message to the528;;> current-error-port. Otherwise, returns a pair of retrieved lines and529;;> amount of total bytes received.530531(define (read-from filename)532 (let-values (((fn-cmd? fn) (filename-unwrap filename)))533 (with-io-error-handler fn534 (lambda ()535 (if fn-cmd?536 (call-with-input-pipe fn port->lines)537 (call-with-input-file fn port->lines))))))538539(define (with-io-error-handler fn thunk)540 (call-with-current-continuation541 (lambda (k)542 (with-exception-handler543 (lambda (eobj)544 (fprintln (current-error-port) fn ": "545 (error-object-message eobj))546 (k #f))547 thunk))))