diff --git a/cmd/jargon/main.go b/cmd/jargon/main.go index 447da2e..946d3d6 100644 --- a/cmd/jargon/main.go +++ b/cmd/jargon/main.go @@ -4,6 +4,7 @@ import ( "bufio" "flag" "fmt" + "io/ioutil" "os" "strconv" "strings" @@ -19,30 +20,6 @@ import ( var version, commit, date string func main() { - // - // Flags. Prefer local instead of global, allowing other funcs to be stateless. - // - filein := flag.String("file", "", "input file path (if none, stdin is used as input)") - fileout := flag.String("out", "", "output file path (if none, stdout is used as input)") - html := flag.Bool("html", false, "parse input as html (keep tags whole)") - lang := flag.String("lang", "english", "language of input, relevant when used with -stem. options:\n"+strings.Join(langs, ", ")) - - // These flags aren't actually consumed, see setFilters below - // Need to declare them anyway, or flags package will consider them errors - flag.Bool("ascii", false, "a filter to replace diacritics with ascii equivalents, e.g. café → cafe") - flag.Bool("contractions", false, "a filter to expand contractions, e.g. Would've → Would have") - flag.Bool("stack", false, "a filter to recognize tech terms as Stack Overflow tags, e.g. Ruby on Rails → ruby-on-rails") - flag.Bool("stem", false, "a filter to stem words using snowball stemmer, e.g. management|manager → manag") - flag.Bool("lemmas", false, "only return tokens that have been changed by a filter (lemmatized)") - flag.Bool("distinct", false, "only return unique tokens") - - count := flag.Bool("count", false, "count the tokens") - lines := flag.Bool("lines", false, "add a line break between all tokens") - - v := flag.Bool("version", false, "display the version") - - flag.Parse() - // Local to prevent mistaken use in other funcs check := func(err error) { if err != nil { @@ -52,6 +29,48 @@ func main() { } } + // + // Flags. We're doing multiple flag sets to organize the Usage output a bit + // + args := os.Args[1:] + + flags := flag.NewFlagSet("Flags", flag.ContinueOnError) + flags.SetOutput(ioutil.Discard) // mute errors by default + + filein := flags.String("file", "", "input file path (if none, stdin is used as input)") + fileout := flags.String("out", "", "output file path (if none, stdout is used as input)") + html := flags.Bool("html", false, "parse input as html (keep tags whole)") + count := flags.Bool("count", false, "count the tokens") + lines := flags.Bool("lines", false, "add a line break between all tokens") + flags.Bool("lemmas", false, "only return tokens that have been changed by a filter (lemmatized)") + flags.Bool("distinct", false, "only return unique tokens") + v := flags.Bool("version", false, "display the version") + + filters := flag.NewFlagSet("Filters", flag.ContinueOnError) + filters.SetOutput(ioutil.Discard) // mute errors by default + + // We don't actually use these flags, see setFilters below; included here for Usage and errors + filters.Bool("ascii", false, "a filter to replace diacritics with ascii equivalents, e.g. café → cafe") + filters.Bool("contractions", false, "a filter to expand contractions, e.g. Would've → Would have") + filters.Bool("stack", false, "a filter to recognize tech terms as Stack Overflow tags, e.g. Ruby on Rails → ruby-on-rails") + filters.Bool("stem", false, "a filter to stem words using snowball stemmer, e.g. management|manager → manag") + lang := filters.String("lang", "english", "language of input, relevant when used with -stem. options:\n"+strings.Join(langs, ", ")) + + // Parse both flag sets + flagsErr := flags.Parse(args) + filtersErr := filters.Parse(args) + + // Working around the default error behavior, a bit of acrobatics here + // If one flag set returns a "not defined" error, see if it's defined in the other flag set + err := checkDefined(flagsErr, filters) + check(err) + err = checkDefined(filtersErr, flags) + check(err) + + // Unmute output + flags.SetOutput(os.Stderr) + filters.SetOutput(os.Stderr) + if *v { fmt.Println("Version: " + version) fmt.Println("Commit: " + commit) @@ -77,8 +96,12 @@ func main() { if err == errNoInput { // Display usage os.Stderr.WriteString(flag.CommandLine.Name() + " takes text from std input and processes it with one or more filters\n\n") + + os.Stderr.WriteString("Filters:\n") + filters.PrintDefaults() + os.Stderr.WriteString("Flags:\n") - flag.PrintDefaults() + flags.PrintDefaults() return } check(err) @@ -120,6 +143,23 @@ func main() { check(err) } +func checkDefined(err error, other *flag.FlagSet) error { + if err != nil && strings.Contains(err.Error(), "not defined") { + // Get the name of the undefined arg + missing := strings.Split(err.Error(), ": -") + if len(missing) == 2 { + // See if it's defined elsewhere + arg := missing[1] + f := other.Lookup(arg) + if f == nil { + return fmt.Errorf("flag provided but not defined: -%s", arg) + } + } + } + + return nil +} + type config struct { Fs afero.Fs