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 +7 -0
- data/bin/srcpress +5 -0
- data/lib/source_press/config.rb +46 -0
- data/lib/source_press/message.rb +46 -0
- data/lib/source_press/press.rb +359 -0
- data/lib/source_press/version.rb +6 -0
- data/lib/source_press.rb +30 -0
- metadata +51 -0
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,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
|
data/lib/source_press.rb
ADDED
@@ -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: []
|