#lang racket/base (require gregor racket/file racket/format racket/list racket/path racket/port racket/set racket/string racket/system "env-support.rkt" "path-output.rkt") (provide coreutils-head coreutils-tail coreutils-wc coreutils-sort coreutils-uniq coreutils-cut coreutils-tee coreutils-tr coreutils-basename coreutils-dirname coreutils-realpath coreutils-readlink coreutils-stat coreutils-du coreutils-df coreutils-mktemp coreutils-printenv coreutils-env coreutils-date) (define (arg->string arg) (cond [(path? arg) (path->string arg)] [(symbol? arg) (symbol->string arg)] [(string? arg) arg] [else (~a arg)])) (define (arg->path arg) (string->path (arg->string arg))) (define (read-lines-from-arguments args) (if (null? args) (port->lines (current-input-port)) (append-map (λ (arg) (call-with-input-file* (arg->path arg) port->lines)) args))) (define (parse-n-option who args default) (cond [(and (pair? args) (member (arg->string (car args)) '("-n" "--lines"))) (unless (pair? (cdr args)) (raise-arguments-error who "missing line count" "arguments" args)) (define n (string->number (arg->string (cadr args)))) (unless (and (exact-integer? n) (>= n 0)) (raise-arguments-error who "line count must be a non-negative integer" "count" (cadr args))) (values n (cddr args))] [else (values default args)])) (define (coreutils-head . args) (define-values (n files) (parse-n-option 'head args 10)) (define lines (read-lines-from-arguments files)) (for ([line (in-list (take lines (min n (length lines))))]) (displayln line))) (define (coreutils-tail . args) (define-values (n files) (parse-n-option 'tail args 10)) (define lines (read-lines-from-arguments files)) (define count (length lines)) (for ([line (in-list (drop lines (max 0 (- count n))))]) (displayln line))) (define (bytes-for-arguments args) (if (null? args) (port->bytes (current-input-port)) (apply bytes-append (for/list ([arg (in-list args)]) (file->bytes (arg->path arg)))))) (define (coreutils-wc . args) (define lines? #f) (define words? #f) (define bytes? #f) (define files '()) (for ([arg (in-list args)]) (define text (arg->string arg)) (cond [(string=? text "-l") (set! lines? #t)] [(string=? text "-w") (set! words? #t)] [(or (string=? text "-c") (string=? text "--bytes")) (set! bytes? #t)] [(string-prefix? text "-") (raise-arguments-error 'wc "unsupported option" "option" text)] [else (set! files (append files (list arg)))])) (when (not (or lines? words? bytes?)) (set! lines? #t) (set! words? #t) (set! bytes? #t)) (define bs (bytes-for-arguments files)) (define text (bytes->string/utf-8 bs #\?)) (define results (filter (λ (x) x) (list (if lines? (number->string (length (regexp-match* #rx"\n" text))) #f) (if words? (number->string (length (regexp-match* #px"\\S+" text))) #f) (if bytes? (number->string (bytes-length bs)) #f)))) (displayln (string-join results " "))) (define (coreutils-sort . args) (define reverse? #f) (define numeric? #f) (define files '()) (for ([arg (in-list args)]) (define text (arg->string arg)) (cond [(or (string=? text "-r") (string=? text "--reverse")) (set! reverse? #t)] [(or (string=? text "-n") (string=? text "--numeric-sort")) (set! numeric? #t)] [(string-prefix? text "-") (raise-arguments-error 'sort "unsupported option" "option" text)] [else (set! files (append files (list arg)))])) (define (less? a b) (if numeric? (< (or (string->number a) +inf.0) (or (string->number b) +inf.0)) (stringstring arg)) (cond [(or (string=? text "-c") (string=? text "--count")) (set! count? #t)] [(string-prefix? text "-") (raise-arguments-error 'uniq "unsupported option" "option" text)] [else (set! files (append files (list arg)))])) (for ([entry (in-list (run-lengths (read-lines-from-arguments files)))]) (if count? (printf "~a ~a\n" (~a (cdr entry) #:min-width 7 #:align 'right) (car entry)) (displayln (car entry))))) (define (parse-field-spec text) (define n (string->number text)) (unless (and (exact-positive-integer? n)) (raise-arguments-error 'cut "only one positive field number is supported" "field" text)) n) (define (coreutils-cut . args) (define delimiter "\t") (define field #f) (define files '()) (let loop ([rest args]) (cond [(null? rest) (void)] [(member (arg->string (car rest)) '("-d" "--delimiter")) (unless (pair? (cdr rest)) (raise-arguments-error 'cut "missing delimiter" "arguments" args)) (set! delimiter (arg->string (cadr rest))) (loop (cddr rest))] [(member (arg->string (car rest)) '("-f" "--fields")) (unless (pair? (cdr rest)) (raise-arguments-error 'cut "missing field" "arguments" args)) (set! field (parse-field-spec (arg->string (cadr rest)))) (loop (cddr rest))] [(string-prefix? (arg->string (car rest)) "-") (raise-arguments-error 'cut "unsupported option" "option" (car rest))] [else (set! files (append files (list (car rest)))) (loop (cdr rest))])) (unless field (raise-arguments-error 'cut "expected -f FIELD" "arguments" args)) (for ([line (in-list (read-lines-from-arguments files))]) (define parts (string-split line delimiter #:trim? #f #:repeat? #f)) (when (<= field (length parts)) (displayln (list-ref parts (sub1 field)))))) (define (coreutils-tee . args) (define append? #f) (define files '()) (for ([arg (in-list args)]) (define text (arg->string arg)) (cond [(or (string=? text "-a") (string=? text "--append")) (set! append? #t)] [(string-prefix? text "-") (raise-arguments-error 'tee "unsupported option" "option" text)] [else (set! files (append files (list (arg->path arg))))])) (define outputs (for/list ([path (in-list files)]) (open-output-file path #:exists (if append? 'append 'truncate/replace)))) (dynamic-wind void (λ () (let loop () (define bs (read-bytes 4096 (current-input-port))) (unless (eof-object? bs) (write-bytes bs (current-output-port)) (for ([out (in-list outputs)]) (write-bytes bs out)) (loop)))) (λ () (for ([out (in-list outputs)]) (close-output-port out))))) (define (expand-character-set text) (define chars (string->list text)) (let loop ([rest chars] [result '()]) (cond [(null? rest) (reverse result)] [(and (pair? (cdr rest)) (pair? (cddr rest)) (char=? (cadr rest) #\-) (char<=? (car rest) (caddr rest))) (define start (char->integer (car rest))) (define end (char->integer (caddr rest))) (define expanded (for/list ([code (in-range start (add1 end))]) (integer->char code))) (loop (cdddr rest) (append (reverse expanded) result))] [else (loop (cdr rest) (cons (car rest) result))]))) (define (squeeze-characters text chars) (define squeeze? (list->seteq chars)) (list->string (let loop ([rest (string->list text)] [previous #f] [result '()]) (cond [(null? rest) (reverse result)] [else (define ch (car rest)) (if (and previous (char=? ch previous) (set-member? squeeze? ch)) (loop (cdr rest) previous result) (loop (cdr rest) ch (cons ch result)))])))) (define (translate-characters text from to) (when (null? to) (raise-arguments-error 'tr "SET2 must not be empty" "SET2" to)) (define last-to (last to)) (define mapping (for/hash ([ch (in-list from)] [i (in-naturals)]) (values ch (if (< i (length to)) (list-ref to i) last-to)))) (list->string (for/list ([ch (in-string text)]) (hash-ref mapping ch ch)))) (define (delete-characters text chars) (define delete-set (list->seteq chars)) (list->string (for/list ([ch (in-string text)] #:unless (set-member? delete-set ch)) ch))) (define (coreutils-tr . args) (define delete? #f) (define squeeze? #f) (define sets '()) (for ([arg (in-list args)]) (define text (arg->string arg)) (cond [(or (string=? text "-d") (string=? text "--delete")) (set! delete? #t)] [(or (string=? text "-s") (string=? text "--squeeze-repeats")) (set! squeeze? #t)] [(or (string=? text "-ds") (string=? text "-sd")) (set! delete? #t) (set! squeeze? #t)] [(string-prefix? text "-") (raise-arguments-error 'tr "unsupported option" "option" text)] [else (set! sets (append sets (list text)))])) (define text (port->string (current-input-port))) (cond [(and delete? squeeze?) (unless (= (length sets) 2) (raise-arguments-error 'tr "expected SET1 SET2 with -ds" "arguments" args)) (define deleted (delete-characters text (expand-character-set (first sets)))) (display (squeeze-characters deleted (expand-character-set (second sets))))] [delete? (unless (= (length sets) 1) (raise-arguments-error 'tr "expected one character set with -d" "arguments" args)) (display (delete-characters text (expand-character-set (first sets))))] [(and squeeze? (= (length sets) 1)) (display (squeeze-characters text (expand-character-set (first sets))))] [else (unless (= (length sets) 2) (raise-arguments-error 'tr "expected SET1 SET2" "arguments" args)) (define to (expand-character-set (second sets))) (define translated (translate-characters text (expand-character-set (first sets)) to)) (display (if squeeze? (squeeze-characters translated to) translated))])) (define (coreutils-basename . args) (unless (= (length args) 1) (raise-arguments-error 'basename "expected one path" "arguments" args)) (define name (file-name-from-path (arg->path (car args)))) (displayln (if name (path->string name) ""))) (define (coreutils-dirname . args) (unless (= (length args) 1) (raise-arguments-error 'dirname "expected one path" "arguments" args)) (define-values (base name dir?) (split-path (arg->path (car args)))) (displayln (cond [(path? base) (path->rash-string base)] [(eq? base 'relative) "."] [else (path->rash-string (arg->path (car args)))]))) (define (coreutils-realpath . args) (unless (= (length args) 1) (raise-arguments-error 'realpath "expected one path" "arguments" args)) (displayln (path->rash-string (simplify-path (path->complete-path (arg->path (car args))) #t)))) (define (coreutils-readlink . args) (unless (= (length args) 1) (raise-arguments-error 'readlink "expected one path" "arguments" args)) (define path (arg->path (car args))) (unless (link-exists? path) (raise-arguments-error 'readlink "path is not a symbolic link" "path" path)) (displayln (path->rash-string (resolve-path path)))) (define (coreutils-stat . args) (when (null? args) (raise-arguments-error 'stat "expected at least one path" "arguments" args)) (for ([arg (in-list args)]) (define path (arg->path arg)) (unless (or (file-exists? path) (directory-exists? path) (link-exists? path)) (raise-arguments-error 'stat "path does not exist" "path" path)) (printf "Path: ~a\n" (path->rash-string (path->complete-path path))) (printf "Type: ~a\n" (cond [(link-exists? path) "link"] [(directory-exists? path) "directory"] [else "file"])) (when (file-exists? path) (printf "Size: ~a\n" (file-size path))) (printf "Modified: ~a\n" (file-or-directory-modify-seconds path)))) (define (path-size path) (cond [(file-exists? path) (file-size path)] [(directory-exists? path) (for/sum ([entry (in-list (directory-list path #:build? #t))]) (path-size entry))] [else 0])) (define (human-size n) (cond [(>= n (* 1024 1024 1024)) (format "~aG" (~r (/ n (* 1024.0 1024 1024)) #:precision '(= 1)))] [(>= n (* 1024 1024)) (format "~aM" (~r (/ n (* 1024.0 1024)) #:precision '(= 1)))] [(>= n 1024) (format "~aK" (~r (/ n 1024.0) #:precision '(= 1)))] [else (format "~aB" n)])) (define (coreutils-du . args) (define human? #f) (define paths '()) (for ([arg (in-list args)]) (define text (arg->string arg)) (cond [(or (string=? text "-h") (string=? text "--human-readable")) (set! human? #t)] [(string-prefix? text "-") (raise-arguments-error 'du "unsupported option" "option" text)] [else (set! paths (append paths (list (arg->path arg))))])) (when (null? paths) (set! paths (list (current-directory)))) (for ([path (in-list paths)]) (define size (path-size path)) (printf "~a\t~a\n" (if human? (human-size size) size) (path->rash-string path)))) (define (coreutils-df . args) (unless (null? args) (raise-arguments-error 'df "this portable implementation does not accept arguments yet" "arguments" args)) (displayln "Filesystem") (for ([root (in-list (filesystem-root-list))]) (displayln (path->rash-string root)))) (define (coreutils-mktemp . args) (define directory? #f) (define template "tmp~a") (for ([arg (in-list args)]) (define text (arg->string arg)) (cond [(or (string=? text "-d") (string=? text "--directory")) (set! directory? #t)] [(string-prefix? text "-") (raise-arguments-error 'mktemp "unsupported option" "option" text)] [else (set! template text)])) (define result (make-temporary-file template (if directory? 'directory #f))) (displayln (path->rash-string result))) (define (environment-name->string name) (bytes->string/locale name)) (define (environment-value->string value) (bytes->string/locale value)) (define (write-environment env) (define names (sort (environment-variables-names env) stringstring)) (for ([name (in-list names)]) (define value (environment-variables-ref env name)) (when value (printf "~a=~a\n" (environment-name->string name) (environment-value->string value))))) (define (coreutils-printenv . args) (define env (current-environment-variables)) (if (null? args) (write-environment env) (for ([arg (in-list args)]) (define name (string->bytes/locale (arg->string arg))) (define value (environment-variables-ref env name)) (when value (displayln (environment-value->string value)))))) (define assignment-rx #px"^([^=]+)=(.*)$") (define (value->environment-string value) (cond [(string? value) value] [(path? value) (path->string value)] [(symbol? value) (symbol->string value)] [(bytes? value) (bytes->string/locale value)] [else (~a value)])) (define (split-environment-arguments args) (let loop ([rest args] [assignments '()]) (cond [(null? rest) (values (reverse assignments) '())] [(environment-assignment? (car rest)) (define assignment (car rest)) (loop (cdr rest) (cons (cons (environment-assignment-name assignment) (environment-assignment-value assignment)) assignments))] [else (define text (arg->string (car rest))) (define match (regexp-match assignment-rx text)) (if match (loop (cdr rest) (cons (cons (list-ref match 1) (list-ref match 2)) assignments)) (values (reverse assignments) rest))]))) (define (run-external-environment-command command) (define executable (find-executable-path (arg->string (car command)))) (unless executable (raise-arguments-error 'env "command was not found on PATH" "command" (car command))) (define ok? (apply system* executable (map arg->string (cdr command)))) (unless ok? (raise-arguments-error 'env "command returned a non-zero exit status" "command" (car command)))) (define (coreutils-env . args) (define-values (assignments command) (split-environment-arguments args)) (define env (environment-variables-copy (current-environment-variables))) (for ([assignment (in-list assignments)]) (environment-variables-set! env (string->bytes/locale (car assignment)) (string->bytes/locale (value->environment-string (cdr assignment))))) (if (null? command) (write-environment env) (parameterize ([current-environment-variables env]) (define runner (current-coreutils-command-runner)) (define handled? (and runner (runner command))) (unless handled? (run-external-environment-command command))))) (define (coreutils-date . args) (define utc? #f) (define iso? #f) (define timezone #f) (define pattern #f) (let loop ([rest args]) (cond [(null? rest) (void)] [(member (arg->string (car rest)) '("-u" "--utc")) (set! utc? #t) (loop (cdr rest))] [(member (arg->string (car rest)) '("-I" "--iso" "--iso-8601")) (set! iso? #t) (loop (cdr rest))] [(string=? (arg->string (car rest)) "--tz") (unless (pair? (cdr rest)) (raise-arguments-error 'date "missing time zone" "arguments" args)) (set! timezone (arg->string (cadr rest))) (loop (cddr rest))] [(string=? (arg->string (car rest)) "--format") (unless (pair? (cdr rest)) (raise-arguments-error 'date "missing CLDR format" "arguments" args)) (set! pattern (arg->string (cadr rest))) (loop (cddr rest))] [else (raise-arguments-error 'date "unsupported argument" "argument" (car rest))])) (define m (cond [utc? (now/moment/utc)] [timezone (now/moment #:tz timezone)] [else (now/moment)])) (displayln (cond [pattern (~t m pattern)] [iso? (moment->iso8601 m)] [else (~t m "EEE MMM dd HH:mm:ss xxx yyyy")])))