ascii-data-tools 0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +3 -0
  2. data/.rvmrc +1 -0
  3. data/.travis.yml +4 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +40 -0
  6. data/LICENSE.GPL2 +339 -0
  7. data/README.rdoc +52 -0
  8. data/Rakefile +42 -0
  9. data/TODO +4 -0
  10. data/ascii-data-tools.gemspec +30 -0
  11. data/bin/ascii-data-cat +13 -0
  12. data/bin/ascii-data-edit +13 -0
  13. data/bin/ascii-data-norm +13 -0
  14. data/bin/ascii-data-qdiff +13 -0
  15. data/bin/ascii-data-tools-config +9 -0
  16. data/examples/big +10000 -0
  17. data/examples/built_in_records.gz +0 -0
  18. data/examples/slightly_modified_built_in_records.gz +0 -0
  19. data/features/ascii-data-cat.feature +110 -0
  20. data/features/ascii-data-edit.feature +91 -0
  21. data/features/ascii-data-qdiff.feature +54 -0
  22. data/features/encoding_decoding.feature +68 -0
  23. data/features/normaliser.feature +27 -0
  24. data/features/plugins.feature +73 -0
  25. data/features/record_recognition.feature +61 -0
  26. data/features/step_definitions/ascii-data-cat_steps.rb +48 -0
  27. data/features/step_definitions/ascii-data-edit_steps.rb +38 -0
  28. data/features/step_definitions/ascii-data-norm_steps.rb +7 -0
  29. data/features/step_definitions/ascii-data-qdiff_steps.rb +43 -0
  30. data/features/step_definitions/encoding_decoding_steps.rb +23 -0
  31. data/features/step_definitions/plugins_steps.rb +11 -0
  32. data/features/step_definitions/record_recognition_steps.rb +10 -0
  33. data/features/support/env.rb +5 -0
  34. data/lib/ascii-data-tools.rb +8 -0
  35. data/lib/ascii-data-tools/configuration.rb +169 -0
  36. data/lib/ascii-data-tools/configuration_printer.rb +38 -0
  37. data/lib/ascii-data-tools/controller.rb +123 -0
  38. data/lib/ascii-data-tools/discover.rb +19 -0
  39. data/lib/ascii-data-tools/external_programs.rb +23 -0
  40. data/lib/ascii-data-tools/filter.rb +148 -0
  41. data/lib/ascii-data-tools/filter/diffing.rb +139 -0
  42. data/lib/ascii-data-tools/formatting.rb +109 -0
  43. data/lib/ascii-data-tools/global_autodiscovery.rb +21 -0
  44. data/lib/ascii-data-tools/record.rb +50 -0
  45. data/lib/ascii-data-tools/record_type.rb +139 -0
  46. data/lib/ascii-data-tools/record_type/builder.rb +50 -0
  47. data/lib/ascii-data-tools/record_type/decoder.rb +77 -0
  48. data/lib/ascii-data-tools/record_type/encoder.rb +17 -0
  49. data/lib/ascii-data-tools/record_type/field.rb +168 -0
  50. data/lib/ascii-data-tools/record_type/normaliser.rb +38 -0
  51. data/lib/ascii-data-tools/ruby_extensions.rb +7 -0
  52. data/lib/ascii-data-tools/version.rb +3 -0
  53. data/spec/ascii-data-tools/configuration_printer_spec.rb +51 -0
  54. data/spec/ascii-data-tools/configuration_spec.rb +153 -0
  55. data/spec/ascii-data-tools/discover_spec.rb +8 -0
  56. data/spec/ascii-data-tools/filter/diffing_spec.rb +82 -0
  57. data/spec/ascii-data-tools/filter_spec.rb +107 -0
  58. data/spec/ascii-data-tools/formatting_spec.rb +106 -0
  59. data/spec/ascii-data-tools/record_spec.rb +49 -0
  60. data/spec/ascii-data-tools/record_type/builder_spec.rb +69 -0
  61. data/spec/ascii-data-tools/record_type/decoder_spec.rb +73 -0
  62. data/spec/ascii-data-tools/record_type/encoder_spec.rb +32 -0
  63. data/spec/ascii-data-tools/record_type/field_spec.rb +160 -0
  64. data/spec/ascii-data-tools/record_type/normaliser_spec.rb +25 -0
  65. data/spec/ascii-data-tools/record_type_spec.rb +175 -0
  66. data/spec/filter_helper.rb +24 -0
  67. data/spec/record_type_helpers.rb +8 -0
  68. data/spec/spec.opts +2 -0
  69. data/spec/spec_helper.rb +5 -0
  70. metadata +196 -0
@@ -0,0 +1,48 @@
1
+ require 'stringio'
2
+
3
+ Before do |scenario|
4
+ @output_stream = StringIO.new
5
+ @input_stream = StringIO.new
6
+ @user_feedback_stream = StringIO.new
7
+
8
+ @command_line = []
9
+
10
+ AsciiDataTools.record_types.clear
11
+ load 'ascii-data-tools/discover.rb'
12
+ @record_types = AsciiDataTools.record_types
13
+ end
14
+
15
+ Given /^a record stream containing$/ do |string|
16
+ @input_stream.string = string
17
+ end
18
+
19
+ Given /^file "([^\"]*)" containing$/ do |filename, string|
20
+ @record_source_filename = filename
21
+ @input_stream.string = string
22
+ end
23
+
24
+ When /^ascii-data-cat is invoked$/ do
25
+ AsciiDataTools::Controller::CatController.new(
26
+ :input_sources => [AsciiDataTools::InputSource.new(@record_source_filename, @input_stream)],
27
+ :output_stream => @output_stream,
28
+ :record_types => @record_types
29
+ ).run
30
+ end
31
+
32
+ When /^([^\"]*) is invoked on a file "([^\"]*)" containing$/ do |executable, filename, string|
33
+ Given "file \"#{filename}\" containing", string
34
+ When "#{executable} is invoked"
35
+ end
36
+
37
+ When /^([^\"]*) is invoked on a record stream containing$/ do |executable, string|
38
+ Given "a record stream containing", string
39
+ When "#{executable} is invoked"
40
+ end
41
+
42
+ Then /^the following is printed out:$/ do |string|
43
+ @output_stream.string.should == string
44
+ end
45
+
46
+ Then /^the user receives the following feedback:$/ do |string|
47
+ @user_feedback_stream.string.should == string
48
+ end
@@ -0,0 +1,38 @@
1
+ When /^ascii-data-edit is invoked$/ do
2
+ AsciiDataTools::Controller::EditController.new(
3
+ :input_sources => [AsciiDataTools::InputSource.new(@record_source_filename, @input_stream)],
4
+ :output_stream => @output_stream,
5
+ :record_types => @record_types,
6
+ :user_feedback_stream => @user_feedback_stream,
7
+ :editor => lambda do |filenames|
8
+ edited_file = File.new(filenames.first)
9
+ edited_file.extend(AsciiDataTools::ExternalPrograms)
10
+ @text_prior_to_edit = edited_file.read
11
+ File.open(edited_file.path, 'w') {|f| f << @text_after_edit}
12
+ edited_file.modify_file_mtime_to(Time.now + 1)
13
+ end
14
+ ).run
15
+ end
16
+
17
+ When /^the output is successfully ascii\-edited to the following:$/ do |string|
18
+ @text_after_edit = string
19
+ When "ascii-data-edit is invoked"
20
+ end
21
+
22
+ When /^the output is ascii\-edited without alteration$/ do
23
+ AsciiDataTools::Controller::EditController.new(
24
+ :input_sources => [AsciiDataTools::InputSource.new(@record_source_filename, @input_stream)],
25
+ :output_stream => @output_stream,
26
+ :record_types => @record_types,
27
+ :user_feedback_stream => @user_feedback_stream,
28
+ :editor => lambda do |filenames| end
29
+ ).run
30
+ end
31
+
32
+ Then /^the editor shows:$/ do |string|
33
+ @text_prior_to_edit.should == string
34
+ end
35
+
36
+ Then /^the encoded record stream contains:$/ do |string|
37
+ @output_stream.string.should == string
38
+ end
@@ -0,0 +1,7 @@
1
+ When /^ascii\-data\-norm is invoked$/ do
2
+ AsciiDataTools::Controller::NormalisationController.new(
3
+ :input_sources => [AsciiDataTools::InputSource.new(@record_source_filename, @input_stream)],
4
+ :output_stream => @output_stream,
5
+ :record_types => @record_types
6
+ ).run
7
+ end
@@ -0,0 +1,43 @@
1
+ Given /^streams containing$/ do |text|
2
+ @input_stream1 = StringIO.new
3
+ @input_stream2 = StringIO.new
4
+
5
+ text.split("\n").each do |line|
6
+ left_line, right_line = line.split("||").map(&:strip)
7
+ @input_stream1 << left_line.gsub('\n', "\n") unless left_line == "-" * left_line.length and not left_line.empty?
8
+ @input_stream2 << right_line.gsub('\n', "\n") unless right_line == "-" * right_line.length and not right_line.empty?
9
+ end
10
+ @input_stream1.rewind
11
+ @input_stream2.rewind
12
+ end
13
+
14
+ When /^ascii\-data\-qdiff is invoked on files containing:$/ do |string|
15
+ Given "streams containing", string
16
+ When "ascii-data-qdiff is invoked"
17
+ end
18
+
19
+ When /^ascii\-data\-qdiff is invoked$/ do
20
+ @actual_output1 = @actual_output2 = nil
21
+ AsciiDataTools::Controller::QDiffController.new(
22
+ :input_sources => [AsciiDataTools::InputSource.new(nil, @input_stream1),
23
+ AsciiDataTools::InputSource.new(nil, @input_stream2)],
24
+ :editor => lambda do |filenames|
25
+ @actual_output1 = File.read(filenames.first)
26
+ @actual_output2 = File.read(filenames.last)
27
+ end,
28
+ :output_stream => @output_stream,
29
+ :record_types => @record_types,
30
+ :user_feedback_stream => @user_feedback_stream
31
+ ).run
32
+ end
33
+
34
+ Then /^the diffed result should be:$/ do |text|
35
+ expected_output1, expected_output2 = "", ""
36
+ text.split("\n").each do |line|
37
+ left_line, right_line = line.split("||").map(&:strip)
38
+ expected_output1 << left_line + "\n" unless left_line == "-" * left_line.length and not left_line.empty?
39
+ expected_output2 << right_line + "\n" unless right_line == "-" * right_line.length and not right_line.empty?
40
+ end
41
+ @actual_output1.should == expected_output1
42
+ @actual_output2.should == expected_output2
43
+ end
@@ -0,0 +1,23 @@
1
+ When /^I decode an encoded record "([^\"]*)" of type "([^\"]*)"$/ do |ascii_text, record_type_name|
2
+ ascii_text.gsub!('\\n', "\n")
3
+ @record = @record_types.find_by_name(record_type_name).decode(:ascii_string => ascii_text)
4
+ end
5
+
6
+ When /^I encode a record of type "([^\"]*)" and contents:$/ do |record_type_name, table|
7
+ record_type = @record_types.find_by_name(record_type_name)
8
+ values = table.hashes.collect {|hash| hash["field value"].gsub('\\n', "\n") }
9
+ @record = AsciiDataTools::Record::Record.new(record_type, values)
10
+ end
11
+
12
+ Then /^I should have a decoded record of type "([^\"]*)" and contents:$/ do |intended_record_type_name, table|
13
+ @record.type_name.should == intended_record_type_name
14
+ table.hashes.each do |hash|
15
+ expected_value = hash["field value"].gsub('\\n', "\n")
16
+ @record[hash["field name"]].should == expected_value
17
+ end
18
+ end
19
+
20
+ Then /^I should have a encoded record "([^\"]*)"$/ do |expected_encoded_text|
21
+ expected_encoded_text.gsub!('\\n', "\n")
22
+ @record.encode.should == expected_encoded_text
23
+ end
@@ -0,0 +1,11 @@
1
+ Given /^the following configuration:$/ do |code|
2
+ @record_types.instance_eval(code)
3
+ end
4
+
5
+ When /^the record type configuration is printed$/ do
6
+ @configuration_printout = AsciiDataTools::RecordTypesConfigurationPrinter.for_record_types(@record_types).summary
7
+ end
8
+
9
+ Then /^it should look like this:$/ do |expected_string|
10
+ @configuration_printout.should == expected_string
11
+ end
@@ -0,0 +1,10 @@
1
+ When /^record "([^\"]*)" coming from ([^\"]*) is analysed$/ do |ascii_text, record_source_name|
2
+ record_source_name = nil if record_source_name == "unspecified"
3
+ ascii_text.gsub!('\\n', "\n")
4
+ type_determiner = AsciiDataTools::RecordType::TypeDeterminer.new(@record_types)
5
+ @type = type_determiner.determine_type_for(:ascii_string => ascii_text, :filename => record_source_name)
6
+ end
7
+
8
+ Then /^its type should be recognised as "([^\"]*)"$/ do |type_name|
9
+ @type.name.should == type_name
10
+ end
@@ -0,0 +1,5 @@
1
+ lib_path = File.expand_path("#{File.dirname(__FILE__)}/../../lib")
2
+ $LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
3
+
4
+ require 'ascii-data-tools'
5
+ require 'ascii-data-tools/configuration_printer'
@@ -0,0 +1,8 @@
1
+ require 'ascii-data-tools/ruby_extensions'
2
+
3
+ require 'ascii-data-tools/record'
4
+ require 'ascii-data-tools/record_type'
5
+ require 'ascii-data-tools/formatting'
6
+ require 'ascii-data-tools/configuration'
7
+ require 'ascii-data-tools/global_autodiscovery'
8
+ require 'ascii-data-tools/controller'
@@ -0,0 +1,169 @@
1
+ require 'optparse'
2
+ require 'zlib'
3
+ require 'tempfile'
4
+
5
+ module AsciiDataTools
6
+ class Configuration
7
+ attr_reader :input_sources, :output_stream, :errors, :record_types, :editor, :user_feedback_stream
8
+
9
+ def initialize(arguments, overrides = {})
10
+ @arguments = arguments
11
+ @overrides = overrides
12
+ @errors = []
13
+
14
+ @opts = define_optionparser_configuration
15
+ remainder = parse(arguments)
16
+
17
+ @output_stream = overrides[:output_stream] || STDOUT
18
+ @input_sources = overrides[:input_sources] || make_input_streams(remainder, overrides)
19
+ @record_types = overrides[:record_types] || load_record_types
20
+ @editor = overrides[:editor]
21
+ @user_feedback_stream = overrides[:user_feedback_stream] || STDOUT
22
+ end
23
+
24
+ def valid?
25
+ @errors.empty?
26
+ end
27
+
28
+ def error_info_with_usage
29
+ @errors.each {|error| puts error}
30
+ puts
31
+ puts @opts
32
+ exit 1
33
+ end
34
+
35
+ protected
36
+ def make_input_streams(remainder, overrides)
37
+ begin
38
+ return InputSourceFactory.new(overrides).input_sources_from(remainder)
39
+ rescue Exception => e
40
+ @errors << e.message
41
+ return nil
42
+ end
43
+ end
44
+
45
+ def define_optionparser_configuration
46
+ OptionParser.new do |opts|
47
+ opts.banner = [
48
+ "Usage: #{File.basename($0)} [options] <input source>",
49
+ "An input source can be either a flat file or a gzipped flat file.",
50
+ @overrides[:input_pipe_accepted] ? "For this command, - (STDIN) is also allowed." : nil,
51
+ "\n"].compact.join("\n")
52
+
53
+ opts.separator ""
54
+ opts.separator "Other options:"
55
+
56
+ opts.on_tail("-h", "--help", "Show this message") do
57
+ puts opts
58
+ exit
59
+ end
60
+ end
61
+ end
62
+
63
+ def parse(arguments)
64
+ begin
65
+ return @opts.parse(arguments)
66
+ rescue SystemExit => e
67
+ exit e.status
68
+ rescue Exception => e
69
+ @errors << e.message
70
+ return []
71
+ end
72
+ end
73
+
74
+ def load_record_types
75
+ AsciiDataTools.autodiscover
76
+ AsciiDataTools.record_types
77
+ end
78
+ end
79
+
80
+ class InputSourceFactory
81
+ def initialize(properties = {})
82
+ @expected_argument_number = properties[:expected_argument_number] || 1
83
+ @input_pipe_accepted = properties[:input_pipe_accepted].nil? ? true : properties[:input_pipe_accepted]
84
+ end
85
+
86
+ def input_sources_from(input_arguments)
87
+ validate_number_of input_arguments
88
+ return input_arguments.collect {|arg| make_input_source_from(arg)}
89
+ end
90
+
91
+ protected
92
+ def validate_number_of(input_arguments)
93
+ raise "No input specified." if input_arguments.empty?
94
+ error_message = "#{input_arguments.size} input sources detected: #{input_arguments.inspect}. " +
95
+ "This command accepts #{@expected_argument_number} input source(s)."
96
+ raise error_message if input_arguments.length != @expected_argument_number
97
+ end
98
+
99
+ def make_input_source_from(input_argument)
100
+ if input_argument == "-"
101
+ if @input_pipe_accepted
102
+ return InputSource.new(nil, STDIN)
103
+ else
104
+ raise "STDIN not accepted for this command."
105
+ end
106
+ end
107
+
108
+ path_to_file = input_argument
109
+ raise "File #{path_to_file} does not exist!" unless File.exists?(path_to_file)
110
+ return InputSource.new(path_to_file, Zlib::GzipReader.open(path_to_file)) if path_to_file =~ /[.]gz$/
111
+ return InputSource.new(path_to_file, File.open(path_to_file))
112
+ end
113
+ end
114
+
115
+ class InputSource < Struct.new(:filename, :stream)
116
+ def read
117
+ stream.readline
118
+ end
119
+
120
+ def has_records?
121
+ not stream.eof?
122
+ end
123
+ end
124
+
125
+ class Editor
126
+ def initialize(&edit_command)
127
+ @tempfiles = {}
128
+ @preedit_mtimes = {}
129
+ @postedit_mtimes = {}
130
+ @edit_command = edit_command
131
+ end
132
+
133
+ def [](n)
134
+ @tempfiles[n] ||= Tempfile.new("ascii_tools")
135
+ end
136
+
137
+ def edit
138
+ close_all_tempfiles
139
+ save_preedit_mtimes
140
+ edit_files
141
+ save_postedit_mtimes
142
+ end
143
+
144
+ def changed?(n)
145
+ not @preedit_mtimes[n] == @postedit_mtimes[n]
146
+ end
147
+
148
+ protected
149
+ def close_all_tempfiles
150
+ @tempfiles.values.each {|f| f.close }
151
+ end
152
+
153
+ def save_preedit_mtimes
154
+ @tempfiles.each {|n, f| @preedit_mtimes[n] = File.mtime(f.path)}
155
+ end
156
+
157
+ def save_postedit_mtimes
158
+ @tempfiles.each {|n, f| @postedit_mtimes[n] = File.mtime(f.path)}
159
+ end
160
+
161
+ def edit_files
162
+ @edit_command[sorted_filenames]
163
+ end
164
+
165
+ def sorted_filenames
166
+ @tempfiles.sort.collect {|number, tempfile| tempfile.path}
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,38 @@
1
+ require 'terminal-table/import'
2
+
3
+ module AsciiDataTools
4
+ class RecordTypesConfigurationPrinter
5
+ def initialize(presenter)
6
+ @presenter = presenter
7
+ end
8
+
9
+ def summary
10
+ table do |t|
11
+ t.headings = @presenter.headings
12
+ @presenter.record_type_summaries.each {|summary| t << summary}
13
+ end.to_s
14
+ end
15
+
16
+ class << self
17
+ def for_record_types(record_types)
18
+ new(RecordTypesConfigurationPresenter.new(record_types))
19
+ end
20
+ end
21
+ end
22
+
23
+ class RecordTypesConfigurationPresenter
24
+ def initialize(record_types)
25
+ @record_types = record_types
26
+ end
27
+
28
+ def headings
29
+ ["type name", "total length", "constraints", "normalised fields"]
30
+ end
31
+
32
+ def record_type_summaries
33
+ @record_types.sort_by {|record_type| record_type.total_length_of_fields}.inject([]) do |summaries, record_type|
34
+ summaries << [record_type.name, record_type.total_length_of_fields, record_type.constraints_description, record_type.names_of_normalised_fields]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,123 @@
1
+ require 'ascii-data-tools/filter'
2
+ require 'ascii-data-tools/filter/diffing'
3
+ require 'ascii-data-tools/external_programs'
4
+
5
+ module AsciiDataTools
6
+ module Controller
7
+ class AbstractController
8
+ def initialize(configuration_or_command_line_arguments)
9
+ case configuration_or_command_line_arguments
10
+ when Hash then
11
+ @configuration = Configuration.new([], defaults.merge(configuration_or_command_line_arguments))
12
+ when Array then
13
+ @configuration = Configuration.new(configuration_or_command_line_arguments, defaults)
14
+ when Configuration then
15
+ @configuration = configuration_or_command_line_arguments
16
+ end
17
+ @configuration.error_info_with_usage unless @configuration.valid?
18
+ end
19
+
20
+ def type_determiner
21
+ @type_determiner ||= RecordType::TypeDeterminer.new(@configuration.record_types)
22
+ end
23
+
24
+ def run
25
+ raise "should be implemented!"
26
+ end
27
+
28
+ protected
29
+ def input_source
30
+ @configuration.input_sources.first
31
+ end
32
+
33
+ def defaults
34
+ {:expected_argument_number => 1, :input_pipe_accepted => true}
35
+ end
36
+ end
37
+
38
+ class CatController < AbstractController
39
+ def run
40
+ formatting_filter = Filter::FormattingFilter.new(input_source.filename, type_determiner)
41
+ formatting_filter << input_source
42
+ formatting_filter.write(@configuration.output_stream)
43
+ end
44
+ end
45
+
46
+ class EditController < AbstractController
47
+ include ExternalPrograms
48
+ include Filter
49
+
50
+ def run
51
+ editor = Editor.new(&@configuration.editor)
52
+ formatting_filter = FormattingFilter.new(input_source.filename, type_determiner)
53
+ formatting_filter << input_source
54
+
55
+ formatting_filter.write(editor[0])
56
+ editor.edit
57
+
58
+ if not editor.changed?(0)
59
+ @configuration.user_feedback_stream.puts "The file is unmodified."
60
+ else
61
+ encoding_filter = Filter::Filter.new {|record| record.encode }
62
+ parsing_filter = ParsingFilter.new(@configuration.record_types)
63
+ encoding_filter << (parsing_filter << InputSource.new(nil, editor[0].open))
64
+ encoding_filter.write(output_stream)
65
+ end
66
+ end
67
+
68
+ protected
69
+ def output_stream
70
+ @configuration.output_stream == STDOUT ? File.open(input_source.filename, 'w') : @configuration.output_stream
71
+ end
72
+
73
+ def defaults
74
+ {:expected_argument_number => 1,
75
+ :input_pipe_accepted => false,
76
+ :editor => lambda {|filenames| edit_differences(filenames)} }
77
+ end
78
+ end
79
+
80
+ class NormalisationController < AbstractController
81
+ def run
82
+ normalising_filter = Filter::NormalisingFilter.new(input_source.filename, type_determiner)
83
+ normalising_filter << input_source
84
+ normalising_filter.write(@configuration.output_stream)
85
+ end
86
+ end
87
+
88
+ class QDiffController < AbstractController
89
+ include ExternalPrograms
90
+ include Filter
91
+ include Filter::Diffing
92
+
93
+ def run
94
+ editor = Editor.new(&@configuration.editor)
95
+
96
+ normaliser1 = NormalisingFilter.new( @configuration.input_sources[0].filename, type_determiner)
97
+ normaliser2 = NormalisingFilter.new( @configuration.input_sources[1].filename, type_determiner)
98
+ sorter1 = SortingFilter.new
99
+ sorter2 = SortingFilter.new
100
+ diff_executer = DiffExecutingFilter.new
101
+ diff_parser = DiffParsingFilter.new
102
+ diff_formatter = DiffFormattingFilter.new(type_determiner)
103
+
104
+ diff_formatter << (diff_parser << (diff_executer << [sorter1 << (normaliser1 << @configuration.input_sources[0]),
105
+ sorter2 << (normaliser2 << @configuration.input_sources[1])]))
106
+
107
+ begin
108
+ diff_formatter.write(editor[0], editor[1])
109
+ editor.edit
110
+ rescue StreamsEqualException => e
111
+ @configuration.user_feedback_stream.puts "The files are identical."
112
+ end
113
+ end
114
+
115
+ protected
116
+ def defaults
117
+ {:expected_argument_number => 2,
118
+ :input_pipe_accepted => false,
119
+ :editor => lambda {|filenames| edit_differences(filenames)} }
120
+ end
121
+ end
122
+ end
123
+ end