lib/sym/app/cli.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'slop'
require 'sym'
require 'colored2'
require 'yaml'
require 'openssl'
require 'highline'

require 'sym/application'
require 'sym/errors'

require 'sym/app/commands'
require 'sym/app/keychain'
require 'sym/app/private_key/handler'

require 'sym/app/output/base'
require 'sym/app/output/file'
require 'sym/app/output/stdout'
require 'sym/app/cli_slop'

module Sym
  module App
    # This is the main interface class for the CLI application.
    # It is responsible for parsing user's input, providing help, examples,
    # coordination of various sub-systems (such as PrivateKey detection), etc.
    #
    # Besides holding the majority of the application state, it contains
    # two primary public methods: +#new+ and +#run+.
    #
    # The constructor is responsible for parsing the flags and determining
    # the the application is about to do. It sets up input/output, but doesn't
    # really execute any encryption or decryption. This happens in the +#run+
    # method called immediately after +#new+.
    #
    # {{Shh::App::CLI}} module effectively performs the translation of
    # the +opts+ object (of type {Slop::Result}) and interpretation of
    # users intentions. It holds on to +opts+ for the duration of the program.
    #
    # == Responsibility Delegated
    #
    # The responsibility of determining the private key from various
    # options provided is performed by the {Sym::App::PrivateKey::Handler}
    # instance. See there for more details.
    #
    # Subsequently, +#run+ method handles the finding of the appropriate
    # {Sym::App::Commands::BaseCommand} subclass to respond to user's request.
    # Command registry, sorting, command dependencies, and finding them is
    # done by the {Sym::App::Coommands} module.
    #
    # User input is handled by the {Sym::App::Input::Handler} instance, while
    # the output is provided by the procs in the {Sym::App::Output} classes.
    #
    # Finally, the Mac OS-X -specific usage of the KeyChain, is encapsulated
    # in a cross-platform way inside the {Sym::App::Keychain} module.

    class CLI
      # brings in #parse(Array[String] args)
      include CLISlop

      attr_accessor :opts, :application, :outputs, :stdin, :stdout, :stderr, :kernel, :args

      def initialize(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr, kernel = nil)
        self.args   = argv
        self.stdin  = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.kernel = kernel

        Sym::App.stdin  = stdin
        Sym::App.stdout = stdout
        Sym::App.stderr = stderr

        begin
          # Re-map any legacy options to the new options
          self.opts = parse(args)

          if opts[:user_home]
            Constants.user_home = opts[:user_home]
            raise InvalidSymHomeDirectory, "#{opts[:user_home]} does not exist!" unless Dir.exist?(Constants.user_home)
          end

          # Deal with SYM_ARGS and -A
          if opts[:sym_args] && non_empty_array?(sym_args)
              args << sym_args
              args.flatten!
              args.compact!
              args.delete('-A')
              args.delete('--sym-args')
              self.opts = parse(args)
          end

          # Disable coloring if requested, or if piping STDOUT
          if opts[:no_color] || !self.stdout.tty?
            Colored2.disable! # reparse options without the colors to create new help msg
            self.opts = parse(args)
          end

        rescue StandardError => e
          log :error, "#{e.message}" if opts
          error exception: e
          quit!(127) if stdin == $stdin
        end

        self.application = ::Sym::Application.new(self.opts, stdin, stdout, stderr, kernel)
      end

      def quit!(code = 0)
        exit(code)
      end

      def sym_args
        (ENV['SYM_ARGS']&.split(/\s+/) || [])
      end

      def execute!
        execute
      end

      def execute
        return Sym::App.exit_code if Sym::App.exit_code != 0
        result = application.execute
        if result.is_a?(Hash)
          self.output_proc ::Sym::App::Args.new({}).output_class
          error(result)
        end
        Sym::App.exit_code
      end

      def command
        @command ||= self.application.command if self.application
      end

      def output_proc(proc = nil)
        if self.application
          self.application.output = proc if proc
          return self.application.output
        end
        nil
      end

      def opts_present
        opts.to_hash.tap do |o|
          o.keys.map { |k| opts[k] ? nil : k }.compact.each { |k| o.delete(k) }
        end
      end

      def log(*args)
        Sym::App.log(*args, **opts.to_hash)
      end


      private

      def non_empty_array?(object)
        object.is_a?(Array) && !object.empty?
      end

      def error(hash)
        hash.merge!(config: opts.to_hash) if opts
        hash.merge!(command: @command) if @command
        Sym::App.error(**hash)
      end

    end
  end
end