From c55bf62438750294bdf45a6e7e1f6da93ad73d0c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 30 Nov 2022 21:14:38 -0800 Subject: [PATCH 1/4] d2: Move cmd to repo root Closes #113 --- ci/release/_build.sh | 2 +- ci/release/changelogs/next.md | 4 ++++ c.go => d2lib/c.go | 2 +- d2.go => d2lib/d2.go | 2 +- docs/INSTALL.md | 2 +- e2etests/e2e_test.go | 2 +- cmd/d2/help.go => help.go | 2 +- cmd/d2/main.go => main.go | 2 +- cmd/d2/main_test.go => main_test.go | 0 {cmd/d2/static => static}/watch.css | 0 {cmd/d2/static => static}/watch.js | 0 cmd/d2/watch.go => watch.go | 0 cmd/d2/watch_dev.go => watch_dev.go | 0 13 files changed, 11 insertions(+), 7 deletions(-) rename c.go => d2lib/c.go (90%) rename d2.go => d2lib/d2.go (98%) rename cmd/d2/help.go => help.go (98%) rename cmd/d2/main.go => main.go (99%) rename cmd/d2/main_test.go => main_test.go (100%) rename {cmd/d2/static => static}/watch.css (100%) rename {cmd/d2/static => static}/watch.js (100%) rename cmd/d2/watch.go => watch.go (100%) rename cmd/d2/watch_dev.go => watch_dev.go (100%) diff --git a/ci/release/_build.sh b/ci/release/_build.sh index c5170219a..4abfd6dec 100755 --- a/ci/release/_build.sh +++ b/ci/release/_build.sh @@ -14,7 +14,7 @@ export GOOS=$(goos "$OS") export GOARCH="$ARCH" sh_c mkdir -p "$HW_BUILD_DIR/bin" sh_c go build -ldflags "'-X oss.terrastruct.com/d2/lib/version.Version=$VERSION'" \ - -o "$HW_BUILD_DIR/bin/d2" ./cmd/d2 + -o "$HW_BUILD_DIR/bin/d2" . ARCHIVE=$PWD/$ARCHIVE cd "$(dirname "$HW_BUILD_DIR")" diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index cf91ce05e..b253f2e8b 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -23,6 +23,10 @@ [#251](https://github.com/terrastruct/d2/pull/251) - [install.sh](./install.sh) prints the dry run message more visibly. [#266](https://github.com/terrastruct/d2/pull/266) +- `d2` now lives in the root folder of the repository instead of as a subcommand. + So you can run `go install oss.terrastruct.com/d2@latest` to install from source + now. + [#xxx](https://github.com/terrastruct/d2/pull/xxx) #### Bugfixes 🔴 diff --git a/c.go b/d2lib/c.go similarity index 90% rename from c.go rename to d2lib/c.go index 1fa3fc225..2774d943e 100644 --- a/c.go +++ b/d2lib/c.go @@ -1,6 +1,6 @@ //go:build cgo -package d2 +package d2lib import "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" diff --git a/d2.go b/d2lib/d2.go similarity index 98% rename from d2.go rename to d2lib/d2.go index d6fdfe8a2..91267326e 100644 --- a/d2.go +++ b/d2lib/d2.go @@ -1,4 +1,4 @@ -package d2 +package d2lib import ( "context" diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 71d3fcd0a..95d677851 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -83,7 +83,7 @@ know where the release directory is for easy uninstall. You can always install from source: ```sh -go install oss.terrastruct.com/d2/cmd/d2@latest +go install oss.terrastruct.com/d2@latest ``` ## Coming soon diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go index 513451247..ea7cfaa65 100644 --- a/e2etests/e2e_test.go +++ b/e2etests/e2e_test.go @@ -16,10 +16,10 @@ import ( "oss.terrastruct.com/diff" - "oss.terrastruct.com/d2" "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2elklayout" + d2 "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/textmeasure" "oss.terrastruct.com/d2/d2target" diff --git a/cmd/d2/help.go b/help.go similarity index 98% rename from cmd/d2/help.go rename to help.go index 947c601e8..6c1cec897 100644 --- a/cmd/d2/help.go +++ b/help.go @@ -22,7 +22,7 @@ It defaults to file.svg if an output path is not provided. Use - to have d2 read from stdin or write to stdout. -See man %[1]s for more detailed docs. +See man d2 for more detailed docs. Flags: %s diff --git a/cmd/d2/main.go b/main.go similarity index 99% rename from cmd/d2/main.go rename to main.go index ffcb4509d..c6fd94647 100644 --- a/cmd/d2/main.go +++ b/main.go @@ -14,8 +14,8 @@ import ( "github.com/spf13/pflag" "go.uber.org/multierr" - "oss.terrastruct.com/d2" "oss.terrastruct.com/d2/d2layouts/d2sequence" + d2 "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2plugin" "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/textmeasure" diff --git a/cmd/d2/main_test.go b/main_test.go similarity index 100% rename from cmd/d2/main_test.go rename to main_test.go diff --git a/cmd/d2/static/watch.css b/static/watch.css similarity index 100% rename from cmd/d2/static/watch.css rename to static/watch.css diff --git a/cmd/d2/static/watch.js b/static/watch.js similarity index 100% rename from cmd/d2/static/watch.js rename to static/watch.js diff --git a/cmd/d2/watch.go b/watch.go similarity index 100% rename from cmd/d2/watch.go rename to watch.go diff --git a/cmd/d2/watch_dev.go b/watch_dev.go similarity index 100% rename from cmd/d2/watch_dev.go rename to watch_dev.go From f334ef595219964356b9914375f7ff6a2e81e3cd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 30 Nov 2022 21:50:55 -0800 Subject: [PATCH 2/4] README: Cleanup examples to make them runnable and testable --- README.md | 50 +------------------ ci/release/changelogs/next.md | 2 +- ci/sub | 2 +- d2lib/d2.go | 10 ++-- docs/examples/lib/1-d2lib/.gitignore | 1 + docs/examples/lib/1-d2lib/d2lib.go | 25 ++++++++++ docs/examples/lib/1-d2lib/d2lib_test.go | 9 ++++ docs/examples/lib/2-d2oracle/d2oracle.go | 36 +++++++++++++ docs/examples/lib/2-d2oracle/d2oracle_test.go | 9 ++++ docs/examples/lib/3-lowlevel/.gitignore | 1 + docs/examples/lib/3-lowlevel/lowlevel.go | 26 ++++++++++ docs/examples/lib/3-lowlevel/lowlevel_test.go | 9 ++++ docs/examples/lib/README.md | 30 +++++++++++ e2etests/e2e_test.go | 4 +- main.go | 6 +-- 15 files changed, 159 insertions(+), 61 deletions(-) create mode 100644 docs/examples/lib/1-d2lib/.gitignore create mode 100644 docs/examples/lib/1-d2lib/d2lib.go create mode 100644 docs/examples/lib/1-d2lib/d2lib_test.go create mode 100644 docs/examples/lib/2-d2oracle/d2oracle.go create mode 100644 docs/examples/lib/2-d2oracle/d2oracle_test.go create mode 100644 docs/examples/lib/3-lowlevel/.gitignore create mode 100644 docs/examples/lib/3-lowlevel/lowlevel.go create mode 100644 docs/examples/lib/3-lowlevel/lowlevel_test.go create mode 100644 docs/examples/lib/README.md diff --git a/README.md b/README.md index e4fd6e387..b6abd44f7 100644 --- a/README.md +++ b/README.md @@ -111,56 +111,8 @@ For detailed installation docs, with alternative methods and examples for each O In addition to being a runnable CLI tool, D2 can also be used to produce diagrams from Go programs. -```go -import ( - "context" - "io/ioutil" - "path/filepath" - "strings" +For examples, see [./docs/examples/lib](./docs/examples/lib). - "oss.terrastruct.com/d2/d2compiler" - "oss.terrastruct.com/d2/d2exporter" - "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" - "oss.terrastruct.com/d2/d2renderers/d2svg" - "oss.terrastruct.com/d2/d2renderers/textmeasure" - "oss.terrastruct.com/d2/d2themes/d2themescatalog" -) - -func main() { - graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), &d2compiler.CompileOptions{UTF16: true}) - ruler, _ := textmeasure.NewRuler() - graph.SetDimensions(nil, ruler) - d2dagrelayout.Layout(context.Background(), graph) - diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID) - out, _ := d2svg.Render(diagram) - ioutil.WriteFile(filepath.Join("out.svg"), out, 0600) -} -``` - -D2 is built to be hackable -- the language has an API built on top of it to make edits -programmatically. Modifying the above diagram: - -```go -import ( - "oss.terrastruct.com/d2/d2renderers/textmeasure" - "oss.terrastruct.com/d2/d2themes/d2themescatalog" -) - -// Create a shape with the ID, "meow" -graph, _, _ = d2oracle.Create(graph, "meow") -// Style the shape green -color := "green" -graph, _ = d2oracle.Set(graph, "meow.style.fill", nil, &color) -// Create a shape with the ID, "cat" -graph, _, _ = d2oracle.Create(graph, "cat") -// Move the shape "meow" inside the container "cat" -graph, _ = d2oracle.Move(graph, "meow", "cat.meow") -// Prints formatted D2 script -println(d2format.Format(graph.AST)) -``` - -This makes it easy to build functionality on top of D2. Terrastruct uses the above API to -implement editing of D2 from mouse actions in a visual interface. ## Themes diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index b253f2e8b..25d993081 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -26,7 +26,7 @@ - `d2` now lives in the root folder of the repository instead of as a subcommand. So you can run `go install oss.terrastruct.com/d2@latest` to install from source now. - [#xxx](https://github.com/terrastruct/d2/pull/xxx) + [#290](https://github.com/terrastruct/d2/pull/290) #### Bugfixes 🔴 diff --git a/ci/sub b/ci/sub index 28fb67e3b..0dacc9c6c 160000 --- a/ci/sub +++ b/ci/sub @@ -1 +1 @@ -Subproject commit 28fb67e3bf11d7df2be9ad57b67b78a1733a7f2d +Subproject commit 0dacc9c6cee5b8d901f22f57e9dc7f9acf640cbc diff --git a/d2lib/d2.go b/d2lib/d2.go index 91267326e..2b4d800fb 100644 --- a/d2lib/d2.go +++ b/d2lib/d2.go @@ -22,7 +22,7 @@ type CompileOptions struct { ThemeID int64 } -func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, error) { +func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) { if opts == nil { opts = &CompileOptions{} } @@ -31,12 +31,12 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target UTF16: opts.UTF16, }) if err != nil { - return nil, err + return nil, nil, err } err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler) if err != nil { - return nil, err + return nil, nil, err } if opts.Layout != nil { @@ -47,11 +47,11 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target err = errors.New("no available layout") } if err != nil { - return nil, err + return nil, nil, err } diagram, err := d2exporter.Export(ctx, g, opts.ThemeID) - return diagram, err + return diagram, g, err } // See c.go diff --git a/docs/examples/lib/1-d2lib/.gitignore b/docs/examples/lib/1-d2lib/.gitignore new file mode 100644 index 000000000..958df5ba9 --- /dev/null +++ b/docs/examples/lib/1-d2lib/.gitignore @@ -0,0 +1 @@ +out.svg diff --git a/docs/examples/lib/1-d2lib/d2lib.go b/docs/examples/lib/1-d2lib/d2lib.go new file mode 100644 index 000000000..421e14677 --- /dev/null +++ b/docs/examples/lib/1-d2lib/d2lib.go @@ -0,0 +1,25 @@ +package main + +import ( + "context" + "io/ioutil" + "path/filepath" + + "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" + "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/d2renderers/textmeasure" + "oss.terrastruct.com/d2/d2themes/d2themescatalog" +) + +// Remember to add if err != nil checks in production. +func main() { + ruler, _ := textmeasure.NewRuler() + diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ + Layout: d2dagrelayout.Layout, + Ruler: ruler, + ThemeID: d2themescatalog.GrapeSoda.ID, + }) + out, _ := d2svg.Render(diagram) + _ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600) +} diff --git a/docs/examples/lib/1-d2lib/d2lib_test.go b/docs/examples/lib/1-d2lib/d2lib_test.go new file mode 100644 index 000000000..53b3d5bcc --- /dev/null +++ b/docs/examples/lib/1-d2lib/d2lib_test.go @@ -0,0 +1,9 @@ +package main + +import ( + "testing" +) + +func TestMain_(t *testing.T) { + main() +} diff --git a/docs/examples/lib/2-d2oracle/d2oracle.go b/docs/examples/lib/2-d2oracle/d2oracle.go new file mode 100644 index 000000000..e9e7ba944 --- /dev/null +++ b/docs/examples/lib/2-d2oracle/d2oracle.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "fmt" + + "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" + "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/d2oracle" + "oss.terrastruct.com/d2/d2renderers/textmeasure" + "oss.terrastruct.com/d2/d2themes/d2themescatalog" +) + +// Remember to add if err != nil checks in production. +func main() { + // From one.go + ruler, _ := textmeasure.NewRuler() + _, graph, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ + Layout: d2dagrelayout.Layout, + Ruler: ruler, + ThemeID: d2themescatalog.GrapeSoda.ID, + }) + + // Create a shape with the ID, "meow" + graph, _, _ = d2oracle.Create(graph, "meow") + // Style the shape green + color := "green" + graph, _ = d2oracle.Set(graph, "meow.style.fill", nil, &color) + // Create a shape with the ID, "cat" + graph, _, _ = d2oracle.Create(graph, "cat") + // Move the shape "meow" inside the container "cat" + graph, _ = d2oracle.Move(graph, "meow", "cat.meow") + // Prints formatted D2 script + fmt.Print(d2format.Format(graph.AST)) +} diff --git a/docs/examples/lib/2-d2oracle/d2oracle_test.go b/docs/examples/lib/2-d2oracle/d2oracle_test.go new file mode 100644 index 000000000..53b3d5bcc --- /dev/null +++ b/docs/examples/lib/2-d2oracle/d2oracle_test.go @@ -0,0 +1,9 @@ +package main + +import ( + "testing" +) + +func TestMain_(t *testing.T) { + main() +} diff --git a/docs/examples/lib/3-lowlevel/.gitignore b/docs/examples/lib/3-lowlevel/.gitignore new file mode 100644 index 000000000..958df5ba9 --- /dev/null +++ b/docs/examples/lib/3-lowlevel/.gitignore @@ -0,0 +1 @@ +out.svg diff --git a/docs/examples/lib/3-lowlevel/lowlevel.go b/docs/examples/lib/3-lowlevel/lowlevel.go new file mode 100644 index 000000000..2bba9bfcf --- /dev/null +++ b/docs/examples/lib/3-lowlevel/lowlevel.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "io/ioutil" + "path/filepath" + "strings" + + "oss.terrastruct.com/d2/d2compiler" + "oss.terrastruct.com/d2/d2exporter" + "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" + "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/d2renderers/textmeasure" + "oss.terrastruct.com/d2/d2themes/d2themescatalog" +) + +// Remember to add if err != nil checks in production. +func main() { + graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil) + ruler, _ := textmeasure.NewRuler() + _ = graph.SetDimensions(nil, ruler) + _ = d2dagrelayout.Layout(context.Background(), graph) + diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID) + out, _ := d2svg.Render(diagram) + _ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600) +} diff --git a/docs/examples/lib/3-lowlevel/lowlevel_test.go b/docs/examples/lib/3-lowlevel/lowlevel_test.go new file mode 100644 index 000000000..53b3d5bcc --- /dev/null +++ b/docs/examples/lib/3-lowlevel/lowlevel_test.go @@ -0,0 +1,9 @@ +package main + +import ( + "testing" +) + +func TestMain_(t *testing.T) { + main() +} diff --git a/docs/examples/lib/README.md b/docs/examples/lib/README.md new file mode 100644 index 000000000..979e9af7e --- /dev/null +++ b/docs/examples/lib/README.md @@ -0,0 +1,30 @@ +# D2 library examples + +We have a few examples in this directory on how to use the D2 library to turn D2 scripts +into rendered svg diagrams and more. + +Each example is runnable though does not include error handling for readability. + +### [./1-d2lib](./1-d2lib) + +A minimal example showing you how to compile the diagram `x -> y` into an svg. + +### [./2-d2oracle](./2-d2oracle) + +D2 is built to be hackable -- the language has an API built on top of it to make edits +programmatically. + +Modifying the previous example, this example demonstrates how +[d2oracle](../../../d2oracle) can be used to create a new shape, style it programatically +and then output the modified d2 script. + +This makes it easy to build functionality on top of D2. Terrastruct uses the +[d2oracle](../../../d2oracle) API to implement editing of D2 from mouse actions in a +visual interface. + +### [./3-lowlevel](./3-lowlevel) + +`d2lib` from the first example is just a wrapper around the lower level APIs. They +can be used directly and this example demonstrates such usage. + +This shouldn't be necessary for most usecases. diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go index ea7cfaa65..1e231f39d 100644 --- a/e2etests/e2e_test.go +++ b/e2etests/e2e_test.go @@ -19,7 +19,7 @@ import ( "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2elklayout" - d2 "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/textmeasure" "oss.terrastruct.com/d2/d2target" @@ -104,7 +104,7 @@ func run(t *testing.T, tc testCase) { } else if layoutName == "elk" { layout = d2elklayout.Layout } - diagram, err := d2.Compile(ctx, tc.script, &d2.CompileOptions{ + diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ UTF16: true, Ruler: ruler, ThemeID: 0, diff --git a/main.go b/main.go index c6fd94647..df06bcaf2 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ import ( "go.uber.org/multierr" "oss.terrastruct.com/d2/d2layouts/d2sequence" - d2 "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2plugin" "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/textmeasure" @@ -196,7 +196,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, theme if os.Getenv("D2_SEQUENCE") == "1" { layout = d2sequence.Layout } - d, err := d2.Compile(ctx, string(input), &d2.CompileOptions{ + diagram, _, err := d2lib.Compile(ctx, string(input), &d2lib.CompileOptions{ Layout: layout, Ruler: ruler, ThemeID: themeID, @@ -205,7 +205,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, theme return nil, false, err } - svg, err := d2svg.Render(d) + svg, err := d2svg.Render(diagram) if err != nil { return nil, false, err } From 14a78f2a30e7ccbcebf827ba90febc04282aa7bf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 30 Nov 2022 21:53:01 -0800 Subject: [PATCH 3/4] README: Update toc --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b6abd44f7..f3f727d20 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,13 @@ - [Related](#related) * [VSCode extension](#vscode-extension) * [Vim extension](#vim-extension) + * [Language docs](#language-docs) * [Misc](#misc) - [FAQ](#faq) -# What does D2 look like? +## What does D2 look like? ```d2 # Actors @@ -113,7 +114,6 @@ Go programs. For examples, see [./docs/examples/lib](./docs/examples/lib). - ## Themes D2 includes a variety of official themes to style your diagrams beautifully right out of From 20abda5ffe737f008f8b097450e18807c06ce8ad Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 30 Nov 2022 22:39:46 -0800 Subject: [PATCH 4/4] ci/sub: Update --- ci/release/template/scripts/lib.sh | 5 +++-- install.sh | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ci/release/template/scripts/lib.sh b/ci/release/template/scripts/lib.sh index 3dc704bb0..486d25ac6 100644 --- a/ci/release/template/scripts/lib.sh +++ b/ci/release/template/scripts/lib.sh @@ -66,7 +66,7 @@ should_color() { fi fi - if [ -t 1 ]; then + if [ -t 1 -a "${TERM-}" != dumb ]; then _COLOR=1 return 0 else @@ -89,7 +89,8 @@ _echo() { get_rand_color() { # 1-6 are regular and 9-14 are bright. # 1,2 and 9,10 are red and green but we use those for success and failure. - pick "$*" 3 4 5 6 11 12 13 14 + pick "$*" 1 2 3 4 5 6 \ + 9 10 11 12 13 14 } echop() { diff --git a/install.sh b/install.sh index 7afad1716..7dca28652 100755 --- a/install.sh +++ b/install.sh @@ -72,7 +72,7 @@ should_color() { fi fi - if [ -t 1 ]; then + if [ -t 1 -a "${TERM-}" != dumb ]; then _COLOR=1 return 0 else @@ -95,7 +95,8 @@ _echo() { get_rand_color() { # 1-6 are regular and 9-14 are bright. # 1,2 and 9,10 are red and green but we use those for success and failure. - pick "$*" 3 4 5 6 11 12 13 14 + pick "$*" 1 2 3 4 5 6 \ + 9 10 11 12 13 14 } echop() {