turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

placeholder.go · 183 lines · 5.9 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 14h ago1package tools
2
3import (
4 "fmt"
5 "strings"
6)
7
8// A tool asks for a value by writing a placeholder into its command:
9//
10// command = "go mod init {{module path}}"
11//
12// The text between the braces is what the editor asks for. A trailing "..."
13// means the value is substituted verbatim rather than shell-quoted, which is
14// how one placeholder can stand for several arguments:
15//
16// command = "go test {{extra flags...}} ./..."
17//
18// **Double braces, not single.** Single ones appear in real commands — `awk
19// '{print $1}'` and `find . -exec rm {} +` are both ordinary things to put in a
20// tools file — and treating those as placeholders would turn a working command
21// into a dialog asking for "print $1".
22const (
23 placeholderOpen = "{{"
24 placeholderClose = "}}"
25 // rawSuffix inside a placeholder asks for the value verbatim.
26 rawSuffix = "..."
27)
28
29// Placeholder is a value a tool asks for before it runs.
30type Placeholder struct {
31 // Label is what the editor asks for: the text between the braces, trimmed,
32 // with any trailing "..." removed.
33 Label string
34 // Raw says the value is substituted verbatim rather than shell-quoted.
35 //
36 // A label written both ways in one command — `{{x}}` here and `{{x...}}`
37 // there — is asked for once and substituted according to *each* occurrence,
38 // so this reports what the first occurrence asked for.
39 Raw bool
40}
41
42// Placeholders returns the values a tool's command asks for, in the order they
43// first appear, with a label used twice reported once.
44//
45// A command with none gives nothing, which is what makes an ordinary tool run
46// straight away rather than opening an empty box.
47//
48// tools.Tool{Command: "go mod init {{module path}}"}.Placeholders()
49// // [{Label: "module path"}]
50func (t Tool) Placeholders() []Placeholder {
51 found, err := parsePlaceholders(t.Command)
52 if err != nil {
53 // Load refuses a command this cannot parse, so a Tool that came from a
54 // file never reaches here with a broken one. A Tool built in code might;
55 // asking for nothing is the safe answer, and running it unchanged shows
56 // the braces rather than silently dropping them.
57 return nil
58 }
59
60 var out []Placeholder
61 seen := map[string]bool{}
62 for _, p := range found {
63 if seen[p.Label] {
64 continue
65 }
66 seen[p.Label] = true
67 out = append(out, Placeholder{Label: p.Label, Raw: p.Raw})
68 }
69 return out
70}
71
72// Fill returns the command with every placeholder replaced by the value given
73// for its label.
74//
75// A value is **shell-quoted** unless its placeholder asked to be raw, because
76// the result is handed to `sh -c` and a value with a space in it silently
77// becoming two arguments is a bug nobody can see. A label with no value in the
78// map is replaced by nothing, which lets a command report its own complaint
79// rather than the editor guessing what an empty field meant.
80//
81// tool := tools.Tool{Command: "go build -o {{output}}"}
82// tool.Fill(map[string]string{"output": "my binary"})
83// // go build -o 'my binary'
84func (t Tool) Fill(values map[string]string) string {
85 found, err := parsePlaceholders(t.Command)
86 if err != nil {
87 return t.Command
88 }
89
90 var b strings.Builder
91 at := 0
92 for _, p := range found {
93 b.WriteString(t.Command[at:p.start])
94 b.WriteString(substitute(values[p.Label], p.Raw))
95 at = p.end
96 }
97 b.WriteString(t.Command[at:])
98 return b.String()
99}
100
101// substitute renders one value into a command line.
102func substitute(value string, raw bool) string {
103 if raw {
104 return value
105 }
106 return ShellQuote(value)
107}
108
109// ShellQuote wraps a string so that `sh -c` sees it as exactly one argument,
110// whatever is in it.
111//
112// Single quotes are used because nothing inside them is special to the shell —
113// no expansion, no escapes — so the only case to handle is a single quote in
114// the value itself, which ends the quoting, adds an escaped quote, and starts
115// it again.
116//
117// tools.ShellQuote("it's here") // 'it'\''s here'
118func ShellQuote(value string) string {
119 return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
120}
121
122// placeholderAt is one occurrence in a command, and where it sits.
123type placeholderAt struct {
124 Label string
125 Raw bool
126 start int // byte offset of the opening brace
127 end int // byte offset just past the closing brace
128}
129
130// parsePlaceholders finds every placeholder in a command, in order.
131//
132// It refuses a command it cannot make sense of rather than guessing: an opener
133// with no closer is a half-typed placeholder, and a placeholder with no label
134// is a box asking for nothing under a blank heading. Both are reported so the
135// person who wrote the file finds out from the editor rather than from a
136// command that ran with braces in it.
137func parsePlaceholders(command string) ([]placeholderAt, error) {
138 var out []placeholderAt
139 at := 0
140
141 for {
142 open := strings.Index(command[at:], placeholderOpen)
143 if open < 0 {
144 return out, nil
145 }
146 open += at
147
148 close := strings.Index(command[open:], placeholderClose)
149 if close < 0 {
150 return nil, fmt.Errorf("%q is never closed", command[open:])
151 }
152 close += open
153
154 inside := command[open+len(placeholderOpen) : close]
155 label, raw := splitRaw(inside)
156 if label == "" {
157 return nil, fmt.Errorf("%s%s%s asks for a value but does not say what it is",
158 placeholderOpen, inside, placeholderClose)
159 }
160
161 out = append(out, placeholderAt{Label: label, Raw: raw, start: open, end: close + len(placeholderClose)})
162 at = close + len(placeholderClose)
163 }
164}
165
166// splitRaw separates a placeholder's label from the "..." that asks for it
167// verbatim.
168func splitRaw(inside string) (label string, raw bool) {
169 trimmed := strings.TrimSpace(inside)
170 if after, found := strings.CutSuffix(trimmed, rawSuffix); found {
171 return strings.TrimSpace(after), true
172 }
173 return trimmed, false
174}
175
176// checkPlaceholders reports what is wrong with a tool's placeholders, naming
177// the tool and the file so the message says where to go.
178func checkPlaceholders(tool Tool, path string) error {
179 if _, err := parsePlaceholders(tool.Command); err != nil {
180 return fmt.Errorf("reading %s: tool %q: %w", path, tool.Name, err)
181 }
182 return nil
183}