yap-shell-parser 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +3 -0
- data/bin/compile_debug_parser +3 -0
- data/bin/compile_parser +3 -0
- data/lib/tasks/gem.rake +60 -0
- data/lib/yap/shell.rb +2 -0
- data/lib/yap/shell/parser.rb +381 -0
- data/lib/yap/shell/parser/grammar.y +151 -0
- data/lib/yap/shell/parser/lexer.rb +311 -0
- data/lib/yap/shell/parser/nodes.rb +205 -0
- data/lib/yap/shell/parser/version.rb +5 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/yap/shell/lexer_spec.rb +697 -0
- data/yap-shell-parser.gemspec +25 -0
- metadata +109 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
module Yap::Shell
|
2
|
+
module Parser::Nodes
|
3
|
+
module Visitor
|
4
|
+
def accept(visitor, *args)
|
5
|
+
visitor.send "visit_#{self.class.name.split("::").last}", self, *args
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class AssignmentNode
|
10
|
+
include Visitor
|
11
|
+
|
12
|
+
attr_reader :lvalue, :rvalue
|
13
|
+
|
14
|
+
def initialize(lvalue, rvalue)
|
15
|
+
@lvalue, @rvalue = lvalue, rvalue
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"A(#{lvalue.inspect}=#{rvalue.inspect})"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class CommandNode
|
28
|
+
include Visitor
|
29
|
+
|
30
|
+
attr_reader :command, :args
|
31
|
+
attr_accessor :heredoc, :redirects
|
32
|
+
|
33
|
+
def initialize(command, *args, literal:false, heredoc:nil)
|
34
|
+
@command = command
|
35
|
+
@args = args.flatten
|
36
|
+
@literal = literal
|
37
|
+
@heredoc = nil
|
38
|
+
@redirects = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def literal?
|
42
|
+
@literal
|
43
|
+
end
|
44
|
+
|
45
|
+
def heredoc?
|
46
|
+
@heredoc
|
47
|
+
end
|
48
|
+
|
49
|
+
def internally_evaluate?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
"CommandNode(#{@command}, args: #{@args}, literal:#{literal?}, heredoc: #{heredoc?}, redirects: #{redirects})"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class EnvNode
|
63
|
+
include Visitor
|
64
|
+
|
65
|
+
attr_reader :env
|
66
|
+
|
67
|
+
def initialize(lvalue, rvalue)
|
68
|
+
@env = {}
|
69
|
+
@env[lvalue] = rvalue
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_var(lvalue, rvalue)
|
73
|
+
@env[lvalue] = rvalue
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class EnvWrapperNode
|
78
|
+
include Visitor
|
79
|
+
|
80
|
+
attr_reader :node
|
81
|
+
|
82
|
+
def initialize(env, node)
|
83
|
+
@env = env
|
84
|
+
@node = node
|
85
|
+
end
|
86
|
+
|
87
|
+
def env
|
88
|
+
@env.env
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class StatementsNode
|
93
|
+
include Visitor
|
94
|
+
|
95
|
+
attr_reader :head, :tail
|
96
|
+
|
97
|
+
def initialize(head, tail=nil)
|
98
|
+
if head.is_a?(StatementsNode) && head.tail.nil?
|
99
|
+
@head = head.head
|
100
|
+
else
|
101
|
+
@head = head
|
102
|
+
end
|
103
|
+
@tail = tail
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_s(indent:0)
|
107
|
+
<<-EOT.gsub(/^\s+\|/, '')
|
108
|
+
| #{' ' * indent}StatementsNode(
|
109
|
+
| #{' ' * indent} #{@head.to_s},
|
110
|
+
| #{' ' * indent} #{@tail.to_s})
|
111
|
+
EOT
|
112
|
+
end
|
113
|
+
|
114
|
+
def inspect
|
115
|
+
to_s
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class ConditionalNode
|
120
|
+
include Visitor
|
121
|
+
|
122
|
+
attr_reader :operator, :expr1, :expr2
|
123
|
+
|
124
|
+
def initialize(operator, expr1, expr2)
|
125
|
+
@operator = operator
|
126
|
+
@expr1 = expr1
|
127
|
+
@expr2 = expr2
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s
|
131
|
+
"ConditionalNode(#{@operator}, #{@expr1}, #{@expr2.to_s})"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class InternalEvalNode
|
136
|
+
include Visitor
|
137
|
+
|
138
|
+
attr_reader :src
|
139
|
+
alias_method :command, :src
|
140
|
+
|
141
|
+
def initialize(src)
|
142
|
+
@src = src
|
143
|
+
end
|
144
|
+
|
145
|
+
def args
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
def heredoc
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def internally_evaluate?
|
154
|
+
true
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_s
|
158
|
+
"InternalEvalNode(#{@src.inspect})"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class PipelineNode
|
163
|
+
include Visitor
|
164
|
+
|
165
|
+
attr_reader :head, :tail
|
166
|
+
|
167
|
+
def initialize(head, tail)
|
168
|
+
@head = head
|
169
|
+
@tail = tail
|
170
|
+
end
|
171
|
+
|
172
|
+
def to_s(indent:0)
|
173
|
+
<<-EOT.gsub(/^\s+\|/, '')
|
174
|
+
| #{' ' * indent}PipelineNode(
|
175
|
+
| #{' ' * indent} #{@head.to_s},
|
176
|
+
| #{' ' * indent} #{@tail.to_s})
|
177
|
+
EOT
|
178
|
+
end
|
179
|
+
|
180
|
+
def inspect
|
181
|
+
to_s
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class RedirectionNode
|
186
|
+
include Visitor
|
187
|
+
|
188
|
+
attr_reader :kind, :target
|
189
|
+
|
190
|
+
def initialize(kind, target)
|
191
|
+
@kind = kind
|
192
|
+
@target = target
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_s(indent:0)
|
196
|
+
"RedirectionNode(#{@kind.to_s}, #{@target.to_s})"
|
197
|
+
end
|
198
|
+
|
199
|
+
def inspect
|
200
|
+
to_s
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause this
|
6
|
+
# file to always be loaded, without a need to explicitly require it in any files.
|
7
|
+
#
|
8
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
9
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
10
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
11
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
12
|
+
# a separate helper file that requires the additional dependencies and performs
|
13
|
+
# the additional setup, and require it from the spec files that actually need it.
|
14
|
+
#
|
15
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
16
|
+
# users commonly want.
|
17
|
+
#
|
18
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
19
|
+
RSpec.configure do |config|
|
20
|
+
# rspec-expectations config goes here. You can use an alternate
|
21
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
22
|
+
# assertions if you prefer.
|
23
|
+
config.expect_with :rspec do |expectations|
|
24
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
25
|
+
# and `failure_message` of custom matchers include text for helper methods
|
26
|
+
# defined using `chain`, e.g.:
|
27
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
28
|
+
# # => "be bigger than 2 and smaller than 4"
|
29
|
+
# ...rather than:
|
30
|
+
# # => "be bigger than 2"
|
31
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
32
|
+
end
|
33
|
+
|
34
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
35
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
36
|
+
config.mock_with :rspec do |mocks|
|
37
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
38
|
+
# a real object. This is generally recommended, and will default to
|
39
|
+
# `true` in RSpec 4.
|
40
|
+
mocks.verify_partial_doubles = true
|
41
|
+
end
|
42
|
+
|
43
|
+
# The settings below are suggested to provide a good initial experience
|
44
|
+
# with RSpec, but feel free to customize to your heart's content.
|
45
|
+
=begin
|
46
|
+
# These two settings work together to allow you to limit a spec run
|
47
|
+
# to individual examples or groups you care about by tagging them with
|
48
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
49
|
+
# get run.
|
50
|
+
config.filter_run :focus
|
51
|
+
config.run_all_when_everything_filtered = true
|
52
|
+
|
53
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
54
|
+
# For more details, see:
|
55
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
56
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
57
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
58
|
+
config.disable_monkey_patching!
|
59
|
+
|
60
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
61
|
+
# be too noisy due to issues in dependencies.
|
62
|
+
config.warnings = true
|
63
|
+
|
64
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
65
|
+
# file, and it's useful to allow more verbose output when running an
|
66
|
+
# individual spec file.
|
67
|
+
if config.files_to_run.one?
|
68
|
+
# Use the documentation formatter for detailed output,
|
69
|
+
# unless a formatter has already been configured
|
70
|
+
# (e.g. via a command-line flag).
|
71
|
+
config.default_formatter = 'doc'
|
72
|
+
end
|
73
|
+
|
74
|
+
# Print the 10 slowest examples and example groups at the
|
75
|
+
# end of the spec run, to help surface which specs are running
|
76
|
+
# particularly slow.
|
77
|
+
config.profile_examples = 10
|
78
|
+
|
79
|
+
# Run specs in random order to surface order dependencies. If you find an
|
80
|
+
# order dependency and want to debug it, you can fix the order by providing
|
81
|
+
# the seed, which is printed after each run.
|
82
|
+
# --seed 1234
|
83
|
+
config.order = :random
|
84
|
+
|
85
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
86
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
87
|
+
# test failures related to randomization by passing the same `--seed` value
|
88
|
+
# as the one that triggered the failure.
|
89
|
+
Kernel.srand config.seed
|
90
|
+
=end
|
91
|
+
end
|
@@ -0,0 +1,697 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yap/shell/parser/lexer'
|
3
|
+
|
4
|
+
describe Yap::Shell::Parser::Lexer do
|
5
|
+
subject { described_class.new.tokenize(str) }
|
6
|
+
|
7
|
+
def t(tag, val, lineno:0, attrs:{})
|
8
|
+
[tag, Yap::Shell::Parser::Lexer::Token.new(tag, val, lineno:lineno, attrs:attrs)]
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "empty string" do
|
12
|
+
let(:str){ "" }
|
13
|
+
it { should eq [] }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "env variables" do
|
17
|
+
let(:str){ "foo $baz" }
|
18
|
+
it { should eq [
|
19
|
+
t(:Command, "foo", lineno:0),
|
20
|
+
t(:Argument, "$baz", lineno:0)
|
21
|
+
]}
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "commands" do
|
25
|
+
describe "can begin with periods: .core" do
|
26
|
+
let(:str){ ".core" }
|
27
|
+
it { should eq [
|
28
|
+
t(:Command, ".core", lineno:0)
|
29
|
+
]}
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "can contain periods: ag.core" do
|
33
|
+
let(:str){ "ag.core" }
|
34
|
+
it { should eq [
|
35
|
+
t(:Command, "ag.core", lineno:0)
|
36
|
+
]}
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "can end with periods: core." do
|
40
|
+
let(:str){ "core." }
|
41
|
+
it { should eq [
|
42
|
+
t(:Command, "core.", lineno:0)
|
43
|
+
]}
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "can contain a number: ab4cd" do
|
47
|
+
let(:str){ "ab4cd" }
|
48
|
+
it { should eq [
|
49
|
+
t(:Command, "ab4cd", lineno:0)
|
50
|
+
]}
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "can contain slashes with periods: foo/bar" do
|
54
|
+
let(:str){ "foo/bar" }
|
55
|
+
it { should eq [
|
56
|
+
t(:Command, "foo/bar", lineno:0)
|
57
|
+
]}
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "can contain asterisks: foo/ba*" do
|
61
|
+
let(:str){ "foo/ba*" }
|
62
|
+
it { should eq [
|
63
|
+
t(:Command, "foo/ba*", lineno:0)
|
64
|
+
]}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "literal commands" do
|
69
|
+
describe "begin with the backslash escape: \\rm" do
|
70
|
+
let(:str){ '\rm' }
|
71
|
+
it { should eq [
|
72
|
+
t(:LiteralCommand, "rm", lineno: 0)
|
73
|
+
]}
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "can contain slashes with periods: \\foo/bar" do
|
77
|
+
let(:str){ "\\foo/bar" }
|
78
|
+
it { should eq [
|
79
|
+
t(:LiteralCommand, "foo/bar", lineno:0)
|
80
|
+
]}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "argument parsing" do
|
85
|
+
describe "commands with no args" do
|
86
|
+
let(:str){ "ls" }
|
87
|
+
it { should eq [
|
88
|
+
t(:Command, "ls", lineno:0)
|
89
|
+
]}
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "commands with a simple arg: ls foo" do
|
93
|
+
let(:str){ "ls foo" }
|
94
|
+
it { should eq [
|
95
|
+
t(:Command, "ls", lineno:0),
|
96
|
+
t(:Argument, "foo", lineno:0)
|
97
|
+
]}
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "commands with a simple arg: ls -al" do
|
101
|
+
let(:str){ "ls -al" }
|
102
|
+
it { should eq [
|
103
|
+
t(:Command, "ls", lineno:0),
|
104
|
+
t(:Argument, "-al", lineno:0)
|
105
|
+
]}
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "commands with multiple args: ls -al foo bar -baz" do
|
109
|
+
let(:str){ "ls -al foo bar -baz" }
|
110
|
+
it { should eq [
|
111
|
+
t(:Command, "ls", lineno:0),
|
112
|
+
t(:Argument, "-al", lineno:0),
|
113
|
+
t(:Argument, "foo", lineno:0),
|
114
|
+
t(:Argument, "bar", lineno:0),
|
115
|
+
t(:Argument, "-baz", lineno:0)
|
116
|
+
]}
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "can contain asterisks: ls foo* b*r" do
|
120
|
+
let(:str){ "ls foo* b*r" }
|
121
|
+
it { should eq [
|
122
|
+
t(:Command, "ls", lineno:0),
|
123
|
+
t(:Argument, "foo*", lineno:0),
|
124
|
+
t(:Argument, "b*r", lineno:0),
|
125
|
+
]}
|
126
|
+
end
|
127
|
+
|
128
|
+
context "single quoted args" do
|
129
|
+
describe "simple args: ls 'hello there'" do
|
130
|
+
let(:str){ "ls 'hello there'" }
|
131
|
+
it { should eq [
|
132
|
+
t(:Command, "ls", lineno:0),
|
133
|
+
t(:Argument, "hello there", lineno:0)
|
134
|
+
]}
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "nested single quotes: ls 'hello \\'there\\''" do
|
138
|
+
let(:str){ "ls 'hello \\'there\\''" }
|
139
|
+
it { should eq [
|
140
|
+
t(:Command, "ls", lineno:0),
|
141
|
+
t(:Argument, "hello 'there'", lineno:0)
|
142
|
+
]}
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "nested double quotes: ls 'hello \"there\"'" do
|
146
|
+
let(:str){ %|ls 'hello "there"'| }
|
147
|
+
it { should eq [
|
148
|
+
t(:Command, "ls", lineno:0),
|
149
|
+
t(:Argument, 'hello "there"', lineno:0)
|
150
|
+
]}
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "multiple levels of nested single quotes: ls 'hello \\'there \\\\'guy\\\\' \\''" do
|
154
|
+
let(:str){ "ls 'hello \\'there \\\\'guy\\\\' \\''" }
|
155
|
+
it { should eq [
|
156
|
+
t(:Command, "ls", lineno:0),
|
157
|
+
t(:Argument, "hello 'there \\'guy\\' '", lineno:0)
|
158
|
+
]}
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "multiple single quoted args: ls 'hello \\'there \\'' 'how are \\'you\\'?'" do
|
162
|
+
let(:str){ "ls 'hello \\'there\\'' 'how are \\'you\\'?'" }
|
163
|
+
it { should eq [
|
164
|
+
t(:Command, "ls", lineno:0),
|
165
|
+
t(:Argument, "hello 'there'", lineno:0),
|
166
|
+
t(:Argument, "how are 'you'?", lineno:0)
|
167
|
+
]}
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "single quotes with spaces and assignment" do
|
171
|
+
let(:str){ "alias z='echo hi'" }
|
172
|
+
it { should eq [
|
173
|
+
t(:Command, "alias", lineno:0),
|
174
|
+
t(:Argument, "z=echo hi", lineno:0),
|
175
|
+
]}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'double quoted args' do
|
180
|
+
describe 'simple args: ls "hello there"' do
|
181
|
+
let(:str){ 'ls "hello there"' }
|
182
|
+
it { should eq [
|
183
|
+
t(:Command, 'ls', lineno:0),
|
184
|
+
t(:Argument, 'hello there', lineno:0)
|
185
|
+
]}
|
186
|
+
end
|
187
|
+
|
188
|
+
describe 'nested singled quotes: ls "hello \'there\'"' do
|
189
|
+
let(:str){ %|ls "hello 'there'"| }
|
190
|
+
it { should eq [
|
191
|
+
t(:Command, 'ls', lineno:0),
|
192
|
+
t(:Argument, "hello 'there'", lineno:0)
|
193
|
+
]}
|
194
|
+
end
|
195
|
+
|
196
|
+
describe 'nested double quotes: ls "hello \\"there\\""' do
|
197
|
+
let(:str){ 'ls "hello \\"there\\""' }
|
198
|
+
it { should eq [
|
199
|
+
t(:Command, 'ls', lineno:0),
|
200
|
+
t(:Argument, 'hello "there"', lineno:0)
|
201
|
+
]}
|
202
|
+
end
|
203
|
+
|
204
|
+
describe 'multiple levels of nested double quotes: ls "hello \\"there \\\\"guy\\\\" \\""' do
|
205
|
+
let(:str){ 'ls "hello \\"there \\\\"guy\\\\" \\""' }
|
206
|
+
it { should eq [
|
207
|
+
t(:Command, 'ls', lineno:0),
|
208
|
+
t(:Argument, 'hello "there \\"guy\\" "', lineno:0)
|
209
|
+
]}
|
210
|
+
end
|
211
|
+
|
212
|
+
describe 'multiple double quoted args: ls "hello \\"there \\"" "how are \\"you\\"?"' do
|
213
|
+
let(:str){ 'ls "hello \\"there\\"" "how are \\"you\\"?"' }
|
214
|
+
it { should eq [
|
215
|
+
t(:Command, 'ls', lineno:0),
|
216
|
+
t(:Argument, 'hello "there"', lineno:0),
|
217
|
+
t(:Argument, 'how are "you"?', lineno:0)
|
218
|
+
]}
|
219
|
+
end
|
220
|
+
|
221
|
+
describe "single quotes with spaces and assignment" do
|
222
|
+
let(:str){ "alias z=\"echo hi\"" }
|
223
|
+
it { should eq [
|
224
|
+
t(:Command, "alias", lineno:0),
|
225
|
+
t(:Argument, "z=echo hi", lineno:0),
|
226
|
+
]}
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context "statements" do
|
232
|
+
["foo;baz", "foo; baz", "foo ;baz", "foo ; baz", "foo ; baz"].each do |src|
|
233
|
+
describe "are separated by a semi-colon: #{src.inspect}" do
|
234
|
+
let(:str){ src }
|
235
|
+
it { should eq [
|
236
|
+
t(:Command, "foo", lineno:0),
|
237
|
+
t(:Separator, ";", lineno:0),
|
238
|
+
t(:Command, "baz", lineno:0)
|
239
|
+
]}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
["foo|baz", "foo| baz", "foo |baz", "foo | baz", "foo | baz"].each do |src|
|
244
|
+
describe "are separated by a semi-colon: #{src.inspect}" do
|
245
|
+
let(:str){ src }
|
246
|
+
it { should eq [
|
247
|
+
t(:Command, "foo", lineno:0),
|
248
|
+
t(:Pipe, "|", lineno:0),
|
249
|
+
t(:Command, "baz", lineno:0)
|
250
|
+
]}
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
["foo&&baz", "foo&& baz", "foo &&baz", "foo && baz", "foo && baz"].each do |src|
|
255
|
+
describe "are separated by double ampersands: #{src.inspect}" do
|
256
|
+
let(:str){ src }
|
257
|
+
it { should eq [
|
258
|
+
t(:Command, "foo", lineno:0),
|
259
|
+
t(:Conditional, "&&", lineno:0),
|
260
|
+
t(:Command, "baz", lineno:0)
|
261
|
+
]}
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
["foo||baz", "foo|| baz", "foo ||baz", "foo || baz", "foo || baz"].each do |src|
|
266
|
+
describe "are separated by double ampersands: #{src.inspect}" do
|
267
|
+
let(:str){ src }
|
268
|
+
it { should eq [
|
269
|
+
t(:Command, "foo", lineno:0),
|
270
|
+
t(:Conditional, "||", lineno:0),
|
271
|
+
t(:Command, "baz", lineno:0)
|
272
|
+
]}
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
context "heredocs" do
|
278
|
+
describe "started with double arrows followed by a character: foo <<E" do
|
279
|
+
let(:str){ "foo <<E\nfoo\nbar\nE" }
|
280
|
+
it { should eq [
|
281
|
+
t(:Command, "foo", lineno:0),
|
282
|
+
t(:Heredoc, "foo\nbar\n", lineno:0)
|
283
|
+
]}
|
284
|
+
end
|
285
|
+
|
286
|
+
describe "started with double arrows followed by multiple character: foo <<FOO" do
|
287
|
+
let(:str){ "foo <<FOO\nhere\nwe\ngo\nFOO"}
|
288
|
+
it { should eq [
|
289
|
+
t(:Command, "foo", lineno:0),
|
290
|
+
t(:Heredoc, "here\nwe\ngo\n", lineno:0)
|
291
|
+
]}
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "started with double arrows followed by numbers characters: foo <<12" do
|
295
|
+
let(:str){ "foo <<12\nnumbers\nman\n12" }
|
296
|
+
it { should eq [
|
297
|
+
t(:Command, "foo", lineno:0),
|
298
|
+
t(:Heredoc, "numbers\nman\n", lineno:0)
|
299
|
+
]}
|
300
|
+
end
|
301
|
+
|
302
|
+
describe "started with double arrows followed by a mixture of alphanumeric characters: foo <<L337" do
|
303
|
+
let(:str){ "foo <<L337\nhere\nwe\ngo\nL337"}
|
304
|
+
it { should eq [
|
305
|
+
t(:Command, "foo", lineno:0),
|
306
|
+
t(:Heredoc, "here\nwe\ngo\n", lineno:0)
|
307
|
+
]}
|
308
|
+
end
|
309
|
+
|
310
|
+
describe "started with a <<-: foo <<-L337" do
|
311
|
+
let(:str){ "foo <<-L337\nhere\nwe\ngo\nL337"}
|
312
|
+
it { should eq [
|
313
|
+
t(:Command, "foo", lineno:0),
|
314
|
+
t(:Heredoc, "here\nwe\ngo\n", lineno:0)
|
315
|
+
]}
|
316
|
+
end
|
317
|
+
|
318
|
+
describe "the ending line can have leading whitespace which isn't consumed" do
|
319
|
+
let(:str){ "foo <<L337\nhere\nwe\ngo\n \t L337"}
|
320
|
+
it { should eq [
|
321
|
+
t(:Command, "foo", lineno:0),
|
322
|
+
t(:Heredoc, "here\nwe\ngo\n", lineno:0)
|
323
|
+
]}
|
324
|
+
end
|
325
|
+
|
326
|
+
describe "the ending line can have trailing whitespace which isn't consumed" do
|
327
|
+
let(:str){ "foo <<L337\nhere\nwe\ngo\nL337 \t "}
|
328
|
+
it { should eq [
|
329
|
+
t(:Command, "foo", lineno:0),
|
330
|
+
t(:Heredoc, "here\nwe\ngo\n", lineno:0)
|
331
|
+
]}
|
332
|
+
end
|
333
|
+
|
334
|
+
describe "the ending line can be followed by a newline" do
|
335
|
+
let(:str){ "foo <<L337\nhere\nwe\ngo\nL337\n"}
|
336
|
+
it { should eq [
|
337
|
+
t(:Command, "foo", lineno:0),
|
338
|
+
t(:Heredoc, "here\nwe\ngo\n", lineno:0)
|
339
|
+
]}
|
340
|
+
end
|
341
|
+
|
342
|
+
describe "the ending line cannot have non-whitespace characters beyond the delimiter" do
|
343
|
+
let(:str){ "foo <<L337\nhere\nwe\ngo\n this is bad L337"}
|
344
|
+
it "raises an error" do
|
345
|
+
expect{ subject }.to raise_error
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context "internal evaluations" do
|
351
|
+
describe "started by a exclamation point: !to_s" do
|
352
|
+
let(:str){ "!to_s" }
|
353
|
+
it { should eq [
|
354
|
+
t(:InternalEval, "to_s", lineno:0)
|
355
|
+
]}
|
356
|
+
end
|
357
|
+
|
358
|
+
describe "keeps strings" do
|
359
|
+
let(:str){ "!Dir.chdir('..')" }
|
360
|
+
it { should eq [
|
361
|
+
t(:InternalEval, "Dir.chdir('..')", lineno:0)
|
362
|
+
]}
|
363
|
+
end
|
364
|
+
|
365
|
+
describe "can handle quoted strings: !a + 'b' + \"c\" + d" do
|
366
|
+
let(:str){ "!a + 'b' + \"c\" + d" }
|
367
|
+
it { should eq [
|
368
|
+
t(:InternalEval, "a + 'b' + \"c\" + d", lineno:0)
|
369
|
+
]}
|
370
|
+
end
|
371
|
+
|
372
|
+
describe "can handle {/} blocks: !foo.map{ |bar| bar + 1 }" do
|
373
|
+
let(:str){ "!foo.map{ |bar| bar + 1 }" }
|
374
|
+
it { should eq [
|
375
|
+
t(:InternalEval, "foo.map{ |bar| bar + 1 }", lineno:0)
|
376
|
+
]}
|
377
|
+
end
|
378
|
+
|
379
|
+
describe "can handle parentheses in method calls: !foo.map(&:bar)" do
|
380
|
+
let(:str){ "!foo.map(&:bar)" }
|
381
|
+
it { should eq [
|
382
|
+
t(:InternalEval, "foo.map(&:bar)", lineno:0)
|
383
|
+
]}
|
384
|
+
end
|
385
|
+
|
386
|
+
describe "doesn't consume beyond a semi-colon terminator into other commands: !foo.map(&:bar) ; grep fox" do
|
387
|
+
let(:str){ "!foo.map(&:bar) ; grep fox" }
|
388
|
+
it { should eq [
|
389
|
+
t(:InternalEval, "foo.map(&:bar)", lineno:0),
|
390
|
+
t(:Separator, ";", lineno:0),
|
391
|
+
t(:Command, "grep", lineno:0),
|
392
|
+
t(:Argument, "fox", lineno:0)
|
393
|
+
]}
|
394
|
+
end
|
395
|
+
|
396
|
+
describe "doesn't consume beyond a pipe terminator into other commands: !foo.map(&:bar) | grep fox" do
|
397
|
+
let(:str){ "!foo.map(&:bar) | grep fox" }
|
398
|
+
it { should eq [
|
399
|
+
t(:InternalEval, "foo.map(&:bar)", lineno:0),
|
400
|
+
t(:Pipe, "|", lineno:0),
|
401
|
+
t(:Command, "grep", lineno:0),
|
402
|
+
t(:Argument, "fox", lineno:0)
|
403
|
+
]}
|
404
|
+
end
|
405
|
+
|
406
|
+
describe "doesn't consume beyond a pipe terminator into other commands: !downcase | sleep 4" do
|
407
|
+
let(:str){ "!downcase | sleep 4" }
|
408
|
+
it { should eq [
|
409
|
+
t(:InternalEval, "downcase", lineno:0),
|
410
|
+
t(:Pipe, "|", lineno:0),
|
411
|
+
t(:Command, "sleep", lineno:0),
|
412
|
+
t(:Argument, "4", lineno:0)
|
413
|
+
]}
|
414
|
+
end
|
415
|
+
|
416
|
+
describe "doesn't consume beyond a double-ampersand terminator into other commands: !foo.map(&:bar) && grep fox" do
|
417
|
+
let(:str){ "!foo.map(&:bar) && grep fox" }
|
418
|
+
it { should eq [
|
419
|
+
t(:InternalEval, "foo.map(&:bar)", lineno:0),
|
420
|
+
t(:Conditional, "&&", lineno:0),
|
421
|
+
t(:Command, "grep", lineno:0),
|
422
|
+
t(:Argument, "fox", lineno:0)
|
423
|
+
]}
|
424
|
+
end
|
425
|
+
|
426
|
+
describe "doesn't consume beyond a double-pipe terminator into other commands: !foo.map(&:bar) || grep fox" do
|
427
|
+
let(:str){ "!foo.map(&:bar) || grep fox" }
|
428
|
+
it { should eq [
|
429
|
+
t(:InternalEval, "foo.map(&:bar)", lineno:0),
|
430
|
+
t(:Conditional, "||", lineno:0),
|
431
|
+
t(:Command, "grep", lineno:0),
|
432
|
+
t(:Argument, "fox", lineno:0)
|
433
|
+
]}
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
describe 'grouping statements' do
|
438
|
+
describe 'simple one command: (bar)' do
|
439
|
+
let(:str){ "(bar)" }
|
440
|
+
it { should eq [
|
441
|
+
t('(', '(', lineno:0),
|
442
|
+
t(:Command, "bar", lineno:0),
|
443
|
+
t(')', ')', lineno:0)
|
444
|
+
]}
|
445
|
+
end
|
446
|
+
|
447
|
+
describe 'simple interval eval: (!bar)' do
|
448
|
+
let(:str){ "(!bar)" }
|
449
|
+
it { should eq [
|
450
|
+
t('(', '(', lineno:0),
|
451
|
+
t(:InternalEval, "bar", lineno:0),
|
452
|
+
t(')', ')', lineno:0)
|
453
|
+
]}
|
454
|
+
end
|
455
|
+
|
456
|
+
describe 'complicated interval eval: (!foo.map(&:bar).map{ |fasdf| baz })' do
|
457
|
+
let(:str){ "(!foo.map(&:bar).map{ |fasdf| baz })" }
|
458
|
+
it { should eq [
|
459
|
+
t('(', '(', lineno:0),
|
460
|
+
t(:InternalEval, "foo.map(&:bar).map{ |fasdf| baz }", lineno:0),
|
461
|
+
t(')', ')', lineno:0)
|
462
|
+
]}
|
463
|
+
end
|
464
|
+
|
465
|
+
describe 'multiple commands: (bar ; baz && foo | yep' do
|
466
|
+
let(:str){ '(bar ; baz && foo | yep)' }
|
467
|
+
it { should eq [
|
468
|
+
t('(', '(', lineno:0),
|
469
|
+
t(:Command, "bar", lineno:0),
|
470
|
+
t(:Separator, ";", lineno:0),
|
471
|
+
t(:Command, "baz", lineno:0),
|
472
|
+
t(:Conditional, "&&", lineno:0),
|
473
|
+
t(:Command, "foo", lineno:0),
|
474
|
+
t(:Pipe, "|", lineno:0),
|
475
|
+
t(:Command, "yep", lineno:0),
|
476
|
+
t(')', ')', lineno:0)
|
477
|
+
]}
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
describe "redirections" do
|
482
|
+
describe "stdin" do
|
483
|
+
describe "can come after the command with no spaces: foo<bar.txt" do
|
484
|
+
let(:str){ "foo<bar.txt" }
|
485
|
+
it { should eq [
|
486
|
+
t(:Command, "foo", lineno: 0),
|
487
|
+
t(:Redirection, "<", lineno: 0, attrs: { target: "bar.txt" }),
|
488
|
+
]}
|
489
|
+
end
|
490
|
+
|
491
|
+
describe "can come after the command with spaces after the command: foo <bar.txt" do
|
492
|
+
let(:str){ "foo <bar.txt" }
|
493
|
+
it { should eq [
|
494
|
+
t(:Command, "foo", lineno: 0),
|
495
|
+
t(:Redirection, "<", lineno: 0, attrs: { target: "bar.txt" }),
|
496
|
+
]}
|
497
|
+
end
|
498
|
+
|
499
|
+
describe "can come after the command with spaces after the redirect: foo < /path/to/bar.txt" do
|
500
|
+
let(:str){ "foo < /path/to/bar.txt" }
|
501
|
+
it { should eq [
|
502
|
+
t(:Command, "foo", lineno: 0),
|
503
|
+
t(:Redirection, "<", lineno: 0, attrs: { target: "/path/to/bar.txt" }),
|
504
|
+
]}
|
505
|
+
end
|
506
|
+
|
507
|
+
describe "can come after command arguments: ls -al < a.txt" do
|
508
|
+
let(:str){ "ls -al < a.txt" }
|
509
|
+
it { should eq [
|
510
|
+
t(:Command, "ls", lineno: 0),
|
511
|
+
t(:Argument, "-al", lineno: 0),
|
512
|
+
t(:Redirection, "<", lineno: 0, attrs: { target: "a.txt" }),
|
513
|
+
]}
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
describe "stdout" do
|
518
|
+
describe "can come after the command with no spaces" do
|
519
|
+
let(:str){ "foo>bar.txt" }
|
520
|
+
it { should eq [
|
521
|
+
t(:Command, "foo", lineno: 0),
|
522
|
+
t(:Redirection, ">", lineno: 0, attrs: { target: "bar.txt" }),
|
523
|
+
]}
|
524
|
+
end
|
525
|
+
|
526
|
+
describe "can come after the command with spaces after the command" do
|
527
|
+
let(:str){ "foo >bar.txt" }
|
528
|
+
it { should eq [
|
529
|
+
t(:Command, "foo", lineno: 0),
|
530
|
+
t(:Redirection, ">", lineno: 0, attrs: { target: "bar.txt" }),
|
531
|
+
]}
|
532
|
+
end
|
533
|
+
|
534
|
+
describe "can come after the command with spaces after the redirect" do
|
535
|
+
let(:str){ "foo > bar.txt" }
|
536
|
+
it { should eq [
|
537
|
+
t(:Command, "foo", lineno: 0),
|
538
|
+
t(:Redirection, ">", lineno: 0, attrs: { target: "bar.txt" }),
|
539
|
+
]}
|
540
|
+
end
|
541
|
+
|
542
|
+
describe "can be specified numerically: 1>" do
|
543
|
+
let(:str){ "foo 1> bar.txt" }
|
544
|
+
it { should eq [
|
545
|
+
t(:Command, "foo", lineno: 0),
|
546
|
+
t(:Redirection, "1>", lineno: 0, attrs: { target: "bar.txt" }),
|
547
|
+
]}
|
548
|
+
end
|
549
|
+
|
550
|
+
describe "can come after command arguments" do
|
551
|
+
let(:str){ "ls -al > a.txt" }
|
552
|
+
it { should eq [
|
553
|
+
t(:Command, "ls", lineno: 0),
|
554
|
+
t(:Argument, "-al", lineno: 0),
|
555
|
+
t(:Redirection, ">", lineno: 0, attrs: { target: "a.txt" }),
|
556
|
+
]}
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
describe "stderr" do
|
561
|
+
describe "without spaces after the command it cannot be redirected" do
|
562
|
+
let(:str){ "foo2>bar.txt" }
|
563
|
+
it { should eq [
|
564
|
+
t(:Command, "foo2", lineno: 0),
|
565
|
+
t(:Redirection, ">", lineno: 0, attrs: { target: "bar.txt" }),
|
566
|
+
]}
|
567
|
+
end
|
568
|
+
|
569
|
+
describe "it comes after the command with spaces after the command" do
|
570
|
+
let(:str){ "foo 2>bar.txt" }
|
571
|
+
it { should eq [
|
572
|
+
t(:Command, "foo", lineno: 0),
|
573
|
+
t(:Redirection, "2>", lineno: 0, attrs: { target: "bar.txt" }),
|
574
|
+
]}
|
575
|
+
end
|
576
|
+
|
577
|
+
describe "it comes after the command with spaces after the redirect" do
|
578
|
+
let(:str){ "foo 2> bar.txt" }
|
579
|
+
it { should eq [
|
580
|
+
t(:Command, "foo", lineno: 0),
|
581
|
+
t(:Redirection, "2>", lineno: 0, attrs: { target: "bar.txt" }),
|
582
|
+
]}
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
describe "stdout / stderr" do
|
587
|
+
describe "stdout redirecting to stderr: foo 1>&2" do
|
588
|
+
let(:str){ "foo 1>&2" }
|
589
|
+
it { should eq [
|
590
|
+
t(:Command, "foo", lineno: 0),
|
591
|
+
t(:Redirection, "1>&2", lineno: 0, attrs: { target: nil }),
|
592
|
+
]}
|
593
|
+
end
|
594
|
+
|
595
|
+
describe "stderr redirecting to stdout: foo 2>&1" do
|
596
|
+
let(:str){ "foo 2>&1" }
|
597
|
+
it { should eq [
|
598
|
+
t(:Command, "foo", lineno: 0),
|
599
|
+
t(:Redirection, "2>&1", lineno: 0, attrs: { target: nil }),
|
600
|
+
]}
|
601
|
+
end
|
602
|
+
|
603
|
+
describe "stdout redirecting to stderr with a file: foo 1>&2 bar.txt (bash incompatible)" do
|
604
|
+
let(:str){ "foo 1>&2 bar.txt" }
|
605
|
+
it { should eq [
|
606
|
+
t(:Command, "foo", lineno: 0),
|
607
|
+
# TODO: This is bash incompatible. Keep it?
|
608
|
+
t(:Redirection, "1>&2", lineno: 0, attrs: { target: "bar.txt" })
|
609
|
+
]}
|
610
|
+
end
|
611
|
+
|
612
|
+
describe "stderr redirecting to stdout with a file: foo 2>&1 bar.txt (bash incompatible)" do
|
613
|
+
let(:str){ "foo 2>&1 bar.txt" }
|
614
|
+
it { should eq [
|
615
|
+
t(:Command, "foo", lineno: 0),
|
616
|
+
# TODO: This is bash incompatible. Keep it?
|
617
|
+
t(:Redirection, "2>&1", lineno: 0, attrs: { target: "bar.txt" })
|
618
|
+
]}
|
619
|
+
end
|
620
|
+
|
621
|
+
describe "stdout and stderr redirecting to a file together: foo &> /dev/null" do
|
622
|
+
let(:str){ "foo &> /dev/null" }
|
623
|
+
it { should eq [
|
624
|
+
t(:Command, "foo", lineno: 0),
|
625
|
+
t(:Redirection, "&>", lineno: 0, attrs: { target: "/dev/null" })
|
626
|
+
]}
|
627
|
+
end
|
628
|
+
|
629
|
+
describe "stdout and sdterr redirecting separately: foo 2> err.txt 1> out.txt" do
|
630
|
+
let(:str){ "foo 2> err.txt 1> out.txt" }
|
631
|
+
it { should eq [
|
632
|
+
t(:Command, "foo", lineno: 0),
|
633
|
+
t(:Redirection, "2>", lineno: 0, attrs: { target: "err.txt" }),
|
634
|
+
t(:Redirection, "1>", lineno: 0, attrs: { target: "out.txt" })
|
635
|
+
]}
|
636
|
+
end
|
637
|
+
|
638
|
+
describe "stdout and sdterr redirecting separately: du -sh 1>&2 1>hey.txt" do
|
639
|
+
let(:str){ "du -sh 2>&1 1>hey.txt" }
|
640
|
+
it { should eq [
|
641
|
+
t(:Command, "du", lineno: 0),
|
642
|
+
t(:Argument, "-sh", lineno: 0),
|
643
|
+
t(:Redirection, "2>&1", lineno: 0, attrs: { target: nil }),
|
644
|
+
t(:Redirection, "1>", lineno: 0, attrs: { target: "hey.txt" })
|
645
|
+
]}
|
646
|
+
end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
describe "variables" do
|
651
|
+
describe "one can be assigned its their own: FOO=123" do
|
652
|
+
let(:str){ "FOO=123" }
|
653
|
+
it { should eq [
|
654
|
+
t(:LValue, "FOO", lineno: 0),
|
655
|
+
t(:RValue, "123", lineno: 0)
|
656
|
+
]}
|
657
|
+
end
|
658
|
+
|
659
|
+
describe "many can be assigned on their own: FOO=123 BAR=a_c BAZ=4-5:6" do
|
660
|
+
let(:str){ "FOO=123 BAR=a_c BAZ=4-5:6" }
|
661
|
+
it { should eq [
|
662
|
+
t(:LValue, "FOO", lineno: 0),
|
663
|
+
t(:RValue, "123", lineno: 0),
|
664
|
+
t(:LValue, "BAR", lineno: 0),
|
665
|
+
t(:RValue, "a_c", lineno: 0),
|
666
|
+
t(:LValue, "BAZ", lineno: 0),
|
667
|
+
t(:RValue, "4-5:6", lineno: 0)
|
668
|
+
]}
|
669
|
+
end
|
670
|
+
|
671
|
+
describe "can be assigned before a command: FOO=123 echo $FOO" do
|
672
|
+
let(:str){ "FOO=123 echo $FOO" }
|
673
|
+
it { should eq [
|
674
|
+
t(:LValue, "FOO", lineno: 0),
|
675
|
+
t(:RValue, "123", lineno: 0),
|
676
|
+
t(:Command, "echo", lineno: 0),
|
677
|
+
t(:Argument, "$FOO", lineno: 0)
|
678
|
+
]}
|
679
|
+
end
|
680
|
+
|
681
|
+
describe "can be assigned before a command: FOO=abc BAR='hello world' ls -l" do
|
682
|
+
let(:str){ "FOO=abc BAR='hello world' ls -l" }
|
683
|
+
it { should eq [
|
684
|
+
t(:LValue, "FOO", lineno: 0),
|
685
|
+
t(:RValue, "abc", lineno: 0),
|
686
|
+
t(:LValue, "BAR", lineno: 0),
|
687
|
+
t(:RValue, "hello world", lineno: 0),
|
688
|
+
t(:Command, "ls", lineno: 0),
|
689
|
+
t(:Argument, "-l", lineno: 0)
|
690
|
+
]}
|
691
|
+
end
|
692
|
+
|
693
|
+
|
694
|
+
|
695
|
+
end
|
696
|
+
|
697
|
+
end
|