-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathconfig.go
623 lines (573 loc) · 23.2 KB
/
config.go
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
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
package mybase
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// OptionValuer should be implemented by anything that can parse and return
// user-supplied values for options. If the struct has a value corresponding
// to the given optionName, it should return the value along with a true value
// for ok. If the struct does not have a value for the given optionName, it
// should return "", false.
type OptionValuer interface {
OptionValue(optionName string) (value string, ok bool)
}
// StringMapValues is the most trivial possible implementation of the
// OptionValuer interface: it just maps option name strings to option value
// strings.
type StringMapValues map[string]string
// OptionValue satisfies the OptionValuer interface, allowing StringMapValues
// to be an option source for Config methods.
func (source StringMapValues) OptionValue(optionName string) (string, bool) {
val, ok := source[optionName]
return val, ok
}
func (source StringMapValues) String() string {
return "runtime override"
}
// Config represents a list of sources for option values -- the command-line
// plus zero or more option files, or any other source implementing the
// OptionValuer interface.
type Config struct {
CLI *CommandLine // Parsed command-line
IsTest bool // true if Config generated from test logic, false otherwise
LooseFileOptions bool // enable to ignore unknown options in all Files
runtimeOverrides StringMapValues // Highest-priority option value overrides
sources []OptionValuer // Sources of option values, excluding CLI, Command, runtimeOverrides; higher indexes override lower indexes
unifiedValues map[string]string // Precomputed cache of option name => value
unifiedSources map[string]OptionValuer // Precomputed cache of option name => which source supplied it
dirty bool // true if source list has changed, meaning next access needs to recompute caches
}
// NewConfig creates a Config object, given a CommandLine and any arbitrary
// number of other OptionValuer option sources. The order of sources matters:
// in case of conflicts (multiple sources providing the same option value),
// later sources override earlier sources. The CommandLine always overrides
// other sources, and should not be supplied redundantly via sources.
func NewConfig(cli *CommandLine, sources ...OptionValuer) *Config {
return &Config{
CLI: cli,
runtimeOverrides: StringMapValues(make(map[string]string)),
sources: sources,
dirty: true,
}
}
// Clone returns a shallow copy of a Config. The copy will point to the same
// CLI value and sources values, but the sources slice itself will be a new
// slice, meaning that a caller can add sources without impacting the original
// Config's source list.
func (cfg *Config) Clone() *Config {
sourcesCopy := make([]OptionValuer, len(cfg.sources))
copy(sourcesCopy, cfg.sources)
runtimeOverridesCopy := StringMapValues(make(map[string]string, len(cfg.runtimeOverrides)))
for rtoName, rtoValue := range cfg.runtimeOverrides {
runtimeOverridesCopy[rtoName] = rtoValue
}
return &Config{
CLI: cfg.CLI,
IsTest: cfg.IsTest,
LooseFileOptions: cfg.LooseFileOptions,
runtimeOverrides: runtimeOverridesCopy,
sources: sourcesCopy,
dirty: true,
}
}
// AddSource adds a new OptionValuer to cfg. It will override previously-added
// sources, with the exception of the CommandLine, which always takes
// precedence.
func (cfg *Config) AddSource(source OptionValuer) {
cfg.sources = append(cfg.sources, source)
cfg.dirty = true
}
// HandleCommand executes the CommandHandler callback associated with the
// Command that was parsed on the CommandLine.
func (cfg *Config) HandleCommand() error {
// Handle --help if supplied as an option instead of as a subcommand
// (Note that format "command help [<subcommand>]" is already parsed properly into help command)
if forCommandName, helpWanted := cfg.CLI.OptionValues["help"]; helpWanted {
// command --help displays help for command
// vs
// command --help <subcommand> displays help for subcommand
cfg.CLI.ArgValues = []string{forCommandName}
return helpHandler(cfg)
}
// Handle --version if supplied as an option instead of as a subcommand
if cfg.CLI.OptionValues["version"] == "1" {
return versionHandler(cfg)
}
return cfg.CLI.Command.Handler(cfg)
}
// Sources returns a slice of OptionValuer values used as option sources for
// cfg. The result is ordered from lowest-priority to highest-priority.
func (cfg *Config) Sources() []OptionValuer {
allSources := make([]OptionValuer, 1, len(cfg.sources)+2)
// Lowest-priority source is the current command, which returns default values
// for any valid option
allSources[0] = cfg.CLI.Command
// Next come cfg.sources, which are already ordered from lowest priority to highest priority
allSources = append(allSources, cfg.sources...)
// Finally, at highest priorities are options provided on the command-line,
// and then runtime overrides
allSources = append(allSources, cfg.CLI, cfg.runtimeOverrides)
return allSources
}
// rebuild iterates over all sources, to construct a single cached key-value
// lookup map. This improves performance of subsequent option value lookups.
func (cfg *Config) rebuild() {
options := cfg.CLI.Command.Options()
cfg.unifiedValues = make(map[string]string, len(options)+len(cfg.CLI.Command.args))
cfg.unifiedSources = make(map[string]OptionValuer, len(options)+len(cfg.CLI.Command.args))
// Iterate over positional CLI args. These have highest precedence of all, and
// are treated as a special-case (not placed in sources and work differently
// than normal options, since they cannot appear in option files)
for pos, arg := range cfg.CLI.Command.args {
if pos < len(cfg.CLI.ArgValues) { // supplied on CLI
cfg.unifiedSources[arg.Name] = cfg.CLI
cfg.unifiedValues[arg.Name] = cfg.CLI.ArgValues[pos]
delete(options, arg.Name) // shadow any normal option that has same name
} else { // not supplied on CLI - using default value
// In this case we intentionally DON'T shadow any normal option with same
// name, since a supplied option should override an unsupplied arg default.
cfg.unifiedSources[arg.Name] = cfg.CLI.Command
cfg.unifiedValues[arg.Name] = arg.Default
}
}
// Iterate over all options, and set them in our maps for tracking values and sources.
// We go in reverse order to start at highest priority and break early when a value is found.
allSources := cfg.Sources()
for name := range options {
var found bool
for n := len(allSources) - 1; n >= 0 && !found; n-- {
source := allSources[n]
if value, ok := source.OptionValue(name); ok {
cfg.unifiedValues[name] = value
cfg.unifiedSources[name] = source
found = true
}
}
if !found {
// If not even the Command provides a value, something is horribly wrong.
panic(fmt.Errorf("Assertion failed: Iterated over option %s not provided by command %s", name, cfg.CLI.Command.Name))
}
}
cfg.dirty = false
}
func (cfg *Config) rebuildIfDirty() {
if cfg.dirty {
cfg.rebuild()
}
}
// MarkDirty causes the config to rebuild itself on next option lookup. This
// is only needed in situations where a source is known to have changed since
// the previous lookup.
//
// Deprecated: Callers should prefer using SetRuntimeOverride, instead of
// directly manipulating a source and then calling MarkDirty.
func (cfg *Config) MarkDirty() {
cfg.dirty = true
}
// SetRuntimeOverride sets an override value for the supplied option name.
// This value takes precedence over all sources, including option values that
// were supplied on the CLI. The supplied name must correspond to a known option
// in cfg, otherwise this method panics.
func (cfg *Config) SetRuntimeOverride(name, value string) {
var optionExists bool
if cfg.dirty {
optionExists = (cfg.FindOption(name) != nil)
} else {
_, optionExists = cfg.unifiedSources[name]
}
if !optionExists {
panic(fmt.Errorf("Assertion failed: option %s does not exist", name))
}
cfg.runtimeOverrides[name] = value
if !cfg.dirty {
// Instead of marking the config as dirty and rebuilding it lazily, we can
// just set the value right away, since runtime overrides are always the
// highest priority source.
cfg.unifiedValues[name] = value
cfg.unifiedSources[name] = cfg.runtimeOverrides
}
}
// Changed returns true if the specified option name has been set, and its
// set value (after unquoting) differs from the option's default value.
func (cfg *Config) Changed(name string) bool {
if !cfg.Supplied(name) {
return false
}
opt := cfg.FindOption(name)
// Note that opt cannot be nil here, so no need to check. If the name didn't
// correspond to an existing option, the previous call to Supplied panics.
return (unquote(cfg.unifiedValues[name]) != opt.Default)
}
// Supplied returns true if the specified option name has been set by some
// configuration source, or false if not.
//
// Note that Supplied returns true even if some source has set the option to a
// value *equal to its default value*. If you want to check if an option
// *differs* from its default value (the more common situation), use Changed. As
// an example, imagine that one source sets an option to a non-default value,
// but some other higher-priority source explicitly sets it back to its default
// value. In this case, Supplied returns true but Changed returns false.
func (cfg *Config) Supplied(name string) bool {
source := cfg.Source(name)
switch source.(type) {
case *Command:
return false
default:
return true
}
}
// SuppliedWithValue returns true if the specified option name has been set by
// some configuration source AND had a value specified, even if that value was
// a blank string or empty value. For example, this returns true even for
// --foo="" or --foo= on a command line, or foo="" or foo= in an option file.
// Returns false for bare --foo on CLI or bare foo in an option file.
// This method is only usable on OptionTypeString options with !RequireValue.
// Panics if the supplied option name does not meet those requirements.
func (cfg *Config) SuppliedWithValue(name string) bool {
opt := cfg.FindOption(name)
if opt.Type != OptionTypeString || opt.RequireValue {
panic(fmt.Errorf("Assertion failed: SuppliedWithValue called on wrong kind of option %s", name))
}
if !cfg.Supplied(name) {
return false
}
// Hacky: this relies on logic in both CommandLine.parseLongArg() and
// File.Parse(), which store empty strings as "''" (vs bareword valueless
// options as ""). This makes it possible to differentiate between the two
// using Config.GetRaw(), since it does not strip quotes like Config.Get().
return cfg.GetRaw(name) != ""
}
// OnCLI returns true if the specified option name was set on the command-line,
// or false otherwise. If the option does not exist, panics to indicate
// programmer error.
func (cfg *Config) OnCLI(name string) bool {
return cfg.Source(name) == cfg.CLI
}
// Source returns the OptionValuer that provided the specified option. If the
// option does not exist, panics to indicate programmer error.
func (cfg *Config) Source(name string) OptionValuer {
cfg.rebuildIfDirty()
source, ok := cfg.unifiedSources[name]
if !ok {
panic(fmt.Errorf("Assertion failed: option %s does not exist", name))
}
return source
}
// FindOption returns an Option by name. It first searches the current command
// hierarchy, but if it fails to find the option there, it then searches all
// other command hierarchies as well. This makes it suitable for use in parsing
// option files, which may refer to options that aren't relevant to the current
// command but exist in some other command.
// Returns nil if no option with that name can be found anywhere.
func (cfg *Config) FindOption(name string) *Option {
myOptions := cfg.CLI.Command.Options()
if opt, ok := myOptions[name]; ok {
return opt
}
for _, arg := range cfg.CLI.Command.args { // args are option-like, but stored differently
if arg.Name == name {
return arg
}
}
var helper func(*Command) *Option
helper = func(cmd *Command) *Option {
if opt, ok := cmd.options[name]; ok {
return opt
}
for _, arg := range cmd.args {
if arg.Name == name {
return arg
}
}
for _, sub := range cmd.SubCommands {
opt := helper(sub)
if opt != nil {
return opt
}
}
return nil
}
return helper(cfg.CLI.Command.Root())
}
// GetRaw returns an option's value as-is as a string. If the option is not set,
// its default value will be returned. Panics if the option does not exist,
// since this is indicative of programmer error, not runtime error.
func (cfg *Config) GetRaw(name string) string {
cfg.rebuildIfDirty()
value, ok := cfg.unifiedValues[name]
if !ok {
panic(fmt.Errorf("Assertion failed: called Get on unknown option %s", name))
}
return value
}
// Get returns an option's value as a string. If the entire value is wrapped
// in quotes (single, double, or backticks) they will be stripped, and
// escaped quotes or backslashes within the string will be unescaped. If the
// option is not set, its default value will be returned. Panics if the option
// does not exist, since this is indicative of programmer error, not runtime
// error.
func (cfg *Config) Get(name string) string {
value := cfg.GetRaw(name)
return unquote(value)
}
// GetAllowEnvVar works like Get, but with additional support for ENV variables:
// If the option value begins with $, it will be replaced with the value of the
// corresponding environment variable, or an empty string if that variable is
// not set.
// Environment value lookups only occur if the option value came from a source
// other than the CLI, since it is assumed that shells already handle the CLI
// use-case appropriately. The value must also either not be quote-wrapped, or
// be wrapped in double-quotes. (This way, literal values that just happen to
// begin with a dollar sign may be expressed by wrapping a string in single-
// quotes.)
// Note that this method does NOT perform full variable interpolation: env
// vars may not be present mid-string, nor can the form ${varname} be used.
func (cfg *Config) GetAllowEnvVar(name string) string {
unquoted, quote := trimQuotes(cfg.GetRaw(name))
if len(unquoted) < 2 || unquoted[0] != '$' || quote == '\'' || quote == '`' || cfg.OnCLI(name) {
return unquoted
}
return os.Getenv(unquoted[1:])
}
// GetSlice returns an option's value as a slice of strings, splitting on
// the provided delimiter. Delimiters contained inside quoted values have no
// effect, nor do backslash-escaped delimiters. Quote-wrapped tokens will have
// their surrounding quotes stripped in the returned value. Leading and trailing
// whitespace in any token will be stripped. Empty values will be removed.
//
// unwrapFullValue determines how an entirely-quoted-wrapped option value is
// treated: if true, a fully quote-wrapped option value will be unquoted before
// being parsed for delimiters. If false, a fully-quote-wrapped option value
// will be treated as a single token, resulting in a one-element slice.
func (cfg *Config) GetSlice(name string, delimiter rune, unwrapFullValue bool) []string {
var value string
if unwrapFullValue {
value = cfg.Get(name)
} else {
value = cfg.GetRaw(name)
}
return splitValueIntoSlice(value, delimiter)
}
// GetSliceAllowEnvVar works like a combination of GetAllowEnvVar and GetSlice:
// if the configured value is of form $FOO, and the $FOO environment variable
// stores a comma-separated list of values, the list will be split using the
// supplied delimiter.
// Options can either be set to literal lists (as per GetSlice) or to a single
// env variable name (as per GetAllowEnvVar), but not a combination. In other
// words, if an option value is set to "a,$FOO,b" then this will not expand
// $FOO.
// unwrapFullValue only applies to values which aren't set via env vars.
func (cfg *Config) GetSliceAllowEnvVar(name string, delimiter rune, unwrapFullValue bool) []string {
raw := cfg.GetRaw(name)
unquoted, quote := trimQuotes(raw)
var value string
if len(unquoted) >= 2 && unquoted[0] == '$' && quote != '\'' && quote != '`' && !cfg.OnCLI(name) {
value = os.Getenv(unquoted[1:])
} else if unwrapFullValue {
value = unquoted
} else {
value = raw
}
return splitValueIntoSlice(value, delimiter)
}
func splitValueIntoSlice(value string, delimiter rune) []string {
tokens := []string{}
var startToken int
var inQuote rune
var escapeNext bool
for n, c := range value + string(delimiter) {
if escapeNext && n < len(value) {
escapeNext = false
continue
}
switch c {
case '\\':
escapeNext = true
case delimiter:
if inQuote == 0 || n == len(value) {
token := strings.TrimSpace(unquote(value[startToken:n]))
if token != "" {
tokens = append(tokens, token)
}
startToken = n + 1
}
case '\'', '"', '`':
if inQuote > 0 {
inQuote = 0
} else {
inQuote = c
}
}
}
return tokens
}
// GetBool returns an option's value as a bool. If the option is not set, its
// default value will be returned. Panics if the flag does not exist.
func (cfg *Config) GetBool(name string) bool {
return BoolValue(cfg.Get(name))
}
// GetInt returns an option's value as an int. If an error occurs in parsing
// the value as an int, it is returned as the second return value. Panics if
// the option does not exist.
func (cfg *Config) GetInt(name string) (int, error) {
return strconv.Atoi(cfg.Get(name))
}
// GetIntOrDefault is like GetInt, but returns the option's default value if
// parsing the supplied value as an int fails. Panics if the option does not
// exist.
func (cfg *Config) GetIntOrDefault(name string) int {
value, err := cfg.GetInt(name)
if err != nil {
defaultValue, _ := cfg.CLI.Command.OptionValue(name)
value, err = strconv.Atoi(defaultValue)
if err != nil {
panic(fmt.Errorf("Assertion failed: default value for option %s is %s, which fails int parsing", name, defaultValue))
}
}
return value
}
// GetEnum returns an option's value as a string if it matches one of the
// supplied allowed values, or its default value (which need not be supplied).
// Otherwise an error is returned. Matching is case-insensitive, but the
// returned value will always be of the same case as it was supplied in
// allowedValues. Panics if the option does not exist.
func (cfg *Config) GetEnum(name string, allowedValues ...string) (string, error) {
value := strings.ToLower(cfg.Get(name))
defaultValue, _ := cfg.CLI.Command.OptionValue(name)
var seenDefaultInAllowed bool
for _, allowedVal := range allowedValues {
if value == strings.ToLower(allowedVal) {
return allowedVal, nil
}
if strings.ToLower(allowedVal) == strings.ToLower(defaultValue) {
seenDefaultInAllowed = true
}
}
if !seenDefaultInAllowed {
if value == strings.ToLower(defaultValue) {
return defaultValue, nil
}
allowedValues = append(allowedValues, defaultValue)
}
for n := range allowedValues {
allowedValues[n] = fmt.Sprintf(`"%s"`, allowedValues[n])
}
allAllowed := strings.Join(allowedValues, ", ")
return "", fmt.Errorf("Option %s can only be set to one of these values: %s", name, allAllowed)
}
// GetBytes returns an option's value as a uint64 representing a number of bytes.
// If the value was supplied with a suffix of K, M, or G (upper or lower case)
// the returned value will automatically be multiplied by 1024, 1024^2, or
// 1024^3 respectively. Suffixes may also be expressed with a trailing 'B',
// e.g. 'KB' and 'K' are equivalent.
// A blank string will be returned as 0, with no error. Aside from that case,
// an error will be returned if the value cannot be parsed as a byte size.
// Panics if the option does not exist.
func (cfg *Config) GetBytes(name string) (uint64, error) {
var multiplier uint64 = 1
value := strings.ToLower(cfg.Get(name))
if value == "" {
return 0, nil
}
if value[len(value)-1] == 'b' {
value = value[0 : len(value)-1]
}
if strings.LastIndexAny(value, "kmg") == len(value)-1 {
multipliers := map[byte]uint64{
'k': 1024,
'm': 1024 * 1024,
'g': 1024 * 1024 * 1024,
}
suffix := value[len(value)-1]
value = value[0 : len(value)-1]
multiplier = multipliers[suffix]
}
numVal, err := strconv.ParseUint(value, 10, 64)
return numVal * multiplier, err
}
// GetRegexp returns an option's value as a compiled *regexp.Regexp. If the
// option value isn't set (empty string), returns nil,nil. If the option value
// is set but cannot be compiled as a valid regular expression, returns nil and
// an error value. Panics if the named option does not exist.
func (cfg *Config) GetRegexp(name string) (*regexp.Regexp, error) {
value := cfg.Get(name)
if value == "" {
return nil, nil
}
re, err := regexp.Compile(value)
if err != nil {
return nil, fmt.Errorf("Invalid regexp for option %s: %s", name, value)
}
return re, nil
}
// GetAbsPath returns an option's value as an absolute path to a file. If the
// option value is already set to an absolute path, it is returned as-is. If
// the option value is set to a relative path, the result depends on where the
// option was set. In an option file, a relative path will be interpreted based
// on the directory containing that option file. In all other cases (command-
// line, option default value, runtime override), a relative path will be
// interpreted based on the working directory at the time of GetAbsPath being
// called.
func (cfg *Config) GetAbsPath(name string) (string, error) {
value := cfg.Get(name)
if value == "" || filepath.IsAbs(value) {
return value, nil
}
if f, ok := cfg.Source(name).(*File); ok {
return filepath.Join(f.Dir, value), nil
}
workingDir, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Join(workingDir, value), nil
}
// unquote takes a string, trims whitespace on both ends, and then examines
// whether the entire string is wrapped in quotes. If it isn't, the string
// is returned as-is after the whitespace is trimmed. Otherwise, the string
// will have its wrapped quotes removed, and escaped values within the string
// will be un-escaped.
func unquote(input string) string {
unquoted, _ := trimQuotes(input)
return unquoted
}
// trimQuotes behaves like unquote, but also returns the quote rune that was
// removed, or a zero-valued rune if no unquoting occurred.
func trimQuotes(input string) (string, rune) {
input = strings.TrimSpace(input)
// If the string isn't quote-wrapped, return as-is.
// Since the only supported quote characters are single-byte, no need to be
// cautious about multi-byte chars in these conditionals.
if len(input) < 2 {
return input, 0
}
quote := rune(input[0])
if (quote != '`' && quote != '"' && quote != '\'') || quote != rune(input[len(input)-1]) {
return input, 0
}
// Do a pass through the string. Store each rune in a buffer, unescaping
// escaped quotes in the process. If we hit a terminating quote midway thru
// the string, return the original value. (We don't unquote or unescape
// anything unless the *entire* value is quoted.)
var escapeNext bool
var buf strings.Builder
buf.Grow(len(input) - 2)
for _, r := range input[1 : len(input)-1] {
if r == quote && !escapeNext {
// we hit an unescaped terminating quote midway in the string, meaning the
// entire input is not quote-wrapped
return input, 0
}
if r == '\\' && !escapeNext {
escapeNext = true
continue
}
escapeNext = false
buf.WriteRune(r)
}
return buf.String(), quote
}