just-bash 0.1.5__py3-none-any.whl
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.
- just_bash/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- just_bash-0.1.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
"""Yq command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: yq [OPTIONS] [FILTER] [FILE]
|
|
4
|
+
|
|
5
|
+
Command-line YAML/XML/INI/CSV/TOML processor.
|
|
6
|
+
Uses jq-style expressions to query and transform data.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-p, --input-format=FMT input format: yaml (default), xml, json, ini, csv, toml
|
|
10
|
+
-o, --output-format=FMT output format: yaml (default), json, xml, ini, csv, toml
|
|
11
|
+
-i, --inplace modify file in-place
|
|
12
|
+
-r, --raw-output output strings without quotes (json only)
|
|
13
|
+
-c, --compact compact output (json only)
|
|
14
|
+
-e, --exit-status set exit status based on output
|
|
15
|
+
-s, --slurp read entire input into array
|
|
16
|
+
-n, --null-input don't read any input
|
|
17
|
+
-j, --join-output don't print newlines after each output
|
|
18
|
+
--help display this help and exit
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import csv
|
|
24
|
+
import io
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any
|
|
27
|
+
from xml.etree import ElementTree as ET
|
|
28
|
+
from configparser import ConfigParser
|
|
29
|
+
|
|
30
|
+
from ...types import CommandContext, ExecResult
|
|
31
|
+
from ...query_engine import parse, evaluate, EvalContext
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Input formats supported
|
|
35
|
+
VALID_INPUT_FORMATS = {"yaml", "json", "xml", "ini", "csv", "toml"}
|
|
36
|
+
VALID_OUTPUT_FORMATS = {"yaml", "json", "xml", "ini", "csv", "toml"}
|
|
37
|
+
|
|
38
|
+
# File extension to format mapping
|
|
39
|
+
EXTENSION_FORMAT_MAP = {
|
|
40
|
+
".yaml": "yaml",
|
|
41
|
+
".yml": "yaml",
|
|
42
|
+
".json": "json",
|
|
43
|
+
".xml": "xml",
|
|
44
|
+
".ini": "ini",
|
|
45
|
+
".csv": "csv",
|
|
46
|
+
".tsv": "csv",
|
|
47
|
+
".toml": "toml",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class YqOptions:
|
|
53
|
+
"""Parsed yq options."""
|
|
54
|
+
input_format: str = "yaml"
|
|
55
|
+
output_format: str = "yaml"
|
|
56
|
+
raw: bool = False
|
|
57
|
+
compact: bool = False
|
|
58
|
+
exit_status: bool = False
|
|
59
|
+
slurp: bool = False
|
|
60
|
+
null_input: bool = False
|
|
61
|
+
join_output: bool = False
|
|
62
|
+
inplace: bool = False
|
|
63
|
+
front_matter: bool = False
|
|
64
|
+
indent: int = 2
|
|
65
|
+
xml_attribute_prefix: str = "+@"
|
|
66
|
+
xml_content_name: str = "+content"
|
|
67
|
+
csv_delimiter: str = "" # Empty means auto-detect
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def detect_format_from_extension(filename: str) -> str | None:
|
|
71
|
+
"""Detect format from file extension."""
|
|
72
|
+
for ext, fmt in EXTENSION_FORMAT_MAP.items():
|
|
73
|
+
if filename.lower().endswith(ext):
|
|
74
|
+
return fmt
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_front_matter(content: str) -> str | None:
|
|
79
|
+
"""Extract YAML front matter from markdown content.
|
|
80
|
+
|
|
81
|
+
Front matter is YAML content between --- markers at the start of a file.
|
|
82
|
+
Returns the YAML content without the markers, or None if no front matter found.
|
|
83
|
+
"""
|
|
84
|
+
content = content.lstrip()
|
|
85
|
+
|
|
86
|
+
# Must start with ---
|
|
87
|
+
if not content.startswith("---"):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# Find the closing ---
|
|
91
|
+
# Start searching after the opening ---
|
|
92
|
+
rest = content[3:]
|
|
93
|
+
|
|
94
|
+
# Skip the newline after opening ---
|
|
95
|
+
if rest.startswith("\n"):
|
|
96
|
+
rest = rest[1:]
|
|
97
|
+
elif rest.startswith("\r\n"):
|
|
98
|
+
rest = rest[2:]
|
|
99
|
+
|
|
100
|
+
# Find the closing --- on its own line
|
|
101
|
+
lines = rest.split("\n")
|
|
102
|
+
front_matter_lines = []
|
|
103
|
+
|
|
104
|
+
for i, line in enumerate(lines):
|
|
105
|
+
stripped = line.strip()
|
|
106
|
+
if stripped == "---":
|
|
107
|
+
# Found the closing marker
|
|
108
|
+
return "\n".join(front_matter_lines)
|
|
109
|
+
front_matter_lines.append(line)
|
|
110
|
+
|
|
111
|
+
# No closing marker found
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def parse_yaml(content: str) -> Any:
|
|
116
|
+
"""Parse YAML content (simplified parser)."""
|
|
117
|
+
# This is a simplified YAML parser that handles common cases
|
|
118
|
+
content = content.strip()
|
|
119
|
+
if not content:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# Check for JSON-style content (array or object)
|
|
123
|
+
if content.startswith("{") or content.startswith("["):
|
|
124
|
+
return json.loads(content)
|
|
125
|
+
|
|
126
|
+
lines = content.split("\n")
|
|
127
|
+
return _parse_yaml_lines(lines, 0, 0)[0]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _parse_yaml_lines(lines: list[str], start: int, base_indent: int) -> tuple[Any, int]:
|
|
131
|
+
"""Parse YAML lines recursively."""
|
|
132
|
+
if start >= len(lines):
|
|
133
|
+
return None, start
|
|
134
|
+
|
|
135
|
+
result = {}
|
|
136
|
+
is_list = False
|
|
137
|
+
list_result = []
|
|
138
|
+
i = start
|
|
139
|
+
|
|
140
|
+
while i < len(lines):
|
|
141
|
+
line = lines[i]
|
|
142
|
+
|
|
143
|
+
# Skip empty lines and comments
|
|
144
|
+
if not line.strip() or line.strip().startswith("#"):
|
|
145
|
+
i += 1
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Calculate indentation
|
|
149
|
+
stripped = line.lstrip()
|
|
150
|
+
indent = len(line) - len(stripped)
|
|
151
|
+
|
|
152
|
+
# If less indented than base, we're done with this block
|
|
153
|
+
if indent < base_indent and i > start:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
# List item
|
|
157
|
+
if stripped.startswith("- "):
|
|
158
|
+
is_list = True
|
|
159
|
+
item_content = stripped[2:].strip()
|
|
160
|
+
|
|
161
|
+
# Check if it's a key: value on same line
|
|
162
|
+
if ":" in item_content and not item_content.startswith('"'):
|
|
163
|
+
# Parse as inline object
|
|
164
|
+
colon_idx = item_content.find(":")
|
|
165
|
+
key = item_content[:colon_idx].strip()
|
|
166
|
+
value = item_content[colon_idx + 1:].strip()
|
|
167
|
+
|
|
168
|
+
if value:
|
|
169
|
+
list_result.append({key: _parse_yaml_value(value)})
|
|
170
|
+
else:
|
|
171
|
+
# Nested object
|
|
172
|
+
nested, i = _parse_yaml_lines(lines, i + 1, indent + 2)
|
|
173
|
+
list_result.append({key: nested})
|
|
174
|
+
continue
|
|
175
|
+
else:
|
|
176
|
+
list_result.append(_parse_yaml_value(item_content))
|
|
177
|
+
i += 1
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Key: value
|
|
181
|
+
if ":" in stripped and not stripped.startswith('"'):
|
|
182
|
+
colon_idx = stripped.find(":")
|
|
183
|
+
key = stripped[:colon_idx].strip()
|
|
184
|
+
value = stripped[colon_idx + 1:].strip()
|
|
185
|
+
|
|
186
|
+
if not value:
|
|
187
|
+
# Nested structure
|
|
188
|
+
nested, i = _parse_yaml_lines(lines, i + 1, indent + 2)
|
|
189
|
+
result[key] = nested
|
|
190
|
+
continue
|
|
191
|
+
else:
|
|
192
|
+
result[key] = _parse_yaml_value(value)
|
|
193
|
+
i += 1
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
i += 1
|
|
197
|
+
|
|
198
|
+
if is_list:
|
|
199
|
+
return list_result, i
|
|
200
|
+
return result if result else None, i
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _parse_yaml_value(value: str) -> Any:
|
|
204
|
+
"""Parse a YAML value."""
|
|
205
|
+
if not value:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
# Quoted string
|
|
209
|
+
if (value.startswith('"') and value.endswith('"')) or \
|
|
210
|
+
(value.startswith("'") and value.endswith("'")):
|
|
211
|
+
return value[1:-1]
|
|
212
|
+
|
|
213
|
+
# Null
|
|
214
|
+
if value.lower() in ("null", "~", ""):
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# Boolean
|
|
218
|
+
if value.lower() in ("true", "yes", "on"):
|
|
219
|
+
return True
|
|
220
|
+
if value.lower() in ("false", "no", "off"):
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
# Number
|
|
224
|
+
try:
|
|
225
|
+
if "." in value:
|
|
226
|
+
return float(value)
|
|
227
|
+
return int(value)
|
|
228
|
+
except ValueError:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# Array (inline)
|
|
232
|
+
if value.startswith("[") and value.endswith("]"):
|
|
233
|
+
try:
|
|
234
|
+
return json.loads(value)
|
|
235
|
+
except json.JSONDecodeError:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
# Object (inline)
|
|
239
|
+
if value.startswith("{") and value.endswith("}"):
|
|
240
|
+
try:
|
|
241
|
+
return json.loads(value)
|
|
242
|
+
except json.JSONDecodeError:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
return value
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def format_yaml(value: Any, indent: int = 2) -> str:
|
|
249
|
+
"""Format a value as YAML."""
|
|
250
|
+
return _format_yaml_value(value, 0, indent)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _format_yaml_value(value: Any, level: int, indent: int) -> str:
|
|
254
|
+
"""Format a YAML value recursively."""
|
|
255
|
+
prefix = " " * (level * indent)
|
|
256
|
+
|
|
257
|
+
if value is None:
|
|
258
|
+
return "null"
|
|
259
|
+
elif isinstance(value, bool):
|
|
260
|
+
return "true" if value else "false"
|
|
261
|
+
elif isinstance(value, (int, float)):
|
|
262
|
+
return str(value)
|
|
263
|
+
elif isinstance(value, str):
|
|
264
|
+
# Check if needs quoting
|
|
265
|
+
if any(c in value for c in [":", "#", "[", "]", "{", "}", "&", "*", "!", "|", ">", "'", '"', "%", "@", "`"]) or \
|
|
266
|
+
value.lower() in ("true", "false", "yes", "no", "null", "on", "off") or \
|
|
267
|
+
not value or value.isspace() or "\n" in value:
|
|
268
|
+
return json.dumps(value)
|
|
269
|
+
return value
|
|
270
|
+
elif isinstance(value, list):
|
|
271
|
+
if not value:
|
|
272
|
+
return "[]"
|
|
273
|
+
lines = []
|
|
274
|
+
for item in value:
|
|
275
|
+
item_str = _format_yaml_value(item, level + 1, indent)
|
|
276
|
+
if isinstance(item, (dict, list)) and item:
|
|
277
|
+
lines.append(f"{prefix}- {item_str.lstrip()}")
|
|
278
|
+
else:
|
|
279
|
+
lines.append(f"{prefix}- {item_str}")
|
|
280
|
+
return "\n".join(lines)
|
|
281
|
+
elif isinstance(value, dict):
|
|
282
|
+
if not value:
|
|
283
|
+
return "{}"
|
|
284
|
+
lines = []
|
|
285
|
+
for k, v in value.items():
|
|
286
|
+
v_str = _format_yaml_value(v, level + 1, indent)
|
|
287
|
+
if isinstance(v, (dict, list)) and v:
|
|
288
|
+
lines.append(f"{prefix}{k}:\n{v_str}")
|
|
289
|
+
else:
|
|
290
|
+
lines.append(f"{prefix}{k}: {v_str}")
|
|
291
|
+
return "\n".join(lines)
|
|
292
|
+
else:
|
|
293
|
+
return str(value)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def parse_xml(content: str, attr_prefix: str = "+@", content_name: str = "+content") -> Any:
|
|
297
|
+
"""Parse XML content into a dict structure."""
|
|
298
|
+
try:
|
|
299
|
+
root = ET.fromstring(content)
|
|
300
|
+
return {root.tag: _xml_element_to_dict(root, attr_prefix, content_name)}
|
|
301
|
+
except ET.ParseError as e:
|
|
302
|
+
raise ValueError(f"XML parse error: {e}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _xml_element_to_dict(elem: ET.Element, attr_prefix: str, content_name: str) -> Any:
|
|
306
|
+
"""Convert an XML element to a dict."""
|
|
307
|
+
result = {}
|
|
308
|
+
|
|
309
|
+
# Add attributes with prefix
|
|
310
|
+
for attr, value in elem.attrib.items():
|
|
311
|
+
result[f"{attr_prefix}{attr}"] = value
|
|
312
|
+
|
|
313
|
+
# Add children
|
|
314
|
+
children = list(elem)
|
|
315
|
+
if children:
|
|
316
|
+
for child in children:
|
|
317
|
+
child_dict = _xml_element_to_dict(child, attr_prefix, content_name)
|
|
318
|
+
if child.tag in result:
|
|
319
|
+
# Multiple children with same tag - convert to list
|
|
320
|
+
existing = result[child.tag]
|
|
321
|
+
if not isinstance(existing, list):
|
|
322
|
+
result[child.tag] = [existing]
|
|
323
|
+
result[child.tag].append(child_dict)
|
|
324
|
+
else:
|
|
325
|
+
result[child.tag] = child_dict
|
|
326
|
+
|
|
327
|
+
# Add text content
|
|
328
|
+
text = (elem.text or "").strip()
|
|
329
|
+
if text:
|
|
330
|
+
if result:
|
|
331
|
+
result[content_name] = text
|
|
332
|
+
else:
|
|
333
|
+
return text
|
|
334
|
+
|
|
335
|
+
return result if result else None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def format_xml(value: Any, root_name: str = "root") -> str:
|
|
339
|
+
"""Format a value as XML."""
|
|
340
|
+
if isinstance(value, dict) and len(value) == 1:
|
|
341
|
+
root_name = list(value.keys())[0]
|
|
342
|
+
value = value[root_name]
|
|
343
|
+
|
|
344
|
+
root = ET.Element(root_name)
|
|
345
|
+
_dict_to_xml_element(value, root)
|
|
346
|
+
return ET.tostring(root, encoding="unicode")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _dict_to_xml_element(value: Any, elem: ET.Element) -> None:
|
|
350
|
+
"""Convert a dict/value to XML element."""
|
|
351
|
+
if isinstance(value, dict):
|
|
352
|
+
for k, v in value.items():
|
|
353
|
+
if k.startswith("+@"):
|
|
354
|
+
# Attribute
|
|
355
|
+
elem.set(k[2:], str(v))
|
|
356
|
+
elif k == "+content":
|
|
357
|
+
elem.text = str(v)
|
|
358
|
+
elif isinstance(v, list):
|
|
359
|
+
for item in v:
|
|
360
|
+
child = ET.SubElement(elem, k)
|
|
361
|
+
_dict_to_xml_element(item, child)
|
|
362
|
+
else:
|
|
363
|
+
child = ET.SubElement(elem, k)
|
|
364
|
+
_dict_to_xml_element(v, child)
|
|
365
|
+
elif isinstance(value, list):
|
|
366
|
+
for i, item in enumerate(value):
|
|
367
|
+
child = ET.SubElement(elem, "item")
|
|
368
|
+
_dict_to_xml_element(item, child)
|
|
369
|
+
elif value is not None:
|
|
370
|
+
elem.text = str(value)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def parse_ini(content: str) -> dict[str, Any]:
|
|
374
|
+
"""Parse INI content into a dict structure."""
|
|
375
|
+
parser = ConfigParser()
|
|
376
|
+
parser.read_string(content)
|
|
377
|
+
|
|
378
|
+
result = {}
|
|
379
|
+
for section in parser.sections():
|
|
380
|
+
result[section] = dict(parser[section])
|
|
381
|
+
|
|
382
|
+
# Handle default section
|
|
383
|
+
if parser.defaults():
|
|
384
|
+
result["DEFAULT"] = dict(parser.defaults())
|
|
385
|
+
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def format_ini(value: dict[str, Any]) -> str:
|
|
390
|
+
"""Format a dict as INI."""
|
|
391
|
+
lines = []
|
|
392
|
+
for section, items in value.items():
|
|
393
|
+
lines.append(f"[{section}]")
|
|
394
|
+
if isinstance(items, dict):
|
|
395
|
+
for k, v in items.items():
|
|
396
|
+
lines.append(f"{k} = {v}")
|
|
397
|
+
lines.append("")
|
|
398
|
+
return "\n".join(lines)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def parse_csv(content: str, delimiter: str = "") -> list[dict[str, str]]:
|
|
402
|
+
"""Parse CSV content into a list of dicts."""
|
|
403
|
+
if not content.strip():
|
|
404
|
+
return []
|
|
405
|
+
|
|
406
|
+
# Auto-detect delimiter
|
|
407
|
+
if not delimiter:
|
|
408
|
+
first_line = content.split("\n")[0]
|
|
409
|
+
if "\t" in first_line:
|
|
410
|
+
delimiter = "\t"
|
|
411
|
+
elif ";" in first_line:
|
|
412
|
+
delimiter = ";"
|
|
413
|
+
else:
|
|
414
|
+
delimiter = ","
|
|
415
|
+
|
|
416
|
+
reader = csv.DictReader(io.StringIO(content), delimiter=delimiter)
|
|
417
|
+
return list(reader)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def format_csv(value: list[dict[str, Any]] | list[list[Any]], delimiter: str = ",") -> str:
|
|
421
|
+
"""Format a value as CSV."""
|
|
422
|
+
if not value:
|
|
423
|
+
return ""
|
|
424
|
+
|
|
425
|
+
output = io.StringIO()
|
|
426
|
+
if isinstance(value[0], dict):
|
|
427
|
+
fieldnames = list(value[0].keys())
|
|
428
|
+
writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=delimiter)
|
|
429
|
+
writer.writeheader()
|
|
430
|
+
writer.writerows(value)
|
|
431
|
+
else:
|
|
432
|
+
writer = csv.writer(output, delimiter=delimiter)
|
|
433
|
+
writer.writerows(value)
|
|
434
|
+
|
|
435
|
+
return output.getvalue()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def parse_toml(content: str) -> dict[str, Any]:
|
|
439
|
+
"""Parse TOML content into a dict structure."""
|
|
440
|
+
# Simple TOML parser for common cases
|
|
441
|
+
result: dict[str, Any] = {}
|
|
442
|
+
current_section: dict[str, Any] = result
|
|
443
|
+
current_section_name = ""
|
|
444
|
+
|
|
445
|
+
for line in content.split("\n"):
|
|
446
|
+
line = line.strip()
|
|
447
|
+
|
|
448
|
+
# Skip empty lines and comments
|
|
449
|
+
if not line or line.startswith("#"):
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
# Section header
|
|
453
|
+
if line.startswith("["):
|
|
454
|
+
section_name = line[1:-1].strip()
|
|
455
|
+
# Handle nested sections like [package.metadata]
|
|
456
|
+
parts = section_name.split(".")
|
|
457
|
+
current_section = result
|
|
458
|
+
for part in parts:
|
|
459
|
+
if part not in current_section:
|
|
460
|
+
current_section[part] = {}
|
|
461
|
+
current_section = current_section[part]
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
# Key = value
|
|
465
|
+
if "=" in line:
|
|
466
|
+
key, value = line.split("=", 1)
|
|
467
|
+
key = key.strip()
|
|
468
|
+
value = value.strip()
|
|
469
|
+
current_section[key] = _parse_toml_value(value)
|
|
470
|
+
|
|
471
|
+
return result
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _parse_toml_value(value: str) -> Any:
|
|
475
|
+
"""Parse a TOML value."""
|
|
476
|
+
value = value.strip()
|
|
477
|
+
|
|
478
|
+
# String
|
|
479
|
+
if value.startswith('"') and value.endswith('"'):
|
|
480
|
+
return value[1:-1].replace('\\"', '"')
|
|
481
|
+
if value.startswith("'") and value.endswith("'"):
|
|
482
|
+
return value[1:-1]
|
|
483
|
+
|
|
484
|
+
# Array
|
|
485
|
+
if value.startswith("[") and value.endswith("]"):
|
|
486
|
+
inner = value[1:-1].strip()
|
|
487
|
+
if not inner:
|
|
488
|
+
return []
|
|
489
|
+
# Simple array parsing
|
|
490
|
+
items = []
|
|
491
|
+
for item in inner.split(","):
|
|
492
|
+
items.append(_parse_toml_value(item.strip()))
|
|
493
|
+
return items
|
|
494
|
+
|
|
495
|
+
# Boolean
|
|
496
|
+
if value == "true":
|
|
497
|
+
return True
|
|
498
|
+
if value == "false":
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
# Number
|
|
502
|
+
try:
|
|
503
|
+
if "." in value:
|
|
504
|
+
return float(value)
|
|
505
|
+
return int(value)
|
|
506
|
+
except ValueError:
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
return value
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def format_toml(value: dict[str, Any], section: str = "") -> str:
|
|
513
|
+
"""Format a dict as TOML."""
|
|
514
|
+
lines = []
|
|
515
|
+
|
|
516
|
+
# First output non-table values
|
|
517
|
+
for k, v in value.items():
|
|
518
|
+
if not isinstance(v, dict):
|
|
519
|
+
lines.append(f"{k} = {_format_toml_value(v)}")
|
|
520
|
+
|
|
521
|
+
# Then output tables
|
|
522
|
+
for k, v in value.items():
|
|
523
|
+
if isinstance(v, dict):
|
|
524
|
+
section_name = f"{section}.{k}" if section else k
|
|
525
|
+
lines.append("")
|
|
526
|
+
lines.append(f"[{section_name}]")
|
|
527
|
+
lines.append(format_toml(v, section_name).strip())
|
|
528
|
+
|
|
529
|
+
return "\n".join(lines)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _format_toml_value(value: Any) -> str:
|
|
533
|
+
"""Format a TOML value."""
|
|
534
|
+
if value is None:
|
|
535
|
+
return '""'
|
|
536
|
+
elif isinstance(value, bool):
|
|
537
|
+
return "true" if value else "false"
|
|
538
|
+
elif isinstance(value, (int, float)):
|
|
539
|
+
return str(value)
|
|
540
|
+
elif isinstance(value, str):
|
|
541
|
+
return json.dumps(value)
|
|
542
|
+
elif isinstance(value, list):
|
|
543
|
+
items = [_format_toml_value(v) for v in value]
|
|
544
|
+
return "[" + ", ".join(items) + "]"
|
|
545
|
+
else:
|
|
546
|
+
return json.dumps(value)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def parse_input(content: str, options: YqOptions) -> Any:
|
|
550
|
+
"""Parse input based on format option."""
|
|
551
|
+
fmt = options.input_format
|
|
552
|
+
|
|
553
|
+
if fmt == "json":
|
|
554
|
+
return json.loads(content)
|
|
555
|
+
elif fmt == "yaml":
|
|
556
|
+
return parse_yaml(content)
|
|
557
|
+
elif fmt == "xml":
|
|
558
|
+
return parse_xml(content, options.xml_attribute_prefix, options.xml_content_name)
|
|
559
|
+
elif fmt == "ini":
|
|
560
|
+
return parse_ini(content)
|
|
561
|
+
elif fmt == "csv":
|
|
562
|
+
return parse_csv(content, options.csv_delimiter)
|
|
563
|
+
elif fmt == "toml":
|
|
564
|
+
return parse_toml(content)
|
|
565
|
+
else:
|
|
566
|
+
raise ValueError(f"Unknown input format: {fmt}")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def format_output(value: Any, options: YqOptions) -> str:
|
|
570
|
+
"""Format output based on format option."""
|
|
571
|
+
fmt = options.output_format
|
|
572
|
+
|
|
573
|
+
if fmt == "json":
|
|
574
|
+
if options.raw and isinstance(value, str):
|
|
575
|
+
return value
|
|
576
|
+
if options.compact:
|
|
577
|
+
return json.dumps(value, separators=(",", ":"))
|
|
578
|
+
return json.dumps(value, indent=options.indent)
|
|
579
|
+
elif fmt == "yaml":
|
|
580
|
+
return format_yaml(value, options.indent)
|
|
581
|
+
elif fmt == "xml":
|
|
582
|
+
return format_xml(value)
|
|
583
|
+
elif fmt == "ini":
|
|
584
|
+
if isinstance(value, dict):
|
|
585
|
+
return format_ini(value)
|
|
586
|
+
raise ValueError("INI output requires object input")
|
|
587
|
+
elif fmt == "csv":
|
|
588
|
+
if isinstance(value, list):
|
|
589
|
+
return format_csv(value)
|
|
590
|
+
raise ValueError("CSV output requires array input")
|
|
591
|
+
elif fmt == "toml":
|
|
592
|
+
if isinstance(value, dict):
|
|
593
|
+
return format_toml(value)
|
|
594
|
+
raise ValueError("TOML output requires object input")
|
|
595
|
+
else:
|
|
596
|
+
raise ValueError(f"Unknown output format: {fmt}")
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class YqCommand:
|
|
600
|
+
"""The yq command - YAML/XML/INI/CSV/TOML processor."""
|
|
601
|
+
|
|
602
|
+
name = "yq"
|
|
603
|
+
|
|
604
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
605
|
+
"""Execute the yq command."""
|
|
606
|
+
if "--help" in args or "-h" in args:
|
|
607
|
+
return ExecResult(
|
|
608
|
+
stdout=(
|
|
609
|
+
"Usage: yq [OPTIONS] [FILTER] [FILE]\n"
|
|
610
|
+
"Command-line YAML/XML/INI/CSV/TOML processor.\n\n"
|
|
611
|
+
"Options:\n"
|
|
612
|
+
" -p, --input-format=FMT input format: yaml (default), xml, json, ini, csv, toml\n"
|
|
613
|
+
" -o, --output-format=FMT output format: yaml (default), json, xml, ini, csv, toml\n"
|
|
614
|
+
" -i, --inplace modify file in-place\n"
|
|
615
|
+
" -r, --raw-output output strings without quotes\n"
|
|
616
|
+
" -c, --compact compact output\n"
|
|
617
|
+
" -e, --exit-status set exit status based on output\n"
|
|
618
|
+
" -s, --slurp read entire input into array\n"
|
|
619
|
+
" -n, --null-input don't read any input\n"
|
|
620
|
+
" -j, --join-output don't print newlines after each output\n"
|
|
621
|
+
" -f, --front-matter extract YAML front matter from markdown\n"
|
|
622
|
+
" --help display this help and exit\n\n"
|
|
623
|
+
"Examples:\n"
|
|
624
|
+
" yq '.name' config.yaml\n"
|
|
625
|
+
" yq -o json '.' config.yaml\n"
|
|
626
|
+
" yq -p json -o yaml '.' data.json\n"
|
|
627
|
+
" yq '.users[0]' users.yaml\n"
|
|
628
|
+
),
|
|
629
|
+
stderr="",
|
|
630
|
+
exit_code=0,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Parse arguments
|
|
634
|
+
options = YqOptions()
|
|
635
|
+
filter_str = "."
|
|
636
|
+
filter_set = False
|
|
637
|
+
files: list[str] = []
|
|
638
|
+
input_format_explicit = False
|
|
639
|
+
|
|
640
|
+
i = 0
|
|
641
|
+
while i < len(args):
|
|
642
|
+
a = args[i]
|
|
643
|
+
|
|
644
|
+
if a.startswith("--input-format="):
|
|
645
|
+
fmt = a[15:]
|
|
646
|
+
if fmt not in VALID_INPUT_FORMATS:
|
|
647
|
+
return ExecResult(
|
|
648
|
+
stdout="",
|
|
649
|
+
stderr=f"yq: unknown input format: {fmt}\n",
|
|
650
|
+
exit_code=2,
|
|
651
|
+
)
|
|
652
|
+
options.input_format = fmt
|
|
653
|
+
input_format_explicit = True
|
|
654
|
+
elif a.startswith("--output-format="):
|
|
655
|
+
fmt = a[16:]
|
|
656
|
+
if fmt not in VALID_OUTPUT_FORMATS:
|
|
657
|
+
return ExecResult(
|
|
658
|
+
stdout="",
|
|
659
|
+
stderr=f"yq: unknown output format: {fmt}\n",
|
|
660
|
+
exit_code=2,
|
|
661
|
+
)
|
|
662
|
+
options.output_format = fmt
|
|
663
|
+
elif a == "-p" or a == "--input-format":
|
|
664
|
+
i += 1
|
|
665
|
+
if i >= len(args):
|
|
666
|
+
return ExecResult(
|
|
667
|
+
stdout="",
|
|
668
|
+
stderr="yq: option requires argument -- 'p'\n",
|
|
669
|
+
exit_code=2,
|
|
670
|
+
)
|
|
671
|
+
fmt = args[i]
|
|
672
|
+
if fmt not in VALID_INPUT_FORMATS:
|
|
673
|
+
return ExecResult(
|
|
674
|
+
stdout="",
|
|
675
|
+
stderr=f"yq: unknown input format: {fmt}\n",
|
|
676
|
+
exit_code=2,
|
|
677
|
+
)
|
|
678
|
+
options.input_format = fmt
|
|
679
|
+
input_format_explicit = True
|
|
680
|
+
elif a == "-o" or a == "--output-format":
|
|
681
|
+
i += 1
|
|
682
|
+
if i >= len(args):
|
|
683
|
+
return ExecResult(
|
|
684
|
+
stdout="",
|
|
685
|
+
stderr="yq: option requires argument -- 'o'\n",
|
|
686
|
+
exit_code=2,
|
|
687
|
+
)
|
|
688
|
+
fmt = args[i]
|
|
689
|
+
if fmt not in VALID_OUTPUT_FORMATS:
|
|
690
|
+
return ExecResult(
|
|
691
|
+
stdout="",
|
|
692
|
+
stderr=f"yq: unknown output format: {fmt}\n",
|
|
693
|
+
exit_code=2,
|
|
694
|
+
)
|
|
695
|
+
options.output_format = fmt
|
|
696
|
+
elif a in ("-r", "--raw-output"):
|
|
697
|
+
options.raw = True
|
|
698
|
+
elif a in ("-c", "--compact"):
|
|
699
|
+
options.compact = True
|
|
700
|
+
elif a in ("-e", "--exit-status"):
|
|
701
|
+
options.exit_status = True
|
|
702
|
+
elif a in ("-s", "--slurp"):
|
|
703
|
+
options.slurp = True
|
|
704
|
+
elif a in ("-n", "--null-input"):
|
|
705
|
+
options.null_input = True
|
|
706
|
+
elif a in ("-j", "--join-output"):
|
|
707
|
+
options.join_output = True
|
|
708
|
+
elif a in ("-i", "--inplace"):
|
|
709
|
+
options.inplace = True
|
|
710
|
+
elif a in ("-f", "--front-matter"):
|
|
711
|
+
options.front_matter = True
|
|
712
|
+
elif a == "-":
|
|
713
|
+
files.append("-")
|
|
714
|
+
elif a.startswith("--"):
|
|
715
|
+
return ExecResult(
|
|
716
|
+
stdout="",
|
|
717
|
+
stderr=f"yq: unknown option: {a}\n",
|
|
718
|
+
exit_code=2,
|
|
719
|
+
)
|
|
720
|
+
elif a.startswith("-"):
|
|
721
|
+
# Handle combined short options
|
|
722
|
+
for c in a[1:]:
|
|
723
|
+
if c == "r":
|
|
724
|
+
options.raw = True
|
|
725
|
+
elif c == "c":
|
|
726
|
+
options.compact = True
|
|
727
|
+
elif c == "e":
|
|
728
|
+
options.exit_status = True
|
|
729
|
+
elif c == "s":
|
|
730
|
+
options.slurp = True
|
|
731
|
+
elif c == "n":
|
|
732
|
+
options.null_input = True
|
|
733
|
+
elif c == "j":
|
|
734
|
+
options.join_output = True
|
|
735
|
+
elif c == "i":
|
|
736
|
+
options.inplace = True
|
|
737
|
+
elif c == "f":
|
|
738
|
+
options.front_matter = True
|
|
739
|
+
else:
|
|
740
|
+
return ExecResult(
|
|
741
|
+
stdout="",
|
|
742
|
+
stderr=f"yq: unknown option: -{c}\n",
|
|
743
|
+
exit_code=2,
|
|
744
|
+
)
|
|
745
|
+
elif not filter_set:
|
|
746
|
+
filter_str = a
|
|
747
|
+
filter_set = True
|
|
748
|
+
else:
|
|
749
|
+
files.append(a)
|
|
750
|
+
i += 1
|
|
751
|
+
|
|
752
|
+
# Auto-detect format from file extension if not explicitly set
|
|
753
|
+
if not input_format_explicit and files and files[0] != "-":
|
|
754
|
+
detected = detect_format_from_extension(files[0])
|
|
755
|
+
if detected:
|
|
756
|
+
options.input_format = detected
|
|
757
|
+
|
|
758
|
+
# Inplace requires a file
|
|
759
|
+
if options.inplace and (not files or files[0] == "-"):
|
|
760
|
+
return ExecResult(
|
|
761
|
+
stdout="",
|
|
762
|
+
stderr="yq: -i/--inplace requires a file argument\n",
|
|
763
|
+
exit_code=1,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
# Read input
|
|
767
|
+
file_path = None
|
|
768
|
+
if options.null_input:
|
|
769
|
+
content = ""
|
|
770
|
+
elif not files or files[0] == "-":
|
|
771
|
+
content = ctx.stdin
|
|
772
|
+
else:
|
|
773
|
+
try:
|
|
774
|
+
file_path = ctx.fs.resolve_path(ctx.cwd, files[0])
|
|
775
|
+
content = await ctx.fs.read_file(file_path)
|
|
776
|
+
except FileNotFoundError:
|
|
777
|
+
return ExecResult(
|
|
778
|
+
stdout="",
|
|
779
|
+
stderr=f"yq: {files[0]}: No such file or directory\n",
|
|
780
|
+
exit_code=2,
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
try:
|
|
784
|
+
# Extract front matter if requested
|
|
785
|
+
if options.front_matter and not options.null_input:
|
|
786
|
+
front_matter = extract_front_matter(content)
|
|
787
|
+
if front_matter is None:
|
|
788
|
+
# No front matter found - return null
|
|
789
|
+
content = ""
|
|
790
|
+
else:
|
|
791
|
+
content = front_matter
|
|
792
|
+
# Front matter is always YAML
|
|
793
|
+
options.input_format = "yaml"
|
|
794
|
+
|
|
795
|
+
# Parse input
|
|
796
|
+
if options.null_input:
|
|
797
|
+
parsed = None
|
|
798
|
+
elif options.front_matter and not content:
|
|
799
|
+
parsed = None
|
|
800
|
+
else:
|
|
801
|
+
parsed = parse_input(content, options)
|
|
802
|
+
|
|
803
|
+
if options.slurp and not options.null_input:
|
|
804
|
+
parsed = [parsed]
|
|
805
|
+
|
|
806
|
+
# Parse and evaluate filter using query engine
|
|
807
|
+
ast = parse(filter_str)
|
|
808
|
+
eval_ctx = EvalContext(env=dict(ctx.env))
|
|
809
|
+
results = evaluate(parsed, ast, eval_ctx)
|
|
810
|
+
|
|
811
|
+
# Format output
|
|
812
|
+
formatted = []
|
|
813
|
+
for result in results:
|
|
814
|
+
formatted.append(format_output(result, options))
|
|
815
|
+
|
|
816
|
+
separator = "" if options.join_output else "\n"
|
|
817
|
+
output = separator.join(f for f in formatted if f)
|
|
818
|
+
if output and not options.join_output:
|
|
819
|
+
output += "\n"
|
|
820
|
+
|
|
821
|
+
# Handle inplace mode
|
|
822
|
+
if options.inplace and file_path:
|
|
823
|
+
await ctx.fs.write_file(file_path, output)
|
|
824
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
825
|
+
|
|
826
|
+
# Determine exit code
|
|
827
|
+
exit_code = 0
|
|
828
|
+
if options.exit_status:
|
|
829
|
+
if not results or all(
|
|
830
|
+
v is None or v is False
|
|
831
|
+
for v in results
|
|
832
|
+
):
|
|
833
|
+
exit_code = 1
|
|
834
|
+
|
|
835
|
+
return ExecResult(stdout=output, stderr="", exit_code=exit_code)
|
|
836
|
+
|
|
837
|
+
except ValueError as e:
|
|
838
|
+
return ExecResult(
|
|
839
|
+
stdout="",
|
|
840
|
+
stderr=f"yq: parse error: {e}\n",
|
|
841
|
+
exit_code=5,
|
|
842
|
+
)
|
|
843
|
+
except Exception as e:
|
|
844
|
+
return ExecResult(
|
|
845
|
+
stdout="",
|
|
846
|
+
stderr=f"yq: error: {e}\n",
|
|
847
|
+
exit_code=1,
|
|
848
|
+
)
|