yerba 0.2.0-x86_64-linux-gnu
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +528 -0
- data/exe/yerba +6 -0
- data/ext/yerba/extconf.rb +111 -0
- data/ext/yerba/yerba.c +752 -0
- data/lib/yerba/3.2/yerba.so +0 -0
- data/lib/yerba/3.3/yerba.so +0 -0
- data/lib/yerba/3.4/yerba.so +0 -0
- data/lib/yerba/4.0/yerba.so +0 -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 +5 -0
- data/lib/yerba.rb +131 -0
- data/rust/Cargo.lock +805 -0
- data/rust/Cargo.toml +36 -0
- data/rust/build.rs +11 -0
- data/rust/cbindgen.toml +27 -0
- data/rust/rustfmt.toml +2 -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 +2237 -0
- data/rust/src/error.rs +45 -0
- data/rust/src/ffi.rs +991 -0
- data/rust/src/json.rs +128 -0
- data/rust/src/lib.rs +29 -0
- data/rust/src/main.rs +72 -0
- data/rust/src/quote_style.rs +42 -0
- data/rust/src/selector.rs +241 -0
- data/rust/src/syntax.rs +262 -0
- data/rust/src/yaml_writer.rs +89 -0
- data/rust/src/yerbafile.rs +475 -0
- data/sig/yerba.rbs +3 -0
- data/yerba.gemspec +52 -0
- metadata +108 -0
data/rust/Cargo.toml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "yerba"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
authors = ["Marco Roth <marco.roth@intergga.ch>"]
|
|
6
|
+
description = "YAML Editing and Refactoring with Better Accuracy"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
repository = "https://github.com/marcoroth/yerba"
|
|
10
|
+
keywords = ["yaml", "editing", "refactoring", "lossless", "cli"]
|
|
11
|
+
categories = ["parser-implementations", "development-tools"]
|
|
12
|
+
|
|
13
|
+
[lib]
|
|
14
|
+
name = "yerba"
|
|
15
|
+
path = "src/lib.rs"
|
|
16
|
+
crate-type = ["cdylib", "rlib"]
|
|
17
|
+
|
|
18
|
+
[[bin]]
|
|
19
|
+
name = "yerba"
|
|
20
|
+
path = "src/main.rs"
|
|
21
|
+
|
|
22
|
+
[dependencies]
|
|
23
|
+
yaml_parser = "0.3"
|
|
24
|
+
rowan = "0.16"
|
|
25
|
+
glob = "0.3"
|
|
26
|
+
serde = { version = "1", features = ["derive"] }
|
|
27
|
+
serde_yaml = "0.9"
|
|
28
|
+
serde_json = "1"
|
|
29
|
+
clap = { version = "4", features = ["derive"] }
|
|
30
|
+
indoc = "2"
|
|
31
|
+
rayon = "1"
|
|
32
|
+
|
|
33
|
+
[build-dependencies]
|
|
34
|
+
cbindgen = "0.28"
|
|
35
|
+
|
|
36
|
+
[dev-dependencies]
|
data/rust/build.rs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
use std::env;
|
|
2
|
+
use std::path::PathBuf;
|
|
3
|
+
|
|
4
|
+
fn main() {
|
|
5
|
+
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
6
|
+
let header_path = PathBuf::from(&crate_dir).join("../ext/yerba/include/yerba.h");
|
|
7
|
+
|
|
8
|
+
if let Ok(bindings) = cbindgen::generate(&crate_dir) {
|
|
9
|
+
bindings.write_to_file(&header_path);
|
|
10
|
+
}
|
|
11
|
+
}
|
data/rust/cbindgen.toml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
language = "C"
|
|
2
|
+
header = """/* Generated by cbindgen — do not edit manually */
|
|
3
|
+
|
|
4
|
+
#include <stdbool.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
#include <stddef.h>"""
|
|
7
|
+
include_guard = "YERBA_H"
|
|
8
|
+
no_includes = true
|
|
9
|
+
|
|
10
|
+
[export]
|
|
11
|
+
include = [
|
|
12
|
+
"YerbaValueType",
|
|
13
|
+
"YerbaResult",
|
|
14
|
+
"YerbaTypedValue",
|
|
15
|
+
"YerbaTypedList",
|
|
16
|
+
"YerbaGetResult",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[enum]
|
|
20
|
+
rename_variants = "ScreamingSnakeCase"
|
|
21
|
+
prefix_with_name = true
|
|
22
|
+
|
|
23
|
+
[fn]
|
|
24
|
+
sort_by = "None"
|
|
25
|
+
|
|
26
|
+
[parse]
|
|
27
|
+
parse_deps = false
|
data/rust/rustfmt.toml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
use std::sync::LazyLock;
|
|
2
|
+
|
|
3
|
+
use indoc::indoc;
|
|
4
|
+
|
|
5
|
+
use super::colorize_examples;
|
|
6
|
+
use super::{output, parse_file, resolve_files};
|
|
7
|
+
|
|
8
|
+
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
9
|
+
colorize_examples(indoc! {r#"
|
|
10
|
+
yerba blank-lines videos.yml 1
|
|
11
|
+
yerba blank-lines videos.yml "[]" 1
|
|
12
|
+
yerba blank-lines videos.yml "[].speakers" 1
|
|
13
|
+
yerba blank-lines config.yml "tags" 0
|
|
14
|
+
"#})
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
#[derive(clap::Args)]
|
|
18
|
+
#[command(
|
|
19
|
+
about = "Enforce blank lines between sequence entries",
|
|
20
|
+
arg_required_else_help = true,
|
|
21
|
+
after_help = EXAMPLES.as_str()
|
|
22
|
+
)]
|
|
23
|
+
pub struct Args {
|
|
24
|
+
file: String,
|
|
25
|
+
/// Selector or count (if a number, treated as count for root-level sequence)
|
|
26
|
+
first: String,
|
|
27
|
+
/// Count (when selector is provided as first positional)
|
|
28
|
+
second: Option<usize>,
|
|
29
|
+
#[arg(long)]
|
|
30
|
+
dry_run: bool,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl Args {
|
|
34
|
+
pub fn run(self) {
|
|
35
|
+
let (selector, count) = if let Some(count) = self.second {
|
|
36
|
+
(self.first.as_str(), count)
|
|
37
|
+
} else if let Ok(count) = self.first.parse::<usize>() {
|
|
38
|
+
("", count)
|
|
39
|
+
} else {
|
|
40
|
+
use super::color::*;
|
|
41
|
+
|
|
42
|
+
eprintln!(
|
|
43
|
+
"{RED}Error:{RESET} expected a number for blank line count, got '{}'",
|
|
44
|
+
self.first
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
std::process::exit(1);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for resolved_file in resolve_files(&self.file) {
|
|
51
|
+
let mut document = parse_file(&resolved_file);
|
|
52
|
+
|
|
53
|
+
if document.enforce_blank_lines(selector, count).is_ok() {
|
|
54
|
+
output(&resolved_file, &document, self.dry_run);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
use std::sync::LazyLock;
|
|
2
|
+
|
|
3
|
+
use indoc::indoc;
|
|
4
|
+
|
|
5
|
+
use super::colorize_examples;
|
|
6
|
+
use super::{output, parse_file, run_op};
|
|
7
|
+
|
|
8
|
+
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
9
|
+
colorize_examples(indoc! {r#"
|
|
10
|
+
yerba delete config.yml "database.pool"
|
|
11
|
+
yerba delete videos.yml "[0].description"
|
|
12
|
+
yerba delete config.yml "database.pool" --dry-run
|
|
13
|
+
"#})
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
#[derive(clap::Args)]
|
|
17
|
+
#[command(
|
|
18
|
+
about = "Delete a key and its value from a map",
|
|
19
|
+
arg_required_else_help = true,
|
|
20
|
+
after_help = EXAMPLES.as_str()
|
|
21
|
+
)]
|
|
22
|
+
pub struct Args {
|
|
23
|
+
file: String,
|
|
24
|
+
selector: String,
|
|
25
|
+
#[arg(long)]
|
|
26
|
+
dry_run: bool,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Args {
|
|
30
|
+
pub fn run(self) {
|
|
31
|
+
let mut document = parse_file(&self.file);
|
|
32
|
+
run_op(|| document.delete(&self.selector));
|
|
33
|
+
output(&self.file, &document, self.dry_run);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
use std::process;
|
|
2
|
+
use std::sync::LazyLock;
|
|
3
|
+
|
|
4
|
+
use indoc::indoc;
|
|
5
|
+
|
|
6
|
+
use super::{colorize_examples, parse_file, resolve_files};
|
|
7
|
+
|
|
8
|
+
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
9
|
+
colorize_examples(indoc! {r#"
|
|
10
|
+
yerba get config.yml "database.host"
|
|
11
|
+
yerba get videos.yml "[].title"
|
|
12
|
+
yerba get videos.yml "[0].title"
|
|
13
|
+
yerba get "data/**/videos.yml" "[].speakers[].name"
|
|
14
|
+
yerba get videos.yml "[]" --select ".title,.speakers"
|
|
15
|
+
yerba get videos.yml "[]" --condition ".kind == keynote"
|
|
16
|
+
yerba get videos.yml "[]" --select ".title" --condition ".kind == keynote"
|
|
17
|
+
yerba get videos.yml "[].speakers[]" --condition "[].video_provider == youtube"
|
|
18
|
+
yerba get videos.yml "[]" --condition ".speakers contains Matz" --raw
|
|
19
|
+
"#})
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
#[derive(clap::Args)]
|
|
23
|
+
#[command(
|
|
24
|
+
about = "Get values, filter items, and select fields from YAML files",
|
|
25
|
+
arg_required_else_help = true,
|
|
26
|
+
after_help = EXAMPLES.as_str()
|
|
27
|
+
)]
|
|
28
|
+
pub struct Args {
|
|
29
|
+
file: String,
|
|
30
|
+
selector: String,
|
|
31
|
+
/// Filter items by condition (e.g. '.kind == keynote', '[].video_provider == youtube')
|
|
32
|
+
#[arg(long)]
|
|
33
|
+
condition: Option<String>,
|
|
34
|
+
/// Comma-separated fields to include (e.g. '.title,.speakers')
|
|
35
|
+
#[arg(long)]
|
|
36
|
+
select: Option<String>,
|
|
37
|
+
/// Output raw values (one per line) instead of JSON
|
|
38
|
+
#[arg(long)]
|
|
39
|
+
raw: bool,
|
|
40
|
+
#[arg(long, hide = true)]
|
|
41
|
+
json: bool,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl Args {
|
|
45
|
+
pub fn run(self) {
|
|
46
|
+
let selector = yerba::Selector::parse(&self.selector);
|
|
47
|
+
let condition_path = self.condition.as_deref().map(yerba::Selector::parse);
|
|
48
|
+
let select_fields: Option<Vec<&str>> = self.select.as_deref().map(|fields| fields.split(',').collect());
|
|
49
|
+
let (search_path, extract_field) = self.resolve_search_scope(&selector, condition_path.as_ref());
|
|
50
|
+
|
|
51
|
+
let normalized_condition = self.condition.as_ref().map(|condition| {
|
|
52
|
+
if !condition.starts_with('.') {
|
|
53
|
+
if let Some(bracket_end) = condition.find(']') {
|
|
54
|
+
let after_bracket = &condition[bracket_end + 1..];
|
|
55
|
+
|
|
56
|
+
if after_bracket.starts_with('.') {
|
|
57
|
+
return after_bracket.to_string();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
condition.clone()
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let search_path_string = search_path.to_selector_string();
|
|
66
|
+
let mut all_results: Vec<serde_json::Value> = Vec::new();
|
|
67
|
+
|
|
68
|
+
for resolved_file in resolve_files(&self.file) {
|
|
69
|
+
let document = parse_file(&resolved_file);
|
|
70
|
+
|
|
71
|
+
let values: Vec<serde_yaml::Value> = if let Some(condition) = &normalized_condition {
|
|
72
|
+
document.filter(&search_path_string, condition)
|
|
73
|
+
} else {
|
|
74
|
+
document.get_values(&search_path_string)
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if values.is_empty()
|
|
78
|
+
&& !selector.has_brackets()
|
|
79
|
+
&& normalized_condition.is_none()
|
|
80
|
+
&& !document.exists(&self.selector)
|
|
81
|
+
{
|
|
82
|
+
use super::color::*;
|
|
83
|
+
eprintln!("{RED}Error:{RESET} path not found: {}", self.selector);
|
|
84
|
+
process::exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for value in values {
|
|
88
|
+
if let Some(field) = &extract_field {
|
|
89
|
+
let field_string = field.to_selector_string();
|
|
90
|
+
let json_value = yerba::json::resolve_select_field(&value, &field_string);
|
|
91
|
+
|
|
92
|
+
if field.ends_with_bracket() {
|
|
93
|
+
if let serde_json::Value::Array(items) = json_value {
|
|
94
|
+
all_results.extend(items);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
all_results.push(json_value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if let Some(fields) = &select_fields {
|
|
104
|
+
let mut result = serde_json::Map::new();
|
|
105
|
+
|
|
106
|
+
result.insert("__file".to_string(), serde_json::Value::String(resolved_file.clone()));
|
|
107
|
+
|
|
108
|
+
for field in fields {
|
|
109
|
+
let json_value = yerba::json::resolve_select_field(&value, field);
|
|
110
|
+
let json_key = yerba::json::select_field_key(field);
|
|
111
|
+
|
|
112
|
+
result.insert(json_key, json_value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
all_results.push(serde_json::Value::Object(result));
|
|
116
|
+
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
all_results.push(yerba::json::yaml_to_json(&value));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if self.raw {
|
|
125
|
+
for value in &all_results {
|
|
126
|
+
match value {
|
|
127
|
+
serde_json::Value::String(string) => println!("{}", string),
|
|
128
|
+
serde_json::Value::Null => println!("null"),
|
|
129
|
+
serde_json::Value::Bool(boolean) => println!("{}", boolean),
|
|
130
|
+
serde_json::Value::Number(number) => println!("{}", number),
|
|
131
|
+
_ => println!("{}", serde_json::to_string(value).unwrap_or_default()),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else if all_results.len() == 1 {
|
|
135
|
+
println!(
|
|
136
|
+
"{}",
|
|
137
|
+
serde_json::to_string_pretty(&all_results[0]).unwrap_or_else(|_| "null".to_string())
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
println!(
|
|
141
|
+
"{}",
|
|
142
|
+
serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string())
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn resolve_search_scope(
|
|
148
|
+
&self,
|
|
149
|
+
selector: &yerba::Selector,
|
|
150
|
+
condition_path: Option<&yerba::Selector>,
|
|
151
|
+
) -> (yerba::Selector, Option<yerba::Selector>) {
|
|
152
|
+
if let Some(condition) = condition_path {
|
|
153
|
+
if condition.is_relative() {
|
|
154
|
+
let (container, field) = selector.split_at_last_bracket();
|
|
155
|
+
|
|
156
|
+
if container.is_empty() {
|
|
157
|
+
return (selector.clone(), None);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let extract = if field.is_empty() { None } else { Some(field) };
|
|
161
|
+
|
|
162
|
+
return (container, extract);
|
|
163
|
+
} else {
|
|
164
|
+
let (container, _) = condition.split_at_first_bracket();
|
|
165
|
+
let container_string = container.to_selector_string();
|
|
166
|
+
let selector_string = selector.to_selector_string();
|
|
167
|
+
|
|
168
|
+
if selector_string.starts_with(&container_string) {
|
|
169
|
+
let rest = selector_string[container_string.len()..].trim_start_matches('.');
|
|
170
|
+
|
|
171
|
+
if rest.is_empty() {
|
|
172
|
+
return (container, None);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return (container, Some(yerba::Selector::parse(&format!(".{}", rest))));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (container, None);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if selector.has_brackets() && !selector.ends_with_bracket() {
|
|
183
|
+
let (container, field) = selector.split_at_last_bracket();
|
|
184
|
+
|
|
185
|
+
if !container.is_empty() {
|
|
186
|
+
let extract = if field.is_empty() { None } else { Some(field) };
|
|
187
|
+
|
|
188
|
+
return (container, extract);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
(selector.clone(), None)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
use std::process;
|
|
3
|
+
|
|
4
|
+
use indoc::indoc;
|
|
5
|
+
|
|
6
|
+
use super::color::*;
|
|
7
|
+
|
|
8
|
+
pub fn run() {
|
|
9
|
+
let filename = "Yerbafile";
|
|
10
|
+
|
|
11
|
+
if Path::new(filename).exists() {
|
|
12
|
+
eprintln!("🧉 {YELLOW}{BOLD}Yerbafile already exists.{RESET}");
|
|
13
|
+
process::exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let content = indoc! {r#"
|
|
17
|
+
# rules:
|
|
18
|
+
# - files: "**/*.yml"
|
|
19
|
+
# pipeline:
|
|
20
|
+
#
|
|
21
|
+
# # Enforce quote style on values and keys
|
|
22
|
+
# - quote_style:
|
|
23
|
+
# key_style: plain # plain, single, double
|
|
24
|
+
# value_style: double # plain, single, double
|
|
25
|
+
#
|
|
26
|
+
# # Override quote style for a specific selector
|
|
27
|
+
# - quote_style:
|
|
28
|
+
# path: "[].speakers"
|
|
29
|
+
# value_style: plain
|
|
30
|
+
#
|
|
31
|
+
# # Sort keys in a predefined order (aborts on unknown keys)
|
|
32
|
+
# - sort_keys:
|
|
33
|
+
# path: "[]"
|
|
34
|
+
# order:
|
|
35
|
+
# - id
|
|
36
|
+
# - title
|
|
37
|
+
# - description
|
|
38
|
+
#
|
|
39
|
+
# # Sort items in a sequence by field(s)
|
|
40
|
+
# - sort:
|
|
41
|
+
# path: ""
|
|
42
|
+
# by: name
|
|
43
|
+
# # case_sensitive: false
|
|
44
|
+
#
|
|
45
|
+
# # Enforce blank lines between sequence entries
|
|
46
|
+
# - blank_lines:
|
|
47
|
+
# count: 1
|
|
48
|
+
#
|
|
49
|
+
# # Set a value (with optional condition)
|
|
50
|
+
# - set:
|
|
51
|
+
# path: "status"
|
|
52
|
+
# value: "published"
|
|
53
|
+
# # condition: ".draft == true"
|
|
54
|
+
#
|
|
55
|
+
# # Insert a new key or sequence item
|
|
56
|
+
# - insert:
|
|
57
|
+
# path: "tags"
|
|
58
|
+
# value: "yaml"
|
|
59
|
+
# # condition: ".tags not_contains yaml"
|
|
60
|
+
#
|
|
61
|
+
# # Delete a key (with optional condition)
|
|
62
|
+
# - delete:
|
|
63
|
+
# path: "deprecated_field"
|
|
64
|
+
# # condition: ".deprecated_field == true"
|
|
65
|
+
#
|
|
66
|
+
# # Rename a key
|
|
67
|
+
# - rename:
|
|
68
|
+
# from: "old_name"
|
|
69
|
+
# to: "new_name"
|
|
70
|
+
#
|
|
71
|
+
# # Remove an item from a sequence
|
|
72
|
+
# - remove:
|
|
73
|
+
# path: "tags"
|
|
74
|
+
# value: "obsolete"
|
|
75
|
+
#
|
|
76
|
+
# # Get a value and store it as a variable
|
|
77
|
+
# - get:
|
|
78
|
+
# path: "[].name"
|
|
79
|
+
# as: "names"
|
|
80
|
+
# # file: "other_file.yml"
|
|
81
|
+
"#};
|
|
82
|
+
|
|
83
|
+
std::fs::write(filename, content).unwrap_or_else(|error| {
|
|
84
|
+
eprintln!("{RED}Error:{RESET} writing Yerbafile: {}", error);
|
|
85
|
+
process::exit(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
eprintln!("🧉 {GREEN}{BOLD}Created Yerbafile{RESET}");
|
|
89
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
use std::sync::LazyLock;
|
|
2
|
+
|
|
3
|
+
use indoc::indoc;
|
|
4
|
+
|
|
5
|
+
use super::colorize_examples;
|
|
6
|
+
use super::{output, parse_file, run_op};
|
|
7
|
+
|
|
8
|
+
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
9
|
+
colorize_examples(indoc! {r#"
|
|
10
|
+
yerba insert config.yml "database.ssl" true
|
|
11
|
+
yerba insert config.yml "database.ssl" true --after "host"
|
|
12
|
+
yerba insert config.yml "database.ssl" true --before "port"
|
|
13
|
+
yerba insert config.yml "tags" "yaml"
|
|
14
|
+
yerba insert config.yml "tags" "yaml" --at 0
|
|
15
|
+
yerba insert config.yml "tags" "yaml" --after "ruby"
|
|
16
|
+
yerba insert speakers.yml "" "name: Bob" --after ".name == Alice"
|
|
17
|
+
yerba insert videos.yml "[0].speakers" "Diana" --before ".name == Charlie"
|
|
18
|
+
yerba insert videos.yml "" --from "new_talk.yml" --after ".id == first-talk"
|
|
19
|
+
"#})
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
#[derive(clap::Args)]
|
|
23
|
+
#[command(
|
|
24
|
+
about = "Insert a new key into a map or item into a sequence",
|
|
25
|
+
arg_required_else_help = true,
|
|
26
|
+
after_help = EXAMPLES.as_str()
|
|
27
|
+
)]
|
|
28
|
+
pub struct Args {
|
|
29
|
+
file: String,
|
|
30
|
+
selector: String,
|
|
31
|
+
value: Option<String>,
|
|
32
|
+
#[arg(long, help = "Read value from a file (use - for stdin)")]
|
|
33
|
+
from: Option<String>,
|
|
34
|
+
#[arg(long, help = "Insert before this value or condition (e.g. \".name == Alice\")")]
|
|
35
|
+
before: Option<String>,
|
|
36
|
+
#[arg(long, help = "Insert after this value or condition (e.g. \".name == Alice\")")]
|
|
37
|
+
after: Option<String>,
|
|
38
|
+
#[arg(long)]
|
|
39
|
+
at: Option<usize>,
|
|
40
|
+
#[arg(long)]
|
|
41
|
+
dry_run: bool,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl Args {
|
|
45
|
+
pub fn run(self) {
|
|
46
|
+
let resolved_value = if let Some(from_path) = self.from {
|
|
47
|
+
if from_path == "-" {
|
|
48
|
+
use std::io::Read;
|
|
49
|
+
let mut buffer = String::new();
|
|
50
|
+
|
|
51
|
+
std::io::stdin().read_to_string(&mut buffer).unwrap_or_else(|error| {
|
|
52
|
+
use super::color::*;
|
|
53
|
+
eprintln!("{RED}Error:{RESET} reading stdin: {}", error);
|
|
54
|
+
|
|
55
|
+
std::process::exit(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
buffer.trim().to_string()
|
|
59
|
+
} else {
|
|
60
|
+
std::fs::read_to_string(&from_path)
|
|
61
|
+
.unwrap_or_else(|error| {
|
|
62
|
+
use super::color::*;
|
|
63
|
+
eprintln!("{RED}Error:{RESET} reading {}: {}", from_path, error);
|
|
64
|
+
std::process::exit(1);
|
|
65
|
+
})
|
|
66
|
+
.trim()
|
|
67
|
+
.to_string()
|
|
68
|
+
}
|
|
69
|
+
} else if let Some(val) = self.value {
|
|
70
|
+
val
|
|
71
|
+
} else {
|
|
72
|
+
use super::color::*;
|
|
73
|
+
eprintln!("{RED}Error:{RESET} either a value argument or --from is required");
|
|
74
|
+
|
|
75
|
+
std::process::exit(1);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
let parent_path = self.selector.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
79
|
+
|
|
80
|
+
let position = if let Some(index) = self.at {
|
|
81
|
+
yerba::InsertPosition::At(index)
|
|
82
|
+
} else if let Some(target) = self.before {
|
|
83
|
+
if target.starts_with('.') {
|
|
84
|
+
yerba::InsertPosition::BeforeCondition(target)
|
|
85
|
+
} else {
|
|
86
|
+
yerba::InsertPosition::Before(target)
|
|
87
|
+
}
|
|
88
|
+
} else if let Some(target) = self.after {
|
|
89
|
+
if target.starts_with('.') {
|
|
90
|
+
yerba::InsertPosition::AfterCondition(target)
|
|
91
|
+
} else {
|
|
92
|
+
yerba::InsertPosition::After(target)
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
yerba::Yerbafile::find()
|
|
96
|
+
.and_then(|yerbafile_path| yerba::Yerbafile::load(&yerbafile_path).ok())
|
|
97
|
+
.and_then(|yerbafile| yerbafile.sort_order_for(&self.file, parent_path))
|
|
98
|
+
.map(yerba::InsertPosition::FromSortOrder)
|
|
99
|
+
.unwrap_or(yerba::InsertPosition::Last)
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let mut document = parse_file(&self.file);
|
|
103
|
+
run_op(|| document.insert_into(&self.selector, &resolved_value, position));
|
|
104
|
+
output(&self.file, &document, self.dry_run);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
use super::color::*;
|
|
2
|
+
|
|
3
|
+
pub fn run() {
|
|
4
|
+
let g = GREEN;
|
|
5
|
+
let b = BOLD;
|
|
6
|
+
let d = DIM;
|
|
7
|
+
let i = "\x1b[3m"; // italic
|
|
8
|
+
let y = YELLOW;
|
|
9
|
+
let r = RESET;
|
|
10
|
+
let hr = format!(" {d}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}{r}");
|
|
11
|
+
|
|
12
|
+
println!();
|
|
13
|
+
println!(" {b}{g}\u{1f9c9} YERBA MATE{r}");
|
|
14
|
+
println!();
|
|
15
|
+
println!(" {d}From the Guarani people of South America to the world.{r}");
|
|
16
|
+
println!(" {d}Brewed from the leaves of{r} {i}Ilex paraguariensis{r}{d}, shared{r}");
|
|
17
|
+
println!(" {d}in a hollowed calabaza, sipped through a metal bombilla.{r}");
|
|
18
|
+
println!(" {d}One cup, many rounds.{r}");
|
|
19
|
+
println!();
|
|
20
|
+
println!("{hr}");
|
|
21
|
+
println!();
|
|
22
|
+
println!(" {b}{g}yerba{r} {d}/\u{02c8}\u{0292}\u{025b}\u{027e}.ba/{r} {y}noun{r}");
|
|
23
|
+
println!(" The dried leaves of {i}Ilex paraguariensis{r}, used to brew");
|
|
24
|
+
println!(" mate. From Guarani {i}ka'a{r}, meaning \"herb\".");
|
|
25
|
+
println!();
|
|
26
|
+
println!(" {b}{g}mate{r} {d}/\u{02c8}ma.te/{r} {y}noun{r}");
|
|
27
|
+
println!(" A traditional South American caffeine-rich infusion.");
|
|
28
|
+
println!(" Also the hollowed calabaza (gourd) from which it is drunk.");
|
|
29
|
+
println!();
|
|
30
|
+
println!(" {b}{g}bombilla{r} {d}/bom\u{02c8}bi.\u{0292}a/{r} {y}noun{r}");
|
|
31
|
+
println!(" A metal straw with a filtered tip at the bottom, used");
|
|
32
|
+
println!(" to sip mate without swallowing the leaves.");
|
|
33
|
+
println!();
|
|
34
|
+
println!(" {b}{g}cebador{r} {d}/se.ba\u{02c8}\u{00f0}o\u{027e}/{r} {y}noun{r}");
|
|
35
|
+
println!(" The person who prepares and serves mate to the group.");
|
|
36
|
+
println!(" A role of care, not hierarchy.");
|
|
37
|
+
println!();
|
|
38
|
+
println!(" {b}{g}ronda{r} {d}/\u{02c8}ron.da/{r} {y}noun{r}");
|
|
39
|
+
println!(" The circle of people sharing mate. The cup passes");
|
|
40
|
+
println!(" from hand to hand until it returns to the cebador.");
|
|
41
|
+
println!();
|
|
42
|
+
println!(" {b}{g}cebar{r} {d}/se\u{02c8}ba\u{027e}/{r} {y}verb{r}");
|
|
43
|
+
println!(" To pour hot water over the yerba and serve a round.");
|
|
44
|
+
println!(" The act of preparing each individual serving.");
|
|
45
|
+
println!();
|
|
46
|
+
println!(" {b}{g}aprontar{r} {d}/ap\u{027e}on\u{02c8}ta\u{027e}/{r} {y}verb{r}");
|
|
47
|
+
println!(" To set up mate before the first pour: arranging the");
|
|
48
|
+
println!(" yerba, heating the water, positioning the bombilla.");
|
|
49
|
+
println!();
|
|
50
|
+
println!(" {b}{g}ensillar{r} {d}/ensi\u{02c8}\u{0292}a\u{027e}/{r} {y}verb{r}");
|
|
51
|
+
println!(" To replace spent yerba with fresh leaves mid-session,");
|
|
52
|
+
println!(" extending the life of the mate.");
|
|
53
|
+
println!();
|
|
54
|
+
println!("{hr}");
|
|
55
|
+
}
|