├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── cl-string-generator-tests.lisp ├── cl-string-generator.asd ├── compute-length.lisp ├── constants.lisp ├── convert.lisp ├── generate.lisp ├── package.lisp ├── regex-class.lisp └── utils.lisp /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | path: ./cl-string-generator 17 | - name: Install Roswell 18 | env: 19 | LISP: ${{ matrix.lisp }} 20 | run: | 21 | curl -L https://raw.githubusercontent.com/roswell/roswell/master/scripts/install-for-ci.sh | sh 22 | - name: Install Rove 23 | run: ros install rove 24 | - name: Run tests 25 | run: | 26 | PATH="~/.roswell/bin:$PATH" 27 | mkdir -p ~/.roswell/local-projects/pokepay 28 | mv $GITHUB_WORKSPACE/cl-string-generator ~/.roswell/local-projects/pokepay/cl-string-generator 29 | cd ~/.roswell/local-projects/pokepay/cl-string-generator 30 | rove cl-string-generator.asd 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pokepay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cl-string-generator 2 | 3 | Generate string from regular expression 4 | 5 | ## Usage 6 | ```common-lisp 7 | (ql:quickload :cl-string-generator) 8 | (use-package :cl-string-generator) 9 | 10 | (generate "[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{8}") 11 | ;; => "A64BE7F3-1041-6C90-D8EB-2A0F46A8" 12 | 13 | (generate "(foo|bar)\\1") 14 | ;; => "foofoo" 15 | 16 | (generate "a*b*" :max-length 3) 17 | ;; => "aab" 18 | 19 | (generate "a*b*" :min-length 3 :max-length 4) 20 | ;; => "aaab" 21 | ``` 22 | 23 | ## License 24 | MIT 25 | -------------------------------------------------------------------------------- /cl-string-generator-tests.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:cl-string-generator-tests 2 | (:use :cl :cl-string-generator :rove :alexandria)) 3 | (in-package #:cl-string-generator-tests) 4 | 5 | (defun ensure-char (string) 6 | (assert (and (stringp string) 7 | (= 1 (length string)))) 8 | (char string 0)) 9 | 10 | (defun simple-test (regex test expected-num) 11 | (loop :with table := (make-array 128 :initial-element nil) 12 | :repeat 1000 13 | :for string := (generate regex) 14 | :always (and (stringp string) (= 1 (length string)) (funcall test (char string 0))) 15 | :do (setf (aref table (char-code (char string 0))) t) 16 | :finally (return (= expected-num (count t table))))) 17 | 18 | (deftest simple-regex-tests 19 | (ok (string= "" (generate ""))) 20 | (ok (string= "a" (generate #\a))) 21 | (ok (string= "a" (generate "a"))) 22 | (ok (string= "abc" (generate "abc"))) 23 | (ok (simple-test "." #'graphic-char-p 95)) 24 | (ok (simple-test "\\d" #'digit-char-p 10)) 25 | (ok (simple-test "\\w" #'ppcre::word-char-p 63)) 26 | (ok (simple-test "\\s" #'ppcre::whitespacep 5)) 27 | (ok (simple-test "\\D" (complement #'digit-char-p) 118)) 28 | (ok (simple-test "\\S" (complement #'ppcre::whitespacep) 123)) 29 | (ok (simple-test "\\W" (complement #'ppcre::word-char-p) 65))) 30 | 31 | (deftest complex-regex-tests 32 | (ok (string= "ab" (generate '(:sequence "a" "b")))) 33 | (ok (loop :with chars := '() 34 | :repeat 100 35 | :for c := (ensure-char (generate "a|b")) 36 | :do (pushnew c chars) 37 | :finally (return (length= chars 2)))) 38 | (ok (loop :with chars := '() 39 | :repeat 100 40 | :for c := (ensure-char (generate "a|b|c")) 41 | :do (pushnew c chars) 42 | :finally (return (length= chars 3)))) 43 | (ok (string= "ab" (generate "(?:ab)"))) 44 | (ok (string= "abcd" (generate '(:group "ab" "cd")))) 45 | (ok (loop :with chars := '() 46 | :repeat 100 47 | :for c := (ensure-char (generate "[a-c]")) 48 | :do (pushnew c chars) 49 | :finally (return (length= chars 3)))) 50 | (ok (loop :with chars := '() 51 | :repeat 100 52 | :for c := (ensure-char (generate "[a-c0-2_]")) 53 | :do (pushnew c chars) 54 | :finally (return (length= chars 7)))) 55 | (ok (loop :with chars := '() 56 | :repeat 100 57 | :for c := (ensure-char (generate "[abc]")) 58 | :do (pushnew c chars) 59 | :finally (return (length= chars 3)))) 60 | (ok (loop :repeat 10 :always (ppcre:scan "^a*$" (generate "a*")))) 61 | (ok (loop :repeat 100 :always (ppcre:scan "^([0-9]*)\\1$" (generate "([0-9]*)\\1"))))) 62 | 63 | (deftest repetition-tests 64 | (ok (loop :repeat 100 65 | :always (string= (generate "a{4}") "aaaa")))) 66 | 67 | (deftest max-length-tests 68 | (ok (loop :repeat 100 69 | :always (<= (length (generate "a*b*" :max-length 3)) 3)))) 70 | 71 | (deftest random-tests 72 | (ok (loop :repeat 100 73 | :always (ppcre:scan "^[a-zA-Z0-9-]*$" 74 | (generate "^[a-zA-Z0-9-]*$"))))) 75 | -------------------------------------------------------------------------------- /cl-string-generator.asd: -------------------------------------------------------------------------------- 1 | (defsystem "cl-string-generator" 2 | :license "MIT" 3 | :author "Pocket Change" 4 | :description "Generate string from regular expression" 5 | :depends-on ("alexandria" "cl-ppcre") 6 | :serial t 7 | :components ((:file "package") 8 | (:file "constants") 9 | (:file "utils") 10 | (:file "regex-class") 11 | (:file "convert") 12 | (:file "generate")) 13 | :in-order-to ((test-op (test-op "cl-string-generator/tests")))) 14 | 15 | (defsystem "cl-string-generator/tests" 16 | :depends-on ("cl-string-generator" "rove") 17 | :components ((:file "cl-string-generator-tests")) 18 | :perform (test-op (o c) (symbol-call :rove '#:run c))) 19 | -------------------------------------------------------------------------------- /compute-length.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-string-generator) 2 | 3 | (defvar *max-length*) 4 | (defvar *registers*) 5 | 6 | (defun minimum (range) 7 | (if (null range) 8 | 0 9 | (loop :for elt :in range 10 | :minimize (if (consp elt) 11 | (car elt) 12 | elt)))) 13 | 14 | (defun maximum (range) 15 | (if (null range) 16 | *max-length* 17 | (loop :for elt :in range 18 | :maximize (if (consp elt) 19 | (cdr elt) 20 | elt)))) 21 | 22 | (defgeneric add-range (x y) 23 | (:method ((x integer) (y integer)) 24 | (+ x y)) 25 | (:method ((x integer) (y cons)) 26 | (cons (+ x (car y)) (+ x (cdr y)))) 27 | (:method ((x cons) (y integer)) 28 | (cons (+ (car x) y) (+ (cdr x) y))) 29 | (:method ((x cons) (y cons)) 30 | (cons (+ (car x) (car y)) (+ (cdr x) (cdr y))))) 31 | 32 | (defun all-length-candidates (range) 33 | (let ((acc '())) 34 | (labels ((f (range sum) 35 | (if (null range) 36 | (pushnew sum acc :test #'equal) 37 | (dolist (n (first range)) 38 | (f (rest range) (add-range n sum)))))) 39 | (f range 0) 40 | (nreverse acc)))) 41 | 42 | (defgeneric compute-range-of-length-aux (regex)) 43 | 44 | (defmethod compute-range-of-length-aux ((regex void)) 45 | (list 0)) 46 | 47 | (defmethod compute-range-of-length-aux ((regex str)) 48 | (list (length (.str regex)))) 49 | 50 | (defmethod compute-range-of-length-aux ((regex seq)) 51 | (all-length-candidates 52 | (loop :for regex-1 :in (.seq regex) 53 | :for range := (compute-range-of-length-aux regex-1) 54 | :collect range))) 55 | 56 | (defmethod compute-range-of-length-aux ((regex alternation)) 57 | (loop :for regex-1 :in (.choices regex) 58 | :for range := (compute-range-of-length-aux regex-1) 59 | :append range)) 60 | 61 | (defmethod compute-range-of-length-aux ((regex repetition)) 62 | (let ((range (compute-range-of-length-aux (.regex regex)))) 63 | (list (cons (* (minimum range) 64 | (.minimum regex)) 65 | (* (maximum range) 66 | (.maximum regex)))))) 67 | 68 | (defmethod compute-range-of-length-aux ((regex register)) 69 | (let ((range (compute-range-of-length-aux (.regex regex)))) 70 | (setf (gethash (.number regex) *registers*) range) 71 | range)) 72 | 73 | (defmethod compute-range-of-length-aux ((regex back-reference)) 74 | (gethash (.number regex) *registers*)) 75 | 76 | (defmethod compute-range-of-length-aux ((regex random-char)) 77 | (list 1)) 78 | 79 | (defun compute-range-of-length (regex) 80 | (let ((*registers* (make-hash-table))) 81 | (compute-range-of-length-aux regex))) 82 | 83 | (defun compute-fix-length (regex *max-length*) 84 | (let ((range (compute-range-of-length regex))) 85 | (mean (loop :for elt :in range 86 | :collect (if (consp elt) 87 | (cdr elt) 88 | elt))))) 89 | -------------------------------------------------------------------------------- /constants.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-string-generator) 2 | 3 | (defparameter +max-repetition+ 100) 4 | (defparameter +string-length-limit+ 10000) 5 | -------------------------------------------------------------------------------- /convert.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-string-generator) 2 | 3 | (defvar *register-number*) 4 | 5 | (defgeneric convert-simple-parse-tree (parse-tree) 6 | (:method ((parse-tree character)) 7 | (make-instance 'str :str (string parse-tree))) 8 | (:method ((parse-tree string)) 9 | (make-instance 'str :str parse-tree)) 10 | (:method ((parse-tree (eql :void))) 11 | (make-instance 'str :str "")) 12 | (:method ((parse-tree (eql :word-boundary)))) 13 | (:method ((parse-tree (eql :non-word-boundary)))) 14 | (:method ((parse-tree (eql :everything))) 15 | (make-instance 'random-char :function #'random-char)) 16 | (:method ((parse-tree (eql :digit-class))) 17 | (make-instance 'random-char :function #'random-digit-char)) 18 | (:method ((parse-tree (eql :word-char-class))) 19 | (make-instance 'random-char :function #'random-word-char)) 20 | (:method ((parse-tree (eql :whitespace-char-class))) 21 | (make-instance 'random-char :function #'random-whitespace-char)) 22 | (:method ((parse-tree (eql :non-digit-class))) 23 | (make-instance 'random-char :function #'random-non-digit-char)) 24 | (:method ((parse-tree (eql :non-word-char-class))) 25 | (make-instance 'random-char :function #'random-non-word-char)) 26 | (:method ((parse-tree (eql :non-whitespace-char-class))) 27 | (make-instance 'random-char :function #'random-non-whitespace-char)) 28 | (:method ((parse-tree (eql :start-anchor)))) 29 | (:method ((parse-tree (eql :end-anchor)))) 30 | (:method ((parse-tree (eql :modeless-start-anchor)))) 31 | (:method ((parse-tree (eql :modeless-end-anchor)))) 32 | (:method ((parse-tree (eql :modeless-end-anchor-no-newline)))) 33 | (:method ((parse-tree (eql :case-insensitive-p)))) 34 | (:method ((parse-tree (eql :case-sensitive-p)))) 35 | (:method ((parse-tree (eql :multi-line-mode-p)))) 36 | (:method ((parse-tree (eql :not-multi-line-mode-p)))) 37 | (:method ((parse-tree (eql :single-line-mode-p)))) 38 | (:method ((parse-tree (eql :not-single-line-mode-p))))) 39 | 40 | (defun convert-sequence (arguments) 41 | (make-instance 'seq 42 | :seq (loop :for parse-tree :in arguments 43 | :collect (convert-aux parse-tree)))) 44 | 45 | (defun random-char-function (item) 46 | (cond ((characterp item) 47 | (constantly (string item))) 48 | ((symbolp item) 49 | (ecase item 50 | ((:digit-class) 51 | #'random-digit-char) 52 | ((:non-digit-class) 53 | #'random-non-digit-char) 54 | ((:whitespace-char-class) 55 | #'random-whitespace-char) 56 | ((:non-whitespace-char-class) 57 | #'random-non-whitespace-char) 58 | ((:word-char-class) 59 | #'random-word-char) 60 | ((:non-word-char-class) 61 | #'random-non-word-char))) 62 | ((and (consp item) 63 | (eq (first item) :property)) 64 | (error "unsupported inverted-property")) 65 | ((and (consp item) 66 | (eq (first item) :inverted-property)) 67 | (error "unsupported inverted-property")) 68 | ((and (consp item) 69 | (eq (first item) :range)) 70 | (destructuring-bind (min max) (rest item) 71 | (lambda () 72 | (code-char (random-integer (char-code min) (char-code max)))))) 73 | (t (error "Unknown item ~A in char-class list." item)))) 74 | 75 | (defgeneric convert-compound-parse-tree (token parse-tree) 76 | (:method ((token (eql :sequence)) parse-tree) 77 | (convert-sequence (rest parse-tree))) 78 | (:method ((token (eql :alternation)) parse-tree) 79 | (make-instance 'alternation 80 | :choices (loop :for parse-tree :in (rest parse-tree) 81 | :collect (convert-aux parse-tree)))) 82 | (:method ((token (eql :group)) parse-tree) 83 | (if (cddr parse-tree) 84 | (convert-sequence (rest parse-tree)) 85 | (convert-aux (second parse-tree)))) 86 | (:method ((token (eql :char-class)) parse-tree) 87 | (let ((functions 88 | (loop :for item :in (rest parse-tree) 89 | :collect (random-char-function item)))) 90 | (make-instance 'random-char 91 | :function (lambda () 92 | (string (funcall (random-choice functions))))))) 93 | (:method ((token (eql :greedy-repetition)) parse-tree) 94 | (destructuring-bind (min max regex) (rest parse-tree) 95 | (make-instance 'repetition 96 | :regex (convert-aux regex) 97 | :greedyp t 98 | :minimum min 99 | :maximum (or max +max-repetition+)))) 100 | (:method ((token (eql :register)) parse-tree) 101 | (destructuring-bind (regex) (rest parse-tree) 102 | (make-instance 'register 103 | :regex (convert-aux regex) 104 | :number (prog1 *register-number* 105 | (incf *register-number*))))) 106 | (:method ((token (eql :back-reference)) parse-tree) 107 | (destructuring-bind (number) (rest parse-tree) 108 | (make-instance 'back-reference :number number)))) 109 | 110 | (defun convert-aux (parse-tree) 111 | (let ((result 112 | (if (consp parse-tree) 113 | (convert-compound-parse-tree (first parse-tree) parse-tree) 114 | (convert-simple-parse-tree parse-tree)))) 115 | result)) 116 | 117 | (defun convert (regex) 118 | (let ((parse-tree (typecase regex 119 | (string 120 | (ppcre:parse-string regex)) 121 | (otherwise 122 | regex))) 123 | (*register-number* 1)) 124 | (convert-aux parse-tree))) 125 | -------------------------------------------------------------------------------- /generate.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-string-generator) 2 | 3 | (defvar *registers*) 4 | 5 | (define-condition value (condition) 6 | ((value :initarg :value :reader value-of)) 7 | (:report (lambda (c s) 8 | (format s "value: ~S" (value-of c))))) 9 | 10 | (defun value (value) 11 | (check-type value string) 12 | (error 'value :value value)) 13 | 14 | (defmacro select ((value form) &body body) 15 | (with-unique-names (c) 16 | `(handler-bind ((value (lambda (,c) 17 | (let ((,value (value-of ,c))) 18 | ,@body) 19 | (when (find-restart 'next) 20 | (invoke-restart 'next))))) 21 | ,form))) 22 | 23 | (defgeneric generate-aux (regex rest-length)) 24 | 25 | (defmethod generate-aux ((regex null) rest-length) 26 | (value "")) 27 | 28 | (defmethod generate-aux ((regex void) rest-length) 29 | (value "")) 30 | 31 | (defmethod generate-aux ((regex str) rest-length) 32 | (let ((string (.str regex))) 33 | (when (<= (length string) rest-length) 34 | (value string)))) 35 | 36 | (defmethod generate-aux ((regex seq) rest-length) 37 | (labels ((f (seq acc rest-length) 38 | (cond ((null seq) 39 | (value acc)) 40 | (t 41 | (select (string (generate-aux (first seq) rest-length)) 42 | (f (rest seq) 43 | (string-append acc string) 44 | (- rest-length (length string)))))))) 45 | (f (.seq regex) "" rest-length))) 46 | 47 | (defmethod generate-aux ((regex alternation) rest-length) 48 | (let ((choices (.choices regex))) 49 | (dolist (choice (shuffle (copy-list choices))) 50 | (select (string (generate-aux choice rest-length)) 51 | (value string))))) 52 | 53 | (defmethod generate-aux ((regex repetition) rest-length) 54 | (let ((inner-regex (.regex regex))) 55 | (labels ((f (n acc rest-length) 56 | (cond ((zerop n) 57 | (value acc)) 58 | (t 59 | (select (string (generate-aux inner-regex rest-length)) 60 | (f (1- n) 61 | (string-append acc string) 62 | (- rest-length (length string)))))))) 63 | (let ((maximum (min (.maximum regex) rest-length)) 64 | (minimum (.minimum regex))) 65 | (dolist (n (shuffle (loop :for n :from minimum :to maximum :collect n))) 66 | (restart-case 67 | (f n "" rest-length) 68 | (next ()))))))) 69 | 70 | (defmethod generate-aux ((regex register) rest-length) 71 | (handler-bind ((value (lambda (value) 72 | (let ((string (value-of value))) 73 | (setf (gethash (.number regex) *registers*) 74 | string))))) 75 | (generate-aux (.regex regex) rest-length))) 76 | 77 | (defmethod generate-aux ((regex back-reference) rest-length) 78 | (let ((string (gethash (.number regex) *registers*))) 79 | (when (<= (length string) rest-length) 80 | (value string)))) 81 | 82 | (defmethod generate-aux ((regex random-char) rest-length) 83 | (when (<= 1 rest-length) 84 | (value (string (funcall (.function regex)))))) 85 | 86 | (defun %generate (regex max-length) 87 | (let (#+sbcl 88 | (sb-kernel:*maximum-error-depth* 1000000) 89 | (*registers* (make-hash-table))) 90 | (restart-case 91 | (generate-aux (convert regex) (or max-length +string-length-limit+)) 92 | (next ())))) 93 | 94 | (defun generate-using-handler (regex max-length function) 95 | (handler-bind ((value (lambda (value) 96 | (cond ((find-restart 'next) 97 | (funcall function (value-of value)) 98 | (invoke-restart 'next)) 99 | (t 100 | (return-from generate-using-handler)))))) 101 | (%generate regex max-length))) 102 | 103 | (defun generate (regex &key (min-length 0) max-length) 104 | (generate-using-handler regex 105 | max-length 106 | (lambda (string) 107 | (when (<= min-length (length string)) 108 | (return-from generate string))))) 109 | 110 | #+(or) 111 | (defun generate* (regex &optional (max-length 10)) 112 | (generate-using-handler regex 113 | max-length 114 | (lambda (string) 115 | (print string)))) 116 | -------------------------------------------------------------------------------- /package.lisp: -------------------------------------------------------------------------------- 1 | (defpackage :cl-string-generator 2 | (:use :cl :alexandria) 3 | (:export :generate)) 4 | -------------------------------------------------------------------------------- /regex-class.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-string-generator) 2 | 3 | (defclass regex () 4 | ()) 5 | 6 | (defclass void (regex) 7 | ()) 8 | 9 | (defclass str (regex) 10 | ((str 11 | :initarg :str 12 | :reader .str))) 13 | 14 | (defclass seq (regex) 15 | ((seq 16 | :initarg :seq 17 | :reader .seq))) 18 | 19 | (defclass alternation (regex) 20 | ((choices 21 | :initarg :choices 22 | :reader .choices))) 23 | 24 | (defclass repetition (regex) 25 | ((regex 26 | :initarg :regex 27 | :reader .regex) 28 | (greedyp 29 | :initarg :greedyp 30 | :reader .greedyp) 31 | (minimum 32 | :initarg :minimum 33 | :reader .minimum) 34 | (maximum 35 | :initarg :maximum 36 | :reader .maximum))) 37 | 38 | (defclass register (regex) 39 | ((regex 40 | :initarg :regex 41 | :reader .regex) 42 | (number 43 | :initarg :number 44 | :reader .number))) 45 | 46 | (defclass back-reference (regex) 47 | ((number 48 | :initarg :number 49 | :reader .number))) 50 | 51 | (defclass random-char (regex) 52 | ((function 53 | :initarg :function 54 | :reader .function))) 55 | -------------------------------------------------------------------------------- /utils.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-string-generator) 2 | 3 | (defun word-char-p (c) 4 | (or (alphanumericp c) 5 | (char= c #\_))) 6 | 7 | (defun random-choice (sequence) 8 | (elt sequence (random (length sequence)))) 9 | 10 | (defun random-integer (min max) 11 | (+ min (random (1+ (- max min))))) 12 | 13 | (defun random-expected-char (test-function) 14 | (loop :for char := (code-char (random 128)) 15 | :until (funcall test-function char) 16 | :finally (return char))) 17 | 18 | (defun random-char () 19 | (random-expected-char #'graphic-char-p)) 20 | 21 | (defun random-digit-char () 22 | (digit-char (random-integer 0 9))) 23 | 24 | (defun random-non-digit-char () 25 | (random-expected-char (complement #'digit-char-p))) 26 | 27 | (defun random-word-char () 28 | (random-expected-char #'word-char-p)) 29 | 30 | (defun random-non-word-char () 31 | (random-expected-char (complement #'word-char-p))) 32 | 33 | (defun random-whitespace-char () 34 | (random-choice ppcre::+whitespace-char-string+)) 35 | 36 | (defun random-non-whitespace-char () 37 | (random-expected-char (complement #'ppcre::whitespacep))) 38 | 39 | (defun string-append (&rest strings) 40 | (with-output-to-string (out) 41 | (dolist (str strings) 42 | (write-string str out)))) 43 | --------------------------------------------------------------------------------