summaryrefslogtreecommitdiff
path: root/pkgtools/pkglint/files/vartype.go
blob: 2bd90c889d66153badb3e2afae858ce51a91bf71 (plain)
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
package pkglint

import (
	"path"
	"sort"
	"strings"
)

// Vartype is a combination of a data type and a permission specification.
// See vardefs.go for examples, and vartypecheck.go for the implementation.
type Vartype struct {
	basicType  *BasicType
	options    vartypeOptions
	aclEntries []ACLEntry
}

func NewVartype(basicType *BasicType, options vartypeOptions, aclEntries ...ACLEntry) *Vartype {
	return &Vartype{basicType, options, aclEntries}
}

type vartypeOptions uint16

const (
	// List is a compound type, consisting of several space-separated elements.
	// Elements can have embedded spaces by enclosing them in double or single
	// quotes, like in the shell.
	//
	// These lists are used in the :M, :S modifiers, in .for loops,
	// and as lists of arbitrary things.
	List vartypeOptions = 1 << iota

	// The variable is not defined by the pkgsrc infrastructure.
	// It follows the common naming convention, therefore its type can be guessed.
	// Sometimes, with files and paths, this leads to wrong decisions.
	Guessed

	// The variable can, or in some cases must, be defined by the package.
	// For several of these variables, the pkgsrc infrastructure provides
	// a reasonable default value, either in bsd.prefs.mk or in bsd.pkg.mk.
	PackageSettable

	// The variable can be defined by the pkgsrc user in mk.conf.
	// Its value is available at load time after bsd.prefs.mk has been included.
	UserSettable

	// This variable is provided by either the pkgsrc infrastructure in
	// mk/*, or by <sys.mk>, which is included at the very beginning.
	//
	// TODO: Clearly distinguish between:
	//  * sys.mk
	//  * bsd.prefs.mk
	//  * bsd.pkg.mk
	//  * other parts of the pkgsrc infrastructure
	//  * environment variables
	//  Having all these possibilities as boolean flags is probably not
	//  expressive enough. This is related to the scope and lifetime of
	//  variables and should be modelled separately.
	//
	// See DefinedInSysMk.
	SystemProvided

	// This variable may be provided in the command line by the pkgsrc
	// user when building a package.
	//
	// Since the values of these variables are not written down in any
	// file, they must not influence the generated binary packages.
	//
	// See UserSettable.
	CommandLineProvided

	// NeedsRationale marks variables that should always contain a comment
	// describing why they are set. Typical examples are NOT_FOR_* variables.
	NeedsRationale

	// When something is appended to this variable, each additional
	// value should be on a line of its own.
	OnePerLine

	// AlwaysInScope is true when the variable is always available.
	//
	// One possibility is that the variable is defined in <sys.mk>,
	// which means that its value is loaded even before the package
	// Makefile is parsed.
	//
	// Another possibility is that the variable is local to a target,
	// such as .TARGET or .IMPSRC.
	//
	// These variables may be used at load time in .if and .for
	// directives even before bsd.prefs.mk is included.
	//
	// XXX: This option is related to the lifetime of the variable.
	//  Other aspects of the lifetime are handled by ACLPermissions,
	//  see aclpUseLoadtime.
	AlwaysInScope

	// DefinedIfInScope is true if the variable is guaranteed to be
	// defined, provided that it is in scope.
	//
	// This means the variable can be used in expressions like ${VAR}
	// without having to add the :U modifier like in ${VAR:U}.
	//
	// This option is independent of the lifetime of the variable,
	// it merely expresses "if the variable is in scope, it is defined".
	// As of December 2019, the lifetime of variables is managed by
	// the ACLPermissions, but is incomplete.
	//
	// TODO: Model the lifetime and scope separately, see SystemProvided.
	//
	// Examples:
	//  MACHINE_PLATFORM (from sys.mk)
	//  PKGPATH (from bsd.prefs.mk)
	//  PREFIX (from bsd.pkg.mk)
	DefinedIfInScope

	// NonemptyIfDefined is true if the variable is guaranteed to be
	// nonempty, provided that the variable is in scope and defined.
	//
	// This is typical for system-provided variables like PKGPATH or
	// MACHINE_PLATFORM, as well as package-settable variables like
	// PKGNAME.
	//
	// This option is independent of the lifetime of the variable,
	// it merely expresses "if the variable is in scope, it is defined".
	// As of December 2019, the lifetime of variables is managed by
	// the ACLPermissions, but is incomplete.
	//
	// TODO: Model the lifetime and scope separately, see SystemProvided.
	//
	// Examples:
	//  MACHINE_PLATFORM (from sys.mk)
	//  PKGPATH (from bsd.prefs.mk)
	//  PREFIX (from bsd.pkg.mk)
	//  PKGNAME (package-settable)
	//  X11_TYPE (user-settable)
	NonemptyIfDefined

	NoVartypeOptions = 0
)

type ACLEntry struct {
	matcher     *pathMatcher
	permissions ACLPermissions
}

func NewACLEntry(glob string, permissions ACLPermissions) ACLEntry {
	return ACLEntry{newPathMatcher(glob), permissions}
}

type ACLPermissions uint8

const (
	aclpSet         ACLPermissions = 1 << iota // VAR = value
	aclpSetDefault                             // VAR ?= value
	aclpAppend                                 // VAR += value
	aclpUseLoadtime                            // OTHER := ${VAR}, OTHER != ${VAR}
	aclpUse                                    // OTHER = ${VAR}

	aclpNone ACLPermissions = 0

	aclpAllWrite   = aclpSet | aclpSetDefault | aclpAppend
	aclpAllRead    = aclpUseLoadtime | aclpUse
	aclpAll        = aclpAllWrite | aclpAllRead
	aclpAllRuntime = aclpAll &^ aclpUseLoadtime
)

// Contains returns whether each permission of the given subset is
// contained in this permission set.
func (perms ACLPermissions) Contains(subset ACLPermissions) bool {
	return perms&subset == subset
}

func (perms ACLPermissions) String() string {
	if perms == 0 {
		return "none"
	}
	return joinSkipEmpty(", ",
		condStr(perms.Contains(aclpSet), "set", ""),
		condStr(perms.Contains(aclpSetDefault), "set-default", ""),
		condStr(perms.Contains(aclpAppend), "append", ""),
		condStr(perms.Contains(aclpUseLoadtime), "use-loadtime", ""),
		condStr(perms.Contains(aclpUse), "use", ""))
}

func (perms ACLPermissions) HumanString() string {
	return joinSkipEmptyOxford("or",
		condStr(perms.Contains(aclpSet), "set", ""),
		condStr(perms.Contains(aclpSetDefault), "given a default value", ""),
		condStr(perms.Contains(aclpAppend), "appended to", ""),
		condStr(perms.Contains(aclpUseLoadtime), "used at load time", ""),
		condStr(perms.Contains(aclpUse), "used", ""))
}

func (vt *Vartype) IsList() bool                { return vt.options&List != 0 }
func (vt *Vartype) IsGuessed() bool             { return vt.options&Guessed != 0 }
func (vt *Vartype) IsPackageSettable() bool     { return vt.options&PackageSettable != 0 }
func (vt *Vartype) IsUserSettable() bool        { return vt.options&UserSettable != 0 }
func (vt *Vartype) IsSystemProvided() bool      { return vt.options&SystemProvided != 0 }
func (vt *Vartype) IsCommandLineProvided() bool { return vt.options&CommandLineProvided != 0 }
func (vt *Vartype) NeedsRationale() bool        { return vt.options&NeedsRationale != 0 }
func (vt *Vartype) IsOnePerLine() bool          { return vt.options&OnePerLine != 0 }
func (vt *Vartype) IsAlwaysInScope() bool       { return vt.options&AlwaysInScope != 0 }
func (vt *Vartype) IsDefinedIfInScope() bool    { return vt.options&DefinedIfInScope != 0 }
func (vt *Vartype) IsNonemptyIfDefined() bool   { return vt.options&NonemptyIfDefined != 0 }

func (vt *Vartype) EffectivePermissions(basename string) ACLPermissions {
	for _, aclEntry := range vt.aclEntries {
		if aclEntry.matcher.matches(basename) {
			return aclEntry.permissions
		}
	}
	return aclpNone
}

// Union returns the union of all possible permissions.
// This can be used to check whether a variable may be defined or used
// at all, or if it is read-only.
func (vt *Vartype) Union() ACLPermissions {
	var permissions ACLPermissions
	for _, aclEntry := range vt.aclEntries {
		permissions |= aclEntry.permissions
	}
	return permissions
}

// AlternativeFiles lists the file patterns in which all of the given
// permissions are allowed, readily formatted to be used in a diagnostic.
//
// If the permission is allowed nowhere, an empty string is returned.
func (vt *Vartype) AlternativeFiles(perms ACLPermissions) string {
	var pos []string
	var neg []string

	merge := func(slice []string) []string {
		di := 0
		for si, early := range slice {
			redundant := false
			for _, late := range slice[si+1:] {
				matched, err := path.Match(late, early)
				assertNil(err, "path.Match")
				if matched {
					redundant = true
					break
				}
			}
			if !redundant {
				slice[di] = early
				di++
			}
		}
		return slice[:di]
	}

	for _, aclEntry := range vt.aclEntries {
		if aclEntry.permissions.Contains(perms) {
			pos = append(pos, aclEntry.matcher.originalPattern)
		} else {
			neg = append(neg, aclEntry.matcher.originalPattern)
		}
	}

	if len(neg) == 0 {
		pos = merge(pos)
	}
	if len(pos) == 0 {
		neg = merge(neg)
	}

	positive := joinSkipEmptyCambridge("or", pos...)
	if positive == "" {
		return ""
	}

	negative := joinSkipEmptyCambridge("or", neg...)
	if negative == "" {
		return positive
	}

	if negative == "*" {
		return positive + " only"
	}

	return positive + ", but not " + negative
}

func (vt *Vartype) MayBeAppendedTo() bool {
	if vt.IsList() {
		return true
	}

	switch vt.basicType {
	case BtAwkCommand, BtSedCommands, BtShellCommand, BtShellCommands, BtConfFiles:
		return true
	case BtComment, BtLicense:
		return true
	}
	return false
}

func (vt *Vartype) String() string {
	var opts []string
	if vt.IsList() {
		opts = append(opts, "list")
	}
	if vt.IsGuessed() {
		opts = append(opts, "guessed")
	}
	if vt.IsPackageSettable() {
		opts = append(opts, "package-settable")
	}
	if vt.IsUserSettable() {
		opts = append(opts, "user-settable")
	}
	if vt.IsSystemProvided() {
		opts = append(opts, "system-provided")
	}
	if vt.IsCommandLineProvided() {
		opts = append(opts, "command-line-provided")
	}

	optsSuffix := ""
	if len(opts) > 0 {
		optsSuffix = " (" + strings.Join(opts, ", ") + ")"
	}

	return vt.basicType.name + optsSuffix
}

func (vt *Vartype) IsShell() bool {
	switch vt.basicType {
	case BtCFlag, // Subtype of ShellWord
		BtLdFlag, // Subtype of ShellWord
		BtSedCommands,
		BtShellCommand,
		BtShellCommands,
		BtShellWord:
		return true
	}
	return false
}

// NeedsQ returns whether variables of this type need the :Q
// modifier to be safely embedded in other variables or shell programs.
//
// Variables that can consist only of characters like A-Za-z0-9-._
// don't need the :Q modifier. All others do, for safety reasons.
func (bt *BasicType) NeedsQ() bool {
	switch bt {
	case BtBuildlinkDepmethod,
		BtCategory,
		BtDistSuffix,
		BtEmulPlatform,
		BtFileMode,
		BtFilename,
		BtIdentifierDirect,
		BtIdentifierIndirect,
		BtInteger,
		BtMachineGnuPlatform,
		BtMachinePlatform,
		BtOption,
		BtPathname,
		BtPerl5Packlist,
		BtPkgname,
		BtPkgOptionsVar,
		BtPkgpath,
		BtPkgrevision,
		BtPrefixPathname,
		BtPythonDependency,
		BtRPkgName,
		BtRPkgVer,
		BtRelativePkgDir,
		BtRelativePkgPath,
		BtStage,
		BtTool, // Sometimes contains a colon, but that should be ok.
		BtUserGroupName,
		BtVersion,
		BtWrkdirSubdirectory,
		BtYesNo,
		BtYesNoIndirectly:
		return false
	}
	return !bt.IsEnum()
}

type BasicType struct {
	name    string
	checker func(*VartypeCheck)
}

func (bt *BasicType) IsEnum() bool {
	return hasPrefix(bt.name, "enum: ")
}

func (bt *BasicType) HasEnum(value string) bool {
	return !contains(value, " ") && contains(bt.name, " "+value+" ")
}

func (bt *BasicType) AllowedEnums() string {
	return bt.name[6 : len(bt.name)-1]
}

// TODO: Try to implement BasicType.PossibleChars()
// TODO: Try to implement BasicType.CanBeEmpty()
// TODO: Try to implement BasicType.PossibleWords() / PossibleValues()

var (
	BtAwkCommand             = &BasicType{"AwkCommand", (*VartypeCheck).AwkCommand}
	BtBasicRegularExpression = &BasicType{"BasicRegularExpression", (*VartypeCheck).BasicRegularExpression}
	BtBuildlinkDepmethod     = &BasicType{"BuildlinkDepmethod", (*VartypeCheck).BuildlinkDepmethod}
	BtCategory               = &BasicType{"Category", (*VartypeCheck).Category}
	BtCFlag                  = &BasicType{"CFlag", (*VartypeCheck).CFlag}
	BtComment                = &BasicType{"Comment", (*VartypeCheck).Comment}
	BtConfFiles              = &BasicType{"ConfFiles", (*VartypeCheck).ConfFiles}
	BtDependency             = &BasicType{"Dependency", (*VartypeCheck).Dependency}
	BtDependencyWithPath     = &BasicType{"DependencyWithPath", (*VartypeCheck).DependencyWithPath}
	BtDistSuffix             = &BasicType{"DistSuffix", (*VartypeCheck).DistSuffix}
	BtEmulPlatform           = &BasicType{"EmulPlatform", (*VartypeCheck).EmulPlatform}
	BtFetchURL               = &BasicType{"FetchURL", (*VartypeCheck).FetchURL}
	BtFilename               = &BasicType{"Filename", (*VartypeCheck).Filename}
	BtFilePattern            = &BasicType{"FilePattern", (*VartypeCheck).FilePattern}
	BtFileMode               = &BasicType{"FileMode", (*VartypeCheck).FileMode}
	BtGccReqd                = &BasicType{"GccReqd", (*VartypeCheck).GccReqd}
	BtHomepage               = &BasicType{"Homepage", (*VartypeCheck).Homepage}
	BtIdentifierDirect       = &BasicType{"Identifier", (*VartypeCheck).IdentifierDirect}
	BtIdentifierIndirect     = &BasicType{"Identifier", (*VartypeCheck).IdentifierIndirect}
	BtInteger                = &BasicType{"Integer", (*VartypeCheck).Integer}
	BtLdFlag                 = &BasicType{"LdFlag", (*VartypeCheck).LdFlag}
	BtLicense                = &BasicType{"License", (*VartypeCheck).License}
	BtMachineGnuPlatform     = &BasicType{"MachineGnuPlatform", (*VartypeCheck).MachineGnuPlatform}
	BtMachinePlatform        = &BasicType{"MachinePlatform", (*VartypeCheck).MachinePlatform}
	BtMachinePlatformPattern = &BasicType{"MachinePlatformPattern", (*VartypeCheck).MachinePlatformPattern}
	BtMailAddress            = &BasicType{"MailAddress", (*VartypeCheck).MailAddress}
	BtMessage                = &BasicType{"Message", (*VartypeCheck).Message}
	BtOption                 = &BasicType{"Option", (*VartypeCheck).Option}
	BtPathlist               = &BasicType{"Pathlist", (*VartypeCheck).Pathlist}
	BtPathPattern            = &BasicType{"PathPattern", (*VartypeCheck).PathPattern}
	BtPathname               = &BasicType{"Pathname", (*VartypeCheck).Pathname}
	BtPerl5Packlist          = &BasicType{"Perl5Packlist", (*VartypeCheck).Perl5Packlist}
	BtPerms                  = &BasicType{"Perms", (*VartypeCheck).Perms}
	BtPkgname                = &BasicType{"Pkgname", (*VartypeCheck).Pkgname}
	BtPkgpath                = &BasicType{"Pkgpath", (*VartypeCheck).Pkgpath}
	BtPkgOptionsVar          = &BasicType{"PkgOptionsVar", (*VartypeCheck).PkgOptionsVar}
	BtPkgrevision            = &BasicType{"Pkgrevision", (*VartypeCheck).Pkgrevision}
	BtPrefixPathname         = &BasicType{"PrefixPathname", (*VartypeCheck).PrefixPathname}
	BtPythonDependency       = &BasicType{"PythonDependency", (*VartypeCheck).PythonDependency}
	BtRPkgName               = &BasicType{"RPkgName", (*VartypeCheck).RPkgName}
	BtRPkgVer                = &BasicType{"RPkgVer", (*VartypeCheck).RPkgVer}
	BtRelativePkgDir         = &BasicType{"RelativePkgDir", (*VartypeCheck).RelativePkgDir}
	BtRelativePkgPath        = &BasicType{"RelativePkgPath", (*VartypeCheck).RelativePkgPath}
	BtRestricted             = &BasicType{"Restricted", (*VartypeCheck).Restricted}
	BtSedCommands            = &BasicType{"SedCommands", (*VartypeCheck).SedCommands}
	BtShellCommand           = &BasicType{"ShellCommand", nil}  // see func init below
	BtShellCommands          = &BasicType{"ShellCommands", nil} // see func init below
	BtShellWord              = &BasicType{"ShellWord", nil}     // see func init below
	BtStage                  = &BasicType{"Stage", (*VartypeCheck).Stage}
	BtTool                   = &BasicType{"Tool", (*VartypeCheck).Tool}
	BtUnknown                = &BasicType{"Unknown", (*VartypeCheck).Unknown}
	BtURL                    = &BasicType{"URL", (*VartypeCheck).URL}
	BtUserGroupName          = &BasicType{"UserGroupName", (*VartypeCheck).UserGroupName}
	BtVariableName           = &BasicType{"VariableName", (*VartypeCheck).VariableName}
	BtVariableNamePattern    = &BasicType{"VariableNamePattern", (*VartypeCheck).VariableNamePattern}
	BtVersion                = &BasicType{"Version", (*VartypeCheck).Version}
	BtWrapperReorder         = &BasicType{"WrapperReorder", (*VartypeCheck).WrapperReorder}
	BtWrapperTransform       = &BasicType{"WrapperTransform", (*VartypeCheck).WrapperTransform}
	BtWrkdirSubdirectory     = &BasicType{"WrkdirSubdirectory", (*VartypeCheck).WrkdirSubdirectory}
	BtWrksrcSubdirectory     = &BasicType{"WrksrcSubdirectory", (*VartypeCheck).WrksrcSubdirectory}
	BtYes                    = &BasicType{"Yes", (*VartypeCheck).Yes}
	BtYesNo                  = &BasicType{"YesNo", (*VartypeCheck).YesNo}
	BtYesNoIndirectly        = &BasicType{"YesNoIndirectly", (*VartypeCheck).YesNoIndirectly}

	BtMachineOpsys            = enumFromValues(machineOpsysValues)
	BtMachineArch             = enumFromValues(machineArchValues)
	BtMachineGnuArch          = enumFromValues(machineGnuArchValues)
	BtEmulOpsys               = enumFromValues(emulOpsysValues)
	BtEmulArch                = enumFromValues(machineArchValues) // Just a wild guess.
	BtMachineGnuPlatformOpsys = BtEmulOpsys

	btForLoop = &BasicType{".for loop", nil /* never called */}
)

// Necessary due to circular dependencies between the checkers.
//
// The Go compiler is stricter than absolutely necessary for this particular case.
// The following methods are only referred to but not invoked during initialization.
func init() {
	BtShellCommand.checker = (*VartypeCheck).ShellCommand
	BtShellCommands.checker = (*VartypeCheck).ShellCommands
	BtShellWord.checker = (*VartypeCheck).ShellWord
}

// TODO: Move these values to VarTypeRegistry.Init and read them from the
//  pkgsrc infrastructure files, as far as possible.
const (
	machineOpsysValues = "" + // See mk/platform
		"AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD " +
		"HPUX Haiku IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare"

	// See mk/emulator/emulator-vars.mk.
	emulOpsysValues = "" +
		"bitrig bsdos cygwin darwin dragonfly freebsd " +
		"haiku hpux interix irix linux mirbsd netbsd openbsd osf1 solaris sunos"

	// Hardware architectures having the same name in bsd.own.mk and the GNU world.
	// These are best-effort guesses, since they depend on the operating system.
	archValues = "" +
		"aarch64 alpha amd64 arc arm cobalt convex dreamcast i386 " +
		"hpcmips hpcsh hppa hppa64 ia64 " +
		"m68k m88k mips mips64 mips64el mipseb mipsel mipsn32 mlrisc " +
		"ns32k pc532 pmax powerpc powerpc64 rs6000 s390 sparc sparc64 vax x86_64"

	// See mk/bsd.prefs.mk:/^GNU_ARCH\./
	machineArchValues = "" +
		archValues + " " +
		"aarch64eb amd64 arm26 arm32 coldfire earm earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 " +
		"earmv5eb earmv6 earmv6eb earmv6hf earmv6hfeb earmv7 earmv7eb earmv7hf earmv7hfeb evbarm " +
		"i386 i586 i686 m68000 mips mips64eb sh3eb sh3el"

	// See mk/bsd.prefs.mk:/^GNU_ARCH\./
	machineGnuArchValues = "" +
		archValues + " " +
		"aarch64_be arm armeb armv4 armv4eb armv6 armv6eb armv7 armv7eb " +
		"i486 m5407 m68010 mips64 mipsel sh shle x86_64"
)

func enumFromValues(spaceSeparated string) *BasicType {
	values := strings.Fields(spaceSeparated)
	sort.Strings(values)
	seen := make(map[string]bool)
	var unique []string
	for _, value := range values {
		if !seen[value] {
			seen[value] = true
			unique = append(unique, value)
		}
	}
	return enum(strings.Join(unique, " "))
}