parsanol 1.3.108 → 1.3.112

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c753a3a41a099b3306b3b18861cd2236b4b7a122c032cbf2a3815bc2777dd9e
4
- data.tar.gz: 0b7fdf682dfa6150ea51cc30d36892bb5657e0bb6c7a833a09a810a146aa5845
3
+ metadata.gz: 683289b7d0453a88c2dfb691d54479ebacf6be93cc30f527c4199557bb99b48d
4
+ data.tar.gz: '0182a60c9bca2c3c1fcf1f450fed4623aeb915fb3b3e80f3a14ee716c09f3ffd'
5
5
  SHA512:
6
- metadata.gz: a94d7bcebe6d9780adc68d3005beccb194b78a6e48b71c3a5479c6dc37f9fa38a9efcf45ec979bb69c6fa521618b5f733909cb28e4eefc7f6baf95a457c1c52e
7
- data.tar.gz: 677c550497c09949077db2c70083e09b72e0b3f9adf57dab4ba68bb69e05ad3de81bc8914e40509326bed4ca4f8fc22dab00c85a0555d52072e86a04f7588683
6
+ metadata.gz: 15607095c23776f36bf34dcf4e53849a739cd373e9a31a766c484331fd998a01b4d66faebd528fb61077d648e58e7e35173e6ec7e3280640e1b2eaee17a884cc
7
+ data.tar.gz: 8fc517dc729088dcac71596911ed5a57a8b72c580d246b315a1b8313cb0157ef7ee63b04b20d723583f03ac4699af4810ca1cb61e5c91d9aed1514fe68806a27
data/Cargo.lock CHANGED
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.18.19"
310
+ version = "0.19.6"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "30daf4ef27d78639cb883ea9d71ec990e7fcfc7ffd295c3651098fe2bcdf70cd"
312
+ checksum = "8ffe7e92d626c65e83d2c53c148a18db5897879858bcd37ebd0f406f5a251036"
313
313
  dependencies = [
314
314
  "ahash",
315
315
  "getrandom 0.3.4",
@@ -326,9 +326,9 @@ dependencies = [
326
326
 
327
327
  [[package]]
328
328
  name = "parsanol-derive"
329
- version = "0.18.19"
329
+ version = "0.19.6"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "7f2c09d80a3a6c4efe1f756642877f73cc830e25f2011f101fc9b8e8ec1f33cd"
331
+ checksum = "3b94bb50df3fe1a9f66b6d8ee784672c27262e9e609568d5e9d14e1f7d6bec91"
332
332
  dependencies = [
333
333
  "proc-macro2",
334
334
  "quote",
@@ -13,4 +13,4 @@ doc = false
13
13
 
14
14
  [dependencies]
15
15
  # 0.17.0 carries LookSpec + the widened opaque writeback
16
- parsanol = { version = "0.18.0" }
16
+ parsanol = { version = "0.19.1" }
@@ -32,7 +32,7 @@ magnus = { version = "0.9" }
32
32
  # parsanol parser library (crates.io; 0.17.0 carries the LookSpec regex
33
33
  # lookbehind (rs#163 gap 1) and the widened opaque writeback: the
34
34
  # lazy-continuation fix, parsanol-ruby#160)
35
- parsanol = { version = "0.18.0", features = ["ruby"] }
35
+ parsanol = { version = "0.19.1", features = ["ruby"] }
36
36
 
37
37
  # Logging
38
38
  log = "0.4"
@@ -195,8 +195,19 @@ module Parsanol
195
195
 
196
196
  unless success
197
197
  last_failure = value
198
+ if ENV["PARSANOL_REP_TRACE"]
199
+ warn "REP body FAIL at iter=#{occurrence} start=#{iteration_start} pos_after=#{source.bytepos} restoring"
200
+ end
201
+ # A failed iteration may have consumed input before its own
202
+ # inner failure (an ordered choice's partial alternative);
203
+ # the repetition owns the rollback — the rep ends at the
204
+ # last SUCCESSFUL iteration's end (ruby#357).
205
+ source.bytepos = iteration_start
198
206
  break
199
207
  end
208
+ if ENV["PARSANOL_REP_TRACE"]
209
+ warn "REP body OK at iter=#{occurrence} start=#{iteration_start} end=#{source.bytepos}"
210
+ end
200
211
 
201
212
  occurrence += 1
202
213
  result[occurrence] = value
@@ -276,8 +287,19 @@ module Parsanol
276
287
 
277
288
  unless success
278
289
  last_failure = value
290
+ if ENV["PARSANOL_REP_TRACE"]
291
+ warn "REP body FAIL at iter=#{occurrence} start=#{iteration_start} pos_after=#{source.bytepos} restoring"
292
+ end
293
+ # A failed iteration may have consumed input before its own
294
+ # inner failure (an ordered choice's partial alternative);
295
+ # the repetition owns the rollback — the rep ends at the
296
+ # last SUCCESSFUL iteration's end (ruby#357).
297
+ source.bytepos = iteration_start
279
298
  break
280
299
  end
300
+ if ENV["PARSANOL_REP_TRACE"]
301
+ warn "REP body OK at iter=#{occurrence} start=#{iteration_start} end=#{source.bytepos}"
302
+ end
281
303
 
282
304
  occurrence += 1
283
305
  result[occurrence] = value
@@ -26,6 +26,10 @@ module Parsanol
26
26
 
27
27
  SUPPORTED_SHAPE = "parsanol-tree/v2"
28
28
 
29
+ # The editions this runtime parses (ruby#357's consolidation):
30
+ # v1 = the legacy shapes; v2 = named trees + explicit spans.
31
+ SUPPORTED_EDITIONS = %w[v1 v2].freeze
32
+
29
33
  def initialize(envelope, path, tables_dir, imports_dir: nil)
30
34
  unless envelope["shape"] == SUPPORTED_SHAPE
31
35
  raise ArtifactError,
@@ -38,6 +42,18 @@ module Parsanol
38
42
  bindings = envelope["bindings"]
39
43
  validate_bindings!(bindings) unless bindings.nil?
40
44
  @envelope = envelope
45
+ # ruby#353: absent field = the pre-1.3.109 semantics. A level
46
+ # above this runtime's is a hard error (the trees would carry
47
+ # nodes this engine never emits); a lower one warns — parsing
48
+ # works, but frozen tree comparisons will differ.
49
+ # The declared edition must be one this runtime understands
50
+ # (ruby#357's consolidation: one axis, `edition`, values v1/v2).
51
+ edition = envelope["edition"] || "v1"
52
+ unless SUPPORTED_EDITIONS.include?(edition)
53
+ raise ArtifactError,
54
+ "artifact declares edition #{edition.inspect}; this " \
55
+ "runtime supports #{SUPPORTED_EDITIONS.map(&:inspect).join(', ')}"
56
+ end
41
57
  @path = path
42
58
  @tables_dir = tables_dir
43
59
  # parsanol-ruby#208: `use`-importing grammars need the imported
@@ -233,13 +249,27 @@ module Parsanol
233
249
  # initial parse's (the native FFI's coarse failure position
234
250
  # loses the deepest offset).
235
251
  captures, reparse_error = recovery_journal(entry_name, input, mode)
236
- partial = recovery_partial(entry_name)
252
+ # The spine boundary, not the failure offset, is where the
253
+ # resync scan starts (rs#304 convergence): junk frequently lies
254
+ # BEFORE the deepest failure (the failed iteration consumed its
255
+ # own prefix), and the scan from the failure offset can never
256
+ # match it. When no iteration matched, the boundary is 0 —
257
+ # recovery still runs (rs#304 Gap B: a declared pattern is
258
+ # usable from the start).
259
+ stash = Parsanol::Recovery.take_partial
260
+ partial = recovery_partial(entry_name, stash)
261
+ boundary = stash&.first || 0
237
262
  resyncs = []
238
- if partial && reparse_error
239
- items = partial.values.first
240
- extended, resyncs = resync_extension(entry_name, input,
241
- reparse_error["offset"])
242
- items.concat(extended) if extended
263
+ if reparse_error
264
+ root_name = entry(entry_name)["root"].to_sym
265
+ items = partial&.values&.first
266
+ created_partial = items.nil?
267
+ items ||= []
268
+ extended, resyncs = resync_extension(entry_name, input, boundary)
269
+ items.concat(extended)
270
+ # Recovered iterations with no prior partial form the partial
271
+ # (the rs engine's stash exists from offset 0).
272
+ partial = { root_name => items } if created_partial && !items.empty?
243
273
  end
244
274
  { "ok" => false,
245
275
  "error" => reparse_error || recovery_error(e),
@@ -367,8 +397,7 @@ module Parsanol
367
397
  # R1.5 (TODO.sota/1): the deepest repetition prefix the failed
368
398
  # journaling pass matched, shaped like the root (the item array
369
399
  # under the root rule's name). Nil when nothing matched.
370
- def recovery_partial(entry_name)
371
- stash = Parsanol::Recovery.take_partial
400
+ def recovery_partial(entry_name, stash = Parsanol::Recovery.take_partial)
372
401
  return nil if stash.nil?
373
402
 
374
403
  _end_pos, items, _tag = stash
@@ -64,22 +64,213 @@ module Parsanol
64
64
  end
65
65
  end
66
66
 
67
+ # The bound model's shape: a nested type tree derived from the
68
+ # binding paths (ruby#308 v2). Dot segments nest objects, a
69
+ # `[]` segment is an array of objects (or of leaf values when
70
+ # the array is itself the leaf), and the leaf carries the
71
+ # binding's type and card (`0..1` = nullable, `*` cards arrive
72
+ # through the array node itself).
73
+ #
74
+ # shape({ "items[].n" => { "type" => "integer", "card" => "1" } })
75
+ # # => { "items" => { "kind" => "array",
76
+ # # "of" => { "n" => { "kind" => "leaf", "type" => "integer", "card" => "1" } } } }
77
+ def shape(fields)
78
+ root = {}
79
+ fields.each do |path, requirements|
80
+ segments = path.split(".")
81
+ node = root
82
+ segments.each_with_index do |segment, index|
83
+ array = segment.end_with?("[]")
84
+ name = array ? segment.delete_suffix("[]") : segment
85
+ leaf = index == segments.length - 1
86
+ if array && leaf
87
+ node[name] = {
88
+ "kind" => "array",
89
+ "of" => { "kind" => "leaf", "type" => requirements["type"], "card" => requirements["card"] },
90
+ }
91
+ break
92
+ end
93
+ slot = (node[name] ||= array ? { "kind" => "array", "of" => {} } : {})
94
+ node = array ? slot["of"] : slot
95
+ next unless leaf
96
+
97
+ slot.update("kind" => "leaf", "type" => requirements["type"], "card" => requirements["card"])
98
+ end
99
+ end
100
+ root
101
+ end
102
+
67
103
  # TypeScript materialization types: one interface per entry, one
68
- # property per bound field. Cards ending in * become arrays;
104
+ # interface per nested object node (v2: the interfaces mirror
105
+ # the bound model's shape). Cards ending in * become arrays;
69
106
  # 0..1 becomes nullable.
70
107
  def to_typescript(schema)
71
108
  lines = ["// Generated by parsanol parg schema --ts — binding-requirements types."]
72
109
  schema.each do |entry, data|
73
110
  lines << ""
74
- lines << "export interface #{ts_name(entry)} {"
75
- data["fields"].each do |path, requirements|
76
- lines << " #{ts_prop(path)}: #{ts_type(requirements)};"
111
+ emit_ts_interface(lines, ts_name(entry), shape(data["fields"]))
112
+ end
113
+ "#{lines.join("\n")}\n"
114
+ end
115
+
116
+ def emit_ts_interface(lines, name, node)
117
+ nested = []
118
+ lines << "export interface #{name} {"
119
+ node.each do |prop, child|
120
+ case child["kind"]
121
+ when "array"
122
+ element = child["of"]
123
+ inner = element["kind"] == "leaf" ? ts_leaf(element) : ts_ref_name(name, prop)
124
+ lines << " #{ts_prop(prop)}: #{inner}[];"
125
+ nested << [ts_ref_name(name, prop), element] unless element["kind"] == "leaf"
126
+ when "leaf"
127
+ lines << " #{ts_prop(prop)}: #{ts_leaf(child)};"
77
128
  end
78
- lines << "}"
129
+ end
130
+ lines << "}"
131
+ lines << ""
132
+ nested.each { |inner, element| emit_ts_interface(lines, inner, element) }
133
+ end
134
+
135
+ def ts_leaf(leaf)
136
+ base = ts_base(leaf["type"])
137
+ if leaf["card"].to_s.end_with?("*")
138
+ "#{base}[]"
139
+ elsif leaf["card"] == "0..1"
140
+ "#{base} | null"
141
+ else
142
+ base
143
+ end
144
+ end
145
+
146
+ def ts_base(type)
147
+ case type
148
+ when "integer", "float" then "number"
149
+ when "boolean" then "boolean"
150
+ else "string"
151
+ end
152
+ end
153
+
154
+ def ts_ref_name(parent, prop)
155
+ "#{parent}#{ts_prop(prop).split('.').map(&:capitalize).join}"
156
+ end
157
+
158
+ # Ruby materialization models: lutaml-model classes (the
159
+ # established serialization framework — the generated file
160
+ # requires it; the consumer's bundle carries the gem).
161
+ def to_ruby(schema)
162
+ lines = [
163
+ "# frozen_string_literal: true",
164
+ "",
165
+ "# Generated by parsanol parg schema --ruby — lutaml-model binding types.",
166
+ "require \"lutaml/model\"",
167
+ "",
168
+ ]
169
+ schema.each do |entry, data|
170
+ emit_ruby_class(lines, ts_name(entry), shape(data["fields"]))
79
171
  end
80
172
  "#{lines.join("\n")}\n"
81
173
  end
82
174
 
175
+ def emit_ruby_class(lines, name, node)
176
+ nested = []
177
+ node.each do |prop, child|
178
+ next unless child["kind"] == "array" && child["of"]["kind"] != "leaf"
179
+
180
+ inner = lutaml_ref_name(name, prop)
181
+ nested << [inner, child["of"]]
182
+ end
183
+ # Ruby resolves the constant at class-body time: the nested
184
+ # models precede their parents.
185
+ nested.each { |inner, element| emit_ruby_class(lines, inner, element) }
186
+ lines << "class #{name} < Lutaml::Model::Serializable"
187
+ lines <<
188
+ node.each do |prop, child|
189
+ attr_name = ts_prop(prop)
190
+ case child["kind"]
191
+ when "array"
192
+ element = child["of"]
193
+ if element["kind"] == "leaf"
194
+ lines << " attribute :#{attr_name}, #{lutaml_type(element)}, collection: true"
195
+ else
196
+ inner = lutaml_ref_name(name, prop)
197
+ lines << " attribute :#{attr_name}, #{inner}, collection: true"
198
+ nested << [inner, element]
199
+ end
200
+ when "leaf"
201
+ # lutaml-model attributes are nilable unless validated:
202
+ # a 0..1 card is the plain attribute; presence-validated
203
+ # cards are the consumer's policy, not the schema's.
204
+ lines << " attribute :#{attr_name}, #{lutaml_type(child)}"
205
+ end
206
+ end
207
+ lines << "end"
208
+ lines << ""
209
+ end
210
+
211
+ def lutaml_type(leaf)
212
+ case leaf["type"]
213
+ when "integer" then ":integer"
214
+ when "float" then ":float"
215
+ when "boolean" then ":boolean"
216
+ else ":string"
217
+ end
218
+ end
219
+
220
+ def lutaml_ref_name(parent, prop)
221
+ "#{parent}#{ts_prop(prop).split('.').map(&:capitalize).join}"
222
+ end
223
+
224
+ # Rust materialization structs (serde): one struct per nested
225
+ # object node, i64/f64/bool/String leaves, Vec for arrays,
226
+ # Option for 0..1.
227
+ def to_rust(schema)
228
+ lines = [
229
+ "// Generated by parsanol parg schema --rust — serde binding types.",
230
+ "",
231
+ ]
232
+ schema.each do |entry, data|
233
+ emit_rust_struct(lines, ts_name(entry), shape(data["fields"]))
234
+ end
235
+ "#{lines.join("\n")}\n"
236
+ end
237
+
238
+ def emit_rust_struct(lines, name, node)
239
+ nested = []
240
+ lines << "#[derive(Debug, serde::Serialize, serde::Deserialize)]"
241
+ lines << "pub struct #{name} {"
242
+ node.each do |prop, child|
243
+ field = ts_prop(prop)
244
+ snake = field.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
245
+ case child["kind"]
246
+ when "array"
247
+ element = child["of"]
248
+ inner = element["kind"] == "leaf" ? rust_leaf(element) : rust_ref_name(name, prop)
249
+ lines << " pub #{snake}: Vec<#{inner}>,"
250
+ nested << [rust_ref_name(name, prop), element] unless element["kind"] == "leaf"
251
+ when "leaf"
252
+ base = rust_leaf(child)
253
+ lines << " pub #{snake}: #{child['card'] == '0..1' ? "Option<#{base}>" : base},"
254
+ end
255
+ end
256
+ lines << "}"
257
+ lines << ""
258
+ nested.each { |inner, element| emit_rust_struct(lines, inner, element) }
259
+ end
260
+
261
+ def rust_leaf(leaf)
262
+ case leaf["type"]
263
+ when "integer" then "i64"
264
+ when "float" then "f64"
265
+ when "boolean" then "bool"
266
+ else "String"
267
+ end
268
+ end
269
+
270
+ def rust_ref_name(parent, prop)
271
+ ts_ref_name(parent, prop)
272
+ end
273
+
83
274
  def ts_name(entry)
84
275
  entry.split(/[_.]/).map(&:capitalize).join
85
276
  end
@@ -29,6 +29,13 @@ module Parsanol
29
29
  repl <file> [--entry E] interactive parse loop (blank
30
30
  line quits, :quit exits)
31
31
  doc <file> [rule] print rule documentation
32
+ export <file|artifact.json> --target xgrammar
33
+ lower the compiled artifact to a
34
+ decoder-facing grammar (ruby#321)
35
+ lint <file> [--suggest-upgrades]
36
+ print lint errors (exit 1) and
37
+ warnings; the flag adds
38
+ machine-applicable upgrades
32
39
 
33
40
  options:
34
41
  --tables DIR directory for alt-from-table data (default:
@@ -51,6 +58,7 @@ module Parsanol
51
58
  @argv.delete_at(index)
52
59
  end
53
60
  @json = !!@argv.delete("--json")
61
+ @suggest_upgrades = !!@argv.delete("--suggest-upgrades")
54
62
  command = @argv.shift
55
63
  case command
56
64
  when "compile" then compile
@@ -60,6 +68,9 @@ module Parsanol
60
68
  when "check" then check
61
69
  when "repl" then repl
62
70
  when "doc" then doc
71
+ when "lint" then lint
72
+ when "export" then export_grammar
73
+ when "diff" then diff_artifacts
63
74
  when "lsp" then Lsp.new.run && 0
64
75
  else
65
76
  warn USAGE
@@ -84,6 +95,53 @@ module Parsanol
84
95
  end
85
96
  end
86
97
 
98
+ # `parg export FILE --target xgrammar`: the decoder-facing
99
+ # lowering (ruby#321). Reads a .parg (compiling it) or an
100
+ # artifact json, prints the target grammar on stdout.
101
+ def export_grammar
102
+ source = @argv.shift
103
+ target = (flag_value("--target") || "xgrammar").to_sym
104
+ json = if source.end_with?(".json")
105
+ File.read(source)
106
+ else
107
+ JSON.generate(compile_source(source).envelope)
108
+ end
109
+ print Export.call(json, target: target)
110
+ 0
111
+ end
112
+
113
+ # `parg lint FILE`: the lint suite on one grammar — errors
114
+ # first (exit 1), then every warning including the advisory
115
+ # strategies. --suggest-upgrades adds the machine-applicable
116
+ # upgrade suggestions on top.
117
+ def lint
118
+ source = @argv.shift
119
+ @captures_radar = @argv.delete("--captures") ? true : false
120
+ tables = @tables_dir || default_tables_dir(source)
121
+ document = Parsanol::PARG::Parser.new(PARG.read_utf8(source)).parse
122
+ Parsanol::PARG::Imports.merge!(document, [File.dirname(source)])
123
+ compiler = Parsanol::PARG::Compiler.new(document, tables)
124
+ document.rules.each_key { |rule| compiler.atom_for(rule) }
125
+
126
+ errors = Lints.errors(document, compiler)
127
+ errors.each { |error| puts "error: #{error}" }
128
+ return 1 unless errors.empty?
129
+
130
+ Lints.warnings(document, compiler).each { |warning| puts "warning: #{warning}" }
131
+ if @suggest_upgrades
132
+ Lints::CoreRuleDuplicates.new.warnings(document, compiler)
133
+ .each { |upgrade| puts "upgrade: #{upgrade}" }
134
+ end
135
+ if @captures_radar
136
+ # The migration radar (ruby#304): what modern mode would name.
137
+ compiler.modern_radar.each { |line| puts "captures: #{line}" }
138
+ end
139
+ 0
140
+ rescue Parsanol::PARG::ParseError, Parsanol::PARG::Error => e
141
+ warn "#{e.class}: #{e.message}"
142
+ 1
143
+ end
144
+
87
145
  def compile
88
146
  source = @argv.shift
89
147
  out = flag_value("-o") || default_artifact_name(source)
@@ -152,16 +210,66 @@ module Parsanol
152
210
 
153
211
  def schema
154
212
  ts = @argv.delete("--ts")
213
+ ruby = @argv.delete("--ruby")
214
+ rust = @argv.delete("--rust")
155
215
  artifact = artifact_for(@argv.shift)
156
216
  schema = Parsanol::PARG::Schema.from_artifact(artifact)
157
217
  if ts
158
218
  puts Parsanol::PARG::Schema.to_typescript(schema)
219
+ elsif ruby
220
+ puts Parsanol::PARG::Schema.to_ruby(schema)
221
+ elsif rust
222
+ puts Parsanol::PARG::Schema.to_rust(schema)
159
223
  else
160
224
  puts JSON.pretty_generate(schema)
161
225
  end
162
226
  0
163
227
  end
164
228
 
229
+ # parg diff OLD.artifact.json NEW.artifact.json [--json] [--suggest-bump] [--check LEVEL]
230
+ # Semantic classification of a grammar change (ruby#306): majors
231
+ # break consumers, minors extend them, patches preserve behavior.
232
+ def diff_artifacts
233
+ old_path = @argv.shift
234
+ new_path = @argv.shift
235
+ unless old_path && new_path
236
+ warn "usage: parsanol parg diff OLD.artifact.json NEW.artifact.json [--json] [--suggest-bump] [--check minor]"
237
+ return 2
238
+ end
239
+ suggest = !!@argv.delete("--suggest-bump")
240
+ check = @argv.delete("--check")
241
+ check_level = check && check.split("=", 2).last&.to_sym
242
+
243
+ old_env = JSON.parse(File.read(old_path))
244
+ new_env = JSON.parse(File.read(new_path))
245
+ findings = Diff.diff(old_env, new_env)
246
+ level = Diff.level(findings)
247
+
248
+ if @json
249
+ payload = {
250
+ level: level.to_s,
251
+ findings: findings.map { |f| { level: f.level.to_s, kind: f.kind.to_s, detail: f.detail } },
252
+ }
253
+ payload[:suggested] = Diff.suggest_bump(findings) if suggest
254
+ puts JSON.generate(payload)
255
+ else
256
+ if findings.empty?
257
+ puts "no semantic changes (#{level})"
258
+ else
259
+ findings.each { |f| puts "#{f.level.upcase} #{f.kind}: #{f.detail}" }
260
+ puts "level: #{level}"
261
+ end
262
+ puts Diff.suggest_bump(findings) if suggest
263
+ end
264
+
265
+ if check_level
266
+ ok = Diff::SEVERITY[level] >= Diff::SEVERITY[check_level]
267
+ warn "check: #{level} is #{ok ? 'at least' : 'BELOW'} #{check_level}" unless @json
268
+ return ok ? 0 : 3
269
+ end
270
+ 0
271
+ end
272
+
165
273
  def parse
166
274
  file = @argv.shift
167
275
  entry = flag_value("--entry")