ned 0.0.1 → 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.
@@ -0,0 +1,40 @@
1
+ module Ned
2
+ class Read < Ned::Command
3
+ def initialize(inputs)
4
+ @inputs = inputs
5
+ end
6
+
7
+ def execute
8
+ line = @next || next_line
9
+ @next = next_line
10
+ line.ensure_trailing_newline if @next
11
+ line.nil? ? nil : line
12
+ end
13
+
14
+ def next_line
15
+ @current ||= @inputs.shift
16
+ return unless @current
17
+
18
+ line = @current.gets("\n")
19
+ return line if line
20
+
21
+ @current.close
22
+ @current = nil
23
+
24
+ next_line
25
+ end
26
+
27
+ def execute_all
28
+ lines = []
29
+ current = @inputs.shift
30
+ while current
31
+ lines.concat(current.readlines)
32
+ current.close
33
+ current = @inputs.shift
34
+ lines[-1].ensure_trailing_newline if !current.nil? && lines[-1]
35
+ end
36
+
37
+ lines
38
+ end
39
+ end
40
+ end
@@ -1,38 +1,52 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Ned
4
- module Sort
2
+ class Sort < Ned::AllCommand
5
3
  DIGIT_MATCH = /^ *((?:0|-?[1-9][0-9]*)(?:[.][0-9]+)?)(.*)/
6
- end
7
- end
8
4
 
9
- Ned::Config.add(
10
- name: 'sort',
11
- description: 'Sort. Defaults to alphabetical.',
12
- short: '-s',
13
- long: '--sort',
14
- params: '["num"|"alpha"]',
15
- validator: Ned::Validators.num(0, 1) do |type|
16
- if type
17
- ['num', 'alpha'].include?(type) ? [type] : nil
18
- else
19
- []
5
+ long_name 'sort'
6
+ short_name 's'
7
+
8
+ option_parser do |opts|
9
+ opts.banner = <<~EOF
10
+ Sort input lines.
11
+
12
+ Usage: ned sort [--numeric]
13
+
14
+ Options:
15
+ EOF
16
+
17
+ opts.on('-n', '--numeric', 'Sort numerically.') do |numeric|
18
+ raise OptionParser::ParseError.new('duplicate flag') if options[:numeric]
19
+
20
+ numeric
21
+ end
22
+
23
+ opts.on('-r', '--reverse', 'Reverse the sort order.') do |reverse|
24
+ raise OptionParser::ParseError.new('duplicate flag') if options[:reverse]
25
+
26
+ reverse
27
+ end
20
28
  end
21
- end,
22
- execute: lambda do |context, type='alpha'|
23
- if type == 'alpha'
24
- context.lines.sort!
25
- else
26
- sortable = context.lines.map do |line|
27
- match = line.match(Ned::Sort::DIGIT_MATCH)
28
- if match
29
- [0, Float(match[1]), match[2], line]
30
- else
31
- [1, nil, nil, line]
29
+
30
+ def execute_internal(lines)
31
+ ensure_trailing_newline
32
+ if options.fetch(:numeric, false)
33
+ sortable = lines.map do |line|
34
+ match = line.match(DIGIT_MATCH)
35
+ if match
36
+ [0, Float(match[1]), match[2], line]
37
+ else
38
+ [1, nil, nil, line]
39
+ end
32
40
  end
41
+ lines = sortable.sort.map { |v| v[3] }
42
+ else
43
+ lines.sort!
33
44
  end
34
- context.lines = sortable.sort.map { |v| v[3] }
45
+
46
+ lines.reverse! if options.fetch(:reverse, false)
47
+ lines
35
48
  end
36
- end,
37
- all: true
38
- )
49
+
50
+ Ned::CommandRegistry.add(Sort)
51
+ end
52
+ end
@@ -1,22 +1,31 @@
1
- # frozen_string_literal: true
2
-
3
- Ned::Config.add(
4
- name: 'tail',
5
- description: 'Print the last n lines. Defaults to 10.',
6
- short: '-t',
7
- long: '--tail',
8
- params: '[num]',
9
- validator: Ned::Validators.num(0, 1) do |num|
10
- if num
11
- Integer(num) rescue nil
12
- else
13
- []
1
+ module Ned
2
+ class Tail < AllCommand
3
+ long_name 'tail'
4
+ short_name 't'
5
+
6
+ option_parser do |opts|
7
+ opts.banner = <<~EOF
8
+ Print only the last input lines.
9
+
10
+ Usage: ned tail [--num NUM]
11
+
12
+ Options:
13
+ EOF
14
+
15
+ opts.on('-n', '--num NUM', Integer, 'Number of input lines to print. Defaults to 10.') do |num|
16
+ raise OptionParser::ParseError.new('duplicate flag') if options[:num]
17
+
18
+ raise OptionParser::ParseError.new("invalid num: #{num}") if num < 0
19
+
20
+ num
21
+ end
14
22
  end
15
- end,
16
- execute: lambda do |context, num=10|
17
- if num < context.lines.length
18
- context.lines = context.lines[-num..-1]
23
+
24
+ def execute_internal(lines)
25
+ num = options.fetch(:num, 10)
26
+ num < lines.length ? lines[-num..-1] : lines
19
27
  end
20
- end,
21
- all: true
22
- )
28
+
29
+ Ned::CommandRegistry.add(Tail)
30
+ end
31
+ end
@@ -1,20 +1,55 @@
1
- # frozen_string_literal: true
2
-
3
- Ned::Config.add(
4
- name: 'uniq',
5
- description: 'Unique.',
6
- short: '-u',
7
- long: '--uniq',
8
- params: nil,
9
- validator: Ned::Validators.none,
10
- execute: lambda do |context|
11
- context.lines = context.lines.each_with_index.flat_map do |line, index|
12
- if index == 0 || context.lines[index - 1] != line
13
- [line]
14
- else
15
- []
1
+ module Ned
2
+ class Uniq < Ned::AllCommand
3
+ long_name 'uniq'
4
+ short_name 'u'
5
+
6
+ option_parser do |opts|
7
+ opts.banner = <<~EOF
8
+ Filter out repeated input lines.
9
+
10
+ Usage: ned uniq [--count]
11
+
12
+ Options:
13
+ EOF
14
+
15
+ opts.on('-c', '--count', 'Precede each output line with the number of times the line occurred in the input, followed by a space.') do |count|
16
+ raise OptionParser::ParseError.new('duplicate flag') if options[:count]
17
+
18
+ count
16
19
  end
17
20
  end
18
- end,
19
- all: true
20
- )
21
+
22
+ def execute_internal(lines)
23
+ ensure_trailing_newline
24
+
25
+ previous = nil
26
+ count = 1
27
+ lines.each_with_index.flat_map do |line, i|
28
+ result = []
29
+ if previous && line != previous
30
+ if options.fetch(:count, false)
31
+ previous.insert(0, "#{count} ")
32
+ result << previous
33
+ else
34
+ result << previous
35
+ end
36
+ count = 1
37
+ elsif previous
38
+ count += 1
39
+ end
40
+ previous = line
41
+ if i == lines.length - 1
42
+ if options.fetch(:count, false)
43
+ line.insert(0, "#{count} ")
44
+ result << line
45
+ else
46
+ result << line
47
+ end
48
+ end
49
+ result
50
+ end
51
+ end
52
+
53
+ Ned::CommandRegistry.add(Uniq)
54
+ end
55
+ end
data/lib/ned/string.rb ADDED
@@ -0,0 +1,14 @@
1
+ class String
2
+ def ensure_trailing_newline
3
+ self << "\n" unless self[-1] == "\n"
4
+ end
5
+
6
+ def insert_before_newline(string)
7
+ if self[-1] == "\n"
8
+ self.insert(-2, string)
9
+ else
10
+ self.insert(-1, string)
11
+ end
12
+ end
13
+ end
14
+
data/lib/ned/strings.rb CHANGED
@@ -23,7 +23,7 @@ module Ned
23
23
  i = i + 1
24
24
  end
25
25
 
26
- string
26
+ nil
27
27
  end
28
28
  end
29
29
  end
data/lib/ned/version.rb CHANGED
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Ned
4
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
5
3
  end
data/lib/ned.rb CHANGED
@@ -1,193 +1,236 @@
1
- # frozen_string_literal: true
1
+ require 'optparse'
2
2
 
3
3
  require 'ned/command'
4
- require 'ned/config'
5
- require 'ned/context'
4
+ require 'ned/command_registry'
5
+ require 'ned/string'
6
6
  require 'ned/strings'
7
- require 'ned/validators'
8
7
  require 'ned/version'
9
8
 
10
9
  Dir["#{__dir__}/ned/commands/**/*.rb"].sort.each { |f| require f }
11
10
 
11
+ # todo
12
+ # - uniq shouldn't be an all command
13
+ # - split into files
14
+ # - commands shouldn't have to implement execute_all, put this in the base or use a wrapper or something
15
+ # - implement help
16
+ # - add --version
17
+ # - write tests
18
+ # - add tail
19
+ # - add head
20
+ # - add replace
21
+ # - add grep
22
+ # - add grep -o or only
23
+ # - add grep -v or except
24
+ # - add index
25
+ # - add join
26
+ # - add count or something like wc
27
+ # - implement inline editing
28
+ # - implement highlighting
29
+ # - implement append
30
+ # - add ability for a command to turn one line into many
31
+ # - add ability for a command to turn one line into zero
32
+ # - handle newlines in quote, replace, etc.
33
+ # - add numeric sort
34
+ # - add reverse sort
35
+ # - maybe add append and prepend
36
+ # - figure out how to distrute as gem without bundle
37
+ # - use OptionParser on main args.
38
+ # - uniq should not need to be an AllCommand. Add ability to check whether there are more lines.
39
+ # - add support for a ~/.ned/ dir
40
+ # - implement highlight
41
+ # - add color
42
+ # - add support for more escape sequences, see https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
43
+ # - add support for editing files in place
44
+ # - add ljust
45
+ # - maybe add ability to highlight columns, not just full lines
46
+ # - write readme
47
+ # - add support for to_set in uniq
48
+ # - add support for overriding existing commands
49
+ # - index shouldn't always be an all command. It should be based on the indices.
12
50
  module Ned
13
- def self.help(error_output)
14
- error_output.puts <<~EOF
15
- A stream editor.
16
-
17
- Usage: ned [file] [options ...]
18
-
19
- Options:
20
- EOF
21
-
22
- lines = [
23
- [' --help', 'Print this message.'],
24
- [' --version', 'Print version.']
25
- ]
26
- max = lines[0][0].length
27
- Ned::Config.all.each do |config|
28
- next if config.hidden?
29
-
30
- line = "#{config.short}, #{config.long} #{config.params}"
31
- lines << [line, config.description]
32
- max = [max, line.length].max
33
- end
51
+ class Main
52
+ def initialize
53
+ @options = { }
34
54
 
35
- lines.each do |line|
36
- error_output.puts " #{line[0].ljust(max)} #{line[1]}"
37
- end
38
- end
55
+ @option_parser = OptionParser.new do |opts|
56
+ opts.banner = <<~EOF
57
+ Ned, a stream editor.
39
58
 
40
- def self.failure(error_output, message)
41
- error_output.puts "fatal: #{message}\n\n"
42
- help(error_output)
43
- end
59
+ Usage: ned [[ned_arg ...] --] [[command] [: command ...]]
44
60
 
45
- def self.run(input: $stdin, output: $stdout, error_output: $stderr, args: ARGV)
46
- args = args.map do |arg|
47
- Ned::Strings.unescape!(arg.dup)
48
- end
61
+ OPTIONS
62
+ EOF
49
63
 
50
- if args.include?('--help')
51
- help(error_output)
52
- return 0
53
- end
64
+ opts.on('-i', '--inplace', 'Edit each file in place by processing each file independently.') do |inplace|
65
+ raise OptionParser::ParseError.new('duplicate flag') if @options[:inplace]
54
66
 
55
- if args.include?('--version')
56
- output.puts("Ned version #{Ned::VERSION}")
57
- return 0
58
- end
67
+ inplace
68
+ end
59
69
 
60
- inputs = []
61
- while args.first && !args.first.match(/^-./)
62
- arg = args.shift
70
+ opts.on('-h', '--help [COMMAND]', 'Print this help.') do |help|
71
+ help || true
72
+ end
63
73
 
64
- if arg == '-'
65
- inputs << input
66
- else
67
- unless File.exist?(arg)
68
- failure(error_output, "no such file: #{arg}")
69
- return 1
74
+ opts.on('-v', '--version', 'Print Ned version.') do |version|
75
+ version
70
76
  end
71
- inputs << arg
72
77
  end
73
78
  end
74
79
 
75
- if input.tty? && (inputs.empty? || (inputs.size == 1 && inputs[0] == input))
76
- failure(error_output, 'no input')
77
- return 1
78
- end
80
+ def help
81
+ help = @option_parser.help
79
82
 
80
- inputs = inputs.unshift(input) if !input.tty? && !inputs.index(input)
83
+ help << "\nCOMMANDS\n"
81
84
 
82
- if inputs.count(input) > 1
83
- failure(error_output, "can't read from standard in twice, silly")
84
- return 1
85
+ Ned::CommandRegistry.all.each do |command|
86
+ help << "\n#{command.long_name} (#{command.short_name})\n\n"
87
+ command.help.split("\n").each do |line|
88
+ help << " #{line}\n"
89
+ end
90
+ end
91
+ help
85
92
  end
86
93
 
87
- if inputs.size > 1
88
- failure(error_output, "more than one input not yet supported")
89
- return 1
90
- end
94
+ def run(args, input, output, error)
95
+ args = args.map do |arg|
96
+ arg = arg.dup
97
+ Ned::Strings.unescape!(arg)
98
+ arg
99
+ end
91
100
 
92
- commands = []
101
+ ned_args = []
102
+ command_args = []
103
+ has_stdin = !input.tty?
93
104
 
94
- while arg = args.shift
95
- config = Ned::Config.all.find do |config|
96
- arg.start_with?(config.short) || arg == config.long || arg.start_with?("#{config.long}=")
105
+ if index = args.index('--')
106
+ ned_args.concat(args.shift(index))
107
+ args.shift
108
+ elsif args[0]&.start_with?('-')
109
+ ned_args = args
110
+ args = []
97
111
  end
98
112
 
99
- unless config
100
- failure(error_output, "invalid option: #{arg}")
113
+ command_args.concat(args.shift(args.size))
114
+
115
+ begin
116
+ @option_parser.parse!(ned_args, into: @options)
117
+ rescue OptionParser::ParseError => e
118
+ error.puts "fatal: error while parsing options: #{e.message}"
101
119
  return 1
102
120
  end
103
121
 
104
- params = []
105
-
106
- if arg.start_with?("#{config.long}=")
107
- params << arg["#{config.long}=".size..-1]
108
- elsif arg.start_with?(config.short) && arg != config.short
109
- params << arg[config.short.size..-1]
110
- else
111
- while !args.empty? && !args.first.start_with?('-')
112
- arg = args.shift
113
- if arg.start_with? '\\-'
114
- arg = arg[1..-1]
122
+ if help_option = @options[:help]
123
+ if help_option.is_a? TrueClass
124
+ output.puts help
125
+ return 0
126
+ elsif help_option.is_a? String
127
+ command = Ned::CommandRegistry.find(help_option)
128
+ unless command
129
+ error.puts "fatal: \"#{help_option}\" is not a valid command"
130
+ return 1
115
131
  end
116
- params << arg
132
+ output.puts command.help
133
+ return 0
117
134
  end
118
135
  end
119
136
 
120
- valid = config.validate(*params)
121
- if valid.nil?
122
- if config.params.nil?
123
- failure(error_output, "invalid param for #{config.name}. found: #{params}, expected none")
124
- return 1
137
+ if @options[:version]
138
+ output.puts "Ned version #{Ned::VERSION}"
139
+ return 0
140
+ end
141
+
142
+ raise 'in-place not implemented' if @options[:inplace]
143
+
144
+ inputs = ned_args.map do |current|
145
+ if current == '-'
146
+ input
125
147
  else
126
- failure(error_output, "invalid param for #{config.name}. found: #{params}, expected: #{config.params}")
127
- return 1
148
+ unless File.exist? current
149
+ error.puts "fatal: file not found \"#{current}\""
150
+ return 1
151
+ end
152
+ File.new(current)
128
153
  end
129
154
  end
130
155
 
131
- commands << Ned::Command.new(config, valid)
132
- end
156
+ inputs << input if inputs.empty?
157
+ inputs.unshift input if !inputs.index(input) && has_stdin
133
158
 
134
- if commands.any?(&:all?)
135
- lines = inputs.flat_map do |i|
136
- if i.is_a? String
137
- IO.readlines(i, sep="\n")
159
+ command_args = command_args.inject([[]]) do |array, arg|
160
+ if arg == ':'
161
+ array << []
138
162
  else
139
- i.readlines(sep="\n")
163
+ array[-1] << arg
140
164
  end
165
+ array
141
166
  end
142
- process_all(commands, lines, output)
143
- else
144
- process_each(commands, inputs, output)
145
- end
146
167
 
147
- 0
148
- end
168
+ if command_args.size == 1 && command_args[0] == []
169
+ command_args.clear
170
+ end
149
171
 
150
- def self.process_all(commands, lines, output)
151
- final_newline = lines[-1]&.[](-1) == "\n" ? "\n" : ''
172
+ if command_args.index([])
173
+ error.puts 'fatal: empty command found'
174
+ return 1
175
+ end
152
176
 
153
- lines = lines.map { |line| line.chomp("\n") }
154
- context = Context.new(lines, 0)
155
- commands.each do |command|
156
- command.execute(context)
157
- end
158
- last = context.lines.length - 1
159
- context.lines.each_with_index do |line, index|
160
- output.print "#{line}#{index == last ? final_newline : "\n"}"
161
- end
162
- end
177
+ command_args.each do |array|
178
+ array.each_with_index do |arg, i|
179
+ array[i] = arg[1..-1] if arg == '\:'
180
+ end
181
+ end
163
182
 
164
- def self.process_each(commands, inputs, output)
165
- input = inputs[0]
183
+ command_args.each do |args|
184
+ unless Ned::CommandRegistry.find(args[0])
185
+ error.puts "fatal: \"#{args[0]}\" is not a valid command"
186
+ return 1
187
+ end
188
+ end
166
189
 
167
- if input.is_a? String
168
- input = File.open(input)
169
- end
190
+ if inputs.count(input) > 1
191
+ error.puts 'fatal: can\'t read from stdin more than once'
192
+ return 1
193
+ end
170
194
 
171
- current = input.gets(sep="\n")
172
- next_line = input.gets(sep="\n")
173
- index = 0
195
+ if inputs.index(input) && @in_place
196
+ error.puts "fatal: option -i specified with stdin"
197
+ return 1
198
+ end
199
+
200
+ previous = Ned::Read.new(inputs)
201
+ command_args.each do |args|
202
+ command = Ned::CommandRegistry.find(args.shift).new(previous)
203
+ begin
204
+ command.parse(args)
205
+ raise OptionParser::ParseError.new("invalid arguments: #{args}") unless args.empty?
206
+ rescue OptionParser::ParseError => e
207
+ error.puts "fatal: error while parsing options for #{command.class.long_name}: #{e.message}"
208
+ return 1
209
+ end
174
210
 
175
- while current
176
- final_newline = current[-1]&.[](-1) == "\n" ? "\n" : ''
177
- context = Context.new([current.chomp("\n")], index)
178
- commands.each do |command|
179
- command.execute(context)
211
+ if command.options[:help]
212
+ output.puts command.class.help
213
+ return 0
214
+ end
215
+
216
+ if command.options[:version]
217
+ output.puts "Ned version #{Ned::VERSION}"
218
+ return 0
219
+ end
220
+
221
+ previous = command
180
222
  end
181
- index = context.start_index + context.lines.size
182
- last = context.lines.length - 1
183
- context.lines.each_with_index do |line, index|
184
- output.print "#{line}#{index == last && next_line.nil? ? final_newline : "\n"}"
223
+
224
+ if inputs.index(input) && !has_stdin
225
+ error.puts 'fatal: can\'t read from stdin'
226
+ return 1
185
227
  end
186
228
 
187
- current = next_line
188
- next_line = input.gets(sep="\n")
189
- end
229
+ print = Ned::Print.new(previous, output)
230
+
231
+ while line = print.execute; end
190
232
 
191
- input.close
233
+ 0
234
+ end
192
235
  end
193
236
  end
File without changes
File without changes
File without changes
File without changes