Answer architecture round 2: every error a custom CLI prints names it, the help marks where the name goes, the version line checks the main module first, and the README embed compiles for data without a table
This commit is contained in:
@@ -441,14 +441,14 @@ import (
|
||||
"github.com/larvit/fejkdata/data"
|
||||
)
|
||||
|
||||
//go:embed *.json *.tsv
|
||||
//go:embed *.json
|
||||
var mydata embed.FS
|
||||
|
||||
func main() { os.Exit(cli.Main(append(data.Modules(), mydata)...)) }
|
||||
```
|
||||
|
||||
It takes `fejkdata`'s flags and exit codes, and names itself by the file it runs from in
|
||||
its help and errors. `--no-shipped-data` leaves out the modules passed to `Main`, and
|
||||
Add `*.tsv` to the embed where your data holds a table. The CLI takes `fejkdata`'s flags
|
||||
and exit codes, and names itself by the file it runs from in its help and errors. `--no-shipped-data` leaves out the modules passed to `Main`, and
|
||||
`--version` prints your module's version beside the fejkdata version it is built with.
|
||||
`fejkdata`'s own `main` is `os.Exit(cli.Main(data.Modules()...))`.
|
||||
|
||||
|
||||
+30
-18
@@ -22,16 +22,16 @@ import (
|
||||
"github.com/larvit/fejkdata"
|
||||
)
|
||||
|
||||
const usage = `Usage: <template> | fejkdata [flags]
|
||||
fejkdata --list | --help | --version
|
||||
const usage = `Usage: <template> | COMMAND [flags]
|
||||
COMMAND --list | --help | --version
|
||||
|
||||
fejkdata reads one template from stdin and renders it:
|
||||
COMMAND reads one template from stdin and renders it:
|
||||
|
||||
echo 'name: {/sv_SE.person.last}' | fejkdata
|
||||
echo '{"format":"{x}","x":["bosse","lina"]}' | fejkdata
|
||||
fejkdata < template.txt
|
||||
echo 'name: {/sv_SE.person.last}' | COMMAND
|
||||
echo '{"format":"{x}","x":["bosse","lina"]}' | COMMAND
|
||||
COMMAND < template.txt
|
||||
|
||||
fejkdata <<'EOF'
|
||||
COMMAND <<'EOF'
|
||||
born {date(1990-01-01,2010-12-31,'2006-01-02')}
|
||||
EOF
|
||||
|
||||
@@ -42,7 +42,7 @@ path into one ({/sv_SE.person.last}), a table's row by key or name
|
||||
({/misc.territory[SE]}), and .. steps up from a row to the row it links to
|
||||
({/geo.SE.locality..municipality.name}).
|
||||
|
||||
Under --format text, the default, what you send is what renders, and fejkdata adds
|
||||
Under --format text, the default, what you send is what renders, and COMMAND adds
|
||||
nothing: a format string keeps every byte, so echo's newline ends each render, and
|
||||
printf '%s' sends none. -n joins renders with --separator, empty by default. A JSON
|
||||
object, array or string drops the whitespace around it, and null alone renders nothing;
|
||||
@@ -569,7 +569,13 @@ func Main(modules ...fs.FS) int {
|
||||
if fi, err := os.Stdin.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
|
||||
stdin = nil
|
||||
}
|
||||
name := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
|
||||
name := filepath.Base(os.Args[0])
|
||||
if ext := filepath.Ext(name); strings.EqualFold(ext, ".exe") {
|
||||
name = strings.TrimSuffix(name, ext)
|
||||
}
|
||||
if name == "." || name == "" {
|
||||
name = "fejkdata"
|
||||
}
|
||||
return run(name, modules, os.Args[1:], stdin, os.Stdout, os.Stderr)
|
||||
}
|
||||
|
||||
@@ -581,7 +587,7 @@ func run(name string, modules []fs.FS, args []string, stdin io.Reader, stdout, s
|
||||
return misuse(stderr, name, err)
|
||||
}
|
||||
if in.help {
|
||||
io.WriteString(stdout, strings.ReplaceAll(usage, "fejkdata", name))
|
||||
io.WriteString(stdout, strings.ReplaceAll(usage, "COMMAND", name))
|
||||
return 0
|
||||
}
|
||||
if in.version {
|
||||
@@ -600,8 +606,7 @@ func run(name string, modules []fs.FS, args []string, stdin io.Reader, stdout, s
|
||||
}
|
||||
f, err := fejkdata.New(in.options(modules)...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 1
|
||||
return fail(stderr, name, err)
|
||||
}
|
||||
if in.list {
|
||||
for _, p := range f.List() {
|
||||
@@ -639,7 +644,7 @@ func fail(stderr io.Writer, name string, err error) int {
|
||||
if errors.As(err, &te) {
|
||||
return misuse(stderr, name, te.error)
|
||||
}
|
||||
fmt.Fprintln(stderr, err)
|
||||
report(stderr, name, err)
|
||||
if errors.Is(err, fejkdata.ErrNoColumns) {
|
||||
fmt.Fprintln(stderr, "wrap that JSON in single quotes, which keep a shell from expanding its braces")
|
||||
}
|
||||
@@ -647,11 +652,16 @@ func fail(stderr io.Writer, name string, err error) int {
|
||||
}
|
||||
|
||||
func misuse(stderr io.Writer, name string, err error) int {
|
||||
// A library error opens with the library's name, so the prefix is not doubled.
|
||||
fmt.Fprintf(stderr, "%s: %s\ntry '%s --help'\n", name, strings.TrimPrefix(err.Error(), "fejkdata: "), name)
|
||||
report(stderr, name, err)
|
||||
fmt.Fprintf(stderr, "try '%s --help'\n", name)
|
||||
return 2
|
||||
}
|
||||
|
||||
// report writes err under the command name, in place of the library's own.
|
||||
func report(stderr io.Writer, name string, err error) {
|
||||
fmt.Fprintf(stderr, "%s: %s\n", name, strings.TrimPrefix(err.Error(), "fejkdata: "))
|
||||
}
|
||||
|
||||
// fejkdataModule is the path of the module holding the library; the root package's path is
|
||||
// the module's.
|
||||
var fejkdataModule = reflect.TypeOf((*fejkdata.Generator)(nil)).Elem().PkgPath()
|
||||
@@ -662,13 +672,15 @@ func versionLine(name string, info *debug.BuildInfo) string {
|
||||
if info == nil {
|
||||
return "fejkdata devel"
|
||||
}
|
||||
line := "fejkdata " + moduleVersion(info.Main)
|
||||
if info.Main.Path == fejkdataModule {
|
||||
return "fejkdata " + moduleVersion(info.Main)
|
||||
}
|
||||
for _, m := range info.Deps {
|
||||
if m.Path == fejkdataModule {
|
||||
line = name + " " + moduleVersion(info.Main) + ", fejkdata " + moduleVersion(*m)
|
||||
return name + " " + moduleVersion(info.Main) + ", fejkdata " + moduleVersion(*m)
|
||||
}
|
||||
}
|
||||
return line
|
||||
return name + " " + moduleVersion(info.Main) + ", fejkdata devel"
|
||||
}
|
||||
|
||||
// moduleVersion is the version m is built at: the replacement's where a replace directive
|
||||
|
||||
@@ -473,6 +473,10 @@ func TestRunNamesItselfByTheCommandName(t *testing.T) {
|
||||
if code := run("mycli", shipped, nil, nil, &out, &errb); code != 2 || !strings.Contains(errb.String(), "| mycli\n") {
|
||||
t.Errorf("mycli with nothing piped = %d, %q, want a pipe into mycli", code, errb.String())
|
||||
}
|
||||
errb.Reset()
|
||||
if code := run("mycli", shipped, []string{"-d", "/nope"}, strings.NewReader("{/sv_SE.person}"), &out, &errb); code != 1 || !strings.HasPrefix(errb.String(), "mycli: ") {
|
||||
t.Errorf("mycli -d /nope = %d, %q, want a load error under mycli", code, errb.String())
|
||||
}
|
||||
if code := run("mycli", shipped, []string{"--help"}, nil, &out, &errb); code != 0 || !strings.HasPrefix(out.String(), "Usage: <template> | mycli [flags]\n") || strings.Contains(out.String(), "fejkdata") {
|
||||
t.Errorf("mycli --help = %d, %q, want mycli named throughout", code, out.String())
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ GitHub has no `tags/{tag}` endpoint and answers 404 whether or not the tag exist
|
||||
|
||||
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
|
||||
|
||||
GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged.
|
||||
GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. The binaries carry the stamped tag, which `--version` prints. v0.1.0 ships first, so it can be tested before it is packaged.
|
||||
|
||||
### 206. Add `zh_CN` with its `geo/CN` tree.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user