-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbind.go
More file actions
168 lines (150 loc) · 4.52 KB
/
bind.go
File metadata and controls
168 lines (150 loc) · 4.52 KB
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
package cli
import (
"path/filepath"
"slices"
"strings"
"github.com/bjaus/bind"
)
// Args captures remaining positional arguments after named args are consumed.
// Declare an Args field (no tag needed) to receive unconsumed arguments:
//
// type CopyCmd struct {
// Src string `arg:"src"` // named arg, gets args[0]
// Dst string `arg:"dst"` // named arg, gets args[1]
// Args cli.Args // captures args[2:]
// }
//
// func (c *CopyCmd) Run(ctx context.Context) error {
// if c.Args.Empty() {
// return errors.New("no extra files")
// }
// for _, file := range c.Args {
// // process file
// }
// }
//
// Only one Args field is allowed per command. The field must be of type cli.Args.
type Args []string
// Len returns the number of arguments.
func (a Args) Len() int { return len(a) }
// Empty returns true if there are no arguments.
func (a Args) Empty() bool { return len(a) == 0 }
// First returns the first argument or empty string if none.
func (a Args) First() string {
if len(a) == 0 {
return ""
}
return a[0]
}
// Last returns the last argument or empty string if none.
func (a Args) Last() string {
if len(a) == 0 {
return ""
}
return a[len(a)-1]
}
// Get returns the argument at index i or empty string if out of bounds.
func (a Args) Get(i int) string {
if i < 0 || i >= len(a) {
return ""
}
return a[i]
}
// Contains returns true if the argument list contains s.
func (a Args) Contains(s string) bool { return slices.Contains(a, s) }
// Index returns the index of s or -1 if not found.
func (a Args) Index(s string) int { return slices.Index(a, s) }
// Tail returns all arguments after the first, or nil if empty.
func (a Args) Tail() Args {
if len(a) == 0 {
return nil
}
return a[1:]
}
// WithBindings registers dependencies for injection into command structs.
// Uses [bind.Option] from github.com/bjaus/bind.
//
// # Binding Modes
//
// Four binding modes are available:
//
// - bind.Value(v) — register a value, matched by its concrete type
// - bind.Interface(v, (*Iface)(nil)) — register a value as an interface type
// - bind.Provider(func() (T, error)) — lazy factory, called on each injection
// - bind.Singleton(func() (T, error)) — lazy factory, called once and cached
//
// # Example
//
// db, _ := sql.Open("postgres", connStr)
// cli.Execute(ctx, root, args,
// cli.WithBindings(
// bind.Value(db), // inject *sql.DB
// bind.Interface(cache, (*Cache)(nil)), // inject as Cache interface
// bind.Singleton(newLogger), // create once, share across commands
// bind.Provider(newRequestID), // create fresh each time
// ),
// )
//
// # Declaring Dependencies
//
// Commands declare dependencies as struct fields without tags:
//
// type ServeCmd struct {
// DB *sql.DB // injected by type
// C Cache // injected by interface
// Port int `flag:"port"` // NOT injected (has flag tag)
// }
//
// Fields with flag:, arg:, or env: tags are not eligible for injection.
// The injector matches by exact type first, then by interface compatibility.
//
// # Auto-Bound Types
//
// [Args] (positional arguments) is auto-bound. Commands can declare an Args
// field to receive remaining positional arguments:
//
// type GrepCmd struct {
// Pattern string `arg:"pattern"`
// Args cli.Args // automatically populated
// }
//
// # Context Lookup
//
// Bindings are also accessible in Run via [bind.Get]:
//
// func (s *ServeCmd) Run(ctx context.Context) error {
// db := bind.Get[*sql.DB](ctx)
// // ...
// }
//
// Use [bind.Lookup] for optional dependencies (returns value, bool).
func WithBindings(opts ...bind.Option) Option {
return func(o *options) {
o.bindOpts = append(o.bindOpts, opts...)
}
}
// stripProgramName removes the first argument if it looks like a program path.
// This allows callers to pass os.Args directly instead of os.Args[1:].
func stripProgramName(args []string, cmdName string) []string {
if len(args) == 0 {
return args
}
first := args[0]
// No path separators means it's likely a subcommand or bare arg.
if !strings.ContainsAny(first, `/\`) {
return args
}
base := filepath.Base(first)
base = strings.TrimSuffix(base, filepath.Ext(base))
if strings.EqualFold(base, cmdName) {
// Absolute paths that match: always strip (no I/O needed).
if filepath.IsAbs(first) {
return args[1:]
}
// Relative paths that match: only strip if executable.
if isExecutable(first) {
return args[1:]
}
}
return args
}