bournish.scm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016, 2017, 2020 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2016 Efraim Flashner <efraim@flashner.co.il>
  4. ;;; Copyright © 2017 Ricardo Wurmus <rekado@elephly.net>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (guix build bournish)
  21. #:use-module (system base language)
  22. #:use-module (system base compile)
  23. #:use-module (system repl command)
  24. #:use-module (system repl common)
  25. #:use-module (ice-9 rdelim)
  26. #:use-module (ice-9 match)
  27. #:use-module (ice-9 ftw)
  28. #:use-module (srfi srfi-1)
  29. #:use-module (srfi srfi-11)
  30. #:use-module (srfi srfi-26)
  31. #:export (%bournish-language))
  32. ;;; Commentary:
  33. ;;;
  34. ;;; This is a super minimal Bourne-like shell language for Guile. It is meant
  35. ;;; to be used at the REPL as a rescue shell. In a way, this is to Guile what
  36. ;;; eshell is to Emacs.
  37. ;;;
  38. ;;; Code:
  39. (define (expand-variable str)
  40. "Return STR or code to obtain the value of the environment variable STR
  41. refers to."
  42. ;; XXX: No support for "${VAR}".
  43. (if (string-prefix? "$" str)
  44. `(or (getenv ,(string-drop str 1)) "")
  45. str))
  46. (define* (display-tabulated lst
  47. #:key
  48. (terminal-width 80)
  49. (column-gap 2))
  50. "Display the list of string LST in as many columns as needed given
  51. TERMINAL-WIDTH. Use COLUMN-GAP spaces between two subsequent columns."
  52. (define len (length lst))
  53. (define column-width
  54. ;; The width of a column. Assume all the columns have the same width
  55. ;; (GNU ls is smarter than that.)
  56. (+ column-gap (reduce max 0 (map string-length lst))))
  57. (define columns
  58. (max 1
  59. (quotient terminal-width column-width)))
  60. (define pad
  61. (if (zero? (modulo len columns))
  62. 0
  63. columns))
  64. (define items-per-column
  65. (quotient (+ len pad) columns))
  66. (define items (list->vector lst))
  67. (let loop ((indexes (unfold (cut >= <> columns)
  68. (cut * <> items-per-column)
  69. 1+
  70. 0)))
  71. (unless (>= (first indexes) items-per-column)
  72. (for-each (lambda (index)
  73. (let ((item (if (< index len)
  74. (vector-ref items index)
  75. "")))
  76. (display (string-pad-right item column-width))))
  77. indexes)
  78. (newline)
  79. (loop (map 1+ indexes)))))
  80. (define-syntax define-command-runtime
  81. (syntax-rules ()
  82. "Define run-time support of a Bournish command. This macro ensures that
  83. the implementation is not subject to inlining, which would prevent compiled
  84. code from referring to it via '@@'."
  85. ((_ (command . args) body ...)
  86. (define-command-runtime command (lambda args body ...)))
  87. ((_ command exp)
  88. (begin
  89. (define command exp)
  90. ;; Prevent inlining of COMMAND.
  91. (set! command command)))))
  92. (define-command-runtime ls-command-implementation
  93. ;; Run-time support procedure.
  94. (case-lambda
  95. (()
  96. (display-tabulated (scandir ".")))
  97. (files
  98. (let ((files (append-map (lambda (file)
  99. (catch 'system-error
  100. (lambda ()
  101. (match (stat:type (lstat file))
  102. ('directory
  103. ;; Like GNU ls, list the contents of
  104. ;; FILE rather than FILE itself.
  105. (match (scandir file
  106. (match-lambda
  107. ((or "." "..") #f)
  108. (_ #t)))
  109. (#f
  110. (list file))
  111. ((files ...)
  112. (map (cut string-append file "/" <>)
  113. files))))
  114. (_
  115. (list file))))
  116. (lambda args
  117. (let ((errno (system-error-errno args)))
  118. (format (current-error-port) "~a: ~a~%"
  119. file (strerror errno))
  120. '()))))
  121. files)))
  122. (display-tabulated files)))))
  123. (define (ls-command . files)
  124. `((@@ (guix build bournish) ls-command-implementation) ,@files))
  125. (define (which-command program)
  126. `(search-path ((@@ (guix build bournish) executable-path))
  127. ,program))
  128. (define (cat-command file)
  129. `(call-with-input-file ,file
  130. (lambda (port)
  131. ((@ (guix build utils) dump-port) port (current-output-port))
  132. *unspecified*)))
  133. (define (rm-command . args)
  134. "Emit code for the 'rm' command."
  135. (cond ((member "-r" args)
  136. `(for-each (@ (guix build utils) delete-file-recursively)
  137. (list ,@(delete "-r" args))))
  138. (else
  139. `(for-each delete-file (list ,@args)))))
  140. (define (lines+chars port)
  141. "Return the number of lines and number of chars read from PORT."
  142. (let loop ((lines 0) (chars 0))
  143. (match (read-char port)
  144. ((? eof-object?) ;done!
  145. (values lines chars))
  146. (#\newline ;recurse
  147. (loop (1+ lines) (1+ chars)))
  148. (_ ;recurse
  149. (loop lines (1+ chars))))))
  150. (define (file-exists?* file)
  151. "Like 'file-exists?' but emits a warning if FILE is not accessible."
  152. (catch 'system-error
  153. (lambda ()
  154. (stat file))
  155. (lambda args
  156. (let ((errno (system-error-errno args)))
  157. (format (current-error-port) "~a: ~a~%"
  158. file (strerror errno))
  159. #f))))
  160. (define (wc-print file)
  161. (let-values (((lines chars)
  162. (call-with-input-file file lines+chars)))
  163. (format #t "~a ~a ~a~%" lines chars file)))
  164. (define (wc-l-print file)
  165. (let-values (((lines chars)
  166. (call-with-input-file file lines+chars)))
  167. (format #t "~a ~a~%" lines file)))
  168. (define (wc-c-print file)
  169. (let-values (((lines chars)
  170. (call-with-input-file file lines+chars)))
  171. (format #t "~a ~a~%" chars file)))
  172. (define-command-runtime (wc-command-implementation . files)
  173. (for-each wc-print (filter file-exists?* files)))
  174. (define-command-runtime (wc-l-command-implementation . files)
  175. (for-each wc-l-print (filter file-exists?* files)))
  176. (define-command-runtime (wc-c-command-implementation . files)
  177. (for-each wc-c-print (filter file-exists?* files)))
  178. (define (wc-command . args)
  179. "Emit code for the 'wc' command."
  180. (cond ((member "-l" args)
  181. `((@@ (guix build bournish) wc-l-command-implementation)
  182. ,@(delete "-l" args)))
  183. ((member "-c" args)
  184. `((@@ (guix build bournish) wc-c-command-implementation)
  185. ,@(delete "-c" args)))
  186. (else
  187. `((@@ (guix build bournish) wc-command-implementation) ,@args))))
  188. (define (reboot-command . args)
  189. "Emit code for 'reboot'."
  190. ;; Normally Bournish is used in the initrd, where 'reboot' is provided
  191. ;; directly by (guile-user). In other cases, just bail out.
  192. `(if (defined? 'reboot)
  193. (reboot)
  194. (begin
  195. (format (current-error-port)
  196. "I don't know how to reboot, sorry about that!~%")
  197. #f)))
  198. (define (help-command . _)
  199. (display "\
  200. Hello, this is Bournish, a minimal Bourne-like shell in Guile!
  201. The shell is good enough to navigate the file system and run commands but not
  202. much beyond that. It is meant to be used as a rescue shell in the initial RAM
  203. disk and is probably not very useful apart from that. It has a few built-in
  204. commands such as 'ls' and 'cd'; it lacks globbing, pipes---everything.\n"))
  205. (define %not-colon (char-set-complement (char-set #\:)))
  206. (define (executable-path)
  207. "Return the search path for programs as a list."
  208. (match (getenv "PATH")
  209. (#f '())
  210. (str (string-tokenize str %not-colon))))
  211. (define %commands
  212. ;; Built-in commands.
  213. `(("echo" ,(lambda strings `(list ,@strings)))
  214. ("cd" ,(lambda (dir) `(chdir ,dir)))
  215. ("pwd" ,(lambda () `(getcwd)))
  216. ("rm" ,rm-command)
  217. ("cp" ,(lambda (source dest) `(copy-file ,source ,dest)))
  218. ("help" ,help-command)
  219. ("ls" ,ls-command)
  220. ("which" ,which-command)
  221. ("cat" ,cat-command)
  222. ("wc" ,wc-command)
  223. ("reboot" ,reboot-command)))
  224. (define (read-bournish port env)
  225. "Read a Bournish expression from PORT, and return the corresponding Scheme
  226. code as an sexp."
  227. (match (read-line port)
  228. ((? eof-object? eof)
  229. eof)
  230. ((= string-tokenize (command args ...))
  231. (match (assoc command %commands)
  232. ((command proc) ;built-in command
  233. (apply proc (map expand-variable args)))
  234. (#f
  235. (let ((command (if (string-prefix? "\\" command)
  236. (string-drop command 1)
  237. command)))
  238. `(system* ,command ,@(map expand-variable args))))))))
  239. (define %bournish-language
  240. (let ((scheme (lookup-language 'scheme)))
  241. ;; XXX: The 'scheme' language lacks a "joiner", so we add one here. This
  242. ;; allows us to have 'read-bournish' read one shell statement at a time
  243. ;; instead of having to read until EOF.
  244. (set! (language-joiner scheme)
  245. (lambda (exps env)
  246. (match exps
  247. (() '(begin))
  248. ((exp) exp)
  249. (_ `(begin ,@exps)))))
  250. (make-language #:name 'bournish
  251. #:title "Bournish"
  252. ;; The reader does all the heavy lifting.
  253. #:reader read-bournish
  254. #:compilers `((scheme . ,(lambda (exp env options)
  255. (values exp env env))))
  256. #:decompilers '()
  257. #:evaluator (language-evaluator scheme)
  258. #:printer (language-printer scheme)
  259. #:make-default-environment
  260. (language-make-default-environment scheme))))
  261. ;; XXX: ",L bournish" won't work unless we call our module (language bournish
  262. ;; spec), which is kinda annoying, so provide another meta-command.
  263. (define-meta-command ((bournish guix) repl)
  264. "bournish
  265. Switch to the Bournish language."
  266. (let ((current (repl-language repl)))
  267. (format #t "Welcome to ~a, a minimal Bourne-like shell!~%To switch back, type `,L ~a'.\n"
  268. (language-title %bournish-language)
  269. (language-name current))
  270. (current-language %bournish-language)
  271. (set! (repl-language repl) %bournish-language)))
  272. ;;; bournish.scm ends here