Merge branch 'master' of rdmp:/mnt/web-sites/dmbcs/guile into rdmp

This commit is contained in:
Dale Mellor 2020-05-18 11:44:40 +01:00
commit 92a77da732
12 changed files with 1724 additions and 221 deletions

View file

@ -90,6 +90,7 @@ guile_TEXINFOS = preface.texi \
libguile-extensions.texi \
api-init.texi \
mod-getopt-long.texi \
mod-command-line-processor.texi \
statprof.texi \
sxml.texi \
texinfo.texi \

View file

@ -357,7 +357,7 @@ available through both Scheme and C interfaces.
* SLIB:: Using the SLIB Scheme library.
* POSIX:: POSIX system calls and networking.
* Web:: HTTP, the web, and all that.
* getopt-long:: Command line handling.
* Command Line Processor:: Command line handling.
* SRFI Support:: Support for various SRFIs.
* R6RS Support:: Modules defined by the R6RS.
* R7RS Support:: Modules defined by the R7RS.
@ -381,7 +381,7 @@ available through both Scheme and C interfaces.
@include slib.texi
@include posix.texi
@include web.texi
@include mod-getopt-long.texi
@include mod-command-line-processor.texi
@include srfi-modules.texi
@include r6rs.texi
@include r7rs.texi

View file

@ -0,0 +1,240 @@
@c -*-texinfo-*-
@c This is part of the GNU Guile Reference Manual.
@c Copyright (C) 2020
@c Free Software Foundation, Inc.
@c See the file guile.texi for copying conditions.
@node Command Line Processor, SRFI Support, Web, Guile Modules
@section The (ice-9 command-line-processor) Module
As its name implies, the @code{(ice-9 command-line-processor)} facility
is supposed to be a one-stop shop for dealing with the command line. It
is inspired by the GNU libc's @code{argp} parser, and can be regarded as
a high-level wrapper around the @xref{getopt-long} module. It is
designed to provide two specific features.
@itemize @bullet
@item
Higher-level (easier to use) abstraction of the command-line user
interface to this application, including available options and program
meta-data.
@item
Automatic handling of @code{--help}, @code{--version} and @code{--usage}
flags. This means meeting GNU coding standards, and helping to
regularize the output from these commands.
@end itemize
The module provides a single syntax extension to the guile language:
@code{process-command-line}.
@menu
* Command Line Examples:: Examples of use.
* Command Line Reference:: Detailed specification of the procedure.
@end menu
Also see @xref{Command Line Format} for precise details of allowed
command-line formats.
@node Command Line Examples, Command Line Reference, Command Line Processor, Command Line Processor
@subsection A Simple Example
A (silly) program which takes two options, the second of which may
provide a numerical value, might include the following lines.
@lisp
(use-modules (ice-9 command-line-processor))
(process-command-line (command-line)
application "my-app"
option (--option -o "the first option")
option (--test=3 -t "another option" string->number))
(when --option (do-something))
(when --test (display --test) (newline))
@end lisp
@noindent
and then the program could be called with command lines like
@example
$ ./my-app -o
@end example
@noindent
or
@example
$ ./my-app --option -t 4 file-1 file-2
@end example
@subsection GNU Mcron
For realistic code, here is the first line of executable code GNU's
@code{mcron} program has (the @code{%} tokens are filled in by the build
system).
@lisp
(process-command-line (command-line)
application "mcron"
version "%VERSION%"
usage "[OPTIONS ...] [FILES ...]"
help-preamble
"Run unattended jobs according to instructions in the FILES... "
"(- for standard input), or use all the files in ~/.config/cron "
"(or the deprecated ~/.cron) with .guile or .vixie extensions.\n"
"Note that --daemon and --schedule are mutually exclusive."
option (--daemon -d "run as a daemon process")
option (--schedule=8 -s string->number
"display the next N (or 8) jobs that will be run,"
"and then exit")
option (--stdin=guile short-i (λ (in) (or (string=? in "guile")
(string=? in "vixie")))
"format of data passed as standard input or file "
"arguments, 'guile' or 'vixie' (default guile)")
help-postamble
"Mandatory or optional arguments to long options are also mandatory or "
"optional for any corresponding short options."
bug-address "%PACKAGE_BUGREPORT%"
copyright "2003, 2006, 2014, 2020 Free Software Foundation, Inc."
license GPLv3)
@end lisp
@noindent
after which there are four new variable bindings in the present
namespace: literally, @code{--daemon}, @code{--stdin}, @code{--schedule}
and @code{--!} (the latter holds all the command-line arguments that did
not partake in option processing) whose values depend on the specific
command-line options the end user furnished... except that if the user
had typed
@example
$ mcron --help
@end example
@noindent
they would be greeted with
@example
Usage: mcron [OPTIONS ...] [FILES ...]
Run unattended jobs according to instructions in the FILES...
(`-' for standard input), or use all the files in ~/.config/cron
(or the deprecated ~/.cron) with .guile or .vixie extensions.
Note that --daemon and --schedule are mutually exclusive.
-d, --daemon run as a daemon process
-s[N], --schedule[=N] display the next N (or 8) jobs that will be run,
and then exit
-i[N], --stdin[=N] format of data passed as standard input or file
arguments, 'guile' or 'vixie' (default guile)
-h, --help display this help and exit
-V, --version output version information and exit
-u, --usage show brief usage summary
Mandatory or optional arguments to long options are also mandatory or
optional for any corresponding short options.
Send bug reports to bug-mcron@@gnu.org.
@end example
@noindent
and the program would immediately have exited.
@node Command Line Reference, , Command Line Examples, Command Line Processor
@subsection process-command-line
@deffn {Scheme Procedure} process-command-line COMMAND-LINE SPECIFICATION
Process the @var{COMMAND-LINE} according to the application
@var{SPECIFICATION}.
@var{COMMAND-LINE} is a list of strings, such as that returned from the
core @code{(command-line)} function: the first string is the name of the
command being run, and the rest are the space-separated tokens that
followed the command on the command line.
@var{SPECIFICATION} is a form holding a space-separated mix of selection
words followed by their respective declarations. The selection words
are @code{application}, @code{author}, @code{bug-address},
@code{copyright}, @code{help-preamble}, @code{help-postamble},
@code{license}, @code{option}, @code{usage} and @code{version}, and can
appear in any order.
@table @asis
@item @code{application}
should be followed by a string: the name of the application with
possibly the package name in parentheses afterwards. This may appear
zero or one times, but ideally should be present.
@item @code{author}
should be followed by a string giving the name of one of the packageʼs
authors. This selection word can be repeated as many times as necessary
to provide the names of all authors.
@item @code{bug-address}
should be followed by a string giving the URL of a contact-point for
sending bug reports, such as an e-mail address or web address of
bug-tracking system interface. This can appear zero or one times.
@item @code{copyright}
should be followed by a string containing a list of years and an entity
to whom the copyright is assigned. This may be repeated to list other
assignees.
@item @code{help-preamble}
should be followed by a number of strings which make up a short
paragraph of text displayed before a full list of the available program
options.
@item @code{help-postamble}
like the preamble, this is followed by strings which make up a paragraph
of text, shown after the list of options.
@item @code{license}
can be followed by one of the words GPLv3 [this is currently the only
standard choice implemented], or else a string which briefly gives out
the terms of the license. Can appear zero or one times.
@item @code{option}
is followed by an option declaration, described below. You can specify
any number of options.
@item @code{usage}
is followed by a string describing the usage of the application on one
line. This can appear zero or one times, but ideally should be present.
@item @code{version}
is followed by a string providing the current version number of this
program. This item may appear zero or one times.
@end table
The option declaration is followed by another form bracketed by
parentheses and holding a space-separated mix of declarations (order
irrelevant).
@itemize @bullet
@item
A word beginning with two hyphens, an optional exclamation point,
alphabetic letters (intermixed with digits, underscore and hyphens), an
optional equals sign, and an optional further word. There must be
exactly one of these, and that determines the long name of the option.
An exclamation point indicates that the option MUST appear on the
command line, an equals indicates that the option MUST have a value
unless it is followed in the specification by a value, in which case the
value on the command-line is optional and the one in the specification
will be taken as the default when not given on the command line.
@item
A word comprised of one hyphen and one letter. There can be exactly
zero or one of these, and it declares that the option has this short
form available on the command-line. As a very special exception: if you
want to use @code{-i} as an option, it must be specified with the
identifier @code{short-i} (a naked @emph{-i} is read as a complex
number); ditto @code{short-I} for @code{-I}.
@item
A number of strings which are catenated together to provide a short,
succinct description of the option. These strings should be
approximately half the width of a page, i.e. about 40 characters.
@item
A function which will be used as a predicate to decide if a value is
allowable for this option. There should be zero or one of these.
@end itemize
For the precise presentation of options on the command-line, the reader
should refer to the @xref{Command Line Format}, part of the description
of the @xref{getopt-long} module, which underlies the present one.
@end deffn
@include mod-getopt-long.texi

View file

@ -4,7 +4,7 @@
@c Free Software Foundation, Inc.
@c See the file guile.texi for copying conditions.
@node getopt-long
@node getopt-long, SRFI Support, Command Line Processor
@section The (ice-9 getopt-long) Module
The @code{(ice-9 getopt-long)} facility is designed to help parse

View file

@ -4,7 +4,7 @@
@c Free Software Foundation, Inc.
@c See the file guile.texi for copying conditions.
@node SRFI Support
@node SRFI Support, R6RS Support, Command Line Processor, Guile Modules
@section SRFI Support Modules
@cindex SRFI

View file

@ -115,6 +115,7 @@ SOURCES = \
ice-9/futures.scm \
ice-9/gap-buffer.scm \
ice-9/getopt-long.scm \
ice-9/command-line-processor.scm \
ice-9/hash-table.scm \
ice-9/hcons.scm \
ice-9/history.scm \

View file

@ -0,0 +1,646 @@
;;;; command-line-processor.scm --- command-line options processing
;;;; -*- scheme -*-
;;;;
;;;; Copyright (C) 1998, 2001, 2006, 2009, 2011, 2020
;;;; Free Software Foundation, Inc.
;;;;
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
;;;; License as published by the Free Software Foundation; either
;;;; version 3 of the License, or (at your option) any later version.
;;;;
;;;; This library 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
;;;; Lesser General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU Lesser General Public
;;;; License along with this library; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
;;;; 02110-1301 USA
;;; Author: Dale Mellor <guile-qf1qmg@rdmp.org> May, 2020
;;; Commentary:
;;; Where the Guile (ice-9 getopt-long) module, modelled after the GNU C
;;; libraryʼs getopt_long function, allows an application to construct
;;; a grammar prescribing the decomposition of the command-line options,
;;; this module, inspired by the C libraryʼs argp parser, gives the
;;; application a higher-level paradigm in which the command-line
;;; processing is specified declaratively. This includes enough of the
;;; application meta-data and some fragmentary help strings for the
;;; completely automatic generation of responses to GNU-standard
;;; --help, --version and --usage options, thus alleviating the
;;; need of the application itself to deal with these things.
;;;
;;; The module has three specific aims.
;;;
;;; 1) Provide higher-level declarative interface, easier to use.
;;;
;;; 2) Automatically respond to --help, --version and --usage
;;; options.
;;;
;;; 3) Allow amalgamation of specifications, so that an application
;;; can mix in requirements from modules into its own option
;;; specification--THIS IS NOT CURRENTLY IMPLEMENTED.
;;;
;;; There is just one function which needs to be called to get all of
;;; this functionality: it is process-command-line, and has the side
;;; effect that new variable bindings appear in the current module
;;; corresponding to all the options. For example, if a declared option
;;; is --do-this, then a variable called, literally, --do-this will
;;; be injected in the current namespace and will have the value
;;; provided on the command-line, or simply #t or #f to indicate whether
;;; or not that option was present on the command line.
;;;
;;; Alternatively, it is possible to create and compose the
;;; specification in separate steps, and then call the above method with
;;; the results. The functions command-line-specification and
;;; merge-command-line-specifications are provided to this end.
;;; (process-command-line COMMAND-LINE SPECIFICATION)
;;; Process the COMMAND-LINE according to the application SPECIFICATION.
;;;
;;; COMMAND-LINE is a list of strings, such as that returned from the
;;; core command-line function.
;;;
;;; SPECIFICATION is a form holding a space-separated mix of selection
;;; words followed by their respective declarations. The selection
;;; words are application, author, bug-address, copyright,
;;; help-preamble, help-postamble, license, option, usage and
;;; version, and can appear in any order.
;;;
;;; application should be followed by a string: the name of the
;;; application with possibly the package name in
;;; parentheses afterwards
;;; author should be followed by a string giving the name of one of
;;; the packageʼs authors. This selection word can be
;;; repeated as many times as necessary to provide the names
;;; of all authors.
;;; bug-address should be followed by a string giving the URL of a
;;; contact-point for sending bug reports, such as an
;;; e-mail address or web address of bug-tracking system
;;; interface
;;; copyright should be followed by a string containing a list of
;;; years and an entity to whom the copyright is assigned.
;;; This may be repeated to list other assignees
;;; help-preamble should be followed by a number of strings which
;;; make up a short paragraph of text displayed before
;;; a full list of the available program options
;;; help-postamble, like the preamble, is followed by strings which
;;; make up a paragraph of text, shown after the list
;;; of options
;;; license can be followed by one of the words GPLv3 [this is
;;; currently the only standard choice implemented], or else
;;; a string which briefly gives out the terms of the license
;;; option is followed by an option declaration, described below
;;; usage is followed by a string describing the usage of the
;;; application on one line
;;; version is followed by a string providing the current version
;;; number of this program
;;;
;;; The option declaration is followed by another form bracketed by
;;; parentheses and holding a space-separated mix of declarations (order
;;; irrelevant).
;;;
;;; A word beginning with two hyphens, an optional exclamation point,
;;; alphabetic letters, an optional equals sign, and an optional
;;; further word. There must be exactly one of these, and they
;;; determine the long name of the option. An exclamation point
;;; indicates that the option MUST appear on the command line, an
;;; equals indicates that the option MUST have a value unless it is
;;; followed in the specification by a value, in which case the value
;;; on the command-line is optional and the one in the specification
;;; will be taken as the default when not given on the command line.
;;;
;;; A word comprised of one hyphen and one letter. There can be
;;; exactly zero or one of these, and it declares that the option has
;;; this short form available on the command-line. As a very special
;;; exception: if you want to use -i as an option, it must be
;;; specified with the identifier short-i (a naked /-i/ is read as
;;; a complex number); ditto short-I for -I.
;;;
;;; A number of strings which are catenated together to provide a
;;; short, succinct description of the option. These strings should
;;; be approximately half the width of a page, i.e. about 40
;;; characters.
;;;
;;; A function which will be used as a predicate to decide if a value
;;; is allowable for this option. There should be zero or one of
;;; these.
;;;
;;; For the precise presentation of options on the command-line, the
;;; reader should refer to the description of the getopt-long module,
;;; which underlies the present one.
;;;
;;; At this point a short example is in order. The main entry point for
;;; the GNU Mcron program has as its first clause
;;;
;;; (process-command-line (command-line)
;;; application "mcron"
;;; version "1.4"
;;; usage "[OPTIONS]... [FILES]..."
;;; help-preamble
;;; "Run an mcron process according to the specifications in the FILE... "
;;; "(`-' for standard input), or use all the files in ~/.config/cron "
;;; "(or the deprecated ~/.cron) with .guile or .vixie extensions.\n"
;;; "Note that --daemon and --schedule are mutually exclusive."
;;; option (--daemon -d
;;; "run as a daemon process")
;;; option (--stdin=guile -i (λ (in) (or (string=? in "guile")
;;; (string=? in "vixie")))
;;; "format of data passed as standard input or file "
;;; "arguments, 'guile' or 'vixie' (default guile)")
;;; option (--schedule=8 -s string->number
;;; "display the next N (or 8) jobs that will be run")
;;; help-postamble
;;; "Mandatory or optional arguments to long options are also mandatory or "
;;; "optional for any corresponding short options."
;;; bug-address "bug-mcron@gnu.org"
;;; copyright "2003, 2006, 2014, 2020 Free Software Foundation, Inc."
;;; license GPLv3)
;;;
;;; after which there are four new variable bindings in the present
;;; namespace: --daemon, --stdin, --schedule and --! (the latter holds
;;; all the command-line arguments that did not partake in option
;;; processing) whose values depend on the specific command-line options
;;; the end user furnished.
;;; (command-line-specification SPECIFICATION)
;;; Compiles an object which encapsulates the given SPECIFICATION.
;;;
;;; For details of how to give a SPECIFICATION, see the description of
;;; the full process-command-line function above. The return from
;;; this method can be used in the partial version of
;;; process-command-line described below, and in the following
;;; merge-command-line-specifications function.
;;; (merge-command-line-specifications SPECIFICATION_OBJECT ...) Make a
;;; single specification object which embodies the amalgamation of all
;;; of the specification objects given as arguments.
;;;
;;; Order is important: if two option items specify the same short form
;;; for the option (a single letter), then only the first option will
;;; actually have that short form available at the command-line.
;;; Similarly, if two options have exactly the same name, the second (or
;;; later) ones will have a numerical digit appended to their name.
;;; (process-command-line COMMAND-LINE SPECIFICATION-OBJECT) Perform
;;; exactly the same function as the full process-command-line
;;; function described above, but takes a pre-made specification object
;;; produced using the two functions above.
;;; Bugs/To do
;;;
;;; 1) This stuff currently only works in the top-level module.
;;;
;;; 2) Want to be able to amalgamate command-line specifications from
;;; different modules. Will need to get to the bottom of the first
;;; issue before we can tackle this one (somehow need to put the
;;; --option variable bindings into the right places, or at least
;;; replicate them all in all modules which want to do some processing
;;; of the command line).
;;;
;;; 3) Want more license boilerplate text; currently we only have GPLv3.
;;; Code:
(define-module (ice-9 command-line-processor)
#:use-module (srfi srfi-1) ;; fold
#:use-module (srfi srfi-9) ;; Records
#:use-module (srfi srfi-9 gnu) ;; set/get-fields
#:use-module (ice-9 getopt-long)
#:use-module (ice-9 regex)
#:export (specific option item
obtain-getopt-long-results
process-getopt-long-results
;; These are the real public exports.
process-command-line
command-line-specification
merge-command-line-specifications))
(define-record-type <<specification>>
(make-specification- preamble postamble copyright authors options)
specification?
(name spec:name spec:set-name!)
(version spec:version spec:set-version!)
(usage spec:usage spec:set-usage!)
(preamble spec:preamble spec:set-preamble!)
(postamble spec:postamble spec:set-postamble!)
(bug-address spec:bugs spec:set-bugs!)
(copyright spec:copyright spec:set-copyright!)
(license spec:license spec:set-license!)
(authors spec:authors spec:set-authors!)
(options spec:options spec:set-all-options!))
(define (make-specification) (make-specification- '() '() '() '() '()))
(define-record-type <<option>>
(make-option- description)
option?
(name option:name option:set-name!)
(required? option:required?)
(short-letter option:short option:set-short!)
(value? option:value?)
(default option:default)
(description option:description option:set-description!)
(predicate option:predicate option:set-predicate!))
(define (make-option) (make-option- '()))
(define (has-option-short-form spec letter)
(if (not letter)
#f
(let loop ((o (spec:options spec)))
(cond ((null? o) #f)
((eq? letter (option:short (car o))) #t)
(else (loop (cdr o)))))))
(define (has-option-name spec name)
(let loop ((o (spec:options spec)))
(cond ((null? o) #f)
((string=? (option:name (car o)) name) #t)
(else (loop (cdr o))))))
(define (merge-command-line-specifications A . B)
"- Scheme Procedure: merge-command-line-specifications A B Append the
list of options in A with those in B, but drop any short-forms in B
which clash with existing ones, and if a long option name clashes then
append a number to make it unique. A and B will be mutilated in the
process, a new specification object will be returned."
(for-each (λ (b-spec)
(for-each (λ (b-option)
(when (has-option-short-form A (option:short b-option))
(option:set-short! b-option #f))
(when (has-option-name A (option:name b-option))
(let ((base-name (option:name b-option)))
(let loop ((count 1))
(let ((new-name (string-append base-name "-"
(number->string count))))
(if (has-option-name A new-name)
(loop (1+ count))
(option:set-name! b-option new-name))))))
(spec:set-all-options! A (append (spec:options A)
(list b-option))))
(spec:options b-spec)))
B)
A)
(define long-re (make-regexp "^--(!)?([a-zA-Z][-_0-9a-zA-Z]*)(=(.+)?)?$"))
(define short-re (make-regexp "^-[a-zA-Z]$"))
(define-syntax item ;; As in, an option item (long name, short form...).
(λ (x) (syntax-case x (short-i short-I)
;; No more work to do.
((item O) #'#t)
;; Next option is a string: take as description.
((item O desc args ...)
(string? (syntax->datum #'desc))
#'(begin (option:set-description! O (append (option:description O)
(list desc)))
(item O args ...)))
;; Next option is short-form.
((item O short-i args ...)
#`(begin (option:set-short! O #\i)
(item O args ...)))
((item O short-I args ...)
#`(begin (option:set-short! O #\I)
(item O args ...)))
((item O short args ...)
(and (identifier? #'short)
(regexp-exec short-re (symbol->string (syntax->datum #'short))))
#`(begin (option:set-short! O (string-ref (symbol->string 'short) 1))
(item O args ...)))
;; Next option is long-form.
((item O long args ...)
(and (identifier? #'long)
(regexp-exec long-re (symbol->string (syntax->datum #'long))))
#`(begin (let ((match (regexp-exec long-re
(symbol->string (syntax->datum #'long)))))
(set! O
(set-fields O
((option:name) (match:substring match 2))
((option:required?) (if (match:substring match 1) #t #f))
((option:value?)
(cond ((not (match:substring match 3)) #f)
((match:substring match 4) 'optional)
(else #t)))
((option:default) (match:substring match 4)))))
(item O args ...)))
;; Next option is a procedure: take as predicate.
((item O (lambda args ...) Args ...)
#'(begin (option:set-predicate! O (lambda args ...))
(item O Args ...)))
((item O pred args ...)
(and (identifier? #'pred)
;; (procedure? (primitive-eval (syntax->datum #'pred)))
)
#'(begin (option:set-predicate! O pred)
(item O args ...))))))
(define-syntax-rule (option args ...)
(let ((O (make-option))) (item O args ...) O))
(define-syntax specific
(λ (x) (syntax-case x (application author bug-address
copyright help-preamble help-postamble
license option usage
version)
((specific spec application A args ...)
(string? (syntax->datum #'A))
#'(begin (spec:set-name! spec A)
(specific spec args ...)))
((specific spec author A args ...)
(string? (syntax->datum #'A))
#'(begin (spec:set-author! spec (append (spec:authors spec)
(list A)))
(specific spec args ...)))
((specific spec bug-address B args ...)
(string? (syntax->datum #'B))
#'(begin (spec:set-bugs! spec B)
(specific spec args ...)))
((specific spec copyright C args ...)
(string? (syntax->datum #'C))
#'(begin (spec:set-copyright! spec (append (spec:copyright spec)
(list C)))
(specific spec args ...)))
((specific spec help-preamble id args ...)
(identifier? #'id)
#'(specific spec id args ...))
((specific spec help-preamble quotation args ...)
(string? (syntax->datum #'quotation))
#'(begin (spec:set-preamble! spec (append (spec:preamble spec)
(list quotation)))
(specific spec help-preamble args ...)))
((specific spec help-postamble id args ...)
(identifier? #'id)
#'(specific spec id args ...))
((specific spec help-postamble quotation args ...)
(string? (syntax->datum #'quotation))
#'(begin (spec:set-postamble! spec (append (spec:postamble spec)
(list quotation)))
(specific spec help-postamble args ...)))
((specific spec license L args ...)
(identifier? #'L)
#'(begin (spec:set-license! spec 'L)
(specific spec args ...)))
((specific spec license L args ...)
(string? (syntax->datum #'L))
#'(begin (spec:set-license! spec L)
(specific spec args ...)))
((specific spec option (args ...) Args ...)
#'(begin (spec:set-all-options! spec
(append (spec:options spec)
(list (option args ...))))
(specific spec Args ...)))
((specific spec usage U args ...)
(string? (syntax->datum #'U))
#'(begin (spec:set-usage! spec U)
(specific spec args ...)))
((specific spec version V args ...)
(string? (syntax->datum #'V))
#'(begin (spec:set-version! spec V)
(specific spec args ...)))
((specific spec) #'#t))))
(define-syntax-rule (command-line-specification args ...)
;; " - Scheme Procedure: command-line-specification ARGS ...
;; Furnish an application specification object with attributes specified in
;; ARGS followed by a number of values for the attribute. Please refer to
;; full documentation for a proper description of a specification object.
;; The attributes are
;; application: string: the formal name of this application. Must appear
;; exactly once.
;; author: string: the name of an author. May appear any number of times.
;; bug-address: string: The URI to which bug reports should be addressed.
;; May appear zero or one times.
;; copyright: string: list of years and owning entity. May appear any
;; number of times.
;; help-preamble: string: text to precede the list of options in a
;; response to the --help option. This attribute may appear any
;; number of times, and each occurrence can be followed by one or
;; more strings which will be assembled together into paragraphs.
;; help-postamble: string: text to succeed the list of options in a help
;; message. Same considerations apply as to help-preamble.
;; license: identifier or string: either the identifier GPLv3 or a
;; string describing the terms of the license.
;; option: (sub-form): the sub-form must contain one identifier composed
;; of two hyphens, an optional exclamation point, a token of
;; letters, numbers, underscore and hyphen, an optional equals
;; sign, and an optional word; the sub-form may have zero or one
;; identifiers composed of a hyphen and a single letter; any
;; number of strings which will be composed into a paragraph of
;; help for the option (these should be sized to half-line
;; lengths); and zero or one procedures which will be applied as a
;; predicate on allowable option values. Any number of these
;; option attributes may appear in the specification.
;; usage: string: a single line of text prototyping the command line.
;; Zero or one of these may appear.
;; version: string: the version number of this application. Zero or one
;; of these attributes may appear."
(let ((spec (make-specification)))
(specific spec args ...)
spec))
(define (version-string spec)
(with-output-to-string (λ ()
(display (if (string? (spec:name spec))
(spec:name spec)
(car (command-line))))
(when (string? (spec:version spec))
(display " ") (display (spec:version spec)) (newline))
(unless (null? (spec:copyright spec))
(display "Copyright © ")
(display (string-join (spec:copyright spec) "\n "))
(newline))
(cond ((eq? (spec:license spec) 'GPLv3)
(display (string-append
"License GPLv3+: GNU GPL version 3 or later "
"<https://gnu.org/licenses/gpl.html>.\nThis is "
"free software: you are free to change and "
"redistribute it.\nThere is NO WARRANTY, to the "
"extent permitted by law.\n")))
((string? (spec:license spec))
(display (spec:license spec)) (newline)))
(unless (null? (spec:authors spec))
(display (string-append "Written by "
(case (length (spec:authors spec))
((1 2) (string-join (spec:authors spec) " and "))
(else
(let loop ((a (cdr (spec:authors spec)))
(ret (car (spec:authors spec))))
(if (null? (cdr a))
(string-append ret " and " (car a))
(loop (cdr a) (string-append ret ", "
(car a)))))))
".\n"))))))
(define (usage-string spec)
(string-append "Usage: " (spec:name spec) " " (spec:usage spec) "\n"))
(define (help-string spec)
(with-output-to-string (λ ()
(display (usage-string spec))
(display (string-join (spec:preamble spec) "\n"))
(display "\n\n")
(let ((max-length (fold (λ (o r) (max r (string-length (option:name o))))
0
(spec:options spec))))
(for-each (λ (o)
(display " ")
(cond ((option:short o)
(display "-") (display (option:short o))
(case (option:value? o) ((#t) (display "N, "))
((optional) (display "[N], "))
(else (display ", "))))
(else (display " ")))
(display "--")
(display (option:name o))
(case (option:value? o) ((#t) (display "=N "))
((optional) (display "[=N]"))
(else (display " ")))
(display (make-string (- max-length
(string-length (option:name o)))
#\space))
(display " ")
(when (option:required? o) (display "*REQUIRED*: "))
(display (string-join (option:description o)
(string-append "\n"
(make-string max-length
#\space)
" ")))
(newline))
(spec:options spec)))
(newline)
(display (string-join (spec:postamble spec) "\n"))
(when (spec:bugs spec)
(display "\nSend bug reports to ")
(display (spec:bugs spec))
(display ".\n")))))
(define (make-getopt-long-input spec)
(map (λ (o)
(append (list (string->symbol (option:name o)))
(cond ((option:short o)
=> (λ (x) (list (list 'single-char x))))
(else '()))
(list (list 'required? (option:required? o)))
(list (list 'value (if (option:default o)
'optional
(option:value? o))))
(cond ((option:predicate o)
=> (λ (x) (list (list 'predicate x))))
(else '()))))
(spec:options spec)))
(define (obtain-getopt-long-results args spec)
(getopt-long args (make-getopt-long-input spec)))
(define (distill-getopt-long-results go-l spec)
(cons (cons "!" (option-ref go-l '() '()))
(map (λ (o)
(let ((g (option-ref go-l
(string->symbol (option:name o))
#f)))
(when (eq? g #t)
(case (string->symbol (option:name o))
((help) (display (help-string spec)) (exit 0))
((version) (display (version-string spec)) (exit 0))
((usage) (display (usage-string spec)) (exit 0))))
(cons (option:name o)
(if (and (eq? #t g)
(not (eq? #f (option:default o))))
(option:default o)
g))))
(spec:options spec))))
(define (process-getopt-long-results go-l spec)
(for-each (λ (option)
(module-define!
(current-module)
(string->symbol (string-append "--" (car option)))
(cdr option)))
(distill-getopt-long-results go-l spec)))
(define-syntax process-command-line
;; "- Scheme Procedure: process-command-line COMMAND-LINE SPECS [...]
;; Process the COMMAND-LINE according to the SPECS, extracting options and
;; their values, dealing with --help, --version and --usage requests. The
;; procedure has no return values, but has the side effect of creating
;; variable bindings in the current module corresponding to the long form
;; of the options, plus a variable called --! which gets a list of all
;; the arguments on the command-line which did not participate in option
;; processing.
;; The COMMAND-LINE is a list of strings, starting with the name of the
;; program and containing all the tokens passed to the program on the
;; command line, such as returned from the core command-line procedure.
;; The SPECS should have been obtained with the
;; command-line-specification procedure, or, as a short-cut, can be
;; supplied directly as arguments to this procedure."
(syntax-rules (GO)
((_ command-line specs GO)
(let ((S (merge-command-line-specifications
specs
(command-line-specification
option (-h --help "display this help and exit")
option (-V --version "output version information and exit")
option (-u --usage "show brief usage summary")))))
(process-getopt-long-results
(obtain-getopt-long-results command-line S)
S)))
((_ command-line specs)
(process-command-line command-line specs GO))
((_ command-line item ...)
(process-command-line
command-line
(command-line-specification item ...)))))

View file

@ -1,5 +1,8 @@
;;; Copyright (C) 1998, 2001, 2006, 2009, 2011 Free Software Foundation, Inc.
;;;
;;;; getopt-long.scm --- long options processing -*- scheme -*-
;;;;
;;;; Copyright (C) 1998, 2001, 2006, 2009, 2011, 2020
;;;; Free Software Foundation, Inc.
;;;;
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
;;;; License as published by the Free Software Foundation; either
@ -12,54 +15,59 @@
;;;;
;;;; You should have received a copy of the GNU Lesser General Public
;;;; License along with this library; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
;;;; 02110-1301 USA
;;; Author: Russ McManus (rewritten by Thien-Thi Nguyen)
;;; Author: Russ McManus
;;; Rewritten by Thien-Thi Nguyen
;;; Rewritten by Dale Mellor 2020-04-14
;;; Commentary:
;;; This module implements some complex command line option parsing, in
;;; the spirit of the GNU C library function `getopt_long'. Both long
;;; the spirit of the GNU C library function getopt_long. Both long
;;; and short options are supported.
;;;
;;; The theory is that people should be able to constrain the set of
;;; options they want to process using a grammar, rather than some arbitrary
;;; structure. The grammar makes the option descriptions easy to read.
;;; options they want to process using a grammar, rather than some ad
;;; hoc procedure. The grammar makes the option descriptions easy to
;;; read.
;;;
;;; `getopt-long' is a procedure for parsing command-line arguments in a
;;; manner consistent with other GNU programs. `option-ref' is a procedure
;;; that facilitates processing of the `getopt-long' return value.
;;; getopt-long is a procedure for parsing command-line arguments in a
;;; manner consistent with other GNU programs. option-ref is a procedure
;;; that facilitates processing of the getopt-long return value.
;;; (getopt-long ARGS GRAMMAR)
;;; Parse the arguments ARGS according to the argument list grammar GRAMMAR.
;;;
;;; ARGS should be a list of strings. Its first element should be the
;;; name of the program; subsequent elements should be the arguments
;;; name of the program, and subsequent elements should be the arguments
;;; that were passed to the program on the command line. The
;;; `program-arguments' procedure returns a list of this form.
;;; program-arguments procedure returns a list of this form.
;;;
;;; GRAMMAR is a list of the form:
;;; ((OPTION (PROPERTY VALUE) ...) ...)
;;;
;;; Each OPTION should be a symbol. `getopt-long' will accept a
;;; command-line option named `--OPTION'.
;;; Each OPTION should be a symbol. getopt-long will accept a
;;; command-line option named --OPTION.
;;; Each option can have the following (PROPERTY VALUE) pairs:
;;;
;;; (single-char CHAR) --- Accept `-CHAR' as a single-character
;;; equivalent to `--OPTION'. This is how to specify traditional
;;; (single-char CHAR) --- Accept -CHAR as a single-character
;;; equivalent to --OPTION. This is how to specify traditional
;;; Unix-style flags.
;;; (required? BOOL) --- If BOOL is true, the option is required.
;;; getopt-long will raise an error if it is not found in ARGS.
;;; (value BOOL) --- If BOOL is #t, the option accepts a value; if
;;; it is #f, it does not; and if it is the symbol
;;; `optional', the option may appear in ARGS with or
;;; optional, the option may appear in ARGS with or
;;; without a value.
;;; (predicate FUNC) --- If the option accepts a value (i.e. you
;;; specified `(value #t)' for this option), then getopt
;;; will apply FUNC to the value, and throw an exception
;;; if it returns #f. FUNC should be a procedure which
;;; accepts a string and returns a boolean value; you may
;;; need to use quasiquotes to get it into GRAMMAR.
;;; specified (value #t) or (value 'optional) for this
;;; option), then getopt will apply FUNC to the value, and
;;; will not take the value if it returns #f. FUNC should
;;; be a procedure which accepts a string and returns a
;;; boolean value; you may need to use quasiquotes to get it
;;; into GRAMMAR.
;;;
;;; The (PROPERTY VALUE) pairs may occur in any order, but each
;;; property may occur only once. By default, options do not have
@ -79,16 +87,22 @@
;;; for "blimps" and "catalexis")
;;; ("-ab" "bang" "-c" "couth") (same)
;;; ("-ac" "couth" "-b" "bang") (same)
;;; ("-abc" "couth" "bang") (an error, since `-b' is not the
;;; last option in its combination)
;;;
;;; If an option's value is optional, then `getopt-long' decides
;;; whether it has a value by looking at what follows it in ARGS. If
;;; the next element is does not appear to be an option itself, then
;;; that element is the option's value.
;;; If an option's value is optional, then getopt-long decides whether
;;; it has a value by looking at what follows it in ARGS. If the next
;;; element does not appear to be an option itself, and passes a
;;; predicate if given, then that element is taken to be the option's
;;; value. Note that predicate functions are invaluable in this respect
;;; for differentiating options and option values, and in the case of
;;; options with optional values, PREDICATES REALLY SHOULD BE GIVEN. If
;;; an option is supposed to take a numerical value, then
;;; string->number can be used as the predicate; this will also allow
;;; negative values to be used, which would ordinarily be regarded as
;;; bad options causing the module, and the application consuming it, to
;;; bail out with an immediate exit to the operating system.
;;;
;;; The value of a long option can appear as the next element in ARGS,
;;; or it can follow the option name, separated by an `=' character.
;;; or it can follow the option name, separated by an = character.
;;; Thus, using the same grammar as above, the following argument lists
;;; are equivalent:
;;; ("--apples" "Braeburn" "--blimps" "Goodyear")
@ -99,27 +113,30 @@
;;; subsequent arguments are returned as ordinary arguments, even if
;;; they resemble options. So, in the argument list:
;;; ("--apples" "Granny Smith" "--" "--blimp" "Goodyear")
;;; `getopt-long' will recognize the `apples' option as having the
;;; value "Granny Smith", but it will not recognize the `blimp'
;;; option; it will return the strings "--blimp" and "Goodyear" as
;;; ordinary argument strings.
;;; getopt-long will recognize the apples option as having the value
;;; "Granny Smith", but it will not recognize the blimp option; it
;;; will return the strings "--blimp" and "Goodyear" as ordinary
;;; argument strings. The first "--" argument itself will *not* appear
;;; in the ordinary arguments list, although the occurrence of
;;; subsequent ones will.
;;;
;;; The `getopt-long' function returns the parsed argument list as an
;;; The getopt-long function returns the parsed argument list as an
;;; assocation list, mapping option names --- the symbols from GRAMMAR
;;; --- onto their values, or #t if the option does not accept a value.
;;; Unused options do not appear in the alist.
;;;
;;; All arguments that are not the value of any option are returned
;;; as a list, associated with the empty list.
;;; All arguments that are not the value of any option are returned as a
;;; list, associated with the empty list in the above returned
;;; association.
;;;
;;; `getopt-long' throws an exception if:
;;; getopt-long throws an exception if:
;;; - it finds an unrecognized property in GRAMMAR
;;; - the value of the `single-char' property is not a character
;;; - the value of the single-char property is not a character
;;; - it finds an unrecognized option in ARGS
;;; - a required option is omitted
;;; - an option that requires an argument doesn't get one
;;; - an option that doesn't accept an argument does get one (this can
;;; only happen using the long option `--opt=value' syntax)
;;; only happen using the long option --opt=value syntax)
;;; - an option predicate fails
;;;
;;; So, for example:
@ -147,9 +164,10 @@
;;; (option-ref OPTIONS KEY DEFAULT)
;;; Return value in alist OPTIONS using KEY, a symbol; or DEFAULT if not
;;; found. The value is either a string or `#t'.
;;; found. The return is either a string or #t, or whatever DEFAULT
;;; is.
;;;
;;; For example, using the `getopt-long' return value from above:
;;; For example, using the getopt-long return value from above:
;;;
;;; (option-ref (getopt-long ...) 'x-includes 42) => "/usr/include"
;;; (option-ref (getopt-long ...) 'not-a-key! 31) => 31
@ -158,12 +176,17 @@
(define-module (ice-9 getopt-long)
#:use-module ((ice-9 common-list) #:select (remove-if-not))
#:use-module (ice-9 control)
#:use-module (srfi srfi-9)
#:use-module (ice-9 match)
#:use-module (ice-9 regex)
#:use-module (ice-9 optargs)
#:use-module (ice-9 receive)
#:export (getopt-long option-ref))
;; Code makes more sense to human beings with this.
(define return values)
(define %program-name (make-fluid "guile"))
(define (program-name)
(fluid-ref %program-name))
@ -175,18 +198,13 @@
(exit 1))
(define-record-type option-spec
(%make-option-spec name required? option-spec->single-char predicate value-policy)
(%make-option-spec name required? single-char predicate value-policy)
option-spec?
(name
option-spec->name set-option-spec-name!)
(required?
option-spec->required? set-option-spec-required?!)
(option-spec->single-char
option-spec->single-char set-option-spec-single-char!)
(predicate
option-spec->predicate set-option-spec-predicate!)
(value-policy
option-spec->value-policy set-option-spec-value-policy!))
(name option-spec->name)
(required? option-spec->required? set-option-spec-required?!)
(single-char option-spec->single-char set-option-spec-single-char!)
(predicate option-spec->predicate set-option-spec-predicate!)
(value-policy option-spec->value-policy set-option-spec-value-policy!))
(define (make-option-spec name)
(%make-option-spec name #f #f #f #f))
@ -195,120 +213,338 @@
(let ((spec (make-option-spec (symbol->string (car desc)))))
(for-each (match-lambda
(('required? val)
(set-option-spec-required?! spec val))
(set-option-spec-required?! spec val))
(('value val)
(set-option-spec-value-policy! spec val))
(set-option-spec-value-policy! spec val))
(('single-char val)
(or (char? val)
(error "`single-char' value must be a char!"))
(set-option-spec-single-char! spec val))
(unless (char? val)
(fatal-error "single-char value must be a char!"))
(set-option-spec-single-char! spec val))
(('predicate pred)
(set-option-spec-predicate!
spec (lambda (name val)
(or (not val)
(pred val)
(fatal-error "option predicate failed: --~a"
name)))))
(set-option-spec-predicate! spec pred))
((prop val)
(error "invalid getopt-long option property:" prop)))
(fatal-error "invalid getopt-long option property:" prop)))
(cdr desc))
spec))
(define (split-arg-list argument-list)
;; Scan ARGUMENT-LIST for "--" and return (BEFORE-LS . AFTER-LS).
;; Discard the "--". If no "--" is found, AFTER-LS is empty.
(let loop ((yes '()) (no argument-list))
(cond ((null? no) (cons (reverse yes) no))
((string=? "--" (car no)) (cons (reverse yes) (cdr no)))
(else (loop (cons (car no) yes) (cdr no))))))
(define short-opt-rx (make-regexp "^-([a-zA-Z]+)(.*)"))
(define long-opt-no-value-rx (make-regexp "^--([^=]+)$"))
(define long-opt-with-value-rx (make-regexp "^--([^=]+)=(.*)"))
;; Extract the name of a long option given that it may or may not be
;; surrounded by '--' and '=...'.
(define isolate-long-name-re (make-regexp "^-*([^=]+)"))
(define (isolate-long-name name)
(cond ((regexp-exec isolate-long-name-re name)
=> (λ (match) (match:substring match 1)))
(else #f)))
;; Whatever the presentation of the long option, make sure it is in a
;; clean, normalized form (but this does NOT account for any value the
;; option might have).
(define (re-present option)
(string-append "--" (isolate-long-name option) "="))
;; The /name/ passed in here must be a string with just the characters
;; of the option name in it. The return is the spec with that name, or
;; #f if such cannot be found.
(define (find-spec-long-name-clear specs name)
(cond ((null? specs) #f)
((string=? (option-spec->name (car specs)) name) (car specs))
(else (find-spec-long-name-clear (cdr specs) name))))
;; The /name/ can take the form of a long option entry on the command
;; line, with whatever decoration that entails. Will return #f if a
;; spec does not exist for this named option.
(define (find-spec-long specs name)
(cond ((isolate-long-name name)
=> (λ (name) (find-spec-long-name-clear specs name)))
(else #f)))
;; Return #f if a spec with the short /letter/ name does not exist.
(define (find-spec-short specs letter)
(cond ((null? specs) #f)
((eq? (option-spec->single-char (car specs)) letter) (car specs))
(else (find-spec-short (cdr specs) letter))))
;; Return the long name (string) of a short option (char).
(define (short->long specs letter)
(cond ((find-spec-short specs letter) => option-spec->name)
(else (string letter))))
;; Take, for example, /short/='-h' to '--help='.
(define (double-up short specs)
(string-append "--" (short->long specs (string-ref short 1)) "="))
;; Can't believe this is not already in Guile, but return a boolean
;; indicating if /a/ is a character of the English alphabet. This
;; should probably be more locale-specific.
(define char-rx (make-regexp "[a-zA-Z]"))
(define (is-alpha a) (regexp-exec char-rx (string a)))
;; This procedure does whatever is necessary to put the (ostensibly)
;; first item on the command-line into the canonical (normal) form
;; '--item=value'; this may mean consuming the next item of the
;; command-line (the first item of /rest/) to get the value. Note that
;; the value may be missing, but the '=' sign will always be there in
;; the return. The first item (/A/) will always be more than two
;; characters, and the first two characters will be "--", i.e. we are
;; processing a long option.
;;
;; A IN string The first argument on the command-line
;; rest IN list of strings The remaining items of the command-line
;; specs IN list of option-spec Options specification
;; remnant OUT list of strings The unprocessed command line
;; processed OUT string New command-line argument
(define (normalize-long-option A rest specs)
(define (return-empty-arg) (return rest (re-present A)))
(define (return-arg-with-value)
(return (cdr rest) (string-append (re-present A) (car rest))))
(cond
((string-index A #\=)
;; The argument is already in the canonical form.
(return rest A))
((null? rest)
;; There are no more arguments to be had, so present an empty
;; value.
(return-empty-arg))
((find-spec-long specs A)
;; There is an option spec for this argument; we must use the
;; /value-policy/ and /predicate/ members to decide whether or
;; not to take the following argument from the command-line as
;; the value of the option.
=> (λ (spec)
(cond
((option-spec->predicate spec)
=> (λ (pred) (if (pred (car rest))
(return-arg-with-value)
(return-empty-arg))))
(else (cond ((eq? (option-spec->value-policy spec) 'optional)
(if (eq? (string-ref (car rest) 0) #\-)
(return-empty-arg)
(return-arg-with-value)))
((and (eq? (option-spec->value-policy spec) #t)
(or (string->number (car rest))
(not (eq? (string-ref (car rest) 0) #\-))))
(return-arg-with-value))
(else (return-empty-arg)))))))
(else
;; We know nothing about this option, abort operations.
(fatal-error "no such option: --~a" (isolate-long-name A)))))
;; This procedure does whatever is necessary to put the (ostensibly)
;; first item on the command-line into the canonical form
;; '--item=value'; this may mean consuming the next item of the
;; command-line (the first item of /rest/) to get the value. Note that
;; the value may be missing, but the '=' sign will always be there in
;; the return. The first item (/A/) will always be exactly two
;; characters, and the first character will be "-", i.e. we are
;; processing an isolated short option.
;;
;; A IN string The first argument on the command-line
;; rest IN list of strings The remaining items of the command-line
;; specs IN list of option-spec Options specification
;; remnant OUT list of strings The unprocessed command line
;; processed OUT string New command-line argument
(define (normalize-free-short-option A rest specs)
(define (return-empty-arg) (return rest (double-up A specs)))
(define (return-arg-with-next-value)
(return (cdr rest)
(string-append (double-up A specs) (car rest))))
(let* ((name (string-ref A 1))
(spec (find-spec-short specs name)))
(unless (is-alpha name) (return rest A))
(unless spec (fatal-error "no such option: -~a" name))
(cond ((null? rest) (return-empty-arg))
((option-spec->predicate spec)
=> (λ (pred) (if (pred (car rest))
(return-arg-with-next-value)
(return-empty-arg))))
((eq? (option-spec->value-policy spec) #f)
(return-empty-arg))
((eq? (option-spec->value-policy spec) 'optional)
(if (eq? (string-ref (car rest) 0) #\-)
(return-empty-arg)
(return-arg-with-next-value)))
(else (return-arg-with-next-value)))))
;; The /sequence/ is a string of characters from the command line, and
;; the task is to decide if those characters, after a '-' sign, are a
;; viable clumped option sequence, possibly using some of the trailing
;; characters as option values, or not.
(define (viable-short sequence specs)
(cond ((eq? 0 (string-length sequence)) #t)
((find-spec-short specs (string-ref sequence 0))
;; If this optionʼs /value-policy/ allows the option to
;; take a value then this string is viable as the
;; remainder can be taken as that value. Otherwise we
;; must assert the viability of the rest of the line by
;; recursion.
=> (λ (spec) (or (not (eq? #f (option-spec->value-policy spec)))
(viable-short (substring sequence 1) specs))))
(else #f)))
;; This procedure does whatever is necessary to put the (ostensibly)
;; first item on the command-line into the canonical form
;; '--item=value'. Note that the value may be missing, but the '='
;; sign will always be there in the return. The first item (/A/) will
;; always be *more* than two characters, and the first character will
;; be "-", i.e. we are processing a short option which is either
;; clumped with other short options, or is clumped with its value.
;;
;; A IN string The first argument on the command-line
;; rest IN list of strings The remaining items of the command-line
;; specs IN list of option-spec Options specification
;; remnant OUT list of strings The unprocessed command line
;; processed OUT string New command-line argument
(define (normalize-clumped-short-option A rest specs)
(define (declump-arg) (return (cons* (string-append "-" (substring A 1 2))
(string-append "-" (substring A 2))
rest)
#f))
(define (construct-arg-from-clumped-value)
(return rest (string-append (double-up A specs)
(substring A 2))))
(unless (is-alpha (string-ref A 1)) (return rest A))
(let ((spec (find-spec-short specs (string-ref A 1))))
(unless spec (fatal-error "no such option: -~a" (string-ref A 1)))
(cond ((option-spec->predicate spec)
=> (λ (pred) (if (pred (substring A 2))
(construct-arg-from-clumped-value)
(declump-arg))))
((eq? (option-spec->value-policy spec) 'optional)
(if (viable-short (substring A 2) specs)
(declump-arg)
(construct-arg-from-clumped-value)))
((eq? (option-spec->value-policy spec) #f) (declump-arg))
(else (construct-arg-from-clumped-value)))))
;; Return a version of the command-line /args/ in which all options are
;; represented in long form with an equals sign (whether they have a
;; value or not).
(define (normalize args specs stop-at-first-non-option)
(call/ec (λ (return)
(let loop ((args args) (processed '()))
(when (null? args) (return (reverse processed)))
(apply loop (call/ec (λ (loop)
(define A (car args))
(define (when-loop cond normalizer)
(when cond
(receive (remainder-args processed-arg)
(normalizer A (cdr args) specs)
(loop (list remainder-args
(if processed-arg
(cons processed-arg processed)
processed))))))
(when (string=? "--" A)
(return (append (reverse processed) args)))
(when-loop (and (> (string-length A) 2)
(string=? (substring A 0 2) "--"))
normalize-long-option)
(when-loop (and (eq? (string-length A) 2)
(eq? (string-ref A 0) #\-))
normalize-free-short-option)
(when-loop (and (> (string-length A) 1)
(eq? (string-ref A 0) #\-))
normalize-clumped-short-option)
(when stop-at-first-non-option
(return (append (reverse processed) args)))
;else
(loop (list (cdr args) (cons A processed))))))))))
;; Check that all the rules inherent in the /specs/ are fulfilled by
;; the /options/.
(define (verify-specs-fullfilled specs options)
(for-each
(λ (spec)
(let* ((name (option-spec->name spec))
(value (assq-ref options (string->symbol name))))
(when (and (option-spec->required? spec) (not value))
(fatal-error "option must be specified: --~a" name))
(let ((policy (option-spec->value-policy spec)))
(when (and (eq? policy #t) (eq? value #t))
(fatal-error "option must be specified with argument: --~a" name))
(when (and (eq? policy #f) (string? value))
(fatal-error "option does not support argument: --~a" name)))
(let ((pred (option-spec->predicate spec)))
(when (and pred (string? value) (not (pred value)))
(fatal-error "option predicate failed: --~a" name)))))
specs))
;; Check that all the options are matched by a specification.
(define (verify-options options specs)
(for-each
(λ (value)
(unless (or (null? (car value))
(find-spec-long specs (symbol->string (car value))))
(fatal-error "no such option: --~a" (car value))))
options))
;; This procedure will simply return if the options and the specs
;; conform with each other, or else will bail out with an error
;; message.
(define (check-compliance options specs)
(verify-specs-fullfilled specs options)
(verify-options options specs))
(define full-option-re (make-regexp "^--([^=]+)=(.+)?$"))
;; The /normal-args/ are a normalized command line in which all
;; options are expressed long-form, and the task here is to construct an
;; /options/ object: an associative array of option names onto values
;; (or #t if there is no value).
(define (extract-options normal-args stop-at-first-non-option)
(let loop ((args normal-args)
(options '())
(non-options '()))
(cond
((null? args) (acons '() (reverse non-options) options))
(else
(cond
((string=? (car args) "--")
(acons '() (append (reverse non-options) (cdr args)) options))
((regexp-exec full-option-re (car args))
=> (λ (match)
(loop (cdr args)
(acons (string->symbol (match:substring match 1))
(or (match:substring match 2) #t)
options)
non-options)))
(stop-at-first-non-option
(acons '() (append (reverse non-options) args) options))
(else
(loop (cdr args) options (cons (car args) non-options))))))))
(define (looks-like-an-option string)
(or (regexp-exec short-opt-rx string)
(regexp-exec long-opt-with-value-rx string)
(regexp-exec long-opt-no-value-rx string)))
(define (process-options specs argument-ls stop-at-first-non-option)
;; Use SPECS to scan ARGUMENT-LS; return (FOUND . ETC).
;; FOUND is an unordered list of option specs for found options, while ETC
;; is an order-maintained list of elements in ARGUMENT-LS that are neither
;; options nor their values.
(let ((idx (map (lambda (spec)
(cons (option-spec->name spec) spec))
specs))
(sc-idx (map (lambda (spec)
(cons (make-string 1 (option-spec->single-char spec))
spec))
(remove-if-not option-spec->single-char specs))))
(let loop ((unclumped 0) (argument-ls argument-ls) (found '()) (etc '()))
(define (eat! spec ls)
(cond
((eq? 'optional (option-spec->value-policy spec))
(if (or (null? ls)
(looks-like-an-option (car ls)))
(loop (- unclumped 1) ls (acons spec #t found) etc)
(loop (- unclumped 2) (cdr ls) (acons spec (car ls) found) etc)))
((eq? #t (option-spec->value-policy spec))
(if (or (null? ls)
(looks-like-an-option (car ls)))
(fatal-error "option must be specified with argument: --~a"
(option-spec->name spec))
(loop (- unclumped 2) (cdr ls) (acons spec (car ls) found) etc)))
(else
(loop (- unclumped 1) ls (acons spec #t found) etc))))
(match argument-ls
(()
(cons found (reverse etc)))
((opt . rest)
(cond
((regexp-exec short-opt-rx opt)
=> (lambda (match)
(if (> unclumped 0)
;; Next option is known not to be clumped.
(let* ((c (match:substring match 1))
(spec (or (assoc-ref sc-idx c)
(fatal-error "no such option: -~a" c))))
(eat! spec rest))
;; Expand a clumped group of short options.
(let* ((extra (match:substring match 2))
(unclumped-opts
(append (map (lambda (c)
(string-append "-" (make-string 1 c)))
(string->list
(match:substring match 1)))
(if (string=? "" extra) '() (list extra)))))
(loop (length unclumped-opts)
(append unclumped-opts rest)
found
etc)))))
((regexp-exec long-opt-no-value-rx opt)
=> (lambda (match)
(let* ((opt (match:substring match 1))
(spec (or (assoc-ref idx opt)
(fatal-error "no such option: --~a" opt))))
(eat! spec rest))))
((regexp-exec long-opt-with-value-rx opt)
=> (lambda (match)
(let* ((opt (match:substring match 1))
(spec (or (assoc-ref idx opt)
(fatal-error "no such option: --~a" opt))))
(if (option-spec->value-policy spec)
(eat! spec (cons (match:substring match 2) rest))
(fatal-error "option does not support argument: --~a"
opt)))))
((and stop-at-first-non-option
(<= unclumped 0))
(cons found (append (reverse etc) argument-ls)))
(else
(loop (- unclumped 1) rest found (cons opt etc)))))))))
(define* (getopt-long program-arguments option-desc-list
#:key stop-at-first-non-option)
"Process options, handling both long and short options, similar to
"- Scheme Procedure: getopt-long PROGRAM-ARGUMENTS OPTION-DESC-LIST
[#:stop-at-first-non-option]
Process options, handling both long and short options, similar to
the glibc function 'getopt_long'. PROGRAM-ARGUMENTS should be a value
similar to what (program-arguments) returns. OPTION-DESC-LIST is a
list of option descriptions. Each option description must satisfy the
@ -337,35 +573,26 @@ or option values.
By default, options are not required, and option values are not
required. By default, single character equivalents are not supported;
if you want to allow the user to use single character options, you need
to add a `single-char' clause to the option description."
to add a single-char clause to the option description."
(with-fluids ((%program-name (car program-arguments)))
(let* ((specifications (map parse-option-spec option-desc-list))
(pair (split-arg-list (cdr program-arguments)))
(split-ls (car pair))
(non-split-ls (cdr pair))
(found/etc (process-options specifications split-ls
stop-at-first-non-option))
(found (car found/etc))
(rest-ls (append (cdr found/etc) non-split-ls)))
(for-each (lambda (spec)
(let ((name (option-spec->name spec))
(val (assq-ref found spec)))
(and (option-spec->required? spec)
(or val
(fatal-error "option must be specified: --~a"
name)))
(let ((pred (option-spec->predicate spec)))
(and pred (pred name val)))))
specifications)
(for-each (lambda (spec+val)
(set-car! spec+val
(string->symbol (option-spec->name (car spec+val)))))
found)
(cons (cons '() rest-ls) found))))
(let* ((specs (map parse-option-spec option-desc-list))
(options (extract-options
(normalize (cdr program-arguments)
specs
stop-at-first-non-option)
stop-at-first-non-option)))
(check-compliance options specs)
options)))
(define (option-ref options key default)
"Return value in alist OPTIONS using KEY, a symbol; or DEFAULT if not found.
The value is either a string or `#t'."
"- Scheme Procedure: option-ref OPTIONS KEY DEFAULT
Return value in alist OPTIONS (as returned from getopt-long),
using KEY, a symbol; or DEFAULT if not found. The value is either
a string or #t, or whatever DEFAULT is."
(or (assq-ref options key) default))
;;; getopt-long.scm ends here

View file

@ -31,19 +31,25 @@
set-fields))
(define (set-record-type-printer! type proc)
"Set PROC as the custom printer for TYPE."
"- Scheme Procedure: set-record-type-printer! TYPE PROC
Set PROC as the custom printer for TYPE."
(struct-set! type vtable-index-printer proc))
(define-syntax-rule (define-immutable-record-type name ctor pred fields ...)
"- Scheme Procedure: define-immutable-record-type NAME CTOR PRED (FIELD GETTER [SETTER]) ..."
((@@ (srfi srfi-9) %define-record-type)
#t (define-immutable-record-type name ctor pred fields ...)
name ctor pred fields ...))
(define-syntax-rule (set-field s (getter ...) expr)
"- Scheme Procedure: set-field RECORD (GETTER ...) EXPR
Set the field in RECORD with the GETTER, to the value of EXPR."
(%set-fields #t (set-field s (getter ...) expr) ()
s ((getter ...) expr)))
(define-syntax-rule (set-fields s . rest)
"- Scheme Procedure: set-fields RECORD ((GETTER ...) EXPR) ...
Set the fields in the RECORD with the given GETTERs to the corresponding EXPRessions."
(%set-fields #t (set-fields s . rest) ()
s . rest))

View file

@ -34,6 +34,7 @@ SCM_TESTS = tests/00-initial-env.test \
tests/c-api.test \
tests/chars.test \
tests/coding.test \
tests/command-line-processor.test \
tests/common-list.test \
tests/compiler.test \
tests/control.test \

View file

@ -0,0 +1,155 @@
;;;; command-line-processor.test --- long options processing -*- scheme -*-
;;;;
;;;; Copyright (C) 2001, 2006, 2011 Free Software Foundation, Inc.
;;;;
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
;;;; License as published by the Free Software Foundation; either
;;;; version 3 of the License, or (at your option) any later version.
;;;;
;;;; This library 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
;;;; Lesser General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU Lesser General Public
;;;; License along with this library; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
;;;; MA 02110-1301 USA
;;; Author: Dale Mellor <guile-qf1qmg@rdmp.org> --- May 2020
(use-modules (test-suite lib)
(ice-9 command-line-processor)
(ice-9 regex))
(define-syntax-rule (pass-if-fatal-exception name exn exp)
(let ((port (open-output-string)))
(with-error-to-port port
(λ ()
(run-test
name #t
(λ ()
(catch (car exn)
(λ () exp #f)
(λ (k . args)
(let ((output (get-output-string port)))
(close-port port)
(if (string-match (cdr exn) output)
#t
(error "Unexpected output" output)))))))))))
(defmacro deferr (name-frag re)
(let ((name (symbol-append 'exception: name-frag)))
`(define ,name (cons 'quit ,re))))
(deferr no-such-option "no such option")
(deferr option-predicate-failed "option predicate failed")
(deferr option-does-not-support-arg "option does not support argument")
(deferr option-must-be-specified "option must be specified")
(deferr option-must-have-arg "option must be specified with argument")
(with-test-prefix "exported procs"
(pass-if "option defined" (defined? 'process-command-line)))
(with-test-prefix "extended mcron options"
(define stdin-predicate (λ (in) (or (string=? in "guile")
(string=? in "vixie"))))
(define app (command-line-specification
application "mcron"
version "1.4"
usage "[OPTIONS]... [FILES]..."
help-preamble
"Run an mcron process according to the specifications in the FILE... "
"(`-' for standard input), or use all the files in ~/.config/cron "
"(or the deprecated ~/.cron) with .guile or .vixie extensions.\n"
"Note that --daemon and --schedule are mutually exclusive."
option (--daemon -d
"run as a daemon process")
option (--stdin=guile -j (λ (in) (or (string=? in "guile")
(string=? in "vixie")))
"format of data passed as standard input or file "
"arguments, 'guile' or 'vixie' (default guile)")
option (--stdin-2=guile -k (lambda (in) (or (string=? in "guile")
(string=? in "vixie")))
"format of data passed as standard input or file "
"arguments, 'guile' or 'vixie' (default guile)")
option (--stdin-3=guile -l stdin-predicate
"format of data passed as standard input or file "
"arguments, 'guile' or 'vixie' (default guile)")
option (--schedule=8 -s string->number
"display the next N (or 8) jobs that will be run")
option (--not-complex short-i "this should just work")
help-postamble
"Mandatory or optional arguments to long options are also mandatory or "
"optional for any corresponding short options."
bug-address "bug-mcron@gnu.org"
copyright "2003, 2006, 2014, 2020 Free Software Foundation, Inc."
license GPLv3))
(pass-if "specification transformed" #t)
(process-command-line '("test" "-d" "leftover0" "--schedule=3" "leftover1"
"--stdin=vixie" "--stdin-2=guile" "--stdin-3" "vixie"
"-i")
app)
(pass-if "process-command-line completed" #t)
(pass-if "-d" (eq? --daemon #t))
(pass-if "--schedule=3" (string=? --schedule "3"))
(pass-if "--!" (equal? --! '("leftover0" "leftover1")))
(pass-if "-j" (string=? --stdin "vixie"))
(pass-if "-k" (string=? --stdin-2 "guile"))
(pass-if "-l" (string=? --stdin-3 "vixie"))
(pass-if "-i" (eq? --not-complex #t))
(process-command-line '("test" "-s9") app)
(pass-if "! -d" (eq? --daemon #f))
(pass-if "-s9" (string=? --schedule "9"))
)
(with-test-prefix "option merging"
(process-command-line (string-split "prog -a -b -d -h" #\space)
(merge-command-line-specifications
(command-line-specification
application "test-1"
option (-a --alpha "option alpha")
option (-b --beta "option beta")
option (-h --eta "option eta"))
(command-line-specification
application "test-2"
option (-b --beta "option beta")
option (-d --delta "option delta"))))
(pass-if "--alpha" (eq? #t --alpha))
(pass-if "--beta" (eq? #t --beta))
(pass-if "--delta" (eq? #t --delta))
(pass-if "--eta" (eq? #t --eta))
(pass-if "--beta-1" (eq? #f --beta-1))
(pass-if "--help" (eq? #f --help))
)
(with-test-prefix "all option combinations"
(define app (command-line-specification option (--alpha -a)))
(pass-if "specification transformed" #t)
)
;;; command-line-processor.test ends here

View file

@ -1,7 +1,6 @@
;;;; getopt-long.test --- long options processing -*- scheme -*-
;;;; Thien-Thi Nguyen <ttn@gnu.org> --- August 2001
;;;;
;;;; Copyright (C) 2001, 2006, 2011 Free Software Foundation, Inc.
;;;; Copyright (C) 2001, 2006, 2011 Free Software Foundation, Inc.
;;;;
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
@ -17,6 +16,10 @@
;;;; License along with this library; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
;;; Author: Thien-Thi Nguyen <ttn@gnu.org> --- August 2001
;;; Dale Mellor <> --- April 2020
(use-modules (test-suite lib)
(ice-9 getopt-long)
(ice-9 regex))
@ -49,9 +52,34 @@
(deferr option-must-be-specified "option must be specified")
(deferr option-must-have-arg "option must be specified with argument")
(define (symbol/>string a)
(if (symbol? a) (symbol->string a) ""))
(define (output-sort out)
(sort out (lambda (a b) (string<? (symbol/>string (car a))
(symbol/>string (car b))))))
(define* (A-TEST args option-specs expectation
#:key stop-at-first-non-option)
(let ((answer
(output-sort
(getopt-long
(cons "foo" (string-split args #\space))
option-specs
#:stop-at-first-non-option stop-at-first-non-option))))
(cond ((equal? answer (output-sort expectation)) #t)
(else (format (current-output-port)
"Test result was \n~s --VS-- \n~s.\n"
answer (output-sort expectation))
#f))))
(with-test-prefix "exported procs"
(pass-if "`option-ref' defined" (defined? 'option-ref))
(pass-if "`getopt-long' defined" (defined? 'getopt-long)))
(pass-if "option-ref defined" (defined? 'option-ref))
(pass-if "getopt-long defined" (defined? 'getopt-long)))
(with-test-prefix "specifying predicate"
@ -92,33 +120,48 @@
(with-test-prefix "value optional"
(define (test3 . args)
(getopt-long args '((foo (value optional) (single-char #\f))
(bar))))
(define (test args expect)
(A-TEST args
'((foo (value optional) (single-char #\f))
(bar))
expect))
(pass-if "long option `foo' w/ arg, long option `bar'"
(equal? (test3 "prg" "--foo" "fooval" "--bar")
'((()) (bar . #t) (foo . "fooval"))))
(pass-if "long option foo w/ arg, long option bar"
(test "--foo fooval --bar"
'((()) (bar . #t) (foo . "fooval"))))
(pass-if "short option `foo' w/ arg, long option `bar'"
(equal? (test3 "prg" "-f" "fooval" "--bar")
'((()) (bar . #t) (foo . "fooval"))))
(pass-if "short option foo w/ arg, long option bar"
(test "-f fooval --bar"
'((()) (bar . #t) (foo . "fooval"))))
(pass-if "short option `foo', long option `bar', no args"
(equal? (test3 "prg" "-f" "--bar")
'((()) (bar . #t) (foo . #t))))
(pass-if "short option foo, long option bar, no args"
(test "-f --bar"
'((()) (bar . #t) (foo . #t))))
(pass-if "long option `foo', long option `bar', no args"
(equal? (test3 "prg" "--foo" "--bar")
'((()) (bar . #t) (foo . #t))))
(pass-if "long option foo, long option bar, no args"
(test "--foo --bar"
'((()) (bar . #t) (foo . #t))))
(pass-if "long option `bar', short option `foo', no args"
(equal? (test3 "prg" "--bar" "-f")
'((()) (foo . #t) (bar . #t))))
(pass-if "long option bar, short option foo, no args"
(test "--bar -f"
'((()) (foo . #t) (bar . #t))))
(pass-if "long option `bar', long option `foo', no args"
(equal? (test3 "prg" "--bar" "--foo")
'((()) (foo . #t) (bar . #t))))
(pass-if "long option bar, long option foo, no args"
(test "--bar --foo"
'((()) (foo . #t) (bar . #t))))
(pass-if "long option with equals and space"
(test "--foo= test"
'((() "test") (foo . #t))))
(pass-if "long option with equals and space, not allowed a value"
(A-TEST "--foo= test"
'((foo (value #f)))
'((() "test") (foo . #t))))
(pass-if "--="
(test "--="
'((() "--="))))
)
@ -133,16 +176,16 @@
(bar)))
'foo #f)))
(pass-if "option-ref `--foo 4'"
(pass-if "option-ref --foo 4"
(test4 "4" "--foo" "4"))
(pass-if "option-ref `-f 4'"
(pass-if "option-ref -f 4"
(test4 "4" "-f" "4"))
(pass-if "option-ref `-f4'"
(pass-if "option-ref -f4"
(test4 "4" "-f4"))
(pass-if "option-ref `--foo=4'"
(pass-if "option-ref --foo=4"
(test4 "4" "--foo=4"))
)
@ -227,11 +270,12 @@
(with-test-prefix "apples-blimps-catalexis example"
(define spec '((apples (single-char #\a))
(blimps (single-char #\b) (value #t))
(catalexis (single-char #\c) (value #t))))
(define (test8 . args)
(equal? (sort (getopt-long (cons "foo" args)
'((apples (single-char #\a))
(blimps (single-char #\b) (value #t))
(catalexis (single-char #\c) (value #t))))
(equal? (sort (getopt-long (cons "foo" args) spec)
(lambda (a b)
(cond ((null? (car a)) #t)
((null? (car b)) #f)
@ -246,9 +290,38 @@
(pass-if "normal 2" (test8 "-ab" "bang" "-c" "couth"))
(pass-if "normal 3" (test8 "-ac" "couth" "-b" "bang"))
(pass-if-fatal-exception "bad ordering causes missing option"
exception:option-must-have-arg
(test8 "-abc" "couth" "bang"))
;;;; Dale Mellor 2020-04-14
;;;;
;;;; I disagree with this test: to my mind 'c' is 'b's argument, and
;;;; the other two arguments are non-options which get passed
;;;; through; there should not be an exception.
;; (pass-if-fatal-exception "bad ordering causes missing option"
;; exception:option-must-have-arg
;; (test8 "-abc" "couth" "bang"))
(pass-if "clumped options with trailing mandatory value"
(A-TEST "-abc couth bang"
spec
'((() "couth" "bang") (apples . #t) (blimps . "c"))))
(pass-if "clumped options with trailing optional value"
(A-TEST "-abc couth bang"
'((apples (single-char #\a))
(blimps (single-char #\b)
(value optional)))
'((() "couth" "bang") (apples . #t) (blimps . "c"))))
(pass-if "clumped options with trailing optional value"
(A-TEST "-abc couth bang"
'((apples (single-char #\a))
(blimps (single-char #\b)
(value optional))
(catalexis (single-char #\c)
(value #t)))
'((() "bang")
(apples . #t) (blimps . #t) (catalexis . "couth"))))
)
@ -291,12 +364,165 @@
(with-test-prefix "stop-at-first-non-option"
(pass-if "guile-tools compile example"
(equal? (getopt-long '("guile-tools" "compile" "-Wformat" "eval.scm" "-o" "eval.go")
(equal? (getopt-long '("guile-tools" "compile" "-Wformat"
"eval.scm" "-o" "eval.go")
'((help (single-char #\h))
(version (single-char #\v)))
#:stop-at-first-non-option #t)
'((() "compile" "-Wformat" "eval.scm" "-o" "eval.go"))))
(pass-if "stop after option"
(equal? (getopt-long '("foo" "-a" "3" "4" "-b" "4")
'((about (single-char #\a) (value #t))
(breathe (single-char #\b) (value #t)))
#:stop-at-first-non-option #t)
'((() "4" "-b" "4") (about . "3"))))
)
(with-test-prefix "stop at end-of-options marker"
(define* (test args expectation #:key stop-at-first-non-option)
(A-TEST args
'((abby) (ben) (charles))
expectation
#:stop-at-first-non-option stop-at-first-non-option))
(pass-if "stop at start" (test "-- --abby" '((() "--abby"))))
(pass-if "stop in middle" (test "--abby dave -- --ben"
'((() "dave" "--ben") (abby . #t))))
(pass-if "stop at end" (test "--abby dave --ben --"
'((() "dave") (abby . #t) (ben . #t))))
(pass-if "marker before first non-option"
(test "--abby -- --ben dave --charles"
'((() "--ben" "dave" "--charles") (abby . #t))
#:stop-at-first-non-option #t))
(pass-if "first non-option before marker"
(test "--abby dave --ben -- --charles"
'((() "dave" "--ben" "--" "--charles") (abby . #t))
#:stop-at-first-non-option #t))
(pass-if "double end marker"
(test "--abby -- -- --ben"
'((() "--" "--ben") (abby . #t))))
(pass-if "separated double end markers"
(test "--abby dave -- --ben -- --charles"
'((() "dave" "--ben" "--" "--charles")
(abby . #t))))
)
(with-test-prefix "negative numbers for option values"
(define (test args expectation)
(A-TEST args
`((arthur (single-char #\a) (value optional)
(predicate ,string->number))
(beth (single-char #\b) (value #t)
(predicate ,string->number))
(charles (single-char #\c) (value optional))
(dave (single-char #\d) (value #t)))
expectation))
(pass-if "predicated --optional=-1"
(test "--arthur=-1" '((()) (arthur . "-1"))))
(pass-if "predicated -o-1"
(test "-a-1" '((()) (arthur . "-1"))))
(pass-if "predicated --optional -1"
(test "--arthur -1" '((()) (arthur . "-1"))))
(pass-if "predicated -o -1"
(test "-a -1" '((()) (arthur . "-1"))))
(pass-if "predicated --mandatory=-1"
(test "--beth=-1" '((()) (beth . "-1"))))
(pass-if "predicated -m-1"
(test "-b-1" '((()) (beth . "-1"))))
(pass-if "predicated --mandatory -1"
(test "--beth -1" '((()) (beth . "-1"))))
(pass-if "predicated -m -1"
(test "-b -1" '((()) (beth . "-1"))))
(pass-if "non-predicated --optional=-1"
(test "--charles=-1" '((()) (charles . "-1"))))
(pass-if "non-predicated -o-1"
(test "-c-1" '((()) (charles . "-1"))))
(pass-if-fatal-exception "non-predicated --optional -1"
exception:no-such-option
(test "--charles -1" '((()) (charles . "-1"))))
(pass-if-fatal-exception "non-predicated -o -1"
exception:no-such-option
(test "-c -1" '((()) (charles . "-1"))))
(pass-if "non-predicated --mandatory=-1"
(test "--dave=-1" '((()) (dave . "-1"))))
(pass-if "non-predicated -m-1"
(test "-d-1" '((()) (dave . "-1"))))
(pass-if "non-predicated --mandatory -1"
(test "--dave -1" '((()) (dave . "-1"))))
(pass-if "non-predicated -m -1"
(test "-d -1" '((()) (dave . "-1"))))
)
(with-test-prefix "mcron backwards compatibility"
(define (test args expectation)
(A-TEST args
`((daemon (single-char #\d) (value #f))
(stdin (single-char #\i) (value #t)
(predicate ,(λ (in) (or (string=? in "guile")
(string=? in "vixie")))))
(schedule (single-char #\s) (value optional)
(predicate ,(λ (in) (or (eq? in #t)
(and (string? in)
(string->number in))))))
(help (single-char #\?))
(version (single-char #\V)))
expectation))
(pass-if "-s8" (test "-s8 file" '((() "file") (schedule . "8"))))
(pass-if "-s 8" (test "-s 8 file" '((() "file") (schedule . "8"))))
(pass-if "-s file"
(test "-s file" '((() "file") (schedule . #t))))
(pass-if "-sd file"
(test "-sd file" '((() "file") (daemon . #t) (schedule . #t))))
(pass-if "-ds file"
(test "-ds file" '((() "file") (daemon . #t) (schedule . #t))))
(pass-if "--schedule=8" (test "--schedule=8 file"
'((() "file") (schedule . "8"))))
(pass-if "--schedule 8" (test "--schedule 8 file"
'((() "file") (schedule . "8"))))
(pass-if "-ds8" (test "-ds8 file"
'((() "file") (daemon . #t) (schedule . "8"))))
(pass-if "-ds 8" (test "-ds 8 file"
'((() "file") (daemon . #t) (schedule . "8"))))
)
;;; getopt-long.test ends here