CLI parsing refactored

This commit is contained in:
Dmitry Sergeev
2026-09-16 11:43:59 +03:00
parent 326a8d6890
commit 765aa8d3e1
3 changed files with 266 additions and 115 deletions
+163 -113
View File
@@ -4,10 +4,14 @@
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"os/signal"
"sort"
"strings"
"syscall"
"github.com/google/uuid"
@@ -47,6 +51,11 @@ type appArgs struct {
audioUUID string
}
type parseResult struct {
args appArgs
shouldRun bool
}
var frameRates = map[string]mxl.Rational{
"23.97": {Num: 24000, Den: 1001},
"24": {Num: 24, Den: 1},
@@ -67,119 +76,126 @@ var samplingRates = map[string]mxl.Rational{
"192": {Num: 192000, Den: 1},
}
func printHelp(fs *pflag.FlagSet) {
fmt.Printf("%s %s\n", APP_NAME, APP_VER)
fmt.Println("Usage: mxl-gen -d <domain> [-v <flowDef.json>] [-a <flowDef.json>] [options]")
fmt.Println(" or: mxl-gen -d <domain> [--with <width px>] [--height <height px>] [--fps <framerate>] \\")
fmt.Println(" [-c <channels amount>] [-f <sample rate>]")
fmt.Println(" or: mxl-gen -d <domain> with default params")
fmt.Println("Video and audio feeds params will be ignored, if flow definition file provided.")
fmt.Println()
func printHelp(w io.Writer, fs *pflag.FlagSet) {
fmt.Fprintf(w, "%s %s\n", APP_NAME, APP_VER)
fmt.Fprintln(w, "Usage: mxl-gen -d <domain> [-v <flowDef.json>] [-a <flowDef.json>] [options]")
fmt.Fprintln(w, " or: mxl-gen -d <domain> [--width <width px>] [--height <height px>] [--fps <framerate>] \\")
fmt.Fprintln(w, " [-c <channels amount>] [-f <sample rate>]")
fmt.Fprintln(w, " or: mxl-gen -d <domain> with default params")
fmt.Fprintln(w, "Video and audio feed parameters are ignored when a flow definition file is provided.")
fmt.Fprintln(w)
fs.SetOutput(w)
fs.PrintDefaults()
}
func printUsage() {
fmt.Fprintln(os.Stderr, "Usage: mxl-gen -d <domain> [-v <flowDef.json>] [-a <flowDef.json>] [options]")
fmt.Fprintln(os.Stderr, "Try 'mxl-gen -h' for more information.")
func printUsage(w io.Writer) {
fmt.Fprintln(w, "Usage: mxl-gen -d <domain> [-v <flowDef.json>] [-a <flowDef.json>] [options]")
fmt.Fprintln(w, "Try 'mxl-gen -h' for more information.")
}
func checkArgs(args *appArgs) {
printUsageAndExit := func() {
printUsage()
os.Exit(2)
func validateDomain(domain string) error {
if domain == "" {
return fmt.Errorf("domain is required")
}
// domain
if args.domain == "" {
fmt.Fprintf(os.Stderr, "Domain is required\n")
printUsageAndExit()
fi, err := os.Stat(domain)
if err != nil {
return fmt.Errorf("invalid MXL domain %q: %w", domain, err)
}
fi, err := os.Stat(args.domain)
if err != nil || !fi.IsDir() {
fmt.Fprintf(os.Stderr, "Invalid MXL domain: %s\n", args.domain)
fmt.Fprintf(os.Stderr, "Reason: %v\n", err)
printUsageAndExit()
if !fi.IsDir() {
return fmt.Errorf("invalid MXL domain %q: not a directory", domain)
}
if ok, err := mxl.IsTmpFs(args.domain); err != nil || !ok {
fmt.Fprintf(os.Stderr, "Invalid MXL domain: %s\n", args.domain)
fmt.Fprintln(os.Stderr, "Domain must be directory in tmps.")
printUsageAndExit()
}
// FlowDef
checkFlowDef := func(label, flowDef string) {
fi, err := os.Stat(flowDef)
if err != nil || fi.IsDir() {
fmt.Fprintf(os.Stderr, "%s flow definition .json file is not accesible\n", label)
printUsageAndExit()
if ok, err := mxl.IsTmpFs(domain); err != nil || !ok {
if err != nil {
return fmt.Errorf("check MXL domain %q: %w", domain, err)
}
return fmt.Errorf("invalid MXL domain %q: directory must be on tmpfs", domain)
}
videoFlowDefProvided, audioFlowDefProvided := false, false
return nil
}
func validateFlowDefPath(label, path string) error {
if path == "" {
return nil
}
fi, err := os.Stat(path)
if err != nil {
return fmt.Errorf("%s flow definition %q is not accessible: %w", label, path, err)
}
if fi.IsDir() {
return fmt.Errorf("%s flow definition %q is a directory", label, path)
}
return nil
}
func validateVideoArgs(args *appArgs) error {
if args.videoFlowDefFile != "" {
checkFlowDef("Video", args.videoFlowDefFile)
videoFlowDefProvided = true
return nil
}
if args.audioFlowDefFile != "" {
checkFlowDef("Audio", args.audioFlowDefFile)
audioFlowDefProvided = true
if args.videoWidth == 0 || args.videoWidth%6 != 0 {
// v210 stores 6 pixels in each 16-byte block.
return fmt.Errorf("video width must be greater than zero and divisible by 6")
}
if args.videoHeight == 0 {
return fmt.Errorf("video height must be greater than zero")
}
if _, exists := frameRates[args.videoFPS]; !exists {
return fmt.Errorf("unsupported video FPS %q (supported: %s); use a flow definition for other rates",
args.videoFPS, sortedMapKeys(frameRates))
}
if _, exists := patterns[args.pattern]; !exists {
return fmt.Errorf("unknown video pattern %q (use --list-patterns to see available patterns)", args.pattern)
}
if args.videoUUID == "" {
args.videoUUID = uuid.NewString()
return nil
}
if _, err := uuid.Parse(args.videoUUID); err != nil {
return fmt.Errorf("invalid video UUID %q: %w", args.videoUUID, err)
}
return nil
}
if !videoFlowDefProvided {
if args.videoWidth == 0 || args.videoWidth%6 != 0 {
// width%6 == 0 - because of v210 (6 pixels per 16-byte block)
fmt.Fprintf(os.Stderr, "Video width must be > 0 and divisible by 6\n")
printUsageAndExit()
}
if args.videoHeight == 0 {
fmt.Fprintf(os.Stderr, "Video height must be > 0\n")
printUsageAndExit()
}
if _, exists := frameRates[args.videoFPS]; !exists {
fmt.Fprintf(os.Stderr, "FPS %s is not in available list.\n", args.videoFPS)
fmt.Fprintln(os.Stderr, "If you need more complex solution, use flow definition .json instead.")
fmt.Fprintln(os.Stderr, "Available list:")
for key, _ := range frameRates {
fmt.Fprintf(os.Stderr, " %s\n", key)
}
printUsageAndExit()
}
if args.videoUUID != "" {
if _, err := uuid.Parse(args.videoUUID); err != nil {
fmt.Fprintf(os.Stderr, "Video UUID %s is not valid.\n", args.videoUUID)
printUsageAndExit()
}
} else {
args.videoUUID = uuid.NewString()
}
if args.pattern != "" {
if _, exists := patterns[args.pattern]; !exists {
fmt.Fprintf(os.Stderr, "Pattern %s is not in available list.\n", args.pattern)
listPatterns(os.Stderr)
os.Exit(2)
}
}
func validateAudioArgs(args *appArgs) error {
if args.audioFlowDefFile != "" || args.audioChannels == 0 {
return nil
}
if _, exists := samplingRates[args.audioSamplingFreq]; !exists {
return fmt.Errorf("unsupported audio sample rate %q (supported: %s); use a flow definition for other rates",
args.audioSamplingFreq, sortedMapKeys(samplingRates))
}
if args.audioUUID == "" {
args.audioUUID = uuid.NewString()
return nil
}
if err := uuid.Validate(args.audioUUID); err != nil {
return fmt.Errorf("invalid audio UUID %q: %w", args.audioUUID, err)
}
return nil
}
if !audioFlowDefProvided {
if args.audioChannels == 0 {
// TODO: ignore audio
}
if _, exists := samplingRates[args.audioSamplingFreq]; !exists {
fmt.Fprintf(os.Stderr, "Sample rate %s is not in available list.\n", args.audioSamplingFreq)
fmt.Fprintln(os.Stderr, "If you need more complex solution, use flow definition .json instead.")
fmt.Fprintln(os.Stderr, "Available list:")
for key, _ := range samplingRates {
fmt.Fprintf(os.Stderr, " %s\n", key)
}
printUsageAndExit()
}
if args.audioUUID != "" {
if err := uuid.Validate(args.audioUUID); err != nil {
fmt.Fprintf(os.Stderr, "Audio UUID %s is not valid.\n", args.audioUUID)
printUsageAndExit()
}
} else {
args.audioUUID = uuid.NewString()
func validateArgs(args *appArgs) error {
checks := []func() error{
func() error { return validateDomain(args.domain) },
func() error { return validateFlowDefPath("video", args.videoFlowDefFile) },
func() error { return validateFlowDefPath("audio", args.audioFlowDefFile) },
func() error { return validateVideoArgs(args) },
func() error { return validateAudioArgs(args) },
}
for _, check := range checks {
if err := check(); err != nil {
return err
}
}
return nil
}
func sortedMapKeys[V any](values map[string]V) string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return strings.Join(keys, ", ")
}
type pattern struct {
@@ -252,21 +268,25 @@ var patterns = map[string]pattern{
},
}
func listPatterns(f *os.File) {
fmt.Fprintln(f, "List of available video patterns:")
var maxNameWidth int = 0
for name, _ := range patterns {
func listPatterns(w io.Writer) {
fmt.Fprintln(w, "List of available video patterns:")
names := make([]string, 0, len(patterns))
maxNameWidth := 0
for name := range patterns {
names = append(names, name)
l := len(name)
if l > maxNameWidth {
maxNameWidth = l
}
}
for name, p := range patterns {
fmt.Fprintf(f, " %-*s - %s\n", maxNameWidth, name, p.description)
sort.Strings(names)
for _, name := range names {
p := patterns[name]
fmt.Fprintf(w, " %-*s - %s\n", maxNameWidth, name, p.description)
}
}
func flagSetAddFlags(fs *pflag.FlagSet, args *appArgs) {
func addFlags(fs *pflag.FlagSet, args *appArgs) {
// common flags
fs.BoolVarP(&args.showHelp, "help", "h", false, "Show help message and exit")
// MXL flags
@@ -308,27 +328,45 @@ func flagSetAddFlags(fs *pflag.FlagSet, args *appArgs) {
fs.StringVar(&args.audioUUID, "audio-id", "", "Audio UUID. Will be created, if not provided [TODO]")
}
func main() {
func parseArgs(argv []string, stdout, stderr io.Writer) (parseResult, error) {
var args appArgs
flagSet := pflag.NewFlagSet("args", pflag.ContinueOnError)
flagSet.SortFlags = false
flagSet.Usage = func() { printUsage() }
flagSetAddFlags(flagSet, &args)
flagSet.SetOutput(stderr)
flagSet.Usage = func() { printUsage(stderr) }
addFlags(flagSet, &args)
if err := flagSet.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
printUsage()
os.Exit(2)
if err := flagSet.Parse(argv); err != nil {
return parseResult{}, err
}
if args.showHelp {
printHelp(flagSet)
return
printHelp(stdout, flagSet)
return parseResult{args: args}, nil
}
if args.listPatterns {
listPatterns(os.Stderr)
listPatterns(stdout)
return parseResult{args: args}, nil
}
if flagSet.NArg() != 0 {
return parseResult{}, fmt.Errorf("unexpected positional arguments: %v", flagSet.Args())
}
if err := validateArgs(&args); err != nil {
return parseResult{}, err
}
return parseResult{args: args, shouldRun: true}, nil
}
func main() {
parsed, err := parseArgs(os.Args[1:], os.Stdout, os.Stderr)
if err != nil {
fmt.Fprintln(os.Stderr, err)
printUsage(os.Stderr)
os.Exit(2)
}
if !parsed.shouldRun {
return
}
checkArgs(&args)
args := parsed.args
type videoInfo struct {
uuid string
@@ -470,3 +508,15 @@ func main() {
mxl.SleepNs(mxl.NsUntilIndex(idx, rate))
}
}
func run(ctx context.Context, args []string) error {
return nil
}
func runVideo(ctx context.Context) error {
return nil
}
func runAudio(ctx context.Context) error {
return nil
}