shell_parser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +330 -0
- data/examples/demo_simplified.rb +89 -0
- data/examples/demo_structure.rb +116 -0
- data/examples/examples.rb +166 -0
- data/examples/test.rb +39 -0
- data/examples/test_structure.rb +134 -0
- data/lib/shell_parser/version.rb +5 -0
- data/lib/shell_parser.rb +523 -0
- metadata +85 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/shell_parser'
|
|
5
|
+
|
|
6
|
+
# Example 1: Syntax Highlighting
|
|
7
|
+
# The parser preserves position information for precise highlighting
|
|
8
|
+
def syntax_highlight(input)
|
|
9
|
+
puts "=" * 60
|
|
10
|
+
puts "SYNTAX HIGHLIGHTING EXAMPLE"
|
|
11
|
+
puts "=" * 60
|
|
12
|
+
puts "Input: #{input.inspect}\n\n"
|
|
13
|
+
|
|
14
|
+
lexer = ShellParser::Lexer.new(input)
|
|
15
|
+
tokens = lexer.tokenize
|
|
16
|
+
|
|
17
|
+
puts "Tokens:"
|
|
18
|
+
tokens.each do |token|
|
|
19
|
+
next if token.type == :eof
|
|
20
|
+
|
|
21
|
+
color = case token.type
|
|
22
|
+
when :word, :word_with_quotes then "\e[37m" # white
|
|
23
|
+
when :pipe then "\e[35m" # magenta
|
|
24
|
+
when :and_if, :or_if then "\e[33m" # yellow
|
|
25
|
+
when :semi, :background then "\e[36m" # cyan
|
|
26
|
+
when :less, :great, :dgreat, :dless then "\e[32m" # green
|
|
27
|
+
else "\e[90m" # gray
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
reset = "\e[0m"
|
|
31
|
+
puts " #{color}#{token.type.to_s.ljust(15)}#{reset} #{token.value.inspect.ljust(20)} pos=#{token.pos} len=#{token.len}"
|
|
32
|
+
end
|
|
33
|
+
puts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Example 2: Shell Execution Helper
|
|
37
|
+
# The AST makes it easy to traverse and execute commands
|
|
38
|
+
def execute_ast(ast, depth = 0)
|
|
39
|
+
indent = " " * depth
|
|
40
|
+
|
|
41
|
+
case ast
|
|
42
|
+
when ShellParser::Command
|
|
43
|
+
puts "#{indent}Execute command:"
|
|
44
|
+
puts "#{indent} Command: #{ast.words.map(&:to_s).join(' ')}"
|
|
45
|
+
ast.redirects.each do |redir|
|
|
46
|
+
puts "#{indent} Redirect: #{redir.type} -> #{redir.target.to_s}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
when ShellParser::Pipeline
|
|
50
|
+
puts "#{indent}Execute pipeline (#{ast.commands.length} commands):"
|
|
51
|
+
ast.commands.each_with_index do |cmd, i|
|
|
52
|
+
puts "#{indent} Stage #{i + 1}:"
|
|
53
|
+
execute_ast(cmd, depth + 2)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
when ShellParser::List
|
|
57
|
+
op_name = { and: '&&', or: '||', semi: ';', background: '&' }[ast.op]
|
|
58
|
+
puts "#{indent}Execute list (operator: #{op_name}):"
|
|
59
|
+
puts "#{indent} Left side:"
|
|
60
|
+
execute_ast(ast.left, depth + 2)
|
|
61
|
+
if ast.right
|
|
62
|
+
puts "#{indent} Right side:"
|
|
63
|
+
execute_ast(ast.right, depth + 2)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def demo_execution(input)
|
|
69
|
+
puts "=" * 60
|
|
70
|
+
puts "EXECUTION EXAMPLE"
|
|
71
|
+
puts "=" * 60
|
|
72
|
+
puts "Input: #{input.inspect}\n\n"
|
|
73
|
+
|
|
74
|
+
ast = ShellParser.parse(input)
|
|
75
|
+
puts "AST Structure:"
|
|
76
|
+
puts ast.inspect
|
|
77
|
+
puts "\nExecution Plan:"
|
|
78
|
+
execute_ast(ast)
|
|
79
|
+
puts
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Example 3: AST Inspection
|
|
83
|
+
def inspect_ast(input)
|
|
84
|
+
puts "=" * 60
|
|
85
|
+
puts "AST INSPECTION"
|
|
86
|
+
puts "=" * 60
|
|
87
|
+
puts "Input: #{input.inspect}\n\n"
|
|
88
|
+
|
|
89
|
+
ast = ShellParser.parse(input)
|
|
90
|
+
|
|
91
|
+
def pretty_print(node, depth = 0)
|
|
92
|
+
indent = " " * depth
|
|
93
|
+
case node
|
|
94
|
+
when ShellParser::Word
|
|
95
|
+
"#{indent}Word: #{node.to_s.inspect} (#{node.parts.length} parts)"
|
|
96
|
+
when ShellParser::Command
|
|
97
|
+
result = "#{indent}Command:\n"
|
|
98
|
+
result += "#{indent} words: [\n"
|
|
99
|
+
node.words.each { |w| result += "#{pretty_print(w, depth + 2)}\n" }
|
|
100
|
+
result += "#{indent} ]\n"
|
|
101
|
+
unless node.redirects.empty?
|
|
102
|
+
result += "#{indent} redirects: [\n"
|
|
103
|
+
node.redirects.each { |r| result += "#{indent} #{r.type} -> #{r.target.to_s}\n" }
|
|
104
|
+
result += "#{indent} ]\n"
|
|
105
|
+
end
|
|
106
|
+
result
|
|
107
|
+
when ShellParser::Pipeline
|
|
108
|
+
result = "#{indent}Pipeline:\n"
|
|
109
|
+
node.commands.each_with_index do |cmd, i|
|
|
110
|
+
result += "#{indent} [#{i}]:\n#{pretty_print(cmd, depth + 2)}"
|
|
111
|
+
end
|
|
112
|
+
result
|
|
113
|
+
when ShellParser::List
|
|
114
|
+
result = "#{indent}List(#{node.op}):\n"
|
|
115
|
+
result += "#{indent} left:\n#{pretty_print(node.left, depth + 2)}"
|
|
116
|
+
if node.right
|
|
117
|
+
result += "#{indent} right:\n#{pretty_print(node.right, depth + 2)}"
|
|
118
|
+
end
|
|
119
|
+
result
|
|
120
|
+
else
|
|
121
|
+
"#{indent}#{node.inspect}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
puts pretty_print(ast)
|
|
126
|
+
puts
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Run examples
|
|
130
|
+
puts "\n"
|
|
131
|
+
|
|
132
|
+
# Simple command
|
|
133
|
+
syntax_highlight("ls -la /tmp")
|
|
134
|
+
demo_execution("ls -la /tmp")
|
|
135
|
+
|
|
136
|
+
# Pipeline
|
|
137
|
+
syntax_highlight("cat file.txt | grep error | wc -l")
|
|
138
|
+
demo_execution("cat file.txt | grep error | wc -l")
|
|
139
|
+
|
|
140
|
+
# Command with redirections
|
|
141
|
+
syntax_highlight("echo hello > output.txt")
|
|
142
|
+
inspect_ast("echo hello > output.txt")
|
|
143
|
+
|
|
144
|
+
# Complex list with && and ||
|
|
145
|
+
syntax_highlight("make clean && make build || echo 'Build failed'")
|
|
146
|
+
demo_execution("make clean && make build || echo 'Build failed'")
|
|
147
|
+
|
|
148
|
+
# Background job
|
|
149
|
+
syntax_highlight("sleep 10 &")
|
|
150
|
+
demo_execution("sleep 10 &")
|
|
151
|
+
|
|
152
|
+
# Quoted strings
|
|
153
|
+
syntax_highlight("echo 'single quotes' \"double quotes\" unquoted")
|
|
154
|
+
inspect_ast("echo 'single quotes' \"double quotes\" unquoted")
|
|
155
|
+
|
|
156
|
+
# Variable expansion
|
|
157
|
+
syntax_highlight("echo $HOME ${USER}")
|
|
158
|
+
inspect_ast("echo $HOME ${USER}")
|
|
159
|
+
|
|
160
|
+
# Command substitution
|
|
161
|
+
syntax_highlight("echo $(date) `whoami`")
|
|
162
|
+
inspect_ast("echo $(date) `whoami`")
|
|
163
|
+
|
|
164
|
+
# Multiple redirections
|
|
165
|
+
syntax_highlight("command < input.txt > output.txt 2>> error.log")
|
|
166
|
+
inspect_ast("command < input.txt > output.txt 2>> error.log")
|
data/examples/test.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/shell_parser'
|
|
5
|
+
|
|
6
|
+
def test(description, input, expected_type)
|
|
7
|
+
print "Testing #{description}... "
|
|
8
|
+
ast = ShellParser.parse(input)
|
|
9
|
+
if ast.is_a?(expected_type)
|
|
10
|
+
puts "ā"
|
|
11
|
+
p ast
|
|
12
|
+
else
|
|
13
|
+
puts "ā Expected #{expected_type}, got #{ast.class}"
|
|
14
|
+
end
|
|
15
|
+
rescue => e
|
|
16
|
+
puts "ā Error: #{e.message}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
puts "Running parser tests:\n\n"
|
|
20
|
+
|
|
21
|
+
test "simple command", "ls -la", ShellParser::Command
|
|
22
|
+
test "pipeline", "cat file | grep pattern", ShellParser::Pipeline
|
|
23
|
+
test "AND list", "make && make test", ShellParser::List
|
|
24
|
+
test "OR list", "make || echo failed", ShellParser::List
|
|
25
|
+
test "background job", "sleep 10 &", ShellParser::List
|
|
26
|
+
test "redirections", "cat < input.txt > output.txt", ShellParser::Command
|
|
27
|
+
test "single quotes", "echo 'hello world'", ShellParser::Command
|
|
28
|
+
test "double quotes", "echo \"hello world\"", ShellParser::Command
|
|
29
|
+
test "variable expansion", "echo $HOME ${USER}", ShellParser::Command
|
|
30
|
+
test "command substitution", "echo $(date)", ShellParser::Command
|
|
31
|
+
test "backtick substitution", "echo `whoami`", ShellParser::Command
|
|
32
|
+
test "complex list", "make clean && make || echo failed", ShellParser::List
|
|
33
|
+
|
|
34
|
+
puts "\nParser implementation complete!"
|
|
35
|
+
puts "Files created:"
|
|
36
|
+
puts " - shell_parser.rb (main parser, ~320 lines)"
|
|
37
|
+
puts " - examples.rb (usage examples)"
|
|
38
|
+
puts " - test.rb (basic tests)"
|
|
39
|
+
puts " - README.md (documentation)"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/shell_parser'
|
|
5
|
+
|
|
6
|
+
def test_structure(description, input, &validator)
|
|
7
|
+
print "Testing #{description}... "
|
|
8
|
+
ast = ShellParser.parse(input)
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
validator.call(ast)
|
|
12
|
+
puts "ā"
|
|
13
|
+
rescue => e
|
|
14
|
+
puts "ā #{e.message}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts "Testing structured parsing:\n\n"
|
|
19
|
+
|
|
20
|
+
# Test command substitution structure
|
|
21
|
+
test_structure("command substitution extracts command", "echo $(date)") do |ast|
|
|
22
|
+
word = ast.words[1]
|
|
23
|
+
part = word.parts[0]
|
|
24
|
+
raise "Not a CommandSub" unless part.is_a?(ShellParser::CommandSub)
|
|
25
|
+
raise "Wrong command: #{part.command}" unless part.command == "date"
|
|
26
|
+
raise "Wrong style" unless part.style == :dollar
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Test nested command substitution
|
|
30
|
+
test_structure("nested command substitution", "echo $(echo $(whoami))") do |ast|
|
|
31
|
+
word = ast.words[1]
|
|
32
|
+
part = word.parts[0]
|
|
33
|
+
raise "Not a CommandSub" unless part.is_a?(ShellParser::CommandSub)
|
|
34
|
+
raise "Wrong command: #{part.command}" unless part.command == "echo $(whoami)"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Test variable extraction
|
|
38
|
+
test_structure("variable extraction", "echo $HOME") do |ast|
|
|
39
|
+
word = ast.words[1]
|
|
40
|
+
part = word.parts[0]
|
|
41
|
+
raise "Not a Variable" unless part.is_a?(ShellParser::Variable)
|
|
42
|
+
raise "Wrong name" unless part.name == "HOME"
|
|
43
|
+
raise "Should not be braced" if part.braced
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Test braced variable
|
|
47
|
+
test_structure("braced variable", "echo ${USER}") do |ast|
|
|
48
|
+
word = ast.words[1]
|
|
49
|
+
part = word.parts[0]
|
|
50
|
+
raise "Not a Variable" unless part.is_a?(ShellParser::Variable)
|
|
51
|
+
raise "Wrong name" unless part.name == "USER"
|
|
52
|
+
raise "Should be braced" unless part.braced
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Test composite word
|
|
56
|
+
test_structure("composite word with multiple parts", "prefix_${VAR}_suffix") do |ast|
|
|
57
|
+
word = ast.words[0]
|
|
58
|
+
raise "Wrong part count: #{word.parts.length}" unless word.parts.length == 3
|
|
59
|
+
raise "Part 0 not Literal" unless word.parts[0].is_a?(ShellParser::Literal)
|
|
60
|
+
raise "Part 1 not Variable" unless word.parts[1].is_a?(ShellParser::Variable)
|
|
61
|
+
raise "Part 2 not Literal" unless word.parts[2].is_a?(ShellParser::Literal)
|
|
62
|
+
raise "Wrong literal 0" unless word.parts[0].value == "prefix_"
|
|
63
|
+
raise "Wrong variable name" unless word.parts[1].name == "VAR"
|
|
64
|
+
raise "Wrong literal 2" unless word.parts[2].value == "_suffix"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Test double-quoted string with expansions
|
|
68
|
+
test_structure("double quotes with expansions", 'echo "Hello $USER"') do |ast|
|
|
69
|
+
word = ast.words[1]
|
|
70
|
+
part = word.parts[0]
|
|
71
|
+
raise "Not DoubleQuoted" unless part.is_a?(ShellParser::DoubleQuoted)
|
|
72
|
+
raise "Wrong inner part count" unless part.parts.length == 2
|
|
73
|
+
raise "First inner not Literal" unless part.parts[0].is_a?(ShellParser::Literal)
|
|
74
|
+
raise "Second inner not Variable" unless part.parts[1].is_a?(ShellParser::Variable)
|
|
75
|
+
raise "Wrong literal" unless part.parts[0].value == "Hello "
|
|
76
|
+
raise "Wrong variable" unless part.parts[1].name == "USER"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Test single-quoted string (no expansion)
|
|
80
|
+
test_structure("single quotes preserve literal", "echo '$HOME'") do |ast|
|
|
81
|
+
word = ast.words[1]
|
|
82
|
+
part = word.parts[0]
|
|
83
|
+
raise "Not SingleQuoted" unless part.is_a?(ShellParser::SingleQuoted)
|
|
84
|
+
raise "Wrong value" unless part.value == "$HOME"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Test backtick command substitution
|
|
88
|
+
test_structure("backtick command substitution", "echo `whoami`") do |ast|
|
|
89
|
+
word = ast.words[1]
|
|
90
|
+
part = word.parts[0]
|
|
91
|
+
raise "Not a CommandSub" unless part.is_a?(ShellParser::CommandSub)
|
|
92
|
+
raise "Wrong command" unless part.command == "whoami"
|
|
93
|
+
raise "Wrong style" unless part.style == :backtick
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Test Word.to_s reconstruction
|
|
97
|
+
test_structure("Word.to_s reconstructs input", "echo $HOME") do |ast|
|
|
98
|
+
word = ast.words[1]
|
|
99
|
+
raise "Wrong reconstruction: #{word.to_s}" unless word.to_s == "$HOME"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
test_structure("Word.to_s with braced var", "echo ${USER}") do |ast|
|
|
103
|
+
word = ast.words[1]
|
|
104
|
+
raise "Wrong reconstruction: #{word.to_s}" unless word.to_s == "${USER}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
test_structure("Word.to_s with command sub", "echo $(date)") do |ast|
|
|
108
|
+
word = ast.words[1]
|
|
109
|
+
raise "Wrong reconstruction: #{word.to_s}" unless word.to_s == "$(date)"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
test_structure("Word.to_s with complex word", 'a${B}c') do |ast|
|
|
113
|
+
word = ast.words[0]
|
|
114
|
+
raise "Wrong reconstruction: #{word.to_s}" unless word.to_s == "a${B}c"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Test command substitution inside double quotes
|
|
118
|
+
test_structure("command sub in double quotes", 'echo "Today: $(date)"') do |ast|
|
|
119
|
+
word = ast.words[1]
|
|
120
|
+
dq = word.parts[0]
|
|
121
|
+
raise "Not DoubleQuoted" unless dq.is_a?(ShellParser::DoubleQuoted)
|
|
122
|
+
raise "Wrong part count" unless dq.parts.length == 2
|
|
123
|
+
raise "First not Literal" unless dq.parts[0].is_a?(ShellParser::Literal)
|
|
124
|
+
raise "Second not CommandSub" unless dq.parts[1].is_a?(ShellParser::CommandSub)
|
|
125
|
+
raise "Wrong command" unless dq.parts[1].command == "date"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
puts "\nā All structure tests passed!"
|
|
129
|
+
puts "\nThe parser now properly represents:"
|
|
130
|
+
puts " - Command substitutions as CommandSub nodes with extracted command text"
|
|
131
|
+
puts " - Variables as Variable nodes with name and braced flag"
|
|
132
|
+
puts " - Double-quoted strings as DoubleQuoted with structured parts"
|
|
133
|
+
puts " - Single-quoted strings as SingleQuoted with literal content"
|
|
134
|
+
puts " - Composite words as sequences of Literal, Variable, and CommandSub parts"
|