scripting: enable shebang line 🚀

This commit is contained in:
vindarel 2022-12-09 22:35:36 +01:00
parent 2804f115d0
commit baf2d2dd51
2 changed files with 93 additions and 4 deletions

View file

@ -49,7 +49,7 @@ Output:
```
$ ciel myscript.lisp you
$ ciel script.lisp you
=>
Hello you!
@ -73,6 +73,38 @@ ciel-user>
Access them with `(uiop:command-line-arguments)`.
## Executable file and shebang line
We can also make a CIEL file executable and run it directly:
```
$ chmod +x script.lisp
$ ./script.lisp
```
Add the following shebang at the beginning:
```sh
#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec /path/to/ciel `basename $0` "$@"
|#
(in-package :ciel-user)
;; lisp code follows.
```
How it works:
- it starts as a /bin/sh script
- all lines starting by `#` are shell comments
- the exec calls the `ciel` binary with this file name as first argument,
the rest of the file (lisp code) is not read by the shell.
- before LOAD-ing this Lisp file, we remove the #!/bin/sh shebang line.
- Lisp ignores comments between `#|` and `|#`.
---
Now, let us iron out the details ;)

View file

@ -1,20 +1,77 @@
(in-package :ciel)
(defun maybe-ignore-shebang (in)
"If this file starts with #!, delete the shebang line,
so we can LOAD the file.
Return: a stream (it is LOADable)."
;; thanks Roswell for the trick.
(let ((first-line (read-line in)))
(make-concatenated-stream
;; remove shebang:
(make-string-input-stream
(format nil "~a"
(if (str:starts-with-p "#!" first-line)
""
first-line)))
;; rest of the file:
in)))
(defun load-without-shebang (file)
"LOAD this file, but exclude the first line if it is a shebang line."
(with-open-file (file-stream file)
(load
(maybe-ignore-shebang file-stream))))
(defun has-shebang (file)
"Return T if the first line of this file is a shell shebang line (starts with #!)."
(with-open-file (s file)
(str:starts-with-p "#!" (read-line s))))
(defun main ()
"Run a lisp file as a script.
If no file is given or if it doesn't exist, run the top-level CIEL REPL.
If no argument is given or if the file doesn't exist, run the top-level CIEL
It should begin with:
The script should begin with:
(in-package :ciel-user)
We have two ways to run a CIEL script:
1) by calling the ciel binary with a file as argument:
$ ciel myscript.lisp
2) by using a shebang. It's a little bit convoluted:
#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec /path/to/ciel `basename $0` \"$@\"
(print \"hello CIEL!\")
How it works:
- it starts as a /bin/sh script
- all lines starting by # are shell comments
- the exec calls the ciel binary with this file name as first argument,
the rest of the file (lisp code) is not read by the shell.
- before LOAD-ing this Lisp file, we remove the #!/bin/sh shebang line.
- Lisp ignore comments between #| and |#
Exciting things to come!"
;; (format t "Hello main! ~S~&" uiop:*command-line-arguments*)
(let ((file (first (uiop:command-line-arguments))))
(cond
((and file
(uiop:file-exists-p file))
(load file))
(if (has-shebang file)
;; I'm a bit cautious about this function.
;; (mostly, small issues when testing at the REPL, should be fine though…)
(load-without-shebang file)
;; So the one with no risk:
(load file)))
(t
(when (and file (not (uiop:file-exists-p file)))
(format t "warn: file ~S does not exist.~&" file))
(sbcli::repl)))))