Aded extended commands and editor

This commit is contained in:
2026-08-20 23:48:49 +02:00
parent 72bf39a27b
commit 4324da1467
12 changed files with 1616 additions and 261 deletions
+23
View File
@@ -1,3 +1,26 @@
0.2.14
- Add `rackedit` as a runtime dependency.
- Add the Rash `edit` command, implemented by `coreutils-edit`.
- Use RackEdit's `rkdt` as the default editor and map `edit --wait FILE` to `#:wait? #t`.
- Add `current-coreutils-editor` so callers can dynamically select another editor procedure.
- Rework the Scribble manual around the commands available in Rash, with each command documenting its internal Racket implementation.
- Add non-GUI regression tests for editor dispatch and `--wait` translation.
0.2.12
- Add a portable `time` command for rash-coreutils commands and executables on PATH.
- Report real, combined CPU, and Racket GC time to stderr so command stdout remains pipeline-safe.
- Include completed subprocess CPU time in the reported CPU measurement.
- Add package-index-safe regression coverage for command timing and stderr separation.
0.2.11
- Make head and tail scan fixed-size byte blocks instead of materializing complete lines.
- Make tail scan seekable files backwards so memory use is independent of file and line size.
- Spool tail input from stdin or multiple files to a temporary file instead of retaining lines in memory.
- Add regression coverage for very long lines in head and tail.
0.2.9
- Make package-build tests independent of the directory from which raco test is invoked.
+42 -9
View File
@@ -1,31 +1,63 @@
# rash-coreutils
Portable Unix-style core utilities for Rash, implemented in Racket.
Portable Unix-style commands for Rash, implemented in Racket.
Version 0.2.9 includes text/pipeline tools (`head`, `tail`, `wc`, `sort`, `uniq`,
`cut`, `tee`, `tr`), path and filesystem helpers (`basename`, `dirname`,
`realpath`, `readlink`, `stat`, `du`, `df`, `mktemp`), environment commands
(`printenv`, `env`), and `date` backed by Gregor.
Version 0.2.14 adds the `edit` command backed by the `rackedit` package and
reworks the manual around the commands that are available directly in Rash.
The manual now describes, per command, which internal `coreutils-*` Racket
procedure implements it.
Rash remains responsible for shell syntax such as globbing, pipelines and
redirection. `rash-coreutils` additionally accepts Racket regular expressions as
path selectors.
## Editing
`edit` opens one file using the editor configured by `current-coreutils-editor`.
The default editor is RackEdit's `rkdt` procedure.
```text
edit notes.rkt
edit --wait notes.rkt
```
`--wait` is passed to RackEdit as `#:wait? #t`. In ordinary Racket code a
different editor can be selected dynamically by parameterizing
`current-coreutils-editor` with a procedure that accepts a file path and the
`#:wait?` keyword.
## Command timing
`time` executes one rash-coreutils command or executable and writes timing data
to standard error. Command output therefore remains suitable for pipelines and
redirection. The portable measurements are elapsed (`real`) time, combined CPU
time (`cpu`) for Racket and completed subprocesses, and Racket garbage-collection
time (`gc`).
```text
time head -n 100 large.txt
time raco test tests/coreutils.rkt
```
## Racket tooling
`rash-coreutils` also exposes the `raco` command from the active Racket installation, without requiring `raco` to be on PATH.
`rash-coreutils` also exposes the `raco` command from the active Racket
installation, without requiring `raco` to be on PATH.
```text
raco setup rash-coreutils
raco pkg show
```
In Racket expression mode, use `coreutils-raco`, for example `(coreutils-raco '(setup rash-coreutils))`. The name `raco` is reserved for the Rash command binding.
In Racket expression mode, use `coreutils-raco`, for example
`(coreutils-raco '(setup rash-coreutils))`. The name `raco` is reserved for the
Rash command binding.
## Windows paths
Commands that print paths use forward slashes on Windows. This keeps their textual output directly reusable in Rash line mode, where a backslash is an escape character. Internally, Racket path values remain native paths.
Commands that print paths use forward slashes on Windows. This keeps their
textual output directly reusable in Rash line mode, where a backslash is an
escape character. Internally, Racket path values remain native paths.
```text
pwd
@@ -35,4 +67,5 @@ mktemp
C:/Users/hans/AppData/Local/Temp/tmp123
```
A native Racket path value can still be passed directly, for example `ls (values (find-system-path 'temp-dir))`.
A native Racket path value can still be passed directly, for example
`ls (values (find-system-path 'temp-dir))`.
+3 -3
View File
@@ -1,7 +1,7 @@
#lang info
(define pkg-authors '(hnmdijkema))
(define version "0.1.6")
(define version "0.2.14")
(define license 'MIT)
(define collection "rash-coreutils")
(define pkg-desc
@@ -11,10 +11,10 @@
'(("scrbl/rash-coreutils.scrbl" () (library 0))))
(define deps
'("base" "rash"))
'("base" "rash" "gregor-lib" "rackedit"))
(define build-deps
'("racket-doc"
"rackunit-lib"
"scribble-lib"
"rash"))
"linea"))
+2 -2
View File
@@ -1,7 +1,7 @@
#lang info
(define pkg-authors '(hnmdijkema))
(define version "0.2.9")
(define version "0.2.14")
(define license 'MIT)
(define collection "rash-coreutils")
(define pkg-desc
@@ -11,7 +11,7 @@
'(("scrbl/rash-coreutils.scrbl" () (library 0))))
(define deps
'("base" "rash" "gregor-lib"))
'("base" "rash" "gregor-lib" "rackedit"))
(define build-deps
'("racket-doc"
+2
View File
@@ -4,6 +4,7 @@
"private/extended.rkt"
"private/commands.rkt"
"private/racket-tools.rkt"
"private/editor.rkt"
"private/dispatcher.rkt"
"private/help.rkt"
"private/aliases.rkt")
@@ -12,6 +13,7 @@
(all-from-out "private/extended.rkt")
(all-from-out "private/commands.rkt")
(all-from-out "private/racket-tools.rkt")
(all-from-out "private/editor.rkt")
(all-from-out "private/dispatcher.rkt")
(all-from-out "private/help.rkt")
(all-from-out "private/aliases.rkt"))
+5 -1
View File
@@ -9,7 +9,7 @@
(provide pwd ls mkdir rmdir rm cp mv cat touch echo which
head tail wc sort uniq cut tee tr basename dirname realpath readlink
stat du df mktemp printenv env date help raco)
stat du df mktemp printenv env date time edit help raco)
(define (make-command-procedure name)
(λ args
@@ -45,6 +45,8 @@
(define printenv-command (make-command-procedure 'printenv))
(define env-command (make-command-procedure 'env))
(define date-command (make-command-procedure 'date))
(define time-command (make-command-procedure 'time))
(define edit-command (make-command-procedure 'edit))
(define raco-command coreutils-raco)
(define (help-command . args)
@@ -131,5 +133,7 @@
(define-pipeline-alias printenv (make-coreutils-alias #'printenv-command))
(define-pipeline-alias env (make-env-alias #'env-command))
(define-pipeline-alias date (make-coreutils-alias #'date-command))
(define-pipeline-alias time (make-coreutils-alias #'time-command))
(define-pipeline-alias edit (make-coreutils-alias #'edit-command))
(define-pipeline-alias help (make-coreutils-alias #'help-command))
(define-pipeline-alias raco (make-coreutils-alias #'raco-command))
+4 -1
View File
@@ -2,7 +2,8 @@
(require "coreutils.rkt"
"extended.rkt"
"racket-tools.rkt")
"racket-tools.rkt"
"editor.rkt")
(provide (struct-out coreutils-command)
coreutils-commands
@@ -42,6 +43,8 @@
(coreutils-command 'printenv coreutils-printenv 'rash-coreutils-printenv)
(coreutils-command 'env coreutils-env 'rash-coreutils-env)
(coreutils-command 'date coreutils-date 'rash-coreutils-date)
(coreutils-command 'time coreutils-time 'rash-coreutils-time)
(coreutils-command 'edit coreutils-edit 'rash-coreutils-edit)
(coreutils-command 'raco coreutils-raco 'rash-coreutils-raco)))
(define (find-coreutils-command name)
+66
View File
@@ -0,0 +1,66 @@
#lang racket/base
(require racket/format
racket/path
racket/string
rackedit)
(provide coreutils-edit
current-coreutils-editor)
(define current-coreutils-editor
(make-parameter
rkdt
(λ (editor)
(unless (procedure? editor)
(raise-argument-error 'current-coreutils-editor "procedure?" editor))
editor)))
(define (arg->path arg)
(cond
[(path? arg) arg]
[(string? arg) (string->path arg)]
[(symbol? arg) (string->path (symbol->string arg))]
[else
(raise-argument-error
'edit
"path, string, or symbol"
arg)]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Open one file in the editor configured for rash-coreutils.
; pre : args contains exactly one filename and optionally --wait.
; post : The configured editor has been invoked once. With --wait, the
; command does not return until the editor procedure returns.
; result : (void).
; internals:
; current-coreutils-editor defaults to rackedit's rkdt procedure.
; The --wait flag is translated to rkdt's #:wait? keyword.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (coreutils-edit . args)
(define wait? #f)
(define reversed-paths '())
(for ([arg (in-list args)])
(define text (~a arg))
(cond
[(string=? text "--wait")
(set! wait? #t)]
[(string-prefix? text "-")
(raise-arguments-error 'edit
"unsupported option"
"option" text)]
[else
(set! reversed-paths (cons (arg->path arg) reversed-paths))]))
(define paths (reverse reversed-paths))
(unless (or (= (length paths) 1) (= (length paths) 0))
(raise-arguments-error 'edit
"expected exactly zero or one file"
"arguments" args))
(if (null? paths)
((current-coreutils-editor) #:wait? wait?)
((current-coreutils-editor) (car paths) #:wait? wait?))
(void))
+827 -133
View File
File diff suppressed because it is too large Load Diff
+552 -103
View File
@@ -1,6 +1,7 @@
#lang scribble/manual
@(require (for-label racket/base
(only-in rackedit rkdt)
(only-in rash-coreutils
coreutils-pwd coreutils-ls coreutils-mkdir coreutils-rmdir
coreutils-rm coreutils-cp coreutils-mv coreutils-cat
@@ -10,140 +11,506 @@
coreutils-dirname coreutils-realpath coreutils-readlink
coreutils-stat coreutils-du coreutils-df coreutils-mktemp
coreutils-printenv coreutils-env coreutils-date
raco
coreutils-raco)))
coreutils-time coreutils-edit current-coreutils-editor
coreutils-help dispatch-coreutils-command
raco coreutils-raco)))
@title{rash-coreutils}
@author["Hans Dijkema / hans@dijkewijk.nl"]
@defmodule[rash-coreutils]
@bold{rash-coreutils} provides platform-independent Unix-style shell commands
for Rash. The commands are implemented in Racket, so common shell utilities do
not depend on @tt{cmd.exe}, PowerShell, or Unix executables being installed.
@bold{rash-coreutils} provides platform-independent Unix-style commands for
Rash. The user-facing API is the set of commands that can be written directly
in Rash line mode. Most commands are implemented by ordinary Racket procedures,
so they do not depend on @tt{cmd.exe}, PowerShell, or Unix utility executables.
Rash remains responsible for shell syntax such as pipelines, globbing, tilde
expansion, variable expansion, and redirection. @bold{rash-coreutils} supplies
the commands themselves plus Racket-specific additions such as regular-expression
path selectors.
expansion, variable expansion, and redirection. The command aliases provided by
this package preserve those Rash semantics and then dispatch to the internal
Racket implementation.
@section{Commands}
@section{How a Rash command is executed}
The package provides @tt{pwd}, @tt{ls}, @tt{mkdir}, @tt{rmdir}, @tt{rm},
@tt{cp}, @tt{mv}, @tt{cat}, @tt{touch}, @tt{echo}, @tt{which}, @tt{head},
@tt{tail}, @tt{wc}, @tt{sort}, @tt{uniq}, @tt{cut}, @tt{tee}, @tt{tr},
@tt{basename}, @tt{dirname}, @tt{realpath}, @tt{readlink}, @tt{stat}, @tt{du},
@tt{df}, @tt{mktemp}, @tt{printenv}, @tt{env}, @tt{date}, @tt{raco}, and @tt{help}.
For a normal command such as
@verbatim{
ls -l *.rkt
}
Rash first processes its line syntax and pipeline semantics. The @tt{ls} alias
then calls @racket[dispatch-coreutils-command], which looks up the registered
command and invokes @racket[coreutils-ls]. Arguments that are Racket regular
expressions are expanded by the rash-coreutils dispatcher before the internal
procedure is called.
This means the shell command is the primary interface, while procedures such as
@racket[coreutils-ls] are the implementation API and are also available to
ordinary Racket code.
The option set is deliberately smaller than GNU coreutils. Unsupported options
raise an error instead of silently approximating another command's behavior.
raise an error instead of silently approximating behavior that is not
implemented.
@section{Racket procedures}
Each shell command has an explicitly named Racket procedure. All procedures
write to the normal current ports so Rash pipeline and redirection semantics
continue to work.
@section{Filesystem commands}
@index["rash-coreutils-pwd"]
@defproc[(coreutils-pwd) void?]{Writes the current directory.}
@subsection{@tt{pwd}}
@verbatim{
pwd
}
Writes the current directory. The command accepts no arguments.
Internally, @tt{pwd} dispatches to @racket[coreutils-pwd].
@defproc[(coreutils-pwd) void?]{Writes @racket[current-directory] using a Rash-friendly textual path.}
@index["rash-coreutils-ls"]
@defproc[(coreutils-ls [arg any/c] ...) void?]{Lists paths. Supports @tt{-a}, @tt{-l}, and their long forms.}
@subsection{@tt{ls}}
@verbatim{
ls
ls -l
ls -a
ls -la
ls *.rkt
ls #px"^info[0-9]+[.]rkt$"
}
Lists files and directories. @tt{-a}/@tt{--all} includes hidden names and
@tt{-l}/@tt{--long} writes the portable long form used by this package. Rash
performs normal glob expansion. A Racket regular-expression value is an
additional rash-coreutils path selector and is matched against names in the
current directory.
Internally, @tt{ls} dispatches to @racket[coreutils-ls].
@defproc[(coreutils-ls [arg any/c] ...) void?]{Implements the listing after Rash and dispatcher argument handling.}
@index["rash-coreutils-mkdir"]
@defproc[(coreutils-mkdir [arg any/c] ...) void?]{Creates directories. Supports @tt{-p}.}
@subsection{@tt{mkdir}}
@verbatim{
mkdir work
mkdir -p build/output
}
Creates one or more directories. @tt{-p}/@tt{--parents} creates missing parent
directories.
Internally, @tt{mkdir} dispatches to @racket[coreutils-mkdir].
@defproc[(coreutils-mkdir [arg any/c] ...) void?]{Creates the requested directories.}
@index["rash-coreutils-rmdir"]
@subsection{@tt{rmdir}}
@verbatim{
rmdir empty-directory
}
Removes one or more empty directories. No options are currently supported.
Internally, @tt{rmdir} dispatches to @racket[coreutils-rmdir].
@defproc[(coreutils-rmdir [arg any/c] ...) void?]{Removes empty directories.}
@index["rash-coreutils-rm"]
@defproc[(coreutils-rm [arg any/c] ...) void?]{Removes paths. Supports recursive and force options.}
@subsection{@tt{rm}}
@verbatim{
rm file.txt
rm -r directory
rm -rf generated
}
Removes files or links. Directories require @tt{-r}, @tt{-R}, or
@tt{--recursive}. @tt{-f}/@tt{--force} suppresses an error for a missing path.
The combined forms @tt{-rf}, @tt{-fr}, @tt{-Rf}, and @tt{-fR} are supported.
Internally, @tt{rm} dispatches to @racket[coreutils-rm].
@defproc[(coreutils-rm [arg any/c] ...) void?]{Removes the requested paths.}
@index["rash-coreutils-cp"]
@defproc[(coreutils-cp [arg any/c] ...) void?]{Copies a source to a destination. Directory copies require @tt{-r}.}
@subsection{@tt{cp}}
@verbatim{
cp source.txt copy.txt
cp -r source-directory copy-directory
}
Copies exactly one source to one destination. Directory copies require
@tt{-r}, @tt{-R}, or @tt{--recursive}.
Internally, @tt{cp} dispatches to @racket[coreutils-cp].
@defproc[(coreutils-cp [arg any/c] ...) void?]{Copies a file or, in recursive mode, a directory tree.}
@index["rash-coreutils-mv"]
@subsection{@tt{mv}}
@verbatim{
mv old-name.txt new-name.txt
}
Moves or renames exactly one source path to one destination path.
Internally, @tt{mv} dispatches to @racket[coreutils-mv].
@defproc[(coreutils-mv [source any/c] [destination any/c]) void?]{Moves or renames a path.}
@index["rash-coreutils-cat"]
@defproc[(coreutils-cat [arg any/c] ...) void?]{Copies files, or standard input, to standard output.}
@index["rash-coreutils-touch"]
@defproc[(coreutils-touch [arg any/c] ...) void?]{Updates modification times or creates empty files.}
@subsection{@tt{touch}}
@index["rash-coreutils-echo"]
@defproc[(coreutils-echo [arg any/c] ...) void?]{Writes arguments separated by spaces.}
@verbatim{
touch notes.txt
}
@index["rash-coreutils-which"]
@defproc[(coreutils-which [arg any/c] ...) void?]{Finds executables using Racket's executable search.}
Creates missing files and updates the modification time of existing files. No
options are currently supported.
@index["rash-coreutils-head"]
@defproc[(coreutils-head [arg any/c] ...) void?]{Writes the first lines of input. Supports @tt{-n}.}
Internally, @tt{touch} dispatches to @racket[coreutils-touch].
@index["rash-coreutils-tail"]
@defproc[(coreutils-tail [arg any/c] ...) void?]{Writes the last lines of input. Supports @tt{-n}.}
@index["rash-coreutils-wc"]
@defproc[(coreutils-wc [arg any/c] ...) void?]{Counts lines, words, and bytes. Supports @tt{-l}, @tt{-w}, and @tt{-c}.}
@index["rash-coreutils-sort"]
@defproc[(coreutils-sort [arg any/c] ...) void?]{Sorts lines. Supports @tt{-r} and @tt{-n}.}
@index["rash-coreutils-uniq"]
@defproc[(coreutils-uniq [arg any/c] ...) void?]{Collapses adjacent duplicate lines. Supports @tt{-c}.}
@index["rash-coreutils-cut"]
@defproc[(coreutils-cut [arg any/c] ...) void?]{Selects one delimited field with @tt{-d} and @tt{-f}.}
@index["rash-coreutils-tee"]
@defproc[(coreutils-tee [arg any/c] ...) void?]{Copies standard input to standard output and files. Supports @tt{-a}.}
@index["rash-coreutils-tr"]
@defproc[(coreutils-tr [arg any/c] ...) void?]{Translates characters from standard input. Character ranges such as @tt{a-z}, @tt{A-Z}, and @tt{0-9} are expanded. @tt{-d} deletes characters and @tt{-s} squeezes repeated characters.}
@defproc[(coreutils-touch [arg any/c] ...) void?]{Touches one or more files.}
@index["rash-coreutils-basename"]
@subsection{@tt{basename}}
@verbatim{
basename a/b/file.txt
}
Writes the final component of one path.
Internally, @tt{basename} dispatches to @racket[coreutils-basename].
@defproc[(coreutils-basename [path any/c]) void?]{Writes the final path component.}
@index["rash-coreutils-dirname"]
@subsection{@tt{dirname}}
@verbatim{
dirname a/b/file.txt
}
Writes the directory portion of one path.
Internally, @tt{dirname} dispatches to @racket[coreutils-dirname].
@defproc[(coreutils-dirname [path any/c]) void?]{Writes the directory portion of a path.}
@index["rash-coreutils-realpath"]
@defproc[(coreutils-realpath [path any/c]) void?]{Writes a complete simplified path.}
@subsection{@tt{realpath}}
@verbatim{
realpath relative/path
}
Writes a complete simplified path with filesystem links resolved where the
current filesystem supports that resolution.
Internally, @tt{realpath} dispatches to @racket[coreutils-realpath].
@defproc[(coreutils-realpath [path any/c]) void?]{Writes the complete simplified path.}
@index["rash-coreutils-readlink"]
@defproc[(coreutils-readlink [path any/c]) void?]{Resolves a symbolic link.}
@subsection{@tt{readlink}}
@verbatim{
readlink link-name
}
Writes the target of one symbolic link and reports an error when the supplied
path is not a link.
Internally, @tt{readlink} dispatches to @racket[coreutils-readlink].
@defproc[(coreutils-readlink [path any/c]) void?]{Writes the resolved symbolic-link target.}
@index["rash-coreutils-stat"]
@subsection{@tt{stat}}
@verbatim{
stat file.txt
stat file.txt directory
}
Writes portable type, size, and modification information for one or more paths.
Internally, @tt{stat} dispatches to @racket[coreutils-stat].
@defproc[(coreutils-stat [path any/c] ...) void?]{Writes basic portable path metadata.}
@index["rash-coreutils-du"]
@defproc[(coreutils-du [arg any/c] ...) void?]{Calculates recursive file size. Supports @tt{-h}.}
@index["rash-coreutils-df"]
@defproc[(coreutils-df) void?]{Lists filesystem roots. Racket has no portable API for total and free filesystem capacity, so this initial implementation deliberately does not invent platform-specific subprocess fallbacks.}
@index["rash-coreutils-mktemp"]
@defproc[(coreutils-mktemp [arg any/c] ...) void?]{Creates a temporary file, or a directory with @tt{-d}, and writes its path.}
@index["rash-coreutils-printenv"]
@defproc[(coreutils-printenv [name any/c] ...) void?]{Writes environment variables.}
@index["rash-coreutils-env"]
@defproc[(coreutils-env [arg any/c] ...) void?]{Creates a copied environment, applies leading @tt{NAME=value} assignments, and either writes that environment or runs the remaining command in it. Racket values are converted to environment strings.}
The Rash form can mix shell-style assignment names with Racket expressions:
@subsection{@tt{du}}
@verbatim{
du
du -h directory
}
Writes the accumulated file size of each requested path. With no path, the
current directory is used. @tt{-h}/@tt{--human-readable} formats the size using
binary K, M, and G units. Symbolic links are not followed.
Internally, @tt{du} dispatches to @racket[coreutils-du].
@defproc[(coreutils-du [arg any/c] ...) void?]{Calculates recursive file size.}
@index["rash-coreutils-df"]
@subsection{@tt{df}}
@verbatim{
df
}
Lists filesystem roots visible to Racket. This portable implementation does not
invent platform-specific subprocess fallbacks for total and free capacity.
Internally, @tt{df} dispatches to @racket[coreutils-df].
@defproc[(coreutils-df) void?]{Lists filesystem roots.}
@index["rash-coreutils-mktemp"]
@subsection{@tt{mktemp}}
@verbatim{
mktemp
mktemp -d
mktemp "rash-coreutils-~a"
}
Creates a unique temporary file and writes its path. @tt{-d}/@tt{--directory}
creates a directory instead. An optional non-option argument is used as the
Racket temporary-file template.
Internally, @tt{mktemp} dispatches to @racket[coreutils-mktemp].
@defproc[(coreutils-mktemp [arg any/c] ...) void?]{Creates a temporary file or directory and writes its path.}
@section{Text and pipeline commands}
@index["rash-coreutils-cat"]
@subsection{@tt{cat}}
@verbatim{
cat file.txt
cat first.txt second.txt
producer | cat
}
Copies files to standard output. With no file arguments, it copies standard
input. This makes the implementation usable in Rash pipelines without special
pipeline code.
Internally, @tt{cat} dispatches to @racket[coreutils-cat].
@defproc[(coreutils-cat [arg any/c] ...) void?]{Copies file or standard-input bytes to the current output port.}
@index["rash-coreutils-echo"]
@subsection{@tt{echo}}
@verbatim{
echo hello world
}
Writes its arguments separated by spaces and terminates the result with a
newline.
Internally, @tt{echo} dispatches to @racket[coreutils-echo].
@defproc[(coreutils-echo [arg any/c] ...) void?]{Writes the textual representation of the arguments.}
@index["rash-coreutils-head"]
@subsection{@tt{head}}
@verbatim{
head file.txt
head -n 20 file.txt
}
Writes the first lines of files or standard input. @tt{-n}/@tt{--lines}
selects a non-negative line count; the default is 10. Input is scanned in fixed
size byte blocks so memory use does not grow with file size or an individual
line length.
Internally, @tt{head} dispatches to @racket[coreutils-head].
@defproc[(coreutils-head [arg any/c] ...) void?]{Writes the requested first lines.}
@index["rash-coreutils-tail"]
@subsection{@tt{tail}}
@verbatim{
tail file.txt
tail -n 20 file.txt
}
Writes the last lines of files or standard input. @tt{-n}/@tt{--lines} selects
a non-negative line count; the default is 10. Seekable files are scanned
backwards in fixed-size byte blocks. Input that cannot be scanned backwards is
spooled to a temporary file, keeping memory use independent of input size and
line length.
Internally, @tt{tail} dispatches to @racket[coreutils-tail].
@defproc[(coreutils-tail [arg any/c] ...) void?]{Writes the requested last lines.}
@index["rash-coreutils-wc"]
@subsection{@tt{wc}}
@verbatim{
wc file.txt
wc -l file.txt
wc -w file.txt
wc -c file.txt
}
Counts lines, words, and bytes. @tt{-l} selects lines, @tt{-w} words, and
@tt{-c}/@tt{--bytes} bytes. With no count options all three values are written.
Internally, @tt{wc} dispatches to @racket[coreutils-wc].
@defproc[(coreutils-wc [arg any/c] ...) void?]{Counts input using bounded streaming buffers.}
@index["rash-coreutils-sort"]
@subsection{@tt{sort}}
@verbatim{
sort file.txt
sort -r file.txt
sort -n numbers.txt
}
Sorts input lines. @tt{-r}/@tt{--reverse} reverses the ordering and
@tt{-n}/@tt{--numeric-sort} uses numeric keys. Large input is divided into
bounded in-memory runs and merged through temporary files.
Internally, @tt{sort} dispatches to @racket[coreutils-sort].
@defproc[(coreutils-sort [arg any/c] ...) void?]{Sorts lines without retaining the complete input in memory.}
@index["rash-coreutils-uniq"]
@subsection{@tt{uniq}}
@verbatim{
uniq sorted.txt
uniq -c sorted.txt
}
Collapses adjacent duplicate lines. @tt{-c}/@tt{--count} prefixes each output
line with its run count. As with the Unix command, input should already be
grouped when non-adjacent equal lines must also be collapsed.
Internally, @tt{uniq} dispatches to @racket[coreutils-uniq].
@defproc[(coreutils-uniq [arg any/c] ...) void?]{Collapses adjacent duplicate lines.}
@index["rash-coreutils-cut"]
@subsection{@tt{cut}}
@verbatim{
cut -f 2 data.txt
cut -d ";" -f 3 data.txt
}
Writes one delimited field from each input line. @tt{-f}/@tt{--fields} is
required and currently accepts one positive field number. @tt{-d}/@tt{--delimiter}
changes the delimiter from the default tab.
Internally, @tt{cut} dispatches to @racket[coreutils-cut].
@defproc[(coreutils-cut [arg any/c] ...) void?]{Selects one field from each input line.}
@index["rash-coreutils-tee"]
@subsection{@tt{tee}}
@verbatim{
producer | tee output.txt
producer | tee -a output.txt
}
Copies standard input to standard output and to each named file.
@tt{-a}/@tt{--append} appends instead of replacing existing file content.
Internally, @tt{tee} dispatches to @racket[coreutils-tee].
@defproc[(coreutils-tee [arg any/c] ...) void?]{Copies the current input stream to the current output and files.}
@index["rash-coreutils-tr"]
@subsection{@tt{tr}}
@verbatim{
echo abc | tr a-z A-Z
echo a1b2c3 | tr -d 0-9
echo "a b" | tr -s " "
}
Translates characters from standard input. Simple ranges such as @tt{a-z},
@tt{A-Z}, and @tt{0-9} are expanded. @tt{-d}/@tt{--delete} deletes selected
characters and @tt{-s}/@tt{--squeeze-repeats} squeezes repeated selected
characters. @tt{-ds} and @tt{-sd} are also accepted.
Internally, @tt{tr} dispatches to @racket[coreutils-tr].
@defproc[(coreutils-tr [arg any/c] ...) void?]{Translates, deletes, or squeezes characters from the current input port.}
@section{Environment, time, and executable commands}
@index["rash-coreutils-which"]
@subsection{@tt{which}}
@verbatim{
which git
which racket
}
Finds commands using Racket's executable search and writes the resolved path.
Internally, @tt{which} dispatches to @racket[coreutils-which].
@defproc[(coreutils-which [arg any/c] ...) void?]{Finds one or more executables on @tt{PATH}.}
@index["rash-coreutils-printenv"]
@subsection{@tt{printenv}}
@verbatim{
printenv
printenv PATH
}
Writes the complete current environment or the values of selected variables.
A missing selected variable produces no output.
Internally, @tt{printenv} dispatches to @racket[coreutils-printenv].
@defproc[(coreutils-printenv [name any/c] ...) void?]{Writes environment-variable values from @racket[current-environment-variables].}
@index["rash-coreutils-env"]
@subsection{@tt{env}}
@verbatim{
env NAME=value printenv NAME
(define x 42)
env ANSWER=(values x) printenv ANSWER
}
A parenthesized expression in Rash line mode is ordinary Racket code. Therefore
the value after an empty @tt{NAME=} token can be a number, symbol, path, string,
or another printable Racket value.
Creates a copy of the current environment, applies leading @tt{NAME=value}
assignments, and either writes the resulting environment or runs the remaining
command in it. A parenthesized expression in Rash line mode is ordinary Racket
code, so the value following an empty @tt{NAME=} token can be a Racket value.
Registered rash-coreutils commands are tried before an executable on @tt{PATH}.
Internally, the @tt{env} Rash alias first preserves Racket expressions in
assignment values and then dispatches to @racket[coreutils-env].
@defproc[(coreutils-env [arg any/c] ...) void?]{Applies temporary environment assignments and optionally runs a command.}
@index["rash-coreutils-date"]
@defproc[(coreutils-date [arg any/c] ...) void?]{Writes the current date and time using Gregor. Supports @tt{-u}/@tt{--utc}, @tt{-I}/@tt{--iso}, @tt{--tz ZONE}, and @tt{--format CLDR-PATTERN}. The format pattern follows Gregor's CLDR syntax rather than pretending to implement every GNU @tt{date} format escape.}
@subsection{@tt{date}}
@verbatim{
date
@@ -152,47 +519,90 @@ date --utc --iso
date --tz Europe/Amsterdam --format "yyyy-MM-dd HH:mm:ss"
}
@index["rash-coreutils-raco"]
@defproc[(raco [arg any/c] ...) void?]{Runs @tt{raco} from the current Racket installation. The executable is resolved from the active Racket installation before PATH is considered, so it also works on Windows when @tt{raco.exe} is not on PATH. Arguments may be strings, symbols, paths, numbers, or nested lists. With no arguments, the underlying @tt{raco} program is invoked without a subcommand.}
Writes the current date and time using Gregor. @tt{-u}/@tt{--utc} selects UTC,
@tt{-I}/@tt{--iso}/@tt{--iso-8601} selects ISO output, @tt{--tz ZONE} selects a
time zone, and @tt{--format CLDR-PATTERN} uses Gregor's CLDR formatting syntax.
@defproc[(coreutils-raco [arg any/c] ...) void?]{Explicit implementation name for @racket[raco].}
Internally, @tt{date} dispatches to @racket[coreutils-date].
@defproc[(coreutils-date [arg any/c] ...) void?]{Writes the current date and time in the requested representation.}
@index["rash-coreutils-time"]
@subsection{@tt{time}}
@verbatim{
time head -n 100 large.txt
time raco test tests/coreutils.rkt
}
Executes one registered rash-coreutils command or an executable on @tt{PATH} and
writes timing data to standard error. The portable measurements are elapsed
@tt{real} time, combined @tt{cpu} time for Racket and completed subprocesses,
and Racket @tt{gc} time. Standard output from the timed command remains usable
in pipelines and redirections.
Internally, @tt{time} dispatches to @racket[coreutils-time].
@defproc[(coreutils-time [command any/c] [arg any/c] ...) void?]{Runs and measures a command.}
@index["rash-coreutils-raco"]
@subsection{@tt{raco}}
@verbatim{
raco setup rash-coreutils
raco pkg show
}
The same @racket[raco] binding is a normal Racket procedure, so expression-mode code can use @racket[(raco '(setup rash-coreutils))]. The explicit implementation name @racket[coreutils-raco] is also exported.
Runs @tt{raco} from the active Racket installation. The executable is resolved
from that installation before @tt{PATH} is considered, which avoids accidentally
using @tt{raco} from another Racket installation and also works on Windows when
@tt{raco.exe} is not on @tt{PATH}.
@section{Path selection}
The Rash command calls @racket[coreutils-raco]. The exported @racket[raco]
binding is the Rash-facing command binding; ordinary Racket code can call
@racket[coreutils-raco] explicitly.
Shell-style argument expansion remains Rash's responsibility. The aliases expand
back into Rash's @tt{=unix-pipe=} operator, preserving Rash globbing, tilde
expansion, and @tt{$} expansion.
@defproc[(coreutils-raco [arg any/c] ...) void?]{Runs the active installation's @tt{raco} executable.}
@section{Editor command}
@index["rash-coreutils-edit"]
@subsection{@tt{edit}}
@verbatim{
ls *.rkt
ls -l info*
cat info*
edit notes.rkt
edit --wait notes.rkt
}
Regular-expression values are an additional @bold{rash-coreutils} selector. A
@racket[#rx""] or @racket[#px""] value is matched against entry names in the
current directory.
Opens exactly one file in the editor configured for rash-coreutils. By default,
the editor is RackEdit's @racket[rkdt] procedure. Without @tt{--wait}, RackEdit
opens the editor and the Rash command can continue. With @tt{--wait}, the flag
is translated to @racket[#:wait?] @racket[#t], so the command returns only after
the editor procedure returns.
Internally, @tt{edit} dispatches to @racket[coreutils-edit]. The implementation
calls the procedure stored in @racket[current-coreutils-editor] with the file
path and a @racket[#:wait?] keyword.
@defproc[(coreutils-edit [arg any/c] ...) void?]{Parses @tt{--wait}, validates that exactly one file was supplied, and invokes @racket[current-coreutils-editor].}
@defthing[current-coreutils-editor parameter?]{
A parameter whose default value is @racket[rkdt]. The configured procedure must
accept one file path and the optional keyword @racket[#:wait?]. A wrapper can be
used to select a different editor for a dynamic extent.
}
@verbatim{
ls #px"^info[0-9]+[.]rkt$"
(parameterize
([current-coreutils-editor
(λ (filename #:wait? [wait? #f])
...)])
...)
}
@section{Help command}
@section{Windows paths}
On Windows, commands that write path names use forward slashes in their textual output. Rash treats backslashes as escape characters in line mode, so a printed path such as @tt{C:/Users/name/AppData/Local/Temp/file} can be copied directly into another Rash command. Racket path values themselves remain native and can be passed directly to a command through a Racket expression.
@section{Help}
@tt{help} uses Racket's documentation search through @tt{raco docs}. Registered
commands use package-specific index terms such as @tt{rash-coreutils-ls}.
@subsection{@tt{help}}
@verbatim{
help
@@ -201,10 +611,49 @@ ls --help
help directory-list
}
With no argument, @tt{help} writes the registered rash-coreutils command names.
For a registered command, it searches Racket documentation using the command's
package-specific index term, for example @tt{rash-coreutils-ls}. Other terms are
passed directly to Racket's documentation search. Supplying @tt{--help} to a
registered command uses the same mechanism.
The @tt{help} alias calls @racket[coreutils-help] directly. The normal command
dispatcher intercepts @tt{--help} before invoking the command implementation.
@defproc[(coreutils-help [arg any/c] ...) void?]{Lists commands or opens Racket documentation for one search term.}
@section{Path selection and Rash expansion}
Normal shell-style argument expansion remains Rash's responsibility. The aliases
expand back into Rash's pipeline machinery, preserving globbing, tilde expansion,
variable expansion, pipelines, and redirection.
@verbatim{
ls *.rkt
ls -l info*
cat info* | wc -l
}
Racket regular-expression values are an additional rash-coreutils selector. A
@racket[#rx""] or @racket[#px""] value is matched against entry names in the
current directory before the internal command procedure is called.
@verbatim{
ls #px"^info[0-9]+[.]rkt$"
}
@section{Windows paths}
On Windows, commands that write path names use forward slashes in their textual
output. Rash treats backslashes as escape characters in line mode, so a printed
path such as @tt{C:/Users/name/AppData/Local/Temp/file} can be copied directly
into another Rash command. Racket path values themselves remain native and can
be passed directly to a command through a Racket expression.
@section{Capturing and redirecting output}
Commands use normal ports. Rash can therefore capture or redirect them without
special support in this package.
The internal Racket procedures use normal current ports. Rash can therefore
capture, pipe, or redirect command output without command-specific support.
@verbatim{
(define git-path { which git |> read-line })
@@ -216,5 +665,5 @@ cat info.rkt &>! copy.rkt
The command names and common options follow Unix conventions, but this package
does not claim complete GNU coreutils compatibility. Platform-independent
behavior, predictable Rash scripting, and useful integration with Racket values
are the primary goals.
behavior, predictable Rash scripting, and integration with Racket values are
the primary goals.
+67
View File
@@ -9,6 +9,7 @@
"../private/dispatcher.rkt"
"../private/help.rkt"
"../private/racket-tools.rkt"
"../private/editor.rkt"
"../private/env-support.rkt")
(define (capture-output procedure . args)
@@ -95,8 +96,36 @@
(check-equal? (coreutils-help-search-term 'directory-list)
"directory-list")
(check-equal? (coreutils-help-search-term 'raco) "rash-coreutils-raco")
(check-equal? (coreutils-help-search-term 'edit) "rash-coreutils-edit")
(check-true (path? (find-raco-executable)))
(check-true (procedure? coreutils-raco))
(check-true (procedure? (current-coreutils-editor)))
(define edit-call #f)
(parameterize
([current-coreutils-editor
(λ (filename #:wait? [wait? #f])
(set! edit-call (list filename wait?)))])
(dispatch-coreutils-command 'edit '--wait "one.txt"))
(check-equal? (path->string (first edit-call)) "one.txt")
(check-true (second edit-call))
(set! edit-call #f)
(parameterize
([current-coreutils-editor
(λ (filename #:wait? [wait? #f])
(set! edit-call (list filename wait?)))])
(dispatch-coreutils-command 'edit "one.txt"))
(check-equal? (path->string (first edit-call)) "one.txt")
(check-false (second edit-call))
(check-exn
exn:fail?
(λ ()
(parameterize
([current-coreutils-editor
(λ (filename #:wait? [wait? #f]) (void))])
(dispatch-coreutils-command 'edit "one.txt" "two.txt"))))
(call-with-output-file "lines.txt"
#:exists 'truncate
@@ -106,6 +135,27 @@
"one\ntwo\n")
(check-equal? (capture-output coreutils-tail "-n" "1" "lines.txt")
"three\n")
;; A long physical line must not force head or tail to materialize that
;; line in memory. The 64 KiB internal block size is crossed many times.
(define long-lines-path (build-path test-root "long-lines.txt"))
(define long-chunk (make-bytes (* 64 1024) 120))
(call-with-output-file long-lines-path
#:exists 'truncate
(λ (out)
(display "first\n" out)
(for ([i (in-range 64)])
(write-bytes long-chunk out))
(display "\nlast-one\nlast-two\n" out)))
(check-equal? (capture-output coreutils-head "-n" "1" long-lines-path)
"first\n")
(check-equal? (capture-output coreutils-tail "-n" "2" long-lines-path)
"last-one\nlast-two\n")
(call-with-input-file long-lines-path
(λ (in)
(parameterize ([current-input-port in])
(check-equal? (capture-output coreutils-tail "-n" "2")
"last-one\nlast-two\n"))))
(check-equal? (capture-output coreutils-wc "-l" "lines.txt")
"3\n")
(check-equal? (capture-output coreutils-basename "a/b/c.txt")
@@ -146,6 +196,23 @@
(check-true (regexp-match? #px"[0-9]{4}-[0-9]{2}-[0-9]{2}T"
(capture-output coreutils-date "--iso")))
;; time writes measurements to stderr without contaminating command output.
(define time-error (open-output-string))
(define timed-output
(parameterize ([current-error-port time-error])
(capture-output
dispatch-coreutils-command
'time
'echo
'timed)))
(check-equal? timed-output "timed\n")
(define time-output (get-output-string time-error))
(check-true
(regexp-match?
#px"^real\\t[0-9]+[.][0-9]{3}s\ncpu\\t[0-9]+[.][0-9]{3}s\ngc\\t[0-9]+[.][0-9]{3}s\n$"
time-output))
(check-exn
exn:fail?
(λ ()
+16 -2
View File
@@ -2,6 +2,7 @@
(require rackunit
racket/file
racket/path
racket/port
rash-coreutils)
@@ -11,13 +12,17 @@
(open-output-string))
(define errors
(open-output-string))
(define edited #f)
(dynamic-wind
void
(λ ()
(parameterize ([current-directory test-root]
[current-output-port output]
[current-error-port errors])
[current-error-port errors]
[current-coreutils-editor
(λ (filename #:wait? [wait? #f])
(set! edited (list filename wait?)))])
{
(call-with-output-file "info.rkt"
#:exists 'truncate
@@ -30,21 +35,30 @@
ls -l rash-coreutils-smoke
ls *.rkt
ls #px"^info[.]rkt$"
edit --wait info.rkt
(define x 42)
env RASH_COREUTILS_VALUE=(values x) printenv RASH_COREUTILS_VALUE
echo one two three | wc -w
time echo timed
date --iso
rm -r rash-coreutils-smoke
(check-false (directory-exists? "rash-coreutils-smoke"))
})
(check-equal? (get-output-string errors) "")
(define error-text (get-output-string errors))
(check-true
(regexp-match?
#px"^real\\t[0-9]+[.][0-9]{3}s\ncpu\\t[0-9]+[.][0-9]{3}s\ngc\\t[0-9]+[.][0-9]{3}s\n$"
error-text))
(define text (get-output-string output))
(check-true (regexp-match? #rx"hello from rash-coreutils" text))
(check-true (regexp-match? #rx"info[.]rkt" text))
(check-true (regexp-match? #rx"42" text))
(check-true (regexp-match? #rx"timed" text))
(check-equal? (path->string (first edited)) "info.rkt")
(check-true (second edited))
(check-true (regexp-match? #rx"\n3\n" text))
(check-true (regexp-match? #px"[0-9]{4}-[0-9]{2}-[0-9]{2}T" text)))
(λ ()