;; cli.lisp -- Command line interface entry point and option parsing ;; Copyright (C) 2024 Alexander Rosenberg ;; ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU 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 the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . (defpackage #:truth-table/cli (:use #:common-lisp #:truth-table/base #:truth-table/args) (:export #:toplevel #:main)) (in-package :truth-table/cli) (defun eval-and-typeset-propositions (prop-strs &key (format "unicode") (implicit-and t) multi-char-names include-intermediate (include-vars t) pretty-print latin-truths) "Evaluate and then typeset PROP-STRS as a table, which is a list of proposition strings. For a description of the key parameters, see each of the functions involved in evaluating and typesetting." (loop with vars = '() for prop-str in prop-strs for (parsed-exp parsed-vars) = (multiple-value-list (parse-proposition-string prop-str :implicit-and implicit-and :multi-char-names multi-char-names)) when parsed-exp collect parsed-exp into exps do (dolist (var parsed-vars) (pushnew var vars :test 'equal)) finally (let ((table (create-combined-truth-table exps (nreverse vars) :include-intermediate include-intermediate :include-vars include-vars))) (return (typeset-table-to-format table format :pretty-print pretty-print :latin-truths latin-truths))))) (defun word-wrap-string (string &optional (cols 80)) (with-output-to-string (str) (loop with word = () with word-len = 0 with cur-col = 0 for char across string when (whitespace-p char) do (if (>= (+ cur-col word-len 1) cols) (progn (terpri str) (setq cur-col 0)) (unless (zerop cur-col) (format str " ") (incf cur-col))) (format str "~{~c~}" (nreverse word)) (setq word nil cur-col (+ cur-col word-len) word-len 0) else do (push char word) (incf word-len) finally (if (>= (+ cur-col word-len 1) cols) (terpri str) (format str " ")) (format str "~{~c~}" (nreverse word))))) (defun ascii-string-p (str) "Return true if STR is only ASCII characters." (loop for char across str unless (<= (char-code char) 127) do (return-from ascii-string-p)) t) (defun format-syntax-string (syntax-list &key ascii-only) "Format SYNTAX-LIST into a string suitable for printing in a table in `print-syntax-help'." (format nil "~{~a~^, ~}" (sort (copy-list (if ascii-only (remove-if-not 'ascii-string-p syntax-list) syntax-list)) 'string<))) (defun print-syntax-help (ascii-only) "Print the syntax help message." (loop for ((sym (name . nicks) desc examples) . rest-desc) = *operator-descriptions* then rest-desc for ((_sym . syntax) . rest-st) = *operator-symbol-table* then rest-st for syntax-str = (format-syntax-string syntax :ascii-only ascii-only) while sym maximize (length name) into name-col-len maximize (length syntax-str) into syntax-col-len collect syntax-str into syntax-entries finally (setq name-col-len (max name-col-len (length "Operator")) syntax-col-len (max syntax-col-len (length "Syntax"))) (with-draw-table (t (list name-col-len syntax-col-len) (if ascii-only *table-border-ascii-alist* *table-border-unicode-alist*) :padding 1 :align :left) (:row '("Operator" "Syntax")) (:seperator) (loop for (sym (name . nicks) desct) in *operator-descriptions* for syntax-str in syntax-entries do (:row (list name syntax-str))))) (terpri) (loop for (sym . syntax) in *operand-symbol-table* for name = (symbol-name sym) for syntax-str = (format-syntax-string syntax :ascii-only ascii-only) collect (string-downcase name) into names maximize (length name) into name-col-len collect syntax-str into syntax-strs maximize (length syntax-str) into syntax-col-len finally (setq name-col-len (max name-col-len (length "Operand")) syntax-col-len (max syntax-col-len (length "Syntax"))) (with-draw-table (t (list name-col-len syntax-col-len) (if ascii-only *table-border-ascii-alist* *table-border-unicode-alist*) :padding 1 :align :left) (:row '("Operand" "Syntax")) (:seperator) (loop for name in names for syntax-str in syntax-strs do (:row (list name syntax-str))))) (format t "~%~a~%Example:~% abc|d = ~a~%" (word-wrap-string "Two operands next to each other is treated as an 'implicit and' (unless this feature is disabled).") (typeset-proposition '(or (and "a" "b" "c") "d") :lookup-table (if ascii-only *operator-ascii-lookup-alist* *operator-unicode-lookup-alist*)))) (defparameter *command-line-spec* '((#\h "help" help nil "print this message, then exit") (nil "syntax-help" syntax-help nil "print a syntax help message, then exit") (#\f "format" format t "specify the output format (*unicode*, ascii, latex, or html)") (#\s "subexps" subexps nil "include sub-expressions in the output table") (#\n "no-vars" no-vars nil "do not include variables in the output table") (#\m "multi-char" multi-char nil "allow multi-character variable names") (#\i "no-implicit" no-implicit nil "do not use implicit 'and' operations") (#\p "pretty" pretty nil "pretty print latex, html, etc. output") (#\l "latin" latin nil "use the Latin T and F characters for truth values")) "Specification for `parse-command-line'. This is of the format: (short long symbol has-arg-p desc).") (defun main (argv) "The main entry point to the program. ARGV is the list of command line arguments." (let ((cmdline-error nil)) (handler-bind (((or proposition-parse-error proposition-eval-error table-format-error) (lambda (c) (format *error-output* "error: ~a~%" c) (uiop:quit 1))) (command-line-error (lambda (c) ;; finish parsing command line before exiting (format *error-output* "error: ~a~%" c) (setq cmdline-error t) (continue)))) (destructuring-bind ((&rest prop-strs) &rest opts) (parse-command-line *command-line-spec* argv) (cond ((option-value 'help opts) (print-usage t *command-line-spec* "truth-table" :general-args "") (uiop:quit (if cmdline-error 1 0))) ((and (not cmdline-error) ;; if option parsing failed, error out (option-value 'syntax-help opts)) (let ((format (option-value 'format opts))) (cond ((or (not format) (equal format "unicode")) (print-syntax-help nil)) ((equal format "ascii") (print-syntax-help t)) (t (cerror "Exit without printing anything" 'command-line-error :message (format nil "The syntax help table is only ~ available in ASCII or Unicode."))))) (uiop:quit)) ((null prop-strs) (cerror *cli-parse-continue-string* 'no-input-error)) (cmdline-error (format *error-output* "Try -h or --help for more information.~%") (uiop:quit 1)) (t (let ((format (option-value 'format opts))) (when (or (not format) (zerop (length format))) (setq format "unicode")) (princ (eval-and-typeset-propositions prop-strs :format format :implicit-and (not (option-value 'no-implicit opts)) :multi-char-names (option-value 'multi-char opts) :include-vars (not (option-value 'no-vars opts)) :include-intermediate (option-value 'subexps opts) :pretty-print (option-value 'pretty opts) :latin-truths (option-value 'latin opts))) (terpri)))))))) (defun toplevel () "Top-level function to be passed to `save-lisp-and-die'." (handler-case (with-user-abort:with-user-abort (main (uiop:command-line-arguments))) (with-user-abort:user-abort () (format *error-output* "Keyboard interrupt~%") (uiop:quit 1))))