-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hujsonfmt): add basic hujsonfmt CLI command
Added as a separate Go module with its own go.mod file to avoid polluting the root module's dependency tree. The CLI interface and flags are very similar to gofmt: $ hujsonfmt -h usage: hujsonfmt [flags] [path ...] -d display diffs instead of rewriting files -l list files whose formatting differs from hujsonfmt's -m minify results -s standardize results to plain JSON -w write result to (source) file instead of stdout Given paths can either point directly to a file, or to a directory. In the case of a directory, it will be recursively walked finding all *.hujson files. Input can also be provided via stdin if no paths are provided, or a single path of "-" is provided: $ echo '{\n// hai\n"foo":"bar"}' | ./hujsonfmt { // hai "foo": "bar", }
- Loading branch information
Showing
4 changed files
with
267 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module github.com/tailscale/hujson/cmd/hujsonfmt | ||
|
||
go 1.18 | ||
|
||
require ( | ||
github.com/hexops/gotextdiff v1.0.3 | ||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= | ||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | ||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= | ||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f h1:n4r/sJ92cBSBHK8n9lR1XLFr0OiTVeGfN5TR+9LaN7E= | ||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"runtime" | ||
"strings" | ||
|
||
"github.com/hexops/gotextdiff" | ||
"github.com/hexops/gotextdiff/myers" | ||
"github.com/hexops/gotextdiff/span" | ||
"github.com/tailscale/hujson" | ||
) | ||
|
||
var ( | ||
min = flag.Bool("m", false, "minify results") | ||
stand = flag.Bool("s", false, "standardize results to plain JSON") | ||
diff = flag.Bool("d", false, "display diffs instead of rewriting files") | ||
list = flag.Bool("l", false, | ||
"list files whose formatting differs from hujsonfmt's", | ||
) | ||
write = flag.Bool("w", false, | ||
"write result to (source) file instead of stdout", | ||
) | ||
|
||
chmodSupported = runtime.GOOS != "windows" | ||
huJSONExt = ".hujson" | ||
) | ||
|
||
func usage() { | ||
fmt.Fprintf(os.Stderr, "usage: hujsonfmt [flags] [path ...]\n") | ||
flag.PrintDefaults() | ||
} | ||
|
||
func main() { | ||
err := mainE() | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "%s\n", err) | ||
usage() | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func mainE() error { | ||
flag.Usage = usage | ||
flag.Parse() | ||
|
||
args := flag.Args() | ||
|
||
if len(args) == 0 || (len(args) == 1 && args[0] == "-") { | ||
stat, _ := os.Stdin.Stat() | ||
if (stat.Mode() & os.ModeCharDevice) != 0 { | ||
return fmt.Errorf("no files paths or stdin provided") | ||
} | ||
if *write { | ||
return fmt.Errorf("cannot use -w with standard input") | ||
} | ||
|
||
return processFile(nil, "<standard input>", os.Stdin) | ||
} | ||
|
||
for _, arg := range args { | ||
info, err := os.Stat(arg) | ||
switch { | ||
case err != nil: | ||
return err | ||
case !info.IsDir(): | ||
err := processFile(info, arg, nil) | ||
if err != nil { | ||
return err | ||
} | ||
default: | ||
err := filepath.WalkDir( | ||
arg, | ||
func(path string, f fs.DirEntry, err error) error { | ||
if err != nil || !isHuJSONFile(f) { | ||
return err | ||
} | ||
|
||
return processFile(info, path, nil) | ||
}, | ||
) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func isHuJSONFile(f fs.DirEntry) bool { | ||
return strings.HasSuffix(f.Name(), huJSONExt) && !f.IsDir() | ||
} | ||
|
||
func processFile(info fs.FileInfo, filename string, in io.Reader) error { | ||
src, err := readFile(filename, in) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// The main hujson functions will sometimes modify the original input byte | ||
// slice. Hence we create a copy of the src byte slice to avoid modifying | ||
// src, enabling us to reliably print diffs. | ||
input := make([]byte, len(src)) | ||
_ = copy(input, src) | ||
|
||
output, err := processSrc(input) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
switch { | ||
case *diff: | ||
printDiff(filename, src, output) | ||
case *list: | ||
fmt.Println(filename) | ||
case *write: | ||
err = writeFile(info, filename, src, output) | ||
if err != nil { | ||
return err | ||
} | ||
default: | ||
fmt.Print(string(output)) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func readFile(path string, in io.Reader) ([]byte, error) { | ||
if in == nil { | ||
f, err := os.Open(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer f.Close() | ||
in = f | ||
} | ||
|
||
src, err := io.ReadAll(in) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return src, nil | ||
} | ||
|
||
func processSrc(src []byte) ([]byte, error) { | ||
var r []byte | ||
var err error | ||
switch { | ||
case *min: | ||
r, err = hujson.Minimize(src) | ||
case *stand: | ||
r, err = hujson.Standardize(src) | ||
default: | ||
r, err = hujson.Format(src) | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return r, nil | ||
} | ||
|
||
func printDiff(filename string, src, modified []byte) { | ||
origFile := filename + ".orig" | ||
old := string(src) | ||
new := string(modified) | ||
edits := myers.ComputeEdits( | ||
span.URIFromPath(origFile), old, new, | ||
) | ||
diff := fmt.Sprint( | ||
gotextdiff.ToUnified(origFile, filename, old, edits), | ||
) | ||
|
||
if diff == "" { | ||
return | ||
} | ||
|
||
fmt.Printf("diff %s %s\n", origFile, filename) | ||
fmt.Println(diff) | ||
} | ||
|
||
func writeFile(info fs.FileInfo, filename string, src, data []byte) error { | ||
if info == nil { | ||
panic("-w should not have been allowed with standard input") | ||
} | ||
|
||
perms := info.Mode().Perm() | ||
|
||
var bak string | ||
bak, err := backupFile(filename, src, perms) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = os.WriteFile(filename, data, perms) | ||
if err != nil { | ||
_ = os.Rename(bak, filename) | ||
|
||
return err | ||
} | ||
|
||
err = os.Remove(bak) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func backupFile( | ||
filename string, | ||
data []byte, | ||
perms fs.FileMode, | ||
) (backupFile string, err error) { | ||
var f *os.File | ||
f, err = os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)) | ||
if err != nil { | ||
return "", err | ||
} | ||
defer f.Close() | ||
|
||
backupFile = f.Name() | ||
|
||
if chmodSupported { | ||
err = f.Chmod(perms) | ||
if err != nil { | ||
_ = os.Remove(backupFile) | ||
|
||
return "", err | ||
} | ||
} | ||
|
||
_, err = f.Write(data) | ||
if err != nil { | ||
_ = os.Remove(backupFile) | ||
|
||
return "", err | ||
} | ||
|
||
return backupFile, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
go 1.18 | ||
|
||
use ( | ||
. | ||
./cmd/hujsonfmt | ||
) |