diff mbox series

[bug#41253,v4] guix repl: Add script execution.

Message ID m1a71gkbzx.fsf@khs-macbook.home
State Accepted
Headers show
Series [bug#41253,v4] guix repl: Add script execution. | expand

Checks

Context Check Description
cbaines/comparison success View comparision
cbaines/git branch success View Git branch
cbaines/applying patch success View Laminar job

Commit Message

Konrad Hinsen June 6, 2020, 5:18 a.m. UTC
* guix/scripts/repl.scm: Add filename options for script execution.
* doc/guix.texi (Invoking guix repl): Document it.
* tests/guix-repl.sh: Test it.
* Makefile.am: (SH_TESTS): Add it.
---
 Makefile.am           |  1 +
 doc/guix.texi         | 51 +++++++++++++++++++++-----
 guix/scripts/repl.scm | 82 ++++++++++++++++++++++++++++--------------
 tests/guix-repl.sh    | 84 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 184 insertions(+), 34 deletions(-)
 create mode 100644 tests/guix-repl.sh

Comments

Ludovic Courtès June 12, 2020, 3:58 p.m. UTC | #1
Hi, and sorry for the delay!

Konrad Hinsen <konrad.hinsen@fastmail.net> skribis:

> * guix/scripts/repl.scm: Add filename options for script execution.
> * doc/guix.texi (Invoking guix repl): Document it.
> * tests/guix-repl.sh: Test it.
> * Makefile.am: (SH_TESTS): Add it.

I have some comments regarding style and how to deal with multiple
scripts, but I think we’re pretty much there.

> +The general syntax is:
> +
> +@example
> +guix repl @var{options} @var{file} @var{args}

Should be: [@var{file} @var{args}@dots{}]
The square brackets show it’s optional.

> +When a @var{file} argument is provided, @var{file} is
> +executed as a Guile scripts:

“When one or more @var{file} argument is provided, each @var{file} is
executed as a Guile program:”

> +  (define script (reverse
> +                  (map cdr
> +                       (filter (lambda (opt)
> +                                 (eq? (car opt) 'script))
> +                               opts))))

To avoid car/cdr (info "(guix) Data Types and Pattern Matching"), I
suggest something along these lines:

  (define scripts  ;plural, no?
    (reverse
     (filter-map (match-lambda
                   (('script . script) script)
                   (_ #f))
                 opts)))

> +  (define script-file
> +    (let ((file (car script))
> +          (directory (getcwd)))
> +      (canonicalize-path
> +       (cond ((string-prefix? "/" file) file)
> +             (else (string-append directory "/" file))))))

I think we can just use file names as they arrive, without attempting to
canonicalize them or anything.

> +    (unless (null? script)
> +      ;; Run script
> +      (save-module-excursion
> +       (lambda ()
> +         (set-program-arguments (cons script-file (cdr script)))
> +         (set-user-module)
> +         (load script-file))))

=> (for-each load scripts)
    
> +cat > "$tmpfile"<<EOF
> +#!/usr/bin/env -S guix repl --

Rather:

  #!$(type -P env)

This will ensure it works even in Guix build environments.

Could you send an updated patch?

Thank you!

Ludo’.
Konrad Hinsen June 13, 2020, 4:39 p.m. UTC | #2
Hi Ludo,

Patch v5 is on its way! A few comments:

> Should be: [@var{file} @var{args}@dots{}]
> The square brackets show it’s optional.

OK.

>> +When a @var{file} argument is provided, @var{file} is
>> +executed as a Guile scripts:
>
> “When one or more @var{file} argument is provided, each @var{file} is
> executed as a Guile program:”

No, that's no longer true. Only one script can be run at a time, because

  guix repl script1.scm script2.scm

now means "run script1.scm with script2.scm as its argument". And therefore...

>   (define scripts  ;plural, no?

This is not a plural. But filter-map is indeed nicer!

>> +  (define script-file
>> +    (let ((file (car script))
>> +          (directory (getcwd)))
>> +      (canonicalize-path
>> +       (cond ((string-prefix? "/" file) file)
>> +             (else (string-append directory "/" file))))))
>
> I think we can just use file names as they arrive, without attempting to
> canonicalize them or anything.

That's what I thought (and tried) as well, at first. Problems:

 - It doesn't work when run via pre-inst-env with a non-absolute
   filename for the script. The script is looked up relative
   to the directory containing repl.scm.

 - The script filename is also the first item of (command-line)
   when called inside the script, and that's useful only it it's
   an absolute filename.

>> +cat > "$tmpfile"<<EOF
>> +#!/usr/bin/env -S guix repl --
>
> Rather:
>
>   #!$(type -P env)

I didn't know that was possible on a shebang line!

> Could you send an updated patch?

Done!

Cheers,
  Konrad
Ludovic Courtès June 13, 2020, 7:44 p.m. UTC | #3
Hi,

Konrad Hinsen <konrad.hinsen@fastmail.net> skribis:

>> “When one or more @var{file} argument is provided, each @var{file} is
>> executed as a Guile program:”
>
> No, that's no longer true. Only one script can be run at a time, because
>
>   guix repl script1.scm script2.scm
>
> now means "run script1.scm with script2.scm as its argument". And therefore...

Ah OK, sorry for the confusion.

>>> +  (define script-file
>>> +    (let ((file (car script))
>>> +          (directory (getcwd)))
>>> +      (canonicalize-path
>>> +       (cond ((string-prefix? "/" file) file)
>>> +             (else (string-append directory "/" file))))))
>>
>> I think we can just use file names as they arrive, without attempting to
>> canonicalize them or anything.
>
> That's what I thought (and tried) as well, at first. Problems:
>
>  - It doesn't work when run via pre-inst-env with a non-absolute
>    filename for the script. The script is looked up relative
>    to the directory containing repl.scm.

Oh right, that’s because we’re using ‘load’.  We should instead do:

  (load-in-vicinity "." file)

Alternatively, (primitive-load file), but in that case the script would
be systematically interpreted.

>  - The script filename is also the first item of (command-line)
>    when called inside the script, and that's useful only it it's
>    an absolute filename.

In what way is it useful?

>>> +cat > "$tmpfile"<<EOF
>>> +#!/usr/bin/env -S guix repl --
>>
>> Rather:
>>
>>   #!$(type -P env)
>
> I didn't know that was possible on a shebang line!

It’s not, but here it’s evaluated as part of the here-document
expansion.

Thanks!

Ludo’.
Konrad Hinsen June 14, 2020, 7:02 a.m. UTC | #4
Hi Ludo,

> Oh right, that’s because we’re using ‘load’.  We should instead do:
>
>   (load-in-vicinity "." file)

Nice! That makes it cleaner indeed, since...

>>  - The script filename is also the first item of (command-line)
>>    when called inside the script, and that's useful only it it's
>>    an absolute filename.
>
> In what way is it useful?

I though it was a POSIX requirement, but (1) I didn't find anything on
that topic and (2) plaing Guile just passes on what I type on the
command line, so I won't try to do "better".

v6 is on the way!

Cheers,
  Konrad
diff mbox series

Patch

diff --git a/Makefile.am b/Makefile.am
index 5b64386b53..859b6a4bc2 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -473,6 +473,7 @@  SH_TESTS =					\
   tests/guix-environment-container.sh		\
   tests/guix-graph.sh				\
   tests/guix-describe.sh			\
+  tests/guix-repl.sh     			\
   tests/guix-lint.sh
 
 TESTS = $(SCM_TESTS) $(SH_TESTS)
diff --git a/doc/guix.texi b/doc/guix.texi
index 056bf011f6..b95709d0c6 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -239,7 +239,7 @@  Programming Interface
 * Derivations::                 Low-level interface to package derivations.
 * The Store Monad::             Purely functional interface to the store.
 * G-Expressions::               Manipulating build expressions.
-* Invoking guix repl::          Fiddling with Guix interactively.
+* Invoking guix repl::          Programming Guix in Guile
 
 Defining Packages
 
@@ -5472,7 +5472,7 @@  package definitions.
 * Derivations::                 Low-level interface to package derivations.
 * The Store Monad::             Purely functional interface to the store.
 * G-Expressions::               Manipulating build expressions.
-* Invoking guix repl::          Fiddling with Guix interactively.
+* Invoking guix repl::          Programming Guix in Guile
 @end menu
 
 @node Package Modules
@@ -8246,12 +8246,47 @@  has an associated gexp compiler, such as a @code{<package>}.
 @node Invoking guix repl
 @section Invoking @command{guix repl}
 
-@cindex REPL, read-eval-print loop
-The @command{guix repl} command spawns a Guile @dfn{read-eval-print loop}
-(REPL) for interactive programming (@pxref{Using Guile Interactively,,, guile,
-GNU Guile Reference Manual}).  Compared to just launching the @command{guile}
+@cindex REPL, read-eval-print loop, script
+The @command{guix repl} command makes it easier to program Guix in Guile
+by launching a Guile @dfn{read-eval-print loop} (REPL) for interactive
+programming (@pxref{Using Guile Interactively,,, guile,
+GNU Guile Reference Manual}), or by running Guile scripts
+(@pxref{Running Guile Scripts,,, guile,
+GNU Guile Reference Manual}).
+Compared to just launching the @command{guile}
 command, @command{guix repl} guarantees that all the Guix modules and all its
-dependencies are available in the search path.  You can use it this way:
+dependencies are available in the search path.
+
+The general syntax is:
+
+@example
+guix repl @var{options} @var{file} @var{args}
+@end example
+
+When a @var{file} argument is provided, @var{file} is
+executed as a Guile scripts:
+
+@example
+guix repl my-script.scm
+@end example
+
+To pass arguments to the script, use @code{--} to prevent them from
+being interpreted as arguments to @command{guix repl} itself:
+
+@example
+guix repl -- my-script.scm --input=foo.txt
+@end example
+
+To make a script executable directly from the shell, using the guix
+executable that is on the user's search path, add the following two
+lines at the top of the script:
+
+@example
+@code{#!/usr/bin/env -S guix repl --}
+@code{!#}
+@end example
+
+Without a file name argument, a Guile REPL is started:
 
 @example
 $ guix repl
@@ -8300,7 +8335,7 @@  Add @var{directory} to the front of the package module search path
 (@pxref{Package Modules}).
 
 This allows users to define their own packages and make them visible to
-the command-line tool.
+the script or REPL.
 
 @item -q
 Inhibit loading of the @file{~/.guile} file.  By default, that
diff --git a/guix/scripts/repl.scm b/guix/scripts/repl.scm
index ff1f208894..4c2537b55d 100644
--- a/guix/scripts/repl.scm
+++ b/guix/scripts/repl.scm
@@ -1,6 +1,7 @@ 
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2020 Simon Tournier <zimon.toutoune@gmail.com>
+;;; Copyright © 2020 Konrad Hinsen <konrad.hinsen@fastmail.net>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -22,6 +23,7 @@ 
   #:use-module (guix scripts)
   #:use-module (guix repl)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-37)
   #:use-module (ice-9 match)
   #:use-module (rnrs bytevectors)
@@ -32,7 +34,8 @@ 
 
 ;;; Commentary:
 ;;;
-;;; This command provides a Guile REPL
+;;; This command provides a Guile script runner and REPL in an environment
+;;; that contains all the modules comprising Guix.
 
 (define %default-options
   `((type . guile)))
@@ -63,8 +66,9 @@ 
 
 
 (define (show-help)
-  (display (G_ "Usage: guix repl [OPTIONS...]
-Start a Guile REPL in the Guix execution environment.\n"))
+  (display (G_ "Usage: guix repl [OPTIONS...] [-- FILE ARGS...]
+In the Guix execution environment, run FILE as a Guile script with
+command-line arguments ARGS.  If no FILE is given, start a Guile REPL.\n"))
   (display (G_ "
   -t, --type=TYPE        start a REPL of the given TYPE"))
   (display (G_ "
@@ -135,12 +139,13 @@  call THUNK."
 
 (define (guix-repl . args)
   (define opts
-    ;; Return the list of package names.
     (args-fold* args %options
                 (lambda (opt name arg result)
                   (leave (G_ "~A: unrecognized option~%") name))
                 (lambda (arg result)
-                  (leave (G_ "~A: extraneous argument~%") arg))
+                  (append `((script . ,arg)
+                            (ignore-dot-guile . #t))
+                          result))
                 %default-options))
 
   (define user-config
@@ -148,28 +153,53 @@  call THUNK."
            (lambda (home)
              (string-append home "/.guile"))))
 
+  (define (set-user-module)
+    (set-current-module user-module)
+    (when (and (not (assoc-ref opts 'ignore-dot-guile?))
+               user-config
+               (file-exists? user-config))
+      (load user-config)))
+
+  (define script (reverse
+                  (map cdr
+                       (filter (lambda (opt)
+                                 (eq? (car opt) 'script))
+                               opts))))
+  (define script-file
+    (let ((file (car script))
+          (directory (getcwd)))
+      (canonicalize-path
+       (cond ((string-prefix? "/" file) file)
+             (else (string-append directory "/" file))))))
+
   (with-error-handling
-    (let ((type (assoc-ref opts 'type)))
-      (call-with-connection (assoc-ref opts 'listen)
-        (lambda ()
-          (case type
-            ((guile)
-             (save-module-excursion
-              (lambda ()
-                (set-current-module user-module)
-                (when (and (not (assoc-ref opts 'ignore-dot-guile?))
-                           user-config
-                           (file-exists? user-config))
-                  (load user-config))
-
-                ;; Do not exit repl on SIGINT.
-                ((@@ (ice-9 top-repl) call-with-sigint)
-                 (lambda ()
-                   (start-repl))))))
-            ((machine)
-             (machine-repl))
-            (else
-             (leave (G_ "~a: unknown type of REPL~%") type))))))))
+
+    (unless (null? script)
+      ;; Run script
+      (save-module-excursion
+       (lambda ()
+         (set-program-arguments (cons script-file (cdr script)))
+         (set-user-module)
+         (load script-file))))
+
+    (when (null? script)
+      ;; Start REPL
+      (let ((type (assoc-ref opts 'type)))
+        (call-with-connection (assoc-ref opts 'listen)
+          (lambda ()
+            (case type
+              ((guile)
+               (save-module-excursion
+                (lambda ()
+                  (set-user-module)
+                  ;; Do not exit repl on SIGINT.
+                  ((@@ (ice-9 top-repl) call-with-sigint)
+                   (lambda ()
+                     (start-repl))))))
+              ((machine)
+               (machine-repl))
+              (else
+               (leave (G_ "~a: unknown type of REPL~%") type)))))))))
 
 ;; Local Variables:
 ;; eval: (put 'call-with-connection 'scheme-indent-function 1)
diff --git a/tests/guix-repl.sh b/tests/guix-repl.sh
new file mode 100644
index 0000000000..2750188468
--- /dev/null
+++ b/tests/guix-repl.sh
@@ -0,0 +1,84 @@ 
+# GNU Guix --- Functional package management for GNU
+# Copyright © 2020 Simon Tournier <zimon.toutoune@gmail.com>
+# Copyright © 2020 Konrad Hinsen <konrad.hinsen@fastmail.net>
+#
+# This file is part of GNU Guix.
+#
+# GNU Guix is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or (at
+# your option) any later version.
+#
+# GNU Guix is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+#
+# Test the `guix repl' command-line utility.
+#
+
+guix repl --version
+
+test_directory="`mktemp -d`"
+export test_directory
+trap 'chmod -Rf +w "$test_directory"; rm -rf "$test_directory"' EXIT
+
+tmpfile="$test_directory/foo.scm"
+rm -f "$tmpfile"
+trap 'rm -f "$tmpfile"' EXIT
+
+module_dir="t-guix-repl-$$"
+mkdir "$module_dir"
+trap 'rm -rf "$module_dir"' EXIT
+
+
+cat > "$tmpfile"<<EOF
+(use-modules (guix packages)
+             (gnu packages base))
+
+(format #t "~a\n" (package-name coreutils))
+EOF
+
+test "`guix repl "$tmpfile"`" = "coreutils"
+
+
+cat > "$module_dir/foo.scm"<<EOF
+(define-module (foo)
+  #:use-module (guix packages)
+  #:use-module (gnu packages base))
+
+(define-public dummy
+  (package (inherit hello)
+    (name "dummy")
+    (version "42")
+    (synopsis "dummy package")
+    (description "dummy package. Only used for testing purposes.")))
+EOF
+
+cat > "$tmpfile"<<EOF
+(use-modules (guix packages)
+             (foo))
+
+(format #t "~a\n" (package-version dummy))
+EOF
+
+test "`guix repl "$tmpfile" -L "$module_dir"`" = "42"
+
+cat > "$tmpfile"<<EOF
+(format #t "~a\n" (cdr (command-line)))
+EOF
+
+test "`guix repl -- "$tmpfile" -a b --input=foo.txt`" = "(-a b --input=foo.txt)"
+
+cat > "$tmpfile"<<EOF
+#!/usr/bin/env -S guix repl --
+!#
+(format #t "~a\n" (cdr (command-line)))
+EOF
+chmod 755 $tmpfile
+
+test "`"$tmpfile" -a b --input=foo.txt`" = "(-a b --input=foo.txt)"