Skriptado en Komunlispo

Esperanto • English
Laste ĝisdatigita: la 23-an de februaro 2022

La lumo kiu fajras duoble brila, fajras duone longa.
—D-ro Eldon TYRELL, Blade Runner (1982)

common-lisp.net emblemo

Enhavotabelo

Enkonduko

Plenaj tutaj sistemoj kaj bibliotekoj estas ĉiam komfortaj areoj por komunlispaj uzantoj. Bedaŭrinde, de longe, ne ekzistis definitiva solvo en komunlispon uzi kiel skriptada lingvo. Skriptada lingvo, en ĉi tiu kunteksto, signifas pri iu, kiu similas anime al komandliniaj ŝeloj—tio estas, iu kiu estas uzita por sistemkomandojn administri kaj regi sur la apa nivelo. La signifo ankaŭ etendas al la aŭtomacioj de la rulo de taskoj kiuj aliamaniere faritaj unu post alia. En ĉi tiu artikolo, mallongan enkondukon pri kiel komunlispon uzi en la skriptada areo mi priparolos.

Unu el la plej oftaj demandoj kiun mi ricevas kiam mi diras, ke skriptadon mi volas fari per komunlispo, estas, kiel tion mi eĉ volas fari kaj tio maleblas. La respondo simplas: plian potencon kaj esprimplenecon mi volas havi. Maturan kaj nelimigitan lingvon mi volas havi. Lingvon kiu miajn ideojn povas esprimi en la malplej kiomo de froto mi volas havi.

Skripto estas nur tiel potenca kiel la lingvo kaj iloj povus permesi. Baŝo kaj siaj amikoj, ekzemple, estas bonegaj por ideojn esprimi, tiel longe kiel komandojn oni maŝinskribas ĉe la komandlinio mem. La konduton ene skripto ĝi imitas. Funkciojn oni povas difini por procedurojn fari, sed ili nur estas tiel. Funkcioj en Baŝo ne estas ie proksimaj al la funkcioj en lingvoj kiel komunlispo. Kiel interaga uzantŝelo, ĝi funkcias bone; escepte tio, ne.

Ekzistas aliaj skriptadaj solvoj en aliaj lingvoj. Haskelo, Pitono, Skimo, kaj Rubeno, kelkajn ilin nomante, ĝin havas. Tamen estas bona eblo de komunlispo, kiu estas malfacila por realigi aŭ ne ekzistas en aliaj aliroj: pro la skriptoj mem estas validaj komunlispaj programoj, la programojn mi povas ŝargi en la REPL kaj afablajn aferojn mi povas fari kun tio. Neniu venas proksima al la flekso, kiun komunlispo provizas interagante kun realtempaj kurantaj programoj.

En ĉi tiu mallonga gvidilo, alian belan aĵon pri komunlispa skriptado mi ankaŭ tuŝetas: multvokaj duumdosieroj. Multvoka duumdosiero estas sola rulebla dosiero kiu povas esti elreferencita per pluraj nomoj. Ĉiu nomo kongruas al specifa proceduro ene tiu sola duumdosiero. La beleco de ĉi tiu aliro, estas, ke anstataŭ multajn malsamajn programojn administri, nur unu dosieron oni administras, kaj la ĝustan proceduron ĝi disdonos kiu uzanto deziras. Ĉi tio similas al kiun Busybox faras. Komunlispe ĉi tio estas traktita de cl-launch.

Antaŭkondiĉoj

Skriptado en komunlispo funkcias super la lingvo, tio estas, en la formo de bibliotekoj kiuj la abstraktadojn disponigas por interagi kun la sistemo kaj la medio. [Utilities for Implementation- and OS- Portability (UIOP)](https://gitlab.common-lisp.net/asdf/asdf/tree/master/uiop) estas aro de abstraktadoj kiuj nin permesas por porteblan lispan kodon skribi. UIOP estas enkonstruita en ASDF3—kiu estas parto de la plejparto de komunlispaj realigoj—do ne ekzistas bezono por ĝin permane instali. inferior-shell helpas por la procezojn administri. cl-scripting helpas por pli da rego.

La programo cl-launch devas esti instalita ĉe la sistemo. Ĝi estos respondeca por la kreado de la multvoka duumdosiero mem. Por je cl-launch instali:

Per APT:

$ sudo apt-get install -y cl-launch

Per Nixpkgs:

$ nix-env -i cl-launch

Bazaferoj

Dosierindikoj

Komencante, novan projektan dosierujon ni kreu. La projekton ni kunmetu en $HOME/common-lisp.

$ mkdir -p ~/common-lisp/my-scripts

Ĉi tiu dosiero estas unu el la laŭnormaj dosierindikoj kiun ASDF rampigos por .asd-dosieroj. Estas inde por noti, ke ne gravas se $HOME/common-lisp estas kutima dosierujo aŭ simbolligilo al iu.

Difinoj

Tiam la dosieron my-scripts.asd ni kreu en tiu dosierujo. Por komenci, la jenan ĝi havos:

#-asdf3.1 (error "ASDF 3.1 or bust!")

(defsystem "my-scripts"
  :version "0.0.1"
  :description "CL scripts"
  :license "MIT"
  :author "Muno VAKELO"
  :class :package-inferred-system
  :depends-on ((:version "cl-scripting" "0.1")
               (:version "inferior-shell" "2.0.3.3")
               (:version "fare-utils" "1.0.0.5")
               "my-scripts/main"))

Kelke da funkcioj kiujn ni bezonas, estas en ASDF 3.1, pro tio la tutan sistemon ni devas kondiĉigi. La dependecojn ĉe cl-scripting ni deklaras, kiu kelkajn helpilojn provizas; kaj ĉe inferior-shell, kiu la aĵojn kiujn ni bezonas por la ŝelajn procezojn administri provizas.

Sekve, la dosieron main.lisp ni kreu en la sama dosierujo. La jenan ĝi havos:

(uiop:define-package :my-scripts/main
    (:use #:cl
          #:uiop
          #:cl-scripting
          #:inferior-shell
          #:fare-utils
          #:cl-launch/dispatch)
  (:export #:getuid
           #:symlink
           #:help
           #:main))

(in-package #:my-scripts/main)

(exporting-definitions
  (defun getuid ()
    #+sbcl (sb-posix:getuid)
    #+cmu (unix:unix-getuid)
    #+clisp (posix:uid)
    #+ecl (ext:getuid)
    #+ccl (ccl::getuid)
    #+allegro (excl.osi:getuid)
    #-(or sbcl cmu clisp ecl ccl allegro) (error "no getuid"))

 (defun symlink (src)
   (let ((binarch (resolve-absolute-location `(,(subpathname (user-homedir-pathname) "bin/")) :ensure-directory t)))
     (with-current-directory (binarch)
       (dolist (i (cl-launch/dispatch:all-entry-names))
         (run `(ln -sf ,src ,i)))))
   (success))

 (defun help ()
   (format! t "~A commands: ~{~A~^ ~}~%" (get-name) (all-entry-names))
   (success))

 (defun main (&rest args)
   (format t "main~%")))

(register-commands :my-scripts/main)

Ni komencu per je UIOP:DEFINE-PACKAGE uzi. Male al DEFPACKAGE, la necesan medion kiu estas amika al UIOP ĉi tio kreas. En la klaŭzo :USE, la helpilojn de aliaj bibliotekoj ni uzos. En la korpo de ĉi tiu dosiero, je EXPORTING-DEFINITIONS oni povas vidi. La limojn de tiuj, kiuj iĝos duumdosieroj aŭ ne, ĉi tiu markilo efektive markas. Ĝi estos uzita de REGISTER-COMMANDS poste.

Ĉi tie, kelkajn funkciojn ni difinas: SYMLINK respondecas pri la kreado de la simbolligiloj por la multvoka duumdosiero; kelkajn bazuzajn informojn HELP montras; kaj MAIN estas la enirejo de nia skripto. La duumdosiero troveblos en $HOME/bin/. Por la procedon pri la kunmetado de la skripto kaj simbolligiloj plifaciligi, la kunmetadajn instrukciojn ni metos en Makefile. La dosieron Makefile en la aktuala dosierujo kreu, tiam la jenan metu:

NAME=my-scripts
BINARY=$(HOME)/bin/$(NAME)
SCRIPT=$(PWD)/$(NAME)
CL=cl-launch

.PHONY: all $(NAME) clean

all: $(NAME)

$(NAME):
    @$(CL) --output $(NAME) --dump ! --lisp sbcl --quicklisp --system $(NAME) --dispatch-system $(NAME)/main

install: $(NAME)
    @ln -sf $(SCRIPT) $(BINARY)
    @$(SCRIPT) symlink $(NAME)

clean:
    @rm -f $(NAME)

En la celo $(NAME), je cl-launch ni voku kun la opcioj por la skripton kunmeti. En la celo install, la skripton ni alvoku kun la opcioj symlink $(NAME), por la simbolligilojn de la multvoka duumdosiero krei. Pro nur tri funkciojn ni difinis ene la korpo de EXPORTING-DEFINITIONS, nur tri simbolligilojn al my-scripts ĝi kreos. La eligan dosieron la opcio ‑‑output $(NAME) precizigas. La ‑‑dump ! signifas, ke bildon ĝi kreos, por pli rapidan startigon ŝalti. La opcio ‑‑lisp sbcl signifas, ke je SBCL ni volas uzi, por ĉi tiu skripto. La opcio ‑‑quicklisp signifas, ke je Quicklisp ni ŝargas kun la bildo. La sistemon, kiun ni kreas la opcio ‑‑system $(NAME) ŝargas. La enirejo de nia programo la opcio ‑‑dispatch-system $(NAME)/main precizigas.

Kunmetado

Ni nun pretas por la skripton kaj la simbolligilojn krei. Por tion fari, rulu:

$ mkdir -p ~/bin
$ make install

La multvokan duumdosieron—./my-scripts—ĉi tiu komando kunmetas kun la respondaj simbolligiloj. La dosieruja arbo de ~/bin devas aspekti jene:

$ tree ~/bin
/home/vakelo/bin
├── getuid -> my-scripts
├── help -> my-scripts
├── my-scripts -> /home/vakelo/common-lisp/my-scripts/my-scripts
└── symlink -> my-scripts

0 directories, 5 files

Por kontroli, ke ĝi efektive funkcias, rulu:

$ getuid

Sa la uzantnumeron ĝi montras, tiam ni povas daŭrigi.

Pli

Ni supozu, ke la baterian staton de la tekkomputilo oni volas scii de la komandlinio. Tion ni povas difini per kelkaj funkcioj. Je my-scripts.asd ni modifu per la aldonajn deklarojn enhavi:

#-asdf3.1 (error "ASDF 3.1 or bust!")

(defsystem "my-scripts"
  :version "0.0.1"
  :description "CL scripts"
  :license "MIT"
  :author "Muno VAKELO"
  :class :package-inferred-system
  :depends-on ((:version "cl-scripting" "0.1")
               (:version "inferior-shell" "2.0.3.3")
               (:version "fare-utils" "1.0.0.5")
               "my-scripts/main"
               "my-scripts/general"))

Tiam, la dosieron general.lisp ni plenigu per la jena enhavo:

(uiop:define-package #:scripts/general
    (:use #:cl
          #:fare-utils
          #:uiop
          #:cl-scripting
          #:inferior-shell
          #:cl-launch/dispatch
          #:optima
          #:optima.ppcre
          #:local-time)
  (:export #:battery
           #:screenshot))

(in-package #:scripts/general)

(defvar *screenshots-dir*
  (subpathname (user-homedir-pathname) "Desktop/"))

(defun battery-status ()
  (let ((base-dir "/sys/class/power_supply/*")
        (exclude-string "/AC/"))
    (with-output (s nil)
      (loop :for dir :in (remove-if #'(lambda (path)
                                        (search exclude-string (native-namestring path)))
                                    (directory* base-dir))
            :for battery = (first (last (pathname-directory dir)))
            :for capacity = (read-file-line (subpathname dir "capacity"))
            :for status = (read-file-line (subpathname dir "status"))
            :do (format s "~A: ~A% (~A)~%" battery capacity status)))))

(exporting-definitions
 (defun battery ()
   (format t "~A" (battery-status))
   (values))

 (defun screenshot (mode)
   (let* ((dir *screenshots-dir*)
          (file (format nil "~A.png" (format-timestring nil (now))))
          (dest (format nil "mv $f ~A" dir))
          (image (format nil "~A/~A" dir file)))
     (flet ((scrot (file dest &rest args)
              (run/i `(scrot ,@args ,file -e ,dest))))
       (match mode
              ((ppcre "(full|f)") (scrot file dest))
              ((ppcre "(region|r)") (scrot file dest '-s))
              (_ (err (format nil "invalid mode ~A~%" mode))))
       (run `("xclip" "-selection" "clipboard" "-t" "image/png" ,image))
       (success)))))

(register-commands :scripts/general)

En la difino de BATTERY, la liveraĵon de (BATTERY-STATUS) ĝi eligas en home amika maniero, tio estas, sen la duoblaj citiloj. Tiam nenian valoron la funkcio BATTERY do liveras. Ĉi tion ni devas fari tial, ke nur la eligon al la alvoko de BATTERY-STATUS ni bezonas.

Aliflanke, enrankopion per scrot la funkcio SCREENSHOT tenas tiam la absolutdosierindikon de la bildo ĝi disponebligas el la tondeja zono per xclip. La bibliotekojn local-time por la data signovico kaj biblioteko; kaj optima, por la ripetiĝa kongruado. Por la komandon screenshot ebligi, la duumdosierojn dependecojn instalu. La jenajn komandojn rulu ĉe APT- kaj Nix-sistemoj, respektive:

$ sudo apt-get install -y scrot xclip
$ nix-env -i scrot xclip

Uzantapojn lanĉi kaj administri facilas. Ni komencu per dependecon aldoni en my-scripts.asd:

#-asdf3.1 (error "ASDF 3.1 or bust!")

(defsystem "my-scripts"
  :version "0.0.1"
  :description "CL scripts"
  :license "MIT"
  :author "Lolu VAKELO"
  :class :package-inferred-system
  :depends-on ((:version "cl-scripting" "0.1")
               (:version "inferior-shell" "2.0.3.3")
               (:version "fare-utils" "1.0.0.5")
               "my-scripts/main"
               "my-scripts/general"
               "my-scripts/apps"))

Tiam je apps.lisp ni plenigu:

(uiop:define-package #:scripts/apps
    (:use #:cl
          #:fare-utils
          #:uiop
          #:inferior-shell
          #:cl-scripting
          #:cl-launch/dispatch)
  (:export #:chrome
           #:kill-chrome
           #:stop-chrome
           #:continue-chrome))

(in-package #:scripts/apps)

(exporting-definitions
 (defun chrome (&rest args)
   (run/i `(google-chrome-beta ,@args)))

 (defun kill-chrome (&rest args)
   (run `(killall ,@args chromium-browser chromium google-chrome chrome)
        :output :interactive :input :interactive :error-output nil :on-error nil)
   (success))

 (defun stop-chrome ()
   (kill-chrome "-STOP"))

 (defun continue-chrome ()
   (kill-chrome "-CONT")))

(register-commands :scripts/apps)

Tiam je my-scripts ni rekunmetu:

$ make install
my-scripts available commands: battery chrome continue-chrome getuid help kill-chrome main screenshot stop-chrome symlink

Hura!

Avertoj

Gravan aferon por teni en la kalkulo, estas, en la difinoj, komunlispajn ŝlosilvortojn oni ne povas uzi kiel la nomo de komando. Do ene EXPORTING-DEFINITIONS la jenan oni ne povas havi:

(exporting-definitions
  (defun t (&rest args)
    (run/i `(urxvt ,@args)`)))

Se tion oni faras kaj la dosieron oni provas kompili, la komunlispa realigo plendos pri nomo kiu estas jam uzata.

Finrimarkoj

Onidire ke komunlispo jam velkiĝis al obskuro; ke ĝin oni ne plu uzas; ke ĝi ne plu utilas. Ne, tio ne pravas. Nur ĉar iu ne plu estas diskutita en plimulta novaĵo, ne signifas, ke ĝi mortas aŭ ĝi ne plu plaĉas al homo. Komunlispo estas normhava lingvo, kaj garantion programo kiu konformiĝas al la normo havas—ĝis tia grado ke—ĝi ankoraŭ povas kuri en la estontecon. Por lingvan normon krei estas monumenta tasko—ĝi postulas, ke malsamaj, eble konfliktaj anaroj devas akordi al unu la alia. Ekzistas multaj diversaj realigoj de komunlispo, kaj la celojn ĉiu realigo strebas atingi, kiuj ne necesas kongruaj al la aliaj realigoj. Tio bonas tial, ke spacon ĝi kreas por realigistoj kaj desegnistoj, pri kiel labori ĉe la bazaj specifoj. Tiel longe kiel ili akordas al la normo, aferoj verdas.

La priresponda homo por komunlispon igi ebla kaj akceptebla estas François-René RIDEAU. Estis ĉi tiu blogo kiu min inspiris por la vivpovecon vidi de komunlispo kiel skriptada lingvo.

La paĝa rubando supre venis el common-lisp.net.