Run Shell Programs in Common Lisp the Lisp Way
I prefer to call the shell command curl like this:
(jkl-cmd:curl "google.com" :v t :jkl-error nil)rather than:
(sb-ext:run-program "curl" '("google.com" "-v") :search t :output *standard-output* :error nil)You will need the normal Common Lisp toolchain, such as SBCL/Quicklisp. The only thing to be aware of is that I am using the str:match macro, which is in a newer version than the Quicklisp version (2024-06-04). Therefore, you may need to download cl-str to the local-projects directory of Quicklisp.
Check the cmds folder to see the implemented commands.
To run curl to access Google:
(jkl-cmd:curl "www.google.com")Just like in the shell:
curl "www.google.com"I will use the cmds/curl command as an example again.
(jkl-cmd:curl :help t)is equivalent to:
curl --helpIn actuality, it equals curl --help t because curl's help accepts a value (category).
-h, --help <category> Get help for commands
So that's how the keyword works in jkl-cmd. The :help keyword translates to --help, and the keyword value, t, translates to t in the shell.
If I do:
(jkl-cmd:curl :help "all")it will show all the help options for curl.
I can use the short option keyword to do the same thing:
(jkl-cmd:curl :h1 "all")Wait a second, why is it h1 rather than h? Common Lisp's keywords in lambda lists are case-insensitive. This means that :h and :H are the same when I call (jkl-cmd:curl :h t). However, they are different in curl:
-H, --header <header/@file> Pass custom header(s) to server -h, --help <category> Get help for commands
So, when the command instance is generated, the second h/H will become :h1. This only applies to short options, of course. Before you use the command, you can check if the keyword is correct or not by using jkl:get-options and jkl:list-options.
For flag options, such as the version of curl (-V, --version), the option doesn't accept any value after it. However, we have to call it with t in jkl. For example:
(jkl-cmd:curl :v1 t)If you have your own command-line application and you want to call it inside your Common Lisp application through jkl, you can create your own command inside the cmds folder (within the jkl-cmd package).
If the options of the command-line app you made are similar to those of curl (e.g., -d, --data <data> HTTP POST data), for example, if you make a command-line app using clingon, you can use jkl-options:option4 and jkl:make-new-command to create the new command:
;; in cmds/my-app.lisp
(in-package :jkl-cmd)
(defparameter *app-help* "option example
-n, --id <INT> the id of quiz
-o, --output <VALUE> output file
")
(defparameter *app*
(make-new-command "app"
(loop for line in (read-line-content *app-help*)
for opt = (parse-option-from-help 'option4 line)
when opt
collect opt)))
;;; Run the app command with args
(defun app (&rest args)
(apply #'run *app* args))
;; For others
(export '(app *app*))Then you can run your app with the following syntax:
(jkl-cmd:app :n 123 :output "output.txt")If a command-line app has subcommands, we can add the :subcommand keyword when we call make-new-command.
(jkl:make-new-command
"top"
(mapcar (lambda (line) (jkl:parse-option-from-help 'jkl-options:option1 line))
'("--capath <dir> CA directory to verify peer against"
"-E, --cert <certificate[:password]> Client certificate file and password"))
:subcommand `(("a" ,(mapcar (lambda (line) (jkl:parse-option-from-help 'jkl-options:option1 line))
'("--capath <dir> CA directory to verify peer against"
"-E, --cert <certificate[:password]> Client certificate file and password"))
:subcommand (("b" ,(mapcar (lambda (line) (jkl:parse-option-from-help 'jkl-options:option2 line))
'("-r, --recursive specify recursive download")))))))The :subcommand option receives a list of make-new-command lambda lists. Then, when you call:
(top :e "cert" "a" :e "cert" "b" :r t)you are effectively calling top -E cert a -E cert b -r in the shell.
List All Options of a Command
Given the keyword sequence feature, you might need to check which is the correct keyword for an option.
;; lambda list of list-options ((comm command) &key short-option-start-with long-option-start-with with-key)
(jkl:list-options jkl-cmd:*curl* :with-key t :short-option-start-with "h")This will output:
(("H"
(T short-option: H
long-option: header
argument: header/@file
description: Pass custom header(s) to server
))
("HEADER"
(NIL short-option: H
long-option: header
argument: header/@file
description: Pass custom header(s) to server
))
("H1"
(T short-option: h
long-option: help
argument: category
description: Get help for commands
))
("HELP"
(NIL short-option: h
long-option: help
argument: category
description: Get help for commands
))
)jkl:list-options will list all options of jkl-cmd:*curl* whose short options start with "h". In curl, this means header and help. So the keywords for header are :h, :H, :HEADER, and :header. Keywords for curl's help are :h1, :H1, :HELP, and :help.
Check If Keywords Are Correct/Get Options by Keywords
;; get-options lambda list is ((comm command) &rest keys)
(jkl:get-options jkl-cmd:*curl* :v :h :h1 :help)This will output:
;; ((short-flag option)...)
((T short-option: v
long-option: verbose
argument:
description: Make the operation more talkative
)
(T short-option: H
long-option: header
argument: header/@file
description: Pass custom header(s) to server
)
(T short-option: h
long-option: help
argument: category
description: Get help for commands
)
(NIL short-option: h
long-option: help
argument: category
description: Get help for commands
))See How Options Look in Shell
jkl:gen-options will show how the arguments look in the shell. I use it for debugging.
(jkl:gen-options jkl-cmd:*curl* :v1 t :H "hello" :H1 "?")
;; Output: ("-V" "-H" "hello" "-h" "?")- Add more Common Lisp implementations.
Currently, I only use SBCL, so I have only implemented the SBCL run function (see the
runfunction incore.lisp). - Add more commands to the
cmdsfolder.