diff --git a/collects/planet/planet.ss b/collects/planet/planet.ss index ffcb943d46..4dc1bbdd96 100644 --- a/collects/planet/planet.ss +++ b/collects/planet/planet.ss @@ -9,8 +9,7 @@ PLANNED FEATURES: that if it's a tool it won't start w/ DrScheme, etc) 4. Examine and alter linkage table |# - (require mzlib/cmdline - mzlib/string + (require mzlib/string mzlib/file (only mzlib/list sort) net/url @@ -18,109 +17,89 @@ PLANNED FEATURES: "config.ss" "private/planet-shared.ss" + "private/command.ss" "resolver.ss" ;; the code I need should be pulled out into a common library "util.ss") - (define actions '()) + (define erase? (make-parameter #f)) + (define displayer (make-parameter (λ () (show-installed-packages)))) (define (start) (make-directory* (PLANET-DIR)) (make-directory* (CACHE-DIR)) - (command-line - "planet" - (current-command-line-arguments) - (once-each - (("--force") - "" - "Used in conjunction with --create-package; force a package to be" - "created even its info.ss file contains errors." - (force-package-building? #t))) - (once-any - (("-f" "--file") - plt-file owner maj min - "" - "Install local file as though it had been downloaded from" - "the planet server. The installed package has path" - " (planet ( ))" - (set! actions (cons (lambda () (install-plt-file plt-file owner maj min)) actions))) - (("-c" "--create-archive") - path - "" - "Create a PLaneT archive in the current directory" - "whose contents are the directory " - (set! actions (cons (lambda () (do-archive path)) actions))) - (("-i" "--install") - owner pkg maj min - "" - "Download and install the package" - " (require (planet \"file.ss\" ( )))" - "would install" - (set! actions (cons (lambda () (download/install owner pkg maj min)) actions))) - (("-d" "--download") - owner pkg maj min - "" - "Download the given package file without installing it" - (set! actions (cons (lambda () (download/no-install owner pkg maj min)) actions))) - (("-r" "--remove") - owner pkg maj min - "" - "Remove the specified package from the local cache" - (set! actions (cons (lambda () (remove owner pkg maj min)) actions))) - (("-e" "--erase") - owner pkg maj min - "" - "Erase the specified package, removing it as -r does and " - "eliminating the package's distribution file from the " - "uninstalled-package cache" - (set! actions (cons (lambda () (erase owner pkg maj min)) actions))) - (("-U" "--unlink-all") - "" - "Clear the linkage table, unlinking all packages and allowing upgrades" - (set! actions (cons unlink-all actions))) - (("-p" "--packages") - "" - "List the packages installed in the local cache" - (set! actions (cons show-installed-packages actions))) - (("-l" "--linkage") - "" - "List the current linkage table" - (set! actions (cons show-linkage actions))) - - (("-a" "--associate") - owner pkg maj min path - "" - "Create a development link between the specified package specifier " - "and the specified directory name" - (set! actions (cons (lambda () (add-hard-link-cmd owner pkg maj min path)) actions))) - - (("-u" "--unassociate") - owner pkg maj min - "" - "Remove any development link associated with the specified package" - (set! actions (cons (lambda () (remove-hard-link-cmd owner pkg maj min)) actions))) - - (("--url") - owner pkg maj min - "Get a URL for the given package" - (set! actions (cons (lambda () (get-download-url owner pkg maj min)) actions))) - - (("--unpack") - plt-file target - "Unpack the contents of the given package into the given directory without installing" - (set! actions (cons (lambda () (do-unpack plt-file target)) actions))) + (svn-style-command-line + #:program "planet" + #:argv (current-command-line-arguments) + "PLT Scheme PLaneT command-line tool. Provides commands to help you manipulate your local planet cache." + ["create" "create a PLaneT archive from a directory" + '("" "Create a PLaneT archive in the current directory whose contents are the directory .") + #:once-each + [("-f" "--force") "force a package to be created even its info.ss file contains errors." + (force-package-building? #t)] + #:args (path) + (do-archive path)] + ["install" "download and install a given package" + '("" "Download and install the package that" + " (require (planet \"file.ss\" ( )))" + "would install") + #:args (owner pkg maj min) + (download/install owner pkg maj min)] + ["remove" "remove the specified package from the local cache" + '("" "Remove the specified package from the local cache, optionally also removing its distribution file") + #:once-each + [("-e" "--erase") "also remove the package's distribution file from the uninstalled-package cache" + (erase? #t)] + #:args (owner pkg maj min) + ((if (erase?) erase remove) owner pkg maj min)] + ["show" "list the packages installed in the local cache" + '("" "List the packages installed in the local cache") + #:once-any + [("-p" "--packages") "show packages only (default)" (displayer show-installed-packages)] + [("-l" "--linkage") "show linkage table only" (displayer show-linkage)] + [("-a" "--all") "show packages and linkage" (displayer (λ () (show-installed-packages) (newline) (show-linkage)))] + #:args () + ((displayer))] + ["clearlinks" "clear the linkage table, unlinking all packages and allowing upgrades" + '("" "Clear the linkage table, unlinking all packages and allowing upgrades") + #:args () + (unlink-all)] + ["fileinject" "install a local file to the planet cache" + '("" "Install local file into the planet cache as though it had been downloaded from the planet server. The installed package has path" + " (planet ( ))") + #:args (owner plt-file maj min) + (install-plt-file plt-file owner maj min)] + ["link" "create a development link" + '("" "Create a development link between the specified package specifier and the specified directory name") + #:args (owner pkg maj min path) + (add-hard-link-cmd owner pkg maj min path)] + ["unlink" "remove development link associated with the given package" + '("" "Remove development link associated with the given package") + #:args (owner pkg maj min) + (remove-hard-link-cmd owner pkg maj min)] + ["fetch" "download a package file without installing it" + '("" "Download the given package file without installing it") + #:args (owner pkg maj min) + (download/no-install owner pkg maj min)] + ["url" "get a URL for the given package" + '("" "Get a URL for the given package." + "This is not necessary for normal use of planet, but may be helpful in some circumstances for retrieving packages.") + #:args (owner pkg maj min) + (get-download-url owner pkg maj min)] + ["open" "unpack the contents of the given package" + '("" "Unpack the contents of the given package into the given directory without installing." + "This command is not necessary for normal use of planet. It is intended to allow you to inspect package contents" + "offline without needing to install the package.") + #:args (plt-file target) + (do-unpack plt-file target)] - ;; unimplemented so far: - #;(("-u" "--unlink") + ;; unimplemented so far: + #;(("-u" "--unlink") module "Remove all linkage the given module has, forcing it to upgrade" ...))) - - (cond - ; make showing the installed packages the default thing to do. - [(null? actions) (show-installed-packages)] - [else (for-each (lambda (f) (f)) actions)])) + ;; ============================================================ ;; FEATURE IMPLEMENTATIONS diff --git a/collects/planet/private/command.ss b/collects/planet/private/command.ss new file mode 100644 index 0000000000..26d417147b --- /dev/null +++ b/collects/planet/private/command.ss @@ -0,0 +1,107 @@ +#lang scheme/base +(require "prefix-dispatcher.ss" + scheme/cmdline + (for-syntax scheme/base)) + +(provide svn-style-command-line) + +;; implements an "svn-style" command-line interface as a wrapper around scheme/cmdline. At the moment, +;; it is light on error-checking and makes choices that are somewhat specific to the PLaneT commandline +;; tool, thus its inclusion in planet/private rather than somewhere more visible. The idea is that you +;; write +#| + +(svn-style-command-line + #:program + #:argv + + [ + ... arguments just like the command-line macro takes, until ... + #:args formals + body-expr] ...) +|# + +;; This macro turns that into a command-line type of thing that implements +;; program command1 ... args ... +;; program command2 ... args ... +;; etc. +;; It provides two nonobvious features: +;; 1. It automatically includes a help feature that prints out all available subcommands +;; 2. It automatically lets users use any unambiguous prefix of any command. +;; This means that no command name may be a prefix of any other command name, because it +;; would mean there was no way to unambiguously name the shorter one. + +(define-syntax (svn-style-command-line stx) + (syntax-case stx () + [(_ #:program prog + #:argv args + general-description + [name description long-description body ... #:args formals final-expr] ...) + (with-syntax ([(n ...) (generate-temporaries #'(name ...))]) + #'(let* ([p prog] + [a args] + [n name] ... + [argslist (cond + [(list? a) a] + [(vector? a) (vector->list a)] + [else (error 'command "expected a vector or list for arguments, received ~e" a)])] + [help (λ () (display-help-message p general-description '((name description) ...)))]) + (let-values ([(the-command remainder) + (if (null? argslist) + (values "help" '()) + (values (car argslist) (cdr argslist)))]) + (prefix-case the-command + [n + (command-line + #:program (format "~a ~a" p n) + #:argv remainder + body ... + #:handlers + (λ (_ . formals) final-expr) + (pimap symbol->string 'formals) + (λ (help-string) + (for-each (λ (l) (display l) (newline)) long-description) + (newline) + (display "Usage:\n") + (display help-string) + (exit)))] ... + ["help" (help)] + [else (help)]))))])) + + +;; display-help-message : string (listof (list string string)) -> void +;; prints out the help message +(define (display-help-message prog general-description commands) + (let ([maxlen (apply max (map (λ (p) (string-length (car p))) commands))]) + (begin + (printf "Usage: ~a [option ...] \n" prog) + (printf "[note: you can name a subcommand by typing any unambiguous prefix of it.]\n\n") + (display general-description) + (newline) + (newline) + (display "For help on a particular subcommand, type 'planet --help'\n") + (display "Available subcommands:\n") + (for-each (λ (command) + (let ([padded-name (pad (car command) maxlen)] + [desc (cadr command)]) + (printf " ~a ~a\n" padded-name desc))) + commands)))) + + +;; ---------------------------------------- +;; utility + +;; pad : string nat[>= string-length str] -> string +;; pads the given string up to the given length. +(define (pad str n) + (let* ([l (string-length str)] + [extra (build-string (- n l) (λ (n) #\space))]) + (string-append str extra))) + +;; pimap : (A -> B) improper-listof A -> improper-listof B +(define (pimap f pil) + (cond + [(null? pil) '()] + [(pair? pil) (cons (pimap f (car pil)) + (pimap f (cdr pil)))] + [else (f pil)])) \ No newline at end of file diff --git a/collects/planet/private/prefix-dispatcher.ss b/collects/planet/private/prefix-dispatcher.ss new file mode 100644 index 0000000000..023454f9eb --- /dev/null +++ b/collects/planet/private/prefix-dispatcher.ss @@ -0,0 +1,89 @@ +#lang scheme/base + +(require (for-syntax scheme/base)) +(provide (all-defined-out)) + +;; ============================================================ +;; PREFIX DISPATCHER +;; Code to determine the entry specified by an arbitrary +;; (unambiguous) prefix of a set of possible entries + +(define-struct (exn:prefix-dispatcher exn:fail) ()) +(define-struct (exn:unknown-command exn:prefix-dispatcher) (entry)) +(define-struct (exn:ambiguous-command exn:prefix-dispatcher) (possibilities)) + +;; get-prefix-dispatcher : (listof (list string A)) -> string -> A +;; gets the +(define (get-prefix-dispatcher options) + ;; implementation strategy is dumb regexp-filter. It is possible to do a trie or something fancy like that, + ;; but it would cost more to build than it would be worth, and we're only expecting lists of a few items anyway + (let ([pre/full (get-prefix-and-suffix (map car options))]) + (when pre/full + (error 'get-prefix-dispatcher "No element may be a strict prefix of any other element; given ~a and ~a" + (car pre/full) + (cadr pre/full)))) + + (λ (target) + (let* ([re (format "^~a" (regexp-quote target))] + [matches (filter (λ (x) (regexp-match re (car x))) options)]) + (cond + [(length=? matches 1) (cadr (car matches))] + [(null? matches) + (raise (make-exn:unknown-command (format "Unknown command: ~a" target) + (current-continuation-marks) + target))] + [else + (raise (make-exn:ambiguous-command (format "Ambiguous command: ~a" target) + (current-continuation-marks) + (map car matches)))])))) +;; length=? : list nat -> boolean +;; determines if the given list has the given length. Running time is proportional +;; to the shorter of the magnitude of the number or the actual length of the list +(define (length=? lst len) + (cond + [(and (null? lst) (zero? len)) #t] + [(null? lst) #f] + [(zero? len) #f] + [else (length=? (cdr lst) (sub1 len))])) + +;; get-prefix-and-suffix : (listof string) -> (list string string) | #f +;; returns a pair of strings in the given list such that the first string is a prefix of the second, +;; or #f if no such pair exists +(define (get-prefix-and-suffix strs) + (cond + [(null? strs) #f] + [else + (sorted-nelist-contains-prefix? (sort strs string (list string string) | #f +;; given a lexicographically-sorted, nonempty list of strings, returns either +;; two strings from the list such that the first is a prefix of the second, or #f if +;; no such pair exists +(define (sorted-nelist-contains-prefix? nel) + (cond + [(null? (cdr nel)) #f] + [(prefix? (car nel) (cadr nel)) + (list (car nel) (cadr nel))] + [else (sorted-nelist-contains-prefix? (cdr nel))])) + +;; prefix? : string string -> boolean +;; determins if s1 is a prefix of s2 +(define (prefix? s1 s2) + (and (<= (string-length s1) (string-length s2)) + (string=? s1 (substring s2 0 (string-length s1))))) + + +(define-syntax (prefix-case stx) + (syntax-case stx (else) + [(_ elt + [option result] ... + [else alternative]) + #'(with-handlers ([exn:prefix-dispatcher? (λ (e) alternative)]) + (((get-prefix-dispatcher (list (list option (λ () result)) ...)) + elt)))] + [(_ elt [option result] ...) + #'(let ([e elt]) (prefix-case e [option result] ... [else (error 'prefix-case "element ~e was not a prefix" e)]))])) + + + +