diff options
author | Colin Okay <okay@toyful.space> | 2022-03-11 08:35:55 -0600 |
---|---|---|
committer | Colin Okay <okay@toyful.space> | 2022-03-11 08:35:55 -0600 |
commit | 08ba2769abb7a36817a725d30d64cfd36f5bcf32 (patch) | |
tree | 3068a5d9e1a4afd06a5f8b236658d4094e30dfef | |
parent | 62afeb3f21d3d8e8db045a001271e686e944a049 (diff) |
separated app and lib modules, -osicat dep, +packages.lisp
-rw-r--r-- | app/app.lisp (renamed from src/app.lisp) | 5 | ||||
-rw-r--r-- | app/package.lisp | 5 | ||||
-rw-r--r-- | clpmfile.lock | 7 | ||||
-rw-r--r-- | lib/lib.lisp (renamed from src/lib.lisp) | 303 | ||||
-rw-r--r-- | lib/oneliner.lisp | 59 | ||||
-rw-r--r-- | lib/package.lisp | 20 | ||||
-rw-r--r-- | lib/prompt.lisp (renamed from src/prompt.lisp) | 5 | ||||
-rw-r--r-- | lib/running.lisp | 47 | ||||
-rw-r--r-- | lib/state.lisp | 62 | ||||
-rw-r--r-- | lib/term.lisp | 15 | ||||
-rw-r--r-- | lib/util.lisp | 37 | ||||
-rw-r--r-- | oneliners.cli.asd | 13 |
12 files changed, 321 insertions, 257 deletions
diff --git a/src/app.lisp b/app/app.lisp index c3b4c0a..44a5f97 100644 --- a/src/app.lisp +++ b/app/app.lisp @@ -15,10 +15,7 @@ ;; You should have received a copy of the GNU Affero General Public License ;; along with this program. If not, see <http://www.gnu.org/licenses/>. -(defpackage #:oneliners.cli.app - (:use #:cl #:net.didierverna.clon) - (:local-nicknames (#:a #:alexandria) - (#:cli #:oneliners.cli))) + (in-package :oneliners.cli.app) diff --git a/app/package.lisp b/app/package.lisp new file mode 100644 index 0000000..7e4c234 --- /dev/null +++ b/app/package.lisp @@ -0,0 +1,5 @@ + +(defpackage #:oneliners.cli.app + (:use #:cl #:net.didierverna.clon) + (:local-nicknames (#:a #:alexandria) + (#:cli #:oneliners.cli))) diff --git a/clpmfile.lock b/clpmfile.lock index e852e8c..76e1373 100644 --- a/clpmfile.lock +++ b/clpmfile.lock @@ -76,7 +76,6 @@ ("oneliners.api-client")) ("oneliners.cli.asd" :version :newest :source :implicit-file :systems ("oneliners.cli")) -("osicat" :version "2022-02-20" :source "quicklisp" :systems ("osicat")) ("proc-parse" :version "2019-08-13" :source "quicklisp" :systems ("proc-parse")) ("quri" :version "2021-06-30" :source "quicklisp" :systems ("quri")) ("smart-buffer" :version "2021-10-21" :source "quicklisp" :systems @@ -109,7 +108,6 @@ ("alexandria" ((:system :name "static-vectors") (:system :name "alexandria")) ((:system :name "quri") (:system :name "alexandria")) ((:system :name "proc-parse") (:system :name "alexandria")) - ((:system :name "osicat") (:system :name "alexandria")) ((:system :name "fast-io") (:system :name "alexandria")) ((:system :name "fast-http") (:system :name "alexandria")) ((:system :name "dexador") (:system :name "alexandria")) @@ -135,8 +133,6 @@ ("cffi" ((:system :name "static-vectors") (:system :name "cffi")) ((:system :name "static-vectors") (:system :name "cffi-grovel")) - ((:system :name "osicat") (:system :name "cffi")) - ((:system :name "osicat") (:system :name "cffi-grovel")) ((:system :name "net.didierverna.clon.termio") (:system :name "cffi")) ((:system :name "cl-readline") (:system :name "cffi")) ((:system :name "cl+ssl") (:system :name "cffi")) @@ -231,8 +227,6 @@ ("oneliners.cli.asd" (t (:asd-file :name "oneliners.cli.asd"))) -("osicat" ((:system :name "oneliners.cli") (:system :name "osicat"))) - ("proc-parse" ((:system :name "jonathan") (:system :name "proc-parse")) ((:system :name "fast-http") (:system :name "proc-parse")) ((:system :name "cl-cookie") (:system :name "proc-parse"))) @@ -251,7 +245,6 @@ ((:system :name "oneliners.cli") (:system :name "trivial-clipboard"))) ("trivial-features" - ((:system :name "osicat") (:system :name "trivial-features")) ((:system :name "dexador") (:system :name "trivial-features")) ((:system :name "cl+ssl") (:system :name "trivial-features")) ((:system :name "cffi") (:system :name "trivial-features")) diff --git a/src/lib.lisp b/lib/lib.lisp index 4e1630e..f49201a 100644 --- a/src/lib.lisp +++ b/lib/lib.lisp @@ -14,44 +14,53 @@ ;; You should have received a copy of the GNU Affero General Public License ;; along with this program. If not, see <http://www.gnu.org/licenses/>. - - -(defpackage #:oneliners.cli - (:use :cl) - (:import-from #:oneliners.cli.prompt #:prompt) - (:local-nicknames (#:api #:oneliners.api-client) - (#:a #:alexandria))) - (in-package :oneliners.cli) ;;; CONFIG AND RESULTS FILE LOCATIONS -(defvar *config* nil - "A configuration plist") - (defvar *ol-output-timeout* 1) -(defun ensure-local-state () - (ensure-config) - (setf api::*host* (getf *config* :host))) - +(defun run-item (ident args &key force-clip (timeout nil timeout-p)) + (with-oneliner (ol ident) + (let ((*ol-output-timeout* (if timeout-p timeout *ol-output-timeout*))) + (bind-vars-and-run-oneliner ol args force-clip)))) -(defun valid-config-p (config) - (and (listp config) - (evenp (length config)) - (stringp (getf config :host)) - t)) +(defun bind-vars-and-run-oneliner (ol args &optional force-clip) + (let* ((oneliner (oneliner-oneliner ol)) + (runstyle (oneliner-runstyle ol)) + (pos-args (collect-positional-arguments oneliner)) + (named-args (collect-named-arguments oneliner))) + + (when (or (not (oneliner-isflagged ol)) + (y-or-n-p "This oneliner is flagged. Are you sure you want to run it?")) + ;; substitute positional args + (loop for param in pos-args + for arg in args + do (setf oneliner (str:replace-all param arg oneliner))) + ;; substitute named args + (setf args + (mapcar (lambda (s) (str:split "=" s)) + (nthcdr (length pos-args) args))) + (loop for var in named-args + for bound = (assoc (subseq var 1) args :test #'equal) + when bound + do (setf oneliner + (str:replace-all var (second bound) oneliner))) + + (handle-run-oneliner oneliner (or force-clip (equalp runstyle "manual")))))) -(defun write-config-to-disk () - (let ((conf-file (config-file))) - (ensure-directories-exist conf-file) - (with-open-file (out conf-file :direction :output :if-exists :supersede) - (print *config* out)))) +(defun handle-run-oneliner (ol &optional clip) + (if clip + (progn (trivial-clipboard:text ol) + (format t "Copied oneliner to clipboard~%")) + (progn + (ensure-config) + (format t "Attempting to run:~%") + (princ ol) + (princ #\newline) + (princ #\newline) + (run-with-shell ol :shell-name (or (get-shell) "bash"))))) -(defun make-config (&key host api-token editor (shell "bash")) - (append (when host (list :host host)) - (when api-token (list :api-token api-token)) - (list :shell shell))) (defun make-fresh-config () (format t "No configuration file has been found. Running Setup~%~%") @@ -77,81 +86,19 @@ (make-fresh-config)) (fetch-config-from-disk)) -(defun host () (getf *config* :host)) -(defun api-token () (getf *config* :api-token)) -(defun (setf api-token) (newval) - (setf (getf *config* :api-token) newval)) -(defun get-shell () - (getf *config* :shell)) -(defun contributor-handle () (getf *config* :handle)) -(defun (setf contributor-handle) (newval) - (setf (getf *config* :handle) newval)) - -(defun config-file () - (merge-pathnames ".config/oneliners.config" (user-homedir-pathname))) - -(defun cached-oneliners-file () - (merge-pathnames ".cache/cached_oneliners" (user-homedir-pathname))) - -(defun wipe-cache () - (uiop:delete-file-if-exists (cached-oneliners-file))) - -(defun merge-into-cache (new-ols) - (let* ((cached - (when (uiop:file-exists-p (cached-oneliners-file)) - (with-open-file (input (cached-oneliners-file)) (read input)))) - (updated - (append - new-ols - (loop for old in cached - for id = (getf old :id) - unless (find id new-ols :key (lambda (x) (getf x :id))) - collect old)))) - (ensure-directories-exist (cached-oneliners-file)) - (with-open-file (output (cached-oneliners-file) :direction :output :if-exists :supersede) - (print updated output)))) ;;; UTILITIES -(defun make-temp-file-name () - (namestring - (merge-pathnames (format nil "~a" (gensym "oneliners")) (uiop:temporary-directory)))) - -(defun string-from-editor (&optional contents) - (let ((filename (make-temp-file-name))) - (when contents (a:write-string-into-file contents filename :if-exists :supersede)) - (unwind-protect - (magic-ed:magic-ed filename :eval nil :output :string) - (uiop:delete-file-if-exists filename)))) - -(defun executable-on-system-p (name) - "A hack that heuristically determins whether or not an executable -with the provided name is on the system. It is not perfect. It -consults the environment PATH, and looks for the command in any of -the directories that appear in the value of that variable." - #+unix - (loop for path in (str:split ":" (uiop:getenv "PATH")) - for directory = (cons :absolute - (cdr (str:split "/" path))) - thereis (uiop:file-exists-p - (make-pathname :name name :directory directory)))) - -(defun tags-from-oneliner (oneliner) - (remove-duplicates - (remove-if-not #'executable-on-system-p (ppcre:split " +" oneliner)) - :test #'equal)) - - - - - -(defun cached-result (n) - (when (uiop:file-exists-p (cached-oneliners-file)) - (let ((contents (with-open-file (input (cached-oneliners-file)) (read input)))) - (etypecase n - (integer - (find n contents :key (lambda (x) (getf x :id)))) - (string - (find n contents :key (lambda (x) (getf x :name)) :test #'equal)))))) + + +;; (defun cached-result (n) +;; (when (uiop:file-exists-p (cached-oneliners-file)) +;; (let ((contents (with-open-file (input (cached-oneliners-file)) (read input)))) +;; (etypecase n +;; (integer +;; (find n contents :key (lambda (x) (getf x :id)))) +;; (string +;; (find n contents :key (lambda (x) (getf x :name)) :test #'equal)))))) + (defmacro with-oneliner ((var name-or-id) &body body) (assert (symbolp var)) @@ -161,19 +108,9 @@ the directories that appear in the value of that variable." (progn ,@body) (format t "Could not find the oneliner specified by ~a~%" ,nvar))))) -(defun print-item-explanation (name-or-number) - (with-oneliner (ol name-or-number) - (set-term-width) - (print-oneliner-result-for-user ol) - (a:when-let (explanation (getf ol :explanation)) - (format t "EXPLANATION:~%~%") - (princ - (string-trim - '(#\newline #\space #\tab) - (if (str:starts-with? (getf ol :oneliner) explanation) - (subseq explanation (length (getf ol :oneliner))) - explanation))) - (terpri)))) + + + ;;; API REQUEST FUNCTIONS @@ -219,73 +156,25 @@ and, failing that, try to fetch from configured server." (:host (host)) (api:put--oneliner-oneliner-locked (getf ol :id) :token (api-token) :value "false")))) -(defun collect-positional-arguments (oneliner) - (remove-duplicates - (sort - (ppcre:all-matches-as-strings "#[1-9][0-9]*" oneliner) - #'string<) - :test #'equal)) - -(defun collect-named-arguments (oneliner) - (remove-duplicates - (ppcre:all-matches-as-strings "#[A-Za-z][A-Za-z0-9_]*" oneliner) - :test #'equal)) - -(defun handle-run-oneliner (ol &optional clip) - (if clip - (progn (trivial-clipboard:text ol) - (format t "Copied oneliner to clipboard~%")) - (progn - (ensure-config) - (format t "Attempting to run:~%") - (princ ol) - (princ #\newline) - (princ #\newline) - (run-with-shell ol :shell-name (or (get-shell) "bash"))))) - -(defun bind-vars-and-run-oneliner (ol args &optional force-clip) - (let* ((oneliner (getf ol :oneliner)) - (runstyle (getf ol :runstyle)) - (pos-args (collect-positional-arguments oneliner)) - (named-args (collect-named-arguments oneliner))) - - (when (or (not (getf ol :isflagged)) - (y-or-n-p "This oneliner is flagged. Are you sure you want to run it?")) - ;; substitute positional args - (loop for param in pos-args - for arg in args - do (setf oneliner (str:replace-all param arg oneliner))) - ;; substitute named args - (setf args - (mapcar (lambda (s) (str:split "=" s)) - (nthcdr (length pos-args) args))) - (loop for var in named-args - for bound = (assoc (subseq var 1) args :test #'equal) - when bound - do (setf oneliner - (str:replace-all var (second bound) oneliner))) - - (handle-run-oneliner oneliner (or force-clip (equalp runstyle "manual")))))) -(defun run-item (ident args &key force-clip (timeout nil timeout-p)) - (with-oneliner (ol ident) - (let ((*ol-output-timeout* (if timeout-p timeout *ol-output-timeout*))) - (bind-vars-and-run-oneliner ol args force-clip)))) -(defun valid-oneliner-string-p (string) - (and (not (find #\newline string)) - (tags-from-oneliner string))) +;;; PRINTING ONELINERS -(defun valid-brief-description-p (string) - (<= (length string) 72)) -(defun valid-runstyle-p (string) - (member string '("auto" "manual") :test 'equalp)) +(defun print-item-explanation (name-or-number) + (with-oneliner (ol name-or-number) + (set-term-width) + (print-oneliner-result-for-user ol) + (a:when-let (explanation (getf ol :explanation)) + (format t "EXPLANATION:~%~%") + (princ + (string-trim + '(#\newline #\space #\tab) + (if (str:starts-with? (getf ol :oneliner) explanation) + (subseq explanation (length (getf ol :oneliner))) + explanation))) + (terpri)))) -(defun valid-oneliner-name-p (string) - (or (equal string "") - (and (< 2 (length string)) - (ppcre:scan "^[a-zA-Z][a-zA-Z0-9_\-]+$" string)))) (defun add-new-oneliner () (ensure-config) @@ -491,16 +380,6 @@ and, failing that, try to fetch from configured server." (defun update-cached-item (item) (merge-into-cache (list item))) -(defvar *term-width* nil) - -(defun set-term-width () - ;; tput cols b/c getenv COLUMNS wasnt working on some terminals - (setf *term-width* - (- - (or (parse-integer (uiop:run-program '("tput" "cols") :output :string) :junk-allowed t) - 80) - 4))) - (defun print-oneliner-result-for-user (oneliner) (unless *term-width* (set-term-width)) ; setting here as a fallback, can set it elswere if desired. (let* ((title-line-format-str @@ -570,53 +449,3 @@ and, failing that, try to fetch from configured server." ;;; RUNNING THINGS IN THE SHELL. -(defun parent-process-name () - "Prints the name of the parent process of the current process." - (let ((ppidfile (format nil "/proc/~a/status" (osicat-posix:getppid)))) - (first (last - (ppcre:split "\\s" - (with-open-file (input ppidfile) - (read-line input))))))) - -(defmacro wait-until ((&key (timeout 1) (poll-every 0.01)) &body check) - "Run CHECK every POLL-EVERY seconds until either TIMEOUT seconds -have passed or CHECK returns non-nil." - (let ((clockvar (gensym)) - (var (gensym))) - `(loop - for ,clockvar from 0 by ,poll-every to ,timeout - for ,var = (progn ,@check) - when ,var - return ,var - do (sleep ,poll-every) - finally (return nil)))) - - - -(defun run-with-shell - (command - &key - (shell-name (parent-process-name)) - (await-output-p *ol-output-timeout*) - (output-stream *standard-output*)) - "run COMMAND, a string, in a fresh shell environment, initialized -with SHELL-NAME. The output from the command read line by line and is -printed to OUTPUT-STREAM. " - (let ((shell - (uiop:launch-program shell-name :input :stream :output :stream))) - (symbol-macrolet ((shell-input (uiop:process-info-input shell)) - (shell-output (uiop:process-info-output shell))) - (write-line command shell-input) - (finish-output shell-input) - (if (and await-output-p - (plusp await-output-p) - (wait-until (:timeout await-output-p :poll-every 0.005) - (listen shell-output))) - (loop while (listen shell-output) - do (princ (read-line shell-output) output-stream) - (terpri output-stream) - (sleep 0.005)))))) - - - - diff --git a/lib/oneliner.lisp b/lib/oneliner.lisp new file mode 100644 index 0000000..b7b404b --- /dev/null +++ b/lib/oneliner.lisp @@ -0,0 +1,59 @@ +;;;; oneliner.lisp -- holds a local representation of onelienrs. + +(in-package :oneliners.cli) + +(defstruct oneliner + id + name + oneliner + tags + brief + explanation + runstyle + createdat + editedat + createdby + isflagged + islocked) + + +(defun collect-positional-arguments (ol) + "Collects the names of all positional arguments in the oneliner, prefix included." + (remove-duplicates + (sort + (ppcre:all-matches-as-strings "#[1-9][0-9]*" (oneliner-oneliner ol)) + #'string<) + :test #'equal)) + +(defun collect-named-arguments (ol) + "Collects the names of all named arguments in the oneliner, prefix included" + (remove-duplicates + (ppcre:all-matches-as-strings "#[A-Za-z][A-Za-z0-9_]*" (oneliner-oneliner ol)) + :test #'equal)) + +(defun tags-from-oneliner (string) + "Splits a string using consequitive whitespace as a separator, +returning a set of tags" + (remove-duplicates + (remove-if-not #'executable-on-system-p (ppcre:split " +" string)) + :test #'equal)) + + + + +;;; VALIDATION OF ONELINER SLOT VALUES + +(defun valid-oneliner-string-p (string) + (and (not (find #\newline string)) + (tags-from-oneliner string))) + +(defun valid-brief-description-p (string) + (<= (length string) 72)) + +(defun valid-runstyle-p (string) + (member string '("auto" "manual") :test 'equalp)) + +(defun valid-oneliner-name-p (string) + (or (equal string "") + (and (< 2 (length string)) + (ppcre:scan "^[a-zA-Z][a-zA-Z0-9_\-]+$" string)))) diff --git a/lib/package.lisp b/lib/package.lisp new file mode 100644 index 0000000..dd336dc --- /dev/null +++ b/lib/package.lisp @@ -0,0 +1,20 @@ + + +(defpackage #:oneliners.cli.running + (:use #:cl) + (:export #:run-with-shell)) + +(defpackage #:oneliners.cli.term + (:use #:cl) + (:export #:*term-width*)) + +(defpackage #:oneliners.cli.prompt + (:use #:cl) + (:local-nicknames (#:rl #:cl-readline)) + (:export #:prompt)) + +(defpackage #:oneliners.cli + (:use :cl) + (:import-from #:oneliners.cli.prompt #:prompt) + (:local-nicknames (#:api #:oneliners.api-client) + (#:a #:alexandria))) diff --git a/src/prompt.lisp b/lib/prompt.lisp index afe8604..6c847f1 100644 --- a/src/prompt.lisp +++ b/lib/prompt.lisp @@ -1,9 +1,6 @@ ;;;; prompt.lisp -- a function using readlline to collect text from the user -(defpackage #:oneliners.cli.prompt - (:use #:cl) - (:local-nicknames (#:rl #:cl-readline)) - (:export #:prompt)) + (in-package :oneliners.cli.prompt) diff --git a/lib/running.lisp b/lib/running.lisp new file mode 100644 index 0000000..5f417e4 --- /dev/null +++ b/lib/running.lisp @@ -0,0 +1,47 @@ +;;;; running.lisp -- functions for running oneliners + + + + +(in-package :oneliners.cli.running) + +(defmacro wait-until ((&key (timeout 1) (poll-every 0.01)) &body check) + "Run CHECK every POLL-EVERY seconds until either TIMEOUT seconds +have passed or CHECK returns non-nil." + (let ((clockvar (gensym)) + (var (gensym))) + `(loop + for ,clockvar from 0 by ,poll-every to ,timeout + for ,var = (progn ,@check) + when ,var + return ,var + do (sleep ,poll-every) + finally (return nil)))) + +(defun run-with-shell + (command + &key + shell-name + await-output-p + (output-stream *standard-output*)) + "run COMMAND, a string, in a fresh shell environment, initialized +with SHELL-NAME. The output from the command read line by line and is +printed to OUTPUT-STREAM. " + (let ((shell + (uiop:launch-program shell-name :input :stream :output :stream))) + (symbol-macrolet ((shell-input (uiop:process-info-input shell)) + (shell-output (uiop:process-info-output shell))) + (write-line command shell-input) + (finish-output shell-input) + (if (and await-output-p + (plusp await-output-p) + (wait-until (:timeout await-output-p :poll-every 0.005) + (listen shell-output))) + (loop while (listen shell-output) + do (princ (read-line shell-output) output-stream) + (terpri output-stream) + (sleep 0.005)))))) + + + + diff --git a/lib/state.lisp b/lib/state.lisp new file mode 100644 index 0000000..b98f5ba --- /dev/null +++ b/lib/state.lisp @@ -0,0 +1,62 @@ +;;;; state.lisp -- functions for dealing with client state + +(in-package :oneliners.cli) + +;;; Config Struct + +(defstruct config + handle + api-token + host + shell) + +(defvar *config* nil + "Holds a config struct instance.") + +(defvar *cache* nil + "Holds cached oneliners as a list.") + +;;; GETTING AND SETTING STATE, DYNAMICALLY BOUND + +(defun merge-oneliners (new) + "Modifies *CACHE*. Merge updated oneliners into the *cache*, ensuring to remove old versions." + (setf *cache* + (nconc + new + (delete-if + (lambda (old-oneliner) + (find (oneliner-id old-oneliner) + new + :key #'oneliner-id + :test #'equal)) + *cache*)))) + +(defun get-cached (id-or-name) + "Looks up a oneliner instance by ID-OR-NAME using the current binding of *cache*. " + (find id-or-name + *cache* + :key (etypecase id-or-name + (integer #'oneliner-id) + (string #'oneliner-name)) + :test #'equal)) + +;;; LOADING AND SAVING STATE + +(defun config-file () + "Returns the pahtname holding the location of the config file." + (merge-pathnames ".config/oneliners.config" (user-homedir-pathname))) + +(defun cached-oneliners-file () + "Returns the pathname holding the location of the cache." + (merge-pathnames ".cache/oneliners.cache" (user-homedir-pathname))) + +(defun wipe-cache () + "Deletes the cache, if present." + (uiop:delete-file-if-exists (cached-oneliners-file))) + +(defun write-config-to-disk () + (print-to-file *config* (config-file))) + +(defun write-cache-to-disk () + (print-to-file *cache* (cached-oneliners-file))) + diff --git a/lib/term.lisp b/lib/term.lisp new file mode 100644 index 0000000..c5b472a --- /dev/null +++ b/lib/term.lisp @@ -0,0 +1,15 @@ +;;;; term.lisp -- functions for dealing with the terminal + + + +(in-package :oneliners.cli.term) + +(defvar *term-width* nil) + +(defun set-term-width () + ;; tput cols b/c getenv COLUMNS wasnt working on some terminals + (setf *term-width* + (- + (or (parse-integer (uiop:run-program '("tput" "cols") :output :string) :junk-allowed t) + 80) + 4))) diff --git a/lib/util.lisp b/lib/util.lisp new file mode 100644 index 0000000..290d541 --- /dev/null +++ b/lib/util.lisp @@ -0,0 +1,37 @@ +;;;; util.lisp + +(in-package :oneliners.cli) + +(defun make-temp-file-name () + "Simply makes a file name for a temp file. Uses +UIOP:TEMPORARY-DIRECTORY for the directory." + (namestring + (merge-pathnames (format nil "~a~a" (gensym "oneliners") (get-universal-time)) + (uiop:temporary-directory)))) + + +(defun string-from-editor (&optional contents) + (let ((filename (make-temp-file-name))) + (when contents (a:write-string-into-file contents filename :if-exists :supersede)) + (unwind-protect + (magic-ed:magic-ed filename :eval nil :output :string) + (uiop:delete-file-if-exists filename)))) + + +(defun executable-on-system-p (name) + "A hack that heuristically determines whether or not an executable +with the provided name is on the system. It is not perfect. It +consults the environment PATH, and looks for the command in any of +the directories that appear in the value of that variable." + #+unix + (loop for path in (str:split ":" (uiop:getenv "PATH")) + for directory = (cons :absolute + (cdr (str:split "/" path))) + thereis (uiop:file-exists-p + (make-pathname :name name :directory directory)))) + +(defun print-to-file (printable-object pathname &optional (if-exists :supersede)) + "Prints an object to a file, ensuring that the containing directory exists first." + (ensure-directories-exist pathname) + (with-open-file (out pathname :direction :output :if-exists if-exists) + (print printable-object out))) diff --git a/oneliners.cli.asd b/oneliners.cli.asd index eee30ba..5b0f2e3 100644 --- a/oneliners.cli.asd +++ b/oneliners.cli.asd @@ -6,15 +6,18 @@ "str" "jonathan" "dexador" - "osicat" "net.didierverna.clon" "cl-readline" - "magic-ed" + "magic-ed" "oneliners.api-client") - :components ((:module "src" + :components ((:module "lib" :components - ((:file "prompt") - (:file "lib") + ((:file "package") + (:file "prompt") + (:file "cli"))) + (:module "app" + :components + ((:file "package") (:file "app")))) :description "") |