Class: ShellOpts::Grammar::Command

Inherits:
IdrNode show all
Defined in:
lib/shellopts/grammar.rb,
lib/shellopts/dump.rb,
lib/shellopts/dump.rb,
lib/shellopts/parser.rb,
lib/shellopts/analyzer.rb,
lib/shellopts/renderer.rb,
lib/shellopts/formatter.rb

Overview

brief one-line commands should optionally use compact options

Direct Known Subclasses

Program

Constant Summary collapse

OPTIONS_ABBR =
"[OPTIONS]"
COMMANDS_ABBR =
"[COMMANDS]"
DESCRS_ABBR =
"ARGS..."

Constants inherited from Node

Node::ALLOWED_PARENTS

Instance Attribute Summary collapse

Attributes inherited from IdrNode

#attr, #command, #ident, #name, #path, #uid

Attributes inherited from Node

#children, #parent, #token

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from IdrNode

#dump_doc

Methods inherited from Node

#analyzer_error, #ancestors, #dump_ast, #dump_attrs, #inspect, #parents, parse, #parser_error, #remove_arg_descr_nodes, #remove_arg_spec_nodes, #remove_brief_nodes, #traverse

Constructor Details

#initialize(parent, token) ⇒ Command

Returns a new instance of Command.



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/shellopts/grammar.rb', line 215

def initialize(parent, token)
  @brief = nil
  @default_brief = nil
  @description = []
  @option_groups = []
  @options = []
  @options_hash = {} # Initialized by the analyzer
  @commands = []
  @commands_hash = {} # Initialized by the analyzer
  @specs = []
  @descrs = []
  super
end

Instance Attribute Details

#briefObject

Brief description of command



193
194
195
# File 'lib/shellopts/grammar.rb', line 193

def brief
  @brief
end

#commandsObject (readonly)

Array of sub-commands. Initialized by the parser but edited by the analyzer



207
208
209
# File 'lib/shellopts/grammar.rb', line 207

def commands
  @commands
end

#descriptionObject (readonly)

Description of command. Array of Paragraph or Code objects. Initialized by the parser



197
198
199
# File 'lib/shellopts/grammar.rb', line 197

def description
  @description
end

#descrsObject (readonly)

Array of ArgDescr objects. Initialized by the parser



213
214
215
# File 'lib/shellopts/grammar.rb', line 213

def descrs
  @descrs
end

#option_groupsObject (readonly)

Array of option groups in declaration order. Initialized by the parser TODO: Rename ‘groups’



201
202
203
# File 'lib/shellopts/grammar.rb', line 201

def option_groups
  @option_groups
end

#optionsObject (readonly)

Array of options in declaration order. Initialized by the analyzer



204
205
206
# File 'lib/shellopts/grammar.rb', line 204

def options
  @options
end

#specsObject (readonly)

Array of Arg objects. Initialized by the parser



210
211
212
# File 'lib/shellopts/grammar.rb', line 210

def specs
  @specs
end

#supercommandObject (readonly)

Supercommand or nil if this is the top-level Program object. Initialized by the analyzer



190
191
192
# File 'lib/shellopts/grammar.rb', line 190

def supercommand
  @supercommand
end

Class Method Details

.command(obj) ⇒ Object

Shorthand to get the associated Grammar::Command object from a Program or a Grammar::Command object



249
250
251
252
# File 'lib/shellopts/grammar.rb', line 249

def self.command(obj)
  constrain obj, Command, ::ShellOpts::Program
  obj.is_a?(Command) ? obj : obj.__grammar__
end

Instance Method Details

#[](key) ⇒ Object

Maps from any (sub-)path, name or identifier of an option or command (including the suffixed ‘!’) to the associated option. #[] and #key? can’t be used until after the analyze phase



232
233
234
235
236
237
238
239
240
# File 'lib/shellopts/grammar.rb', line 232

def [](key)
  case key
    when String; lookup(key.split("."))
    when Symbol; lookup(key.to_s.sub(".", "!.").split(".").map(&:to_sym))
    when Array; lookup(key)
  else
    nil
  end
end

#collect_optionsObject



27
28
29
# File 'lib/shellopts/analyzer.rb', line 27

def collect_options
  @options = option_groups.map(&:options).flatten
end

#compute_command_hashesObject



54
55
56
57
58
59
60
61
62
63
# File 'lib/shellopts/analyzer.rb', line 54

def compute_command_hashes
  commands.each { |command|
    # TODO Check for dash-collision
    !@commands_hash.key?(command.name) or 
        analyzer_error command.token, "Duplicate command name: #{command.name}"
    @commands_hash[command.name] = command
    @commands_hash[command.ident] = command
    command.compute_command_hashes
  }
end

#compute_option_hashesObject



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/shellopts/analyzer.rb', line 41

def compute_option_hashes
  options.each { |option|
    option.idents.zip(option.names).each { |ident, name|
      !@options_hash.key?(name) or 
          analyzer_error option.token, "Duplicate option name: #{name}"
      @options_hash[name] = option
      !@options_hash.key?(ident) or 
          analyzer_error option.token, "Can't use both #{@options_hash[ident].name} and #{name}"
      @options_hash[ident] = option
    }
  }
end

#dump_idr(short = false) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/shellopts/dump.rb', line 102

def dump_idr(short = false)
  if short
    puts name
    indent { 
      options.each { |option| option.dump_idr(short) }
      commands.each { |command| command.dump_idr(short) }
      descrs.each { |descr| descr.dump_idr(short) }
    } 
  else
    puts "#{name}: #{classname}"
    dump_attrs :uid, :path, :ident, :name, :options, :commands, :specs, :descrs, :brief
  end
end

#dump_structure(device = $stdout) ⇒ Object



22
23
24
25
26
27
28
29
# File 'lib/shellopts/dump.rb', line 22

def dump_structure(device = $stdout)
  device.puts ident
  device.indent { |dev|
    option_groups.each { |group| dev.puts group.options.map(&:name).join(" ") }
    commands.each { |command| command.dump_structure(dev) }
    descrs.each { |descr| dev.puts descr.text }
  }
end

#key?(key) ⇒ Boolean

Returns:



242
# File 'lib/shellopts/grammar.rb', line 242

def key?(key) !self.[](key).nil? end

#keysObject

Mostly for debug. Has questional semantics because it only lists local keys



245
# File 'lib/shellopts/grammar.rb', line 245

def keys() @options_hash.keys + @commands_hash.keys end

#names(root: false) ⇒ Object



94
95
96
# File 'lib/shellopts/renderer.rb', line 94

def names(root: false)
  (root ? ancestors : []) + [self]
end

#parseObject



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/shellopts/parser.rb', line 108

def parse
  if parent
    path_names = token.source.sub("!", "").split(".")
    @name = path_names.last
    @path = path_names.map { |cmd| "#{cmd}!".to_sym }
  else
    @path = []
    @name = token.source
  end
  super
end

#puts_briefObject



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
# File 'lib/shellopts/formatter.rb', line 55

def puts_brief
  width = Formatter.rest
  option_briefs = option_groups.map { |group| [group.render(:enum), group.brief&.words] }
  command_briefs = commands.map { |command| [command.render(:single, width), command.brief&.words] }
  widths = Formatter::compute_columns(width, option_briefs + command_briefs)

  if brief
    puts brief
    puts
  end

  puts "Usage"
  indent { puts_usage(bol: true) }

  if options.any?
    puts
    puts "Options"
    indent { Formatter::puts_columns(widths, option_briefs) }
  end

  if commands.any?
    puts
    puts "Commands"
    indent { Formatter::puts_columns(widths, command_briefs) }
  end
end

#puts_descr(prefix, brief: !self.brief.nil?,, name: :path) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/shellopts/formatter.rb', line 82

def puts_descr(prefix, brief: !self.brief.nil?, name: :path)
  puts Ansi.bold([prefix, render(:single, Formatter.rest)].flatten.compact.join(" "))
  indent {
    if brief
      puts self.brief.words.wrap(Formatter.rest)
    else
      newline = false
      children.each { |child|
        puts if newline
        newline = true

        if child.is_a?(Command)
          child.puts_descr(prefix, name: :path)
         else
          child.puts_descr
        end
      }
    end
  }
end

#puts_helpObject



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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/shellopts/formatter.rb', line 103

def puts_help
  puts Ansi.bold "NAME"
  full_name = [Formatter::command_prefix, name].join
  indent { puts brief ? "#{full_name} - #{brief}" : full_name }
  puts

  puts Ansi.bold "USAGE"
  indent { puts_usage(bol: true) }

  section = {
    Paragraph => "DESCRIPTION",
    OptionGroup => "OPTIONS",
    Command => "COMMANDS"
  }

  newline = false # True if a newline should be printed before child 
  indent {
    children.each { |child|
      if child.is_a?(Section) # Explicit section
#             p :A
        puts
        indent(-1).puts Ansi.bold child.name
        section.delete_if { |_,v| v == child.name }
        section.delete(Paragraph)
        newline = false
        next
      elsif s = section[child.class] # Implicit section
#             p :B
        puts  
        indent(-1).puts Ansi.bold s
        section.delete(child.class)
        section.delete(Paragraph)
        newline = false
      else # Any other node add a newline
#             p :C
        puts if newline
        newline = true
      end

      if child.is_a?(Command)
#             prefix = child.parent != self ? nil : child.supercommand&.name
        prefix = child.supercommand == self ? nil : child.supercommand&.name
        child.puts_descr(prefix, brief: false, name: :path)
        newline = true
       else
        child.puts_descr
        newline = true
      end
    }

    # Also emit commands not declared in nested scope
    (commands - children.select { |child| child.is_a?(Command) }).each { |cmd|
      puts if newline
      newline = true
      prefix = cmd.supercommand == self ? nil : cmd.supercommand&.name
      cmd.puts_descr(prefix, brief: false, name: path)
    }
  }
end

#puts_usage(bol: false) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/shellopts/formatter.rb', line 40

def puts_usage(bol: false)
  if descrs.size == 0
    print (lead = Formatter.command_prefix || "")
    indent(lead.size, ' ', bol: bol && lead == "") { 
      puts render(:multi, Formatter::USAGE_MAX_WIDTH) 
    }
  else
    lead = Formatter.command_prefix || ""
    descrs.each { |descr|
      print lead
      puts render(:single, Formatter::USAGE_MAX_WIDTH, args: [descr.text]) 
    } 
  end
end

#render(format, width, root: false, **opts) ⇒ Object

Format can be one of :single, :enum, or :multi. :single force one-line output and compacts options and commands if needed. :enum outputs a :single line for each argument specification/description, :multi tries one-line output but wrap options if needed. Multiple argument specifications/descriptions are always compacted



84
85
86
87
88
89
90
91
92
# File 'lib/shellopts/renderer.rb', line 84

def render(format, width, root: false, **opts)
  case format
    when :single; render_single(width, **opts)
    when :enum; render_enum(width, **opts)
    when :multi; render_multi2(width, **opts)
  else
    raise ArgumentError, "Illegal format: #{format.inspect}"
  end
end

#render_structureObject

Usable after parsing



16
17
18
19
20
# File 'lib/shellopts/dump.rb', line 16

def render_structure
  io = StringIO.new
  dump_structure(io)
  io.string
end

#reorder_optionsObject

Move options before first command



32
33
34
35
36
37
38
39
# File 'lib/shellopts/analyzer.rb', line 32

def reorder_options
  if commands.any?
    if i = children.find_index { |child| child.is_a?(Command) }
      options, rest = children[i+1..-1].partition { |child| child.is_a?(OptionGroup) }
      @children = children[0, i] + options + children[i..i] + rest
    end
  end
end

#set_supercommandObject



23
24
25
# File 'lib/shellopts/analyzer.rb', line 23

def set_supercommand
  commands.each { |child| child.instance_variable_set(:@supercommand, self) }
end