kubetailrb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/journey_log.md ADDED
@@ -0,0 +1,469 @@
1
+ # Journey log
2
+
3
+ > Here lies the chronicle of my journey learning Ruby, a collection of trials,
4
+ > tribulations, and triumphs as I navigated the world of this dynamic
5
+ > programming language.
6
+
7
+
8
+ ## 🤔 Things that I'm curious about
9
+
10
+
11
+ ---
12
+
13
+ ## 2024-10-27
14
+ ### Context
15
+
16
+ I already learned some basic Ruby by reading the
17
+ [official Ruby documentation](https://www.ruby-lang.org/en/) as well as doing
18
+ the [Ruby Koans](https://www.rubykoans.com/).
19
+
20
+ Now it's time to the practical exercise by creating a new project.
21
+ Since I'm on the team to "embrace suffering to learn efficiently", I think
22
+ coding and meeting lots of issue while I'm building this tool will heavily
23
+ benefit my Ruby skill and its ecosystem.
24
+
25
+ The subject of the exercise is to have something relevant to my day-to-day work,
26
+ something I'm sure I'll often use.
27
+
28
+ We are using Kubernetes at work (like most companies), and we are using
29
+ structured logs following the [Elastic Common Schema (ECS) specification](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html),
30
+ so not quite easily readable by a human.
31
+
32
+ Tools like [stern](https://github.com/stern/stern) or [kubetail](https://github.com/johanhaleby/kubetail)
33
+ are useful to watch multiple Kubernetes pod logs directly from the terminal.
34
+ However, they do not format the logs in JSON format easily. I could pipe the
35
+ result and use other tools like [jq](https://github.com/jqlang/jq), but it's not
36
+ fun, and I wanted to really learn Ruby, hence this project was created.
37
+
38
+ Let's hope I will see it through and manage to implement the whole project 🤞.
39
+
40
+ ### Project goal
41
+
42
+ The idea is to have a CLI, like [stern](https://github.com/stern/stern), to read
43
+ and follow the Kubernetes pod logs directly from the terminal, so something like
44
+ this:
45
+
46
+ ```bash
47
+ kubetailrb --namespace my-namespace pod-name-regex
48
+ ```
49
+
50
+ The name `kubetailrb` is copied from [kubetail](https://github.com/johanhaleby/kubetail)
51
+ with a simple prefix `rb` to indicate that it's implemented in Ruby, so quite
52
+ straightforward.
53
+
54
+ I want to have it like a library, so a Gem, that can also be used by other Ruby
55
+ project.
56
+ I also have the ambition to learn [Ruby on Rails](https://rubyonrails.org/), so
57
+ I also plan to implement a web version of `kubetailrb`.
58
+
59
+ ### Project initialization
60
+
61
+ There are lots of way to create a new Ruby project.
62
+
63
+ I first went for the tutorial at [RubyGems](https://guides.rubygems.org/make-your-own-gem/).
64
+ It did work for a bit. However, the projects at my work are using [Bundler](https://bundler.io/),
65
+ which seems to better scale Ruby projects, in the sense that it can track and
66
+ manage dependencies with the idea of `Gemfile.lock`.
67
+
68
+ So I followed the [tutorial from Bundler](https://bundler.io/guides/creating_gem.html#testing-our-gem)
69
+ which is quite complete as it provides some boilerplate to quickly help me start
70
+ up a new project, like:
71
+
72
+ - the basic project structure,
73
+ - a `Gemfile` as well as the `kubetailrb.gemspec`,
74
+ - a `Rakefile` to execute some task, like running the tests.
75
+
76
+ The project generation was performed with a single command line:
77
+
78
+ ```bash
79
+ bundle gem kubetailrb --bin --no-coc --no-ext --mit --test=minitest --ci=github --linter=rubocop
80
+ ```
81
+
82
+ Lots of things to learn. Let's take it one by one.
83
+
84
+ ### Project structure
85
+
86
+ It seems the convention is:
87
+
88
+ - `lib/` contains the source code.
89
+ - `test/` (or `spec/` depending on the test framework) contains the test code.
90
+ - `features` contains the cucumber scenarios, i.e. integration tests.
91
+ - `bin/` contains some scripts that can help the developer experience,
92
+ - Rails projects also have scripts in this `bin/` directory.
93
+ - `exec/` contains the executables that will be installed to the user system if
94
+ the latter is installing the gem
95
+ - It seems to be a convention from Bundler, but that is configurable in the
96
+ `gemspec` file.
97
+
98
+ I'm still not sure about the other directories, but I'll find out sooner or
99
+ later.
100
+
101
+ ### Gemfile vs gemspec
102
+
103
+ The `Gemfile` is used to manage gem dependencies for our library’s development.
104
+ This file contains a `gemspec` line meaning that Bundler will include
105
+ dependencies specified in `kubetailrb.gemspec` too. It’s best practice to
106
+ specify all the gems that our library depends on in the `gemspec`.
107
+
108
+ The `gemspec` is the Gem Specification file. This is where we provide
109
+ information for Rubygems' consumption such as the name, description and homepage
110
+ of our gem. This is also where we specify the dependencies our gem needs to run.
111
+
112
+ > The benefit of putting this dependency specification inside of
113
+ > `foodie.gemspec` rather than the `Gemfile` is that anybody who runs gem
114
+ > install `foodie --dev` will get these development dependencies installed too.
115
+ > This command is used for when people wish to test a gem without having to fork
116
+ > it or clone it from GitHub.
117
+
118
+ src: https://bundler.io/guides/creating_gem.html#testing-our-gem
119
+
120
+ So, I'll put the dependencies to the `gemspec` by default. Looking at some
121
+ project, like [cucumber-ruby](https://github.com/cucumber/cucumber-ruby/tree/main), they are also putting everything in their `gemspec`.
122
+
123
+ ### Rake
124
+
125
+ [Rake](https://github.com/ruby/rake) is a popular task runner in Ruby.
126
+
127
+ In a newly created project, it only runs the tests and the linter (Rubocop).
128
+
129
+ A good tutorial on Rake: https://www.rubyguides.com/2019/02/ruby-rake/
130
+
131
+ I wanted to add [cucumber](https://github.com/cucumber/cucumber-ruby/tree/main)
132
+ in the `:default` task so that it execute all the tests (minitest + cucumber).
133
+
134
+ To know how to add this step, I directly looked at the source code of
135
+ [cucumber-ruby](https://github.com/cucumber/cucumber-ruby/blob/main/lib/cucumber/rake/task.rb) and add it to my [Rakefile](./Rakefile):
136
+
137
+ ```ruby
138
+ # ... previous code
139
+
140
+ require "cucumber/rake"
141
+ Cucumber::Rake::Task.new
142
+
143
+ task default: %i[test cucumber rubocop]
144
+ ```
145
+
146
+ ### `require` vs `require_relative`
147
+
148
+ Difference between `require` and `require_relative`:
149
+ - `require` is global.
150
+ - `require_relative` is relative to this current directory of this file.
151
+ - `require "./some_file"` is relative to your current working directory.
152
+
153
+ src: https://stackoverflow.com/a/3672600/3612053
154
+
155
+ ### Create a Ruby CLI application
156
+
157
+ We can parse CLI options using only stdlib.
158
+ No need to use some fancy library, like Thor or cli-ui.
159
+ The goal is to learn Ruby, not to learn to use 3rd party libraries.
160
+
161
+ src: https://www.rubyguides.com/2018/12/ruby-argv/
162
+
163
+ Some tools if I ever decide to change mind:
164
+
165
+ - [rails/thor: Thor is a toolkit for building powerful command-line interfaces](https://github.com/rails/thor)
166
+ - [TTY: The Ruby terminal apps toolkit](https://ttytoolkit.org/)
167
+ - [Shopify/cli-ui: CLI tooling framework with simple interactive widgets](https://github.com/Shopify/cli-ui?tab=readme-ov-file)
168
+
169
+ ### Bundle exec everything?
170
+
171
+ The documentation is always prefixing all the command with `bundle exec`, e.g.
172
+ `bundle exec rake`. But I already have `rake` in my `$PATH`, so why do they
173
+ suggest adding this `bundle exec` which seems to provide more typing.
174
+
175
+ > In some cases, running executables without bundle exec may work, if the
176
+ > executable happens to be installed in your system and does not pull in any
177
+ > gems that conflict with your bundle.
178
+ >
179
+ > However, this is unreliable and is the source of considerable pain. Even if it
180
+ > looks like it works, it may not work in the future or on another machine.
181
+
182
+ src: https://stackoverflow.com/a/6588708/3612053
183
+
184
+ ## 2024-10-29
185
+ ### RUBYGEMS_GEMDEPS env variable
186
+
187
+ Previously, to ensure we are using the right gems, we needed to prefix all our
188
+ ruby/gem commands with `bundle exec`. But it seems there's a better way: set the
189
+ `RUBYGEMS_GEMDEPS=-` environment variable. This will autodetect the `Gemfile` in
190
+ the current or parent directories or set it to the path of your `Gemfile`.
191
+
192
+ > `use_gemdeps(path = nil)`
193
+ >
194
+ > Looks for a gem dependency file at path and
195
+ > activates the gems in the file if found. If the file is not found an
196
+ > ArgumentError is raised.
197
+ >
198
+ > If path is not given the RUBYGEMS_GEMDEPS environment variable is used, but if
199
+ > no file is found no exception is raised.
200
+ >
201
+ > If ‘-’ is given for path RubyGems searches up from the current working
202
+ > directory for gem dependency files (gem.deps.rb, Gemfile, Isolate) and
203
+ > activates the gems in the first one found.
204
+ >
205
+ > You can run this automatically when rubygems starts. To enable, set the
206
+ > RUBYGEMS_GEMDEPS environment variable to either the path of your gem
207
+ > dependencies file or “-” to auto-discover in parent directories.
208
+ >
209
+ > NOTE: Enabling automatic discovery on multiuser systems can lead to execution
210
+ > of arbitrary code when used from directories outside your control.
211
+
212
+ src: https://ruby-doc.org/3.3.5/stdlibs/rubygems/Gem.html
213
+
214
+ ### Guard to execute tests automatically
215
+
216
+ I want to have fast feedback loop, and to get this developer experience, I need
217
+ something that will run automatically the tests every time I update a file.
218
+
219
+ I could have use [entr](https://github.com/clibs/entr) as usual, but since I'm
220
+ learning Ruby, let's try to keep on Ruby's ecosystem.
221
+
222
+ And it appears there's a tool for that: [Guard](https://github.com/guard/guard).
223
+
224
+ It's quite powerful, especially because it's also offering several plugins to
225
+ support multiple use cases, such as
226
+ [guard-minitest](https://rubygems.org/gems/guard-minitest) to run Minitest and
227
+ Test/Unit tests, and [guard-cucumber](https://github.com/guard/guard-cucumber)
228
+ to re-run changed/affected Cucumber features.
229
+
230
+ ## 2024-10-30
231
+ ### Debugging
232
+
233
+ I thought the keyword `pry` was native to Ruby. It appears I need to add some
234
+ gems to enable debugging:
235
+
236
+ ```ruby
237
+ spec.add_development_dependency "pry"
238
+ spec.add_development_dependency "pry-byebug"
239
+ ```
240
+
241
+ The latter is needed to go step-by-step. I also have to add a `.pryrc` at the
242
+ root of the project in order to have some nice shortcuts, like `n` for `next`.
243
+
244
+ To add a breaking point, I had to add:
245
+
246
+ ```ruby
247
+ require "pry"
248
+ require "pry-byebug"
249
+
250
+ binding.pry
251
+ ```
252
+
253
+ ## 2024-10-31
254
+ ### Require in tests
255
+
256
+ I tried to test my [`OptsParser`](./lib/kubetailrb/opts_parser.rb) with
257
+ [`OptsParserTest`](./test/kubetailrb/opts_parser_test.rb), but I got an error
258
+ while creating a new instance:
259
+
260
+ ```
261
+ 1) Error:
262
+ no argument provided#test_0001_should return help command:
263
+ NameError: uninitialized constant Kubetailrb::OptsParser
264
+ test/kubetailrb/opts_parser_test.rb:7:in `block (2 levels) in <class:OptsParserTest>'
265
+ ```
266
+
267
+ So I had to add the following to my test file:
268
+
269
+ ```ruby
270
+ require "kubetailrb/opts_parser"
271
+ ```
272
+
273
+ But do I not need to add this line for [`VersionTest`](./test/kubetailrb/cmd/version_test.rb) and [`HelpTest`](./test/kubetailrb/cmd/help_test.rb)?
274
+
275
+ It was because I had the following in my
276
+ [`CLI`](./lib/kubetailrb/cli.rb):
277
+
278
+ ```ruby
279
+ require "cmd/help"
280
+ require "cmd/version"
281
+ ```
282
+
283
+ which include my classes.
284
+
285
+ That raises the question of when should we add those `require` /
286
+ `require_relative`? How can we know if one is already provided by an upstream
287
+ class?
288
+
289
+ ### Method verb conjugation
290
+
291
+ I wanted to check if a `String` started with some prefix `-`, so I figured there
292
+ was a method to do it, like most programming language:
293
+
294
+ ```ruby
295
+ 'some string'.starts_with?("some")
296
+ ```
297
+
298
+ But then, I got an error:
299
+
300
+ ```
301
+ NoMethodError: undefined method `starts_with?' for an instance of String
302
+ ```
303
+
304
+ It appears it was a typo and maybe it's a convention to have the verb be in
305
+ infinitive form, so in this case `start_with`, but not for all methods, e.g. `is_a?`.
306
+
307
+ ### Executing shell commands directly from Ruby code
308
+
309
+ It's quite easy to execute some shell commands directly from Ruby code! I was
310
+ quite impressed by the simplicity:
311
+
312
+ ```ruby
313
+ # using Kernel#`
314
+ with_backtick = `ls`
315
+ # or with %x
316
+ another_way = %x|ls|
317
+ ```
318
+
319
+ src: https://www.rubyguides.com/2018/12/ruby-system/
320
+
321
+ ### On default argument
322
+
323
+ There's a particular behavior that I did not expect for default argument.
324
+
325
+ Let's say, we have the following method:
326
+
327
+ ```ruby
328
+ def foobar(var = 1)
329
+ puts var
330
+ end
331
+
332
+ foobar # will print: 1
333
+
334
+ foobar(nil) # will print nothing
335
+ ```
336
+
337
+ So `nil` will not make the method use the default argument. It's by design, so
338
+ be careful when using those default argument.
339
+
340
+ src: https://stackoverflow.com/a/10506137/3612053
341
+
342
+ ### Double colons
343
+
344
+ ```ruby
345
+ # Sometime, we are prefixing the class with ::
346
+ some_var = ::SomePackage::SomeClass
347
+
348
+ # Some other time, we do not
349
+ some_var = SomePackage::SomeClass
350
+ ```
351
+
352
+ I believe the first one is to avoid collision. So then, why not always writing
353
+ the first one? Why bother writing the second form, if there's a risk of
354
+ collision?
355
+
356
+ It's more verbose to add the `::` prefix, because we have to put the absolute
357
+ path to the class.
358
+
359
+ It's all about scopes and readability.
360
+
361
+ In case of ambiguity, go for `::` prefix.
362
+
363
+ Check https://cirw.in/blog/constant-lookup.html for more in-depth information.
364
+
365
+ ### On duck typing
366
+
367
+ Duck typing is really powerful and can make one program flexible and decouple
368
+ code.
369
+
370
+ However, how does one can keep easily keep track of which classes are
371
+ implementing a particular behavior? If we want to rename a method, how can we
372
+ ensure all the implementations are also updated?
373
+
374
+ With static typed programming language, we have the compiler to help us track
375
+ and update the method name.
376
+ For small projects, it is manageable and can be updated with careful `grep`, but
377
+ for really large projects (hundred thousands to millions of LoC), how can one
378
+ keep ~~their sanity~~track of the class methods?
379
+
380
+ Some strategies to track the implementations:
381
+
382
+ - Use [`caller_location`](https://devdocs.io/ruby~3/kernel#method-i-caller_locations)
383
+ to log which classes are calling the monitored method, and monitor for a few
384
+ days/weeks/months, then refactor.
385
+ - Really slow refactor...
386
+ - Rely on failed tests to check the impact radius of the update.
387
+
388
+ An approach is to create an interface-like class:
389
+
390
+ ```ruby
391
+ class Foobar
392
+ def a_method
393
+ raise NotImplementedError
394
+ end
395
+
396
+ def another_method
397
+ raise NotImplementedError
398
+ end
399
+ end
400
+
401
+ class SomeFoobarImplementation
402
+ def a_method
403
+ puts 'Implementation of Foobar#a_method'
404
+ end
405
+
406
+ def another_method
407
+ puts 'Implementation of Foobar#another_method'
408
+ end
409
+ end
410
+ ```
411
+
412
+ Another approach with tests instead of interfaces:
413
+ https://morningcoffee.io/interfaces-in-ruby.html
414
+
415
+ ### Getter on boolean
416
+
417
+ We can easily add getters with the keyword `attr_reader`. However, by
418
+ convention, methods that return a boolean should have the suffix `?`.
419
+
420
+ But `attr_reader` does not seem to add this suffix `?` to the boolean variable
421
+ (which is logical, since Ruby is a dynamic programming language, so it cannot
422
+ know in advance if the variable is boolean or not).
423
+
424
+ Do we have to manually implement this getter?
425
+
426
+ ```ruby
427
+ class Foobar
428
+ def initialize
429
+ @foo = true
430
+ end
431
+
432
+ def foo?
433
+ @foo
434
+ end
435
+ end
436
+ ```
437
+
438
+ There is no default accessors for `boolean` because Ruby does not know if the
439
+ variable is a `boolean` or not.
440
+
441
+ ### On ensure
442
+
443
+ While I was implementing the `FileReader`, I must close the file regardless of
444
+ the result (otherwise, bad things may happen).
445
+
446
+ So, I used the `rescue` keyword:
447
+
448
+ ```ruby
449
+ def foobar
450
+ file = File.open('/path/to/file')
451
+ # so some stuff with file
452
+ ensure
453
+ file&.close
454
+ end
455
+ ```
456
+
457
+ Is it the right way to do it? Is there a better way?
458
+
459
+ In general, if the class provides a block in their methods, it will take care of
460
+ cleaning up. Example:
461
+
462
+ ```ruby
463
+ File.open(@filepath, 'r').each_line do |line|
464
+ # Do something with file.
465
+ end
466
+ # File is now closed here.
467
+ ```
468
+
469
+ Otherwise, using `ensure` is also an idiomatic way of closing resources.
@@ -0,0 +1,5 @@
1
+ FROM ruby:alpine3.20
2
+
3
+ COPY clock.rb /
4
+
5
+ CMD [ "ruby", "/clock.rb" ]
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ loop do
4
+ puts Time.now
5
+ sleep 1
6
+ # NOTE: You need to flush stdout so that it's printed out in the console!
7
+ $stdout.flush
8
+ end
@@ -0,0 +1,5 @@
1
+ FROM ruby:alpine3.20
2
+
3
+ COPY clock_json.rb /
4
+
5
+ CMD [ "ruby", "/clock_json.rb" ]
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('json')
4
+
5
+ def print_log
6
+ now = "#{Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3N")}Z"
7
+ {
8
+ '@timestamp' => now,
9
+ 'log.level' => 'INFO',
10
+ 'message' => "Time is #{now}"
11
+ }
12
+ end
13
+
14
+ loop do
15
+ puts JSON[print_log]
16
+ sleep 1
17
+ $stdout.flush
18
+ end
data/k3d/default.yml ADDED
@@ -0,0 +1,23 @@
1
+ ---
2
+ #
3
+ # Local Kubernetes cluster.
4
+ # src: https://k3d.io/v5.6.3/usage/configfile/
5
+ #
6
+
7
+ apiVersion: k3d.io/v1alpha5
8
+ kind: Simple
9
+ servers: 1
10
+ agents: 0
11
+ image: docker.io/rancher/k3s:v1.30.1-k3s1
12
+ # ingress
13
+ ports:
14
+ - port: 80:80
15
+ nodeFilters:
16
+ - server:0
17
+ # will use host docker registry
18
+ registries:
19
+ create:
20
+ name: registry.localhost
21
+ host: "0.0.0.0"
22
+ hostPort: "5000"
23
+
data/kubetailrb.png ADDED
Binary file
data/lib/boolean.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Just a simple module to represent a boolean in Ruby, as it appears there's
5
+ # not much to know if an object is a boolean or not...
6
+ # So I'll use this occasion to override some native classes like suggested.
7
+ # src: https://stackoverflow.com/a/3028378/3612053
8
+ #
9
+ module Boolean
10
+ end
11
+
12
+ # Include the `Boolean` module to native `TrueClass` so I can do something liks
13
+ # this: true.is_a?(Boolean).
14
+ class TrueClass
15
+ include Boolean
16
+ end
17
+
18
+ # Include the `Boolean` module to native `TrueClass` so I can do something liks
19
+ # this: false.is_a?(Boolean).
20
+ class FalseClass
21
+ include Boolean
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'opts_parser'
4
+
5
+ module Kubetailrb
6
+ # CLI application to run kubetailrb.
7
+ class CLI
8
+ def execute(*args)
9
+ cmd = OptsParser.new(*args).parse
10
+
11
+ # NOTE: Is it better to use this approach by checking the method existence
12
+ # or is it better to use a raise/rescue approach? Or another approach?
13
+ raise 'Invalid cmd' unless cmd.respond_to?(:execute)
14
+
15
+ begin
16
+ cmd.execute
17
+ # Capture Ctrl+c so the program will not display an error in the
18
+ # terminal.
19
+ rescue SignalException
20
+ puts '' # No need to display anything.
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kubetailrb/file_reader'
4
+
5
+ module Kubetailrb
6
+ module Cmd
7
+ # Command to read a file.
8
+ class File
9
+ DEFAULT_NB_LINES = 10
10
+ DEFAULT_FOLLOW = false
11
+ FILE_FLAG = '--file'
12
+ TAIL_FLAG = '--tail'
13
+ attr_reader :reader
14
+
15
+ def initialize(filepath:, last_nb_lines: DEFAULT_NB_LINES, follow: DEFAULT_FOLLOW)
16
+ @reader = Kubetailrb::FileReader.new(filepath: filepath, last_nb_lines: last_nb_lines, follow: follow)
17
+ end
18
+
19
+ def execute
20
+ @reader.read
21
+ end
22
+
23
+ class << self
24
+ def create(*args)
25
+ new(filepath: parse_filepath(*args), last_nb_lines: parse_nb_lines(*args), follow: parse_follow(*args))
26
+ end
27
+
28
+ def applicable?(*args)
29
+ args.include?(FILE_FLAG)
30
+ end
31
+
32
+ private
33
+
34
+ #
35
+ # Parse the file path from arguments provided in the CLI, e.g.
36
+ #
37
+ # kubetailrb --file /path/to/file
38
+ #
39
+ def parse_filepath(*args)
40
+ index = args.find_index { |arg| arg == FILE_FLAG }.to_i
41
+
42
+ raise MissingFileError, "Missing #{FILE_FLAG} value." if args[index + 1].nil?
43
+
44
+ args[index + 1]
45
+ end
46
+
47
+ #
48
+ # Parse nb lines from arguments provided in the CLI, e.g.
49
+ #
50
+ # kubetailrb --file /path/to/file --tail 3
51
+ #
52
+ # will return 3.
53
+ #
54
+ # Will raise `MissingNbLinesValueError` if the value is not provided:
55
+ #
56
+ # kubetailrb --file /path/to/file --tail
57
+ #
58
+ # Will raise `InvalidNbLinesValueError` if the provided value is not a
59
+ # number:
60
+ #
61
+ # kubetailrb --file /path/to/file --tail some-string
62
+ #
63
+ def parse_nb_lines(*args)
64
+ return DEFAULT_NB_LINES unless args.include?(TAIL_FLAG)
65
+
66
+ index = args.find_index { |arg| arg == TAIL_FLAG }.to_i
67
+
68
+ raise MissingNbLinesValueError, "Missing #{TAIL_FLAG} value." if args[index + 1].nil?
69
+
70
+ last_nb_lines = args[index + 1].to_i
71
+
72
+ raise InvalidNbLinesValueError, "Invalid #{TAIL_FLAG} value: #{args[index + 1]}." if last_nb_lines.zero?
73
+
74
+ last_nb_lines
75
+ end
76
+
77
+ def parse_follow(*args)
78
+ flags = %w[-f --follow]
79
+
80
+ return DEFAULT_FOLLOW unless args.any? { |arg| flags.include?(arg) }
81
+
82
+ true
83
+ end
84
+ end
85
+ end
86
+
87
+ class MissingNbLinesValueError < RuntimeError
88
+ end
89
+
90
+ class MissingFileError < RuntimeError
91
+ end
92
+
93
+ class InvalidNbLinesValueError < RuntimeError
94
+ end
95
+ end
96
+ end