Browse Source

Initial cli rewrite

Pierre Curto 6 years ago
parent
commit
b71609e4ad

+ 31 - 0
cmd/lz4c/main.go

@@ -0,0 +1,31 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+
+	"github.com/pierrec/lz4/internal/cmdflag"
+	"github.com/pierrec/lz4/internal/cmds"
+)
+
+func init() {
+	const onError = flag.ExitOnError
+	cmdflag.New(
+		"compress", "[arguments] [<file name> ...]",
+		"Compress the given files or from stdin to stdout.",
+		onError, cmds.Compress)
+	cmdflag.New(
+		"uncompress", "[arguments] [<file name> ...]",
+		"Uncompress the given files or from stdin to stdout.",
+		onError, cmds.Uncompress)
+}
+
+func main() {
+	flag.CommandLine.Bool(cmdflag.VersionBoolFlag, false, "print the program version")
+
+	err := cmdflag.Parse()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+}

+ 1 - 1
go.mod

@@ -1,3 +1,3 @@
 module github.com/pierrec/lz4
 
-require github.com/pkg/profile v1.2.1
+require code.cloudfoundry.org/bytefmt v0.0.0-20180906201452-2aa6f33b730c

+ 2 - 2
go.sum

@@ -1,2 +1,2 @@
-github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
-github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
+code.cloudfoundry.org/bytefmt v0.0.0-20180906201452-2aa6f33b730c h1:VzwteSWGbW9mxXTEkH+kpnao5jbgLynw3hq742juQh8=
+code.cloudfoundry.org/bytefmt v0.0.0-20180906201452-2aa6f33b730c/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=

+ 152 - 0
internal/cmdflag/cmdflag.go

@@ -0,0 +1,152 @@
+// Package cmdflag adds single level cmdflag support to the standard library flag package.
+package cmdflag
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"runtime"
+	"runtime/debug"
+	"strings"
+	"sync"
+)
+
+//TODO add a -fullversion command to display module versions, compiler version
+//TODO add multi level command support
+
+// VersionBoolFlag is the flag name to be used as a boolean flag to display the program version.
+const VersionBoolFlag = "version"
+
+// Usage is the function used for help.
+var Usage = func() {
+	fset := flag.CommandLine
+	out := fset.Output()
+
+	program := programName(os.Args[0])
+	fmt.Fprintf(out, "Usage of %s:\n", program)
+	fset.PrintDefaults()
+
+	fmt.Fprintf(out, "\nSubcommands:")
+	for _, sub := range subs {
+		fmt.Fprintf(out, "\n%s\n%s %s\n", sub.desc, sub.name, sub.argsdesc)
+		fs, _ := sub.init(out)
+		fs.PrintDefaults()
+	}
+}
+
+// Handler is the function called when a matching cmdflag is found.
+type Handler func(...string) error
+
+type subcmd struct {
+	errh     flag.ErrorHandling
+	name     string
+	argsdesc string
+	desc     string
+	ini      func(*flag.FlagSet) Handler
+}
+
+func (sub *subcmd) init(out io.Writer) (*flag.FlagSet, Handler) {
+	fname := fmt.Sprintf("cmdflag `%s`", sub.name)
+	fs := flag.NewFlagSet(fname, sub.errh)
+	fs.SetOutput(out)
+	return fs, sub.ini(fs)
+}
+
+var (
+	mu   sync.Mutex
+	subs []*subcmd
+)
+
+// New instantiates a new cmdflag with its name and description.
+//
+// It is safe to be called from multiple go routines (typically in init functions).
+//
+// The cmdflag initializer is called only when the cmdflag is present on the command line.
+// The handler is called with the remaining arguments once the cmdflag flags have been parsed successfully.
+//
+// It will panic if the cmdflag already exists.
+func New(name, argsdesc, desc string, errh flag.ErrorHandling, initializer func(*flag.FlagSet) Handler) {
+	sub := &subcmd{
+		errh:     errh,
+		name:     name,
+		argsdesc: argsdesc,
+		desc:     desc,
+		ini:      initializer,
+	}
+
+	mu.Lock()
+	defer mu.Unlock()
+	for _, sub := range subs {
+		if sub.name == name {
+			panic(fmt.Errorf("cmdflag %s redeclared", name))
+		}
+	}
+	subs = append(subs, sub)
+}
+
+// Parse parses the command line arguments including the global flags and, if any, the cmdflag and its flags.
+//
+// If the VersionBoolFlag is defined as a global boolean flag, then the program version is displayed and the program stops.
+func Parse() error {
+	args := os.Args
+	if len(args) == 1 {
+		return nil
+	}
+
+	// Global flags.
+	fset := flag.CommandLine
+	fset.Usage = Usage
+	out := fset.Output()
+
+	if err := fset.Parse(args[1:]); err != nil {
+		return err
+	}
+
+	// Handle version request.
+	if f := fset.Lookup(VersionBoolFlag); f != nil {
+		if v, ok := f.Value.(flag.Getter); ok {
+			// All values implemented by the flag package implement the flag.Getter interface.
+			if b, ok := v.Get().(bool); ok && b {
+				// The flag was defined as a bool and is set.
+				program := programName(args[0])
+				if bi, ok := debug.ReadBuildInfo(); ok {
+					fmt.Fprintf(out, "%s version %s", program, bi.Main.Version)
+				} else {
+					fmt.Fprintf(out, "%s no version available (not built with module support)", program)
+				}
+				fmt.Fprintf(out, " %s/%s\n", runtime.GOOS, runtime.GOARCH)
+				return nil
+			}
+		}
+	}
+
+	// No cmdflag.
+	if fset.NArg() == 0 {
+		return nil
+	}
+
+	// Subcommand.
+	idx := len(args) - fset.NArg()
+	s := args[idx]
+	args = args[idx+1:]
+	for _, sub := range subs {
+		if sub.name != s {
+			continue
+		}
+
+		fs, handler := sub.init(out)
+		if err := fs.Parse(args); err != nil {
+			return err
+		}
+		return handler(args[len(args)-fs.NArg():]...)
+	}
+
+	return fmt.Errorf("%s is not a valid cmdflag", s)
+}
+
+func programName(s string) string {
+	name := filepath.Base(s)
+	return strings.TrimSuffix(name, ".exe")
+}

+ 163 - 0
internal/cmdflag/cmdflag_test.go

@@ -0,0 +1,163 @@
+package cmdflag_test
+
+import (
+	"flag"
+	"os"
+	"testing"
+
+	"github.com/pierrec/lz4/internal/cmdflag"
+)
+
+func TestGlobalFlagOnly(t *testing.T) {
+	cmd, args := flag.CommandLine, os.Args
+	defer func() { flag.CommandLine, os.Args = cmd, args }()
+
+	flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
+	var gv1 string
+	flag.StringVar(&gv1, "v1", "val1", "usage1")
+	os.Args = []string{"program", "-v1=gcli1"}
+
+	if err := cmdflag.Parse(); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := gv1, "gcli1"; got != want {
+		t.Fatalf("got %s; want %s", got, want)
+	}
+}
+
+func TestInvalidcmdflag(t *testing.T) {
+	cmd, args := flag.CommandLine, os.Args
+	defer func() { flag.CommandLine, os.Args = cmd, args }()
+
+	flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
+	os.Args = []string{"program", "invalidsub"}
+
+	if err := cmdflag.Parse(); err == nil {
+		t.Fatal("expected invalid command error")
+	}
+}
+
+func TestOnecmdflag(t *testing.T) {
+	h1 := 0
+	handle1 := func(fset *flag.FlagSet) cmdflag.Handler {
+		return func(args ...string) error {
+			h1++
+			return nil
+		}
+	}
+	cmdflag.New("sub1", "", "", flag.ExitOnError, handle1)
+
+	args := os.Args
+	defer func() { os.Args = args }()
+	os.Args = []string{"program", "sub1"}
+
+	if err := cmdflag.Parse(); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := h1, 1; got != want {
+		t.Fatalf("got %d; want %d", got, want)
+	}
+}
+
+func TestOnecmdflagOneFlag(t *testing.T) {
+	h1 := 0
+	handle1 := func(fset *flag.FlagSet) cmdflag.Handler {
+		h1++
+
+		var v1 string
+		fset.StringVar(&v1, "v1", "val1", "usage1")
+
+		return func(args ...string) error {
+			if got, want := v1, "cli1"; got != want {
+				t.Fatalf("got %s; want %s", got, want)
+			}
+			return nil
+		}
+	}
+	cmdflag.New("sub1flag", "", "", flag.ExitOnError, handle1)
+
+	args := os.Args
+	defer func() { os.Args = args }()
+	os.Args = []string{"program", "sub1flag", "-v1=cli1"}
+
+	if err := cmdflag.Parse(); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := h1, 1; got != want {
+		t.Fatalf("got %d; want %d", got, want)
+	}
+}
+
+func TestGlobalFlagOnecmdflag(t *testing.T) {
+	h1 := 0
+	handle1 := func(fset *flag.FlagSet) cmdflag.Handler {
+		h1++
+
+		var v1 string
+		fset.StringVar(&v1, "v1", "val1", "usage1")
+
+		return func(args ...string) error {
+			return nil
+		}
+	}
+	cmdflag.New("subglobal", "", "", flag.ExitOnError, handle1)
+
+	cmd, args := flag.CommandLine, os.Args
+	defer func() { flag.CommandLine, os.Args = cmd, args }()
+
+	flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
+	var gv1 string
+	flag.StringVar(&gv1, "v1", "val1", "usage1")
+
+	os.Args = []string{"program", "-v1=gcli1", "subglobal"}
+
+	if err := cmdflag.Parse(); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := h1, 1; got != want {
+		t.Fatalf("got %d; want %d", got, want)
+	}
+
+	if got, want := gv1, "gcli1"; got != want {
+		t.Fatalf("got %s; want %s", got, want)
+	}
+}
+
+func TestGlobalFlagOnecmdflagOneFlag(t *testing.T) {
+	h1 := 0
+	handle1 := func(fset *flag.FlagSet) cmdflag.Handler {
+		h1++
+
+		var v1 string
+		fset.StringVar(&v1, "v1", "val1", "usage1")
+
+		return func(args ...string) error {
+			if got, want := v1, "cli1"; got != want {
+				t.Fatalf("got %s; want %s", got, want)
+			}
+			return nil
+		}
+	}
+	cmdflag.New("subglobal1flag", "", "", flag.ExitOnError, handle1)
+
+	cmd, args := flag.CommandLine, os.Args
+	defer func() { flag.CommandLine, os.Args = cmd, args }()
+
+	flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
+	var gv1 string
+	flag.StringVar(&gv1, "v1", "val1", "usage1")
+
+	os.Args = []string{"program", "subglobal1flag", "-v1=cli1"}
+
+	if err := cmdflag.Parse(); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := h1, 1; got != want {
+		t.Fatalf("got %d; want %d", got, want)
+	}
+}

+ 45 - 0
internal/cmdflag/example_test.go

@@ -0,0 +1,45 @@
+package cmdflag_test
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"github.com/pierrec/lz4/internal/cmdflag"
+)
+
+func ExampleParse() {
+	// Declare the `split` cmdflag.
+	cmdflag.New(
+		"split",
+		"argsdesc",
+		"desc",
+		flag.ExitOnError,
+		func(fs *flag.FlagSet) cmdflag.Handler {
+			// Declare the cmdflag specific flags.
+			var s string
+			fs.StringVar(&s, "s", "", "string to be split")
+
+			// Return the handler to be executed when the cmdflag is found.
+			return func(...string) error {
+				i := len(s) / 2
+				fmt.Println(s[:i], s[i:])
+				return nil
+			}
+		})
+
+	// The following is only used to emulate passing command line arguments to `program`.
+	// It is equivalent to running:
+	// ./program split -s hello
+	args := os.Args
+	defer func() { os.Args = args }()
+	os.Args = []string{"program", "split", "-s", "hello"}
+
+	// Process the command line arguments.
+	if err := cmdflag.Parse(); err != nil {
+		panic(err)
+	}
+
+	// Output:
+	// he llo
+}

+ 87 - 0
internal/cmds/compress.go

@@ -0,0 +1,87 @@
+package cmds
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+
+	"code.cloudfoundry.org/bytefmt"
+
+	"github.com/pierrec/lz4"
+	"github.com/pierrec/lz4/internal/cmdflag"
+)
+
+//TODO add progress bar and stats
+
+// Compress compresses a set of files or from stdin to stdout.
+func Compress(fs *flag.FlagSet) cmdflag.Handler {
+	var blockMaxSize string
+	fs.StringVar(&blockMaxSize, "size", "4M", "block max size [64K,256K,1M,4M]")
+	var blockChecksum bool
+	fs.BoolVar(&blockChecksum, "bc", false, "enable block checksum")
+	var streamChecksum bool
+	fs.BoolVar(&streamChecksum, "sc", false, "disable stream checksum")
+	var level int
+	fs.IntVar(&level, "l", 0, "compression level (0=fastest)")
+
+	return func(args ...string) error {
+		sz, err := bytefmt.ToBytes(blockMaxSize)
+		if err != nil {
+			return err
+		}
+
+		zw := lz4.NewWriter(nil)
+		zw.Header = lz4.Header{
+			BlockChecksum:    blockChecksum,
+			BlockMaxSize:     int(sz),
+			NoChecksum:       streamChecksum,
+			CompressionLevel: level,
+		}
+
+		// Use stdin/stdout if no file provided.
+		if len(args) == 0 {
+			zw.Reset(os.Stdout)
+			_, err := io.Copy(zw, os.Stdin)
+			if err != nil {
+				return err
+			}
+			return zw.Close()
+		}
+
+		for _, filename := range args {
+			// Input file.
+			file, err := os.Open(filename)
+			if err != nil {
+				return err
+			}
+			finfo, err := file.Stat()
+			if err != nil {
+				return err
+			}
+			mode := finfo.Mode() // use the same mode for the output file
+
+			// Output file.
+			zfilename := fmt.Sprintf("%s%s", filename, lz4.Extension)
+			zfile, err := os.OpenFile(zfilename, os.O_CREATE|os.O_WRONLY, mode)
+			if err != nil {
+				return err
+			}
+			zw.Reset(zfile)
+
+			// Compress.
+			_, err = io.Copy(zw, file)
+			if err != nil {
+				return err
+			}
+			for _, c := range []io.Closer{zw, zfile} {
+				err := c.Close()
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		return nil
+	}
+}

+ 62 - 0
internal/cmds/uncompress.go

@@ -0,0 +1,62 @@
+package cmds
+
+import (
+	"flag"
+	"io"
+	"os"
+	"strings"
+
+	"github.com/pierrec/lz4"
+	"github.com/pierrec/lz4/internal/cmdflag"
+)
+
+//TODO add progress bar and stats
+
+// Uncompress uncompresses a set of files or from stdin to stdout.
+func Uncompress(_ *flag.FlagSet) cmdflag.Handler {
+	return func(args ...string) error {
+		zr := lz4.NewReader(nil)
+
+		// Use stdin/stdout if no file provided.
+		if len(args) == 0 {
+			zr.Reset(os.Stdin)
+			_, err := io.Copy(os.Stdout, zr)
+			return err
+		}
+
+		for _, zfilename := range args {
+			// Input file.
+			zfile, err := os.Open(zfilename)
+			if err != nil {
+				return err
+			}
+			zinfo, err := zfile.Stat()
+			if err != nil {
+				return err
+			}
+			mode := zinfo.Mode() // use the same mode for the output file
+
+			// Output file.
+			filename := strings.TrimSuffix(zfilename, lz4.Extension)
+			file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, mode)
+			if err != nil {
+				return err
+			}
+			zr.Reset(zfile)
+
+			// Uncompress.
+			_, err = io.Copy(file, zr)
+			if err != nil {
+				return err
+			}
+			for _, c := range []io.Closer{zfile, file} {
+				err := c.Close()
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		return nil
+	}
+}

+ 0 - 122
lz4c/main.go

@@ -1,122 +0,0 @@
-// Command line utility for the lz4 package.
-package main
-
-import (
-	"flag"
-	"fmt"
-	"io"
-	"log"
-	"os"
-	"path"
-	"runtime"
-	"strings"
-
-	"github.com/pierrec/lz4"
-	"github.com/pkg/profile"
-)
-
-func main() {
-	// Process command line arguments
-	var (
-		blockMaxSizeDefault  = 4 << 20
-		flagStdout           = flag.Bool("c", false, "output to stdout")
-		flagDecompress       = flag.Bool("d", false, "decompress flag")
-		flagBlockMaxSize     = flag.Int("B", blockMaxSizeDefault, "block max size [64Kb,256Kb,1Mb,4Mb]")
-		flagBlockChecksum    = flag.Bool("BX", false, "enable block checksum")
-		flagStreamChecksum   = flag.Bool("Sx", false, "disable stream checksum")
-		flagCompressionLevel = flag.Int("l", 0, "compression level (0=fastest)")
-		profileName          = flag.String("p", "", "path to the profile file")
-		mode                 = flag.String("profile.mode", "", "enable profiling mode, one of [cpu, mem, mutex, block]")
-	)
-	flag.Usage = func() {
-		fmt.Fprintf(os.Stderr, "Usage:\n\t%s [arg] [input]...\n\tNo input means [de]compress stdin to stdout\n\n", os.Args[0])
-		flag.PrintDefaults()
-	}
-	flag.Parse()
-
-	ppath := profile.ProfilePath(*profileName)
-	switch *mode {
-	case "cpu":
-		defer profile.Start(profile.CPUProfile, ppath).Stop()
-	case "mem":
-		defer profile.Start(profile.MemProfile, ppath).Stop()
-	case "mutex":
-		defer profile.Start(profile.MutexProfile, ppath).Stop()
-	case "block":
-		defer profile.Start(profile.BlockProfile, ppath).Stop()
-	default:
-		// do nothing
-	}
-	flag.Parse()
-
-	// Use all CPUs
-	runtime.GOMAXPROCS(runtime.NumCPU())
-
-	zr := lz4.NewReader(nil)
-	zw := lz4.NewWriter(nil)
-	zh := lz4.Header{
-		BlockChecksum:    *flagBlockChecksum,
-		BlockMaxSize:     *flagBlockMaxSize,
-		NoChecksum:       *flagStreamChecksum,
-		CompressionLevel: *flagCompressionLevel,
-	}
-
-	worker := func(in io.Reader, out io.Writer) {
-		if *flagDecompress {
-			zr.Reset(in)
-			if _, err := io.Copy(out, zr); err != nil {
-				log.Fatalf("Error while decompressing input: %v", err)
-			}
-		} else {
-			zw.Reset(out)
-			zw.Header = zh
-			if _, err := io.Copy(zw, in); err != nil {
-				log.Fatalf("Error while compressing input: %v", err)
-			}
-			if err := zw.Close(); err != nil {
-				log.Fatalf("Error while closing stream: %v", err)
-			}
-		}
-	}
-
-	// No input means [de]compress stdin to stdout
-	if len(flag.Args()) == 0 {
-		worker(os.Stdin, os.Stdout)
-		os.Exit(0)
-	}
-
-	// Compress or decompress all input files
-	for _, inputFileName := range flag.Args() {
-		outputFileName := path.Clean(inputFileName)
-
-		if !*flagStdout {
-			if *flagDecompress {
-				outputFileName = strings.TrimSuffix(outputFileName, lz4.Extension)
-				if outputFileName == inputFileName {
-					log.Fatalf("Invalid output file name: same as input: %s", inputFileName)
-				}
-			} else {
-				outputFileName += lz4.Extension
-			}
-		}
-
-		inputFile, err := os.Open(inputFileName)
-		if err != nil {
-			log.Fatalf("Error while opening input: %v", err)
-		}
-
-		outputFile := os.Stdout
-		if !*flagStdout {
-			outputFile, err = os.Create(outputFileName)
-			if err != nil {
-				log.Fatalf("Error while opening output: %v", err)
-			}
-		}
-		worker(inputFile, outputFile)
-
-		inputFile.Close()
-		if !*flagStdout {
-			outputFile.Close()
-		}
-	}
-}