aboutsummaryrefslogtreecommitdiff
path: root/opts.go
blob: b8c8b07f8f9de119998a874c770bdc5bba7130de (plain) (blame)
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
// Package opts implements unicode-aware getopt(3)- and getopt_long(3)
// flag parsing.
//
// The opts package aims to provide as simple an API as possible.  If
// your usecase requires more advanced argument-parsing or a more robust
// API, this may not be the ideal package for you.
//
// While the opts package aims to closely follow the POSIX getopt(3) and
// GNU getopt_long(3) C functions, there are some notable differences.
// This package properly supports unicode flags, but also does not
// support a leading ‘:’ in the [Get] function’s option string; all
// user-facing I/O is delegrated to the caller.
package opts

import (
	"slices"
	"strings"
)

// ArgMode represents the whether or not a long-option takes an argument.
type ArgMode int

// These tokens can be used to specify whether or not a long-option takes
// an argument.
const (
	None     ArgMode = iota // long opt takes no argument
	Required                // long opt takes an argument
	Optional                // long opt optionally takes an argument
)

// Flag represents a parsed command-line flag.  Key corresponds to the
// rune that was passed on the command-line, and Value corresponds to the
// flags argument if one was provided.  In the case of long-options Key
// will map to the corresponding short-code, even if a long-option was
// used.
type Flag struct {
	Key   rune   // the flag that was passed
	Value string // the flags argument
}

// LongOpt represents a long-option to attempt to parse.  All long
// options have a short-hand form represented by Short and a long-form
// represented by Long.  Arg is used to represent whether or not a takes
// an argument.
//
// In the case that you want to parse a long-option which doesn’t have a
// short-hand form, you can set Short to a negative integer.
type LongOpt struct {
	Short rune
	Long  string
	Arg   ArgMode
}

// Get parses the command-line arguments in args according to optstr.
// Unlike POSIX-getopt(3), a leading ‘:’ in optstr is not supported and
// will be ignored and no I/O is ever performed.
//
// Get will look for the flags listed in optstr (i.e., it will look for
// ‘-a’, ‘-ß’, and ‘λ’ given optstr == "aßλ").  The optstr need not be
// sorted in any particular order.  If an option takes a required
// argument, it can be suffixed by a colon.  If an option takes an
// optional argument, it can be suffixed by two colons.  As an example,
// optstr == "a::ßλ:" will search for ‘-a’ with an optional argument,
// ‘-ß’ with no argument, and ‘-λ’ with a required argument.
//
// A successful parse returns the flags in the flags slice and the index
// of the first non-option argument in optind.  In the case of failure,
// err will be one of [BadOptionError] or [NoArgumentError].
func Get(args []string, optstr string) (flags []Flag, optind int, err error) {
	if len(args) == 0 {
		return
	}

	optrs := []rune(optstr)
	var i int
	for i = 1; i < len(args); i++ {
		arg := args[i]
		if len(arg) == 0 || arg == "-" || arg[0] != '-' {
			break
		} else if arg == "--" {
			i++
			break
		}

		rs := []rune(arg[1:])
		for j, r := range rs {
			k := slices.Index(optrs, r)
			if k == -1 {
				return nil, 0, BadOptionError{r: r}
			}

			var s string
			switch am := colonsToArgMode(optrs[k+1:]); {
			case am != None && j < len(rs)-1:
				s = string(rs[j+1:])
			case am == Required:
				i++
				if i >= len(args) {
					return nil, 0, NoArgumentError{r: r}
				}
				s = args[i]
			default:
				flags = append(flags, Flag{Key: r})
				continue
			}

			flags = append(flags, Flag{r, s})
			break
		}
	}

	return flags, i, nil
}

func GetLong(args []string, opts []LongOpt) (flags []Flag, optind int, err error) {
	if len(args) == 0 {
		return
	}

	var i int
	for i = 1; i < len(args); i++ {
		arg := args[i]
		if len(arg) == 0 || arg == "-" || arg[0] != '-' {
			break
		} else if arg == "--" {
			i++
			break
		}

		if strings.HasPrefix(arg, "--") {
			arg = arg[2:]

			n := arg
			j := strings.IndexByte(n, '=')
			if j != -1 {
				n = arg[:j]
			}

			var s string
			o, ok := optStruct(opts, n)

			switch {
			case !ok:
				return nil, 0, BadOptionError{s: n}
			case o.Arg != None && j != -1:
				s = arg[j+1:]
			case o.Arg == Required:
				i++
				if i >= len(args) {
					return nil, 0, NoArgumentError{s: n}
				}
				s = args[i]
			}

			flags = append(flags, Flag{Key: o.Short, Value: s})
		} else {
			rs := []rune(arg[1:])
			for j, r := range rs {
				var s string

				switch am, ok := getModeRune(opts, r); {
				case !ok:
					return nil, 0, BadOptionError{r: r}
				case am != None && j < len(rs)-1:
					s = string(rs[j+1:])
				case am == Required:
					i++
					if i >= len(args) {
						return nil, 0, NoArgumentError{r: r}
					}
					s = args[i]
				default:
					flags = append(flags, Flag{Key: r})
					continue
				}

				flags = append(flags, Flag{r, s})
				break
			}
		}
	}

	return flags, i, nil
}

func getModeRune(os []LongOpt, r rune) (ArgMode, bool) {
	for _, o := range os {
		if o.Short == r {
			return o.Arg, true
		}
	}
	return 0, false
}

func optStruct(os []LongOpt, s string) (LongOpt, bool) {
	i := -1
	for j, o := range os {
		if strings.HasPrefix(o.Long, s) {
			if i != -1 {
				return LongOpt{}, false
			}
			i = j
		}
	}
	if i == -1 {
		return LongOpt{}, false
	}
	return os[i], true
}

func colonsToArgMode(rs []rune) ArgMode {
	if len(rs) >= 2 && rs[0] == ':' && rs[1] == ':' {
		return Optional
	}
	if len(rs) >= 1 && rs[0] == ':' {
		return Required
	}
	return None
}