2018-10-31 19:34:48 +00:00
|
|
|
;;; Gash --- Guile As SHell
|
2018-07-16 07:41:10 +01:00
|
|
|
;;; Copyright © 2018 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
|
|
|
|
;;;
|
|
|
|
;;; This file is part of Gash.
|
|
|
|
;;;
|
|
|
|
;;; Gash 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.
|
|
|
|
;;;
|
|
|
|
;;; Gash 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 Gash. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
(define-module (gash script)
|
|
|
|
#:use-module (ice-9 ftw)
|
|
|
|
#:use-module (ice-9 getopt-long)
|
|
|
|
#:use-module (ice-9 local-eval)
|
|
|
|
#:use-module (ice-9 match)
|
|
|
|
#:use-module (ice-9 pretty-print)
|
2018-11-04 19:45:40 +00:00
|
|
|
#:use-module (ice-9 rdelim)
|
2018-07-16 07:41:10 +01:00
|
|
|
#:use-module (ice-9 receive)
|
|
|
|
#:use-module (ice-9 regex)
|
|
|
|
|
|
|
|
#:use-module (srfi srfi-1)
|
|
|
|
#:use-module (srfi srfi-26)
|
|
|
|
|
|
|
|
#:use-module (gash bournish-commands)
|
|
|
|
#:use-module (gash builtins)
|
|
|
|
#:use-module (gash config)
|
|
|
|
#:use-module (gash environment)
|
|
|
|
#:use-module (gash gash)
|
|
|
|
#:use-module (gash io)
|
|
|
|
#:use-module (gash job)
|
|
|
|
#:use-module (gash pipe)
|
|
|
|
#:use-module (gash util)
|
|
|
|
|
|
|
|
#:export (
|
2018-11-04 08:29:02 +00:00
|
|
|
and-terms
|
2018-07-16 07:41:10 +01:00
|
|
|
background
|
2018-11-04 19:45:40 +00:00
|
|
|
brace-group
|
2018-07-16 07:41:10 +01:00
|
|
|
builtin
|
|
|
|
command
|
2018-11-24 11:55:57 +00:00
|
|
|
delim
|
2018-07-16 07:41:10 +01:00
|
|
|
doublequotes
|
2018-11-04 19:45:40 +00:00
|
|
|
file-name
|
2018-11-14 20:09:21 +00:00
|
|
|
for-clause
|
|
|
|
do-group
|
|
|
|
expression
|
2018-07-16 07:41:10 +01:00
|
|
|
glob
|
|
|
|
ignore-error
|
2018-10-21 14:24:24 +01:00
|
|
|
literal
|
2018-11-04 08:29:02 +00:00
|
|
|
or-terms
|
2018-07-16 07:41:10 +01:00
|
|
|
pipeline
|
|
|
|
run
|
|
|
|
script-status
|
|
|
|
sequence
|
|
|
|
singlequotes
|
2018-11-17 18:23:51 +00:00
|
|
|
source
|
2018-07-16 07:41:10 +01:00
|
|
|
splice
|
|
|
|
split
|
|
|
|
substitution
|
|
|
|
word
|
|
|
|
xtrace
|
|
|
|
))
|
|
|
|
|
|
|
|
(define (background term)
|
|
|
|
(format (current-error-port) "background: ~s\n" term)
|
|
|
|
(match (pke 'background-term term)
|
|
|
|
(('pipeline command) (pke 'background: `(pipeline+ #f ,command)))
|
|
|
|
(_ term)))
|
|
|
|
|
2018-11-17 18:23:51 +00:00
|
|
|
(define (source file-name)
|
|
|
|
(let* ((string (with-input-from-file file-name read-string))
|
|
|
|
(ast (parse-string string)))
|
|
|
|
(run ast)))
|
|
|
|
|
2018-11-17 07:33:13 +00:00
|
|
|
(define (command . args)
|
2018-12-02 08:51:41 +00:00
|
|
|
(define (flatten o)
|
|
|
|
(match o
|
|
|
|
((h t ...) (append (flatten h) (append-map flatten t)))
|
|
|
|
(_ (list o))))
|
2018-07-16 07:41:10 +01:00
|
|
|
(define (exec command)
|
|
|
|
(cond ((procedure? command) command)
|
2018-11-04 19:45:40 +00:00
|
|
|
((assoc-ref %functions (car command))
|
|
|
|
=>
|
|
|
|
(lambda (function)
|
|
|
|
(parameterize ((%command-line args))
|
|
|
|
(last (apply function args)))))
|
2018-07-16 07:41:10 +01:00
|
|
|
((every string? command)
|
|
|
|
(let* ((program (car command))
|
|
|
|
(escape-builtin? (and (string? program) (string-prefix? "\\" program)))
|
|
|
|
(program (if escape-builtin? (string-drop program 1) program))
|
|
|
|
(command (cons program (cdr command))))
|
2018-10-30 06:09:29 +00:00
|
|
|
(or (builtin command #:prefer-builtin? (or %prefer-builtins?
|
|
|
|
escape-builtin?))
|
2018-10-31 19:22:13 +00:00
|
|
|
(lambda _ (status:exit-val (apply system* command))))))
|
2018-07-16 07:41:10 +01:00
|
|
|
(else (lambda () #t))))
|
2018-12-01 09:15:33 +00:00
|
|
|
(when (> %debug-level 1)
|
|
|
|
(format (current-error-port) "command: ~s\n" args))
|
2018-12-02 08:51:41 +00:00
|
|
|
(let ((args (flatten args)))
|
|
|
|
(match args
|
|
|
|
(((or "." "source") file-name)
|
|
|
|
(let* ((string (with-input-from-file file-name read-string))
|
|
|
|
(ast (parse-string string)))
|
|
|
|
(run ast)
|
|
|
|
0))
|
|
|
|
(((? string?) ..1) (exec (append-map glob args)))
|
|
|
|
(_ (exec (append-map glob args))))))
|
2018-07-16 07:41:10 +01:00
|
|
|
|
2018-11-05 21:37:55 +00:00
|
|
|
(define (glob? pattern)
|
|
|
|
(and (string? pattern) (string-match "\\?|\\*" pattern)))
|
|
|
|
|
|
|
|
(define* (glob->regex pattern #:key (begin "^") (end "$"))
|
2018-07-16 07:41:10 +01:00
|
|
|
(let* ((pattern (regexp-substitute/global #f "\\." pattern 'pre "\\." 'post))
|
|
|
|
(pattern (regexp-substitute/global #f "\\?" pattern 'pre "." 'post))
|
|
|
|
(pattern (regexp-substitute/global #f "\\*" pattern 'pre ".*" 'post)))
|
2018-11-05 21:37:55 +00:00
|
|
|
(make-regexp (string-append begin pattern end))))
|
|
|
|
|
|
|
|
(define (glob pattern)
|
2018-07-16 07:41:10 +01:00
|
|
|
(define (glob-match regex path) ;; pattern path -> bool
|
|
|
|
(regexp-match? (regexp-exec regex path)))
|
|
|
|
(define (glob- pattern file-names)
|
|
|
|
(map (lambda (file-name)
|
|
|
|
(if (string-prefix? "./" file-name) (string-drop file-name 2) file-name))
|
|
|
|
(append-map (lambda (file-name)
|
|
|
|
(map (cut string-append (if (string=? "/" file-name) "" file-name) "/" <>)
|
|
|
|
(filter (conjoin (negate (cut string-prefix? "." <>))
|
2018-11-05 21:37:55 +00:00
|
|
|
(cute glob-match (glob->regex pattern) <>))
|
2018-07-16 07:41:10 +01:00
|
|
|
(or (scandir file-name) '()))))
|
|
|
|
file-names)))
|
|
|
|
(cond
|
|
|
|
((not pattern) '(""))
|
|
|
|
((glob? pattern) (let ((absolute? (string-prefix? "/" pattern)))
|
|
|
|
(let loop ((patterns (filter (negate string-null?) (string-split pattern #\/)))
|
|
|
|
(file-names (if absolute? '("/") '("."))))
|
|
|
|
(if (null? patterns)
|
|
|
|
file-names
|
|
|
|
(begin
|
|
|
|
(loop (cdr patterns) (glob- (car patterns) file-names)))))))
|
|
|
|
(#t (list pattern))))
|
|
|
|
|
|
|
|
(define (singlequotes . o)
|
|
|
|
(string-join o ""))
|
|
|
|
|
|
|
|
(define (doublequotes . o)
|
|
|
|
(string-join (append-map glob o) ""))
|
|
|
|
|
|
|
|
(define (sequence . args)
|
|
|
|
(let ((glob (append-map glob (apply append args))))
|
|
|
|
glob))
|
|
|
|
|
2018-11-17 08:57:46 +00:00
|
|
|
(define (run script)
|
|
|
|
;; fixme: work towards simple eval -- must remove begin for now
|
|
|
|
(match script
|
|
|
|
(('begin script ...)
|
|
|
|
(last (map (cut local-eval <> (the-environment)) script)))
|
|
|
|
(_ (local-eval script (the-environment)))))
|
2018-07-16 07:41:10 +01:00
|
|
|
|
|
|
|
(define (script-status)
|
|
|
|
((compose string->number variable) "?"))
|
|
|
|
|
2018-11-14 20:09:21 +00:00
|
|
|
(define (for-clause name sequence body)
|
2018-07-16 07:41:10 +01:00
|
|
|
(for-each (lambda (value)
|
|
|
|
(assignment name value)
|
|
|
|
(body))
|
2018-11-14 20:09:21 +00:00
|
|
|
sequence))
|
2018-07-16 07:41:10 +01:00
|
|
|
|
|
|
|
(define (split o)
|
|
|
|
((compose string-tokenize string-trim-right) o))
|
|
|
|
|
|
|
|
(define (xtrace o)
|
|
|
|
(o))
|
|
|
|
|
2018-10-21 14:24:24 +01:00
|
|
|
(define (literal o)
|
|
|
|
o)
|
|
|
|
|
2018-07-16 07:41:10 +01:00
|
|
|
(define (word . o)
|
|
|
|
(define (flatten o)
|
|
|
|
(match o
|
|
|
|
((h t ...) (append (flatten h) (append-map flatten t)))
|
|
|
|
(_ (list o))))
|
2018-11-12 06:14:05 +00:00
|
|
|
(match o
|
|
|
|
(((? string?) ...) (string-join (flatten o) ""))
|
2018-12-01 09:40:20 +00:00
|
|
|
((((? string?) ...)) (flatten (car o)))
|
2018-11-12 06:14:05 +00:00
|
|
|
(_ o)))
|
2018-07-16 07:41:10 +01:00
|
|
|
|
|
|
|
(define-syntax-rule (substitution commands)
|
2018-11-04 19:45:40 +00:00
|
|
|
(string-trim-right (with-output-to-string (lambda _ commands))))
|
2018-07-16 07:41:10 +01:00
|
|
|
|
|
|
|
(define-syntax-rule (ignore-error o)
|
|
|
|
(let ((errexit (shell-opt? "errexit")))
|
|
|
|
(when errexit
|
|
|
|
(set-shell-opt! "errexit" #f))
|
|
|
|
(let ((r o))
|
|
|
|
(assignment "?" "0")
|
|
|
|
(when errexit
|
|
|
|
(set-shell-opt! " errexit" #t))
|
|
|
|
r)))
|
|
|
|
|
2018-11-17 08:57:46 +00:00
|
|
|
(define-syntax true?
|
2018-07-16 07:41:10 +01:00
|
|
|
(lambda (x)
|
|
|
|
(syntax-case x ()
|
2018-11-17 08:57:46 +00:00
|
|
|
((_ pipeline)
|
2018-07-16 07:41:10 +01:00
|
|
|
(with-syntax ((it (datum->syntax x 'it)))
|
2018-11-17 08:57:46 +00:00
|
|
|
#'(let ((it (ignore-error pipeline)))
|
|
|
|
(status->bool it)))))))
|
2018-11-14 18:59:18 +00:00
|
|
|
|
2018-11-17 08:57:46 +00:00
|
|
|
(define (status->bool o)
|
|
|
|
(match o
|
|
|
|
(#t #t)
|
|
|
|
((? number?) (zero? o))
|
|
|
|
(_ #f)))
|
2018-07-16 07:41:10 +01:00
|
|
|
|
2018-11-14 20:09:21 +00:00
|
|
|
(define-syntax expression
|
|
|
|
(lambda (x)
|
|
|
|
(syntax-case x ()
|
|
|
|
((_ (command word ...))
|
|
|
|
#'(list word ...)))))
|
|
|
|
|
|
|
|
(define-syntax do-group
|
|
|
|
(lambda (x)
|
|
|
|
(syntax-case x ()
|
|
|
|
((_ term ...)
|
|
|
|
#'(lambda _ term ...)))))
|
|
|
|
|
2018-11-04 08:29:02 +00:00
|
|
|
(define-syntax and-terms
|
|
|
|
(lambda (x)
|
|
|
|
(syntax-case x ()
|
|
|
|
((_ left right)
|
|
|
|
(with-syntax ((it (datum->syntax x 'it)))
|
|
|
|
#'(let ((it left))
|
|
|
|
(if (zero? it) right it)))))))
|
|
|
|
|
|
|
|
(define-syntax or-terms
|
|
|
|
(lambda (x)
|
|
|
|
(syntax-case x ()
|
|
|
|
((_ left right)
|
|
|
|
(with-syntax ((it (datum->syntax x 'it)))
|
|
|
|
#'(let ((it (ignore-error left)))
|
|
|
|
(if (zero? it) it right)))))))
|
|
|
|
|
2018-07-16 07:41:10 +01:00
|
|
|
(define (pipeline . commands)
|
|
|
|
(define (handle job)
|
|
|
|
(when (> %debug-level 1)
|
|
|
|
(format (current-error-port) "job=~s\n" job))
|
|
|
|
(let* ((stati (cond ((job? job) (map status:exit-val (job-status job)))
|
|
|
|
((boolean? job) (list (if job 0 1)))
|
|
|
|
((number? job) (list job))
|
|
|
|
(else (list 0))))
|
|
|
|
(foo (when (> %debug-level 1)
|
|
|
|
(format (current-error-port) "stati=~s\n" stati)))
|
|
|
|
(status (if (shell-opt? "pipefail") (or (find (negate zero?) stati) 0)
|
|
|
|
(car stati)))
|
|
|
|
(pipestatus (string-append
|
|
|
|
"("
|
|
|
|
(string-join
|
|
|
|
(map (lambda (s i)
|
|
|
|
(format #f "[~a]=\"~a\"" s i))
|
|
|
|
stati
|
|
|
|
(iota (length stati))))
|
|
|
|
")")))
|
|
|
|
(assignment "PIPESTATUS" pipestatus)
|
|
|
|
(assignment "?" (number->string status))
|
|
|
|
(when (and (not (zero? status))
|
|
|
|
(shell-opt? "errexit"))
|
2018-11-17 08:57:46 +00:00
|
|
|
(when (> %debug-level 0)
|
|
|
|
(format (current-error-port) "set -e: exiting\n"))
|
2018-07-16 07:41:10 +01:00
|
|
|
(exit status))
|
2018-11-17 08:57:46 +00:00
|
|
|
(status->bool status)))
|
2018-10-30 06:09:29 +00:00
|
|
|
(let ((commands (filter (lambda (x) (not (eq? x *unspecified*))) commands)))
|
|
|
|
(when (> %debug-level 1)
|
|
|
|
(format (current-error-port) "pijp: commands=~s\n" commands))
|
|
|
|
;; FIXME: after running a builtin, we still end up here with the builtin's result
|
|
|
|
;; that should probably not happen, however, cater for it here for now
|
|
|
|
(match commands
|
|
|
|
(((and (? boolean?) boolean))
|
|
|
|
(handle boolean))
|
|
|
|
(((and (? number?) number))
|
|
|
|
(handle number))
|
|
|
|
(((? unspecified?))
|
|
|
|
(handle #t))
|
|
|
|
(((? unspecified?) t ... #t)
|
|
|
|
#t)
|
|
|
|
(_ (handle (apply pipeline+ #t commands))))))
|
2018-07-16 07:41:10 +01:00
|
|
|
|
|
|
|
(define* (builtin ast #:key prefer-builtin?)
|
|
|
|
;; FIXME: distinguish between POSIX compliant builtins and
|
|
|
|
;; `best-effort'/`fallback'?
|
|
|
|
"Possibly modify command to use a builtin."
|
|
|
|
(when (> %debug-level 0)
|
|
|
|
(format (current-error-port) "builtin ast=~s\n" ast))
|
|
|
|
(receive (command args)
|
|
|
|
(match ast
|
|
|
|
(((and (? string?) command) args ...) (values command args))
|
|
|
|
(_ (values #f #f)))
|
|
|
|
(let ((program (and command
|
|
|
|
(cond ((string-prefix? "/" command)
|
|
|
|
(when (not (file-exists? command))
|
|
|
|
(format (current-error-port) "gash: ~a: no such file or directory\n" command))
|
|
|
|
command)
|
|
|
|
(else (PATH-search-path command))))))
|
|
|
|
;; FIXME: find some generic strerror/errno way: what about permissions and stuff?
|
|
|
|
;; after calling system* we're too late for that?
|
|
|
|
(when (> %debug-level 0)
|
|
|
|
(format (current-error-port) "command ~a => ~s ~s\n" (or program 'builtin) command args))
|
|
|
|
(cond ((and program (not prefer-builtin?))
|
|
|
|
(when (not program)
|
|
|
|
(format (current-error-port) "gash: ~a: command not found\n" command))
|
|
|
|
(when (not (access? program X_OK))
|
|
|
|
(format (current-error-port) "gash: ~a: permission denied\n" command))
|
|
|
|
#f)
|
|
|
|
((and command (or (assoc-ref %builtin-commands command)
|
2018-10-30 18:45:01 +00:00
|
|
|
(assoc-ref (%bournish-commands) command)))
|
2018-07-16 07:41:10 +01:00
|
|
|
=>
|
|
|
|
(lambda (command)
|
|
|
|
(if args
|
|
|
|
(apply command (map (cut local-eval <> (the-environment)) args))
|
|
|
|
(command))))
|
|
|
|
(else #f)))))
|
2018-11-04 19:45:40 +00:00
|
|
|
|
|
|
|
(define (brace-group . o)
|
|
|
|
o)
|
|
|
|
|
|
|
|
(define (file-name o)
|
|
|
|
o)
|
2018-11-05 21:37:55 +00:00
|
|
|
|
|
|
|
(define (regexp-exec-non-greedy regexp string)
|
|
|
|
(let ((max (string-length string)))
|
|
|
|
(let loop ((size 1))
|
|
|
|
(and (<= size max)
|
|
|
|
(or (regexp-exec regexp (substring string 0 size))
|
|
|
|
(loop (1+ size)))))))
|
|
|
|
|
|
|
|
(define (regexp-exec-non-greedy-reverse regexp string)
|
|
|
|
(let ((max (string-length string)))
|
|
|
|
(let loop ((start (1- max)))
|
|
|
|
(and (>= start 0)
|
|
|
|
(or (regexp-exec regexp (substring string start))
|
|
|
|
(loop (1- start)))))))
|
|
|
|
|
2018-11-15 18:57:10 +00:00
|
|
|
(define (variable-regex name sep pattern)
|
|
|
|
(match sep
|
|
|
|
("##" (variable-hash-hash name pattern))
|
|
|
|
("#" (variable-hash name pattern))
|
|
|
|
("%%" (variable-percent-percent name pattern))
|
|
|
|
("%" (variable-percent name pattern))
|
|
|
|
("/" (variable-replace name pattern))))
|
|
|
|
|
|
|
|
(define (variable-replace name pattern)
|
|
|
|
(let* ((value (variable name))
|
|
|
|
(at (string-index pattern #\/))
|
|
|
|
(regex (if at (substring pattern 0 at) pattern))
|
|
|
|
(subst (if at (substring pattern (1+ at)) "")))
|
|
|
|
(regexp-substitute/global #f regex value 'pre subst 'post)))
|
|
|
|
|
2018-11-05 21:37:55 +00:00
|
|
|
(define (variable-hash name pattern)
|
|
|
|
(let ((value (variable name))
|
|
|
|
(glob? (glob? pattern)))
|
|
|
|
(if glob? (let* ((regexp (glob->regex pattern #:end ""))
|
|
|
|
(match (regexp-exec-non-greedy regexp value)))
|
|
|
|
(if match (string-drop value (match:end match))
|
|
|
|
value))
|
|
|
|
(if (string-prefix? pattern value) (string-drop value (string-length pattern))
|
|
|
|
value))))
|
|
|
|
|
|
|
|
(define (variable-hash-hash name pattern)
|
|
|
|
(let ((value (variable name))
|
|
|
|
(glob? (glob? pattern)))
|
|
|
|
(if glob? (let* ((regexp (glob->regex pattern #:end ""))
|
|
|
|
(match (regexp-exec regexp value)))
|
|
|
|
(if match (string-drop value (match:end match))
|
|
|
|
value))
|
|
|
|
(if (string-prefix? pattern value) (string-drop value (string-length pattern))
|
|
|
|
value))))
|
|
|
|
|
|
|
|
(define (variable-percent name pattern)
|
|
|
|
(let ((value (variable name))
|
|
|
|
(glob? (glob? pattern)))
|
|
|
|
(if glob? (let* ((regexp (glob->regex pattern #:begin ""))
|
|
|
|
(match (regexp-exec-non-greedy-reverse regexp value)))
|
|
|
|
(if match (substring value 0 (- (string-length value) (match:end match)))
|
|
|
|
value))
|
|
|
|
(if (string-suffix? pattern value) (substring value 0 (string-length pattern))
|
|
|
|
value))))
|
|
|
|
|
|
|
|
(define (variable-percent-percent name pattern)
|
|
|
|
(let ((value (variable name))
|
|
|
|
(glob? (glob? pattern)))
|
|
|
|
(if glob? (let* ((regexp (glob->regex pattern #:begin ""))
|
|
|
|
(match (regexp-exec regexp value)))
|
|
|
|
(if match (substring value 0 (match:start match))
|
|
|
|
value))
|
|
|
|
(if (string-suffix? pattern value) (substring value 0 (string-length pattern))
|
|
|
|
value))))
|
2018-11-10 09:16:50 +00:00
|
|
|
|
2018-11-10 09:25:02 +00:00
|
|
|
(define (number o)
|
|
|
|
o)
|
|
|
|
|
2018-11-10 09:16:50 +00:00
|
|
|
(define (pat o)
|
|
|
|
o)
|
|
|
|
|
|
|
|
(define (str o)
|
|
|
|
o)
|
|
|
|
|
|
|
|
(define* (variable-slash name pattern #:optional (replace ""))
|
|
|
|
(let ((value (variable name))
|
|
|
|
(glob? (glob? pattern)))
|
|
|
|
(let ((match (if glob? (let ((regexp (glob->regex pattern #:begin "" #:end "")))
|
|
|
|
(regexp-exec regexp value))
|
|
|
|
(string-match pattern value))))
|
|
|
|
(if match (string-append
|
|
|
|
(substring value 0 (match:start match))
|
|
|
|
replace
|
|
|
|
(substring value (match:end match)))
|
|
|
|
value))))
|
2018-11-12 06:14:05 +00:00
|
|
|
|
|
|
|
(define (compound . o)
|
2018-11-14 20:09:21 +00:00
|
|
|
(match o
|
2018-11-12 06:14:05 +00:00
|
|
|
((h ... t) t)
|
|
|
|
(_ o)))
|
|
|
|
|
2018-12-01 09:40:20 +00:00
|
|
|
(define (delim o . rest)
|
|
|
|
(match rest
|
|
|
|
(() o)
|
|
|
|
(((? string?) ...) (string-append o (string-join rest "")))
|
|
|
|
((((? string?) ...)) (string-append o (string-join (car rest) "")))))
|
2018-11-12 06:14:05 +00:00
|
|
|
|
|
|
|
(define (name o)
|
|
|
|
o)
|
2018-11-14 18:50:43 +00:00
|
|
|
|
2018-11-15 18:57:10 +00:00
|
|
|
(define (regex-sep o)
|
|
|
|
o)
|
|
|
|
|
2018-11-25 07:46:10 +00:00
|
|
|
(define (shift . o)
|
|
|
|
(apply (shift-command) o))
|