diff --git a/README.md b/README.md index 788d0ae..5dd4fbf 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ In case of trouble: [Go Modules FAQ][mod-faq]. ## Tools -Go-toml provides two handy command line tools: +Go-toml provides three handy command line tools: * `tomljson`: Reads a TOML file and outputs its JSON representation. @@ -225,6 +225,13 @@ Go-toml provides two handy command line tools: $ jsontoml --help ``` + * `tomll`: Lints and reformats a TOML file. + + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest + $ tomll --help + ``` + ## Migrating from v1 This section describes the differences between v1 and v2, with some pointers on diff --git a/cmd/jsontoml/main.go b/cmd/jsontoml/main.go index 0470586..5f14fc6 100644 --- a/cmd/jsontoml/main.go +++ b/cmd/jsontoml/main.go @@ -13,15 +13,20 @@ import ( "github.com/pelletier/go-toml/v2/internal/cli" ) -func main() { - usage := `jsontoml can be used in two ways: +const usage = `jsontoml can be used in two ways: Reading from stdin: cat file.json | jsontoml > file.toml Reading from a file: jsontoml file.json > file.toml ` - cli.Execute(usage, convert) + +func main() { + p := cli.Program{ + Usage: usage, + Fn: convert, + } + p.Execute() } func convert(r io.Reader, w io.Writer) error { diff --git a/cmd/tomljson/main.go b/cmd/tomljson/main.go index 9627ae9..966e37d 100644 --- a/cmd/tomljson/main.go +++ b/cmd/tomljson/main.go @@ -15,15 +15,20 @@ import ( "github.com/pelletier/go-toml/v2/internal/cli" ) -func main() { - usage := `tomljson can be used in two ways: +const usage = `tomljson can be used in two ways: Reading from stdin: cat file.toml | tomljson > file.json Reading from a file: tomljson file.toml > file.json ` - cli.Execute(usage, convert) + +func main() { + p := cli.Program{ + Usage: usage, + Fn: convert, + } + p.Execute() } func convert(r io.Reader, w io.Writer) error { diff --git a/cmd/tomll/main.go b/cmd/tomll/main.go new file mode 100644 index 0000000..3cfca7e --- /dev/null +++ b/cmd/tomll/main.go @@ -0,0 +1,46 @@ +// Tomll is a linter for TOML +// +// Usage: +// cat file.toml | tomll > file_linted.toml +// tomll file1.toml file2.toml # lint the two files in place +package main + +import ( + "io" + + "github.com/pelletier/go-toml/v2" + "github.com/pelletier/go-toml/v2/internal/cli" +) + +const usage = `tomll can be used in two ways: + +Reading from stdin, writing to stdout: + cat file.toml | tomll > file.toml + +Reading and updating a list of files in place: + tomll a.toml b.toml c.toml + +When given a list of files, tomll will modify all files in place without asking. +` + +func main() { + p := cli.Program{ + Usage: usage, + Fn: convert, + Inplace: true, + } + p.Execute() +} + +func convert(r io.Reader, w io.Writer) error { + var v interface{} + + d := toml.NewDecoder(r) + err := d.Decode(&v) + if err != nil { + return err + } + + e := toml.NewEncoder(w) + return e.Encode(v) +} diff --git a/cmd/tomll/main_test.go b/cmd/tomll/main_test.go new file mode 100644 index 0000000..8f5db52 --- /dev/null +++ b/cmd/tomll/main_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvert(t *testing.T) { + examples := []struct { + name string + input string + expected string + errors bool + }{ + { + name: "valid toml", + input: ` +mytoml.a = 42.0 +`, + expected: `[mytoml] +a = 42.0 + +`, + }, + { + name: "invalid toml", + input: `[what`, + errors: true, + }, + } + + for _, e := range examples { + b := new(bytes.Buffer) + err := convert(strings.NewReader(e.input), b) + if e.errors { + require.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, e.expected, b.String()) + } + } +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 86acece..ea94578 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,22 +1,32 @@ package cli import ( + "bytes" "flag" "fmt" "io" + "io/ioutil" "os" ) type ConvertFn func(r io.Reader, w io.Writer) error -func Execute(usage string, fn ConvertFn) { - flag.Usage = func() { fmt.Fprintf(os.Stderr, usage) } - flag.Parse() - os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr, fn)) +type Program struct { + Usage string + Fn ConvertFn + // Inplace allows the command to take more than one file as argument and + // perform convertion in place on each provided file. + Inplace bool } -func processMain(files []string, input io.Reader, output, error io.Writer, f ConvertFn) int { - err := run(files, input, output, f) +func (p *Program) Execute() { + flag.Usage = func() { fmt.Fprintf(os.Stderr, p.Usage) } + flag.Parse() + os.Exit(p.main(flag.Args(), os.Stdin, os.Stdout, os.Stderr)) +} + +func (p *Program) main(files []string, input io.Reader, output, error io.Writer) int { + err := p.run(files, input, output) if err != nil { fmt.Fprintln(error, err.Error()) return -1 @@ -24,8 +34,11 @@ func processMain(files []string, input io.Reader, output, error io.Writer, f Con return 0 } -func run(files []string, input io.Reader, output io.Writer, convert ConvertFn) error { +func (p *Program) run(files []string, input io.Reader, output io.Writer) error { if len(files) > 0 { + if p.Inplace { + return p.runAllFilesInPlace(files) + } f, err := os.Open(files[0]) if err != nil { return err @@ -33,5 +46,31 @@ func run(files []string, input io.Reader, output io.Writer, convert ConvertFn) e defer f.Close() input = f } - return convert(input, output) + return p.Fn(input, output) +} + +func (p *Program) runAllFilesInPlace(files []string) error { + for _, path := range files { + err := p.runFileInPlace(path) + if err != nil { + return err + } + } + return nil +} + +func (p *Program) runFileInPlace(path string) error { + in, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + out := new(bytes.Buffer) + + err = p.Fn(bytes.NewReader(in), out) + if err != nil { + return err + } + + return ioutil.WriteFile(path, out.Bytes(), 0600) } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 2eeef8b..7624736 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "os" + "path" "strings" "testing" @@ -13,6 +14,11 @@ import ( "github.com/stretchr/testify/require" ) +func processMain(args []string, input io.Reader, stdout, stderr io.Writer, f ConvertFn) int { + p := Program{Fn: f} + return p.main(args, input, stdout, stderr) +} + func TestProcessMainStdin(t *testing.T) { stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) @@ -72,3 +78,79 @@ func TestProcessMainFileDoesNotExist(t *testing.T) { assert.Empty(t, stdout.String()) assert.NotEmpty(t, stderr.String()) } + +func TestProcessMainFilesInPlace(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path1 := path.Join(dir, "file1") + path2 := path.Join(dir, "file2") + + err = ioutil.WriteFile(path1, []byte("content 1"), 0600) + require.NoError(t, err) + err = ioutil.WriteFile(path2, []byte("content 2"), 0600) + require.NoError(t, err) + + p := Program{ + Fn: dummyFileFn, + Inplace: true, + } + + exit := p.main([]string{path1, path2}, os.Stdin, os.Stdout, os.Stderr) + + require.Equal(t, 0, exit) + + v1, err := ioutil.ReadFile(path1) + require.NoError(t, err) + require.Equal(t, "1", string(v1)) + + v2, err := ioutil.ReadFile(path2) + require.NoError(t, err) + require.Equal(t, "2", string(v2)) +} + +func TestProcessMainFilesInPlaceErrRead(t *testing.T) { + p := Program{ + Fn: dummyFileFn, + Inplace: true, + } + + exit := p.main([]string{"/this/path/is/invalid"}, os.Stdin, os.Stdout, os.Stderr) + + require.Equal(t, -1, exit) +} + +func TestProcessMainFilesInPlaceFailFn(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path1 := path.Join(dir, "file1") + + err = ioutil.WriteFile(path1, []byte("content 1"), 0600) + require.NoError(t, err) + + p := Program{ + Fn: func(io.Reader, io.Writer) error { return fmt.Errorf("oh no") }, + Inplace: true, + } + + exit := p.main([]string{path1}, os.Stdin, os.Stdout, os.Stderr) + + require.Equal(t, -1, exit) + + v1, err := ioutil.ReadFile(path1) + require.NoError(t, err) + require.Equal(t, "content 1", string(v1)) +} + +func dummyFileFn(r io.Reader, w io.Writer) error { + b, err := ioutil.ReadAll(r) + if err != nil { + return err + } + v := strings.SplitN(string(b), " ", 2)[1] + _, err = w.Write([]byte(v)) + return err +}