From 15979209ebd4f9ffc07b566cccf37a4baefd5bc6 Mon Sep 17 00:00:00 2001 From: pg9182 <96569817+pg9182@users.noreply.github.com> Date: Sat, 13 Apr 2024 16:35:52 -0400 Subject: [PATCH] vpkutil, cmd/*: Refactor common CLI argument parsing and setup --- cmd/tf2-vpk2tar/main.go | 62 +++++------------- cmd/tf2-vpklist/main.go | 51 ++++----------- cmd/tf2-vpkoptim/main.go | 34 ++-------- cmd/tf2-vpkunpack/main.go | 64 +++++-------------- vpkutil/vpkcli.go | 131 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 163 deletions(-) create mode 100644 vpkutil/vpkcli.go diff --git a/cmd/tf2-vpk2tar/main.go b/cmd/tf2-vpk2tar/main.go index 8f60ca2..b46b219 100644 --- a/cmd/tf2-vpk2tar/main.go +++ b/cmd/tf2-vpk2tar/main.go @@ -9,20 +9,19 @@ import ( "runtime" "time" - "github.com/pg9182/tf2vpk" - "github.com/pg9182/tf2vpk/internal" + "github.com/pg9182/tf2vpk/vpkutil" "github.com/spf13/pflag" ) var ( - Output = pflag.StringP("output", "o", "-", "The file to write the tar archive to") - VPKPrefix = pflag.StringP("vpk-prefix", "p", "english", "VPK prefix") - Verbose = pflag.BoolP("verbose", "v", false, "Write information about processed files to stderr") - Test = pflag.BoolP("test", "t", false, "Don't create a tar archive; only attempt to read the entire VPK and verify checksums") - Threads = pflag.IntP("threads", "j", runtime.NumCPU(), "The number of decompression threads to use (0 to only decompress chunks as they are read) (defaults to the number of cores)") + ResolveOpen = vpkutil.NewCLIResolveOpen(pflag.CommandLine, 0, false) - Exclude = pflag.StringSlice("exclude", nil, "Excludes files or directories matching the provided glob (anchor to the start with /)") - Include = pflag.StringSlice("include", nil, "Negates --exclude for files or directories matching the provided glob") + Output = pflag.StringP("output", "o", "-", "The file to write the tar archive to") + Verbose = pflag.BoolP("verbose", "v", false, "Write information about processed files to stderr") + Test = pflag.BoolP("test", "t", false, "Don't create a tar archive; only attempt to read the entire VPK and verify checksums") + Threads = pflag.IntP("threads", "j", runtime.NumCPU(), "The number of decompression threads to use (0 to only decompress chunks as they are read) (defaults to the number of cores)") + + IncludeExclude = vpkutil.NewCLIIncludeExclude(pflag.CommandLine) Help = pflag.BoolP("help", "h", false, "Show this help message") ) @@ -30,9 +29,8 @@ var ( func main() { pflag.Parse() - argv := pflag.Args() - if len(argv) == 0 || len(argv) > 2 || *Help { - fmt.Fprintf(os.Stderr, "usage: %s [options] (vpk_dir vpk_name)|vpk_path\n\noptions:\n%s", os.Args[0], pflag.CommandLine.FlagUsages()) + if !ResolveOpen.ArgCheck() || *Help { + fmt.Fprintf(os.Stderr, "usage: %s [options] %s\n\noptions:\n%s", os.Args[0], ResolveOpen.ArgHelp(), pflag.CommandLine.FlagUsages()) if !*Help { os.Exit(2) } @@ -46,23 +44,9 @@ func main() { runtime.GOMAXPROCS(*Threads) } - var ( - err error - vpk tf2vpk.ValvePak - ) - if len(argv) == 2 { - vpk, err = tf2vpk.VPK(argv[0], *VPKPrefix, argv[1]), nil - } else { - vpk, err = tf2vpk.VPKFromPath(argv[0], *VPKPrefix) - } + _, r, err := ResolveOpen.ResolveOpen() if err != nil { - fmt.Fprintf(os.Stderr, "error: resolve vpk: %v\n", err) - os.Exit(1) - } - - r, err := tf2vpk.NewReader(vpk) - if err != nil { - fmt.Fprintf(os.Stderr, "error: open vpk: %v\n", err) + fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } defer r.Close() @@ -92,24 +76,10 @@ func main() { } for _, f := range r.Root.File { - var excluded bool - for _, x := range *Exclude { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - fmt.Fprintf(os.Stderr, "error: process excludes: match %q against glob %q: %v\n", f.Path, x, err) - os.Exit(1) - } else if m { - excluded = true - } - } - for _, x := range *Include { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - fmt.Fprintf(os.Stderr, "error: process includes: match %q against glob %q: %v\n", f.Path, x, err) - os.Exit(1) - } else if m { - excluded = false - } - } - if excluded { + if skip, err := IncludeExclude.Skip(f); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } else if skip { continue } var sz uint64 diff --git a/cmd/tf2-vpklist/main.go b/cmd/tf2-vpklist/main.go index 3afd640..ea24de3 100644 --- a/cmd/tf2-vpklist/main.go +++ b/cmd/tf2-vpklist/main.go @@ -10,11 +10,12 @@ import ( "github.com/pg9182/tf2vpk" "github.com/pg9182/tf2vpk/internal" + "github.com/pg9182/tf2vpk/vpkutil" "github.com/spf13/pflag" ) var ( - VPKPrefix = pflag.StringP("vpk-prefix", "p", "english", "VPK prefix") + ResolveOpen = vpkutil.NewCLIResolveOpen(pflag.CommandLine, 0, false) HumanReadable = pflag.BoolP("human-readable", "h", false, "Show values in human-readable form") HumanReadableFlags = pflag.BoolP("human-readable-flags", "f", false, "If displaying flags, also show them in human-readable form at the very end of the line (delimited by a #)") @@ -24,8 +25,7 @@ var ( Threads = pflag.IntP("threads", "j", runtime.NumCPU(), "The number of decompression threads to use while verifying checksums (0 to only decompress chunks as they are read) (defaults to the number of cores)") - Exclude = pflag.StringSlice("exclude", nil, "Excludes files or directories matching the provided glob (anchor to the start with /)") - Include = pflag.StringSlice("include", nil, "Negates --exclude for files or directories matching the provided glob") + IncludeExclude = vpkutil.NewCLIIncludeExclude(pflag.CommandLine) Help = pflag.Bool("help", false, "Show this help message") ) @@ -33,9 +33,8 @@ var ( func main() { pflag.Parse() - argv := pflag.Args() - if len(argv) == 0 || len(argv) > 2 || *Help { - fmt.Fprintf(os.Stderr, "usage: %s [options] (vpk_dir vpk_name)|vpk_path\n\noptions:\n%s", os.Args[0], pflag.CommandLine.FlagUsages()) + if !ResolveOpen.ArgCheck() || *Help { + fmt.Fprintf(os.Stderr, "usage: %s [options] %s\n\noptions:\n%s", os.Args[0], ResolveOpen.ArgHelp(), pflag.CommandLine.FlagUsages()) if !*Help { os.Exit(2) } @@ -49,23 +48,9 @@ func main() { runtime.GOMAXPROCS(*Threads) } - var ( - err error - vpk tf2vpk.ValvePak - ) - if len(argv) == 2 { - vpk, err = tf2vpk.VPK(argv[0], *VPKPrefix, argv[1]), nil - } else { - vpk, err = tf2vpk.VPKFromPath(argv[0], *VPKPrefix) - } - if err != nil { - fmt.Fprintf(os.Stderr, "error: resolve vpk: %v\n", err) - os.Exit(1) - } - - r, err := tf2vpk.NewReader(vpk) + _, r, err := ResolveOpen.ResolveOpen() if err != nil { - fmt.Fprintf(os.Stderr, "error: open vpk: %v\n", err) + fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } defer r.Close() @@ -77,24 +62,10 @@ func main() { var testErrCount int for _, f := range r.Root.File { - var excluded bool - for _, x := range *Exclude { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - fmt.Fprintf(os.Stderr, "error: process excludes: match %q against glob %q: %v\n", f.Path, x, err) - os.Exit(1) - } else if m { - excluded = true - } - } - for _, x := range *Include { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - fmt.Fprintf(os.Stderr, "error: process includes: match %q against glob %q: %v\n", f.Path, x, err) - os.Exit(1) - } else if m { - excluded = false - } - } - if excluded { + if skip, err := IncludeExclude.Skip(f); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } else if skip { continue } diff --git a/cmd/tf2-vpkoptim/main.go b/cmd/tf2-vpkoptim/main.go index 2768d06..030c85c 100644 --- a/cmd/tf2-vpkoptim/main.go +++ b/cmd/tf2-vpkoptim/main.go @@ -14,6 +14,7 @@ import ( "github.com/pg9182/tf2vpk" "github.com/pg9182/tf2vpk/internal" + "github.com/pg9182/tf2vpk/vpkutil" "github.com/spf13/pflag" ) @@ -24,9 +25,7 @@ var ( DryRun = pflag.BoolP("dry-run", "n", false, "Don't write output files") Merge = pflag.Bool("merge", false, "Merges all blocks (i.e., _XXX.vpk)") - Exclude = pflag.StringSlice("exclude", nil, "Excludes files or directories matching the provided glob (anchor to the start with /)") - ExcludeBSPLump = pflag.IntSlice("exclude-bsp-lump", nil, "Shortcut for --exclude to remove %04x.bsp_lump") - Include = pflag.StringSlice("include", nil, "Negates --exclude for files or directories matching the provided glob") + IncludeExclude = vpkutil.NewCLIIncludeExclude(pflag.CommandLine) Help = pflag.BoolP("help", "h", false, "Show this help message") ) @@ -177,32 +176,9 @@ func optimize(ctx context.Context, inputDir, outputDir, vpkName string) error { var nf []tf2vpk.ValvePakFile var nfd int for _, f := range r.Root.File { - var excluded bool - for _, x := range *Exclude { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - return fmt.Errorf("process excludes: match %q against glob %q: %w", f.Path, x, err) - } else if m { - vlog(VDebug, "--- exclude %q: matched %q", x, f.Path) - excluded = true - } - } - for _, x := range *ExcludeBSPLump { - if m, err := internal.MatchGlobParents(fmt.Sprintf("%04x.bsp_lump", x), f.Path); err != nil { - return fmt.Errorf("process bsp lump excludes: match %q against glob %q: %w", f.Path, x, err) - } else if m { - vlog(VDebug, "--- exclude lump %d: matched %q", x, f.Path) - excluded = true - } - } - for _, x := range *Include { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - return fmt.Errorf("process includes: match %q against glob %q: %w", f.Path, x, err) - } else if m { - excluded = false - vlog(VDebug, "--- include %q: matched %q", x, f.Path) - } - } - if excluded { + if skip, err := IncludeExclude.Skip(f); err != nil { + return err + } else if skip { vlog(VVerbose, "--- excluding %s", f.Path) nfd++ continue diff --git a/cmd/tf2-vpkunpack/main.go b/cmd/tf2-vpkunpack/main.go index 754b462..b8a7fd6 100644 --- a/cmd/tf2-vpkunpack/main.go +++ b/cmd/tf2-vpkunpack/main.go @@ -11,21 +11,19 @@ import ( "path/filepath" "runtime" - "github.com/pg9182/tf2vpk" "github.com/pg9182/tf2vpk/internal" "github.com/pg9182/tf2vpk/vpkutil" "github.com/spf13/pflag" ) var ( - VPKPrefix = pflag.StringP("vpk-prefix", "p", "english", "VPK prefix") + ResolveOpen = vpkutil.NewCLIResolveOpen(pflag.CommandLine, 1, true) VPKFlagsExplicit = pflag.Bool("vpkflags-explicit", false, "Do not optimize vpkflags for inheritance; generate one line for each file") VPKIgnoreEmpty = pflag.Bool("vpkignore-no-default", false, "Do not add default vpkignore entries") Threads = pflag.IntP("threads", "j", runtime.NumCPU(), "The number of decompression threads to use while verifying checksums (0 to only decompress chunks as they are read) (defaults to the number of cores)") - Exclude = pflag.StringSlice("exclude", nil, "Excludes files or directories matching the provided glob (anchor to the start with /)") - Include = pflag.StringSlice("include", nil, "Negates --exclude for files or directories matching the provided glob") + IncludeExclude = vpkutil.NewCLIIncludeExclude(pflag.CommandLine) Help = pflag.Bool("help", false, "Show this help message") ) @@ -33,9 +31,9 @@ var ( func main() { pflag.Parse() - argv := pflag.Args() - if len(argv) == 0 || len(argv) > 3 || *Help { - fmt.Fprintf(os.Stderr, "usage: %s [options] empty_output_path [(vpk_dir vpk_name)|vpk_path]\n\noptions:\n%s", os.Args[0], pflag.CommandLine.FlagUsages()) + args := pflag.Args() + if len(args) == 0 || !ResolveOpen.ArgCheck() || *Help { + fmt.Fprintf(os.Stderr, "usage: %s [options] empty_output_path %s\n\noptions:\n%s", os.Args[0], ResolveOpen.ArgHelp(), pflag.CommandLine.FlagUsages()) if !*Help { os.Exit(2) } @@ -49,36 +47,20 @@ func main() { runtime.GOMAXPROCS(*Threads) } - vpkOut := argv[0] + vpkOut := args[0] - var ( - err error - vpk tf2vpk.ValvePak - ) - if len(argv) == 3 { - fmt.Printf("unpacking vpk %q (in %q) to %q\n", argv[1], argv[2], vpkOut) - vpk, err = tf2vpk.VPK(argv[1], *VPKPrefix, argv[2]), nil - } else if len(argv) == 2 { - fmt.Printf("unpacking vpk %q to %q\n", argv[1], vpkOut) - vpk, err = tf2vpk.VPKFromPath(argv[1], *VPKPrefix) - } else { + if len(args) == 1 { fmt.Printf("initializing new vpk in %q\n", vpkOut) - vpk, err = nil, nil + } else { + fmt.Printf("unpacking vpk to %q\n", vpkOut) } + + _, r, err := ResolveOpen.ResolveOpen() if err != nil { - fmt.Fprintf(os.Stderr, "error: resolve vpk: %v\n", err) + fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } - var r *tf2vpk.Reader - if vpk != nil { - if r, err = tf2vpk.NewReader(vpk); err != nil { - fmt.Fprintf(os.Stderr, "error: open vpk: %v\n", err) - os.Exit(1) - } - defer r.Close() - } - if *VPKFlagsExplicit && r != nil { fmt.Printf("... generating .vpkflags (without inheritance)\n") } else { @@ -153,24 +135,10 @@ func main() { var excludedCount int if r != nil { for i, f := range r.Root.File { - var excluded bool - for _, x := range *Exclude { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - fmt.Fprintf(os.Stderr, "error: process excludes: match %q against glob %q: %v\n", f.Path, x, err) - os.Exit(1) - } else if m { - excluded = true - } - } - for _, x := range *Include { - if m, err := internal.MatchGlobParents(x, f.Path); err != nil { - fmt.Fprintf(os.Stderr, "error: process includes: match %q against glob %q: %v\n", f.Path, x, err) - os.Exit(1) - } else if m { - excluded = false - } - } - if excluded { + if skip, err := IncludeExclude.Skip(f); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } else if skip { excludedCount++ fmt.Printf("[%4d/%4d] %s (excluded)\n", i+1, len(r.Root.File), f.Path) continue diff --git a/vpkutil/vpkcli.go b/vpkutil/vpkcli.go new file mode 100644 index 0000000..641bb9e --- /dev/null +++ b/vpkutil/vpkcli.go @@ -0,0 +1,131 @@ +package vpkutil + +import ( + "fmt" + + "github.com/pg9182/tf2vpk" + "github.com/pg9182/tf2vpk/internal" + "github.com/spf13/pflag" +) + +// CLIIncludeExclude filters VPK files using the provided globs. +type CLIIncludeExclude struct { + Exclude *[]string + ExcludeBSPLump *[]int + Include *[]string +} + +// NewCLIIncludeExclude creates a new CLIIncludeExclude and registers it with +// the provided [pflag.FlagSet]. +func NewCLIIncludeExclude(set *pflag.FlagSet) CLIIncludeExclude { + return CLIIncludeExclude{ + Exclude: pflag.StringSlice("exclude", nil, "Excludes files or directories matching the provided glob (anchor to the start with /)"), + ExcludeBSPLump: pflag.IntSlice("exclude-bsp-lump", nil, "Shortcut for --exclude to remove %04x.bsp_lump"), + Include: pflag.StringSlice("include", nil, "Negates --exclude for files or directories matching the provided glob"), + } +} + +// Skip determines whether to skip the specified file. +func (ie CLIIncludeExclude) Skip(f tf2vpk.ValvePakFile) (bool, error) { + var excluded bool + for _, x := range *ie.Exclude { + if m, err := internal.MatchGlobParents(x, f.Path); err != nil { + return false, fmt.Errorf("process excludes: match %q against glob %q: %w", f.Path, x, err) + } else if m { + excluded = true + break + } + } + if !excluded { + for _, n := range *ie.ExcludeBSPLump { + x := fmt.Sprintf("%04x.bsp_lump", n) + if m, err := internal.MatchGlobParents(x, f.Path); err != nil { + return false, fmt.Errorf("process bsp lump excludes: match %q against glob %q: %w", f.Path, x, err) + } else if m { + excluded = true + break + } + } + } + for _, x := range *ie.Include { + if m, err := internal.MatchGlobParents(x, f.Path); err != nil { + return false, fmt.Errorf("process includes: match %q against glob %q: %w", f.Path, x, err) + } else if m { + excluded = false + break + } + } + return excluded, nil +} + +// CLIResolveOpen takes over the last 1 or 2 arguments, using them to resolve +// and open a VPK. +type CLIResolveOpen struct { + Arg int + Optional bool + VPKPrefix *string + set *pflag.FlagSet +} + +// NewCLIResolveOpen creates a new CLIResolveOpen and registers it with the +// provided [pflag.FlagSet]. It starts processing arguments at the provided +// index (where 0 is the first argument after flags have been parsed). +func NewCLIResolveOpen(set *pflag.FlagSet, arg int, optional bool) CLIResolveOpen { + return CLIResolveOpen{ + Arg: arg, + Optional: optional, + VPKPrefix: pflag.StringP("vpk-prefix", "p", "english", "VPK prefix"), + set: set, + } +} + +// ArgHelp returns help text to add to the arguments usage. +func (ro CLIResolveOpen) ArgHelp() string { + h := "(vpk_dir vpk_name)|vpk_path" + if ro.Optional { + h = "[" + h + "]" + } + return h +} + +// ArgCheck ensures at 1-2 (or 0-2 if optional) arguments are provided at the +// specified argument index. +func (ro CLIResolveOpen) ArgCheck() bool { + n := ro.set.NArg() + if ro.Optional { + return ro.Arg <= n && n <= ro.Arg+2 + } + return ro.Arg < n && n <= ro.Arg+2 +} + +// Resolve resolves the VPK path. +func (ro CLIResolveOpen) Resolve() (vpk tf2vpk.ValvePak, err error) { + args := ro.set.Args() + args = args[min(len(args), ro.Arg):] + switch len(args) { + case 2: + vpk, err = tf2vpk.VPK(args[0], *ro.VPKPrefix, args[1]), nil + case 1: + vpk, err = tf2vpk.VPKFromPath(args[0], *ro.VPKPrefix) + default: + if len(args) != 0 || !ro.Optional { + panic("invalid argument count, expected last arguments to be (vpk_dir vpk_name)|vpk_path") + } + } + if err != nil { + err = fmt.Errorf("resolve vpk: %w", err) + return + } + return +} + +// ResolveOpen resolves the VPK path and opens a reader. +func (ro CLIResolveOpen) ResolveOpen() (vpk tf2vpk.ValvePak, r *tf2vpk.Reader, err error) { + vpk, err = ro.Resolve() + if err == nil && vpk != nil { + if r, err = tf2vpk.NewReader(vpk); err != nil { + err = fmt.Errorf("open vpk: %w", err) + } + } + return +}