2022-11-14 04:35:22PM

This commit is contained in:
Alexander Wang 2022-11-14 16:35:22 -08:00
parent 1fb855cf79
commit 4d4a00743d
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
6 changed files with 174 additions and 60 deletions

View file

@ -28,13 +28,13 @@ Subcommands:
%[1]s layout [layout name] - Display long help for a particular layout engine %[1]s layout [layout name] - Display long help for a particular layout engine
See more docs and the source code at https://oss.terrastruct.com/d2 See more docs and the source code at https://oss.terrastruct.com/d2
`, ms.Name, ms.FlagHelp()) `, ms.Name, ms.Opts.Help())
} }
func layoutHelp(ctx context.Context, ms *xmain.State) error { func layoutHelp(ctx context.Context, ms *xmain.State) error {
if len(ms.FlagSet.Args()) == 1 { if len(ms.Opts.Args()) == 1 {
return shortLayoutHelp(ctx, ms) return shortLayoutHelp(ctx, ms)
} else if len(ms.FlagSet.Args()) == 2 { } else if len(ms.Opts.Args()) == 2 {
return longLayoutHelp(ctx, ms) return longLayoutHelp(ctx, ms)
} else { } else {
return pluginSubcommand(ctx, ms) return pluginSubcommand(ctx, ms)
@ -61,7 +61,7 @@ func shortLayoutHelp(ctx context.Context, ms *xmain.State) error {
%s %s
Usage: Usage:
To use a particular layout engine, set the environment variable D2_LAYOUT=[layout name]. To use a particular layout engine, set the environment variable D2_LAYOUT=[name] or flag --layout=[name].
Example: Example:
D2_LAYOUT=dagre d2 in.d2 out.svg D2_LAYOUT=dagre d2 in.d2 out.svg
@ -75,7 +75,7 @@ See more docs at https://oss.terrastruct.com/d2
} }
func longLayoutHelp(ctx context.Context, ms *xmain.State) error { func longLayoutHelp(ctx context.Context, ms *xmain.State) error {
layout := ms.FlagSet.Arg(1) layout := ms.Opts.Arg(1)
plugin, path, err := d2plugin.FindPlugin(ctx, layout) plugin, path, err := d2plugin.FindPlugin(ctx, layout)
if errors.Is(err, exec.ErrNotFound) { if errors.Is(err, exec.ErrNotFound) {
return layoutNotFound(ctx, layout) return layoutNotFound(ctx, layout)
@ -119,13 +119,13 @@ For more information on setup, please visit https://github.com/terrastruct/d2.`,
} }
func pluginSubcommand(ctx context.Context, ms *xmain.State) error { func pluginSubcommand(ctx context.Context, ms *xmain.State) error {
layout := ms.FlagSet.Arg(1) layout := ms.Opts.Arg(1)
plugin, _, err := d2plugin.FindPlugin(ctx, layout) plugin, _, err := d2plugin.FindPlugin(ctx, layout)
if errors.Is(err, exec.ErrNotFound) { if errors.Is(err, exec.ErrNotFound) {
return layoutNotFound(ctx, layout) return layoutNotFound(ctx, layout)
} }
ms.Args = ms.FlagSet.Args()[2:] ms.Opts.SetArgs(ms.Opts.Args()[2:])
return d2plugin.Serve(plugin)(ctx, ms) return d2plugin.Serve(plugin)(ctx, ms)
} }

View file

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
@ -32,19 +31,20 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
// :( // :(
ctx = xmain.DiscardSlog(ctx) ctx = xmain.DiscardSlog(ctx)
watchFlag := ms.FlagSet.BoolP("watch", "w", false, "watch for changes to input and live reload. Use $PORT and $HOST to specify the listening address.\n$D2_PORT and $D2_HOST are also accepted and take priority. Default is localhost:0") watchFlag := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n$D2_HOST and $D2_PORT are also accepted and take priority (default localhost:0, which is will open on a randomly available local port).")
themeFlag := ms.FlagSet.Int64P("theme", "t", 0, "set the diagram theme. For a list of available options, see https://oss.terrastruct.com/d2") bundleFlag := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "bundle all assets and layers into the output svg.")
bundleFlag := ms.FlagSet.BoolP("bundle", "b", true, "bundle all assets and layers into the output svg") debugFlag := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.")
versionFlag := ms.FlagSet.BoolP("version", "v", false, "get the version and check for updates") layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used.`)
debugFlag := ms.FlagSet.BoolP("debug", "d", false, "print debug logs") themeFlag := ms.Opts.Int64("D2_THEME", "theme", "t", 0, "the diagram theme ID. For a list of available options, see https://oss.terrastruct.com/d2")
err = ms.FlagSet.Parse(ms.Args) versionFlag := ms.Opts.Bool("", "version", "v", false, "get the version and check for updates")
err = ms.Opts.Parse()
if !errors.Is(err, pflag.ErrHelp) && err != nil { if !errors.Is(err, pflag.ErrHelp) && err != nil {
return xmain.UsageErrorf("failed to parse flags: %v", err) return xmain.UsageErrorf("failed to parse flags: %v", err)
} }
if len(ms.FlagSet.Args()) > 0 { if len(ms.Opts.Args()) > 0 {
switch ms.FlagSet.Arg(0) { switch ms.Opts.Arg(0) {
case "layout": case "layout":
return layoutHelp(ctx, ms) return layoutHelp(ctx, ms)
} }
@ -62,25 +62,25 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
var inputPath string var inputPath string
var outputPath string var outputPath string
if len(ms.FlagSet.Args()) == 0 { if len(ms.Opts.Args()) == 0 {
if versionFlag != nil && *versionFlag { if versionFlag != nil && *versionFlag {
version.CheckVersion(ctx, ms.Log) version.CheckVersion(ctx, ms.Log)
return nil return nil
} }
help(ms) help(ms)
return nil return nil
} else if len(ms.FlagSet.Args()) >= 3 { } else if len(ms.Opts.Args()) >= 3 {
return xmain.UsageErrorf("too many arguments passed") return xmain.UsageErrorf("too many arguments passed")
} }
if len(ms.FlagSet.Args()) >= 1 { if len(ms.Opts.Args()) >= 1 {
if ms.FlagSet.Arg(0) == "version" { if ms.Opts.Arg(0) == "version" {
version.CheckVersion(ctx, ms.Log) version.CheckVersion(ctx, ms.Log)
return nil return nil
} }
inputPath = ms.FlagSet.Arg(0) inputPath = ms.Opts.Arg(0)
} }
if len(ms.FlagSet.Args()) >= 2 { if len(ms.Opts.Args()) >= 2 {
outputPath = ms.FlagSet.Arg(1) outputPath = ms.Opts.Arg(1)
} else { } else {
if inputPath == "-" { if inputPath == "-" {
outputPath = "-" outputPath = "-"
@ -93,16 +93,11 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
if match == (d2themes.Theme{}) { if match == (d2themes.Theme{}) {
return xmain.UsageErrorf("-t[heme] could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *themeFlag) return xmain.UsageErrorf("-t[heme] could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *themeFlag)
} }
ms.Env.Setenv("D2_THEME", fmt.Sprintf("%d", *themeFlag)) ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
envD2Layout := ms.Env.Getenv("D2_LAYOUT") plugin, path, err := d2plugin.FindPlugin(ctx, *layoutFlag)
if envD2Layout == "" {
envD2Layout = "dagre"
}
plugin, path, err := d2plugin.FindPlugin(ctx, envD2Layout)
if errors.Is(err, exec.ErrNotFound) { if errors.Is(err, exec.ErrNotFound) {
return layoutNotFound(ctx, envD2Layout) return layoutNotFound(ctx, *layoutFlag)
} else if err != nil { } else if err != nil {
return err return err
} }
@ -111,14 +106,14 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
if path != "" { if path != "" {
pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path)) pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path))
} }
ms.Log.Debug.Printf("using layout plugin %s (%s)", envD2Layout, pluginLocation) ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, pluginLocation)
if *watchFlag { if *watchFlag {
if inputPath == "-" { if inputPath == "-" {
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin") return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
} }
ms.Env.Setenv("LOG_TIMESTAMPS", "1") ms.Env.Setenv("LOG_TIMESTAMPS", "1")
w, err := newWatcher(ctx, ms, plugin, inputPath, outputPath) w, err := newWatcher(ctx, ms, plugin, *themeFlag, inputPath, outputPath)
if err != nil { if err != nil {
return err return err
} }
@ -132,7 +127,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
_ = 343 _ = 343
} }
_, err = compile(ctx, ms, plugin, inputPath, outputPath) _, err = compile(ctx, ms, plugin, *themeFlag, inputPath, outputPath)
if err != nil { if err != nil {
return err return err
} }
@ -140,7 +135,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return nil return nil
} }
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, inputPath, outputPath string) ([]byte, error) { func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, themeID int64, inputPath, outputPath string) ([]byte, error) {
input, err := ms.ReadPath(inputPath) input, err := ms.ReadPath(inputPath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -151,7 +146,6 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, input
return nil, err return nil, err
} }
themeID, _ := strconv.ParseInt(ms.Env.Getenv("D2_THEME"), 10, 64)
d, err := d2.Compile(ctx, string(input), &d2.CompileOptions{ d, err := d2.Compile(ctx, string(input), &d2.CompileOptions{
Layout: plugin.Layout, Layout: plugin.Layout,
Ruler: ruler, Ruler: ruler,

View file

@ -42,6 +42,7 @@ type watcher struct {
ms *xmain.State ms *xmain.State
layoutPlugin d2plugin.Plugin layoutPlugin d2plugin.Plugin
themeID int64
inputPath string inputPath string
outputPath string outputPath string
@ -68,7 +69,7 @@ type compileResult struct {
SVG string `json:"svg"` SVG string `json:"svg"`
} }
func newWatcher(ctx context.Context, ms *xmain.State, layoutPlugin d2plugin.Plugin, inputPath, outputPath string) (*watcher, error) { func newWatcher(ctx context.Context, ms *xmain.State, layoutPlugin d2plugin.Plugin, themeID int64, inputPath, outputPath string) (*watcher, error) {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
w := &watcher{ w := &watcher{
@ -78,6 +79,7 @@ func newWatcher(ctx context.Context, ms *xmain.State, layoutPlugin d2plugin.Plug
ms: ms, ms: ms,
layoutPlugin: layoutPlugin, layoutPlugin: layoutPlugin,
themeID: themeID,
inputPath: inputPath, inputPath: inputPath,
outputPath: outputPath, outputPath: outputPath,
@ -325,7 +327,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
recompiledPrefix = "re" recompiledPrefix = "re"
} }
b, err := compile(ctx, w.ms, w.layoutPlugin, w.inputPath, w.outputPath) b, err := compile(ctx, w.ms, w.layoutPlugin, w.themeID, w.inputPath, w.outputPath)
if err != nil { if err != nil {
err = fmt.Errorf("failed to %scompile: %w", recompiledPrefix, err) err = fmt.Errorf("failed to %scompile: %w", recompiledPrefix, err)
w.ms.Log.Error.Print(err) w.ms.Log.Error.Print(err)

View file

@ -19,12 +19,12 @@ import (
// Also see execPlugin in exec.go for the d2 binary plugin protocol. // Also see execPlugin in exec.go for the d2 binary plugin protocol.
func Serve(p Plugin) func(context.Context, *xmain.State) error { func Serve(p Plugin) func(context.Context, *xmain.State) error {
return func(ctx context.Context, ms *xmain.State) (err error) { return func(ctx context.Context, ms *xmain.State) (err error) {
if len(ms.Args) < 1 { if len(ms.Opts.Args()) < 1 {
return errors.New("expected first argument to plugin binary to be function name") return errors.New("expected first argument to plugin binary to be function name")
} }
reqFunc := ms.Args[0] reqFunc := ms.Opts.Arg(0)
switch ms.Args[0] { switch ms.Opts.Arg(0) {
case "info": case "info":
return info(ctx, p, ms) return info(ctx, p, ms)
case "layout": case "layout":

132
lib/xmain/opts.go Normal file
View file

@ -0,0 +1,132 @@
package xmain
import (
"fmt"
"io"
"strconv"
"strings"
"github.com/spf13/pflag"
"oss.terrastruct.com/cmdlog"
"oss.terrastruct.com/xos"
)
type Opts struct {
args []string
flags *pflag.FlagSet
env *xos.Env
log *cmdlog.Logger
registeredEnvs []string
}
func NewOpts(env *xos.Env, args []string, log *cmdlog.Logger) *Opts {
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
flags.SortFlags = false
flags.Usage = func() {}
flags.SetOutput(io.Discard)
return &Opts{
args: args,
flags: flags,
env: env,
log: log,
}
}
func (o *Opts) Help() string {
b := &strings.Builder{}
o.flags.SetOutput(b)
o.flags.PrintDefaults()
if len(o.registeredEnvs) > 0 {
b.WriteString("\nYou may persistently set the following as environment variables (flags take precedent):\n")
for i, e := range o.registeredEnvs {
s := fmt.Sprintf("- $%s", e)
if i != len(o.registeredEnvs)-1 {
s += "\n"
}
b.WriteString(s)
}
}
return b.String()
}
func (o *Opts) Int64(envKey, flag, shortFlag string, defaultVal int64, usage string) *int64 {
if envKey != "" {
if o.env.Getenv(envKey) != "" {
envVal, err := strconv.ParseInt(o.env.Getenv(envKey), 10, 64)
if err != nil {
o.log.Error.Printf(`ignoring invalid environment variable %s. Expected int64. Found "%v".`, envKey, envVal)
} else if envVal != defaultVal {
defaultVal = envVal
}
}
o.registeredEnvs = append(o.registeredEnvs, envKey)
}
return o.flags.Int64P(flag, shortFlag, defaultVal, usage)
}
func (o *Opts) String(envKey, flag, shortFlag string, defaultVal, usage string) *string {
if envKey != "" {
if o.env.Getenv(envKey) != "" {
envVal := o.env.Getenv(envKey)
if envVal != defaultVal {
defaultVal = envVal
}
}
o.registeredEnvs = append(o.registeredEnvs, envKey)
}
return o.flags.StringP(flag, shortFlag, defaultVal, usage)
}
func (o *Opts) Bool(envKey, flag, shortFlag string, defaultVal bool, usage string) *bool {
if envKey != "" {
if o.env.Getenv(envKey) != "" {
envVal := o.env.Getenv(envKey)
if !boolyEnv(envVal) {
o.log.Error.Printf(`ignoring invalid environment variable %s. Expected bool. Found "%s".`, envKey, envVal)
} else if (defaultVal && falseyEnv(envVal)) ||
(!defaultVal && truthyEnv(envVal)) {
defaultVal = !defaultVal
}
}
o.registeredEnvs = append(o.registeredEnvs, envKey)
}
return o.flags.BoolP(flag, shortFlag, defaultVal, usage)
}
func boolyEnv(s string) bool {
return falseyEnv(s) || truthyEnv(s)
}
func falseyEnv(s string) bool {
return s == "0" || s == "false" || s == "f"
}
func truthyEnv(s string) bool {
return s == "1" || s == "true" || s == "t"
}
func (o *Opts) Parse() error {
err := o.flags.Parse(o.args)
if err != nil {
return err
}
return nil
}
func (o *Opts) SetArgs(args []string) {
o.args = args
}
func (o *Opts) Args() []string {
return o.flags.Args()
}
func (o *Opts) Arg(i int) string {
return o.flags.Arg(i)
}

View file

@ -9,13 +9,11 @@ import (
"io" "io"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
"cdr.dev/slog" "cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/sloghuman"
"github.com/spf13/pflag"
"oss.terrastruct.com/xos" "oss.terrastruct.com/xos"
@ -41,14 +39,10 @@ func Main(run RunFunc) {
Stdout: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr, Stderr: os.Stderr,
Env: xos.NewEnv(os.Environ()), Env: xos.NewEnv(os.Environ()),
FlagSet: pflag.NewFlagSet("", pflag.ContinueOnError),
Args: args,
} }
ms.Log = cmdlog.Log(ms.Env, os.Stderr) ms.Log = cmdlog.Log(ms.Env, os.Stderr)
ms.FlagSet.SortFlags = false ms.Opts = NewOpts(ms.Env, args, ms.Log)
ms.FlagSet.Usage = func() {}
ms.FlagSet.SetOutput(io.Discard)
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
@ -88,10 +82,9 @@ type State struct {
Stdout io.WriteCloser Stdout io.WriteCloser
Stderr io.WriteCloser Stderr io.WriteCloser
Log *cmdlog.Logger Log *cmdlog.Logger
Env *xos.Env Env *xos.Env
Args []string Opts *Opts
FlagSet *pflag.FlagSet
} }
func (ms *State) Main(ctx context.Context, sigs <-chan os.Signal, run func(context.Context, *State) error) error { func (ms *State) Main(ctx context.Context, sigs <-chan os.Signal, run func(context.Context, *State) error) error {
@ -129,13 +122,6 @@ func (ms *State) Main(ctx context.Context, sigs <-chan os.Signal, run func(conte
} }
} }
func (ms *State) FlagHelp() string {
b := &strings.Builder{}
ms.FlagSet.SetOutput(b)
ms.FlagSet.PrintDefaults()
return b.String()
}
type ExitError struct { type ExitError struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` Message string `json:"message"`