rstfilter 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: acaf57f79e7bee11c42958205a4970e6c85a2de855f82513076e40eba42a5d4d
4
+ data.tar.gz: 540a4a20d663769298947079626f5ec8d4ff1d10d72d724d126d5a7a2f9400bd
5
+ SHA512:
6
+ metadata.gz: a3371ad347e2797e051a9c9ac0621eca32022aae6b41ef859c961928e3b1159235e64e75e307901ca8dc17dba27e9e9e56d8646af02683b1eddc8eb2b68184a1
7
+ data.tar.gz: 7abd528a4b660261a2be171609191c16ee848348ad0e6adf7d7c5421ba3aabf4757be69657f3fb6c6b4195083c4c9102d85964a10d086189a843924a923684d2
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.3
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rstfilter.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Koichi Sasada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Rstfilter
2
+
3
+ This tool prints a Ruby script with execution results.
4
+
5
+ ```
6
+ $ cat sample.rb
7
+ a = 1
8
+ b = 2
9
+ c = a + b
10
+ puts "Hello" * c
11
+
12
+ $ rstfilter sample.rb -a
13
+ a = 1 #=> 1
14
+ b = 2 #=> 2
15
+ c = a + b #=> 3
16
+ puts "Hello" * c #=> nil
17
+ #out: HelloHelloHello
18
+ ```
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'rstfilter'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle install
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install rstfilter
35
+
36
+ ## Usage
37
+
38
+ ```
39
+ Usage: rstfilter [options] SCRIPT
40
+ -c, --comment Show result only on comment
41
+ -o, --output Show output results
42
+ -d, --decl Show results on declaration
43
+ --no-exception Do not show exception
44
+ -a, --all Show all results/output
45
+ --pp Use pp to represent objects
46
+ --comment-indent=NUM Specify comment indent size (default: 50)
47
+ --verbose Verbose mode
48
+ ```
49
+
50
+ ## Implementation
51
+
52
+ With parser gem, rstfilter translates the given script and run it.
53
+
54
+ For example, the first example is translated to:
55
+
56
+ ```ruby
57
+ (a = (1).__rst_record__(1, 5)).__rst_record__(1, 5)
58
+ (b = (2).__rst_record__(2, 5)).__rst_record__(2, 5)
59
+ (c = (a + b).__rst_record__(3, 9)).__rst_record__(3, 9)
60
+ (puts "Hello" * c).__rst_record__(4, 16)
61
+ ```
62
+
63
+ and `__rst_record__` method records the results. After that, rstfilter prints the script with the results.
64
+ `--verbose` option shows the translated script and collected results.
65
+
66
+ ## Contribution
67
+
68
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ko1/rstfilter.
69
+
70
+ ## License
71
+
72
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rstfilter"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/rstfilter ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rstfilter'
5
+ rescue LoadError
6
+ $:.unshift File.expand_path('../lib', __dir__)
7
+ require 'rstfilter'
8
+ end
9
+
10
+ f = RstFilter::Exec.new
11
+ f.optparse! ARGV
12
+ ARGV.each{|name| f.process name}
@@ -0,0 +1,3 @@
1
+ module Rstfilter
2
+ VERSION = "0.1.0"
3
+ end
data/lib/rstfilter.rb ADDED
@@ -0,0 +1,287 @@
1
+ # require "rstfilter/version"
2
+
3
+ require 'parser/current'
4
+ require 'stringio'
5
+ require 'optparse'
6
+ require 'ostruct'
7
+ require 'pp'
8
+
9
+ module RstFilter
10
+ class RecordAll < Parser::TreeRewriter
11
+ def initialize opt
12
+ @decl = opt.show_decl || (opt.show_all_results == false)
13
+
14
+ super()
15
+ end
16
+
17
+ def add_record node
18
+ if le = node&.location&.expression
19
+ insert_before(le.begin, '(')
20
+ insert_after(le.end, ").__rst_record__(#{le.end.line}, #{le.end.column})")
21
+ end
22
+ end
23
+
24
+ def process node
25
+ return unless node
26
+ case node.type
27
+ when :resbody, :rescue, :begin
28
+ # skip
29
+ when :def, :class
30
+ add_record node if @decl
31
+ else
32
+ add_record node
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ def on_class node
39
+ _name, sup, body = node.children
40
+ process sup
41
+ process body
42
+ end
43
+
44
+ def on_def node
45
+ _name, args, body = node.children
46
+ args.children.each{|arg|
47
+ case arg.type
48
+ when :optarg
49
+ _name, opexpr = arg.children
50
+ process opexpr
51
+ when :kwoptarg
52
+ _name, kwexpr = arg.children
53
+ process kwexpr
54
+ end
55
+ }
56
+ process body
57
+ end
58
+
59
+ def on_send node
60
+ end
61
+ end
62
+
63
+ class Exec
64
+ DEFAULT_SETTING = {
65
+ # default setting
66
+ show_all_results: true,
67
+ show_exceptions: true,
68
+ show_output: false,
69
+ show_decl: false,
70
+ show_specific_line: nil,
71
+
72
+ use_pp: false,
73
+ comment_indent: 50,
74
+ comment_pattern: '#=>',
75
+ verbose: false,
76
+ }
77
+
78
+ ConfigOption = Struct.new(*DEFAULT_SETTING.keys, keyword_init: true)
79
+
80
+ def update_opt opt = {}
81
+ opt = @opt.to_h.merge(opt)
82
+ @opt = ConfigOption.new(**opt)
83
+ end
84
+
85
+ def initialize
86
+ @opt = ConfigOption.new(**DEFAULT_SETTING)
87
+ end
88
+
89
+ def optparse! argv
90
+ opt = {}
91
+ o = OptionParser.new
92
+ o.on('-c', '--comment', 'Show result only on comment'){
93
+ opt[:show_all_results] = false
94
+ }
95
+ o.on('-o', '--output', 'Show output results'){
96
+ opt[:show_output] = true
97
+ }
98
+ o.on('-d', '--decl', 'Show results on declaration'){
99
+ opt[:show_decl] = true
100
+ }
101
+ o.on('--no-exception', 'Do not show exception'){
102
+ opt[:show_exception] = false
103
+ }
104
+ o.on('-a', '--all', 'Show all results/output'){
105
+ opt[:show_output] = true
106
+ opt[:show_all_results] = true
107
+ opt[:show_exceptions] = true
108
+ }
109
+ o.on('--pp', 'Use pp to represent objects'){
110
+ opt[:use_pp] = true
111
+ }
112
+ o.on('--comment-indent=NUM', "Specify comment indent size (default: #{DEFAULT_SETTING[:comment_indent]})"){|n|
113
+ opt[:comment_indent] = n.to_i
114
+ }
115
+ o.on('--verbose', 'Verbose mode'){
116
+ opt[:verbose] = true
117
+ }
118
+ o.parse!(argv)
119
+ update_opt opt
120
+ end
121
+
122
+ def capture_out
123
+ return yield unless @opt.show_output
124
+
125
+ begin
126
+ prev_out = $stdout
127
+ prev_err = $stderr
128
+
129
+ $captured_out = $stdout = StringIO.new
130
+ if false # debug
131
+ $captured_err = $captured_out
132
+ else
133
+ $captured_err = $stderr = StringIO.new
134
+ end
135
+
136
+ yield
137
+ ensure
138
+ $stdout = prev_out
139
+ $stderr = prev_err
140
+ end
141
+ end
142
+
143
+ def record_rescue
144
+ if @opt.show_exceptions
145
+ TracePoint.new(:raise) do |tp|
146
+ caller_locations.each{|loc|
147
+ if loc.path == @filename
148
+ $__rst_record[loc.lineno][0] = [tp.raised_exception, '', '']
149
+ break
150
+ end
151
+ }
152
+ end.enable do
153
+ yield
154
+ end
155
+ else
156
+ yield
157
+ end
158
+ end
159
+
160
+ class ::BasicObject
161
+ def __rst_record__ line, col
162
+ out, err = *[$captured_out, $captured_err].map{|o|
163
+ str = o.string
164
+ o.string = ''
165
+ str
166
+ } if $captured_out
167
+
168
+ # ::STDERR.puts [line, col, self, out, err].inspect
169
+ $__rst_record[line][col] = [self, out, err]
170
+
171
+ self
172
+ end
173
+ end
174
+
175
+ $__rst_record = Hash.new{|h, k| h[k] = []}
176
+
177
+ def process filename
178
+ @filename = filename
179
+
180
+ code = File.read(filename)
181
+
182
+ begin
183
+ ast, comments = Parser::CurrentRuby.parse_with_comments(code)
184
+ rescue Parser::SyntaxError => e
185
+ puts e
186
+ exit 1
187
+ end
188
+
189
+ end_line = ast.loc.expression.end.line
190
+ code = code.lines[0..end_line].join # remove __END__ and later
191
+
192
+ buffer = Parser::Source::Buffer.new('(example)')
193
+ buffer.source = code
194
+ rewriter = RecordAll.new @opt
195
+
196
+ mod_src = rewriter.rewrite(buffer, ast)
197
+ puts mod_src.lines.map.with_index{|line, i| '%4d: %s' % [i+1, line] } if @opt.verbose
198
+
199
+ begin
200
+ capture_out do
201
+ record_rescue do
202
+ ::TOPLEVEL_BINDING.eval(mod_src, filename)
203
+ end
204
+ end
205
+ rescue Exception => e
206
+ if @opt.verbose
207
+ STDERR.puts e
208
+ STDERR.puts e.backtrace
209
+ else
210
+ STDERR.puts "RstFilter: exit with #{e.inspect}"
211
+ end
212
+ end
213
+
214
+ replace_comments = {}
215
+
216
+ comments.each{|c|
217
+ next unless c.text.start_with? @opt.comment_pattern
218
+ e = c.loc.expression
219
+ line, _col = e.begin.line, e.begin.column
220
+ if $__rst_record.has_key? line
221
+ result = $__rst_record[line].last
222
+ replace_comments[line] = result
223
+ end
224
+ }
225
+
226
+ pp $__rst_record if @opt.verbose
227
+
228
+ code.each_line.with_index{|line, i|
229
+ line_result = $__rst_record[i+1]&.last
230
+
231
+ if line_result && line.match(/(.+)#{@opt.comment_pattern}.*$/)
232
+ prefix = $1
233
+ r = line_result.first
234
+ if @opt.use_pp
235
+ result_lines = PP.pp(r, '').lines
236
+ else
237
+ result_lines = r.inspect.lines
238
+ end
239
+ puts line.sub(/#{@opt.comment_pattern}.*$/, "#{@opt.comment_pattern} #{result_lines.shift.chomp}")
240
+ cont_comment = '#' + ' ' * @opt.comment_pattern.size
241
+ result_lines.each{|result_line|
242
+ puts ' ' * prefix.size + "#{cont_comment}#{result_line}"
243
+ }
244
+ elsif @opt.show_all_results && line_result
245
+ r = line_result.first
246
+ indent = ' ' * [@opt.comment_indent - line.chomp.length, 0].max
247
+ if @opt.use_pp
248
+ result_lines = PP.pp(r, '').lines
249
+ else
250
+ result_lines = r.inspect.lines
251
+ end
252
+ prefix = line.chomp.concat "#{indent}"
253
+ puts "#{prefix} #=> #{result_lines.shift}"
254
+ cont_comment = '#' + ' ' * @opt.comment_pattern.size
255
+ result_lines.each{|result_line|
256
+ puts ' ' * prefix.size + " #{cont_comment}#{result_line}"
257
+ }
258
+ else
259
+ puts line
260
+ end
261
+
262
+ if @opt.show_output && line_result
263
+ out, err = *line_result[1..2]
264
+ if m = line.match(/^\s+/)
265
+ indent = ' ' * m[0].size
266
+ else
267
+ indent = ''
268
+ end
269
+
270
+ {out: out, err: err}.each{|k, o|
271
+ o.strip!
272
+ o.each_line{|ol|
273
+ puts "#{indent}##{k}: #{ol}"
274
+ } unless o.empty?
275
+ }
276
+ end
277
+ }
278
+ end
279
+ end
280
+ end
281
+
282
+ if __FILE__ == $0
283
+ filter = RstFilter::Exec.new
284
+ filter.optparse ARGV
285
+ file = ARGV.shift
286
+ filter.process File.expand_path(file)
287
+ end
data/rstfilter.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ require_relative 'lib/rstfilter/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rstfilter"
5
+ spec.version = Rstfilter::VERSION
6
+ spec.authors = ["Koichi Sasada"]
7
+ spec.email = ["ko1@atdot.net"]
8
+
9
+ spec.summary = %q{Show Ruby script with execution results.}
10
+ spec.description = %q{Show Ruby script with execution results.}
11
+ spec.homepage = "https://github.com/ko1/rstfilter"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_runtime_dependency 'parser'
31
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rstfilter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Koichi Sasada
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Show Ruby script with execution results.
28
+ email:
29
+ - ko1@atdot.net
30
+ executables:
31
+ - rstfilter
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - ".travis.yml"
37
+ - Gemfile
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - bin/console
42
+ - bin/setup
43
+ - exe/rstfilter
44
+ - lib/rstfilter.rb
45
+ - lib/rstfilter/version.rb
46
+ - rstfilter.gemspec
47
+ homepage: https://github.com/ko1/rstfilter
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ homepage_uri: https://github.com/ko1/rstfilter
52
+ source_code_uri: https://github.com/ko1/rstfilter
53
+ changelog_uri: https://github.com/ko1/rstfilter
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.3.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.1.6
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Show Ruby script with execution results.
73
+ test_files: []