-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
330 lines (287 loc) · 9.41 KB
/
main.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
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/alecthomas/kong"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/cli/cmd"
"github.com/zk-org/zk/internal/core"
executil "github.com/zk-org/zk/internal/util/exec"
)
var Version = "dev"
var Build = "dev"
var root struct {
Init cmd.Init `cmd group:"zk" help:"Create a new notebook in the given directory."`
Index cmd.Index `cmd group:"zk" help:"Index the notes to be searchable."`
New cmd.New `cmd group:"notes" help:"Create a new note in the given notebook directory."`
List cmd.List `cmd group:"notes" help:"List notes matching the given criteria."`
Graph cmd.Graph `cmd group:"notes" help:"Produce a graph of the notes matching the given criteria."`
Edit cmd.Edit `cmd group:"notes" help:"Edit notes matching the given criteria."`
Tag cmd.Tag `cmd group:"notes" help:"Manage the note tags."`
NotebookDir string `type:path placeholder:PATH help:"Turn off notebook auto-discovery and set manually the notebook where commands are run."`
WorkingDir string `short:W type:path placeholder:PATH help:"Run as if zk was started in <PATH> instead of the current working directory."`
NoInput NoInput `help:"Never prompt or ask for confirmation."`
// ForceInput is a debugging flag overriding the default value of interaction prompts.
ForceInput string `hidden xor:"input"`
Debug bool `default:"0" hidden help:"Print a debug stacktrace on SIGINT."`
DebugStyle bool `default:"0" hidden help:"Force styling output as XML tags."`
ShowHelp ShowHelp `cmd hidden default:"1"`
LSP cmd.LSP `cmd hidden`
Version kong.VersionFlag `hidden help:"Print zk version."`
}
// NoInput is a flag preventing any user prompt when enabled.
type NoInput bool
func (f NoInput) BeforeApply(container *cli.Container) error {
container.Terminal.NoInput = true
return nil
}
// ShowHelp is the default command run. It's equivalent to `zk --help`.
type ShowHelp struct{}
func (cmd *ShowHelp) Run(container *cli.Container) error {
parser, err := kong.New(&root, options(container)...)
if err != nil {
return err
}
ctx, err := parser.Parse([]string{"--help"})
if err != nil {
return err
}
return ctx.Run(container)
}
func main() {
args := os.Args[1:]
// Create the dependency graph.
container, err := cli.NewContainer(Version)
fatalIfError(err)
// Open the notebook if there's any.
dirs, args, err := parseDirs(args)
fatalIfError(err)
searchDirs, err := notebookSearchDirs(dirs)
fatalIfError(err)
err = container.SetCurrentNotebook(searchDirs)
fatalIfError(err)
// Run the alias or command.
if isAlias, err := runAlias(container, args); isAlias {
fatalIfError(err)
} else {
parser, err := kong.New(&root, options(container)...)
fatalIfError(err)
ctx, err := parser.Parse(args)
fatalIfError(err)
if root.Debug {
setupDebugMode()
}
if root.DebugStyle {
container.Styler.Styler = core.TagStyler
}
container.Terminal.ForceInput = root.ForceInput
// Index the current notebook except if the user is running the `index`
// command, otherwise it would hide the stats.
if ctx.Command() != "index" {
if notebook, err := container.CurrentNotebook(); err == nil {
index := cmd.Index{Quiet: true}
err = index.RunWithNotebook(container, notebook)
ctx.FatalIfErrorf(err)
}
}
err = ctx.Run(container)
ctx.FatalIfErrorf(err)
}
}
func options(container *cli.Container) []kong.Option {
term := container.Terminal
return []kong.Option{
kong.Bind(container),
kong.Name("zk"),
kong.UsageOnError(),
kong.HelpOptions{
Compact: true,
FlagsLast: true,
WrapUpperBound: 100,
NoExpandSubcommands: true,
},
kong.Vars{
"version": "zk " + strings.TrimPrefix(Version, "v"),
},
kong.Groups(map[string]string{
"cmd": "Commands:",
"filter": "Filtering",
"sort": "Sorting",
"format": "Formatting",
"notes": term.MustStyle("NOTES", core.StyleYellow, core.StyleBold) + "\n" + term.MustStyle("Edit or browse your notes", core.StyleBold),
"zk": term.MustStyle("NOTEBOOK", core.StyleYellow, core.StyleBold) + "\n" + term.MustStyle("A notebook is a directory containing a collection of notes", core.StyleBold),
}),
}
}
func fatalIfError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "zk: error: %v\n", err)
os.Exit(1)
}
}
func setupDebugMode() {
c := make(chan os.Signal)
go func() {
stacktrace := make([]byte, 8192)
for _ = range c {
length := runtime.Stack(stacktrace, true)
fmt.Fprintf(os.Stderr, "%s\n", string(stacktrace[:length]))
os.Exit(1)
}
}()
signal.Notify(c, os.Interrupt)
}
// runAlias will execute a user alias if the command is one of them.
func runAlias(container *cli.Container, args []string) (bool, error) {
if len(args) < 1 {
return false, nil
}
runningAlias := os.Getenv("ZK_RUNNING_ALIAS")
for alias, cmdStr := range container.Config.Aliases {
if alias == runningAlias || alias != args[0] {
continue
}
// Prevent infinite loop if an alias calls itself.
os.Setenv("ZK_RUNNING_ALIAS", alias)
// Move to the current notebook's root directory before running the alias.
if notebook, err := container.CurrentNotebook(); err == nil {
cmdStr = `cd "` + notebook.Path + `" && ` + cmdStr
}
cmd := executil.CommandFromString(cmdStr, args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
if err, ok := err.(*exec.ExitError); ok {
os.Exit(err.ExitCode())
return true, nil
} else {
return true, err
}
}
return true, nil
}
return false, nil
}
// notebookSearchDirs returns the places where zk will look for a notebook.
// The first successful candidate will be used as the working directory from
// which path arguments are relative from.
//
// By order of precedence:
// 1. --notebook-dir flag
// 2. current working directory
// 3. ZK_NOTEBOOK_DIR environment variable
func notebookSearchDirs(dirs cli.Dirs) ([]cli.Dirs, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
// 1. --notebook-dir flag
if dirs.NotebookDir != "" {
// If --notebook-dir is used, we want to only check there to report
// "notebook not found" errors.
if dirs.WorkingDir == "" {
dirs.WorkingDir = wd
}
return []cli.Dirs{dirs}, nil
}
candidates := []cli.Dirs{}
// 2. current working directory
wdDirs := dirs
if wdDirs.WorkingDir == "" {
wdDirs.WorkingDir = wd
}
wdDirs.NotebookDir = wdDirs.WorkingDir
candidates = append(candidates, wdDirs)
// 3. ZK_NOTEBOOK_DIR environment variable
if notebookDir, ok := os.LookupEnv("ZK_NOTEBOOK_DIR"); ok {
dirs := dirs
dirs.NotebookDir = notebookDir
if dirs.WorkingDir == "" {
dirs.WorkingDir = notebookDir
}
candidates = append(candidates, dirs)
}
return candidates, nil
}
// parseDirs returns the paths specified with the --notebook-dir and
// --working-dir flags.
//
// We need to parse these flags before Kong, because we might need it to
// resolve zk command aliases before parsing the CLI.
func parseDirs(args []string) (cli.Dirs, []string, error) {
var d cli.Dirs
var err error
// Split str by first "=" if present and return the split pair, otherwise return nil
makeSplitPair := func(str string) (pair []string) {
re := regexp.MustCompile(`=`)
slice := re.FindStringIndex(str)
if slice == nil {
return nil
}
return []string{str[:slice[0]], str[slice[1]:]}
}
// Peek ahead at next value and pair with current if it exists, otherwise return nil
makePeekPair := func(args []string, index int) (pair []string) {
if len(args) <= (index + 1) {
return nil
}
return []string{args[index], args[index+1]}
}
matchesLongOrShort := func(str string, long string, short string) bool {
return str == long || (short != "" && str == short)
}
findFlag := func(long string, short string, args []string) (string, []string, error) {
newArgs := []string{}
for i, arg := range args {
// We can be given "--notebook-dir x" (two args) or "--notebook-dir=x" (one arg)
// so we must test against the current argument split into two, and
// the current argument + the next.
splitPair := makeSplitPair(arg)
peekPair := makePeekPair(args, i)
var option string
var value string
if splitPair != nil && matchesLongOrShort(splitPair[0], long, short) {
option = splitPair[0]
value = splitPair[1]
// skip 1 ahead
newArgs = append(newArgs, args[i+1:]...)
} else if peekPair != nil && matchesLongOrShort(peekPair[0], long, short) {
option = peekPair[0]
value = peekPair[1]
// skip 2 ahead (arg and value)
newArgs = append(newArgs, args[i+2:]...)
} else {
// we either had no split pair or peek pair, or they didn't match the
// needle, so just save the given arg and keep looking.
newArgs = append(newArgs, arg)
}
if option != "" && value != "" {
path, err := filepath.Abs(value)
return path, newArgs, err
} else if option != "" && value == "" {
return "", newArgs, errors.New(option + " requires a path argument")
} else if len(args) == (i+1) && matchesLongOrShort(arg, long, short) {
return "", newArgs, errors.New(arg + " requires a path argument")
}
}
return "", newArgs, nil
}
d.NotebookDir, args, err = findFlag("--notebook-dir", "", args)
if err != nil {
return d, args, err
}
d.WorkingDir, args, err = findFlag("--working-dir", "-W", args)
if err != nil {
return d, args, err
}
return d, args, nil
}