File: flag_types.go

package info (click to toggle)
miller 6.16.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 87,928 kB
  • sloc: ruby: 162; sh: 119; makefile: 87
file content (535 lines) | stat: -rw-r--r-- 19,656 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
// ================================================================
// Miller support for command-line flags.
//
// * Flags are used for several purposes:
//
//   o Command-line parsing the main mlr program.
//
//   o Record-reader and record-writer options for a few verbs such as join and
//     tee. E.g. `mlr --csv join -f foo.tsv --tsv ...`: the main input files are
//     CSV but the join-in file is TSV>
//
//   o Processing .mlrrc files.
//
//   o Autogenerating on-line help for `mlr help flags`.
//
//   o Autogenerating the manpage for `man mlr`.
//
//   o Autogenerating webdocs (mkdocs).
//
// * For these reasons, flags are organized into tables; for documentation
//   purposes, flags are organized into sections (see pkg/cli/option_parse.go).
//
// * The Flag struct separates out flag name (e.g. `--csv`), any alternate
//   names (e.g. `-c`), any arguments the flag may take, a help string, and a
//   command-line parser function.
//
// * The tabular structure may seem overwrought; in fact it has been a blessing
//   to develop the tabular structure since these flags objects need to serve
//   so many roles as listed above.
//
// * I don't use Go flags for a few reasons. The most important one is that I
//   need to handle repeated flags, e.g. --from can be used more than once for
//   mlr, and -f/-n/-r etc can be used more than once for mlr sort, etc. I also
//   insist on total control of flag formatting including alphabetization of
//   flags for on-line help and documentation systems.
// ================================================================

package cli

import (
	"fmt"
	"sort"
	"strings"

	"github.com/johnkerl/miller/v6/pkg/colorizer"
	"github.com/johnkerl/miller/v6/pkg/lib"
)

// ----------------------------------------------------------------
// Data types used within the flags table.

// FlagParser is a function which takes a flag such as `--foo`.
//   - It should assume that a flag.Owns method has already been invoked to be
//     sure that this function is indeed the right one to call for `--foo`.
//   - The FlagParser function is responsible for advancing *pargi by 1 (if
//     `--foo`) or 2 (if `--foo bar`), checking to see if argc is long enough in
//     the latter case, and mutating the options struct.
//   - Successful handling of the flag is indicated by this function making a
//     non-zero increment of *pargi.
type FlagParser func(
	args []string,
	argc int,
	pargi *int,
	options *TOptions,
)

// ----------------------------------------------------------------

// FlagTable holds all the flags for Miller, organized into sections.
type FlagTable struct {
	sections []*FlagSection
}

// FlagSection holds all the flags in a given cateogory, where these
// categories exist for documentation purposes.
//
// The name should be right-cased for webdocs. For on-line help and
// manpage use, it will get fully uppercased.
//
// The infoPrinter provides summary/overview for all flags in the
// section, for on-line help / webdocs.
type FlagSection struct {
	name        string
	infoPrinter func()
	flags       []Flag
}

// Flag is a container for all runtime as well as documentation information for
// a flag.
type Flag struct {
	// In most cases, the flag has just one spelling, like "--ifs".
	name string

	// In some cases, the flag has more than one spelling, like "-h" and
	// "--help", or "-c" and "--csv". The altNames field can be omitted from
	// struct initializers, which in Go means it will read as nil.
	altNames []string

	// If not "", a name for the flag's argument, for on-line help. E.g. the
	// "bar" in ""--foo {bar}". It should always be written in curly braces.
	arg string

	// Help string for `mlr help flags`, `man mlr`, and webdocs.
	// * It should be all one line within the source code. The text will be
	//   reformatted as a paragraph for on-line help / manpage, so there should
	//   be no attempt at line-breaking within the help string.
	// * Any code bits should be marked with backticks. These look OK for
	//   on-line help / manpage, and render marvelously for webdocs which
	//   take markdown.
	// * After changing flags you can run `make precommit` in the Miller
	//   repo base directory followed by `git diff` to see how the output
	//   looks. See also the README.md files in the docs and man directories
	//   for how to look at the autogenned docs pre-commit.
	help string

	// A function for parsing the command line, as described above.
	parser FlagParser

	// For format-conversion keystroke-savers, a matrix is plenty -- we don't
	// need to print a tedious 60-line list.
	suppressFlagEnumeration bool
}

// ================================================================
// FlagTable methods

// Sort organizes the sections in the table alphabetically, to make on-line
// help easier to read. This is done from func-init context so on-line help
// will always be easy to navigate.
func (ft *FlagTable) Sort() {
	// Go sort API: for ascending sort, return true if element i < element j.
	sort.Slice(ft.sections, func(i, j int) bool {
		return strings.ToLower(ft.sections[i].name) < strings.ToLower(ft.sections[j].name)
	})
}

// Parse is for parsing a flag on the command line. Given say `--foo`, if a
// Flag object is found which owns the flag, and if its parser accepts it (e.g.
// `bar` is present and spelt correctly if the flag-parser expects `--foo bar`)
// then the return value is true, else false.
func (ft *FlagTable) Parse(
	args []string,
	argc int,
	pargi *int,
	options *TOptions,
) bool {
	for _, section := range ft.sections {
		for _, flag := range section.flags {
			if flag.Owns(args[*pargi]) {
				// Let the flag-parser advance *pargi, depending on how many
				// arguments follow the flag. E.g. `--ifs pipe` will advance
				// *pargi by 2; `-I` will advance it by 1.
				oargi := *pargi
				flag.parser(args, argc, pargi, options)
				nargi := *pargi
				return nargi > oargi
			}
		}
	}
	return false
}

// ShowHelp prints all-in-one on-line help, nominally for `mlr help flags`.
func (ft *FlagTable) ShowHelp() {
	for i, section := range ft.sections {
		if i > 0 {
			fmt.Println()
		}
		fmt.Println(colorizer.MaybeColorizeHelp(strings.ToUpper(section.name), true))
		fmt.Println()
		section.PrintInfo()
		section.ShowHelpForFlags()
	}
}

// ListFlagSections exposes some of the flags-table structure, so Ruby autogen
// scripts for on-line help and webdocs can traverse the structure with looping
// inside their own code.
func (ft *FlagTable) ListFlagSections() {
	for _, section := range ft.sections {
		fmt.Println(section.name)
	}
}

// PrintInfoForSection exposes some of the flags-table structure, so Ruby
// autogen scripts for on-line help and webdocs can traverse the structure with
// looping inside their own code.
func (ft *FlagTable) ShowHelpForSection(sectionName string) bool {
	for _, section := range ft.sections {
		if sectionName == section.name {
			section.PrintInfo()
			section.ShowHelpForFlags()
			return true
		}
	}
	return false
}

// Sections are named like "CSV-only flags". `mlr help` uses `mlr help
// csv-only-flags`. The latter is downcased from the former, with spaces
// replaced by dashes -- hence "downdashed section name". Here we look up
// flag-section help given a downdashed section name.
func (ft *FlagTable) ShowHelpForSectionViaDowndash(downdashSectionName string) bool {
	for _, section := range ft.sections {
		if downdashSectionName == section.GetDowndashSectionName() {
			fmt.Println(colorizer.MaybeColorizeHelp(strings.ToUpper(section.name), true))
			section.PrintInfo()
			section.ShowHelpForFlags()
			return true
		}
	}
	return false
}

// PrintInfoForSection exposes some of the flags-table structure, so Ruby
// autogen scripts for on-line help and webdocs can traverse the structure with
// looping inside their own code.
func (ft *FlagTable) PrintInfoForSection(sectionName string) bool {
	for _, section := range ft.sections {
		if sectionName == section.name {
			section.PrintInfo()
			return true
		}
	}
	return false
}

// ListFlagsForSection exposes some of the flags-table structure, so Ruby
// autogen scripts for on-line help and webdocs can traverse the structure with
// looping inside their own code.
func (ft *FlagTable) ListFlagsForSection(sectionName string) bool {
	for _, section := range ft.sections {
		if sectionName == section.name {
			section.ListFlags()
			return true
		}
	}
	return false
}

// Given flag named `--foo`, altName `-f`, and argument spec `{bar}`, the
// headline is `--foo or -f {bar}`. This is the bit which is highlighted in
// on-line help; its length is also used for alignment decisions in the on-line
// help and the manapge.
func (ft *FlagTable) ShowHeadlineForFlag(flagName string) bool {
	for _, fs := range ft.sections {
		for _, flag := range fs.flags {
			if flag.Owns(flagName) {
				fmt.Println(flag.GetHeadline())
				return true
			}
		}
	}
	return false
}

// ShowHelpForFlag prints the flag's help-string all on one line.  This is for
// webdoc usage where the browser does dynamic line-wrapping, as the user
// resizes the browser window.
func (ft *FlagTable) ShowHelpForFlag(flagName string) bool {
	return ft.showHelpForFlagMaybeWithName(flagName, false)
}

// ShowHelpForFlagWithName prints the flag's name colorized, then flag's
// help-string all on one line.  This is for on-line help usage.
func (ft *FlagTable) ShowHelpForFlagWithName(flagName string) bool {
	return ft.showHelpForFlagMaybeWithName(flagName, true)
}

// showHelpForFlagMaybeWithName supports ShowHelpForFlag and ShowHelpForFlagWithName.
// webdoc usage where the browser does dynamic line-wrapping, as the user
// resizes the browser window.
func (ft *FlagTable) showHelpForFlagMaybeWithName(flagName string, showName bool) bool {
	for _, fs := range ft.sections {
		for _, flag := range fs.flags {
			if flag.Owns(flagName) {
				if showName {
					fmt.Println(colorizer.MaybeColorizeHelp(flagName, true))
				}
				fmt.Println(flag.GetHelpOneLine())
				return true
			}
		}
	}
	return false
}

// ShowHelpForFlagApproximateWithName is like ShowHelpForFlagWithName
// but allows substring matches.  This is for on-line help usage.
func (ft *FlagTable) ShowHelpForFlagApproximateWithName(searchString string) bool {
	for _, fs := range ft.sections {
		for _, flag := range fs.flags {
			if flag.Matches(searchString) {
				fmt.Println(colorizer.MaybeColorizeHelp(flag.name, true))
				fmt.Println(flag.GetHelpOneLine())
			}
		}
	}
	return false
}

// Map "CSV-only flags" to "csv-only-flags" etc. for the benefit of per-section
// help in `mlr help topics`.
func (ft *FlagTable) GetDowndashSectionNames() []string {
	downdashSectionNames := make([]string, len(ft.sections))
	for i, fs := range ft.sections {
		// Get names like "CSV-only flags" from the FLAG_TABLE.
		// Downcase and replace spaces with dashes to get names like
		// "csv-only-flags"
		downdashSectionNames[i] = fs.GetDowndashSectionName()
	}
	return downdashSectionNames
}

// NilCheck checks to see if any flag/section is missing help info. This arises
// since in Go you needn't specify all struct initializers, so for example a
// Flag struct-initializer which doesn't say `help: "..."` will have empty help
// string. This nil-checking doesn't need to be done on every Miller
// invocation, but rather, only at build time. The `mlr help` terminal has an
// entrypoint wherein a regression-test case can do `mlr help nil-check` and
// make this function exits cleanly.
func (ft *FlagTable) NilCheck() {
	lib.InternalCodingErrorWithMessageIf(ft.sections == nil, "Nil table sections")
	lib.InternalCodingErrorWithMessageIf(len(ft.sections) == 0, "Zero table sections")
	for _, fs := range ft.sections {
		fs.NilCheck()
	}
	fmt.Println("Flag-table nil check completed successfully.")
}

// ================================================================
// FlagSection methods

// Sort organizes the flags in the section alphabetically, to make on-line help
// easier to read.  This is done from func-init context so on-line help will
// always be easy to navigate.
func (fs *FlagSection) Sort() {
	// Go sort API: for ascending sort, return true if element i < element j.
	sort.Slice(fs.flags, func(i, j int) bool {
		return strings.ToLower(fs.flags[i].name) < strings.ToLower(fs.flags[j].name)
	})
}

// ShowHelpForFlags prints all-in-one on-line help, nominally for `mlr help
// flags`.
func (fs *FlagSection) ShowHelpForFlags() {
	for _, flag := range fs.flags {
		// For format-conversion keystroke-savers, a matrix is plenty -- we don't
		// need to print a tedious 60-line list.
		if flag.suppressFlagEnumeration {
			continue
		}
		flag.ShowHelp()
	}
}

// PrintInfo exposes some of the flags-table structure, so Ruby autogen scripts
// for on-line help and webdocs can traverse the structure with looping inside
// their own code.
func (fs *FlagSection) PrintInfo() {
	fs.infoPrinter()
	fmt.Println()
}

// ListFlags exposes some of the flags-table structure, so Ruby autogen scripts
// for on-line help and webdocs can traverse the structure with looping inside
// their own code.
func (fs *FlagSection) ListFlags() {
	for _, flag := range fs.flags {
		fmt.Println(flag.name)
	}
}

// Map "CSV-only flags" to "csv-only-flags" etc. for the benefit of per-section
// help in `mlr help topics`.
func (fs *FlagSection) GetDowndashSectionName() string {
	return strings.ReplaceAll(strings.ToLower(fs.name), " ", "-")
}

// See comments above FlagTable's NilCheck method.
func (fs *FlagSection) NilCheck() {
	lib.InternalCodingErrorWithMessageIf(fs.name == "", "Empty section name")
	lib.InternalCodingErrorWithMessageIf(fs.infoPrinter == nil, "Nil infoPrinter for section "+fs.name)
	lib.InternalCodingErrorWithMessageIf(fs.flags == nil, "Nil flags for section "+fs.name)
	lib.InternalCodingErrorWithMessageIf(len(fs.flags) == 0, "Zero flags for section "+fs.name)
	for _, flag := range fs.flags {
		flag.NilCheck()
	}
}

// ================================================================
// Flag methods

// Owns determines whether this object handles a command-line flag such as
// "--foo".  This is used for command-line parsing, as well as for on-line help
// with exact match on flag name.
func (flag *Flag) Owns(input string) bool {
	if flag.name == input {
		return true
	}
	for _, name := range flag.altNames {
		if name == input {
			return true
		}
	}
	return false
}

// Matches is like Owns but is for substring matching, for on-line help with
// approximate match on flag name.
func (flag *Flag) Matches(input string) bool {
	if strings.Contains(flag.name, input) {
		return true
	}
	for _, name := range flag.altNames {
		if strings.Contains(name, input) {
			return true
		}
	}
	return false
}

// ShowHelp produces formatting for `mlr help flags` and manpage use.
// Example:
// * Flag name is `--foo`
// * altName is `-f`
// * Argument spec is `{bar}`
// * Help string is "Lorem ipsum dolor sit amet, consectetur adipiscing elit,
//   sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
//   ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
//   ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
//   velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
//   cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
//   est laborum."
// * The headline (see the GetHeadline function) is `--foo or -f {bar}`.
// * We place the headline left in a 25-character column, colorized with the
//   help color.
// * We format the help text as 55-character lines and place them
//   to the right.
// * The result looks like
//
//   --foo or -f {bar}        Lorem ipsum dolor sit amet, consectetur adipiscing
//                            elit, sed do eiusmod tempor incididunt ut labore et
//                            dolore magna aliqua. Ut enim ad minim veniam, quis
//                            nostrud exercitation ullamco laboris nisi ut aliquip
//                            ex ea commodo consequat. Duis aute irure dolor in
//                            reprehenderit in voluptate velit esse cillum dolore
//                            eu fugiat nulla pariatur. Excepteur sint occaecat
//                            cupidatat non proident, sunt in culpa qui officia
//                            deserunt mollit anim id est laborum.
//
// * If the headline is too long we put the first help line a line below like this:
//
//   --foo-flag-is-very-very-long {bar}
//                            Lorem ipsum dolor sit amet, consectetur adipiscing
//                            elit, sed do eiusmod tempor incididunt ut labore et
//                            dolore magna aliqua. Ut enim ad minim veniam, quis
//                            nostrud exercitation ullamco laboris nisi ut aliquip
//                            ex ea commodo consequat. Duis aute irure dolor in
//                            reprehenderit in voluptate velit esse cillum dolore
//                            eu fugiat nulla pariatur. Excepteur sint occaecat
//                            cupidatat non proident, sunt in culpa qui officia
//                            deserunt mollit anim id est laborum.
//

func (flag *Flag) ShowHelp() {
	headline := flag.GetHeadline()
	displayHeadline := fmt.Sprintf("%-25s", headline)
	broken := len(headline) >= 25

	helpLines := lib.FormatAsParagraph(flag.help, 55)

	if broken {
		fmt.Printf("%s\n", colorizer.MaybeColorizeHelp(displayHeadline, true))
		for _, helpLine := range helpLines {
			fmt.Printf("%25s%s\n", " ", helpLine)
		}
	} else {
		fmt.Printf("%s", colorizer.MaybeColorizeHelp(displayHeadline, true))
		if len(helpLines) == 0 {
			fmt.Println()
		}
		for i, helpLine := range helpLines {
			if i == 0 {
				fmt.Printf("%s\n", helpLine)
			} else {
				fmt.Printf("%25s%s\n", " ", helpLine)
			}
		}
	}
}

// GetHeadline puts together the flag name, any altNames, and any argument spec
// into a single string for the left column of online help / manpage content.
// Given flag named `--foo`, altName `-f`, and argument spec `{bar}`, the
// headline is `--foo or -f {bar}`. This is the bit which is highlighted in
// on-line help; its length is also used for alignment decisions in the on-line
// help and the manapge.
func (flag *Flag) GetHeadline() string {
	displayNames := make([]string, 1)
	displayNames[0] = flag.name
	if flag.altNames != nil {
		displayNames = append(displayNames, flag.altNames...)
	}
	displayText := strings.Join(displayNames, " or ")
	if flag.arg != "" {
		displayText += " "
		displayText += flag.arg
	}
	return displayText
}

// Gets the help string all on one line (just in case anyone typed it in using
// multiline string-literal backtick notation in Go). This is suitable for
// webdoc use where we create all one line, and the browser dynamically
// line-wraps as the user resizes the window.
func (flag *Flag) GetHelpOneLine() string {
	return strings.Join(strings.Split(flag.help, "\n"), " ")
}

// See comments above FlagTable's NilCheck method.
func (flag *Flag) NilCheck() {
	lib.InternalCodingErrorWithMessageIf(flag.name == "", "Empty flag name")
	lib.InternalCodingErrorWithMessageIf(flag.help == "", "Empty flag help for flag "+flag.name)
	lib.InternalCodingErrorWithMessageIf(flag.parser == nil, "Nil parser help for flag "+flag.name)
}

// ================================================================
// Helper methods

// NoOpParse1 is a helper function for flags which take no argument and are
// backward-compatibility no-ops.
func NoOpParse1(args []string, argc int, pargi *int, options *TOptions) {
	*pargi += 1
}