Glob as variant of Arg Flag type : accepts multiple files, as shell-expanded. use in um sort and um cat

NOTE: um sort expects .md files, while um cat expects .um : .um filelists are concatted together, while .md lists are just lists
This commit is contained in:
bth 2026-07-15 17:20:09 -07:00
commit 5aac37f8a8
5 changed files with 74 additions and 22 deletions

View file

@ -30,7 +30,7 @@ const FILE_LINK_REGEXP = `(?m)` + HR_BLOCK_STRIP + `([0-9]+\.[^\.]*\.*md\n)+\n`
var fileLinkRegexp *regexp.Regexp = regexp.MustCompile(FILE_LINK_REGEXP) var fileLinkRegexp *regexp.Regexp = regexp.MustCompile(FILE_LINK_REGEXP)
type options struct { type options struct {
Filelist flags.Arg Filelist flags.Glob
Base flags.String Base flags.String
KeepHeader flags.Bool KeepHeader flags.Bool
KeepTitle flags.Bool KeepTitle flags.Bool
@ -40,7 +40,7 @@ type options struct {
func initOpts() options { func initOpts() options {
return options{ return options{
flags.Arg{"", "filelist. accepts from stdin if not provided"}, flags.Glob{nil, ".um filelist. accepts multiple. reads from stdin if not provided"},
flags.String{"--base", "-b", "", "base directory prepended to files in filelist"}, flags.String{"--base", "-b", "", "base directory prepended to files in filelist"},
flags.Bool{"--keep-header", "-d", false, "preserve um headers in concatenated file. overrides --keep-title"}, flags.Bool{"--keep-header", "-d", false, "preserve um headers in concatenated file. overrides --keep-title"},
flags.Bool{"--keep-title", "-t", false, "preserve um titles in concatenated file"}, flags.Bool{"--keep-title", "-t", false, "preserve um titles in concatenated file"},
@ -120,7 +120,8 @@ func Cat(args []string) {
} }
log.Fatalf("um %s: %s", CMD, err) log.Fatalf("um %s: %s", CMD, err)
} }
files, err := pipe.FileListSplitMaybeStdin(opts.Filelist.Val) // NOTE: um cat expects .um files, the content of which is assembled:
files, err := pipe.FileListFromGlobOrStdin(opts.Filelist.Val)
if err != nil { if err != nil {
log.Fatalf("um %s: %s", CMD, err) log.Fatalf("um %s: %s", CMD, err)
} }

View file

@ -31,6 +31,11 @@ type Arg struct {
Help string Help string
} }
type Glob struct {
Val []string
Help string
}
type String struct { type String struct {
Long string Long string
Short string Short string
@ -69,6 +74,33 @@ func (f *Arg) MaybeIncrement(i int) int {
return i return i
} }
func (f *Glob) Set(arg string) {
f.Val = append(f.Val, arg)
}
func (f *Glob) Valid(args []string, i int) bool {
return true
}
func (f *Glob) Match(arg string, i, j int) bool {
// NOTE: this is the key difference from Arg: by not requiring that the arg and flag positions
// match, a shell-expanded glob of files of any length will populate the value.
return i >= j && !hasDashPrefix(arg)
}
func (f *Glob) IsSet() bool {
return f.Val != nil && len(f.Val) != 0
}
func (f *Glob) IsHelp() bool {
return false
}
func (f *Glob) MaybeIncrement(i int) int {
// NOTE: because this is only needed where a flag --prefix exists:
return i
}
func (f *String) Set(arg string) { func (f *String) Set(arg string) {
f.Val = arg f.Val = arg
} }
@ -145,16 +177,16 @@ func expandOpts(opts any) ([]Flag, error) {
for j := 0; j < v.NumField(); j++ { for j := 0; j < v.NumField(); j++ {
field := v.Field(j) field := v.Field(j)
if field.Kind() != reflect.Struct { if field.Kind() != reflect.Struct {
return nil, ParseError{fmt.Sprintf("field must be a struct: %#v", field.Kind())} return nil, ParseError{fmt.Sprintf("field must be a struct: %v", field.Kind())}
} }
if !field.CanAddr() { if !field.CanAddr() {
return nil, ParseError{fmt.Sprintf("field must be addressable type: %#v", field.Type())} return nil, ParseError{fmt.Sprintf("field must be addressable type: %v", field.Type())}
} }
// NOTE: Addr() gets the underlying address: // NOTE: Addr() gets the underlying address:
if f, ok := field.Addr().Interface().(Flag); ok { if f, ok := field.Addr().Interface().(Flag); ok {
flags = append(flags, f) flags = append(flags, f)
} else { } else {
return nil, ParseError{fmt.Sprintf("field must be a []Flag interface: %#v", field.Type())} return nil, ParseError{fmt.Sprintf("field must be a []Flag interface: %v", field.Type())}
} }
} }
return flags, nil return flags, nil

View file

@ -61,9 +61,12 @@ func (h HelpError) Help(opts any) error {
tField := t.Field(i) tField := t.Field(i)
vField := v.Field(i) vField := v.Field(i)
switch vField.Interface().(type) { switch vField.Interface().(type) {
case Arg: case Arg, Glob:
positional += fmt.Sprintf(" [%s]", strings.ToLower(tField.Name)) positional += fmt.Sprintf(" [%s]", strings.ToLower(tField.Name))
fmt.Fprintf(w, "[%v]\tstring\t%v\n", strings.ToLower(tField.Name), vField.FieldByName("Help")) fmt.Fprintf(w, "[%v]\t%v\t%v\n",
strings.ToLower(tField.Name),
vField.FieldByName("Val").Type(),
vField.FieldByName("Help"))
case String, Bool: case String, Bool:
if tField.Name != "Help" { if tField.Name != "Help" {
positional += fmt.Sprintf(" [%s]", vField.FieldByName("Long")) positional += fmt.Sprintf(" [%s]", vField.FieldByName("Long"))
@ -75,7 +78,7 @@ func (h HelpError) Help(opts any) error {
vField.FieldByName("Help"), vField.FieldByName("Help"),
) )
default: default:
return ParseError{fmt.Sprintf("needs a []Flag interface: %#v", vField.Type())} return ParseError{fmt.Sprintf("Help did not recognize the Flag: %v", vField.Type())}
} }
} }

View file

@ -29,15 +29,30 @@ func GetStdin() ([]string, error) {
return strings.Split(s, Newline), nil return strings.Split(s, Newline), nil
} }
// reads files from stdin if present, otherwise from the filename: // reads files from stdin if present, otherwise just returns the glob:
func FileListSplitMaybeStdin(f string) ([]string, error) { func GlobOrStdin(glob []string) ([]string, error) {
filelist, err := GetStdin() filelist, err := GetStdin()
if err != nil { if err != nil {
// BORK: don't see a neater way of wrapping these two when we depend on the non-nil in this block: if glob == nil || len(glob) == 0 {
// if i just return FileListSplit(), we lose the first err return nil, fmt.Errorf("glob is empty")
filelist, err2 := FileListSplit(f) }
if err2 != nil { return glob, nil
return nil, fmt.Errorf("%w: %w", err, err2) }
return filelist, nil
}
// reads files from stdin if present, otherwise reads content from files in glob and assembles into a list.
func FileListFromGlobOrStdin(glob []string) ([]string, error) {
filelist, err := GetStdin()
if err != nil {
for _, f := range glob {
// TODO: don't see a neater way of wrapping these two when we depend on the non-nil in this block:
// if i just return FileListSplit(), we lose the first err
fl, err2 := FileListSplit(f)
if err2 != nil {
return nil, fmt.Errorf("%w: %w", err, err2)
}
filelist = append(filelist, fl...)
} }
return filelist, nil return filelist, nil
} }

View file

@ -19,15 +19,15 @@ const (
) )
type options struct { type options struct {
Source flags.String Filelist flags.Glob
Key flags.String Key flags.String
Write flags.Bool Write flags.Bool
Help flags.Bool Help flags.Bool
} }
func initOpts() options { func initOpts() options {
return options{ return options{
flags.String{"--source", "-s", "", "path to source list. reads from stdin if omitted."}, flags.Glob{nil, ".md filelist. accepts multiple. reads from stdin if not provided"},
flags.String{"--key", "-k", "", "path to sort key"}, flags.String{"--key", "-k", "", "path to sort key"},
flags.Bool{"--write", "-w", false, "write sorted list back to --key file"}, flags.Bool{"--write", "-w", false, "write sorted list back to --key file"},
flags.Bool{"--help", "-h", false, "show help"}, flags.Bool{"--help", "-h", false, "show help"},
@ -84,7 +84,8 @@ func Sort(args []string) {
return return
} }
sslice, err := pipe.FileListSplitMaybeStdin(opts.Source.Val) // NOTE: um sort expects a list of .md files, in contrast to um cat.
sslice, err := pipe.GlobOrStdin(opts.Filelist.Val)
if err != nil { if err != nil {
log.Fatalf("um %s: %s", CMD, err) log.Fatalf("um %s: %s", CMD, err)
} }