Skip to content

Commit

Permalink
refactor: merge with textfuncs
Browse files Browse the repository at this point in the history
Signed-off-by: Nico Braun <[email protected]>
  • Loading branch information
bluebrown committed Nov 24, 2024
1 parent 62cb3e2 commit 12cb589
Show file tree
Hide file tree
Showing 21 changed files with 558 additions and 131 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
pull_request: {}
push:
branches:
- main
- main
permissions:
contents: read
pull-requests: read
Expand All @@ -13,8 +13,8 @@ jobs:
name: checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with: {go-version: '1.22.2'}
- run: gofmt -s -d -e .
- run: go test -cover ./...
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with: {go-version: '1.23.3'}
- run: gofmt -s -d -e .
- run: go test -cover ./...
49 changes: 24 additions & 25 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,34 @@ name: release
on:
push:
branches:
- main
- main
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- id: release
uses: google-github-actions/release-please-action@v4
with:
config-file: .github/release-please-config.json
manifest-file: .github/release-please-manifest.json
- if: ${{ steps.release.outputs.release_created }}
uses: actions/checkout@v3
- if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-go@v4
with:
go-version: '1.22.2'
- if: ${{ steps.release.outputs.release_created }}
env:
RELEASE_TAG: ${{ steps.release.outputs.tag_name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOBIN: ${{ github.workspace }}/bin
run: |
bash .github/build.sh -v "$RELEASE_TAG" -o dist <<EOT
linux/amd64
darwin/amd64
windows/amd64
EOT
gh release upload "$RELEASE_TAG" ./dist/*
- id: release
uses: google-github-actions/release-please-action@v4
with:
config-file: .github/release-please-config.json
manifest-file: .github/release-please-manifest.json
- if: ${{ steps.release.outputs.release_created }}
uses: actions/checkout@v3
- if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-go@v4
with:
go-version: '1.23.3'
- if: ${{ steps.release.outputs.release_created }}
env:
RELEASE_TAG: ${{ steps.release.outputs.tag_name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOBIN: ${{ github.workspace }}/bin
run: |
bash .github/build.sh -v "$RELEASE_TAG" -o dist <<EOT
linux/amd64
darwin/amd64
windows/amd64
EOT
gh release upload "$RELEASE_TAG" ./dist/*
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (C) 2021, 2022 by Nico Braun <[email protected]>
Copyright (C) 2021 by Nico Braun <[email protected]>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
Expand Down
66 changes: 51 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

Render json, yaml, & toml with go templates from the command line.

The templates are executed with the [text/template](https://pkg.go.dev/text/template) package. This means they come with the additional risks and benefits of the text template engine.
The templates are executed with the
[text/template](https://pkg.go.dev/text/template) package. This means
they come with the additional risks and benefits of the text template
engine.

## Synopsis

Expand All @@ -22,7 +25,9 @@ Options:

## Input Data

The input data is read from stdin via pipe or redirection. It is actually not required to provide any input data. If no input data is provided, the template is executed with nil data.
The input data is read from stdin via pipe or redirection. It is
actually not required to provide any input data. If no input data is
provided, the template is executed with nil data.

```bash
# Redirection
Expand All @@ -35,48 +40,76 @@ tpl '{{ . }}'

## Templates

The default templates name is `_gotpl_default` and positional arguments are parsed into this root template. That means while its possible to specify multiple arguments, they will overwrite each other unless they use the `define` keyword to define a named template that can be referenced later when executing the template. If a named template is parsed multiple times, the last one will override the previous ones.
The default templates name is `_gotpl_default` and positional arguments
are parsed into this root template. That means while its possible to
specify multiple arguments, they will overwrite each other unless they
use the `define` keyword to define a named template that can be
referenced later when executing the template. If a named template is
parsed multiple times, the last one will override the previous ones.

Templates from the flags `--file` and `--glob` are parsed in the order they are specified. So the override rules of the text/template package apply. If a file with the same name is specified multiple times, the last one wins. Even if they are in different directories.
Templates from the flags `--file` and `--glob` are parsed in the order
they are specified. So the override rules of the text/template package
apply. If a file with the same name is specified multiple times, the
last one wins. Even if they are in different directories.

The behavior of the cli tries to stay consistent with the actual behavior of the go template engine.
The behavior of the cli tries to stay consistent with the actual
behavior of the go template engine.

If the default template exists it will be used unless the `--name` flag is specified. If no default template exists because no positional argument has been provided, the template with the given file name is used, as long as only one file has been parsed. If multiple files have been parsed, the `--name` flag is required to avoid ambiguity.
If the default template exists it will be used unless the `--name` flag
is specified. If no default template exists because no positional
argument has been provided, the template with the given file name is
used, as long as only one file has been parsed. If multiple files have
been parsed, the `--name` flag is required to avoid ambiguity.

```bash
tpl '{{ . }}' --file foo.tpl --glob 'templates/*.tpl' # default will be used
tpl --file foo.tpl # foo.tpl will be used
tpl --file foo.tpl --glob 'templates/*.tpl' --name foo.tpl # the --name flag is required to select a template by name
```

The ability to parse multiple templates makes sense when defining helper snippets and other named templates to reference using the builtin `template` keyword or the custom `include` function which can be used in pipelines.
The ability to parse multiple templates makes sense when defining helper
snippets and other named templates to reference using the builtin
`template` keyword or the custom `include` function which can be used in
pipelines.

note globs need to quotes to avoid shell expansion.

## Decoders

By default input data is decoded as json and passed to the template to execute. It is possible to use an alternative decoder. The supported decoders are:
By default input data is decoded as json and passed to the template to
execute. It is possible to use an alternative decoder. The supported
decoders are:

- json
- yaml
- toml

While json could technically be decoded using the yaml decoder, this is not done by default for performance reasons.
While json could technically be decoded using the yaml decoder, this is
not done by default for performance reasons.

## Options

The `--options` flag is passed to the template engine. Possible options can be found in the [documentation of the template engine](https://pkg.go.dev/text/template#Template.Option).
The only option currently known is `missingkey`. Since the input data is decoded into `interface{}`, setting `missingkey=zero` will show `<no value>`, if the key does not exist, which is the same as the default. However, `missingkey=error` has some actual use cases.
The `--options` flag is passed to the template engine. Possible options
can be found in the [documentation of the template
engine](https://pkg.go.dev/text/template#Template.Option).
The only option currently known is `missingkey`. Since the input data is
decoded into `interface{}`, setting `missingkey=zero` will show `<no
value>`, if the key does not exist, which is the same as the default.
However, `missingkey=error` has some actual use cases.

## Functions

Next to the builtin functions, [Sprig functions](http://masterminds.github.io/sprig/) and [treasure-map functions](https://github.com/bluebrown/treasure-map) are available.
Next to the builtin functions, [Sprig
functions](http://masterminds.github.io/sprig/) and some [custom
function](./textfunc/) are available.

## Installation

### Binary

Download the binary from the [release page](https://github.com/bluebrown/go-template-cli/releases). For example
Download the binary from the [release
page](https://github.com/bluebrown/go-template-cli/releases). For
example

```bash
curl -fsSL https://github.com/bluebrown/go-template-cli/releases/latest/download/tpl-linux-amd64 >tpl
Expand All @@ -85,15 +118,18 @@ chmod 755 tpl

### Go

If you have go installed, you can use the `go install` command to install the binary.
If you have go installed, you can use the `go install` command to
install the binary.

```bash
go install github.com/bluebrown/go-template-cli/cmd/tpl@latest
```

## Example

Review the [examples](https://github.com/bluebrown/go-template-cli/tree/main/assets/examples) directory, for more examples.
Review the
[examples](https://github.com/bluebrown/go-template-cli/tree/main/assets/examples)
directory, for more examples.

```bash
curl -s https://jsonplaceholder.typicode.com/users | tpl '<table>
Expand Down
29 changes: 14 additions & 15 deletions cmd/tpl/cli.go → cli.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package cli

import (
"encoding/json"
Expand All @@ -10,15 +10,14 @@ import (

"github.com/BurntSushi/toml"
"github.com/Masterminds/sprig/v3"
"github.com/bluebrown/treasure-map/textfunc"
"github.com/bluebrown/go-template-cli/textfunc"
"github.com/spf13/pflag"
"gopkg.in/yaml.v3"
)

var version = "dev"

// the state of the program
type state struct {
type commandLine struct {
// options
defaultTemplateName string
files []string
Expand All @@ -34,14 +33,14 @@ type state struct {
template *template.Template
}

// create a new cli instance and bind flags to it
// create a New cli instance and bind flags to it
// flag.Parse is called on run
func new(fs *pflag.FlagSet) *state {
func New(fs *pflag.FlagSet) *commandLine {
if fs == nil {
fs = pflag.CommandLine
}

cli := &state{
cli := &commandLine{
flagSet: fs,
decoder: decodeJson,
defaultTemplateName: "_gotpl_default",
Expand All @@ -59,7 +58,7 @@ func new(fs *pflag.FlagSet) *state {
}

// parse the options and input, decode the input and render the result
func (cli *state) run(args []string, r io.Reader, w io.Writer) (err error) {
func (cli *commandLine) Run(args []string, r io.Reader, w io.Writer) (err error) {
if err := cli.parse(args); err != nil {
return fmt.Errorf("parse: %w", err)
}
Expand Down Expand Up @@ -87,7 +86,7 @@ func (cli *state) run(args []string, r io.Reader, w io.Writer) (err error) {
// 1. parse the options into the flagset
// 2. parse positional arguments into the default template
// 3. parse files and globs in the order specified
func (cli *state) parse(rawArgs []string) error {
func (cli *commandLine) parse(rawArgs []string) error {
if err := cli.parseFlagset(rawArgs); err != nil {
return fmt.Errorf("parse raw args: %s", err)
}
Expand All @@ -103,7 +102,7 @@ func (cli *state) parse(rawArgs []string) error {
return nil
}

func (cli *state) parseFlagset(rawArgs []string) error {
func (cli *commandLine) parseFlagset(rawArgs []string) error {
cli.flagSet.SortFlags = false

if err := cli.flagSet.Parse(rawArgs); err != nil {
Expand All @@ -117,7 +116,7 @@ func (cli *state) parseFlagset(rawArgs []string) error {

// parse all positional arguments into the "default" template. should be called
// after parseFlagset
func (cli *state) parsePositional() (err error) {
func (cli *commandLine) parsePositional() (err error) {
for _, arg := range cli.flagSet.Args() {
cli.template, err = cli.template.Parse(arg)
if err != nil {
Expand All @@ -129,7 +128,7 @@ func (cli *state) parsePositional() (err error) {

// parse files and globs in the order they were specified, to align with go's
// template engine. should be called after parseFlagset
func (cli *state) parseFilesAndGlobs(rawArgs []string) (*template.Template, error) {
func (cli *commandLine) parseFilesAndGlobs(rawArgs []string) (*template.Template, error) {
var (
err error
fileIndex uint8
Expand Down Expand Up @@ -158,7 +157,7 @@ func (cli *state) parseFilesAndGlobs(rawArgs []string) (*template.Template, erro
}

// decode the input stream into context data
func (cli *state) decode(r io.Reader) (any, error) {
func (cli *commandLine) decode(r io.Reader) (any, error) {
if r == nil || cli.decoder == nil {
return nil, nil
}
Expand Down Expand Up @@ -222,7 +221,7 @@ func decodeJson(in io.Reader, out any) error {
}

// render a template
func (cli *state) render(w io.Writer, data any) error {
func (cli *commandLine) render(w io.Writer, data any) error {
templateName, err := cli.selectTemplate()
if err != nil {
return fmt.Errorf("select template: %w", err)
Expand All @@ -244,7 +243,7 @@ func (cli *state) render(w io.Writer, data any) error {
// 2. default name, if at least 1 positional arg
// 3. templates name, if exactly 1 template
// 4. --name flag required, otherwise
func (cli *state) selectTemplate() (string, error) {
func (cli *commandLine) selectTemplate() (string, error) {
templates := cli.template.Templates()

if len(templates) == 0 {
Expand Down
4 changes: 2 additions & 2 deletions cmd/tpl/cli_test.go → cli_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package cli

import (
"bytes"
Expand Down Expand Up @@ -143,7 +143,7 @@ func Test_state_run(t *testing.T) {
in = strings.NewReader(tt.giveInput)
}

err := new(pflag.NewFlagSet(tt.name, pflag.ContinueOnError)).run(tt.giveArgs, in, output)
err := New(pflag.NewFlagSet(tt.name, pflag.ContinueOnError)).Run(tt.giveArgs, in, output)

if len(tt.wantErrorMatch) > 0 {
if err == nil {
Expand Down
4 changes: 3 additions & 1 deletion cmd/tpl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"io"
"os"

cli "github.com/bluebrown/go-template-cli"
)

func main() {
Expand All @@ -18,7 +20,7 @@ func main() {
}

// try to run the program
err := new(nil).run(os.Args[1:], input, os.Stdout)
err := cli.New(nil).Run(os.Args[1:], input, os.Stdout)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(2)
Expand Down
15 changes: 7 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@ module github.com/bluebrown/go-template-cli
go 1.22

require (
github.com/BurntSushi/toml v1.3.2
github.com/Masterminds/sprig/v3 v3.2.3
github.com/bluebrown/treasure-map v0.0.0-20220418173404-da5d8eccbd25
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/spf13/pflag v1.0.5
gopkg.in/yaml.v3 v3.0.1
)

require (
dario.cat/mergo v1.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
)
Loading

0 comments on commit 12cb589

Please sign in to comment.