Skip to content

Commit 799cbe9

Browse files
authored
Ruby CLI: Support stdin as an option for [file] (#1186)
This pull request updates the Ruby CLI to accept `stdin` as an option for all commands that accept `[file]`. Now, the following is supported: ```bash echo "<div>Hello</div>" | herb lex cat file.html.erb | herb parse ``` You can also use `-` to explicitly read from stdin: ```bash herb compile - ``` Resolves #1180
1 parent 14c5776 commit 799cbe9

3 files changed

Lines changed: 315 additions & 9 deletions

File tree

lib/herb/cli.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ class Herb::CLI
1313
def initialize(args)
1414
@args = args
1515
@command = args[0]
16-
@file = args[1]
1716
end
1817

1918
def call
2019
options
20+
@file = @args[1]
2121

2222
if silent
2323
if result.failed?
@@ -61,16 +61,23 @@ def directory
6161
end
6262

6363
def file_content
64-
if @file && File.exist?(@file)
64+
if @file && @file != "-" && File.exist?(@file)
6565
File.read(@file)
66-
elsif @file
66+
elsif @file && @file != "-"
6767
puts "File doesn't exist: #{@file}"
6868
exit(1)
69+
elsif @file == "-" || !$stdin.tty?
70+
$stdin.read
6971
else
7072
puts "No file provided."
7173
puts
7274
puts "Usage:"
7375
puts " bundle exec herb #{@command} [file] [options]"
76+
puts
77+
puts "You can also pipe content via stdin:"
78+
puts " echo \"<div>Hello</div>\" | bundle exec herb #{@command}"
79+
puts " cat file.html.erb | bundle exec herb #{@command}"
80+
puts " bundle exec herb #{@command} -"
7481
exit(1)
7582
end
7683
end
@@ -100,6 +107,14 @@ def help(exit_code = 0)
100107
bundle exec herb playground [file] Open the content of the source file in the playground
101108
bundle exec herb version Prints the versions of the Herb gem and the libherb library.
102109
110+
stdin:
111+
Commands that accept [file] also accept input via stdin:
112+
echo "<div>Hello</div>" | bundle exec herb lex
113+
cat file.html.erb | bundle exec herb parse
114+
115+
Use `-` to explicitly read from stdin:
116+
bundle exec herb compile -
117+
103118
Options:
104119
#{option_parser.to_s.strip.gsub(/^ /, " ")}
105120

test/engine/cli_stdin_test.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# frozen_string_literal: true
2+
3+
require "English"
4+
require_relative "../test_helper"
5+
6+
require "tempfile"
7+
require "json"
8+
9+
module Engine
10+
class CLIStdinTest < Minitest::Spec
11+
def setup
12+
skip "Shell stdin tests are skipped in CI" if ENV["CI"]
13+
end
14+
15+
def with_temp_file(content)
16+
file = Tempfile.new(["test_template", ".erb"])
17+
file.write(content)
18+
file.close
19+
yield file.path
20+
ensure
21+
file&.unlink
22+
end
23+
24+
test "shell: lex accepts piped stdin" do
25+
output = `echo '<div>Hello</div>' | bundle exec herb lex 2>&1`
26+
27+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
28+
assert_includes output, "TOKEN_HTML_TAG_START"
29+
assert_includes output, "div"
30+
end
31+
32+
test "shell: parse accepts piped stdin" do
33+
output = `echo '<div>Hello</div>' | bundle exec herb parse 2>&1`
34+
35+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
36+
assert_includes output, "DocumentNode"
37+
assert_includes output, "HTMLElementNode"
38+
end
39+
40+
test "shell: compile accepts piped stdin" do
41+
output = `echo '<div><%= name %></div>' | bundle exec herb compile 2>&1`
42+
43+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
44+
assert_includes output, "_buf = ::String.new"
45+
assert_includes output, "__herb.h((name))"
46+
end
47+
48+
test "shell: ruby accepts piped stdin" do
49+
output = `echo '<div><%= user.name %></div>' | bundle exec herb ruby 2>&1`
50+
51+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
52+
assert_includes output, "user.name"
53+
end
54+
55+
test "shell: html accepts piped stdin" do
56+
output = `echo '<div><%= user.name %></div>' | bundle exec herb html 2>&1`
57+
58+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
59+
assert_includes output, "<div>"
60+
assert_includes output, "</div>"
61+
end
62+
63+
test "shell: render accepts piped stdin" do
64+
output = `echo '<div>Static</div>' | bundle exec herb render 2>&1`
65+
66+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
67+
assert_includes output, "<div>Static</div>"
68+
end
69+
70+
test "shell: compile with json flag and stdin" do
71+
output = `echo '<div>Test</div>' | bundle exec herb compile --json 2>&1`
72+
73+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
74+
json_data = JSON.parse(output)
75+
assert_equal true, json_data["success"]
76+
assert_includes json_data["source"], "_buf = ::String.new"
77+
end
78+
79+
test "shell: lex with json flag and stdin" do
80+
output = `echo '<div>Test</div>' | bundle exec herb lex --json 2>&1`
81+
82+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
83+
json_data = JSON.parse(output)
84+
assert_kind_of Array, json_data
85+
assert(json_data.any? { |token| token["type"] == "TOKEN_HTML_TAG_START" })
86+
end
87+
88+
test "shell: compile with multiple flags and stdin" do
89+
output = `echo '<div><%= x %></div>' | bundle exec herb compile --no-escape --freeze 2>&1`
90+
91+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
92+
assert_includes output, "# frozen_string_literal: true"
93+
assert_includes output, "(x).to_s"
94+
end
95+
96+
test "shell: stdin with file redirection and dash" do
97+
with_temp_file("<article>Content</article>") do |file_path|
98+
output = `bundle exec herb lex - < #{file_path} 2>&1`
99+
100+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
101+
assert_includes output, "TOKEN_HTML_TAG_START"
102+
assert_includes output, "article"
103+
end
104+
end
105+
106+
test "shell: cat pipe to lex" do
107+
with_temp_file("<section>Data</section>") do |file_path|
108+
output = `cat #{file_path} | bundle exec herb lex 2>&1`
109+
110+
assert $CHILD_STATUS.success?, "Command failed with: #{output}"
111+
assert_includes output, "TOKEN_HTML_TAG_START"
112+
assert_includes output, "section"
113+
end
114+
end
115+
end
116+
end

test/engine/cli_test.rb

Lines changed: 181 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ def with_temp_file(content)
3838
file&.unlink
3939
end
4040

41+
def with_stdin(content)
42+
original_stdin = $stdin
43+
$stdin = StringIO.new(content)
44+
yield
45+
ensure
46+
$stdin = original_stdin
47+
end
48+
4149
test "compile valid template" do
4250
template = "<div>Hello <%= name %>!</div>"
4351

@@ -188,13 +196,22 @@ def with_temp_file(content)
188196
end
189197

190198
test "compile no file provided" do
191-
assert_raises(SystemExit) do
192-
Herb::CLI.new(["compile"]).call
193-
end
199+
original_stdin = $stdin
200+
mock_stdin = StringIO.new
201+
def mock_stdin.tty? = true
202+
$stdin = mock_stdin
194203

195-
output = captured_output
196-
assert_includes output, "No file provided"
197-
assert_includes output, "Usage:"
204+
begin
205+
assert_raises(SystemExit) do
206+
Herb::CLI.new(["compile"]).call
207+
end
208+
209+
output = captured_output
210+
assert_includes output, "No file provided"
211+
assert_includes output, "Usage:"
212+
ensure
213+
$stdin = original_stdin
214+
end
198215
end
199216

200217
test "help includes compile command" do
@@ -309,5 +326,163 @@ def with_temp_file(content)
309326
assert_includes output, "Unknown command"
310327
assert_includes output, "compile [file]"
311328
end
329+
330+
test "lex reads from stdin with dash argument" do
331+
template = "<div>Hello</div>"
332+
333+
with_stdin(template) do
334+
Herb::CLI.new(["lex", "-"]).call
335+
336+
output = captured_output
337+
assert_includes output, "TOKEN_HTML_TAG_START"
338+
assert_includes output, "TOKEN_IDENTIFIER"
339+
assert_includes output, "div"
340+
end
341+
end
342+
343+
test "parse reads from stdin with dash argument" do
344+
template = "<div>Hello</div>"
345+
346+
with_stdin(template) do
347+
Herb::CLI.new(["parse", "-"]).call
348+
349+
output = captured_output
350+
assert_includes output, "DocumentNode"
351+
assert_includes output, "HTMLElementNode"
352+
assert_includes output, "div"
353+
end
354+
end
355+
356+
test "compile reads from stdin with dash argument" do
357+
template = "<div><%= name %></div>"
358+
359+
with_stdin(template) do
360+
assert_raises(SystemExit) do
361+
Herb::CLI.new(["compile", "-"]).call
362+
end
363+
364+
output = captured_output
365+
assert_includes output, "_buf = ::String.new"
366+
assert_includes output, "<div>"
367+
assert_includes output, "__herb.h((name))"
368+
end
369+
end
370+
371+
test "ruby reads from stdin with dash argument" do
372+
template = "<div><%= user.name %></div>"
373+
374+
with_stdin(template) do
375+
assert_raises(SystemExit) do
376+
Herb::CLI.new(["ruby", "-"]).call
377+
end
378+
379+
output = captured_output
380+
assert_includes output, "user.name"
381+
end
382+
end
383+
384+
test "html reads from stdin with dash argument" do
385+
template = "<div><%= user.name %></div>"
386+
387+
with_stdin(template) do
388+
assert_raises(SystemExit) do
389+
Herb::CLI.new(["html", "-"]).call
390+
end
391+
392+
output = captured_output
393+
assert_includes output, "<div>"
394+
assert_includes output, "</div>"
395+
end
396+
end
397+
398+
test "stdin with json output" do
399+
template = "<div>Hello</div>"
400+
401+
with_stdin(template) do
402+
Herb::CLI.new(["lex", "-", "--json"]).call
403+
404+
output = captured_output
405+
json_data = JSON.parse(output)
406+
407+
assert_kind_of Array, json_data
408+
assert(json_data.any? { |token| token["type"] == "TOKEN_HTML_TAG_START" })
409+
assert(json_data.any? { |token| token["value"] == "div" })
410+
end
411+
end
412+
413+
test "compile stdin with json output" do
414+
template = "<div><%= title %></div>"
415+
416+
with_stdin(template) do
417+
assert_raises(SystemExit) do
418+
Herb::CLI.new(["compile", "-", "--json"]).call
419+
end
420+
421+
output = captured_output
422+
json_data = JSON.parse(output)
423+
424+
assert_equal true, json_data["success"]
425+
assert_includes json_data["source"], "_buf = ::String.new"
426+
assert_equal "-", json_data["filename"]
427+
end
428+
end
429+
430+
test "compile stdin with options" do
431+
template = "<div><%= user_input %></div>"
432+
433+
with_stdin(template) do
434+
assert_raises(SystemExit) do
435+
Herb::CLI.new(["compile", "-", "--no-escape", "--freeze"]).call
436+
end
437+
438+
output = captured_output
439+
assert_includes output, "# frozen_string_literal: true"
440+
assert_includes output, "(user_input).to_s"
441+
refute_includes output, "__herb.h("
442+
end
443+
end
444+
445+
test "render reads from stdin" do
446+
template = "<div>Rendered content</div>"
447+
448+
with_stdin(template) do
449+
assert_raises(SystemExit) do
450+
Herb::CLI.new(["render", "-"]).call
451+
end
452+
453+
output = captured_output
454+
assert_includes output, "<div>Rendered content</div>"
455+
end
456+
end
457+
458+
test "help includes stdin documentation" do
459+
assert_raises(SystemExit) do
460+
Herb::CLI.new(["help"]).call
461+
end
462+
463+
output = captured_output
464+
assert_includes output, "stdin:"
465+
assert_includes output, "echo"
466+
assert_includes output, "cat"
467+
end
468+
469+
test "no file provided message includes stdin hint" do
470+
original_stdin = $stdin
471+
mock_stdin = StringIO.new
472+
def mock_stdin.tty? = true
473+
$stdin = mock_stdin
474+
475+
begin
476+
assert_raises(SystemExit) do
477+
Herb::CLI.new(["compile"]).call
478+
end
479+
480+
output = captured_output
481+
assert_includes output, "No file provided"
482+
assert_includes output, "pipe content via stdin"
483+
ensure
484+
$stdin = original_stdin
485+
end
486+
end
312487
end
313488
end

0 commit comments

Comments
 (0)