line 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3e6ded1a189126b7fba919e66731a3707065fe38
4
+ data.tar.gz: 556d3175f3e8b67bf7f745424391ce94af4b5b55
5
+ SHA512:
6
+ metadata.gz: e74e2df08ffe42b3db39cd7b596d833bc5a5d9557b99423461680db4cc028b185ddec942c47de26c6384be3e041a4ae2aec14fc5212dbb85bb8ee4a63c6049e5
7
+ data.tar.gz: 92e7eedd6d1afe3dd0d1cf239167eb75dfa9ed8384d4ad7a415a4b1d86d9d544349e673c4a886a79f6120fb46b9b2d92ed2ffeb5a634fc896eadcfaf136291fd
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ line (1.0.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.1)
10
+ rspec (2.13.0)
11
+ rspec-core (~> 2.13.0)
12
+ rspec-expectations (~> 2.13.0)
13
+ rspec-mocks (~> 2.13.0)
14
+ rspec-core (2.13.1)
15
+ rspec-expectations (2.13.0)
16
+ diff-lcs (>= 1.1.3, < 2.0)
17
+ rspec-mocks (2.13.0)
18
+ surrogate (0.7.0)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ line!
25
+ rspec
26
+ surrogate
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ Line
2
+ ====
3
+
4
+ Command line tool to print the lines from stdinput that are matched by the matchers
5
+ e.g. `line 1` prints the first line
6
+
7
+
8
+ Usage: line [options] matchers
9
+
10
+ matchers:
11
+ 2 matches the second line
12
+ -2 matches the second from the last line
13
+ ^2 matches lines other than the second
14
+ 1..10 matches lines 1 through 10 (the numbers can be negative)
15
+ ^5..10 matches all lines before the fifth and all lines after the tenth
16
+
17
+ options:
18
+ -l, --line-numbers show line numbers in output
19
+ -s, --strip strip leading and tailing whitespace
20
+ -f, --force do not err when told to print a line number beyond the input
21
+ -c, --chomp no newlines between lines in the output
22
+ -h, --help this help screen
23
+
24
+ examples:
25
+ line 1 22 # prints lines 1 and 22
26
+ line -1 # prints the last line
27
+ line ^1 ^-1 # prints all lines but the first and the last
28
+ line 1..10 # prints lines 1 through 10
29
+ line 5..-5 # prints all lines except the first and last four
30
+ line ^5..10 # prins all lines except 5 through ten
31
+ line 5..10 ^6..8 # prints lines 5, 9, 10
32
+ line 5..10 ^7 # prints lines 5, 6, 8, 9
33
+
34
+ License
35
+ =======
36
+
37
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
38
+ Version 2, December 2004
39
+
40
+ Copyright (C) 2012 Josh Cheek <josh.cheek@gmail.com>
41
+
42
+ Everyone is permitted to copy and distribute verbatim or modified
43
+ copies of this license document, and changing it is allowed as long
44
+ as the name is changed.
45
+
46
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
47
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
48
+
49
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
50
+
data/bin/line ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path '../../lib', __FILE__
4
+ require 'line'
5
+ require 'line/options_parser'
6
+
7
+ exit Line.call Line::OptionParser.call ARGV
@@ -0,0 +1,21 @@
1
+ require 'line/matchers/helpers'
2
+
3
+ class Line
4
+ module Matchers
5
+ class And
6
+ include Helpers
7
+
8
+ def initialize(matcher1, matcher2)
9
+ @matcher1, @matcher2 = matcher1, matcher2
10
+ end
11
+
12
+ def matches?(*args)
13
+ @matcher1.matches?(*args) && @matcher2.matches?(*args)
14
+ end
15
+
16
+ def inspect(parent=false)
17
+ inspect_helper parent, "#{@matcher1.inspect self} && #{@matcher2.inspect self}", true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ class Line
2
+ module Matchers
3
+ module Helpers
4
+ def inspect_helper(parent, inspected, has_children)
5
+ return inspected if parent && parent.class == self.class
6
+ return "(#{inspected})" if parent && has_children
7
+ return inspected if parent
8
+ "Matcher(#{inspected})"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ require 'line/matchers/helpers'
2
+
3
+ class Line
4
+ module Matchers
5
+ class Index
6
+ include Helpers
7
+
8
+ attr_accessor :index
9
+
10
+ def initialize(index)
11
+ self.index = index
12
+ end
13
+
14
+ def matches?(line, positive_index, negative_index)
15
+ positive_index == index || negative_index == index
16
+ end
17
+
18
+ def inspect(parent=false)
19
+ inspect_helper parent, index.to_s, false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ require 'line/matchers/helpers'
2
+
3
+ class Line
4
+ module Matchers
5
+ class MatchEverything
6
+ include Helpers
7
+
8
+ def matches?(*)
9
+ true
10
+ end
11
+
12
+ def inspect(parent=false)
13
+ inspect_helper parent, "MatchEverything", false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'line/matchers/helpers'
2
+
3
+ class Line
4
+ module Matchers
5
+ class MatchNothing
6
+ include Helpers
7
+
8
+ def matches?(*)
9
+ false
10
+ end
11
+
12
+ def inspect(parent=false)
13
+ inspect_helper parent, "MatchNothing", false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ require 'line/matchers/helpers'
2
+
3
+ class Line
4
+ module Matchers
5
+ class Not
6
+ include Helpers
7
+
8
+ attr_accessor :matcher
9
+
10
+ def initialize(matcher)
11
+ self.matcher = matcher
12
+ end
13
+
14
+ def matches?(line, positive_index, negative_index)
15
+ !matcher.matches?(line, positive_index, negative_index)
16
+ end
17
+
18
+ def inspect(parent=false)
19
+ inspect_helper parent, "^#{matcher.inspect self}", true
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ require 'line/matchers/helpers'
2
+
3
+ class Line
4
+ module Matchers
5
+ class Or
6
+ include Helpers
7
+
8
+ def initialize(matcher1, matcher2)
9
+ @matcher1, @matcher2 = matcher1, matcher2
10
+ end
11
+
12
+ def matches?(*args)
13
+ @matcher1.matches?(*args) || @matcher2.matches?(*args)
14
+ end
15
+
16
+ def inspect(parent=false)
17
+ inspect_helper parent, "#{@matcher1.inspect self} || #{@matcher2.inspect self}", true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ require 'line/matchers/helpers'
2
+
3
+ class Line
4
+ module Matchers
5
+ class Range
6
+ include Helpers
7
+
8
+ attr_accessor :lower, :upper
9
+
10
+ def initialize(lower, upper)
11
+ self.lower, self.upper = lower, upper
12
+ end
13
+
14
+ def matches?(line, positive_index, negative_index)
15
+ if negative_index && lower < 0 && upper < 0 then lower <= negative_index && negative_index <= upper
16
+ elsif negative_index && lower < 0 then lower <= negative_index && positive_index <= upper
17
+ elsif negative_index && upper < 0 then lower <= positive_index && negative_index <= upper
18
+ elsif 0 < lower && 0 < upper then lower <= positive_index && positive_index <= upper
19
+ elsif 0 < lower then lower <= positive_index
20
+ elsif 0 < upper then positive_index <= upper
21
+ else false
22
+ end
23
+ end
24
+
25
+ def inspect(parent=false)
26
+ inspect_helper parent, "#{lower}..#{upper}", false
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ require 'line/matchers/or'
2
+ require 'line/matchers/not'
3
+ require 'line/matchers/and'
4
+ require 'line/matchers/index'
5
+ require 'line/matchers/range'
6
+ require 'line/matchers/match_nothing'
7
+ require 'line/matchers/match_everything'
@@ -0,0 +1,51 @@
1
+ class Line
2
+ class Options
3
+ attr_accessor :show_help, :strip, :force, :chomp, :indexes, :errors, :line_matcher, :line_numbers
4
+ attr_accessor :instream, :outstream, :errstream, :help_screen, :buffer_size, :debug
5
+
6
+ def initialize(attributes={})
7
+ update attributes
8
+ yield self if block_given?
9
+ end
10
+
11
+ def update(attributes)
12
+ attributes.each { |attribute, value| __send__ "#{attribute}=", value }
13
+ self
14
+ end
15
+
16
+ alias line_numbers? line_numbers
17
+ alias show_help? show_help
18
+ alias debug? debug
19
+ alias strip? strip
20
+ alias force? force
21
+ alias chomp? chomp
22
+
23
+ def help_screen
24
+ @help_screen ||= ''
25
+ end
26
+
27
+ def indexes
28
+ @indexes ||= []
29
+ end
30
+
31
+ def errors
32
+ @errors ||= {}
33
+ end
34
+
35
+ def instream
36
+ @instream ||= $stdin
37
+ end
38
+
39
+ def outstream
40
+ @outstream ||= $stdout
41
+ end
42
+
43
+ def errstream
44
+ @errstream ||= $stderr
45
+ end
46
+
47
+ def buffer_size
48
+ @buffer_size ||= 0
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,130 @@
1
+ require 'line/options'
2
+ require 'line/matchers'
3
+
4
+ class Line
5
+ class OptionParser
6
+ def self.call(*args)
7
+ new(*args).call
8
+ end
9
+
10
+ def initialize(arguments)
11
+ @arguments = arguments
12
+ end
13
+
14
+ def call
15
+ return @options if @options
16
+ self.options = Options.new
17
+
18
+ @arguments.each do |arg|
19
+ case arg
20
+ when '-l', '--line-numbers' then options.line_numbers = true
21
+ when '-h', '--help' then options.show_help = true
22
+ when '-s', '--strip' then options.strip = true
23
+ when '-f', '--force' then options.force = true
24
+ when '-c', '--chomp' then options.chomp = true
25
+ when '-d', '--debug' then options.debug = true
26
+ when '0' then invalid_args << arg
27
+ # range
28
+ when /\A-?\d+..-?\d+/
29
+ lower, upper = arg.split('..').map(&:to_i)
30
+ options.indexes << lower << upper
31
+ positive_matchers << Matchers::Range.new(lower, upper)
32
+ # index
33
+ when /\A-?\d+\Z/
34
+ index = arg.to_i
35
+ options.indexes << index
36
+ positive_matchers << Matchers::Index.new(index)
37
+ # negated index
38
+ when /\A\^-?\d+\Z/
39
+ index = arg[1..-1].to_i
40
+ options.indexes << index
41
+ negative_matchers << Matchers::Not.new(Matchers::Index.new(index))
42
+ when /\A\^-?\d+..-?\d+\Z/
43
+ lower, upper = arg[1..-1].split('..').map(&:to_i)
44
+ options.indexes << lower << upper
45
+ negative_matchers << Matchers::Not.new(Matchers::Range.new(lower, upper))
46
+ else
47
+ invalid_args << arg
48
+ end
49
+ end
50
+
51
+ options.errors[:line_numbers] = error_msg_for_line_numbers if error_msg_for_line_numbers
52
+ options.line_matcher = consolidate_args_to_line_matcher
53
+ options.buffer_size = [0, *options.indexes].min.abs
54
+ options.help_screen = help_screen
55
+ options
56
+ end
57
+
58
+ private
59
+
60
+ attr_accessor :options
61
+
62
+ def error_msg_for_line_numbers
63
+ if invalid_args.size == 1
64
+ "#{invalid_args.first.inspect} is not a valid line number, offsets start from 1"
65
+ elsif invalid_args.size > 1
66
+ inspected_args = invalid_args.map(&:inspect)
67
+ inspected_args[-1] = "and #{inspected_args[-1]}"
68
+ "#{inspected_args.join ', '} are not valid line numbers, offsets start from 1"
69
+ elsif positive_matchers.empty? && negative_matchers.empty? && !options.line_numbers?
70
+ 'No matchers provided'
71
+ end
72
+ end
73
+
74
+ def consolidate_args_to_line_matcher
75
+ positive_matcher = positive_matchers.inject(Matchers::MatchNothing.new) { |memo, current| Matchers::Or.new memo, current }
76
+ negative_matcher = negative_matchers.inject(Matchers::MatchEverything.new) { |memo, current| Matchers::And.new memo, current }
77
+
78
+ if positive_matchers.any? && negative_matchers.any? then Matchers::And.new positive_matcher, negative_matcher
79
+ elsif positive_matchers.any? then positive_matcher
80
+ elsif negative_matchers.any? then negative_matcher
81
+ else Matchers::MatchEverything.new
82
+ end
83
+ end
84
+
85
+ def invalid_args
86
+ @invalid_args ||= []
87
+ end
88
+
89
+ def negative_matchers
90
+ @negative_matchers ||= []
91
+ end
92
+
93
+ def positive_matchers
94
+ @positive_matchers ||= []
95
+ end
96
+
97
+ def help_screen
98
+ <<-HELP.gsub(/^ /, '')
99
+ Usage: line [options] matchers
100
+
101
+ Prints the lines from stdinput that are matched by the matchers
102
+ e.g. `line 1` prints the first line
103
+
104
+ matchers:
105
+ 2 matches the second line
106
+ -2 matches the second from the last line
107
+ ^2 matches lines other than the second
108
+ 1..10 matches lines 1 through 10 (the numbers can be negative)
109
+ ^5..10 matches all lines before the fifth and all lines after the tenth
110
+
111
+ options:
112
+ -l, --line-numbers show line numbers in output
113
+ -s, --strip strip leading and tailing whitespace
114
+ -f, --force do not err when told to print a line number beyond the input
115
+ -c, --chomp no newlines between lines in the output
116
+ -h, --help this help screen
117
+
118
+ examples:
119
+ line 1 22 # prints lines 1 and 22
120
+ line -1 # prints the last line
121
+ line ^1 ^-1 # prints all lines but the first and the last
122
+ line 1..10 # prints lines 1 through 10
123
+ line 5..-5 # prints all lines except the first and last four
124
+ line ^5..10 # prins all lines except 5 through ten
125
+ line 5..10 ^6..8 # prints lines 5, 9, 10
126
+ line 5..10 ^7 # prints lines 5, 6, 8, 9
127
+ HELP
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,65 @@
1
+ class Line
2
+ class QueueWithIndexes
3
+ def initialize(num_negatives=0, &input_generator)
4
+ self.positive_index = 0
5
+ self.buffer = []
6
+ self.num_negatives = num_negatives
7
+ self.input_generator = input_generator
8
+ end
9
+
10
+ include Enumerable
11
+
12
+ def each(&block)
13
+ return to_enum :each unless block
14
+ fill_the_buffer until dry_generator? || full_buffer?
15
+ flow_through_buffer(&block) until dry_generator?
16
+ drain_the_buffer(&block) until empty?
17
+ self
18
+ end
19
+
20
+ def empty?
21
+ fill_the_buffer
22
+ buffer.empty?
23
+ end
24
+
25
+ private
26
+
27
+ attr_accessor :dry_generator, :positive_index, :buffer, :num_negatives, :input_generator
28
+ alias dry_generator? dry_generator
29
+
30
+ def full_buffer?
31
+ num_negatives <= buffer.size
32
+ end
33
+
34
+ def fill_the_buffer
35
+ input = generate_input
36
+ buffer << input unless dry_generator?
37
+ end
38
+
39
+ def flow_through_buffer(&block)
40
+ flow_in = generate_input
41
+ return if dry_generator?
42
+ buffer << flow_in
43
+ flow_out = buffer.shift
44
+ block.call [flow_out, increment_index, nil]
45
+ end
46
+
47
+ def drain_the_buffer(&block)
48
+ negative_index = -buffer.size
49
+ value = buffer.shift
50
+ block.call [value, increment_index, negative_index]
51
+ end
52
+
53
+ def generate_input
54
+ input_generator.call
55
+ rescue StopIteration
56
+ self.dry_generator = true
57
+ end
58
+
59
+ def increment_index
60
+ old_index = positive_index
61
+ self.positive_index = positive_index + 1
62
+ old_index
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,3 @@
1
+ class Line
2
+ VERSION = '1.0.0'
3
+ end
data/lib/line.rb ADDED
@@ -0,0 +1,96 @@
1
+ require 'line/queue_with_indexes'
2
+
3
+ class Line
4
+ def self.call(*args)
5
+ new(*args).call
6
+ end
7
+
8
+ attr_accessor :options
9
+
10
+ def initialize(options)
11
+ @options = options
12
+ end
13
+
14
+ def call
15
+ print_matcher if options.debug?
16
+
17
+ if options.show_help?
18
+ print_help
19
+ return 0
20
+ end
21
+
22
+ if options.errors.any?
23
+ print_errors
24
+ return 1
25
+ end
26
+
27
+ print_lines
28
+
29
+ if unseen_indexes.any? && !options.force?
30
+ print_unseen_indexes
31
+ return 1
32
+ end
33
+
34
+ return 0
35
+ end
36
+
37
+ private
38
+
39
+ attr_accessor :max_index
40
+
41
+ def unseen_indexes
42
+ @unseen_indexes ||= options.indexes.dup
43
+ end
44
+
45
+ def print_help
46
+ options.outstream.puts options.help_screen
47
+ end
48
+
49
+ def print_matcher
50
+ options.errstream.puts options.line_matcher.inspect
51
+ end
52
+
53
+ def high_indexes
54
+ @high_indexes ||= options.indexes.select { |index| index > max_index }
55
+ end
56
+
57
+ def print_errors
58
+ options.errors.each do |type, message|
59
+ options.errstream.puts message
60
+ end
61
+ end
62
+
63
+ def print_unseen_indexes
64
+ options.errstream.puts "Only saw #{max_index} lines of input, can't print lines: #{unseen_indexes.join ', '}"
65
+ end
66
+
67
+ def print_lines
68
+ each_line do |line, positive_index, negative_index|
69
+ unseen_indexes.delete positive_index
70
+ unseen_indexes.delete negative_index
71
+ next unless options.line_matcher.matches? line, positive_index, negative_index
72
+ line = line.strip if options.strip?
73
+ line = "#{positive_index}\t#{line}" if options.line_numbers?
74
+ if options.chomp?
75
+ options.outstream.print line.chomp
76
+ else
77
+ options.outstream.puts line
78
+ end
79
+ end
80
+ end
81
+
82
+ def each_line
83
+ each_line = options.instream.each_line.method(:next)
84
+ QueueWithIndexes.new(options.buffer_size, &each_line).each do |line, positive_index, negative_index|
85
+ positive_index += 1
86
+ self.max_index = positive_index
87
+ debug_line line, positive_index, negative_index
88
+ yield line, positive_index, negative_index
89
+ end
90
+ end
91
+
92
+ def debug_line(line, positive_index, negative_index)
93
+ return unless options.debug?
94
+ options.errstream.puts "#{line.inspect}, #{positive_index.inspect}, #{negative_index.inspect}"
95
+ end
96
+ end
data/line.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "line/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "line"
7
+ s.version = Line::VERSION
8
+ s.authors = ["Josh Cheek"]
9
+ s.email = ["josh.cheek@gmail.com"]
10
+ s.homepage = "https://github.com/JoshCheek/surrogate"
11
+ s.summary = %q{Command line tool to filter lines of input based on index.}
12
+ s.description = %q{Command line tool to filter lines of input based on index.}
13
+
14
+ s.rubyforge_project = "line"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rspec"
22
+ s.add_development_dependency "surrogate"
23
+ end
@@ -0,0 +1,6 @@
1
+ describe 'integration' do
2
+ it 'is actually all hooked together correctly' do
3
+ bin = File.expand_path '../../bin/line', __FILE__
4
+ `echo 1 | #{bin} 1`.should == "1\n"
5
+ end
6
+ end
data/spec/line_spec.rb ADDED
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ describe Line do
4
+ let(:_stderr) { StringIO.new }
5
+ let(:_stdout) { StringIO.new }
6
+ let(:_stdin) { StringIO.new 100.times.map { |i| "line#{i.next}" }.join("\n") }
7
+ let(:args) { ['-l'] }
8
+ let(:options) { parse(args).update errstream: _stderr, outstream: _stdout, instream: _stdin }
9
+ let(:stderr) { exitstatus; _stderr.string }
10
+ let(:stdout) { exitstatus; _stdout.string }
11
+ let(:exitstatus) { described_class.new(options).call }
12
+
13
+ context 'when there are errors' do
14
+ it 'prints the errors to the error stream and has an exit status of 1' do
15
+ options.errors = {whatever: "MAH ERRAH"}
16
+ stderr.should == "MAH ERRAH\n"
17
+ exitstatus.should == 1
18
+ end
19
+ end
20
+
21
+ context 'when debug is set' do
22
+ before { args << '-d' }
23
+ it "prints the matcher's inspection to the errstream" do
24
+ stderr.should include 'Matcher'
25
+ end
26
+
27
+ it "prints each line, and its indexes" do
28
+ _stdin.string = "a\nb\nc\n"
29
+ args << '-2'
30
+ stderr.should include '"a\n", 1, nil'
31
+ stderr.should include '"b\n", 2, -2'
32
+ stderr.should include '"c\n", 3, -1'
33
+ end
34
+ end
35
+
36
+ context 'when there are no errors' do
37
+ it 'prints nothing to stderr, and has an exit status of 0' do
38
+ options.errors = {}
39
+ stderr.should == ""
40
+ exitstatus.should == 0
41
+ end
42
+ end
43
+
44
+ specify 'when show_help is set, it displays the help screen and exits with 0' do
45
+ options.help_screen = 'HELPME'
46
+ options.show_help = true
47
+ stderr.should == ''
48
+ stdout.should == "HELPME\n"
49
+ exitstatus.should == 0
50
+ end
51
+
52
+ it 'prints the input lines at the specified indexes, starting at 1' do
53
+ args.replace %w[1 3]
54
+ stdout.should == "line1\nline3\n"
55
+ end
56
+
57
+ it 'prints negative indexes from the end' do
58
+ args.replace %w[98 100 -2]
59
+ stdout.should == "line98\nline99\nline100\n"
60
+ end
61
+
62
+ context 'when there are not enough lines to print the given indexes' do
63
+ it 'does not print an error and exits 0 when force is set' do
64
+ args.replace %w[-f 101]
65
+ exitstatus.should == 0
66
+ stderr.should be_empty
67
+ end
68
+
69
+ it 'prints an error and exits 1 when force is not set' do
70
+ args.replace %w[100 101 102 -100 -101]
71
+ exitstatus.should == 1
72
+ stderr.should == "Only saw 100 lines of input, can't print lines: 101, 102, -101\n"
73
+ end
74
+ end
75
+
76
+ context 'when there is whitespace wrapping the lines' do
77
+ before do
78
+ _stdin.string = " 1 \n2"
79
+ args.replace %w[1 2]
80
+ end
81
+
82
+ it 'does not print the whitespace when strip is set' do
83
+ options.strip = true
84
+ stdout.should == "1\n2\n"
85
+ end
86
+
87
+ it 'prints the whitespace when strip is not set' do
88
+ options.strip = false
89
+ stdout.should == " 1 \n2\n"
90
+ end
91
+ end
92
+
93
+ context 'when chomp is set' do
94
+ it 'does not print newlines between the input lines' do
95
+ _stdin.string = " 1 \n2"
96
+ args.replace %w[-c 1 2]
97
+ stdout.should == " 1 2"
98
+ end
99
+ end
100
+
101
+ context 'when line_numbers is set' do
102
+ it 'prints the line numbers in front of the lines' do
103
+ args.replace %w[1 2 -l]
104
+ _stdin.string = "line1\nline2"
105
+ stdout.should == "1\tline1\n2\tline2\n"
106
+ end
107
+
108
+ it 'does not interfere with other options' do
109
+ args.replace %w[1 -s -l]
110
+ _stdin.string = ' 123 '
111
+ stdout.should == "1\t123\n"
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,189 @@
1
+ require 'spec_helper'
2
+
3
+ describe Line::OptionParser do
4
+ specify 'it sets the help screen on the options' do
5
+ parse([]).help_screen.tap do |help|
6
+ help.should include '-h'
7
+ help.should include '-s'
8
+ help.should include '-f'
9
+ help.should include '-c'
10
+ help.should include '-l'
11
+ end
12
+ end
13
+
14
+ specify '-h, --help sets show_help?' do
15
+ parse([]).show_help?.should be_false
16
+ parse(['-h']).show_help?.should be_true
17
+ parse(['--help']).show_help?.should be_true
18
+ end
19
+
20
+ specify '-s, --strip sets strip' do
21
+ parse([]).strip?.should be_false
22
+ parse(['-s']).strip?.should be_true
23
+ parse(['--strip']).strip?.should be_true
24
+ end
25
+
26
+ specify '-f, --force sets force' do
27
+ parse([]).force?.should be_false
28
+ parse(['-f']).force?.should be_true
29
+ parse(['--force']).force?.should be_true
30
+ end
31
+
32
+ specify '-c, --chomp sets chomp' do
33
+ parse([]).chomp?.should be_false
34
+ parse(['-c']).chomp?.should be_true
35
+ parse(['--chomp']).chomp?.should be_true
36
+ end
37
+
38
+ specify '-l, --line-numbers sets the line numbers option' do
39
+ parse([]).line_numbers?.should be_false
40
+ parse(['-l']).line_numbers?.should be_true
41
+ parse(['--line-numbers']).line_numbers?.should be_true
42
+ end
43
+
44
+ specify '-l matches everything when no other matchers are provided' do
45
+ parse(['-l']).line_matcher.should match_indexes *1..5
46
+ parse(['-l', '1']).line_matcher.should match_index 1
47
+ parse(['-l', '1']).line_matcher.should match_no_indexes 0, -1
48
+ end
49
+
50
+ specify '-d sets debug' do
51
+ parse([]).debug?.should be_false
52
+ parse(['-d']).debug?.should be_true
53
+ end
54
+
55
+ it 'sets the buffer_size to 0 if there are no negative indexes' do
56
+ parse(['1']).buffer_size.should == 0
57
+ end
58
+
59
+ it 'sets the buffer_size such that it can access all the negative numbers' do
60
+ parse(['1', '-1', '-3', '-2', '10']).buffer_size.should == 3
61
+ end
62
+
63
+ it 'has errors when no matchers are provided and -l is not set' do
64
+ parse(['1']).errors.should_not have_key :line_numbers
65
+ parse(['-l']).errors.should_not have_key :line_numbers
66
+ parse([]).errors[:line_numbers].should == "No matchers provided"
67
+ end
68
+
69
+ it 'has errors for 0, informing user that offset starts at 1' do
70
+ parse(['1']).errors.should_not have_key :line_numbers
71
+ parse(['0']).errors[:line_numbers].should == '"0" is not a valid line number, offsets start from 1'
72
+ parse(['0', 'a', 'b']).errors[:line_numbers].should == '"0", "a", and "b" are not valid line numbers, offsets start from 1'
73
+ end
74
+
75
+ context 'when given integral arguments (e.g. 1, -1)' do
76
+ it 'has no errors' do
77
+ parse(['1']).errors.should == {}
78
+ end
79
+
80
+ it 'sets the buffer size' do
81
+ parse(['-5']).buffer_size.should == 5
82
+ end
83
+
84
+ it 'treats them as as lines to match' do
85
+ parse(['-1', '1', '11']).indexes.should == [-1, 1, 11]
86
+ matcher = parse(['-1', '1', '11']).line_matcher
87
+ matcher.should match_indexes -1, 1, 11
88
+ matcher.should match_no_indexes -2, 0, 2, 10, 12
89
+ end
90
+ end
91
+
92
+ context 'when given ranges (e.g. 1..10, -5..-3)' do
93
+ it 'has no errors' do
94
+ parse(['1..10']).errors.should == {}
95
+ end
96
+
97
+ it 'puts the edges in the expected indexes' do
98
+ parse(['5..8']).indexes.should == [5, 8]
99
+ parse(['-8..-5']).indexes.should == [-8, -5]
100
+ end
101
+
102
+ it 'sets the buffer size' do
103
+ parse(['-8..-5']).buffer_size.should == 8
104
+ parse(['5..-5']).buffer_size.should == 5
105
+ end
106
+
107
+ it 'creates a matcher that matches the range' do
108
+ matcher = parse(['5..8']).line_matcher
109
+ matcher.should match_indexes [5, -1], [5, nil], [5, -100000]
110
+ matcher.should match_indexes [8, -1], [8, nil], [8, -100000]
111
+ matcher.should match_indexes [6, -1], [6, nil], [6, -100000]
112
+ matcher.should match_no_indexes [4, -1], [9, nil]
113
+
114
+ matcher = parse(['-8..-5']).line_matcher
115
+ matcher.should match_indexes -8, -7, -6, -5
116
+ matcher.should match_no_indexes -9, -4
117
+
118
+ matcher = parse(['5..-5']).line_matcher
119
+ matcher.should match_indexes [5, -5], [6, nil], [6, -6]
120
+ matcher.should match_no_indexes [4, nil], [4, -4], [4, -5], [5, -4]
121
+
122
+ matcher = parse(['-5..5']).line_matcher
123
+ matcher.should match_indexes [5, -5], [5, -4], [4, -5], [5, nil], [4, nil]
124
+ matcher.should match_no_indexes [5, -6], [6, nil], [6, -5]
125
+ end
126
+ end
127
+
128
+ context 'when given negated ranges (e.g. ^1..10, ^-5..-3)' do
129
+ it 'has no errors' do
130
+ parse(['^1..10']).errors.should == {}
131
+ end
132
+
133
+ it 'puts the edges in the expected indexes' do
134
+ parse(['^5..8']).indexes.should == [5, 8]
135
+ parse(['^-8..-5']).indexes.should == [-8, -5]
136
+ end
137
+
138
+ it 'sets the buffer size' do
139
+ parse(['^-8..-5']).buffer_size.should == 8
140
+ parse(['^5..-5']).buffer_size.should == 5
141
+ end
142
+
143
+ it 'creates a matcher that matches the range', t:true do
144
+ matcher = parse(['^5..8']).line_matcher
145
+ matcher.should match_no_indexes [5, -1], [5, nil], [5, -100000]
146
+ matcher.should match_no_indexes [8, -1], [8, nil], [8, -100000]
147
+ matcher.should match_no_indexes [6, -1], [6, nil], [6, -100000]
148
+ matcher.should match_indexes [4, -1], [9, nil]
149
+
150
+ matcher = parse(['^-8..-5']).line_matcher
151
+ matcher.should match_no_indexes -8, -7, -6, -5
152
+ matcher.should match_indexes -9, -4
153
+
154
+ matcher = parse(['^5..-5']).line_matcher
155
+ matcher.should match_no_indexes [5, -5], [6, nil], [6, -6]
156
+ matcher.should match_indexes [4, nil], [4, -4], [4, -5], [5, -4]
157
+
158
+ matcher = parse(['^-5..5']).line_matcher
159
+ matcher.should match_no_indexes [5, -5], [5, -4], [4, -5], [5, nil], [4, nil]
160
+ matcher.should match_indexes [5, -6], [6, nil], [6, -5]
161
+ end
162
+
163
+ end
164
+
165
+ context 'when given negated numbers (e.g. ^1, ^-1)' do
166
+ it 'has no errors' do
167
+ parse(['^1']).errors.should == {}
168
+ end
169
+
170
+ it 'puts them in the expected indexes' do
171
+ parse(['^1']).indexes.should == [1]
172
+ end
173
+
174
+ it 'sets the buffer size' do
175
+ parse(['^-3']).buffer_size.should == 3
176
+ end
177
+
178
+ it 'creates a matcher that matches their negation' do
179
+ parse(['^2', '^4', '^-2']).line_matcher.should match_indexes 1, 3, 5, -1, -3
180
+ parse(['^2', '^4', '^-2']).line_matcher.should match_no_indexes 2, 4, -2
181
+ end
182
+
183
+ it 'ands them together, and then ors them with everything else' do
184
+ matcher = parse(['^6', '5..9', '^8', '11']).line_matcher
185
+ matcher.should match_indexes 5, 7, 9, 11
186
+ matcher.should match_no_indexes 4, 6, 8, 10, 12
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe Line::Options do
4
+ let(:options) { described_class.new }
5
+
6
+ it 'takes a hash of key/value pairs that it sets' do
7
+ options = described_class.new(instream: 1, outstream: 2)
8
+ options.instream.should == 1
9
+ options.outstream.should == 2
10
+ end
11
+
12
+ it 'sets the instream to stdin by default' do
13
+ options.instream.should == $stdin
14
+ options.instream = 123
15
+ options.instream.should == 123
16
+ end
17
+
18
+ it 'sets the outstream to stdout by default' do
19
+ options.outstream.should == $stdout
20
+ options.outstream = 123
21
+ options.outstream.should == 123
22
+ end
23
+
24
+ it 'sets the errstream to stderr by default' do
25
+ options.errstream.should == $stderr
26
+ options.errstream = 123
27
+ options.errstream.should == 123
28
+ end
29
+
30
+ specify 'indexes defaults to an empty collection' do
31
+ options.indexes.should == []
32
+ options.indexes.should equal options.indexes
33
+ end
34
+
35
+ specify 'errors defaults to an empty hash' do
36
+ options.errors.should == {}
37
+ options.errors.should equal options.errors
38
+ end
39
+
40
+ specify 'the help screen defaults to an empty string' do
41
+ options.help_screen.should == ''
42
+ options.help_screen = 'help screen'
43
+ options.help_screen.should == 'help screen'
44
+ end
45
+
46
+ specify 'buffer_size is 0 by default' do
47
+ options.buffer_size.should == 0
48
+ options.buffer_size = 123
49
+ options.buffer_size.should == 123
50
+ end
51
+ end
52
+
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe Line::QueueWithIndexes do
4
+ def generator
5
+ values = ['a', 'b', 'c']
6
+ lambda { values.shift || raise(StopIteration) }
7
+ end
8
+
9
+ it 'takes a function that generates values' do
10
+ described_class.new(&generator).to_a.should == [['a', 0, nil], ['b', 1, nil], ['c', 2, nil]]
11
+ end
12
+
13
+ it 'stops reading from the generator once the input_generator raises StopIteration' do
14
+ values = %w[a b c]
15
+ queue = described_class.new do
16
+ if values.empty?
17
+ values = %w[a b c]
18
+ raise StopIteration
19
+ end
20
+ values.shift
21
+ end
22
+ queue.map(&:first).should == %w[a b c]
23
+ queue.map(&:first).should == []
24
+ end
25
+
26
+ it 'can be given a size, to determine how much to buffer in order to be able to tell you negative indexes' do
27
+ described_class.new(2, &generator).to_a.should == [['a', 0, nil], ['b', 1, -2], ['c', 2, -1]]
28
+ end
29
+
30
+ example 'when the size given is greater than the number of values, every value has a negative index' do
31
+ described_class.new(4, &generator).to_a.should == [['a', 0, -3], ['b', 1, -2], ['c', 2, -1]]
32
+ end
33
+
34
+ it 'is cool with each being called all multiple times and such' do
35
+ queue = described_class.new(2, &generator)
36
+ queue.take(1).should == [['a', 0, nil]]
37
+ queue.take(1).should == [['b', 1, -2]]
38
+ queue.take(1).should == [['c', 2, -1]]
39
+ queue.take(1).should == []
40
+ end
41
+
42
+ specify '#each returns the queue' do
43
+ queue = described_class.new(&generator)
44
+ queue.each {}.should equal queue
45
+ end
46
+
47
+ specify '#each is lazy' do
48
+ described_class.new(&generator).each.map { 1 }.should == [1, 1, 1]
49
+ end
50
+
51
+ describe 'empty?' do
52
+ it 'is true when there are no elements in the input' do
53
+ described_class.new { raise StopIteration }.should be_empty
54
+ end
55
+
56
+ it 'is false when there are elements in the input' do
57
+ described_class.new(&generator).should_not be_empty
58
+ end
59
+
60
+ it 'becomes true when it runs out of inputs' do
61
+ queue = described_class.new(2, &generator)
62
+ queue.take 2
63
+ queue.should_not be_empty
64
+ queue.take 1
65
+ queue.should be_empty
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,75 @@
1
+ require 'line'
2
+ require 'line/options_parser'
3
+ require 'surrogate/rspec'
4
+
5
+ module LineSpecHelpers
6
+ def parse(args)
7
+ Line::OptionParser.call(args)
8
+ end
9
+
10
+ def index_matcher(index)
11
+ Line::Matchers::Index.new index
12
+ end
13
+
14
+ def index_matchers(*indexes)
15
+ indexes.map { |index| index_matcher index }
16
+ end
17
+
18
+ def universal_matcher
19
+ Line::Matchers::MatchEverything.new
20
+ end
21
+
22
+ def match_indexes(*indexes)
23
+ MatchIndexes.new(true, *indexes)
24
+ end
25
+
26
+ def match_no_indexes(*indexes)
27
+ MatchIndexes.new(false, *indexes)
28
+ end
29
+
30
+ alias match_index match_indexes
31
+ end
32
+
33
+
34
+ class MatchIndexes
35
+ def initialize(is_positive, *indexes)
36
+ @positive = is_positive
37
+ @indexes = indexes
38
+ @unmatched = []
39
+ @matched = []
40
+ end
41
+
42
+ attr_reader :indexes, :matched, :unmatched, :matcher, :positive
43
+
44
+ alias positive? positive
45
+
46
+ def matches?(matcher)
47
+ @matcher = matcher
48
+
49
+ self.indexes.each do |index|
50
+ index.kind_of?(Array) ? indexes = index :
51
+ index < 0 ? indexes = [0, index] :
52
+ indexes = [index, nil]
53
+
54
+ if matcher.matches? '', *indexes
55
+ matched << index
56
+ else
57
+ unmatched << index
58
+ end
59
+ end
60
+
61
+ positive? ? unmatched.empty? : matched.empty?
62
+ end
63
+
64
+ def failure_message_for_should(*)
65
+ if positive?
66
+ "#{matcher.inspect} should have matched #{unmatched.inspect}"
67
+ else
68
+ "#{matcher.inspect} shouldn't have matched #{matched.inspect}"
69
+ end
70
+ end
71
+ end
72
+
73
+ RSpec.configure do |config|
74
+ config.include LineSpecHelpers
75
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'match_indexes (fucking helpers are too complex -.-)' do
4
+
5
+ class MatcherInterface
6
+ Surrogate.endow self
7
+ define(:matches?) { |line, positive_index, negative_index| true }
8
+ define(:inspect) { |parent=nil| }
9
+ end
10
+
11
+ let(:spy) { MatcherInterface.new }
12
+
13
+ specify 'Matchers implement the MatcherInterface' do
14
+ Line::Matchers::Index.should substitute_for MatcherInterface, subset: true, names: true
15
+ end
16
+
17
+ it 'returns true when they match single positive indexes' do
18
+ spy.will_matches? true
19
+ match_indexes(1, 2).matches?(spy).should be_true
20
+ spy.will_matches? false
21
+ match_indexes(1, 2).matches?(spy).should be_false
22
+ end
23
+
24
+ it 'passes positive indexes in the positive index parameter, and nil as the negative index' do
25
+ match_indexes(1, 2).matches?(spy).should be_true
26
+ spy.was asked_if(:matches?).with(anything, 1, nil)
27
+ spy.was asked_if(:matches?).with(anything, 2, nil)
28
+ end
29
+
30
+ it 'passes negative indexes in the negative index parameter' do
31
+ match_indexes(1, -2).matches?(spy).should be_true
32
+ spy.was asked_if(:matches?).with('', 1, nil)
33
+ spy.was asked_if(:matches?).with('', anything, -2)
34
+ end
35
+
36
+ it 'passes the first and second element in an array to the pos/neg index parameters' do
37
+ match_index([100, -100], [200, nil]).matches?(spy)
38
+ spy.was asked_if(:matches?).with('', 100, -100)
39
+ spy.was asked_if(:matches?).with('', 200, nil)
40
+ end
41
+ end
42
+
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: line
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Cheek
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: surrogate
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Command line tool to filter lines of input based on index.
42
+ email:
43
+ - josh.cheek@gmail.com
44
+ executables:
45
+ - line
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - Gemfile
50
+ - Gemfile.lock
51
+ - README.md
52
+ - bin/line
53
+ - lib/line.rb
54
+ - lib/line/matchers.rb
55
+ - lib/line/matchers/and.rb
56
+ - lib/line/matchers/helpers.rb
57
+ - lib/line/matchers/index.rb
58
+ - lib/line/matchers/match_everything.rb
59
+ - lib/line/matchers/match_nothing.rb
60
+ - lib/line/matchers/not.rb
61
+ - lib/line/matchers/or.rb
62
+ - lib/line/matchers/range.rb
63
+ - lib/line/options.rb
64
+ - lib/line/options_parser.rb
65
+ - lib/line/queue_with_indexes.rb
66
+ - lib/line/version.rb
67
+ - line.gemspec
68
+ - spec/integration_spec.rb
69
+ - spec/line_spec.rb
70
+ - spec/options_parser_spec.rb
71
+ - spec/options_spec.rb
72
+ - spec/queue_with_indexes_spec.rb
73
+ - spec/spec_helper.rb
74
+ - spec/spec_helper_spec.rb
75
+ homepage: https://github.com/JoshCheek/surrogate
76
+ licenses: []
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project: line
94
+ rubygems_version: 2.0.0
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Command line tool to filter lines of input based on index.
98
+ test_files:
99
+ - spec/integration_spec.rb
100
+ - spec/line_spec.rb
101
+ - spec/options_parser_spec.rb
102
+ - spec/options_spec.rb
103
+ - spec/queue_with_indexes_spec.rb
104
+ - spec/spec_helper.rb
105
+ - spec/spec_helper_spec.rb