Raw File
;;; bash.scm ---

;; Copyright (C) 2014 Dmitry Bogatov <Dmitry Bogatov <KAction@gnu.org>>

;; Author: Dmitry Bogatov <Dmitry Bogatov <KAction@gnu.org>>

;; This program 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.

;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

(define-module (gnu bash)
  #:export ((bash-eval . eval))
  #:replace ((e-if . if))
  #:replace ((e-when . when))
  #:replace ((e-unless . unless)))
(use-modules (system foreign))
(use-modules (system ffi))
(use-modules (ice-9 match))
(use-modules (ice-9 format))
(use-modules (ice-9 curried-definitions))

(define-syntax-rule (define-public-from-c-code name)
  (begin
    (define name (false-if-exception (@ (gnu bash internal) name)))
    (export name)))

(define-public-from-c-code array->alist)
(define-public-from-c-code bind-dynamic-variable)

;; Since interface, provided by (system ffi) contains too much details
;; of Bash internals, we use this macro to provide more clean
;; interface.
;;
;; `define-override` works absolutely same as define*, but defines in body
;; `this-proc` symbol, referring to previous value of procedure variable.
(define-syntax define-override
  (lambda (x)
    (syntax-case x ()
      ((_ (name arg ...) stmt stmt* ...)
       (with-syntax ((this-proc (datum->syntax x 'this-proc)))
	 #'(define name
	     (let ((this-proc name))
	       (export name)
	       (lambda* (arg ...) stmt stmt* ...))))))))

(define* (pointer-increment ptr #:optional (offset (sizeof '*)))
  (make-pointer (+ (pointer-address ptr) offset)))

;; Flags is implementation detail, it is not actually useful for end-user,
;; so I save code and maintenance not decoding them.
(define-ffi-struct alias: ((string: name) (string: value) (int: flags)))

;; This is a bit tricky because of terminology fuss.
;; Address is, well, pointer to pointer to struct alias.
;; Pointer is, pointer to struct alias. See diagram, where
;; arrow means pointer dereferencing.
;;
;;  +-----------------+     	+---------+	   +----------------------+
;;  | address := argv |  --*>   | pointer |  --*>  + name | value | flags + (struct alias)
;;  +-----------------+         +---------+ 	   +----------------------+
;;
;;  +-----------------+     	+---------+	   +----------------------+
;;  |     address     |  --*>   | pointer |  --*>  + name | value | flags + (struct alias)
;;  +-----------------+         +---------+ 	   +----------------------+
;;
;;  ...
;;
;;  +-----------------+     	+---------+
;;  |     address     |  --*>   |   NULL  |
;;  +-----------------+         +---------+
(define (pointer->argv<alias> argv)
  (if (null-pointer? argv)
      '()
    (let loop ((address argv) (acc '()))
      (let ((pointer (dereference-pointer address)))
	(if (null-pointer? pointer)
	    acc
	  (loop (pointer-increment address)
		(cons (pointer->alias pointer) acc)))))))

(define-ffi-object argv<alias>:
  #:decoder pointer->argv<alias>
  #:free c-free)

(define-ffi all-aliases (-> #:alloc argv<alias>:))
(define-override (all-aliases)
  (define (alias->pair a)
    (match a
      (($ <struct-alias> name value) (cons name value))))
  (map alias->pair (this-proc)))


(define-ffi add-alias ((#:const string-like: name) (#:const string-like: value) -> void:))
(define-ffi get-alias-value ((#:const string-like: name) -> ?string:))
(export add-alias get-alias-value)

(define-ffi remove-alias ((#:const string-like: name) -> int:))
(define-override (remove-alias name)
  (let ((successfully-removed? (not {(this-proc name) = -1})))
    successfully-removed?))

(define-ffi delete-all-aliases (-> void:))
(define-ffi alias-expand ((#:const string-like: name) -> #:alloc string:))
(export alias-expand delete-all-aliases)

(define-extern (int: last-command-exit-value))
(define-syntax $? (identifier-syntax (last-command-exit-value)))
(export $?)

(define-ffi-mask eval-flags:
  ((nonint    #x001)
   (interact  #x002)
   (nohist    #x004)
   (nofree    #x008)
   (resetline #x010)
   (parseonly #x020)
   (nolongjmp #x040)))

;; It is unsafe, since at least following is true:
;;
;;  * If errexit is set, it segfaults
;;
;; For safe version see bash-eval.
(define-ffi unsafe-evalstring ((#:const string: string) (#:const string: from) eval-flags: -> int:)
  #:symbol "evalstring")

;; Force `nofree` flag, or Bash will attempt to free what it should not.
(define-override (unsafe-evalstring string from flags)
  (this-proc string from (cons 'nofree flags)))

(define (unsafe-format/eval . args)
  (unsafe-evalstring (apply format #f args) "(gnu bash)" '(nohist)))

(define (enable-option-argument opt)
  (format #f "-o ~a" opt))
(define (disable-option-argument opt)
  (format #f "+o ~a" opt))

(define (make-restore-shellopts-thunk options)
  (let* ((shellopts-string (get-string-value "SHELLOPTS"))
	 (shellopts (string-split shellopts-string #\:))
	 (shellopts-symbols (map string->symbol shellopts)))
    (define (option->set-argument opt)
      (if (member opt shellopts-symbols)
	  (enable-option-argument opt)
	(disable-option-argument opt)))
    (lambda ()
      (define saved-$? (last-command-exit-value))
      (unsafe-format/eval "set ~{~a ~}" (map option->set-argument options))
      (set! (last-command-exit-value) saved-$?))))

(define ((make-enable-shellopts-thunk options))
  (unsafe-format/eval "set ~{-o ~a ~}" options))

(define ((make-disable-shellopts-thunk options))
  (unsafe-format/eval "set ~{+o ~a ~}" options))

(define (call-with-shellopts options thunk)
  (dynamic-wind
    (make-enable-shellopts-thunk options)
    thunk
    (make-restore-shellopts-thunk options)))

(define (call-without-shellopts options thunk)
  (dynamic-wind
    (make-disable-shellopts-thunk options)
    thunk
    (make-restore-shellopts-thunk options)))

(define-syntax-rule (with-shellopts (opt opt* ...) stmt stmt* ...)
  (call-with-shellopts '(opt opt* ...) (lambda () stmt stmt* ...)))

(define-syntax-rule (without-shellopts (opt opt* ...) stmt stmt* ...)
  (call-without-shellopts '(opt opt* ...) (lambda () stmt stmt* ...)))

(export call-with-shellopts call-without-shellopts)
(export with-shellopts without-shellopts)

;; Safe version of `unsafe-evalstring`.
(define (bash-eval str)
  (without-shellopts (errexit)
    (unsafe-evalstring str "(gnu bash)" '(nohist))))

(define-ffi-mask variable-flags:
  ((exported	#x0000001)	; export to environment
   (readonly	#x0000002)	; cannot change
   (array	#x0000004)	; value is an array
   (function	#x0000008)	; value is a function
   (integer	#x0000010)	; internal representation is int
   (local	#x0000020)	; variable is local to a function
   (assoc	#x0000040)	; variable is an associative array
   (trace	#x0000080)	; function is traced with DEBUG trap
   (uppercase	#x0000100)	; word converted to uppercase on assignment
   (lowercase	#x0000200)	; word converted to lowercase on assignment
   (capcase	#x0000400)	; word capitalized on assignment
   (nameref	#x0000800)	; word is a name reference
   (invisible	#x0001000)	; cannot see
   (nounset	#x0002000)	; cannot unset
   (noassign	#x0004000)	; assignment not allowed
   (imported	#x0008000)	; came from environment
   (special	#x0010000)	; requires special handling
   (nofree	#x0020000)	; do not free value on unset
   (tempvar	#x0100000)	; variable came from the temp environment
   (propagate	#x0200000)))	; propagate to previous scope

(define-ffi-struct variable:
  ((string: name)
   (?string: value)
   ;; This field isn't used, so why waste effort on decoding it?
   (*: exportstr)
   ;; These two fields are very delicate and worth several days of
   ;; segfault debugging with no output, so they are handled in C.
   (*: dynamic-value)
   (*: assign-func)
   (variable-flags: attributes)
   (int: context)))

(define-ffi get-string-value ((#:const string: var-name) -> ?string:))
(define-ffi find-variable ((#:const string: name) -> ?variable:))
(define-ffi bind-variable
  ((#:const string: name) (#:const string: value) (variable-flags: flags)
    -> variable:))
(define-ffi bind-global-variable
  ((#:const string: name) (#:const string: value) (variable-flags: flags)
   -> variable:))
(define-ffi quote-escapes ((#:const string: str) -> #:alloc string:))
(define (show x)
  (format #f "~a" x))
(define quote-show (compose quote-escapes show))

(define (args->eval-string args)
  (string-join (map quote-show args) " "))

(define-public exception-on-error (make-parameter #f))

(define (eval-args args)
  (define eval-str (args->eval-string args))
  (define result (zero? (bash-eval eval-str)))
  (when (and (not result) (exception-on-error))
    (error 'bash-command-error eval-str (last-command-exit-value)))
  result)

(define-syntax-rule (e-if test then else)
  (if (parameterize ((exception-on-error #f)) test) then else))
(define-syntax-rule (e-when test stmt stmt* ...)
  (when (parameterize ((exception-on-error #f)) test) stmt stmt* ...))

(define-syntax-rule (e-unless test stmt stmt* ...)
  (unless (parameterize ((exception-on-error #f)) test) stmt stmt* ...))

(define (capture-output args)
  (define eval-str (args->eval-string args))
  (bash-eval (format #f "SCM_OUTPUT=$(~a)" eval-str))
  (unless (zero? (last-command-exit-value))
    (error 'bash-command-error eval-str (last-command-exit-value)))
  (get-string-value "SCM_OUTPUT"))

(define ($-hash-reader _unused port)
  (define (skip-chars pred port)
    (define current-char (read-char port))
    (when (eof-object? current-char)
      (error 'read-error 'premature-EOF))
    (if (pred current-char)
	(skip-chars pred port)
      current-char))
  (define char-after (skip-chars char-whitespace? port))
  (case char-after
    ((#\?) '((@@ (gnu bash) last-command-exit-value)))
    (else
     (unread-char char-after port)
     (let ((sexp (read port)))
       (case char-after
	 ((#\") `((@@ (gnu bash) bash-eval) ,sexp))
	 ((#\[)  (list '(@@ (gnu bash) eval-args)
		       (list 'quasiquote sexp)))
	 ((#\()  (list '(@@ (gnu bash) capture-output)
		       (list 'quasiquote sexp)))
	 (else (list '(@@ (gnu bash) $) (list 'quote sexp))))))))

(read-hash-extend #\$ $-hash-reader)

(define ($-ref name)
  (get-string-value (show name)))

(define ($-set name value)
  (bind-variable (show name) (show value) '()))

(define-public $ (make-procedure-with-setter $-ref $-set))

(define-public bind-scm-function
  (let ()
    (define *funcs* (make-hash-table))
    (define special-varname 'SCM_FUNCTIONS_MAGIC_VARIABLE)
    (define (special-var-thunk)
      (define cur-func-symbol (string->symbol ($ 'FUNCNAME)))
      (define thunk (hashq-ref *funcs* cur-func-symbol))
      (catch #t
	(lambda ()
	  (define retval (thunk))
	  (if (integer? retval)
	      (format #f "~a" retval)
	    "0"))
	(lambda (key . args)
	  (print-exception (current-error-port) #f key args)
	  "41")))

    (bind-dynamic-variable special-varname special-var-thunk)
    ;; Problem is that in dynamic variable handler I have no access to
    ;; dollar variables. Quick and dirty solution is just save $@ into
    ;; some other array. Yeah, global variables are evil, but it saves
    ;; load of code to maintain and debug.
    (lambda (symbol thunk)
      (hashq-set! *funcs* symbol thunk)
      (unsafe-format/eval
       "function ~a {
            SCM_ARGS=($@)    ;
            local retval=$~a ;
            unset SCM_ARGS   ;
            return $retval   ;
       }"
       symbol special-varname))))

(export define-bash-function)
(define-syntax-rule (define-bash-function (fn-name arg ...) stmt stmt* ...)
  (begin
    (define (fn-name arg ...)
      stmt stmt* ...)
    (bind-scm-function
     'fn-name
     (lambda ()
       (define bash-args (map cdr (array->alist 'SCM_ARGS)))
       (match bash-args
	 ((arg ...) (fn-name arg ...))
	 (_ (scm-error
	     'wrong-number-of-args
	     (symbol->string 'fn-name)
	     "scm-function called from Bash with args ~A failed to match signature ~A"
	     (list bash-args '(arg ...))
	     #f)))))))
back to top