Class: RockBooks::CommandLineInterface

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
JournalEntryFilters
Defined in:
lib/rock_books/cmd_line/command_line_interface.rb

Defined Under Namespace

Classes: BadCommandError, Command

Constant Summary collapse

PROJECT_URL =

For conveniently finding the project on Github from the shell

'https://github.com/keithrbennett/rock_books'
HELP_TEXT =

Help text to be used when requested by ‘h’ command, in case of unrecognized or nonexistent command, etc.

"
Command Line Switches:                    [rock-books version #{RockBooks::VERSION} at https://github.com/keithrbennett/rock_books]

-i   input directory specification, default: '#{DEFAULT_INPUT_DIR}'
-o   output (reports) directory specification, default: '#{DEFAULT_OUTPUT_DIR}'
-r   receipts directory, default: '#{DEFAULT_RECEIPT_DIR}'
-s   run in shell mode

Commands:

rec[eipts]                - receipts: a/:a all, m/:m missing, e/:e existing, u/:u unused
rep[orts]                 - return an OpenStruct containing all reports (interactive shell mode only)
w[rite_reports]           - write all reports to the output directory (see -o option)
c[hart_of_accounts]       - chart of accounts
h[elp]                    - prints this help
jo[urnals]                - list of the journals' short names
proj[ect_page]            - prints the RockBooks Github project page URL
rel[oad_data]             - reload data from input files
q[uit]                    - exits this program (interactive shell mode only) (see also 'x')
x[it]                     - exits this program (interactive shell mode only) (see also 'q')

When in interactive shell mode:
* use quotes for string parameters such as method names.
* for pry commands, use prefix `%`.
* you can use the global variable $filter to filter reports

"

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from JournalEntryFilters

account_code, all, any, date_in_range, date_on_or_after, date_on_or_before, day, filter, month, none, null_filter, to_date, year

Constructor Details

#initialize(run_options) ⇒ CommandLineInterface

Returns a new instance of CommandLineInterface.



71
72
73
74
75
76
77
78
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 71

def initialize(run_options)
  @run_options = run_options
  @interactive_mode = !!(run_options.interactive_mode)
  @verbose_mode = run_options.verbose

  validate_run_options(run_options)
  # book_set is set with a lazy initializer
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *method_args) ⇒ Object

For use by the shell when the user types the DSL commands



215
216
217
218
219
220
221
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 215

def method_missing(method_name, *method_args)
  attempt_command_action(method_name.to_s, *method_args) do
    puts(%Q{"#{method_name}" is not a valid command or option. } \
      << 'If you intend for this to be a string literal, ' \
      << 'use quotes or %q{}/%Q{}.')
  end
end

Instance Attribute Details

#book_setObject (readonly)

Returns the value of attribute book_set.



19
20
21
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 19

def book_set
  @book_set
end

#interactive_modeObject (readonly)

Returns the value of attribute interactive_mode.



19
20
21
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 19

def interactive_mode
  @interactive_mode
end

#run_optionsObject (readonly)

Returns the value of attribute run_options.



19
20
21
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 19

def run_options
  @run_options
end

#verbose_modeObject (readonly)

Returns the value of attribute verbose_mode.



19
20
21
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 19

def verbose_mode
  @verbose_mode
end

Instance Method Details

#attempt_command_action(command, *args, &error_handler_block) ⇒ Object

Look up the command name and, if found, run it. If not, execute the passed block.



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 193

def attempt_command_action(command, *args, &error_handler_block)
  no_command_specified = command.nil?
  command = 'help' if no_command_specified

  action = find_command_action(command)
  result = nil

  if action
    result = action.(*args)
  else
    error_handler_block.call
    nil
  end

  if no_command_specified
    puts enclose_in_hyphen_lines('! No operations specified !')
  end
  result
end

#callObject



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 410

def call
  begin
    # By this time, the Main class has removed the command line options, and all that is left
    # in ARGV is the commands and their options.
    if @interactive_mode
      run_shell
    else
      process_command_line
    end

  rescue BadCommandError => error
    separator_line = "! #{'-' * 75} !\n"
    puts '' << separator_line << error.to_s << "\n" << separator_line
    exit(-1)
  end
end

#cmd_cObject



248
249
250
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 248

def cmd_c
  puts chart_of_accounts.report_string
end

#cmd_hObject



253
254
255
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 253

def cmd_h
  print_help
end

#cmd_jObject



258
259
260
261
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 258

def cmd_j
  journal_names = book_set.journals.map(&:short_name)
  interactive_mode ? journal_names : ap(journal_names)
end

#cmd_projObject



300
301
302
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 300

def cmd_proj
  puts 'https://github.com/keithrbennett/rock_books'
end

#cmd_rec(options) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 305

def cmd_rec(options)
  unless run_options.do_receipts
    raise Error.new("Receipt processing was requested but has been disabled with --no-receipts.")
  end

  data = ReceiptsReportData.new(all_entries, run_options.receipt_dir).fetch

  missing, existing, unused = data[:missing], data[:existing], data[:unused]

  print_missing  = -> { puts "\n\nMissing Receipts:";  ap missing }
  print_existing = -> { puts "\n\nExisting Receipts:"; ap existing }
  print_unused   = -> { puts "\n\nUnused Receipts:";   ap unused }

  case options.first.to_s
    when 'a'  # all
      if run_options.interactive_mode
        data
      else
        print_missing.()
        print_existing.()
        print_unused.()
      end

    when 'm'
      run_options.interactive_mode ? missing : print_missing.()

    when 'e'
      run_options.interactive_mode ? existing : print_existing.()

    when 'u'
      run_options.interactive_mode ? unused : print_unused.()

    when 'x'
      run_options.interactive_mode ? missing : print_missing.()
      run_options.interactive_mode ? unused : print_unused.()

  else
    message = "Invalid option for receipts." + \
        " Must be 'a' for all, 'm' for missing, 'e' for existing, 'u' for unused, or 'x' for errors (missing/unused)."
    if run_options.interactive_mode
      puts message
    else
      raise Error.new(message)
    end
  end
end

#cmd_relObject



275
276
277
278
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 275

def cmd_rel
  reload_data
  nil
end

#cmd_repObject

All reports as Ruby objects; only makes sense in shell mode.



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 282

def cmd_rep
  unless run_options.interactive_mode
    raise Error.new("Option 'all_reports' is only available in shell mode. Try 'write_reports'.")
  end

  os = OpenStruct.new(book_set.all_reports($filter))

  # add hash methods for convenience
  def os.keys; to_h.keys; end
  def os.values; to_h.values; end

  # to access as array, e.g. `a.at(1)`
  def os.at(index); self.public_send(keys[index]); end

  os
end

#cmd_wObject



352
353
354
355
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 352

def cmd_w
  BookSetReporter.new(book_set, run_options.output_dir, $filter).generate
  nil
end

#cmd_xObject



358
359
360
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 358

def cmd_x
  quit
end

#commandsObject



363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 363

def commands
  @commands_ ||= [
      Command.new('rec', 'receipts',          -> (*options)  { cmd_rec(options)  }),
      Command.new('rep', 'reports',           -> (*_options) { cmd_rep           }),
      Command.new('w',   'write_reports',     -> (*_options) { cmd_w             }),
      Command.new('c',   'chart_of_accounts', -> (*_options) { cmd_c             }),
      Command.new('jo',  'journals',          -> (*_options) { cmd_j             }),
      Command.new('h',   'help',              -> (*_options) { cmd_h             }),
      Command.new('proj','project_page',      -> (*_options) { cmd_proj          }),
      Command.new('q',   'quit',              -> (*_options) { cmd_x             }),
      Command.new('rel', 'reload_data',       -> (*_options) { cmd_rel           }),
      Command.new('x',   'xit',               -> (*_options) { cmd_x             })
  ]
end

#enclose_in_hyphen_lines(string) ⇒ Object



148
149
150
151
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 148

def enclose_in_hyphen_lines(string)
  hyphen_line = "#{'-' * 80}\n"
  hyphen_line + string + "\n" + hyphen_line
end

#find_command_action(command_string) ⇒ Object



379
380
381
382
383
384
385
386
387
388
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 379

def find_command_action(command_string)
  return nil if command_string.nil?

  result = commands.detect do |cmd|
    cmd.max_string.start_with?(command_string) \
  && \
  command_string.length >= cmd.min_string.length  # e.g. 'c' by itself should not work
  end
  result ? result.action : nil
end

#load_dataObject Also known as: reload_data



269
270
271
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 269

def load_data
  @book_set = BookSetLoader.load(run_options)
end

#post_process(object) ⇒ Object

If a post-processor has been configured (e.g. YAML or JSON), use it.



392
393
394
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 392

def post_process(object)
  post_processor ? post_processor.(object) : object
end

#post_processorObject



397
398
399
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 397

def post_processor
  run_options.post_processor
end


143
144
145
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 143

def print_help
  puts HELP_TEXT
end

#process_command_lineObject

Processes the command (ARGV) and any relevant options (ARGV).

CAUTION! In interactive mode, any strings entered (e.g. a network name) MUST be in a form that the Ruby interpreter will recognize as a string, i.e. single or double quotes, %q, %Q, etc. Otherwise it will assume it’s a method name and pass it to method_missing!



230
231
232
233
234
235
236
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 230

def process_command_line
  attempt_command_action(ARGV[0], *ARGV[1..-1]) do
    print_help
    raise BadCommandError.new(
        %Q{! Unrecognized command. Command was #{ARGV.first.inspect} and options were #{ARGV[1..-1].inspect}.})
  end
end

#quitObject



239
240
241
242
243
244
245
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 239

def quit
  if interactive_mode
    exit(0)
  else
    puts "This command can only be run in shell mode."
  end
end

#run_pryObject

Pry will output the content of the method from which it was called. This small method exists solely to reduce the amount of pry’s output that is not needed here.



157
158
159
160
161
162
163
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 157

def run_pry
  binding.pry

  # the seemingly useless line below is needed to avoid pry's exiting
  # (see https://github.com/deivid-rodriguez/pry-byebug/issues/45)
  _a = nil
end

#run_shellObject

Runs a pry session in the context of this object. Commands and options specified on the command line can also be specified in the shell.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 168

def run_shell
  begin
    require 'pry'
  rescue LoadError
    message = "The 'pry' gem and/or one of its prerequisites, required for running the shell, was not found." +
        " Please `gem install pry` or, if necessary, `sudo gem install pry`."
    raise Error.new(message)
  end

  print_help

  # Enable the line below if you have any problems with pry configuration being loaded
  # that is messing up this runtime use of pry:
  # Pry.config.should_load_rc = false

  # Strangely, this is the only thing I have found that successfully suppresses the
  # code context output, which is not useful here. Anyway, this will differentiate
  # a pry command from a DSL command, which _is_ useful here.
  Pry.config.command_prefix = '%'

  run_pry
end

#td(date_string) ⇒ Object

Easier than remembering and typing Date.iso8601.



405
406
407
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 405

def td(date_string)
  Date.iso8601(date_string)
end

#validate_run_options(options) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/rock_books/cmd_line/command_line_interface.rb', line 81

def validate_run_options(options)

  if [
      # the command requested was to show the project page
      find_command_action(ARGV[0]) == find_command_action('proj'),

      options.suppress_command_line_validation,
  ].any?
    return  # do not validate
  end

  validate_input_dir = -> do
    File.directory?(options.input_dir) ? nil : "Input directory '#{options.input_dir}' does not exist. "
  end

  validate_output_dir = -> do

    # We need to create the reports directory if it does not already exist.
    # mkdir_p silently returns if the directory already exists.
    begin
      FileUtils.mkdir_p(options.output_dir)
      nil
    rescue Errno::EACCES => error
      "Output directory '#{options.output_dir}' does not exist and could not be created. "
    end
  end

  validate_receipt_dir = -> do
    File.directory?(options.receipt_dir) ? nil : \
        "Receipts directory '#{options.receipt_dir}' does not exist. "
  end

  output = []
  output << validate_input_dir.()
  output << validate_output_dir.()
  if run_options.do_receipts
    output << validate_receipt_dir.()
  end

  output.compact!

  unless output.empty?
    message = <<~HEREDOC
    #{output.compact.join("\n")}

    Running this program assumes that you you have:

    * an input directory containing documents with your accounting data. 
      The default directory for this is #{DEFAULT_INPUT_DIR} and can be overridden
      with the -i/--input_dir option.

    * Unless receipt handling is disabled with the --no-receipts option,
      a directory where receipts can or will be stored.
      The default directory for this is #{DEFAULT_RECEIPT_DIR} and can be overridden
      with the -r/--receipt_dir option.
    
    HEREDOC
    raise Error.new(message)
  end
end