Module: Xolo::Admin::Interactive

Defined in:
lib/xolo/admin/interactive.rb

Overview

Module for gathering and validating xadm options from an interactive terminal session

Constant Summary collapse

MULTILINE_EDITORS =

Constants

{
  'vim (vi)' => '/usr/bin/vim',
  'mg (emacs)' => '/usr/bin/mg',
  'pico (nano)' => '/usr/bin/pico'
}
MULTILINE_HEADER_SEPARATOR =
"\nDO NOT EDIT anything above the next line:\n=================================="
DEFAULT_HIGHLINE_READLINE_PROMPT =
'Enter value'
HIGHLINE_READLINE_GATHER_ERR_INSTRUCTIONS =
<<~ENDINSTR
  Use tab for auto-completion, tab twice to see available choices
  Type '#{Xolo::X}' to exit."
ENDINSTR

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#last_converted_valueObject

getter/setter for the value converted by the last validation method call - we do this so the same value is available in the

convert and validate lambdas

Returns:

  • (Object)


765
766
767
# File 'lib/xolo/admin/interactive.rb', line 765

def last_converted_value
  @last_converted_value
end

#last_validation_errorString

getter/setter for any validation error message when a validation fails.

Returns:

  • (String)


771
772
773
# File 'lib/xolo/admin/interactive.rb', line 771

def last_validation_error
  @last_validation_error
end

Class Method Details

.included(includer) ⇒ Object

when this module is included



40
41
42
# File 'lib/xolo/admin/interactive.rb', line 40

def self.included(includer)
  Xolo.verbose_include includer, self
end

Instance Method Details

#app_name_bundleid_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



191
192
193
194
195
# File 'lib/xolo/admin/interactive.rb', line 191

def app_name_bundleid_na
  return unless walkthru_cmd_opts[:version_script] || walkthru_cmd_opts[:subscribed]

  'N/A when using Version Script, or subcribing via a Patch Source'
end

#default_for_value(key, deets, curr_val) ⇒ Object

The ‘default’ value for the highline question when prompting for a value TODO: POSSIBLY NOT NEEDED. See commented-out call above



648
649
650
651
652
653
654
# File 'lib/xolo/admin/interactive.rb', line 648

def default_for_value(key, deets, curr_val)
  # default is the current value, or the
  # defined value if no current.
  default = walkthru_cmd_opts[key] || curr_val || deets[:default]
  default = default.join(Xolo::COMMA_JOIN) if default.is_a? Array
  default
end

#display_name_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



162
163
164
165
166
# File 'lib/xolo/admin/interactive.rb', line 162

def display_name_na
  return unless walkthru_cmd_opts[:subscribed]

  'N/A when subcribing via a Patch Source'
end

#display_walkthru_headervoid

This method returns an undefined value.

Clear the terminal window and display the menu header above the highline menu



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/xolo/admin/interactive.rb', line 278

def display_walkthru_header
  header_text = Xolo::Admin::Options::COMMANDS[cli_cmd.command][:walkthru_header].dup
  return unless header_text

  header_text.sub! Xolo::Admin::Options::TARGET_TITLE_PLACEHOLDER, cli_cmd.title if cli_cmd.title
  header_text.sub! Xolo::Admin::Options::TARGET_VERSION_PLACEHOLDER, cli_cmd.version if cli_cmd.version
  if edit_command?
    header_text <<
      if current_opt_values[:subscribed]
        " (subscribed as '#{current_opt_values[:display_name]}')"
      else
        ' (managed)'
      end
  end
  header_sep_line = Xolo::DASH * header_text.length

  system 'clear'
  puts <<~ENDPUTS
    #{header_sep_line}
    #{header_text}
    #{header_sep_line}
    Current Settings -> New Settings

  ENDPUTS
end

#display_walkthru_menuvoid

This method returns an undefined value.

Build and display the walkthru menu for the given command



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
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
# File 'lib/xolo/admin/interactive.rb', line 80

def display_walkthru_menu
  cmd = cli_cmd.command
  done_with_menu = false

  # we start off with our walkthru_cmd_opts being the same
  # the same as current_opt_values
  current_opt_values.to_h.each { |k, v| walkthru_cmd_opts[k] = v }

  until done_with_menu
    # clear the screen and show the menu header
    display_walkthru_header

    # Generate the menu items
    highline_cli.choose do |menu|
      menu.select_by = :index

      menu.responses[:ambiguous_completion] = nil
      menu.responses[:no_completion] = 'Unknown Choice'

      # The menu items for setting values
      cmd_details(cmd)[:opts].each do |key, deets|
        # dont displau items for the other kind of action
        next if deets[:add_only] && edit_command?
        next if deets[:edit_only] && add_command?

        # only show items for the current title type, if applicable
        if deets[:title_type]
          if walkthru_cmd_opts[:subscribed]
            next if deets[:title_type] == Xolo::MANAGED
          elsif deets[:title_type] == Xolo::SUBSCRIBED
            next
          end
        end

        curr_val = current_opt_values[key]

        not_avail = send(deets[:walkthru_na]) if deets[:walkthru_na]

        # if a value is not available, remove any previously set value
        walkthru_cmd_opts.delete_field(key) if not_avail && walkthru_cmd_opts.to_h.key?(key)

        new_val = walkthru_cmd_opts[key]

        menu_item = menu_item_text(deets[:label], oldval: curr_val, newval: new_val, not_avail: not_avail)

        # no processing if item not available
        if not_avail
          menu.choice(menu_item) {}
        else
          menu.choice(menu_item) { prompt_for_walkthru_value key, deets, curr_val }
        end
      end

      # always show 'Cancel' in the same position
      menu.choice(Xolo::CANCEL) do
        done_with_menu = true
        @walkthru_cancelled = true
      end

      # check for any required values missing or if
      # there's internal inconsistency between given values
      still_needed = missing_values
      consistency_error = internal_consistency_error

      # only show 'done' when all required values are there and
      # consistency is OK
      menu.choice(nil, nil, 'Done') { done_with_menu = true } if still_needed.empty? && consistency_error.nil?

      # The prompt will include info about required values and consistency
      prompt = Xolo::BLANK
      prompt = "#{prompt}\n- Missing: #{still_needed.join Xolo::COMMA_JOIN}" unless still_needed.empty?
      prompt = "#{prompt}\n- #{consistency_error}" if consistency_error
      prompt = "#{prompt}\nYour Choice: "
      menu.prompt = prompt
    end

  end # until done with menu
end

#do_walkthruvoid

This method returns an undefined value.

Use an interactive walkthru session to populate Xolo::Admin::Options.walkthru_cmd_opts



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/xolo/admin/interactive.rb', line 63

def do_walkthru
  return unless walkthru?

  # only one readline thing per line
  Readline.completion_append_character = nil
  # all chars are allowed in readline choices
  Readline.basic_word_break_characters = "\n"

  # if the command doesn't take any options, there's nothing to walk through
  return if Xolo::Admin::Options::COMMANDS[cli_cmd.command][:opts].empty?

  display_walkthru_menu
end

#edited_multiline_value(editor, desc, text_to_edit) ⇒ String

Save some text in a temp file, edit it with the desired multiline editor, save it then return the edited value.

Parameters:

  • editor (String, Pathname)

    The path to the editor to use

  • text_to_edit (String)

    The text to edit

Returns:

  • (String)

    the edited text.



881
882
883
884
885
886
887
# File 'lib/xolo/admin/interactive.rb', line 881

def edited_multiline_value(editor, desc, text_to_edit)
  f = Pathname.new(Tempfile.new('xadm-multiline'))
  editor_content = "#{desc.chomp}\n#{MULTILINE_HEADER_SEPARATOR}\n#{text_to_edit}"
  f.pix_save editor_content
  system "#{editor} #{f}"
  f.read.split(MULTILINE_HEADER_SEPARATOR).last
end

#expiration_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



244
245
246
247
248
# File 'lib/xolo/admin/interactive.rb', line 244

def expiration_na
  no_uninstall_script = walkthru_cmd_opts[:uninstall_script].pix_empty?
  no_uninstall_ids = walkthru_cmd_opts[:uninstall_ids].pix_empty?
  'N/A until either uninstall_script or uninstall-ids are set' if no_uninstall_script && no_uninstall_ids
end

#expiration_paths_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



253
254
255
# File 'lib/xolo/admin/interactive.rb', line 253

def expiration_paths_na
  'N/A unless expiration is > 0' unless walkthru_cmd_opts[:expiration].to_i.positive?
end

#highline_cliHighline

Returns Our HighLine instance. Word wrap at terminal-width minus 5.

Returns:

  • (Highline)

    Our HighLine instance. Word wrap at terminal-width minus 5



51
52
53
54
55
56
57
# File 'lib/xolo/admin/interactive.rb', line 51

def highline_cli
  return @highline_cli if @highline_cli

  @highline_cli ||= HighLine.new
  @highline_cli.wrap_at = terminal_word_wrap if STDOUT.tty?
  @highline_cli
end

#internal_consistency_errorString?

Returns any current internal consistency error. will be nil when none remain.

Returns:

  • (String, nil)

    any current internal consistency error. will be nil when none remain



268
269
270
271
272
273
# File 'lib/xolo/admin/interactive.rb', line 268

def internal_consistency_error
  validate_internal_consistency walkthru_cmd_opts
  nil
rescue Xolo::InvalidDataError => e
  e.to_s
end

Returns the menu item text.

Parameters:

  • lbl (String)

    the label to use at the start of the text e.g. ‘Description’

  • oldval (Object) (defaults to: nil)

    the original value before we started doing whatever we are doing

  • newval (Object) (defaults to: nil)

    the latest value that was entered by the user

  • not_avail (String) (defaults to: nil)

    if the menu item is unavailable, this expalins why.

Returns:

  • (String)

    the menu item text



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/xolo/admin/interactive.rb', line 313

def menu_item_text(lbl, oldval: nil, newval: nil, not_avail: nil)
  oldval = oldval.join(Xolo::COMMA_JOIN) if oldval.is_a? Array
  newval = newval.join(Xolo::COMMA_JOIN) if newval.is_a? Array

  txt = "#{lbl}:"
  return "#{txt} ** #{not_avail}" if not_avail

  txt = "#{lbl}: #{oldval}"
  txt = "#{lbl}:\n#{oldval}\n" if txt.length >= terminal_word_wrap
  return txt if oldval == newval

  txt = "#{lbl}: #{oldval} -> #{newval}"
  txt = "#{lbl}:\n#{oldval}\n  ->\n#{newval}\n" if txt.length >= terminal_word_wrap
  txt
end

#missing_valuesArray<String>

Returns The names of any required opts that have no current value. Displayed at the bottom of the walkthru menu.

Returns:

  • (Array<String>)

    The names of any required opts that have no current value. Displayed at the bottom of the walkthru menu.



789
790
791
792
793
794
795
796
797
798
799
800
# File 'lib/xolo/admin/interactive.rb', line 789

def missing_values
  missing_values = []
  required_values.each do |key, deets|
    next if walkthru_cmd_opts[key]

    missing_values << deets[:label]
  end

  missing_values += title_missing_values if title_command?

  missing_values
end

#multiline_editor_to_useString

Prompt for an editor to use from those in MULTILINE_EDITORS

Returns:

  • (String)

    the path to an editor to use for multiline values.



860
861
862
863
864
865
866
867
868
869
870
871
# File 'lib/xolo/admin/interactive.rb', line 860

def multiline_editor_to_use
  return config.editor if config.editor

  highline_cli.choose do |menu|
    menu.select_by = :index
    menu.prompt = 'Choose an editor:'
    MULTILINE_EDITORS.each do |name, cmd|
      menu.choice(cmd, nil, name)
    end # MULTILINE_EDITORS.each
    menu.choice(Xolo::CANCEL)
  end # @cli.choose
end

#patch_source_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



200
201
202
203
204
205
206
207
# File 'lib/xolo/admin/interactive.rb', line 200

def patch_source_na
  # if walkthru_cmd_opts[:version_script] || walkthru_cmd_opts[:app_name] || walkthru_cmd_opts[:app_bundle_id]
  #   'N/A when using Version Script or App Name/BundleID'
  # elsif walkthru_cmd_opts[:publisher] || walkthru_cmd_opts[:display_name]
  #   'N/A when using Display Name or Publisher'
  # end
  nil unless walkthru_cmd_opts[:subscribed]
end

#prompt_for_local_files_via_readline(question:, q_desc:, deets:, validate: nil) ⇒ Object

Highline’s ability to do autocompletion for local file selection is limited at best (it only will autocomplete within a single directory, defaulting to the one containing the executable)

So if we want a shell-style autocompletion for selecting one or more files then we’ll use readline directly, where its pretty simple to do.

Parameters:

  • question (String)

    The question to ask

  • q_desc (String)

    A longer description of what we’re asking for

  • deets (Hash)

    The option-details for the value for which we are prompting

  • validate (Lambda) (defaults to: nil)

    The lambda for validating the answer before conversion

Returns:

  • (Object)

    The validated and converted value given by the user.



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/xolo/admin/interactive.rb', line 515

def prompt_for_local_files_via_readline(question:, q_desc:, deets:, validate: nil)
  prompt = setup_for_readline_local_files(deets)

  highline_cli.say "#{q_desc}\n#{question}"

  validated_new_val = deets[:multi] ? [] : nil
  all_done = false
  until all_done
    latest_input = Readline.readline(prompt, true).strip
    # dragging in items from the finder will esacpe spaces in the path with \'s
    # in the shell this is good, but ruby is interpreting the \'s, so lets remove them.
    latest_input.gsub!(/\\ /, ' ')

    break if latest_input == Xolo::X
    return Xolo::NONE if !deets[:required] && (latest_input == Xolo::NONE)

    if validate
      if validate.call latest_input
        latest_input = last_converted_value
      else
        highline_cli.say "#{last_validation_error}\nType 'x' to exit"
        next
      end
    end
    # if we are here, the latest_input is valid

    # We only validate individual items, but the validation
    # method might return an array (which it does for CLI option validation
    # for options that are stored in arrays - it validates them  all at once)
    # so deal with that here or we'll get nested arrays here.
    latest_input = latest_input.first if latest_input.is_a?(Array)

    if deets[:multi]
      validated_new_val << latest_input
    else
      validated_new_val = latest_input
      all_done = true
    end

  end # until all_done

  validated_new_val
end

#prompt_for_multi_values_with_highline(question:, q_desc:, deets:, convert: nil, validate: nil) ⇒ Array, String

Prompt for an array of values using highline ‘ask’ with ‘gather’ and possibly readline auto-completion from an array

Parameters:

  • question (String)

    The question to ask

  • q_desc (String)

    A longer description of what we’re asking for

  • convert (Lambda) (defaults to: nil)

    The lambda for converting the validated value

  • validate (Lambda) (defaults to: nil)

    The lambda for validating the answer before conversion

  • deets (Hash)

    The option-details for the value for which we are prompting

Returns:

  • (Array, String)

    The validated and converted values given by the user, or ‘none’



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/xolo/admin/interactive.rb', line 440

def prompt_for_multi_values_with_highline(question:, q_desc:, deets:, convert: nil, validate: nil)
  use_readline, convert, validate = setup_for_readline_in_highline(deets, convert, validate)

  highline_cli.say q_desc

  chosen_values = highline_cli.ask(question, convert) do |q|
    if use_readline
      q.readline = true
      q.responses[:no_completion] = "Unknown Choice.#{HIGHLINE_READLINE_GATHER_ERR_INSTRUCTIONS}"
      q.responses[:ambiguous_completion] = "Ambiguous Choice.#{HIGHLINE_READLINE_GATHER_ERR_INSTRUCTIONS}"
    end
    if validate
      q.validate = validate
      q.responses[:not_valid] = ->(_x) { "\nERROR: #{last_validation_error}" }
      q.responses[:ask_on_error] = :question
    end
    q.gather = Xolo::X
  end
  chosen_values.flatten!

  # don't return an empty array if none was chosen, but
  # return 'none' so that the whole value is cleared.
  chosen_values = Xolo::NONE if chosen_values.include? Xolo::NONE
  chosen_values
end

#prompt_for_single_value_with_highline(question:, q_desc:, convert:, validate:, deets:) ⇒ Object

Prompt for a one-line single value via highline, possibly with readline auto-completion from an array of possible values

Parameters:

  • question (String)

    The question to ask

  • q_desc (String)

    A longer description of what we’re asking for

  • convert (Lambda)

    The lambda for converting the validated value

  • validate (Lambda)

    The lambda for validating the answer before conversion

  • deets (Hash)

    The option-details for the value for which we are prompting

Returns:

  • (Object)

    The validated and converted value given by the user.



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/xolo/admin/interactive.rb', line 412

def prompt_for_single_value_with_highline(question:, q_desc:, convert:, validate:, deets:)
  use_readline, convert, validate = setup_for_readline_in_highline(deets, convert, validate)

  highline_cli.say q_desc

  highline_cli.ask(question, convert) do |q|
    q.readline = use_readline
    q.echo = '*' if deets[:secure_interactive_input]

    if validate
      q.validate = validate
      q.responses[:not_valid] = ->(_x) { "\nERROR: #{last_validation_error}" }
      q.responses[:ask_on_error] = :question
    end
  end
end

#prompt_for_walkthru_value(key, deets, _curr_val) ⇒ void

This method returns an undefined value.

prompt for an option value and store it in walkthru_cmd_opts

Parameters:

  • key (Symbol)

    One of the keys of the opts hash for the current command; the value for which we are prompting

  • deets (Hash)

    the details about the option key we prompting for

  • curr_val (Object)

    The current value of the option, if any



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/xolo/admin/interactive.rb', line 338

def prompt_for_walkthru_value(key, deets, _curr_val)
  # current_value = default_for_value(key, deets, curr_val) # Needed??
  current_value = walkthru_cmd_opts[key]
  q_desc = question_desc(deets)
  question = question_for_value(deets)

  # Highline wants a separate lambda for conversion
  # and validation, validation just returns boolean,
  # but conversion returns the converted value.
  # but our validation methods do the conversion.
  #
  # so we'll just return the last_converted_value we got
  # when we validate, or nil if we don't validate
  #
  validate = validation_lambda(key, deets)
  convert = validate ? ->(_ans) { last_converted_value } : ->(ans) { ans }

  answer =
    if deets[:multiline]
      prompt_via_multiline_editor(
        question: question,
        q_desc: q_desc,
        current_value: current_value,
        validate: validate
      )
    elsif deets[:readline] == :get_files
      prompt_for_local_files_via_readline(
        question: question,
        q_desc: q_desc,
        deets: deets,
        validate: validate
      )
    elsif deets[:multi]
      prompt_for_multi_values_with_highline(
        question: question,
        q_desc: q_desc,
        deets: deets,
        convert: convert,
        validate: validate
      )
    else
      prompt_for_single_value_with_highline(
        question: question,
        q_desc: q_desc,
        convert: convert,
        validate: validate,
        deets: deets
      )
    end

  # answer = answer.map(&:strip).join("\n") if deets[:multiline]
  # x means keep the current value
  # answer = nil if answer == 'x'

  # if no answer, keep the current value
  return if answer.pix_empty?

  # if 'none', erase the value in walkthru_cmd_opts
  answer = nil if answer == Xolo::NONE

  walkthru_cmd_opts[key] = answer
end

#prompt_via_multiline_editor(question:, q_desc:, current_value: Xolo::BLANK, validate: nil) ⇒ String

Prompt for a single multiline value via an editor, like vim. This always returns a string. We handle validation ourselves, since we can’t use highline.ask

Parameters:

  • question (String)

    The question to ask

  • q_desc (String)

    A longer description of what we’re asking for

  • current_value (String) (defaults to: Xolo::BLANK)

    The string to start editing.

  • validate (Lambda) (defaults to: nil)

    The lambda for validating the answer before conversion

Returns:

  • (String)

    the edited value.



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/xolo/admin/interactive.rb', line 476

def prompt_via_multiline_editor(question:, q_desc:, current_value: Xolo::BLANK, validate: nil)
  highline_cli.say "#{question}\n#{q_desc}"

  new_val = nil
  validated_new_val = nil
  editor = multiline_editor_to_use
  return if editor == Xolo::CANCEL

  until validated_new_val
    new_val = edited_multiline_value editor, q_desc, current_value
    if validate
      if validate.call new_val
        validated_new_val = last_converted_value
      else
        again = highline_cli.ask("\n#{last_validation_error}\nType a return to edit again, '#{Xolo::X}' to exit")
        break if again == Xolo::X
      end
    else
      validated_new_val = new_val
    end
  end

  validated_new_val || current_value
end

#publisher_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



171
172
173
174
175
# File 'lib/xolo/admin/interactive.rb', line 171

def publisher_na
  return unless walkthru_cmd_opts[:subscribed]

  'N/A when subcribing via a Patch Source'
end

#pw_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



260
261
262
263
264
# File 'lib/xolo/admin/interactive.rb', line 260

def pw_na
  admin_empty = walkthru_cmd_opts[:admin].pix_empty?
  host_empty = walkthru_cmd_opts[:hostname].pix_empty?
  'N/A until hostname and admin name are set' if host_empty || admin_empty
end

#question_desc(deets) ⇒ String

The multi-lines of text describing the value above the prompt

Parameters:

  • deets (Hash)

    The option-details for the value for which we are prompting

Returns:

  • (String)

    the text to display



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
# File 'lib/xolo/admin/interactive.rb', line 660

def question_desc(deets)
  q_desc = +"============= #{deets[:label]} =============\n"
  q_desc << deets[:desc]

  if deets[:multiline]
    # nada, will be shown in the editor
  elsif deets[:multi]
    q_desc << "\nEnter one value per line."
    q_desc << "\nUse tab for auto-completion, tab twice to see available choices" if deets[:readline]
    q_desc << "\nType '#{Xolo::X}' on a line by itself to exit." if deets[:validate]

  else
    q_desc << "\nType a return to keep the current value."
  end

  q_desc
end

#question_for_value(deets) ⇒ String

The line of text prompting for a value. End with a space to keep prompt on same line

Parameters:

  • deets (Hash)

    The option-details for the value for which we are prompting

Returns:

  • (String)

    the one-line prompt to display



684
685
686
687
688
689
690
691
692
693
694
# File 'lib/xolo/admin/interactive.rb', line 684

def question_for_value(deets)
  question = +"Enter #{deets[:label]}"

  if deets[:type] == :boolean
    question << ', (y/n)'
  elsif !deets[:required]
    question << ", use '#{Xolo::NONE}' to unset"
  end
  question << ': ' unless deets[:readline]
  question
end

#setup_for_readline_in_highline(deets, convert, validate) ⇒ Array

should we use readline, and if so should we use an array of values or not?

Parameters:

  • convert (Lambda)

    The lambda for converting the validated value

  • validate (Lambda)

    The lambda for validating the answer before conversion

  • deets (Hash)

    The option-details for the value for which we are prompting

Returns:

  • (Array)

    Three items:

    • if we should use readline (boolean)

    • the new ‘convert’ value - either the original passed to us or an array of possible values

    • the new ‘validate’ value - either the original passed to us or nil if using an array of values



571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
# File 'lib/xolo/admin/interactive.rb', line 571

def setup_for_readline_in_highline(deets, convert, validate)
  # if deets[:readline] is a symbol, its an xadm method that returns an array
  # of the possible values for readline completion and validation;
  # only things in the array are allowed, so no need for other validation or conversion
  # We add 'x' and 'none' to the list for clearing it, and if a multi-value we add 'x'
  # for exiting out of the prompt
  #
  # if its just truthy then we use readline without a pre-set list of values
  # (e.g. paths, which might not exist locally) and may have a separate validate
  # and convert lambdas
  use_readline =
    if deets[:readline]
      if deets[:readline].is_a? Symbol
        convert = send deets[:readline]
        convert << Xolo::NONE unless convert.include?(Xolo::NONE) || deets[:required]
        convert << Xolo::X if deets[:multi] && !convert.include?(Xolo::X)
        # if we're doing release groups, make sure the list includes 'all',
        if !convert.include?(Xolo::TARGET_ALL) && deets[:label] == Xolo::Admin::Title::ATTRIBUTES[:release_groups][:label]
          convert << Xolo::TARGET_ALL
        end
        validate = nil
      end
      true
    else
      false
    end

  if use_readline
    # Case Insensitivity, aka deets[:readline_casefold]
    #
    # If deets[:readline_casefold] is explicitly true or false, we honor that.
    #
    # Otherwise, when we have an array of possible values, we make readline case insensitive.
    # and without such array, we make it sensitive (e.g. when selecting existing file paths)
    #
    # Setting this ENV is how we use our monkey patch to make this work
    ENV['XADM_HIGHLINE_READLINE_CASE_INSENSITIVE'] =
      case deets[:readline_casefold]
      when true
        Xolo::X
      when false
        nil
      else
        convert.is_a?(Array) ? Xolo::X : nil
      end

    # Setting this ENV is how we use our monkey patch to make this work
    prompt = deets[:readline_prompt] || deets[:multi_prompt] || deets[:label] || DEFAULT_HIGHLINE_READLINE_PROMPT
    ENV['XADM_HIGHLINE_READLINE_PROMPT'] = "#{prompt}: "
  end # if use_readline

  [use_readline, convert, validate]
end

#setup_for_readline_local_files(deets) ⇒ String

set up readline for local file autocomplete return the prompt we’ll be using with readline

Parameters:

  • deets (Hash)

    The option-details for the value for which we are prompting

Returns:

  • (String)

    the prompt to use with readline



632
633
634
635
636
637
638
639
640
641
642
# File 'lib/xolo/admin/interactive.rb', line 632

def setup_for_readline_local_files(deets)
  Readline.completion_append_character = nil
  Readline.basic_word_break_characters = "\n"
  Readline.completion_proc = proc do |str|
    str = Pathname.new(str).expand_path
    str = str.directory? ? "#{str}/" : str.to_s
    Dir[str + '*'].grep(/^#{Regexp.escape(str)}/)
  end
  prompt = deets[:readline_prompt] || deets[:label] || DEFAULT_HIGHLINE_READLINE_PROMPT
  "#{prompt}: "
end

#ssvc_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



221
222
223
224
225
# File 'lib/xolo/admin/interactive.rb', line 221

def ssvc_na
  tgt_all = walkthru_cmd_opts[:release_groups]&.include?(Xolo::TARGET_ALL)

  "N/A if Release Group is '#{Xolo::TARGET_ALL}'" if tgt_all
end

#title_id_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



212
213
214
215
216
# File 'lib/xolo/admin/interactive.rb', line 212

def title_id_na
  return if walkthru_cmd_opts[:patch_source]

  'N/A until Patch Source is set'
end

#title_missing_valuesArray<String>

Process missing valus for titles

Returns:

  • (Array<String>)

    The title-specific missing values



805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
# File 'lib/xolo/admin/interactive.rb', line 805

def title_missing_values
  title_missing_values = []

  # if subscribing, need patch source and title id
  # if walkthru_cmd_opts[:type] == Xolo::SUBSCRIBED
  if walkthru_cmd_opts[:subscribed]
    unless walkthru_cmd_opts[:patch_source]
      title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:patch_source][:label]
    end
    title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:title_id][:label] unless walkthru_cmd_opts[:title_id]

  # if managed, need display name, publisher and version script or app name/bundle id,
  else
    unless walkthru_cmd_opts[:publisher]
      title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:publisher][:label]
    end
    unless walkthru_cmd_opts[:display_name]
      title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:display_name][:label]
    end

    # version script or app name/bundle id
    if walkthru_cmd_opts[:version_script] || walkthru_cmd_opts[:app_name] || walkthru_cmd_opts[:app_bundle_id]
      # need both app name and bundle id if using either
      if walkthru_cmd_opts[:app_name] && !walkthru_cmd_opts[:app_bundle_id]
        title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:app_bundle_id][:label]
      elsif walkthru_cmd_opts[:app_bundle_id] && !walkthru_cmd_opts[:app_name]
        title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:app_name][:label]
      end
    else
      title_missing_values << "#{Xolo::Admin::Title::ATTRIBUTES[:version_script][:label]} OR #{Xolo::Admin::Title::ATTRIBUTES[:app_name][:label]} & #{Xolo::Admin::Title::ATTRIBUTES[:app_bundle_id][:label]}"
    end

  end

  # if expiring, need expire path
  if walkthru_cmd_opts[:expiration].to_i.positive? && !walkthru_cmd_opts[:expiration_paths].pix_empty?
    title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:expiration_paths][:label]
  end

  # if in ssvc, need category and icon
  return title_missing_values unless walkthru_cmd_opts[:self_service]

  unless walkthru_cmd_opts[:self_service_category]
    title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:self_service_category][:label]
  end
  return title_missing_values if walkthru_cmd_opts[:self_service_icon]

  title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:self_service_icon][:label]

  title_missing_values
end

#uninstall_ids_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



237
238
239
# File 'lib/xolo/admin/interactive.rb', line 237

def uninstall_ids_na
  'N/A if using Uninstall Script' unless walkthru_cmd_opts[:uninstall_script].pix_empty?
end

#uninstall_script_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



230
231
232
# File 'lib/xolo/admin/interactive.rb', line 230

def uninstall_script_na
  'N/A if using Uninstall IDs' unless walkthru_cmd_opts[:uninstall_ids].pix_empty?
end

#validation_lambda(key, deets) ⇒ Lambda?

Retun a lambda that calls one of our validation methods to validate a walkthru value.

Highline requires validation lambdas to return a boolean, and uses a separate lambda for type conversion. Since our validation methods do both, this lambda will put the converted result into the ‘last_converted_value’ accessor, or capture the error, and then return a boolean.

Later the lambda we give to highline for conversion will just return the last converted value, as stored in the last_converted_value accessor.

Returns:

  • (Lambda, nil)

    The lambda that highline will use to validate (and convert) a value, nil if we accept whatever was given.



712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
# File 'lib/xolo/admin/interactive.rb', line 712

def validation_lambda(key, deets)
  val_meth = validation_method(key, deets)
  return unless val_meth

  # lambda to validate the value given.
  # must return boolean for Highline to deal with it.
  lambda do |ans|
    # to start, the converted value is just the given value.
    #
    # Use self here, otherwise the lambda sees 'last_converted_value ='
    # as a local variable assignment, not a setter method call
    self.last_converted_value = ans

    # default to the pre-written error message
    self.last_validation_error = deets[:invalid_msg]

    # if entering multi-values, a 'x' is how we get out of
    # the loop
    return true if deets[:multi] && ans == Xolo::X

    # but for anything not multi, an empty response
    # means user just hit return, nothing to validate,
    # no changes to make
    return true if ans.pix_empty?

    # If this value isn't required, accept 'none'
    # which clears the value
    return true if !deets[:required] && (ans == Xolo::NONE)

    # otherwise 'none' becomes nil and will be validated
    # and will fail if a value is required
    ans_to_validate = ans == Xolo::NONE ? nil : ans

    # validate using the val_meth,
    # saving the validated/converted value for use in the
    # convert method.
    self.last_converted_value = send(val_meth, ans_to_validate)
    true

  # if validation fails, set the last_validation_error
  # so we can display it and ask again
  rescue Xolo::InvalidDataError => e
    self.last_validation_error = e.to_s
    false
  end # lambda
end

#validation_method(key, deets) ⇒ String, ...

The method used to validate and convert a value

Parameters:

  • deets (Hash)

    The option-details for the value for which we are prompting

  • key (Symbol)

    One of the keys of the opts hash for the current command; the value for which we are prompting

Returns:

  • (String, Symbol, nil)

    The method which will validate the value for the key



779
780
781
782
783
784
# File 'lib/xolo/admin/interactive.rb', line 779

def validation_method(key, deets)
  case deets[:validate]
  when TrueClass then "validate_#{key}"
  when Symbol then deets[:validate]
  end
end

#version_script_naString?

Returns If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.

Returns:

  • (String, nil)

    If a string, a reason why the given menu item is not available now. If nil, the menu item is displayed normally.



180
181
182
183
184
185
186
# File 'lib/xolo/admin/interactive.rb', line 180

def version_script_na
  unless walkthru_cmd_opts[:app_name] || walkthru_cmd_opts[:app_bundle_id] || walkthru_cmd_opts[:subscribed]
    return
  end

  'N/A when using App Name/BundleID, or subcribing via a Patch Source'
end