yerba 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +492 -15
- data/ext/yerba/extconf.rb +87 -30
- data/ext/yerba/include/yerba.h +168 -0
- data/ext/yerba/yerba.c +752 -0
- data/lib/yerba/collection.rb +31 -0
- data/lib/yerba/document.rb +59 -0
- data/lib/yerba/formatting.rb +18 -0
- data/lib/yerba/location.rb +5 -0
- data/lib/yerba/map.rb +166 -0
- data/lib/yerba/scalar.rb +85 -0
- data/lib/yerba/sequence.rb +182 -0
- data/lib/yerba/version.rb +1 -1
- data/lib/yerba.rb +30 -4
- data/rust/Cargo.lock +378 -2
- data/rust/Cargo.toml +5 -1
- data/rust/build.rs +11 -0
- data/rust/cbindgen.toml +27 -0
- data/rust/src/commands/apply.rs +5 -0
- data/rust/src/commands/blank_lines.rs +58 -0
- data/rust/src/commands/check.rs +5 -0
- data/rust/src/commands/delete.rs +35 -0
- data/rust/src/commands/get.rs +194 -0
- data/rust/src/commands/init.rs +89 -0
- data/rust/src/commands/insert.rs +106 -0
- data/rust/src/commands/mate.rs +55 -0
- data/rust/src/commands/mod.rs +349 -0
- data/rust/src/commands/move_item.rs +54 -0
- data/rust/src/commands/move_key.rs +87 -0
- data/rust/src/commands/quote_style.rs +62 -0
- data/rust/src/commands/remove.rs +35 -0
- data/rust/src/commands/rename.rs +37 -0
- data/rust/src/commands/set.rs +59 -0
- data/rust/src/commands/sort.rs +52 -0
- data/rust/src/commands/sort_keys.rs +62 -0
- data/rust/src/commands/version.rs +8 -0
- data/rust/src/document.rs +764 -333
- data/rust/src/error.rs +0 -5
- data/rust/src/ffi.rs +991 -0
- data/rust/src/json.rs +49 -90
- data/rust/src/lib.rs +9 -2
- data/rust/src/main.rs +55 -843
- data/rust/src/selector.rs +241 -0
- data/rust/src/syntax.rs +97 -21
- data/rust/src/yaml_writer.rs +89 -0
- data/rust/src/yerbafile.rs +11 -126
- data/yerba.gemspec +4 -0
- metadata +33 -1
data/rust/src/main.rs
CHANGED
|
@@ -1,860 +1,72 @@
|
|
|
1
|
-
mod
|
|
1
|
+
mod commands;
|
|
2
2
|
|
|
3
|
-
use std::
|
|
4
|
-
use std::process;
|
|
3
|
+
use std::sync::LazyLock;
|
|
5
4
|
|
|
6
|
-
use clap::{
|
|
5
|
+
use clap::builder::styling::{AnsiColor, Effects, Styles};
|
|
6
|
+
use clap::Parser;
|
|
7
7
|
use indoc::indoc;
|
|
8
8
|
|
|
9
|
+
const STYLES: Styles = Styles::styled()
|
|
10
|
+
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
|
|
11
|
+
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
|
|
12
|
+
.literal(AnsiColor::Cyan.on_default())
|
|
13
|
+
.placeholder(AnsiColor::Yellow.on_default())
|
|
14
|
+
.valid(AnsiColor::Green.on_default());
|
|
15
|
+
|
|
16
|
+
static HELP: LazyLock<String> = LazyLock::new(|| {
|
|
17
|
+
commands::colorize_help(indoc! {r#"
|
|
18
|
+
Selectors:
|
|
19
|
+
key A single key "database.host"
|
|
20
|
+
key.nested Nested key path "database.settings.pool"
|
|
21
|
+
[] All items in array "[].title"
|
|
22
|
+
[N] Item at index "[0].title"
|
|
23
|
+
[].key[].nested Nested array access "[].speakers[].name"
|
|
24
|
+
|
|
25
|
+
Conditions:
|
|
26
|
+
.key == value Equality ".kind == keynote"
|
|
27
|
+
.key != value Inequality ".status != draft"
|
|
28
|
+
.key contains val Substring or member ".title contains Ruby"
|
|
29
|
+
.key not_contains Negated contains ".title not_contains test"
|
|
30
|
+
|
|
31
|
+
Yerbafile:
|
|
32
|
+
yerba init Create a new Yerbafile in the current directory
|
|
33
|
+
yerba check Check if all files match the rules (exits 1 if not)
|
|
34
|
+
yerba apply Apply all rules and write changes
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
yerba get config.yml "database.host"
|
|
38
|
+
yerba get videos.yml "[0].title"
|
|
39
|
+
yerba get videos.yml "[]" --select ".title,.speakers"
|
|
40
|
+
yerba get "data/**/videos.yml" "[]" --condition ".kind == keynote" --select ".id,.title"
|
|
41
|
+
yerba set config.yml "database.host" "0.0.0.0"
|
|
42
|
+
yerba insert config.yml "tags" "yaml" --after "ruby"
|
|
43
|
+
yerba insert speakers.yml "" --from "speaker.yml" --after ".name == Alice"
|
|
44
|
+
yerba delete config.yml "database.pool"
|
|
45
|
+
yerba move videos.yml "" ".id == talk-2" --after ".id == talk-1"
|
|
46
|
+
yerba sort-keys config.yml "database" "id,host,port,name"
|
|
47
|
+
yerba quote-style "data/**/*.yml" --values double
|
|
48
|
+
"#})
|
|
49
|
+
});
|
|
50
|
+
|
|
9
51
|
#[derive(Parser)]
|
|
10
52
|
#[command(
|
|
11
53
|
name = "yerba",
|
|
12
54
|
version = yerba::version(),
|
|
55
|
+
disable_version_flag = true,
|
|
56
|
+
styles = STYLES,
|
|
13
57
|
about = "Yerba 🧉 YAML Editing and Refactoring with Better Accuracy",
|
|
14
58
|
arg_required_else_help = true,
|
|
15
|
-
override_usage = "
|
|
59
|
+
override_usage = "\x1b[1myerba\x1b[0m <command> <file> <selector> [options]",
|
|
16
60
|
disable_help_subcommand = true,
|
|
17
|
-
after_help =
|
|
18
|
-
Examples:
|
|
19
|
-
yerba get config.yml database.host
|
|
20
|
-
yerba set config.yml database.host 0.0.0.0
|
|
21
|
-
yerba insert config.yml database.ssl true --after host
|
|
22
|
-
yerba delete config.yml database.pool
|
|
23
|
-
yerba find "data/**/videos.yml" "[]" --condition '.kind == keynote' --select 'id,title'
|
|
24
|
-
yerba sort-keys config.yml database 'id,host,port,name'
|
|
25
|
-
yerba quote-style "data/**/*.yml" double
|
|
26
|
-
yerba apply
|
|
27
|
-
"#}
|
|
61
|
+
after_help = HELP.as_str()
|
|
28
62
|
)]
|
|
29
|
-
|
|
63
|
+
#[allow(clippy::upper_case_acronyms)]
|
|
64
|
+
struct CLI {
|
|
30
65
|
#[command(subcommand)]
|
|
31
|
-
command: Command,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
#[derive(Subcommand)]
|
|
35
|
-
enum Command {
|
|
36
|
-
#[command(
|
|
37
|
-
about = "Get values at a path (single or multi-value with [] brackets)",
|
|
38
|
-
arg_required_else_help = true,
|
|
39
|
-
after_help = indoc! {r#"
|
|
40
|
-
Examples:
|
|
41
|
-
yerba get config.yml database.host
|
|
42
|
-
yerba get config.yml database.host --condition '.port == 5432'
|
|
43
|
-
yerba get videos.yml "[].title"
|
|
44
|
-
yerba get "data/**/videos.yml" "[].speakers[].name"
|
|
45
|
-
yerba get videos.yml "[0].title"
|
|
46
|
-
"#}
|
|
47
|
-
)]
|
|
48
|
-
Get {
|
|
49
|
-
file: String,
|
|
50
|
-
path: String,
|
|
51
|
-
#[arg(long)]
|
|
52
|
-
condition: Option<String>,
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
#[command(
|
|
56
|
-
about = "Find and filter items with conditions, output as JSON or raw YAML",
|
|
57
|
-
arg_required_else_help = true,
|
|
58
|
-
after_help = indoc! {r#"
|
|
59
|
-
Examples:
|
|
60
|
-
yerba find "data/**/videos.yml" "[]" --condition '.kind == keynote'
|
|
61
|
-
yerba find "data/**/videos.yml" "[]" --select 'id,title' --condition '.kind == keynote'
|
|
62
|
-
yerba find "data/**/videos.yml" "[]" --select 'id,title,speakers[0].name'
|
|
63
|
-
yerba find "data/**/videos.yml" "[]" --condition '.title contains Ruby' --raw
|
|
64
|
-
yerba find "data/**/videos.yml" "[]" --condition '.speakers contains "Matz"'
|
|
65
|
-
"#}
|
|
66
|
-
)]
|
|
67
|
-
Find {
|
|
68
|
-
file: String,
|
|
69
|
-
path: String,
|
|
70
|
-
#[arg(long)]
|
|
71
|
-
condition: Option<String>,
|
|
72
|
-
/// Comma-separated fields to include (supports dot paths like speakers[].name)
|
|
73
|
-
#[arg(long)]
|
|
74
|
-
select: Option<String>,
|
|
75
|
-
/// Output raw YAML instead of JSON
|
|
76
|
-
#[arg(long)]
|
|
77
|
-
raw: bool,
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
#[command(
|
|
81
|
-
about = "Update an existing value at a path (preserves quote style)",
|
|
82
|
-
arg_required_else_help = true,
|
|
83
|
-
after_help = indoc! {r#"
|
|
84
|
-
Examples:
|
|
85
|
-
yerba set config.yml database.host 0.0.0.0
|
|
86
|
-
yerba set config.yml database.host 0.0.0.0 --if-exists
|
|
87
|
-
yerba set config.yml database.host 0.0.0.0 --condition '.port == 5432'
|
|
88
|
-
yerba set config.yml database.host 0.0.0.0 --dry-run
|
|
89
|
-
yerba set "data/**/event.yml" website "" --if-exists
|
|
90
|
-
"#}
|
|
91
|
-
)]
|
|
92
|
-
Set {
|
|
93
|
-
file: String,
|
|
94
|
-
path: String,
|
|
95
|
-
value: String,
|
|
96
|
-
#[arg(long)]
|
|
97
|
-
if_exists: bool,
|
|
98
|
-
#[arg(long)]
|
|
99
|
-
if_missing: bool,
|
|
100
|
-
#[arg(long)]
|
|
101
|
-
condition: Option<String>,
|
|
102
|
-
#[arg(long)]
|
|
103
|
-
dry_run: bool,
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
#[command(
|
|
107
|
-
about = "Insert a new key into a map or item into a sequence",
|
|
108
|
-
arg_required_else_help = true,
|
|
109
|
-
after_help = indoc! {r#"
|
|
110
|
-
Examples:
|
|
111
|
-
yerba insert config.yml database.ssl true
|
|
112
|
-
yerba insert config.yml database.ssl true --after host
|
|
113
|
-
yerba insert config.yml database.ssl true --before port
|
|
114
|
-
yerba insert config.yml tags yaml
|
|
115
|
-
yerba insert config.yml tags yaml --at 0
|
|
116
|
-
yerba insert config.yml tags yaml --after ruby
|
|
117
|
-
"#}
|
|
118
|
-
)]
|
|
119
|
-
Insert {
|
|
120
|
-
file: String,
|
|
121
|
-
path: String,
|
|
122
|
-
value: String,
|
|
123
|
-
#[arg(long)]
|
|
124
|
-
before: Option<String>,
|
|
125
|
-
#[arg(long)]
|
|
126
|
-
after: Option<String>,
|
|
127
|
-
#[arg(long)]
|
|
128
|
-
at: Option<usize>,
|
|
129
|
-
#[arg(long)]
|
|
130
|
-
dry_run: bool,
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
#[command(
|
|
134
|
-
about = "Rename a key in a map (preserves value and position)",
|
|
135
|
-
arg_required_else_help = true,
|
|
136
|
-
after_help = indoc! {"
|
|
137
|
-
Examples:
|
|
138
|
-
yerba rename config.yml database.host database.hostname
|
|
139
|
-
yerba rename config.yml database.host hostname
|
|
140
|
-
yerba rename config.yml database.host settings.db_host
|
|
141
|
-
"}
|
|
142
|
-
)]
|
|
143
|
-
Rename {
|
|
144
|
-
file: String,
|
|
145
|
-
source: String,
|
|
146
|
-
destination: String,
|
|
147
|
-
#[arg(long)]
|
|
148
|
-
dry_run: bool,
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
#[command(
|
|
152
|
-
about = "Delete a key and its value from a map",
|
|
153
|
-
arg_required_else_help = true,
|
|
154
|
-
after_help = indoc! {"
|
|
155
|
-
Examples:
|
|
156
|
-
yerba delete config.yml database.pool
|
|
157
|
-
yerba delete config.yml database.pool --dry-run
|
|
158
|
-
"}
|
|
159
|
-
)]
|
|
160
|
-
Delete {
|
|
161
|
-
file: String,
|
|
162
|
-
path: String,
|
|
163
|
-
#[arg(long)]
|
|
164
|
-
dry_run: bool,
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
#[command(
|
|
168
|
-
about = "Remove an item from a sequence by its value",
|
|
169
|
-
arg_required_else_help = true,
|
|
170
|
-
after_help = indoc! {"
|
|
171
|
-
Examples:
|
|
172
|
-
yerba remove config.yml tags rust
|
|
173
|
-
"}
|
|
174
|
-
)]
|
|
175
|
-
Remove {
|
|
176
|
-
file: String,
|
|
177
|
-
path: String,
|
|
178
|
-
value: String,
|
|
179
|
-
#[arg(long)]
|
|
180
|
-
dry_run: bool,
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
#[command(
|
|
184
|
-
about = "Move a sequence item to a new position",
|
|
185
|
-
arg_required_else_help = true,
|
|
186
|
-
after_help = indoc! {"
|
|
187
|
-
Examples:
|
|
188
|
-
yerba move config.yml tags rust --before ruby
|
|
189
|
-
yerba move config.yml tags rust --after yaml
|
|
190
|
-
yerba move config.yml tags 2 --to 0
|
|
191
|
-
"}
|
|
192
|
-
)]
|
|
193
|
-
Move {
|
|
194
|
-
file: String,
|
|
195
|
-
path: String,
|
|
196
|
-
item: String,
|
|
197
|
-
#[arg(long)]
|
|
198
|
-
before: Option<String>,
|
|
199
|
-
#[arg(long)]
|
|
200
|
-
after: Option<String>,
|
|
201
|
-
#[arg(long)]
|
|
202
|
-
to: Option<usize>,
|
|
203
|
-
#[arg(long)]
|
|
204
|
-
dry_run: bool,
|
|
205
|
-
},
|
|
206
|
-
|
|
207
|
-
#[command(
|
|
208
|
-
about = "Move a key to a new position within a map",
|
|
209
|
-
arg_required_else_help = true,
|
|
210
|
-
after_help = indoc! {"
|
|
211
|
-
Examples:
|
|
212
|
-
yerba move-key config.yml database.pool --before database.host
|
|
213
|
-
yerba move-key config.yml database.name --to 0
|
|
214
|
-
"}
|
|
215
|
-
)]
|
|
216
|
-
MoveKey {
|
|
217
|
-
file: String,
|
|
218
|
-
path: String,
|
|
219
|
-
#[arg(long)]
|
|
220
|
-
before: Option<String>,
|
|
221
|
-
#[arg(long)]
|
|
222
|
-
after: Option<String>,
|
|
223
|
-
#[arg(long)]
|
|
224
|
-
to: Option<usize>,
|
|
225
|
-
#[arg(long)]
|
|
226
|
-
dry_run: bool,
|
|
227
|
-
},
|
|
228
|
-
|
|
229
|
-
#[command(
|
|
230
|
-
about = "Sort keys in a map by a predefined order (aborts on unknown keys)",
|
|
231
|
-
arg_required_else_help = true,
|
|
232
|
-
after_help = indoc! {r#"
|
|
233
|
-
Examples:
|
|
234
|
-
yerba sort-keys config.yml database 'host,port,name,pool'
|
|
235
|
-
yerba sort-keys "data/**/event.yml" "" "id,title,kind,location"
|
|
236
|
-
yerba sort-keys "data/**/videos.yml" "[]" "id,title,speakers"
|
|
237
|
-
yerba sort-keys config.yml database 'host,port' --dry-run
|
|
238
|
-
"#}
|
|
239
|
-
)]
|
|
240
|
-
SortKeys {
|
|
241
|
-
file: String,
|
|
242
|
-
path: String,
|
|
243
|
-
/// Comma-separated key order
|
|
244
|
-
order: String,
|
|
245
|
-
#[arg(long)]
|
|
246
|
-
dry_run: bool,
|
|
247
|
-
},
|
|
248
|
-
|
|
249
|
-
#[command(
|
|
250
|
-
about = "Sort items in a sequence by field(s)",
|
|
251
|
-
arg_required_else_help = true,
|
|
252
|
-
after_help = indoc! {r#"
|
|
253
|
-
Examples:
|
|
254
|
-
yerba sort config.yml tags
|
|
255
|
-
yerba sort videos.yml "" --by title
|
|
256
|
-
yerba sort videos.yml "" --by "date:desc,title"
|
|
257
|
-
yerba sort videos.yml "[].speakers"
|
|
258
|
-
yerba sort videos.yml "[].speakers" --by name
|
|
259
|
-
yerba sort videos.yml "" --by "kind,date:desc,title" --dry-run
|
|
260
|
-
"#}
|
|
261
|
-
)]
|
|
262
|
-
Sort {
|
|
263
|
-
file: String,
|
|
264
|
-
path: String,
|
|
265
|
-
/// Comma-separated sort fields, optionally with :desc (e.g. "date:desc,title")
|
|
266
|
-
#[arg(long)]
|
|
267
|
-
by: Option<String>,
|
|
268
|
-
/// Case-sensitive sort (default: case-insensitive)
|
|
269
|
-
#[arg(long)]
|
|
270
|
-
case_sensitive: bool,
|
|
271
|
-
#[arg(long)]
|
|
272
|
-
dry_run: bool,
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
#[command(
|
|
276
|
-
about = "Enforce a consistent quote style on values, keys, or both",
|
|
277
|
-
arg_required_else_help = true,
|
|
278
|
-
after_help = indoc! {"
|
|
279
|
-
Examples:
|
|
280
|
-
yerba quote-style config.yml double
|
|
281
|
-
yerba quote-style config.yml plain --keys
|
|
282
|
-
yerba quote-style config.yml double --all
|
|
283
|
-
yerba quote-style config.yml single --path database.host
|
|
284
|
-
"}
|
|
285
|
-
)]
|
|
286
|
-
QuoteStyle {
|
|
287
|
-
file: String,
|
|
288
|
-
/// Quote style
|
|
289
|
-
style: yerba::QuoteStyle,
|
|
290
|
-
/// Scope to a specific path
|
|
291
|
-
#[arg(long)]
|
|
292
|
-
path: Option<String>,
|
|
293
|
-
/// Apply to keys only
|
|
294
|
-
#[arg(long)]
|
|
295
|
-
keys: bool,
|
|
296
|
-
/// Apply to both keys and values
|
|
297
|
-
#[arg(long)]
|
|
298
|
-
all: bool,
|
|
299
|
-
#[arg(long)]
|
|
300
|
-
dry_run: bool,
|
|
301
|
-
},
|
|
302
|
-
|
|
303
|
-
#[command(
|
|
304
|
-
about = "Enforce blank lines between sequence entries",
|
|
305
|
-
arg_required_else_help = true,
|
|
306
|
-
after_help = indoc! {r#"
|
|
307
|
-
Examples:
|
|
308
|
-
yerba blank-lines videos.yml "" 1
|
|
309
|
-
yerba blank-lines "data/**/videos.yml" "[]" 1
|
|
310
|
-
yerba blank-lines config.yml tags 0
|
|
311
|
-
"#}
|
|
312
|
-
)]
|
|
313
|
-
BlankLines {
|
|
314
|
-
file: String,
|
|
315
|
-
path: String,
|
|
316
|
-
/// Number of blank lines between entries (0 = no blanks, 1 = one empty line)
|
|
317
|
-
count: usize,
|
|
318
|
-
#[arg(long)]
|
|
319
|
-
dry_run: bool,
|
|
320
|
-
},
|
|
321
|
-
|
|
322
|
-
#[command(about = "Apply all rules from the Yerbafile and write changes")]
|
|
323
|
-
Apply,
|
|
324
|
-
#[command(about = "Check if all files match Yerbafile rules (exits 1 if not)")]
|
|
325
|
-
Check,
|
|
326
|
-
#[command(about = "Print the yerba version")]
|
|
327
|
-
Version,
|
|
66
|
+
command: commands::Command,
|
|
328
67
|
}
|
|
329
68
|
|
|
330
69
|
fn main() {
|
|
331
|
-
let cli =
|
|
332
|
-
|
|
333
|
-
match cli.command {
|
|
334
|
-
Command::Get { file, path, condition } => {
|
|
335
|
-
for resolved_file in resolve_files(&file) {
|
|
336
|
-
let document = parse_file(&resolved_file);
|
|
337
|
-
|
|
338
|
-
if let Some(condition) = &condition {
|
|
339
|
-
let parent_path = path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
340
|
-
|
|
341
|
-
if !document.evaluate_condition(parent_path, condition) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
let values = document.get_all(&path);
|
|
347
|
-
|
|
348
|
-
if values.is_empty() && !path.contains('[') && !document.exists(&path) {
|
|
349
|
-
eprintln!("Path not found: {}", path);
|
|
350
|
-
process::exit(1);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
for value in values {
|
|
354
|
-
println!("{}", value);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
Command::Find {
|
|
360
|
-
file,
|
|
361
|
-
path,
|
|
362
|
-
condition,
|
|
363
|
-
select,
|
|
364
|
-
raw,
|
|
365
|
-
} => {
|
|
366
|
-
let select_fields: Option<Vec<&str>> = select.as_deref().map(|fields| fields.split(',').collect());
|
|
367
|
-
|
|
368
|
-
if raw {
|
|
369
|
-
for resolved_file in resolve_files(&file) {
|
|
370
|
-
let document = parse_file(&resolved_file);
|
|
371
|
-
|
|
372
|
-
let matches = match &condition {
|
|
373
|
-
Some(condition) => document.find_items(&path, condition),
|
|
374
|
-
None => document.find_all(&path),
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
for (index, item) in matches.iter().enumerate() {
|
|
378
|
-
if index > 0 {
|
|
379
|
-
println!();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
eprintln!("# {}:{}", resolved_file, item.line);
|
|
383
|
-
println!("{}", item.text);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
let mut all_results: Vec<serde_json::Value> = Vec::new();
|
|
388
|
-
|
|
389
|
-
for resolved_file in resolve_files(&file) {
|
|
390
|
-
let document = parse_file(&resolved_file);
|
|
391
|
-
|
|
392
|
-
let matches = match &condition {
|
|
393
|
-
Some(condition) => document.find_items(&path, condition),
|
|
394
|
-
None => document.find_all(&path),
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
for item in &matches {
|
|
398
|
-
let yaml_with_dash = format!("- {}", item.text.trim_start_matches("- "));
|
|
399
|
-
|
|
400
|
-
if let Ok(parsed) = serde_yaml::from_str::<Vec<serde_yaml::Value>>(&yaml_with_dash) {
|
|
401
|
-
for value in parsed {
|
|
402
|
-
let mut result = serde_json::Map::new();
|
|
403
|
-
|
|
404
|
-
result.insert("__file".to_string(), serde_json::Value::String(resolved_file.clone()));
|
|
405
|
-
|
|
406
|
-
result.insert("__line".to_string(), serde_json::Value::Number(item.line.into()));
|
|
407
|
-
|
|
408
|
-
match &select_fields {
|
|
409
|
-
Some(fields) => {
|
|
410
|
-
for field in fields {
|
|
411
|
-
let json_value = json::resolve_select_field(&value, field);
|
|
412
|
-
let json_key = json::select_field_key(field);
|
|
413
|
-
|
|
414
|
-
result.insert(json_key, json_value);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
None => {
|
|
419
|
-
if let serde_yaml::Value::Mapping(map) = &value {
|
|
420
|
-
for (key, yaml_value) in map {
|
|
421
|
-
let json_key = match key {
|
|
422
|
-
serde_yaml::Value::String(string) => string.clone(),
|
|
423
|
-
_ => format!("{:?}", key),
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
result.insert(json_key, json::yaml_to_json(yaml_value));
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
all_results.push(serde_json::Value::Object(result));
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
println!(
|
|
439
|
-
"{}",
|
|
440
|
-
serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string())
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
Command::Set {
|
|
446
|
-
file,
|
|
447
|
-
path,
|
|
448
|
-
value,
|
|
449
|
-
if_exists,
|
|
450
|
-
if_missing,
|
|
451
|
-
condition,
|
|
452
|
-
dry_run,
|
|
453
|
-
} => {
|
|
454
|
-
let mut document = parse_file(&file);
|
|
455
|
-
let parent_path = path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
456
|
-
|
|
457
|
-
let should_set = if if_exists {
|
|
458
|
-
document.exists(&path)
|
|
459
|
-
} else if if_missing {
|
|
460
|
-
!document.exists(&path)
|
|
461
|
-
} else if let Some(condition) = &condition {
|
|
462
|
-
document.evaluate_condition(parent_path, condition)
|
|
463
|
-
} else {
|
|
464
|
-
true
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
if should_set {
|
|
468
|
-
run(|| document.set(&path, &value));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
output(&file, &document, dry_run);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
Command::Insert {
|
|
475
|
-
file,
|
|
476
|
-
path,
|
|
477
|
-
value,
|
|
478
|
-
before,
|
|
479
|
-
after,
|
|
480
|
-
at,
|
|
481
|
-
dry_run,
|
|
482
|
-
} => {
|
|
483
|
-
let parent_path = path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
484
|
-
|
|
485
|
-
let position = if let Some(index) = at {
|
|
486
|
-
yerba::InsertPosition::At(index)
|
|
487
|
-
} else if let Some(target) = before {
|
|
488
|
-
yerba::InsertPosition::Before(target)
|
|
489
|
-
} else if let Some(target) = after {
|
|
490
|
-
yerba::InsertPosition::After(target)
|
|
491
|
-
} else {
|
|
492
|
-
yerba::Yerbafile::find()
|
|
493
|
-
.and_then(|yerbafile_path| yerba::Yerbafile::load(&yerbafile_path).ok())
|
|
494
|
-
.and_then(|yerbafile| yerbafile.sort_order_for(&file, parent_path))
|
|
495
|
-
.map(yerba::InsertPosition::FromSortOrder)
|
|
496
|
-
.unwrap_or(yerba::InsertPosition::Last)
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
let mut document = parse_file(&file);
|
|
500
|
-
run(|| document.insert_into(&path, &value, position));
|
|
501
|
-
output(&file, &document, dry_run);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
Command::Rename {
|
|
505
|
-
file,
|
|
506
|
-
source,
|
|
507
|
-
destination,
|
|
508
|
-
dry_run,
|
|
509
|
-
} => {
|
|
510
|
-
let mut document = parse_file(&file);
|
|
511
|
-
run(|| document.rename(&source, &destination));
|
|
512
|
-
output(&file, &document, dry_run);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
Command::Delete { file, path, dry_run } => {
|
|
516
|
-
let mut document = parse_file(&file);
|
|
517
|
-
run(|| document.delete(&path));
|
|
518
|
-
output(&file, &document, dry_run);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
Command::Remove {
|
|
522
|
-
file,
|
|
523
|
-
path,
|
|
524
|
-
value,
|
|
525
|
-
dry_run,
|
|
526
|
-
} => {
|
|
527
|
-
let mut document = parse_file(&file);
|
|
528
|
-
run(|| document.remove(&path, &value));
|
|
529
|
-
output(&file, &document, dry_run);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
Command::Move {
|
|
533
|
-
file,
|
|
534
|
-
path,
|
|
535
|
-
item,
|
|
536
|
-
before,
|
|
537
|
-
after,
|
|
538
|
-
to,
|
|
539
|
-
dry_run,
|
|
540
|
-
} => {
|
|
541
|
-
let mut document = parse_file(&file);
|
|
542
|
-
let (from_index, to_index) = resolve_move_indexes(
|
|
543
|
-
&document,
|
|
544
|
-
&path,
|
|
545
|
-
&item,
|
|
546
|
-
before,
|
|
547
|
-
after,
|
|
548
|
-
to,
|
|
549
|
-
|document, path, reference| document.resolve_sequence_index(path, reference),
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
run(|| document.move_item(&path, from_index, to_index));
|
|
553
|
-
output(&file, &document, dry_run);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
Command::MoveKey {
|
|
557
|
-
file,
|
|
558
|
-
path,
|
|
559
|
-
before,
|
|
560
|
-
after,
|
|
561
|
-
to,
|
|
562
|
-
dry_run,
|
|
563
|
-
} => {
|
|
564
|
-
let (parent_path, key) = path.rsplit_once('.').unwrap_or(("", &path));
|
|
565
|
-
|
|
566
|
-
let mut document = parse_file(&file);
|
|
567
|
-
|
|
568
|
-
let before_key = before.map(|target| {
|
|
569
|
-
let (target_parent, target_key) = target.rsplit_once('.').unwrap_or(("", &target));
|
|
570
|
-
|
|
571
|
-
if target_parent != parent_path {
|
|
572
|
-
eprintln!(
|
|
573
|
-
"Error: cannot move key across different maps ({} → {})\n\n Use 'yerba rename' to relocate keys to a different path.",
|
|
574
|
-
path, target
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
process::exit(1);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
target_key.to_string()
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
let after_key = after.map(|target| {
|
|
584
|
-
let (target_parent, target_key) = target.rsplit_once('.').unwrap_or(("", &target));
|
|
585
|
-
|
|
586
|
-
if target_parent != parent_path {
|
|
587
|
-
eprintln!(
|
|
588
|
-
"Error: cannot move key across different maps ({} → {})\n\n Use 'yerba rename' to relocate keys to a different path.",
|
|
589
|
-
path, target
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
process::exit(1);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
target_key.to_string()
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
let (from_index, to_index) = resolve_move_indexes(
|
|
599
|
-
&document,
|
|
600
|
-
parent_path,
|
|
601
|
-
key,
|
|
602
|
-
before_key,
|
|
603
|
-
after_key,
|
|
604
|
-
to,
|
|
605
|
-
|document, parent_path, reference| document.resolve_key_index(parent_path, reference),
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
run(|| document.move_key(parent_path, from_index, to_index));
|
|
609
|
-
output(&file, &document, dry_run);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
Command::SortKeys {
|
|
613
|
-
file,
|
|
614
|
-
path,
|
|
615
|
-
order,
|
|
616
|
-
dry_run,
|
|
617
|
-
} => {
|
|
618
|
-
let key_order: Vec<&str> = order.split(',').collect();
|
|
619
|
-
let files = resolve_files(&file);
|
|
620
|
-
|
|
621
|
-
let mut has_errors = false;
|
|
622
|
-
|
|
623
|
-
for resolved_file in &files {
|
|
624
|
-
let document = parse_file(resolved_file);
|
|
625
|
-
|
|
626
|
-
if let Err(error) = document.validate_sort_keys(&path, &key_order) {
|
|
627
|
-
eprintln!("Error in {}: {}", resolved_file, error);
|
|
628
|
-
has_errors = true;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if has_errors {
|
|
633
|
-
process::exit(1);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
for resolved_file in &files {
|
|
637
|
-
let mut document = parse_file(resolved_file);
|
|
638
|
-
|
|
639
|
-
if document.sort_keys(&path, &key_order).is_ok() {
|
|
640
|
-
output(resolved_file, &document, dry_run);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
Command::QuoteStyle {
|
|
646
|
-
file,
|
|
647
|
-
style,
|
|
648
|
-
path,
|
|
649
|
-
keys,
|
|
650
|
-
all,
|
|
651
|
-
dry_run,
|
|
652
|
-
} => {
|
|
653
|
-
let dot_path = path.as_deref();
|
|
654
|
-
|
|
655
|
-
for resolved_file in resolve_files(&file) {
|
|
656
|
-
let mut document = parse_file(&resolved_file);
|
|
657
|
-
|
|
658
|
-
if keys {
|
|
659
|
-
let _ = document.enforce_key_style(&style, dot_path);
|
|
660
|
-
} else if all {
|
|
661
|
-
let _ = document.enforce_key_style(&style, dot_path);
|
|
662
|
-
let _ = document.enforce_quotes_at(&style, dot_path);
|
|
663
|
-
} else {
|
|
664
|
-
let _ = document.enforce_quotes_at(&style, dot_path);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
output(&resolved_file, &document, dry_run);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
Command::Sort {
|
|
672
|
-
file,
|
|
673
|
-
path,
|
|
674
|
-
by,
|
|
675
|
-
case_sensitive,
|
|
676
|
-
dry_run,
|
|
677
|
-
} => {
|
|
678
|
-
let sort_fields = by.as_deref().map(yerba::SortField::parse_list).unwrap_or_default();
|
|
679
|
-
|
|
680
|
-
for resolved_file in resolve_files(&file) {
|
|
681
|
-
let mut document = parse_file(&resolved_file);
|
|
682
|
-
|
|
683
|
-
if document.sort_items(&path, &sort_fields, case_sensitive).is_ok() {
|
|
684
|
-
output(&resolved_file, &document, dry_run);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
Command::BlankLines {
|
|
690
|
-
file,
|
|
691
|
-
path,
|
|
692
|
-
count,
|
|
693
|
-
dry_run,
|
|
694
|
-
} => {
|
|
695
|
-
for resolved_file in resolve_files(&file) {
|
|
696
|
-
let mut document = parse_file(&resolved_file);
|
|
697
|
-
|
|
698
|
-
if document.enforce_blank_lines(&path, count).is_ok() {
|
|
699
|
-
output(&resolved_file, &document, dry_run);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
Command::Apply => run_yerbafile(true),
|
|
705
|
-
Command::Check => run_yerbafile(false),
|
|
706
|
-
|
|
707
|
-
Command::Version => {
|
|
708
|
-
println!("🧉 yerba v{}", yerba::version());
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
fn run_yerbafile(write: bool) {
|
|
714
|
-
let yerbafile_path = yerba::Yerbafile::find().unwrap_or_else(|| {
|
|
715
|
-
eprintln!("No Yerbafile found. Create one in the current directory or a parent.");
|
|
716
|
-
process::exit(1);
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
let yerbafile = yerba::Yerbafile::load(&yerbafile_path).unwrap_or_else(|error| {
|
|
720
|
-
eprintln!("Error loading {}: {}", yerbafile_path.display(), error);
|
|
721
|
-
process::exit(1);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
eprintln!("🧉 Using {}", yerbafile_path.display());
|
|
725
|
-
|
|
726
|
-
let results = yerbafile.apply(write);
|
|
727
|
-
let mut has_changes = false;
|
|
728
|
-
let mut has_errors = false;
|
|
729
|
-
|
|
730
|
-
for result in &results {
|
|
731
|
-
if let Some(error) = &result.error {
|
|
732
|
-
eprintln!(" error: {} — {}", result.file, error);
|
|
733
|
-
has_errors = true;
|
|
734
|
-
} else if result.changed {
|
|
735
|
-
if write {
|
|
736
|
-
eprintln!(" updated: {}", result.file);
|
|
737
|
-
} else {
|
|
738
|
-
eprintln!(" would change: {}", result.file);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
has_changes = true;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if !has_changes && !has_errors {
|
|
746
|
-
eprintln!(" All files match the rules.");
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if !write && has_changes {
|
|
750
|
-
process::exit(1);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if has_errors {
|
|
754
|
-
process::exit(1);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
fn resolve_move_indexes(
|
|
759
|
-
document: &yerba::Document,
|
|
760
|
-
path: &str,
|
|
761
|
-
item: &str,
|
|
762
|
-
before: Option<String>,
|
|
763
|
-
after: Option<String>,
|
|
764
|
-
to: Option<usize>,
|
|
765
|
-
resolve: impl Fn(&yerba::Document, &str, &str) -> Result<usize, yerba::YerbaError>,
|
|
766
|
-
) -> (usize, usize) {
|
|
767
|
-
let from_index = resolve(document, path, item).unwrap_or_else(|error| {
|
|
768
|
-
eprintln!("Error: {}", error);
|
|
769
|
-
process::exit(1);
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
let to_index = if let Some(index) = to {
|
|
773
|
-
index
|
|
774
|
-
} else if let Some(target) = &before {
|
|
775
|
-
let target_index = resolve(document, path, target).unwrap_or_else(|error| {
|
|
776
|
-
eprintln!("Error: {}", error);
|
|
777
|
-
process::exit(1);
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
if from_index < target_index {
|
|
781
|
-
target_index - 1
|
|
782
|
-
} else {
|
|
783
|
-
target_index
|
|
784
|
-
}
|
|
785
|
-
} else if let Some(target) = &after {
|
|
786
|
-
let target_index = resolve(document, path, target).unwrap_or_else(|error| {
|
|
787
|
-
eprintln!("Error: {}", error);
|
|
788
|
-
process::exit(1);
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
if from_index <= target_index {
|
|
792
|
-
target_index
|
|
793
|
-
} else {
|
|
794
|
-
target_index + 1
|
|
795
|
-
}
|
|
796
|
-
} else {
|
|
797
|
-
eprintln!("Error: specify --before, --after, or --to");
|
|
798
|
-
process::exit(1);
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
(from_index, to_index)
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
fn resolve_files(pattern: &str) -> Vec<String> {
|
|
805
|
-
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
|
|
806
|
-
let paths: Vec<String> = glob::glob(pattern)
|
|
807
|
-
.unwrap_or_else(|error| {
|
|
808
|
-
eprintln!("Invalid glob pattern '{}': {}", pattern, error);
|
|
809
|
-
process::exit(1);
|
|
810
|
-
})
|
|
811
|
-
.filter_map(|entry| entry.ok())
|
|
812
|
-
.map(|path| path.to_string_lossy().to_string())
|
|
813
|
-
.collect();
|
|
814
|
-
|
|
815
|
-
if paths.is_empty() {
|
|
816
|
-
eprintln!("No files matched pattern: {}", pattern);
|
|
817
|
-
process::exit(1);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
paths
|
|
821
|
-
} else {
|
|
822
|
-
vec![pattern.to_string()]
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
fn parse_file(file: &str) -> yerba::Document {
|
|
827
|
-
yerba::parse_file(file).unwrap_or_else(|error| {
|
|
828
|
-
match &error {
|
|
829
|
-
yerba::YerbaError::IoError(io_error) => match io_error.kind() {
|
|
830
|
-
std::io::ErrorKind::NotFound => eprintln!("Error: file not found: {}", file),
|
|
831
|
-
std::io::ErrorKind::PermissionDenied => {
|
|
832
|
-
eprintln!("Error: permission denied: {}", file)
|
|
833
|
-
}
|
|
834
|
-
_ => eprintln!("Error reading {}: {}", file, io_error),
|
|
835
|
-
},
|
|
836
|
-
_ => eprintln!("Error parsing {}: {}", file, error),
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
process::exit(1);
|
|
840
|
-
})
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
fn run(operation: impl FnOnce() -> Result<(), yerba::YerbaError>) {
|
|
844
|
-
operation().unwrap_or_else(|error| {
|
|
845
|
-
eprintln!("Error: {}", error);
|
|
846
|
-
process::exit(1);
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
fn output(file: &str, document: &yerba::Document, dry_run: bool) {
|
|
851
|
-
if dry_run {
|
|
852
|
-
println!("--- {}", file);
|
|
853
|
-
print!("{}", document);
|
|
854
|
-
} else {
|
|
855
|
-
fs::write(file, document.to_string()).unwrap_or_else(|error| {
|
|
856
|
-
eprintln!("Error writing {}: {}", file, error);
|
|
857
|
-
process::exit(1);
|
|
858
|
-
});
|
|
859
|
-
}
|
|
70
|
+
let cli = CLI::parse();
|
|
71
|
+
cli.command.run();
|
|
860
72
|
}
|