#lang racket
(require (prefix-in srfi-1: srfi/1)
"spec-ast.rkt"
"usage.rkt")
(struct exn:fc:match exn:fail:contract (usage-info) #:transparent)
(struct exn:fc:bad-command exn:fc:match () #:transparent)
(struct exn:fc:bad-switch exn:fc:match () #:transparent)
(struct exn:fc:help-request exn:fc:match () #:transparent)
(struct exn:fc:missing-argument exn:fc:match () #:transparent)
(struct exn:fc:extra-argument exn:fc:match () #:transparent)
(struct exn:fc:duplicate-switch exn:fc:match () #:transparent)
(struct exn:fc:multiple-group-members exn:fc:match () #:transparent)
(provide/contract
[match-spec (spec? (listof string?) . -> . any)])
(provide
(struct-out exn:fc:match)
(struct-out exn:fc:bad-command)
(struct-out exn:fc:bad-switch)
(struct-out exn:fc:help-request)
(struct-out exn:fc:missing-argument)
(struct-out exn:fc:extra-argument)
(struct-out exn:fc:duplicate-switch)
(struct-out exn:fc:multiple-group-members))
(define match-spec
(lambda (spec args)
(match args
[(list) (print-usage spec)]
[(or (list "help") (list "--help"))
(raise (exn:fc:help-request
""
(current-continuation-marks)
(format-usage spec)))]
[(cons (? switch?) _)
(raise (exn:fc:bad-command
(format "~a: expected command~n"
(spec-program spec))
(current-continuation-marks)
(format-usage spec)))]
[(list-rest "help" command _)
(let ([cmd-spec (find-command spec command)])
(if cmd-spec
(raise (exn:fc:help-request
""
(current-continuation-marks)
(format-command-usage cmd-spec)))
(raise (exn:fc:bad-command
(format "Unknown command ~a" command)
(current-continuation-marks)
(format-usage spec)))))]
[(list-rest cmd-name args)
(let ([cmd-spec (find-command spec cmd-name)])
(if cmd-spec
(match-command cmd-spec args)
(raise (exn:fc:bad-command
(format "Unknown command ~a" cmd-name)
(current-continuation-marks)
(format-usage spec)))))])))
(struct match-state (args cmd-spec formal-positionals
actual-positionals rest-positionals
keyword-table groups)
#:transparent)
(define match-command
(lambda (cmd-spec args)
(match-main-loop
(match-state args cmd-spec (command-positional-args cmd-spec)
null null (hasheq) (hasheq)))))
(define match-main-loop
(lambda (state)
(match state
[(match-state (list) cmd (list) actual-positionals (list) keyword-table _)
(invoke-cmd-fn (command-function cmd)
actual-positionals
null
keyword-table)]
[(match-state (list) cmd (list) actual-positionals rest-positionals
keyword-table _)
(cond
[(not (command-rest-arg cmd))
(raise (exn:fc:extra-argument
(format "Extra positional argument~a: ~a"
(plural-check rest-positionals)
(format-list rest-positionals))
(current-continuation-marks)
(format-command-usage cmd)))]
[else (invoke-cmd-fn (command-function cmd)
actual-positionals
rest-positionals
keyword-table)])]
[(match-state (list) cmd formal-positionals _ _ _ _)
(raise-missing-argument formal-positionals cmd)]
[(match-state (list-rest (? switch? s) args-rest) _ _ _ _ _ _)
(match-command-switch
s
(struct-copy match-state state [args args-rest]))]
[(match-state (list-rest args-first args-rest) _ (list) _ rest-pos _ _)
(match-main-loop
(struct-copy match-state state
[args args-rest]
[rest-positionals (snoc args-first rest-pos)]))]
[(match-state (list-rest args-first args-rest) _ formals actuals _ _ _)
(match-main-loop
(struct-copy match-state state
[args args-rest]
[formal-positionals (cdr formals)]
[actual-positionals (snoc args-first actuals)]))])))
(define match-command-switch
(lambda (switch state)
(match (find-switch switch (command-flags (match-state-cmd-spec state)))
[(optional-flag _ kwd _ formal-args)
(match-optional-switch state switch kwd formal-args)]
[(multi-flag _ kwd _ formal-args)
(match-multi-switch state switch kwd formal-args)]
[(cons (optional-flag _ kwd _ formal-args) g)
(match-group-switch state switch kwd formal-args g)]
[#f
(raise (exn:fc:bad-switch
(format "Unknown switch: ~a~n" switch)
(current-continuation-marks)
(format-command-usage (match-state-cmd-spec state))))])))
(define match-optional-switch
(lambda (state switch kwd formal-args)
(let ([args (match-state-args state)]
[num-formals (length formal-args)]
[kwd-table (match-state-keyword-table state)])
(cond
[(hash-has-key? kwd-table kwd)
(raise (exn:fc:duplicate-switch
(format "Switch ~a may only appear once~n" switch)
(current-continuation-marks)
(format-command-usage (match-state-cmd-spec state))))]
[(< (length args) num-formals)
(let ([missing-args (list-tail formal-args (length args))])
(raise-missing-argument missing-args (match-state-cmd-spec state)))]
[else
(let-values ([(actuals rest) (srfi-1:split-at args num-formals)])
(match-main-loop
(struct-copy match-state state
[args rest]
[keyword-table (hash-set kwd-table kwd actuals)])))]))))
(define match-multi-switch
(lambda (state switch kwd formal-args)
(let ([args (match-state-args state)]
[num-formals (length formal-args)])
(cond
[(< (length args) num-formals)
(let ([missing-args (list-tail formal-args (length args))])
(raise-missing-argument missing-args (match-state-cmd-spec state)))]
[else
(let-values ([(actuals rest)
(srfi-1:split-at args num-formals)])
(match-main-loop
(struct-copy match-state state
[args rest]
[keyword-table
(hash-update
(match-state-keyword-table state)
kwd
(lambda (args)
(map snoc actuals args))
(map (lambda (x) null) actuals))])))]))))
(define match-group-switch
(lambda (state switch kwd formal-args g)
(let ([args (match-state-args state)]
[num-formals (length formal-args)])
(cond
[(hash-ref (match-state-groups state) g #f) =>
(lambda (prev-switch)
(raise (exn:fc:multiple-group-members
(format "Switches ~a and ~a may not both appear.~n"
prev-switch
switch)
(current-continuation-marks)
(format-command-usage (match-state-cmd-spec state)))))]
[(< (length args) num-formals)
(let ([missing-args (list-tail formal-args (length args))])
(raise-missing-argument missing-args (match-state-cmd-spec state)))]
[else
(let-values ([(actuals rest)
(srfi-1:split-at args num-formals)])
(match-main-loop
(struct-copy match-state state
[args rest]
[keyword-table
(hash-set (match-state-keyword-table state)
kwd
actuals)]
[groups
(hash-set (match-state-groups state) g switch)])))]))))
(define invoke-cmd-fn
(lambda (fn actuals rest-args kwd-table)
(let* ([unsorted-kwd-args (hash-map kwd-table cons)]
[sorted-kwd-args (sort unsorted-kwd-args keyword<? #:key car)])
(keyword-apply fn
(map car sorted-kwd-args)
(map cdr sorted-kwd-args)
(append actuals rest-args)))))
(define find-switch
(lambda (sw flags)
(match flags
[(list) #f]
[(cons (and flag (or (optional-flag switches _ _ _)
(multi-flag switches _ _ _))) rest)
(if (set-member? switches sw)
flag
(find-switch sw rest))]
[(cons (and grp (group flags)) rest)
(let ([spec (find-switch sw flags)])
(if spec
(cons spec grp)
(find-switch sw rest)))])))
(define snoc
(lambda (x xs)
(cond
[(null? xs) (cons x null)]
[else (cons (car xs) (snoc x (cdr xs)))])))
(define plural-check
(lambda (lst #:singular (singular "") #:plural (plural "s"))
(match lst
[(list _) singular]
[else plural])))
(define format-list
(lambda (l)
(string-join (map (lambda (x) (format "~a" x)) l) " ")))
(define raise-missing-argument
(lambda (missing-formals cmd-spec)
(raise (exn:fc:missing-argument
(format "Missing required argument~a: ~a"
(plural-check missing-formals)
(format-list missing-formals))
(current-continuation-marks)
(format-command-usage cmd-spec)))))