;;; Field and record parsing utilities for scsh. ;;; Copyright (c) 1994 by Olin Shivers. See file COPYING. ;;; Notes: ;;; - Comment on the dependencies here... ;;; - Awk should deal with case-insensitivity. ;;; - Should I change the field-splitters to return lists? It's the ;;; right thing, and costs nothing in terms of efficiency. ;;; Looping primitives: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; It is nicer for loops that loop over a bunch of different things ;;; if you can encapsulate the idea of iterating over a data structure ;;; with a ;;; (next-element state) -> elt next-state ;;; (more-elements? state) -? #t/#f ;;; generator/termination-test pair. You can use the generator with REDUCE ;;; to make a list; you can stick it into a loop macro to loop over the ;;; elements. For example, if we had an extensible Yale-loop style loop macro, ;;; we could have a loop clause like ;;; ;;; (loop (for field in-infix-delimited-string ":" path) ;;; (do (display field) (newline))) ;;; ;;; and it would be simple to expand this into code using the generator. ;;; With procedural inlining, you can get pretty optimal loops over data ;;; structures this way. ;;; ;;; As of now, you are forced to parse fields into a buffer, and loop ;;; over that. This is inefficient of time and space. If I ever manage to do ;;; an extensible loop macro for Scheme 48, I'll have to come back to this ;;; package and rethink how to provide this functionality. ;;; Forward-progress guarantees and empty string matches. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; A loop that pulls text off a string by matching a regexp against ;;; that string can conceivably get stuck in an infinite loop if the ;;; regexp matches the empty string. For example, the regexps ;;; ^, $, .*, foo|[^f]* can all match the empty string. ;;; ;;; The regexp-loop routines in this code are careful to handle this case. ;;; If a regexp matches the empty string, the next search starts, not from ;;; the end of the match (which in the empty string case is also the ;;; beginning -- there's the rub), but from the next character over. ;;; This is the correct behaviour. Regexps match the longest possible ;;; string at a given location, so if the regexp matched the empty string ;;; at location i, then it is guaranteed they could not have matched ;;; a longer pattern starting with character #i. So we can safely begin ;;; our search for the next match at char i+1. ;;; ;;; So every iteration through the loop makes some forward progress, ;;; and the loop is guaranteed to terminate. ;;; ;;; This has the effect you want with field parsing. For example, if you split ;;; a string with the empty pattern, you will explode the string into its ;;; individual characters: ;;; ((suffix-splitter (rx "")) "foo") -> #("" "f" "o" "o") ;;; However, even though this boundary case is handled correctly, we don't ;;; recommend using it. Say what you mean -- just use a field splitter: ;;; ((field-splitter (rx any)) "foo") -> #("f" "o" "o") ;;; FIELD PARSERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; This section defines routines to split a string into fields. ;;; You can parse by specifying a pattern that *separates* fields, ;;; a pattern that *terminates* fields, or a pattern that *matches* ;;; fields. (define (->delim-matcher x) (if (procedure? x) x ; matcher proc (let ((re (cond ((string? x) (re-string x)) ((char-set? x) (re-char-set x)) ((char? x) (re-string (string x))) ((regexp? x) x) (else (error "Illegal field-reader delimiter value" x))))) (lambda (s i) (cond ((regexp-search re s i) => (lambda (m) (values (match:start m 0) (match:end m 0)))) (else (values #f #f))))))) ;;; (infix-splitter [re num-fields handle-delim]) -> parser ;;; (suffix-splitter [re num-fields handle-delim]) -> parser ;;; (sloppy-suffix-splitter [re num-fields handle-delim]) -> parser ;;; (field-splitter [re num-fields]) -> parser ;;; ;;; (parser string [start]) -> string-list (define (make-field-parser-generator default-delim-matcher loop-proc) ;; This is the parser-generator (lambda args (let-optionals args ((delim-spec default-delim-matcher) (num-fields #f) (handle-delim 'trim)) ;; Process and error-check the args (let ((match-delim (->delim-matcher delim-spec)) (cons-field (case handle-delim ; Field is s[i,j). ((trim) ; Delimiter is s[j,k). (lambda (s i j k fields) (cons (substring s i j) fields))) ((split) (lambda (s i j k fields) (cons (substring s j k) (cons (substring s i j) fields)))) ((concat) (lambda (s i j k fields) (cons (substring s i k) fields))) (else (error "Illegal handle-delim spec" handle-delim))))) (receive (num-fields nfields-exact?) (cond ((not num-fields) (values #f #f)) ((not (integer? num-fields)) (error "Illegal NUM-FIELDS value" num-fields)) ((<= num-fields 0) (values (- num-fields) #f)) (else (values num-fields #t))) ;; This is the parser. (lambda (s . maybe-start) (reverse (loop-proc s (:optional maybe-start 0) match-delim cons-field num-fields nfields-exact?)))))))) ;;; Default field spec is runs of non-whitespace chars. (define default-field-matcher (->delim-matcher (rx (+ (~ white))))) ;;; (field-splitter [field-spec num-fields]) (define (field-splitter . args) (let-optionals args ((field-spec default-field-matcher) (num-fields #f)) ;; Process and error-check the args (let ((match-field (->delim-matcher field-spec))) (receive (num-fields nfields-exact?) (cond ((not num-fields) (values #f #f)) ((not (integer? num-fields)) (error "Illegal NUM-FIELDS value" field-splitter num-fields)) ((<= num-fields 0) (values (- num-fields) #f)) (else (values num-fields #t))) ;; This is the parser procedure. (lambda (s . maybe-start) (reverse (fieldspec-field-loop s (:optional maybe-start 0) match-field num-fields nfields-exact?))))))) ;;; These four procedures implement the guts of each parser ;;; (field, infix, suffix, and sloppy-suffix). ;;; ;;; The CONS-FIELD argument is a procedure that parameterises the ;;; HANDLE-DELIM action for the field parser. ;;; ;;; The MATCH-DELIM argument is used to match a delimiter. ;;; (MATCH-DELIM S I) returns two integers [start, end] marking ;;; the next delimiter after index I in string S. If no delimiter is ;;; found, it returns [#f #f]. ;;; In the main loop of each parser, the loop variable LAST-NULL? tells if the ;;; previous delimiter-match matched the empty string. If it did, we start our ;;; next delimiter search one character to the right of the match, so we won't ;;; loop forever. This means that an empty delimiter regexp "" simply splits ;;; the string at each character, which is the correct thing to do. ;;; ;;; These routines return the answer as a reversed list. (define (fieldspec-field-loop s start match-field num-fields nfields-exact?) (let ((end (string-length s))) (let lp ((i start) (nfields 0) (fields '()) (last-null? #f)) (let ((j (if last-null? (+ i 1) i)) ; Where to start next delim search. ;; Check to see if we made our quota before returning answer. (finish-up (lambda () (if (and num-fields (< nfields num-fields)) (error "Too few fields in record." num-fields s) fields)))) (cond ((> j end) (finish-up)) ; We are done. Finish up. ;; Read too many fields. Bomb out. ((and nfields-exact? (> nfields num-fields)) (error "Too many fields in record." num-fields s)) ;; Made our lower-bound quota. Quit early. ((and num-fields (= nfields num-fields) (not nfields-exact?)) (if (= i end) fields ; Special case hackery. (cons (substring s i end) fields))) ;; Match off another field & loop. (else (receive (m0 m1) (match-field s j) (if m0 (lp m1 (+ nfields 1) (cons (substring s m0 m1) fields) (= m0 m1)) (finish-up))))))))) ; No more matches. Finish up. (define (infix-field-loop s start match-delim cons-field num-fields nfields-exact?) (let ((end (string-length s))) (if (= start end) '() ; Specially hack empty string. (let lp ((i start) (nfields 0) (fields '()) (last-null? #f)) (let ((finish-up (lambda () ;; s[i,end) is the last field. Terminate the loop. (cond ((and num-fields (< (+ nfields 1) num-fields)) (error "Too few fields in record." num-fields s)) ((and nfields-exact? (>= nfields num-fields)) (error "Too many fields in record." num-fields s)) (else (cons (substring s i end) fields))))) (j (if last-null? (+ i 1) i))) ; Where to start next search. (cond ;; If we've read NUM-FIELDS fields, quit early . ((and num-fields (= nfields num-fields)) (if nfields-exact? (error "Too many fields in record." num-fields s) (cons (substring s i end) fields))) ((<= j end) ; Match off another field. (receive (m0 m1) (match-delim s j) (if m0 (lp m1 (+ nfields 1) (cons-field s i m0 m1 fields) (= m0 m1)) (finish-up)))) ; No more delimiters - finish up. ;; We've run off the end of the string. This is a weird ;; boundary case occuring with empty-string delimiters. (else (finish-up)))))))) ;;; Match off an optional initial delimiter, ;;; then jump off to the suffix parser. (define (sloppy-suffix-field-loop s start match-delim cons-field num-fields nfields-exact?) ;; If sloppy-suffix, skip an initial delimiter if it's there. (let ((start (receive (i j) (match-delim s start) (if (and i (zero? i)) j start)))) (suffix-field-loop s start match-delim cons-field num-fields nfields-exact?))) (define (suffix-field-loop s start match-delim cons-field num-fields nfields-exact?) (let ((end (string-length s))) (let lp ((i start) (nfields 0) (fields '()) (last-null? #f)) (let ((j (if last-null? (+ i 1) i))) ; Where to start next delim search. (cond ((= i end) ; We are done. (if (and num-fields (< nfields num-fields)) ; Didn't make quota. (error "Too few fields in record." num-fields s) fields)) ;; Read too many fields. Bomb out. ((and nfields-exact? (= nfields num-fields)) (error "Too many fields in record." num-fields s)) ;; Made our lower-bound quota. Quit early. ((and num-fields (= nfields num-fields) (not nfields-exact?)) (cons (substring s i end) fields)) (else ; Match off another field. (receive (m0 m1) (match-delim s j) (if m0 (lp m1 (+ nfields 1) (cons-field s i m0 m1 fields) (= m0 m1)) (error "Missing field terminator" s))))))))) ;;; Now, build the exported procedures: {infix,suffix,sloppy-suffix}-splitter. (define default-suffix-matcher (->delim-matcher (rx (| (+ white) eos)))) (define default-infix-matcher (->delim-matcher (rx (+ white)))) (define infix-splitter (make-field-parser-generator default-infix-matcher infix-field-loop)) (define suffix-splitter (make-field-parser-generator default-suffix-matcher suffix-field-loop)) (define sloppy-suffix-splitter (make-field-parser-generator default-suffix-matcher sloppy-suffix-field-loop)) ;;; Reading records ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define default-record-delims (char-set #\newline)) ;;; (record-reader [delims elide? handle-delim]) -> reader ;;; (reader [port]) -> string or eof (define (record-reader . args) (let-optionals args ((delims default-record-delims) (elide? #f) (handle-delim 'trim)) (let ((delims (x->char-set delims))) (case handle-delim ((trim) ; TRIM-delimiter reader. (lambda maybe-port (let ((s (apply read-delimited delims maybe-port))) (if (and (not (eof-object? s)) elide?) (apply skip-char-set delims maybe-port)) ; Snarf extra delims. s))) ((concat) ; CONCAT-delimiter reader. (let ((not-delims (char-set-complement delims))) (lambda maybe-port (let* ((p (:optional maybe-port (current-input-port))) (s (read-delimited delims p 'concat))) (if (or (not elide?) (eof-object? s)) s (let ((extra-delims (read-delimited not-delims p 'peek))) (if (eof-object? extra-delims) s (string-append s extra-delims)))))))) ((split) ; SPLIT-delimiter reader. (let ((not-delims (char-set-complement delims))) (lambda maybe-port (let ((p (:optional maybe-port (current-input-port)))) (receive (s delim) (read-delimited delims p 'split) (if (eof-object? s) (values s s) (values s (if (or (not elide?) (eof-object? delim)) delim ;; Elide: slurp in extra delims. (let ((delim (string delim)) (extras (read-delimited not-delims p 'peek))) (if (eof-object? extras) delim (string-append delim extras))))))))))) (else (error "Illegal delimiter-action" handle-delim)))))) ;;; Reading and parsing records ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; (field-reader [field-parser rec-reader]) -> reader ;;; (reader [port]) -> [raw-record parsed-record] or [eof '()] ;;; ;;; This is the field reader, which is basically just a composition of ;;; RECORD-READER and FIELD-PARSER. (define default-field-parser (field-splitter)) (define (field-reader . args) (let-optionals args ((parser default-field-parser) (rec-reader read-line)) (lambda maybe-port (let ((record (apply rec-reader maybe-port))) (if (eof-object? record) (values record '()) (values record (parser record))))))) ;;; Parse fields by regexp ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; This code parses up a record into fields by matching a regexp specifying ;;; the field against the record. The regexp describes the *field*. In the ;;; other routines, the regexp describes the *delimiters*. They are ;;; complimentary. ;;; Repeatedly do (APPLY PROC M STATE) to generate new state values, ;;; where M is a regexp match structure made from matching against STRING. ;(define (regexp-fold string start regexp proc . state) ; (let ((end (string-length string))) ; (let lp ((i start) (state state) (last-null? #f)) ; (let ((j (if last-null? (+ i 1) i))) ; (cond ((and (<= j end) (regexp-search regexp string j)) => ; (lambda (m) ; (receive state (apply proc m state) ; (lp (match:end m) state (= (match:start m) (match:end m)))))) ; (else (apply values state))))))) ; ;(define (all-regexp-matches regexp string) ; (reverse (regexp-fold string 0 regexp ; (lambda (m ans) (cons (match:substring m 0) ans)) ; '())))