source_press 0.0.2

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
+ SHA1:
3
+ metadata.gz: c4cd754bed549fb75d2a3c17d75786a1da8ef662
4
+ data.tar.gz: a74932296e917ffc87808cf643b57e5e75c4303c
5
+ SHA512:
6
+ metadata.gz: b39e5a56124aec24c8366a8ef74f2bdf3997bb3aba8aeb7b11078ea126598b8c75d78d89326ac457ab786139c5935bfdaade2b57448c17800968e3276fc3b409
7
+ data.tar.gz: b5d671425245f73141954bec612c67f62bf92927b2ae148d3185c83b9ee990a01594bf849c8727b089217b4549a9d62755adb59f05d5e8aafc753c971065e98a
data/bin/srcpress ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "source_press"
4
+
5
+ main(ARGV)
@@ -0,0 +1,46 @@
1
+ #
2
+ # Generates a default config file.
3
+ #
4
+ module Config
5
+ def self.generate(file)
6
+ abort("Error: #{file} already in directory") if File.exist?(file)
7
+
8
+ File.open(file.to_s, "w").puts(default)
9
+ puts "Generated default config file - #{file}"
10
+ exit
11
+ end
12
+
13
+ #
14
+ # Returns default config file
15
+ #
16
+ def self.default
17
+ <<~END
18
+ # ScriptPress configuration file
19
+
20
+ # Name + extension of compiled file.
21
+ # Can be left as null/blank
22
+ OutputFile: null
23
+
24
+ # When set to true, overrides output file if it's
25
+ # already in the directory.
26
+ OverrideOutput: false
27
+
28
+ # Language specific file/library import keywords.
29
+ # ie:
30
+ # Ruby - 'require', 'require_relative'
31
+ # Python - 'import', 'from'
32
+ # C/C++ - '#include'
33
+ # Can be left as null/blank
34
+ ImportKeywords:
35
+ - null
36
+
37
+ # Relative/full path to files in the order
38
+ # in which they should appear in the compiled file.
39
+ #
40
+ # If the order is unimportant, please include a path
41
+ # to the directory/directories containing the files.
42
+ FileOrder:
43
+ - null
44
+ END
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ #
2
+ # Displays messages to the user
3
+ #
4
+ module Message
5
+ #
6
+ # Outputs errors to the command line
7
+ #
8
+ class Error
9
+ def self.no_files(errors = nil)
10
+ msg = "Could not load file(s):"
11
+ error_puts(msg, errors)
12
+ end
13
+
14
+ def self.no_file(type, file, extra = "")
15
+ msg = "Could not load #{type} file, #{file}"
16
+ error_puts(msg << "\n " << extra)
17
+ end
18
+
19
+ # Print error message
20
+ def self.error_puts(message, errors = nil)
21
+ puts "\nError: #{message}"
22
+ errors&.each { |e| puts " - " << e }
23
+
24
+ exit # Fatal error, exit program
25
+ end
26
+ end
27
+
28
+ #
29
+ # Outputs warnings to the command line
30
+ #
31
+ class Warning
32
+ def self.ext_warn(warnings, ext)
33
+ msg = "Trying to compile multiple extensions " \
34
+ "(expected #{ext}), found:"
35
+ warning_puts(msg, warnings)
36
+ end
37
+
38
+ #
39
+ # Print warning
40
+ #
41
+ def self.warning_puts(message, warnings = nil)
42
+ puts "\nWarning: #{message}"
43
+ warnings&.each { |e| puts " - " << e }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,359 @@
1
+ require_relative "message"
2
+ require "yaml"
3
+
4
+ module Press
5
+ #
6
+ # Defines settings required by the compiler.
7
+ # Settings are loaded from a .press.yml config file.
8
+ #
9
+ # Each user setting is evaluated and errors are handled
10
+ # beforehand so the compiler only receives valid input
11
+ #
12
+ class Settings
13
+ attr_accessor :output,
14
+ :ovr_output,
15
+ :ext,
16
+ :file_order,
17
+ :import_kwords
18
+
19
+ def initialize(file)
20
+ self.file_order = []
21
+ paths = load(file)
22
+ error_check(paths)
23
+
24
+ # No output file provided
25
+ self.output = "out" << ext if output.nil? || output.empty?
26
+ end
27
+
28
+ private
29
+
30
+ #
31
+ # Load .press.yml settings file
32
+ # Params:
33
+ # +file+:: Path to .press.yml
34
+ #
35
+ def load(file)
36
+ help = "Use `srcpress gen-config` to generate a template config file"
37
+ Message::Error.no_file("config", file, help) unless File.exist?(file)
38
+
39
+ press_yml = YAML.load_file(file)
40
+ self.output = press_yml["OutputFile"]
41
+ self.ovr_output = press_yml["OverrideOutput"]
42
+ self.import_kwords = press_yml["ImportKeywords"]
43
+
44
+ press_yml["FileOrder"]
45
+ end
46
+
47
+ #
48
+ # Run error checks on each file
49
+ # self.file_order can only contain valid file paths
50
+ # Params:
51
+ # +paths+:: Array of file/dir paths provided in .press.yml
52
+ #
53
+ def error_check(paths)
54
+ e = check_files(paths)
55
+ Message::Error.no_files(e) unless e.empty? && file_order.any?
56
+
57
+ file_order.map! { |f| File.absolute_path(f) }
58
+
59
+ e = check_extnames
60
+ self.ext ||= ""
61
+ Message::Warning.ext_warn(e, ext) unless e.empty?
62
+ end
63
+
64
+ #
65
+ # Check if selected paths are valid files or directories
66
+ # If path is directory get all files in the directory
67
+ # Add invalid files to `errors`
68
+ # Params:
69
+ # +paths+:: Array of file/dir paths provided in .press.yml
70
+ #
71
+ def check_files(paths)
72
+ Message::Error.error_puts("No files to load") if paths.nil?
73
+
74
+ errors = []
75
+ paths.each do |f|
76
+ f = "nil" if f.nil?
77
+
78
+ if Dir.exist?(f)
79
+ # Get files in directory
80
+ Dir["#{f}/*"].each { |x| Dir.exist?(x) || file_order << x }
81
+ next
82
+ end
83
+
84
+ if File.exist?(f)
85
+ file_order << f
86
+ next
87
+ end
88
+
89
+ # Invalid path
90
+ errors << File.basename(f)
91
+ end
92
+
93
+ errors
94
+ end
95
+
96
+ #
97
+ # Check if the current file_order contains more than
98
+ # one type of extension, raises a warning if true.
99
+ #
100
+ def check_extnames
101
+ errors = []
102
+ file_order.each do |f|
103
+ self.ext = File.extname(f) if ext.nil?
104
+ next if File.extname(f) == ext
105
+
106
+ # Multiple extensions
107
+ errors << File.basename(f)
108
+ end
109
+
110
+ errors
111
+ end
112
+ end
113
+
114
+ #
115
+ # Reads settings.file_order line by line, evaluates whether
116
+ # the line is an "import" statement and appends the result
117
+ # to a temporary file.
118
+ # A header containing all of the necessary imports is then
119
+ # compiled with the contents in the tmp file.
120
+ #
121
+ # The compiler should never load a file into memory in its
122
+ # entirety, the file should instead be read line by line
123
+ # as to save memory.
124
+ # This process is certainly slower, however, it is a good
125
+ # compromise, especially when loading large files.
126
+ #
127
+ class Compiler
128
+ attr_accessor :settings,
129
+ :check_import,
130
+ :head,
131
+ :removed,
132
+ :ln_ending,
133
+ :tmp_files,
134
+ :silent
135
+
136
+ def initialize(settings, silent = false)
137
+ self.settings = settings
138
+ self.silent = silent
139
+ self.check_import = true
140
+ self.head = []
141
+ self.removed = []
142
+ self.tmp_files = []
143
+
144
+ import_kwords_warning
145
+ end
146
+
147
+ #
148
+ # Runs compiler
149
+ #
150
+ def run
151
+ start = Time.now
152
+
153
+ # Parse lines removing import statements
154
+ count = parse_files
155
+
156
+ out_name = settings.output
157
+ # Delete
158
+ (settings.ovr_output && File.exist?(out_name)) && File.delete(out_name)
159
+
160
+ # Combine head with output and export compiled file
161
+ final_out = make_file(out_name)
162
+ compile_output(final_out)
163
+ final_out.close
164
+
165
+ silent || puts(compile_info(count, final_out, (Time.now - start) * 1000))
166
+ end
167
+
168
+ private
169
+
170
+ #
171
+ # Cycles through files in settings.file_order, creates a
172
+ # temporary file for appending and calls parse_lines
173
+ # to evaluate each line in the current file.
174
+ # Params:
175
+ # +out+:: Temporary file
176
+ #
177
+ def parse_files
178
+ ln_count = 0
179
+
180
+ settings.file_order.each do |f|
181
+ out = make_file("tmp.press")
182
+
183
+ ln_count += parse_lines(f, out)
184
+
185
+ out.close
186
+ tmp_files << out
187
+ end
188
+
189
+ ln_count
190
+ end
191
+
192
+ #
193
+ # Reads lines in file_order and appends them to a
194
+ # temporary file unless the line is an import statement
195
+ # as defined in `settings.import_kwords`
196
+ # Params:
197
+ # +input+:: File to read
198
+ # +out+:: Output File for appending
199
+ #
200
+ def parse_lines(input, out)
201
+ lines = 0
202
+
203
+ File.readlines(input).each do |ln|
204
+ lines += 1
205
+
206
+ # Get line ending
207
+ self.ln_ending ||= ln if ln.chomp.empty?
208
+
209
+ # Append line to out unless it is an import
210
+ out.puts(ln) unless line_import?(ln)
211
+ end
212
+ lines
213
+ rescue SystemCallError => e
214
+ Message::Warning.warning_puts("Could not load #{input} - #{e}")
215
+ return 0
216
+ end
217
+
218
+ #
219
+ # Checks if current line is an import statement
220
+ # by checking it agains `settings.import_kwords`
221
+ # Params:
222
+ # +ln+:: Current lone
223
+ #
224
+ def line_import?(ln)
225
+ return false unless check_import
226
+
227
+ ln = ln.strip
228
+ words = ln.split(" ")
229
+ is_kword = settings.import_kwords.include?(words[0])
230
+ return false unless is_kword
231
+
232
+ head << ln unless head.include?(ln) || local_import?(ln, words)
233
+ true
234
+ end
235
+
236
+ #
237
+ # Checks if import statement is importing a file
238
+ # that will be compiled.
239
+ # Params:
240
+ # +ln+:: Current line
241
+ # +words+:: Line split by spaces
242
+ #
243
+ def local_import?(ln, words)
244
+ tmp = nil
245
+
246
+ settings.file_order.each do |f|
247
+ words[1..-1].each do |w|
248
+ w.delete!("'\"")
249
+ # Remove . from relative path
250
+ while w[0] == "." do w = w[1..-1] end
251
+
252
+ tmp = ln if f.include?(w.downcase)
253
+ end
254
+ end
255
+
256
+ removed << tmp unless tmp.nil?
257
+ !tmp.nil?
258
+ end
259
+
260
+ #
261
+ # Copies lines from a file to another, removing
262
+ # any leading new lines.
263
+ # Params:
264
+ # +input+:: Read from
265
+ # +out+:: Append to
266
+ #
267
+ def copy_lines(input, out)
268
+ last = ""
269
+ leading = nil
270
+
271
+ File.readlines(input).each do |ln|
272
+ # Check for leading blank lines
273
+ leading ||= ln unless ln.chomp.empty?
274
+ out.puts ln unless leading.nil?
275
+
276
+ last = ln
277
+ end
278
+
279
+ last
280
+ end
281
+
282
+ #
283
+ # Combines head with tmp_file
284
+ # Params:
285
+ # +tmp_file+:: Name of temporary file
286
+ # +out+:: Output File object
287
+ #
288
+ def compile_output(out)
289
+ head.each { |ln| out.puts ln }
290
+
291
+ # Add new line between import statements and output
292
+ out.puts ln_ending
293
+
294
+ tmp_files.each do |tmp|
295
+ last = copy_lines(tmp, out)
296
+
297
+ out.puts ln_ending unless last.chomp.empty?
298
+ File.delete(tmp)
299
+ end
300
+ end
301
+
302
+ #
303
+ # Creates and returns a file for appending
304
+ # Params:
305
+ # +fname+:: New file name
306
+ #
307
+ def make_file(fname)
308
+ ext = File.extname(fname)
309
+
310
+ i = 1
311
+ while File.exist?(fname)
312
+ # In case a file with the same name already exists
313
+ fname = File.basename(fname, ext)
314
+ fname = fname.sub(/\d+$/, "") << i.to_s << ext
315
+
316
+ i += 1
317
+ end
318
+
319
+ File.open(fname, "a")
320
+ end
321
+
322
+ #
323
+ # Displays a warning if no "import" keywords were
324
+ # defined in the .press.yml config file and disables
325
+ # checking for imports when reading files.
326
+ #
327
+ def import_kwords_warning
328
+ kwords = settings.import_kwords
329
+ return unless kwords.nil? || kwords.none?
330
+
331
+ msg = "ImportKeywords left empty in config file"
332
+ Message::Warning.warning_puts(msg)
333
+
334
+ self.check_import = false
335
+ end
336
+
337
+ #
338
+ # Displays info after the compiling is complete
339
+ # Params:
340
+ # +elapsed+:: Time elapsed during run in milliseconds
341
+ #
342
+ def compile_info(lines, out, elapsed)
343
+ elapsed_s = format("%.2fms", elapsed)
344
+
345
+ tmp = "\nProcess completed (#{settings.file_order.size} files, " \
346
+ "#{lines} lines) in #{elapsed_s} " \
347
+ "- output in #{out.path}\n"
348
+ return tmp if removed.empty?
349
+
350
+ rm_ln = "\nRemoved lines:\n"
351
+ removed.each { |ln| rm_ln << " - #{ln}\n" }
352
+
353
+ rm_ln << "\nThey are believed to be local " \
354
+ "#{settings.import_kwords[0]} statements.\n" \
355
+ "Please verify that is the case.\n"
356
+ rm_ln << tmp
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,6 @@
1
+ #
2
+ # Current source_press version
3
+ #
4
+ class SourcePress
5
+ VERSION = "0.0.2".freeze
6
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "source_press/version"
2
+ require_relative "source_press/press"
3
+ require_relative "source_press/config"
4
+
5
+ #
6
+ # Runs compiler
7
+ #
8
+ def start_press(config_file, is_silent)
9
+ settings = Press::Settings.new(config_file)
10
+ Press::Compiler.new(settings, is_silent).run
11
+ end
12
+
13
+ #
14
+ # Main entry point
15
+ #
16
+ def main(args)
17
+ is_silent = false
18
+ config = ".press.yml"
19
+
20
+ unless args.empty?
21
+ abort(SourcePress::VERSION) if args[0] == "-v"
22
+ is_silent = args.include?("--silent")
23
+
24
+ m = args[0].match(/config=(.*)/)
25
+ config = m[1].strip unless m.nil?
26
+
27
+ Config.generate(config) if args[0].strip == "gen-config"
28
+ end
29
+ start_press(config, is_silent)
30
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: source_press
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - D Stillwwater
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-05-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Easily combine multiple source files into a single file, works on any
14
+ language as long as .press.yml is configured correctly.
15
+ email: stillwwater@gmail.com
16
+ executables:
17
+ - srcpress
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/srcpress
22
+ - lib/source_press.rb
23
+ - lib/source_press/config.rb
24
+ - lib/source_press/message.rb
25
+ - lib/source_press/press.rb
26
+ - lib/source_press/version.rb
27
+ homepage: http://github.com/stillwwater/source_press
28
+ licenses:
29
+ - WTFPL
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.5.2
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Compiles multiple source files into a single file
51
+ test_files: []