Class: Gloo::Shell::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/gloo/shell/runner.rb

Direct Known Subclasses

Docs::HelpShell

Constant Summary collapse

DEFAULT_PROMPT =
' -> '.freeze
UNKNOWN_COMMAND =
'Unknown command'.freeze
QUIT_NAME =
'quit'.freeze
QUIT_DESCRIPTION =
'Quit the application'.freeze
QUIT_METHOD =
'cmd_quit'.freeze

Instance Method Summary collapse

Constructor Details

#initialize(engine, opts = {}) ⇒ Runner

Initialize the shell runner.

Parameters:

  • engine (Gloo::App::Engine)

    The running gloo engine. Not used directly by the base Runner, but stored for subclasses' command methods to query (dictionary, heap, etc).

  • opts (Hash) (defaults to: {})

    :prompt, :on_error, :on_unknown_command, :on_empty_command, :before_action, :after_action - all Procs except :prompt (String) and :include_quit (Boolean).



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/gloo/shell/runner.rb', line 45

def initialize( engine, opts = {} )
  @engine = engine
  @prompt_text = opts[ :prompt ]
  @on_error = opts[ :on_error ]
  @on_unknown_command = opts[ :on_unknown_command ]
  @on_empty_command = opts[ :on_empty_command ]
  @before_action = opts[ :before_action ]
  @after_action = opts[ :after_action ]
  @include_quit = opts[ :include_quit ] || false

  @context = Gloo::Shell::Context.new
  @root = Gloo::Shell::CommandNode.new( nil )

  add_quit_command if @include_quit
end

Instance Method Details

#add_command_node(command_data) ⇒ Object

Add a command node to the root, dynamically.

Parameters:

  • command_data (Hash)

    The command data hash.



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/gloo/shell/runner.rb', line 187

def add_command_node( command_data )
  node = build_node_from_data( command_data )

  existing_block = @root.instance_variable_get( :@children_block )
  if existing_block
    existing_nodes = existing_block.call( @context )
    all_nodes = existing_nodes + [ node ]
    @root.instance_variable_set( :@children_block, proc { |_ctx| all_nodes } )
  else
    @root.instance_variable_set( :@children_block, proc { |_ctx| [ node ] } )
  end

  return node
end

#build_node_from_data(data) ⇒ Object

Build a command node from data.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/gloo/shell/runner.rb', line 161

def build_node_from_data( data )
  if data[ :dynamic ]
    return Gloo::Shell::CommandNode.new(
      data[ :name ], description: data[ :description ], obj: data[ :obj ] ) do |ctx|
      ctx.send( data[ :source ] ).map do |item|
        Gloo::Shell::CommandNode.new( item, method: data[ :child_method ], obj: item )
      end
    end
  elsif data[ :children ]
    return Gloo::Shell::CommandNode.new(
      data[ :name ], description: data[ :description ],
      method: data[ :method ], obj: data[ :obj ] ) do |_ctx|
      data[ :children ].map { |child_data| build_node_from_data( child_data ) }
    end
  else
    return Gloo::Shell::CommandNode.new(
      data[ :name ], description: data[ :description ],
      method: data[ :method ], obj: data[ :obj ] )
  end
end

#cmd_quit(_obj, context) ⇒ Object

Quit the shell.



127
128
129
130
# File 'lib/gloo/shell/runner.rb', line 127

def cmd_quit( _obj, context )
  puts 'Quitting…'
  context.done = true
end

#execute_command(command_node, _args, _parent_node = nil) ⇒ Object

Execute a command.



150
151
152
153
154
155
156
# File 'lib/gloo/shell/runner.rb', line 150

def execute_command( command_node, _args, _parent_node = nil )
  if command_node.method
    send( command_node.method, command_node.obj, @context )
  elsif command_node.name && !command_node.description.empty?
    puts "#{command_node.description}: #{command_node.name}"
  end
end

#execute_once(tokens) ⇒ Object

Execute a single command from the given tokens and return.



209
210
211
212
213
214
215
216
217
218
# File 'lib/gloo/shell/runner.rb', line 209

def execute_once( tokens )
  result = traverse( @root, tokens )
  if result[ :node ]
    run_before_action
    execute_command( result[ :node ], tokens, result[ :parent ] )
    run_after_action
  else
    handle_unknown_command
  end
end

#handle_empty_commandObject

Handle an empty command - run the on_empty_command hook if given.



89
90
91
# File 'lib/gloo/shell/runner.rb', line 89

def handle_empty_command
  @on_empty_command&.call
end

#handle_unknown_commandObject

Handle an unknown command - run the on_unknown_command hook if given, otherwise show the default message.



97
98
99
100
101
# File 'lib/gloo/shell/runner.rb', line 97

def handle_unknown_command
  return if @on_unknown_command&.call

  puts UNKNOWN_COMMAND
end

#promptObject

Get the prompt string.



82
83
84
# File 'lib/gloo/shell/runner.rb', line 82

def prompt
  return @prompt_text ? "#{@prompt_text} " : DEFAULT_PROMPT
end

#replObject

Run the REPL loop.



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/gloo/shell/runner.rb', line 277

def repl
  setup_completion

  while !@context.done && ( line = Readline.readline( prompt, true ) )
    tokens = line.strip.split( ' ' )
    if tokens.empty?
      handle_empty_command
      next
    end

    result = traverse( @root, tokens )

    if result[ :node ]
      run_before_action
      execute_command( result[ :node ], tokens, result[ :parent ] )
      run_after_action
    else
      handle_unknown_command
    end
  end
end

#run_after_actionObject

Run the after_action hook, if given.



113
114
115
# File 'lib/gloo/shell/runner.rb', line 113

def run_after_action
  @after_action&.call
end

#run_before_actionObject

Run the before_action hook, if given.



106
107
108
# File 'lib/gloo/shell/runner.rb', line 106

def run_before_action
  @before_action&.call
end

#run_on_errorObject

Run the on_error hook, if given.



120
121
122
# File 'lib/gloo/shell/runner.rb', line 120

def run_on_error
  @on_error&.call
end

#set_context(key, value_list) ⇒ Object

Set a context list, available to dynamic command nodes.



139
140
141
# File 'lib/gloo/shell/runner.rb', line 139

def set_context( key, value_list )
  @context.set( key, value_list )
end

#setup_completionObject

Setup readline completion.



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/gloo/shell/runner.rb', line 247

def setup_completion
  Readline.completion_append_character = ' '
  Readline.basic_word_break_characters = " \t\n\"\\'`@$><=;|&{("

  Readline.completion_proc = proc do |input|
    buffer = Readline.line_buffer
    tokens = buffer.split( ' ' )
    tokens << '' if buffer.end_with?( ' ' )

    result = traverse( @root, tokens[ 0..-2 ] )
    current = result[ :node ]
    options = current ? current.children( @context ).map( &:name ) : []

    matches = options.grep( /^#{Regexp.escape( input )}/ )

    if matches.length > 1
      puts
      current.children( @context ).each do |child|
        puts "#{child.name.ljust( 15 )} #{child.description}" if matches.include?( child.name )
      end
      print "#{prompt}#{buffer}"
    end

    matches
  end
end

#startObject

Start the shell.



68
69
70
# File 'lib/gloo/shell/runner.rb', line 68

def start
  repl
end

#stopObject

Flag the shell as done. Next time through the loop it will stop.



75
76
77
# File 'lib/gloo/shell/runner.rb', line 75

def stop
  @context.done = true
end

#traverse(node, tokens) ⇒ Hash

Traverse the command tree to find the matching node.

Parameters:

  • node (CommandNode)

    The current node.

  • tokens (Array<String>)

    The tokens to traverse.

Returns:

  • (Hash)

    Hash with :node and :parent keys.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/gloo/shell/runner.rb', line 228

def traverse( node, tokens )
  current = node
  parent = nil

  tokens.each do |token|
    children = current.children( @context )
    match = children.find { |c| c.name == token }
    return { node: nil, parent: nil } unless match

    parent = current
    current = match
  end

  return { node: current, parent: parent }
end