Module: NRSER::Rash::CLI

Includes:
SemanticLogger::Loggable
Defined in:
lib/nrser/rash/cli/call.rb,
lib/nrser/rash/cli/help.rb,
lib/nrser/rash/cli/list.rb,
lib/nrser/rash/cli/run.rb,
lib/nrser/rash/cli.rb

Overview

Where the CLI commands live as class methods.

Class Method Summary collapse

Class Method Details

.call(name, argv) ⇒ Object

Call a Rash function.

Parameters:

  • name (String)

    Name of the function.

  • argv (Array<String>)

    The shell arguments.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
# File 'lib/nrser/rash/cli/call.rb', line 24

def self.call name, argv

  logger.trace "calling #{ name.inspect }",
    argv: argv
  
  # look for options
  options = {}
  args = argv.reject do |arg|
    # long options of form '--<name>=<value>'
    if m = arg.match(/\A--([^=]*)(=?)(.*)/)
      options[m[1].to_sym] = if m[2] == ''
        true
      else
        m[3]
      end
      true
    # short options of form '-<char>'
    elsif arg.start_with? '-'
      arg[1..-1].each_char do |char|
        options[char] = true
      end
      true
    end
  end
  
  logger.trace "Extracted options",
    argv: argv,
    args: args,
    options: options

  NRSER::Rash.load_functions
  
  if name.include? '.'
    *namespace, method_name = name.split '.'
    namespace = namespace.flat_map { |s| s.split '::' }
  else
    namespace = []
    method_name = name
  end
  
  # Find the module the function is in
  mod = NRSER::Rash::Functions
  namespace.each do |submod_name|
    mod = mod.const_get(submod_name.capitalize)
  end
  
  # Get the function itself ({Metood} instance)
  meth = mod.method method_name
  
  # Symbolize option keys if the method takes keyword args
  if meth.parameters.any? { |type, *_|
    [:key, :keyreq, :keyrest].include? type
  }
    options = options.map { |key, value|
      [key.to_sym, value]
    }.to_h
  end
  
  # options are the last arg, unless we didn't find any
  #
  # NOTE: this causes functions without an optional `options` arg
  #       on the end to raise an exception when called with any options.
  #       
  #       i think this is the correct behavior: the function can't handle
  #       options, and should error out.
  args << options unless options.empty?
  
  begin
    result = meth.call *args
  rescue Exception => error
    if NRSER::Rash.config( 'PRINT_ERRORS' ).truthy?
      if NRSER::Rash.config( 'STACKTRACE' ).truthy?
        raise error
      else
        logger.fatal error
        # this is the error code that ruby seems to exit with when you
        # don't check the exception
        exit 1
      end
    end
  end
  
  logger.debug "Function #{ name } returned",
    result: result
  
  if formatted = format( result )
    puts formatted
  end
  
  exit 0
end

.format(obj) ⇒ Object

formats an object to write to $stdout. returns nil if there's nothing to write (different than returning '', indicating the empty string should be written).



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/nrser/rash/cli/call.rb', line 120

def self.format(obj)
  # TODO: should formatter be overridable with a env var?
  # formatter = begin config('FORMATTER'); rescue NameError => e; nil; end
  # logger.debug "formatter: #{ formatter.inspect }"
  if obj.rash_formatter
    # there is some sort of formatting info
    # even if the value is a string, we want to follow the instructions
    # hell, maybe we want to write the string out as json or something :/
    if obj.rash_formatter.respond_to? :call
      # the value responds to `call`, so call it to get the value
      # TODO: should we check that it returns a `String`?
      obj.rash_formatter.call(obj)
    # NOTE: `Object.singleton_methods` returns an array of strings in 1.8
    #       and an array of symbols in 1.9, so use strings to work in
    #       both.
    elsif NRSER::Rash::Formatters.
        singleton_methods.
        map {|_| _.to_s }.
        include? obj.rash_formatter.to_s
      # it's a symbol that identifies on of the {NRSER::Rash::Formatters}
      # class methods, so call that
      NRSER::Rash::Formatters.send obj.rash_formatter, obj
    else
      # whatever, call the default
      NRSER::Rash::Formatters.send config('DEFAULT_FORMATTER'), obj
    end
  else
    # there is no formatting info
    if obj.is_a? String
      # in the case that a method passed back a `String`, i think we should
      # consider it formatted and just print it
      obj
    elsif obj.nil?
      # the result is nil and there has been no attempt to format it in
      # a specific way that some program might need to recongize or
      # something. at this point, i'm going to say that the function
      # took some successful action and doesn't have anyhting to say about
      # it, which i think means we shouldn't print anything
    else
      # otherwise, send it to the default formatter
      NRSER::Rash::Formatters.send \
        NRSER::Rash.config('DEFAULT_FORMATTER'), obj
    end
  end
end

.helpObject



16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/nrser/rash/cli/help.rb', line 16

def self.help
  # TODO: woefully incomplete
  puts <<~EOF
    Usage: rash COMMAND [ARGS...]

    Commands:
      list      List functions you can call.
      call      Call a method from NRSER::Rash::Functions.
      test      Run tests for a method from NRSER::Rash::Functions.
      help      This message.
  EOF
end

.listObject

List the Rash functions available.



31
32
33
34
# File 'lib/nrser/rash/cli/list.rb', line 31

def self.list
  NRSER::Rash.load_functions
  puts NRSER::Rash.function_names
end

.runObject

start here.

this is the entry point for the script when executed. it expects a command as the first argument and dispatches to the associated method.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/nrser/rash/cli/run.rb', line 20

def self.run
  SemanticLogger.add_appender io: $stderr, formatter: {
    color: {
      ap: {multiline: true},
    }
  }
  SemanticLogger.default_level = NRSER::Rash.config( 'LOG_LEVEL' ).to_sym
  
  # the command is the first argument
  # we won't need or want it after this, so delete it
  command = ARGV.shift
  # filter out the command line config options
  NRSER::Rash.filter_and_set_config ARGV
  case command
  when 'call'
    # we are being asked to call a function. the name of the function is
    # the second argument (now at index 0 since we deleted the command)
    # and the rest of the arguments are passed to the function
    call ARGV[0], ARGV[1..-1]
  when 'test'
    NRSER::Rash._test_function ARGV.shift #, ARGV[1, ARGV.length]
  when 'list'
    list
  when 'help', '-h', '--help', nil
    help
    # Exit with error so these calls don't work with `&&` in shell
    exit 1
  when '--version', 'version', '-v'
    puts NRSER::Rash::VERSION
  else
    call command, ARGV
  end
end