mucgly 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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@"