mucgly 0.0.1

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.
data/lib/mucgly.rb ADDED
@@ -0,0 +1,627 @@
1
+ module Mucgly
2
+
3
+
4
+ def Mucgly::error( str )
5
+ if Opt['warn_only'].given
6
+ puts "*** Mucgly Error: #{str}"
7
+ else
8
+ raise RuntimeError, "*** Mucgly Error: #{str}"
9
+ end
10
+ end
11
+
12
+ def Mucgly::debug( str )
13
+ puts "\nMucglyDebug: #{str}" if Opt['debug'].given
14
+ end
15
+
16
+
17
+ # Execution environment for MucglyFile.
18
+ class Env
19
+
20
+ @@fileStartHook = nil
21
+ @@fileEndHook = nil
22
+
23
+ attr_accessor :_separators
24
+ attr_accessor :_inIO
25
+ attr_accessor :_outIO
26
+
27
+ def initialize
28
+ @_outIO = STDOUT
29
+ end
30
+
31
+ def Env.fileStartHook=( val )
32
+ @@fileStartHook = val
33
+ end
34
+
35
+ def Env.fileEndHook=( val )
36
+ @@fileEndHook = val
37
+ end
38
+
39
+ def Env.fileStartHook
40
+ @@fileStartHook
41
+ end
42
+
43
+ def Env.fileEndHook
44
+ @@fileEndHook
45
+ end
46
+
47
+ def fileStartHook
48
+ @@fileStartHook
49
+ end
50
+
51
+ def fileEndHook
52
+ @@fileEndHook
53
+ end
54
+
55
+ def write( str )
56
+ @_outIO.write str
57
+ end
58
+
59
+ def puts( str )
60
+ @_outIO.puts str
61
+ end
62
+
63
+ def source( file )
64
+ instance_eval( File.read( file ), file )
65
+ end
66
+
67
+
68
+ # ------------------------------------------------------------
69
+ # Interface methods:
70
+
71
+
72
+ def _processFilePair( filePair )
73
+ _openInput( filePair[ 0 ] )
74
+ _openOutput( filePair[ 1 ] )
75
+ Mucgly::MucglyFile.new( self )
76
+ end
77
+
78
+ def _processFilePairs( filePairs )
79
+ filePairs.each do |pair|
80
+ _processFilePair( pair )
81
+ end
82
+ end
83
+
84
+ def _openInput( filename )
85
+ @_inIO = EasyFile::ReadStack.new( filename )
86
+ end
87
+
88
+ def _openOutput( filename )
89
+ @_outIO = EasyFile::WriteStack.new( filename )
90
+ end
91
+
92
+ def _openString( filename )
93
+ @_outIO = EasyFile::String.open( filename )
94
+ end
95
+
96
+ def _pushInput( name )
97
+ @_inIO.push( name )
98
+ end
99
+
100
+ def _pushOutput( name )
101
+ @_outIO.push( name )
102
+ end
103
+
104
+ def _closeInput
105
+ @_inIO.close
106
+ end
107
+
108
+ def _closeOutput
109
+ @_outIO.close
110
+ end
111
+
112
+ def _ofilename
113
+ @_outIO.filename
114
+ end
115
+
116
+ def _olinenumber
117
+ @_outIO.line
118
+ end
119
+
120
+ def _ifilename
121
+ @_inIO.filename
122
+ end
123
+
124
+ def _ilinenumber
125
+ @_inIO.line
126
+ end
127
+
128
+ def _eval( cmd )
129
+ instance_eval cmd
130
+ end
131
+
132
+ end
133
+
134
+
135
+ # MucglyFile includes processing and status for a file that is treated
136
+ # as Mucgly file. Separators are copied from the upper level
137
+ # context i.e. lower level settings does NOT affect higher level
138
+ # settings. Status of parsing is kept within the class.
139
+ class MucglyFile
140
+
141
+ attr_accessor :env, :parseState
142
+
143
+ def initialize( env )
144
+
145
+ @env = env
146
+
147
+ # Process fileStartHook callback:
148
+ @env._eval( @env.fileStartHook ) if @env.fileStartHook
149
+
150
+ # Copy upper level separators.
151
+ @env._separators = @env._separators.copy
152
+
153
+ # Process the Mucgly file.
154
+ parse
155
+ end
156
+
157
+
158
+ # Input is a stream of characters or control values.
159
+ #
160
+ # Supported types: char, hookEnd, hookBeg, escape, eof, and
161
+ # empty
162
+ class Token
163
+
164
+ attr_accessor :type, :value
165
+
166
+ def initialize( type, value = nil )
167
+ @type = type
168
+ @value = value
169
+ end
170
+
171
+ def isPunct
172
+ !isChar
173
+ end
174
+
175
+ def isChar
176
+ @type == :char
177
+ end
178
+
179
+ def to_s
180
+ "#{@type.to_s}:#{value}"
181
+ end
182
+
183
+ end
184
+
185
+
186
+ # Parse state regarding buffered tokens.
187
+ class ParseState
188
+
189
+ def initialize( host )
190
+ @host = host
191
+ @stack = [[]]
192
+ @level = 0
193
+ @active = false
194
+ end
195
+
196
+ # Add token.
197
+ def shift( token )
198
+ if @stack[-1][-1] && @stack[-1][-1].type == token.type
199
+ @stack[-1][-1].value += token.value
200
+ else
201
+ @stack[-1].push token
202
+ end
203
+ end
204
+
205
+ # Push new parse context.
206
+ def push
207
+ unless inside?
208
+ flush
209
+ end
210
+ inc
211
+ @stack.push []
212
+ end
213
+
214
+ # Pop top parse context.
215
+ def pop
216
+ if @active
217
+ # Execute.
218
+ eval( @stack[-1] )
219
+ @stack.pop
220
+ else
221
+ # Output literal.
222
+ output( @stack[-1] )
223
+ @stack.pop
224
+ end
225
+
226
+ dec
227
+ end
228
+
229
+ # Output shifted tokens.
230
+ def flush
231
+ output( @stack[-1] )
232
+ @stack[-1] = []
233
+ end
234
+
235
+ # Output list of tokens.
236
+ def output( list )
237
+ list.each do |t|
238
+ @host.env.write t.value
239
+ end
240
+ end
241
+
242
+ # Inside macro def.
243
+ def inside?
244
+ @level > 0
245
+ end
246
+
247
+ # Inc macro level.
248
+ def inc
249
+ @active = true
250
+ @level += 1
251
+ end
252
+
253
+ # Dec macro level.
254
+ def dec
255
+ @level -= 1
256
+ @active = false
257
+ end
258
+
259
+ # Evaluate shifted tokens.
260
+ def eval( list )
261
+ idx = 0
262
+
263
+ # Skip hookbeg.
264
+ idx += 1
265
+
266
+ # Check if macro is internal command.
267
+ if list[idx].value[0] == "."
268
+
269
+ # Variable reference.
270
+
271
+ varname = list[idx].value[1..-1]
272
+
273
+ # Skip hookend.
274
+ idx += 2
275
+
276
+ raise RuntimeError, "Garbage after hookend." if list[ idx ]
277
+
278
+ @host.env._eval( "write @#{varname}" )
279
+
280
+ elsif list[idx].value[0] == ":"
281
+
282
+ # Command.
283
+
284
+ cmd, rest = list[idx].value[1..-1].scan( /([a-z]+) (.*)/ )[0]
285
+
286
+ # Skip hookend.
287
+ idx += 2
288
+
289
+ raise RuntimeError, "Garbage after hookend." if list[ idx ]
290
+
291
+ case cmd
292
+
293
+ when 'include'
294
+ @host.env._pushInput( rest )
295
+ @host.env._inIO.automode = false
296
+ @host.parse
297
+
298
+ when 'output'
299
+ @host.env._pushOutput( rest )
300
+
301
+ when 'close'
302
+ @host.env._closeOutput
303
+
304
+ when 'comment'
305
+ # ' mode coloring fix comment.
306
+ true
307
+
308
+ when 'source'
309
+ @host.env.source( rest )
310
+
311
+ when 'hook'
312
+ if rest.split.length == 2
313
+ @host.env._separators.hookBegChars = rest.split[0]
314
+ @host.env._separators.hookEndChars = rest.split[1]
315
+ else
316
+ @host.env._separators.hookBegChars = rest
317
+ @host.env._separators.hookEndChars = rest
318
+ end
319
+
320
+ when 'hookbeg'
321
+ @host.env._separators.hookBegChars = rest
322
+
323
+ when 'hookend'
324
+ @host.env._separators.hookEndChars = rest
325
+
326
+ when 'escape'
327
+ @host.env._separators.escapeChar = rest
328
+
329
+ when 'exit'
330
+ exit
331
+
332
+ else
333
+ Mucgly::error "unkown internal command: \"#{cmd}\""
334
+
335
+ end
336
+
337
+ elsif list[idx].value[0] == "#"
338
+
339
+ # Remove macro cancel char from macro and output
340
+ # the macro again.
341
+
342
+ @host.env.write( list[idx-1].value )
343
+ @host.env.write( list[idx].value[1..-1] )
344
+ @host.env.write( list[idx+1].value )
345
+
346
+ # Skip hookend.
347
+ idx += 2
348
+
349
+ raise RuntimeError, "Garbage after hookend." if list[ idx ]
350
+
351
+ else
352
+
353
+ # Ruby code to evaluate.
354
+
355
+ code = list[idx].value
356
+
357
+ # Skip hookend.
358
+ idx += 2
359
+
360
+ raise RuntimeError, "Garbage after hookend." if list[ idx ]
361
+
362
+ @host.env._eval code
363
+
364
+ end
365
+
366
+ end
367
+
368
+
369
+ # Return char either from character buffer (String) or file stream.
370
+ def getChars( cnt = 1 )
371
+ c = @host.env._inIO.read( cnt )
372
+ Mucgly::debug( "Read char: \"#{c}\"" )
373
+ c
374
+ end
375
+
376
+ # Put character back.
377
+ def putBack( c )
378
+ @host.env._inIO.putback( c )
379
+ Mucgly::debug( "Putback char: \"#{c}\"" )
380
+ end
381
+
382
+ # Find a given string from input (true/false).
383
+ def findInInput( this )
384
+
385
+ str = getChars( this.length )
386
+
387
+ if str == this
388
+ # Found requested string from input.
389
+ true
390
+ else
391
+ # Mismatch to requested string, put back to input.
392
+ putBack( str )
393
+ false
394
+ end
395
+ end
396
+
397
+
398
+ # Create a token based on the current characters. The
399
+ # token returned is effected by parsing context and
400
+ # separator settings.
401
+ def getTokenRaw
402
+
403
+ escapeChar = @host.env._separators.escapeChar
404
+ hookEndChars = @host.env._separators.hookEndChars
405
+ hookBegChars = @host.env._separators.hookBegChars
406
+
407
+
408
+ c = getChars
409
+
410
+ if c == nil
411
+
412
+ Token.new( :eof )
413
+
414
+ else
415
+
416
+ # Handle escape characters before hooks.
417
+ if c == escapeChar
418
+
419
+ c = getChars
420
+
421
+ if c == "\n"
422
+ # Escaped newline.
423
+ return Token.new( :empty, "" )
424
+
425
+ elsif c == escapeChar
426
+ # Escaped escape.
427
+ return Token.new( :char, escapeChar )
428
+
429
+ elsif inside?
430
+
431
+ # Escape inside first level macro.
432
+
433
+ if ( c == " " || c == "\n" ) and
434
+ ( escapeChar ==
435
+ hookEndChars )
436
+
437
+ # Hookend when same as escape is
438
+ # "hookend+space".
439
+ return Token.new( :hookEnd, escapeChar + c )
440
+
441
+ elsif c == hookEndChars[0]
442
+
443
+ # Escaped hookend.
444
+ return Token.new( :char, c )
445
+
446
+ else
447
+
448
+ # Ineffective escape.
449
+ return Token.new( :char,
450
+ escapeChar + c )
451
+ end
452
+
453
+ else
454
+
455
+ # Escape outside macro.
456
+
457
+ if escapeChar == hookEndChars
458
+
459
+ putBack( c )
460
+ return Token.new( :hookBeg,
461
+ hookBegChars )
462
+
463
+ elsif c == hookEndChars[0]
464
+
465
+ return Token.new( :char, c )
466
+
467
+ elsif ( c == " " || c == "\n" )
468
+
469
+ # Remove escaped space and newlines.
470
+ return Token.new( :empty )
471
+
472
+ elsif c == hookBegChars[0]
473
+
474
+ return Token.new( :char, c )
475
+
476
+ else
477
+
478
+ # Ineffective escape.
479
+ return Token.new( :char,
480
+ escapeChar + c )
481
+
482
+ end
483
+
484
+ end
485
+
486
+ else
487
+
488
+ # No escape, putback and search for hooks.
489
+ putBack c
490
+
491
+ end
492
+
493
+
494
+ # Look for hooks or return a character.
495
+ if inside?
496
+
497
+ # Inside macro.
498
+
499
+ if findInInput( hookEndChars )
500
+ return Token.new( :hookEnd, hookEndChars )
501
+ end
502
+
503
+ else
504
+
505
+ # Outside macro.
506
+
507
+ if findInInput( hookBegChars )
508
+ return Token.new( :hookBeg, hookBegChars )
509
+ end
510
+
511
+ end
512
+
513
+
514
+ # No escapes, no hooks, so return a char.
515
+ c = getChars
516
+
517
+ if c == nil
518
+ return Token.new( :eof )
519
+ else
520
+ return Token.new( :char, c )
521
+ end
522
+
523
+ end
524
+ end
525
+
526
+
527
+ def getToken
528
+ t = getTokenRaw
529
+ Mucgly::debug( "Got token: #{t.type}:\"#{t.value}\"" )
530
+ t
531
+ end
532
+
533
+ end
534
+
535
+
536
+ # Process a Mucgly file.
537
+ def parse
538
+
539
+ parseState = ParseState.new( self )
540
+
541
+ while true
542
+
543
+ t = parseState.getToken
544
+
545
+ if t.type == :eof
546
+
547
+ parseState.flush
548
+
549
+ # Process fileEndHook callback:
550
+ @env._eval( @env.fileEndHook ) if @env.fileEndHook
551
+
552
+ # End of stream.
553
+ # @fp.close
554
+
555
+ @env._inIO.close
556
+
557
+ return
558
+
559
+ elsif t.type == :hookBeg
560
+
561
+ parseState.push
562
+ parseState.shift( t )
563
+
564
+ elsif t.type == :hookEnd
565
+
566
+ parseState.shift( t )
567
+ parseState.pop
568
+
569
+ else
570
+
571
+ parseState.shift( t )
572
+
573
+ end
574
+
575
+
576
+ end
577
+
578
+ end
579
+
580
+ end
581
+
582
+
583
+
584
+ # Separators are collection of special characters used by Mucgly.
585
+ #
586
+ # These are:
587
+ # [escape] Escape char is used to convert special chars into their
588
+ # literal form or to remove the following char. Special
589
+ # characters are: escape char, hookbeg, hookend. Escape
590
+ # can remove the following newline, and space in special
591
+ # circumtances.
592
+ # [hookbeg] Hookbeg char (or string) starts macros. If user wants
593
+ # a literal hookbeg, then <escape> should be put before
594
+ # it.
595
+ # [hookend] Hookend char (or string) terminates macros. Hookend
596
+ # can also be escaped. If hookend end is same character
597
+ # as escape, then hookend followed by a space is
598
+ # considered as hookend.
599
+ class Separators
600
+
601
+ # Set of control characters for hook and escape indentification.
602
+ attr_reader :escapeChar
603
+ attr_accessor :hookBegChars, :hookEndChars
604
+
605
+ def initialize
606
+ @escapeChar = "\\"
607
+ @hookBegChars = "-<"
608
+ @hookEndChars = ">-"
609
+ end
610
+
611
+ def escapeChar=( value )
612
+ if value.length != 1
613
+ Mucgly::error( "Escape must be 1 char long, got \"#{value}\"" )
614
+ end
615
+ @escapeChar = value
616
+ end
617
+
618
+ def copy
619
+ c = Separators.new
620
+ c.escapeChar = @escapeChar
621
+ c.hookBegChars = @hookBegChars
622
+ c.hookEndChars = @hookEndChars
623
+ return c
624
+ end
625
+ end
626
+
627
+ end
@@ -0,0 +1,19 @@
1
+ Line 1 from file "-<write _ifilename>-".
2
+ -<:hook \\>-\
3
+ Test escapes (backslash, empty): "\\" "\ \ " \
4
+ ... and the rest of the line.
5
+ Next the test file "test_include.txt" is included.
6
+ \:comment :include test_include.txt\ \
7
+ \:include test_include.txt\ \
8
+ This is again in file "\write _ifilename\ ".
9
+ Next line should be three times "bar" in a row.
10
+ \@foo="bar";\ \.foo\ \write @foo\ \.foo\
11
+ A line of text.
12
+ \puts "Just some text from Mucgly."\ \
13
+ Source "test_include.rb" and instantiate the class defined in there.
14
+ \:source test_include.rb\ \@obj = MyClass.new\ \
15
+ Print out a sum using the previously instantiated class.
16
+ \puts "Sum of 1+2 is: #{@obj.sum(1,2)}..."\ \
17
+ The last line in "\write _ifilename\ "
18
+ The name of this file is: "\write _ifilename\ ".
19
+ We are outputting to file \write _ofilename\ and \write _olinenumber.to_s\
@@ -0,0 +1,6 @@
1
+ # Normal Ruby code.
2
+ class MyClass
3
+ def sum( a, b )
4
+ a + b
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ Line 1 in test file test_included.txt.
2
+ Line 2 in test file test_included.txt.
3
+ ...which is not terminated with a NL.
@@ -0,0 +1,32 @@
1
+ require 'test/unit'
2
+
3
+
4
+ # Test execution routine.
5
+ def runTest( cmdopts, test, sep = "" )
6
+ Dir.chdir( 'test' )
7
+ FileUtils.mkdir_p "result"
8
+ rf = "result/#{test}#{sep}.txt"
9
+ gf = "golden/#{test}#{sep}.txt"
10
+
11
+ system( "export RUBYLIB=#{ENV['RUBYLIB']}:../lib; ../bin/mucgly #{cmdopts} -i #{test}.rx.txt -o #{rf}" )
12
+
13
+ if false
14
+ # Populate golden files after inspection.
15
+ system( "cp #{rf} #{gf}" )
16
+ end
17
+
18
+ assert( system( "diff #{rf} #{gf}" ), "FAILED: diff #{rf} #{gf}" )
19
+
20
+ Dir.chdir( '..' )
21
+ end
22
+
23
+
24
+ class MucglyTest < Test::Unit::TestCase
25
+
26
+ def test_basic() runTest( "", "test_basic" ); end
27
+ def test_specials_cli1() runTest( "-s @ -e /", "test_specials_cli", "_1" ); end
28
+ def test_specials_cli2() runTest( "-sb @ -se @ -e /", "test_specials_cli", "_2" ); end
29
+ def test_specials_cmd() runTest( "", "test_specials_cmd" ); end
30
+ def test_multi() runTest( "-m", "test_multi" ); end
31
+
32
+ end
@@ -0,0 +1,4 @@
1
+ -<@counter = 0>-\
2
+ -<write "Counter value is #{@counter} at round 1">-
3
+ -<#@counter += 1>-\
4
+ -<#write "Counter value is #{@counter} at round 2">-
@@ -0,0 +1,11 @@
1
+ Line @write (_ilinenumber+1).to_s@ from file "@write _ifilename@".
2
+ Test escapes (backslash, empty): "//" "@ @" /
3
+ ... and the rest of the line.
4
+ Next the test file "test_include.txt" is included.
5
+ @:include test_include.txt@/
6
+ This is again in file "@write _ifilename@ ".
7
+ Next line should be three times "bar" in a row.
8
+ @/@foo="bar"@@.foo@@.foo@@puts /@foo@/
9
+ A line of text.
10
+ @puts "Just some text from Mucgly."@/
11
+ The last line in "@write _ifilename@"