lib/hash_delegator.rb in markdown_exec-2.0.5 vs lib/hash_delegator.rb in markdown_exec-2.0.6
- old
+ new
@@ -198,11 +198,11 @@
# Set block_name based on block_name_from_cli
block_name = @cli_block_name if block_name_from_cli
# Determine the state of breaker based on was_using_cli and the block type
# true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block[:shell] equals BlockType::BASH. In all other scenarios, breaker is false.
- breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH
+ breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
# Reset block_name_from_cli if the conditions are not met
block_name_from_cli ||= false
[block_name, block_name_from_cli, breaker]
@@ -235,45 +235,21 @@
# Evaluates the given string as Ruby code within a safe context.
# If an error occurs, it calls the error_handler method with 'safeval'.
# @param str [String] The string to be evaluated.
# @return [Object] The result of evaluating the string.
def safeval(str)
- # # Restricting to evaluate only expressions
- # unless str.match?(/\A\s*\w+\s*[\+\-\*\/\=\%\&\|\<\>\!]+\s*\w+\s*\z/)
- # error_handler('safeval') # 'Invalid expression'
- # return
- # end
-
- # # Whitelisting allowed operations
- # allowed_methods = %w[+ - * / == != < > <= >= && || % & |]
- # unless allowed_methods.any? { |op| str.include?(op) }
- # error_handler('safeval', 'Operation not allowed')
- # return
- # end
-
- # # Sanitize input (example: removing potentially harmful characters)
- # str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
-
- # Evaluate the sanitized string
result = nil
binding.eval("result = #{str}")
result
rescue StandardError # catches NameError, StandardError
+ pp $!, $@
+ pp "code: #{str}"
error_handler('safeval')
+ exit 1
end
- # # Evaluates the given string as Ruby code and rescues any StandardErrors.
- # # If an error occurs, it calls the error_handler method with 'safeval'.
- # # @param str [String] The string to be evaluated.
- # # @return [Object] The result of evaluating the string.
- # def safeval(str)
- # eval(str)
- # rescue StandardError # catches NameError, StandardError
- # error_handler('safeval')
- # end
-
def set_file_permissions(file_path, chmod_value)
File.chmod(chmod_value, file_path)
end
# Creates a TTY prompt with custom settings. Specifically, it disables the default 'cross' symbol and
@@ -309,15 +285,15 @@
FileUtils.mkdir_p File.dirname(filespec)
File.write(
filespec,
["-STDOUT-\n",
- format_execution_streams(ExecutionStreams::StdOut, files),
+ format_execution_streams(ExecutionStreams::STD_OUT, files),
"-STDERR-\n",
- format_execution_streams(ExecutionStreams::StdErr, files),
+ format_execution_streams(ExecutionStreams::STD_ERR, files),
"-STDIN-\n",
- format_execution_streams(ExecutionStreams::StdIn, files),
+ format_execution_streams(ExecutionStreams::STD_IN, files),
"\n"].join
)
end
# Yields a line as a new block if the selected message type includes :line.
@@ -385,11 +361,11 @@
# Determines if a given path is absolute or substitutes a placeholder in an expression with the path.
# @param path [String] The input path to check or fill in.
# @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
# @return [String] The absolute path or the expression with the wildcard replaced by the path.
def self.resolve_path_or_substitute(path, expression)
- if path.include?('/')
+ if path.start_with?('/')
path
else
expression.gsub('*', path)
end
end
@@ -507,21 +483,33 @@
case menu_state
when MenuState::BACK
history_state_partition
option_name = @delegate_object[:menu_option_back_name]
insert_at_top = @delegate_object[:menu_back_at_top]
+ when MenuState::EDIT
+ option_name = @delegate_object[:menu_option_edit_name]
+ insert_at_top = @delegate_object[:menu_load_at_top]
when MenuState::EXIT
option_name = @delegate_object[:menu_option_exit_name]
insert_at_top = @delegate_object[:menu_exit_at_top]
+ when MenuState::LOAD
+ option_name = @delegate_object[:menu_option_load_name]
+ insert_at_top = @delegate_object[:menu_load_at_top]
+ when MenuState::SAVE
+ option_name = @delegate_object[:menu_option_save_name]
+ insert_at_top = @delegate_object[:menu_load_at_top]
+ when MenuState::VIEW
+ option_name = @delegate_object[:menu_option_view_name]
+ insert_at_top = @delegate_object[:menu_load_at_top]
end
formatted_name = format(@delegate_object[:menu_link_format],
HashDelegator.safeval(option_name))
chrome_block = FCB.new(
chrome: true,
dname: HashDelegator.new(@delegate_object).string_send_color(
- formatted_name, :menu_link_color
+ formatted_name, :menu_chrome_color
),
oname: formatted_name
)
if insert_at_top
@@ -615,10 +603,22 @@
blocks
rescue StandardError
HashDelegator.error_handler('blocks_from_nested_files')
end
+ def block_state_for_name_from_cli(block_name)
+ SelectedBlockMenuState.new(
+ @dml_blocks_in_file.find do |item|
+ item[:oname] == block_name
+ end&.merge(
+ block_name_from_cli: true,
+ block_name_from_ui: false
+ ),
+ MenuState::CONTINUE
+ )
+ end
+
# private
def calc_logged_stdout_filename(block_name:)
return unless @delegate_object[:saved_stdout_folder]
@@ -723,18 +723,18 @@
@run_state.in_own_window = false
Open3.popen3(@delegate_object[:shell],
'-c', command,
@delegate_object[:filename],
*args) do |stdin, stdout, stderr, exec_thr|
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
yield nil, line, nil, exec_thr if block_given?
end
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
yield nil, nil, line, exec_thr if block_given?
end
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
stdin.puts(line)
yield line, nil, nil, exec_thr if block_given?
end
wait_for_stream_processing
@@ -748,38 +748,21 @@
rescue Errno::ENOENT => err
# Handle ENOENT error
@run_state.aborted_at = Time.now.utc
@run_state.error_message = err.message
@run_state.error = err
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
@fout.fout "Error ENOENT: #{err.inspect}"
rescue SignalException => err
# Handle SignalException
@run_state.aborted_at = Time.now.utc
@run_state.error_message = 'SIGTERM'
@run_state.error = err
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
@fout.fout "Error ENOENT: #{err.inspect}"
end
- def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
- if @delegate_object[:block_name].present?
- block = all_blocks.find do |item|
- item[:oname] == @delegate_object[:block_name]
- end&.merge(block_name_from_ui: false)
- else
- block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
- default)
- block = block_state.block&.merge(block_name_from_ui: true)
- state = block_state.state
- end
-
- SelectedBlockMenuState.new(block, state)
- rescue StandardError
- HashDelegator.error_handler('load_cli_or_user_selected_block')
- end
-
# This method is responsible for handling the execution of generic blocks in a markdown document.
# It collects the required code lines from the document and, depending on the configuration,
# may display the code for user approval before execution. It then executes the approved block.
#
# @param mdoc [Object] The markdown document object containing code blocks.
@@ -797,13 +780,18 @@
end
execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
link_state.block_name = nil
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
end
+ # Check if the expression contains wildcard characters
+ def contains_wildcards?(expr)
+ expr.match(%r{\*|\?|\[})
+ end
+
def copy_to_clipboard(required_lines)
text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
Clipboard.copy(text)
@fout.fout "Clipboard updated: #{required_lines.count} blocks," \
" #{required_lines.flatten.count} lines," \
@@ -948,10 +936,316 @@
HashDelegator.write_execution_output_to_file(@run_state.files,
@delegate_object[:logged_stdout_filespec])
end
+ # Select and execute a code block from a Markdown document.
+ #
+ # This method allows the user to interactively select a code block from a
+ # Markdown document, obtain approval, and execute the chosen block of code.
+ #
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
+ def document_inpseq
+ @menu_base_options = @delegate_object
+ @dml_link_state = LinkState.new(
+ block_name: @delegate_object[:block_name],
+ document_filename: @delegate_object[:filename]
+ )
+ @run_state.block_name_from_cli = @dml_link_state.block_name.present?
+ @cli_block_name = @dml_link_state.block_name
+ @dml_now_using_cli = @run_state.block_name_from_cli
+ @dml_menu_default_dname = nil
+ @dml_block_state = SelectedBlockMenuState.new
+ @doc_saved_lines_files = []
+
+ ## load file with code lines per options
+ #
+ if @menu_base_options[:load_code].present?
+ @dml_link_state.inherited_lines = []
+ @menu_base_options[:load_code].split(':').map do |path|
+ @dml_link_state.inherited_lines += File.readlines(path, chomp: true)
+ end
+
+ inherited_block_names = []
+ inherited_dependencies = {}
+ selected = { oname: 'load_code' }
+ pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
+ end
+
+ item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
+ item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
+ item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
+ item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
+ item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
+
+ @run_state.batch_random = Random.new.rand
+ @run_state.batch_index = 0
+
+ InputSequencer.new(
+ @delegate_object[:filename],
+ @delegate_object[:input_cli_rest]
+ ).run do |msg, data|
+ case msg
+ when :parse_document # once for each menu
+ # puts "@ - parse document #{data}"
+ inpseq_parse_document(data)
+
+ if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
+
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
+ files = sf ? Dir.glob(sf) : []
+ @doc_saved_lines_files = files.count.positive? ? files : []
+
+ lines_count = @dml_link_state.inherited_lines&.count || 0
+
+ # add menu items (glob, load, save) and enable selectively
+ menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD)
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT)
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), lines_count, 'lines', menu_state: MenuState::SAVE)
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), lines_count, 'lines', menu_state: MenuState::VIEW)
+ end
+
+ when :display_menu
+ # warn "@ - display menu:"
+ # ii_display_menu
+ @dml_block_state = SelectedBlockMenuState.new
+ @delegate_object[:block_name] = nil
+
+ when :user_choice
+ if @dml_link_state.block_name.present?
+ # @prior_block_was_link = true
+ @dml_block_state.block = @dml_blocks_in_file.find do |item|
+ item[:oname] == @dml_link_state.block_name
+ end
+ @dml_link_state.block_name = nil
+ else
+ # puts "? - Select a block to execute (or type #{$texit} to exit):"
+ break if inpseq_user_choice == :break # into @dml_block_state
+ break if @dml_block_state.block.nil? # no block matched
+ end
+ # puts "! - Executing block: #{data}"
+ # @dml_block_state.block[:oname]
+ @dml_block_state.block&.fetch(:oname, nil)
+
+ when :execute_block
+ case (block_name = data)
+ when item_back
+ debounce_reset
+ @menu_user_clicked_back_link = true
+ load_file_link_state = pop_link_history_and_trigger_load
+ @dml_link_state = load_file_link_state.link_state
+
+ InputSequencer.merge_link_state(
+ @dml_link_state,
+ InputSequencer.next_link_state(
+ block_name: @dml_link_state.block_name,
+ document_filename: @dml_link_state.document_filename,
+ prior_block_was_link: true
+ )
+ )
+
+ when item_edit
+ edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
+ @dml_link_state.inherited_lines = edited.split("\n") if edited
+ InputSequencer.next_link_state(prior_block_was_link: true)
+
+ when item_load
+ debounce_reset
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
+ load_filespec = load_filespec_from_expression(sf)
+ if load_filespec
+ @dml_link_state.inherited_lines ||= []
+ @dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
+ end
+ InputSequencer.next_link_state(prior_block_was_link: true)
+
+ when item_save
+ debounce_reset
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
+ save_filespec = save_filespec_from_expression(sf)
+ if save_filespec && !write_file_with_directory_creation(
+ save_filespec,
+ HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
+ )
+ return :break
+
+ end
+
+ InputSequencer.next_link_state(prior_block_was_link: true)
+
+ when item_view
+ warn @dml_link_state.inherited_lines.join("\n")
+ InputSequencer.next_link_state(prior_block_was_link: true)
+
+ else
+ @dml_block_state = block_state_for_name_from_cli(block_name)
+ if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
+ debounce_reset
+ link_state = LinkState.new
+ options_state = read_show_options_and_trigger_reuse(
+ selected: @dml_block_state.block,
+ link_state: link_state
+ )
+
+ @menu_base_options.merge!(options_state.options)
+ @delegate_object.merge!(options_state.options)
+ options_state.load_file_link_state.link_state
+ else
+ inpseq_execute_block(block_name)
+
+ if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
+ selected: @dml_block_state.block)
+ return :break
+ end
+
+ ## order of block name processing: link block, cli, from user
+ #
+ @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
+ HashDelegator.next_link_state(
+ block_name: @dml_link_state.block_name,
+ block_name_from_cli: !@dml_link_state.block_name.present?,
+ block_state: @dml_block_state,
+ was_using_cli: @dml_now_using_cli
+ )
+
+ if !@dml_block_state.block[:block_name_from_ui] && cli_break
+ # &bsp '!block_name_from_ui + cli_break -> break'
+ return :break
+ end
+
+ InputSequencer.next_link_state(
+ block_name: @dml_link_state.block_name,
+ prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
+ )
+ end
+ end
+
+ when :exit?
+ data == $texit
+ when :stay?
+ data == $stay
+ else
+ raise "Invalid message: #{msg}"
+ end
+ end
+ rescue StandardError
+ HashDelegator.error_handler('document_inpseq',
+ { abort: true })
+ end
+
+ # remove leading "./"
+ # replace characters: / : . * (space) with: (underscore)
+ def document_name_in_glob_as_file_name(document_filename, glob)
+ return document_filename if document_filename.nil? || document_filename.empty?
+
+ format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
+ end
+
+ def dump_and_warn_block_state(selected:)
+ if selected.nil?
+ Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
+ { abort: true })
+ end
+
+ return unless @delegate_object[:dump_selected_block]
+
+ warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
+ end
+
+ # Outputs warnings based on the delegate object's configuration
+ #
+ # @param delegate_object [Hash] The delegate object containing configuration flags.
+ # @param blocks_in_file [Hash] Hash of blocks present in the file.
+ # @param menu_blocks [Hash] Hash of menu blocks.
+ # @param link_state [LinkState] Current state of the link.
+ def dump_delobj(blocks_in_file, menu_blocks, link_state)
+ warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
+
+ if @delegate_object[:dump_blocks_in_file]
+ warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
+ label: 'blocks_in_file')
+ end
+
+ if @delegate_object[:dump_menu_blocks]
+ warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
+ label: 'menu_blocks')
+ end
+
+ warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
+ warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
+ return unless @delegate_object[:dump_inherited_lines]
+
+ warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
+ end
+
+ # Opens text in an editor for user modification and returns the modified text.
+ #
+ # This method reads the provided text, opens it in the default editor,
+ # and allows the user to modify it. If the user makes changes, the
+ # modified text is returned. If the user exits the editor without
+ # making changes or the editor is closed abruptly, appropriate messages
+ # are displayed.
+ #
+ # @param [String] initial_text The initial text to be edited.
+ # @param [String] temp_name The base name for the temporary file (default: 'edit_text').
+ # @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly.
+ def edit_text(initial_text, temp_name: 'edit_text')
+ # Create a temporary file to store the initial text
+ temp_file = Tempfile.new(temp_name)
+ temp_file.write(initial_text)
+ temp_file.rewind
+
+ # Capture the modification time of the temporary file before editing
+ before_mtime = temp_file.mtime
+
+ # Open the temporary file in the default editor
+ system("#{ENV['EDITOR'] || 'vi'} #{temp_file.path}")
+
+ # Capture the exit status of the editor
+ editor_exit_status = $?.exitstatus
+
+ # Reopen the file to ensure the updated modification time is read
+ temp_file.open
+ after_mtime = temp_file.mtime
+
+ # Check if the editor was exited normally or was interrupted
+ if editor_exit_status != 0
+ warn 'The editor was closed abruptly. No changes were made.'
+ temp_file.close
+ temp_file.unlink
+ return
+ end
+
+ result_text = nil
+ # Read the file if it was modified
+ if before_mtime != after_mtime
+ temp_file.rewind
+ result_text = temp_file.read
+ end
+
+ # Remove the temporary file
+ temp_file.close
+ temp_file.unlink
+
+ result_text
+ end
+
+ def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
+ lfls = execute_shell_type(
+ selected: selected,
+ mdoc: mdoc,
+ link_state: link_state,
+ block_source: block_source
+ )
+
+ # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
+ [lfls.link_state,
+ lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
+ #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
+ end
+
# Executes a block of code that has been approved for execution.
# It sets the script block name, writes command files if required, and handles the execution
# including output formatting and summarization.
#
# @param required_lines [Array<String>] The lines of code to be executed.
@@ -1005,11 +1299,11 @@
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
next_block_name: '',
next_document_filename: @delegate_object[:filename],
- next_load_file: LoadFile::Reuse
+ next_load_file: LoadFile::REUSE
)
elsif selected[:shell] == BlockType::VARS
debounce_reset
block_names = []
@@ -1021,21 +1315,21 @@
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
next_block_name: '',
next_document_filename: @delegate_object[:filename],
- next_load_file: LoadFile::Reuse
+ next_load_file: LoadFile::REUSE
)
elsif debounce_allows
compile_execute_and_trigger_reuse(mdoc: mdoc,
selected: selected,
link_state: link_state,
block_source: block_source)
else
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
end
end
# Retrieves a specific data symbol from the delegate object, converts it to a string,
# and applies a color style based on the specified color symbol.
@@ -1058,10 +1352,27 @@
command_execute(formatted_command, args: @pass_args)
@fout.fout fetch_color(data_sym: :script_execution_tail,
color_sym: :script_execution_frame_color)
end
+ # Format expression using environment variables and run state
+ def format_expression(expr)
+ data = link_load_format_data
+ ENV.each { |key, value| data[key] = value }
+ format(expr, data)
+ end
+
+ # Formats multiline body content as a title string.
+ # indents all but first line with two spaces so it displays correctly in menu
+ # @param body_lines [Array<String>] The lines of body content.
+ # @return [String] Formatted title.
+ def format_multiline_body_as_title(body_lines)
+ body_lines.map.with_index do |line, index|
+ index.zero? ? line : " #{line}"
+ end.join("\n") + "\n"
+ end
+
# Formats a string based on a given context and applies color styling to it.
# It retrieves format and color information from the delegate object and processes accordingly.
#
# @param default [String] The default value if the format symbol is not found (unused in current implementation).
# @param context [Hash] Contextual data used for string formatting.
@@ -1074,10 +1385,15 @@
formatted_string = format(@delegate_object.fetch(format_sym, ''),
context).to_s
string_send_color(formatted_string, color_sym)
end
+ # Expand expression if it contains format specifiers
+ def formatted_expression(expr)
+ expr.include?('%{') ? format_expression(expr) : expr
+ end
+
# Processes a block to generate its summary, modifying its attributes based on various matching criteria.
# It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
#
# @param fcb [Object] An object representing a functional code block.
# @return [Object] The modified functional code block with updated summary attributes.
@@ -1108,20 +1424,10 @@
fcb.fetch(:indent, nil)
)
fcb
end
- # Formats multiline body content as a title string.
- # indents all but first line with two spaces so it displays correctly in menu
- # @param body_lines [Array<String>] The lines of body content.
- # @return [String] Formatted title.
- def format_multiline_body_as_title(body_lines)
- body_lines.map.with_index do |line, index|
- index.zero? ? line : " #{line}"
- end.join("\n") + "\n"
- end
-
# Updates the delegate object's state based on the provided block state.
# It sets the block name and determines if the user clicked the back link in the menu.
#
# @param block_state [Object] An object representing the state of a block in the menu.
def handle_back_or_continue(block_state)
@@ -1170,10 +1476,50 @@
in_fenced_block: false,
headings: []
}
end
+ def inpseq_execute_block(block_name)
+ @dml_block_state = block_state_for_name_from_cli(block_name)
+
+ dump_and_warn_block_state(selected: @dml_block_state.block)
+ @dml_link_state, @dml_menu_default_dname = \
+ exec_bash_next_state(
+ selected: @dml_block_state.block,
+ mdoc: @dml_mdoc,
+ link_state: @dml_link_state,
+ block_source: {
+ document_filename: @delegate_object[:filename],
+ time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
+ }
+ )
+ end
+
+ def inpseq_parse_document(_document_filename)
+ @run_state.batch_index += 1
+ @run_state.in_own_window = false
+
+ # &bsp 'loop', block_name_from_cli, @cli_block_name
+ @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
+ set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
+ now_using_cli: @dml_now_using_cli,
+ link_state: @dml_link_state)
+ end
+
+ def inpseq_user_choice
+ @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
+ menu_blocks: @dml_menu_blocks,
+ default: @dml_menu_default_dname)
+ # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
+ if !@dml_block_state
+ HashDelegator.error_handler('block_state missing', { abort: true })
+ elsif @dml_block_state.state == MenuState::EXIT
+ # &bsp 'load_cli_or_user_selected_block -> break'
+ :break
+ end
+ end
+
# Iterates through blocks in a file, applying the provided block to each line.
# The iteration only occurs if the file exists.
# @yield [Symbol] :filter Yields to obtain selected messages for processing.
def iter_blocks_from_nested_files(&block)
return unless check_file_existence(@delegate_object[:filename])
@@ -1196,22 +1542,22 @@
Tempfile.open do |file|
cmd = "#{@delegate_object[:shell]} #{file.path}"
file.write(all_code.join("\n"))
file.rewind
- if link_block_data.fetch(LinkKeys::Exec, false)
+ if link_block_data.fetch(LinkKeys::EXEC, false)
@run_state.files = Hash.new([])
Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
output_lines.push(line)
end
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
output_lines.push(line)
end
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
stdin.puts(line)
end
wait_for_stream_processing
sleep 0.1
@@ -1273,118 +1619,10 @@
inherited_lines: inherited_lines
)
)
end
- # format + glob + select for file in load block
- # name has references to ENV vars and doc and batch vars incl. timestamp
- def load_filespec_from_expression(expression)
- # Process expression with embedded formatting
- expanded_expression = formatted_expression(expression)
-
- # Handle wildcards or direct file specification
- if contains_wildcards?(expanded_expression)
- load_filespec_wildcard_expansion(expanded_expression)
- else
- expanded_expression
- end
- end
-
- def save_filespec_from_expression(expression)
- # Process expression with embedded formatting
- formatted = formatted_expression(expression)
-
- # Handle wildcards or direct file specification
- if contains_wildcards?(formatted)
- save_filespec_wildcard_expansion(formatted)
- else
- formatted
- end
- end
-
- # private
-
- # Expand expression if it contains format specifiers
- def formatted_expression(expr)
- expr.include?('%{') ? format_expression(expr) : expr
- end
-
- # Format expression using environment variables and run state
- def format_expression(expr)
- data = link_load_format_data
- ENV.each { |key, value| data[key] = value }
- format(expr, data)
- end
-
- # Check if the expression contains wildcard characters
- def contains_wildcards?(expr)
- expr.match(%r{\*|\?|\[})
- end
-
- # Handle expression with wildcard characters
- def load_filespec_wildcard_expansion(expr)
- files = find_files(expr)
- case files.count
- when 0
- HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
- when 1
- files.first
- else
- prompt_select_code_filename(files)
- end
- end
-
- # Handle expression with wildcard characters
- # allow user to select or enter
- def puts_gets_oprompt_(filespec)
- puts format(@delegate_object[:prompt_show_expr_format],
- { expr: filespec })
- puts @delegate_object[:prompt_enter_filespec]
- gets.chomp
- end
-
- # prompt user to enter a path (i.e. containing a path separator)
- # or name to substitute into the wildcard expression
- def prompt_for_filespec_with_wildcard(filespec)
- puts format(@delegate_object[:prompt_show_expr_format],
- { expr: filespec })
- puts @delegate_object[:prompt_enter_filespec]
- PathUtils.resolve_path_or_substitute(gets.chomp, filespec)
- end
-
- # def read_block_name(line)
- # bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
- # name = bm[:title]
-
- # if @delegate_object[:block_name_nick_match].present? && line =~ Regexp.new(@delegate_object[:block_name_nick_match])
- # name = $~[0]
- # else
- # name = bm && bm[1] ? bm[:title] : name
- # end
- # name
- # end
-
- # Handle expression with wildcard characters
- # allow user to select or enter
- def save_filespec_wildcard_expansion(filespec)
- files = find_files(filespec)
- case files.count
- when 0
- prompt_for_filespec_with_wildcard(filespec)
- else
- ## user selects from existing files or other
- # input into path with wildcard for easy entry
- #
- name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
- if name == @delegate_object[:prompt_filespec_other]
- prompt_for_filespec_with_wildcard(filespec)
- else
- name
- end
- end
- end
-
def link_load_format_data
{
batch_index: @run_state.batch_index,
batch_random: @run_state.batch_random,
block_name: @delegate_object[:block_name],
@@ -1393,33 +1631,10 @@
home: Dir.pwd,
started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format])
}
end
- # # Loads auto link block.
- # def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
- # block_name = @delegate_object[:document_load_link_block_name]
- # return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
-
- # block = HashDelegator.block_find(all_blocks, :oname, block_name)
- # return unless block
-
- # if block.fetch(:shell, '') != BlockType::LINK
- # HashDelegator.error_handler('must be Link block type', { abort: true })
-
- # else
- # # debounce_reset
- # push_link_history_and_trigger_load(
- # link_block_body: block.fetch(:body, ''),
- # mdoc: mdoc,
- # selected: block,
- # link_state: link_state,
- # block_source: block_source
- # )
- # end
- # end
-
# Loads auto blocks based on delegate object settings and updates if new filename is detected.
# Executes a specified block once per filename.
# @param all_blocks [Array] Array of all block elements.
# @return [Boolean, nil] True if values were modified, nil otherwise.
def load_auto_opts_block(all_blocks)
@@ -1435,10 +1650,59 @@
@most_recent_loaded_filename = @delegate_object[:filename]
true
end
+ def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
+ if @delegate_object[:block_name].present?
+ block = all_blocks.find do |item|
+ item[:oname] == @delegate_object[:block_name]
+ end&.merge(block_name_from_ui: false)
+ else
+ block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
+ default)
+ block = block_state.block&.merge(block_name_from_ui: true)
+ state = block_state.state
+ end
+
+ SelectedBlockMenuState.new(block, state)
+ rescue StandardError
+ HashDelegator.error_handler('load_cli_or_user_selected_block')
+ end
+
+ # format + glob + select for file in load block
+ # name has references to ENV vars and doc and batch vars incl. timestamp
+ def load_filespec_from_expression(expression)
+ # Process expression with embedded formatting
+ expanded_expression = formatted_expression(expression)
+
+ # Handle wildcards or direct file specification
+ if contains_wildcards?(expanded_expression)
+ load_filespec_wildcard_expansion(expanded_expression)
+ else
+ expanded_expression
+ end
+ end
+ # Handle expression with wildcard characters
+ def load_filespec_wildcard_expansion(expr, auto_load_single: false)
+ files = find_files(expr)
+ if files.count.zero?
+ HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
+ elsif auto_load_single && files.count == 1
+ files.first
+ else
+ ## user selects from existing files or other
+ #
+ case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
+ when @delegate_object[:prompt_filespec_back]
+ # do nothing
+ else
+ name
+ end
+ end
+ end
+
def mdoc_and_blocks_from_nested_files
menu_blocks = blocks_from_nested_files
mdoc = MDoc.new(menu_blocks) do |nopts|
@delegate_object.merge!(nopts)
end
@@ -1451,19 +1715,45 @@
all_blocks, mdoc = mdoc_and_blocks_from_nested_files
# recreate menu with new options
#
all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
- # load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
menu_blocks = mdoc.fcbs_per_options(@delegate_object)
add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
### compress empty lines
- HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true
+ HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
[all_blocks, menu_blocks, mdoc]
end
+ def menu_add_disabled_option(name)
+ raise unless name.present?
+ raise if @dml_menu_blocks.nil?
+
+ block = @dml_menu_blocks.find { |item| item[:oname] == name }
+
+ # create menu item when it is needed (count > 0)
+ #
+ return unless block.nil?
+
+ # append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD)
+ chrome_block = FCB.new(
+ chrome: true,
+ disabled: '',
+ dname: HashDelegator.new(@delegate_object).string_send_color(
+ name, :menu_inherited_lines_color
+ ),
+ oname: formatted_name
+ )
+
+ if insert_at_top
+ @dml_menu_blocks.unshift(chrome_block)
+ else
+ @dml_menu_blocks.push(chrome_block)
+ end
+ end
+
# Formats and optionally colors a menu option based on delegate object's configuration.
# @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
# @return [String] The formatted and possibly colored value of the menu option.
def menu_chrome_colored_option(option_symbol = :menu_option_back_name)
formatted_option = menu_chrome_formatted_option(option_symbol)
@@ -1484,10 +1774,51 @@
else
option_value
end
end
+ def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
+ raise unless name.present?
+ raise if @dml_menu_blocks.nil?
+
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
+
+ # create menu item when it is needed (count > 0)
+ #
+ if item.nil? && count.positive?
+ append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
+ end
+
+ # update item if it exists
+ #
+ return unless item
+
+ item[:dname] = "#{name} (#{count} #{type})"
+ if count.positive?
+ item.delete(:disabled)
+ else
+ item[:disabled] = ''
+ end
+ end
+
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
+ if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
+ # &bsp 'pause cli control, allow user to select block'
+ block_name_from_cli = false
+ now_using_cli = false
+ @menu_base_options[:block_name] = \
+ @delegate_object[:block_name] = \
+ link_state.block_name = \
+ @cli_block_name = nil
+ end
+
+ @delegate_object = @menu_base_options.dup
+ @menu_user_clicked_back_link = false
+ [block_name_from_cli, now_using_cli]
+ end
+
# If a method is missing, treat it as a key for the @delegate_object.
def method_missing(method_name, *args, &block)
if @delegate_object.respond_to?(method_name)
@delegate_object.send(method_name, *args, &block)
elsif method_name.to_s.end_with?('=') && args.size == 1
@@ -1565,35 +1896,35 @@
HashDelegator.code_merge(pop.inherited_lines, code_lines)
)
@link_history.push(next_state)
next_state.block_name = nil
- LoadFileLinkState.new(LoadFile::Load, next_state)
+ LoadFileLinkState.new(LoadFile::LOAD, next_state)
else
# no history exists; must have been called independently => retain script
link_history_push_and_next(
curr_block_name: selected[:oname],
curr_document_filename: @delegate_object[:filename],
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
- next_block_name: '', # not link_block_data[LinkKeys::Block] || ''
+ next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || ''
next_document_filename: @delegate_object[:filename], # not next_document_filename
- next_load_file: LoadFile::Reuse # not next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
+ next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
)
- # LoadFileLinkState.new(LoadFile::Reuse, link_state)
+ # LoadFileLinkState.new(LoadFile::REUSE, link_state)
end
end
# This method handles the back-link operation in the Markdown execution context.
# It updates the history state and prepares to load the next block.
#
# @return [LoadFileLinkState] An object indicating the action to load the next block.
def pop_link_history_and_trigger_load
pop = @link_history.pop
peek = @link_history.peek
- LoadFileLinkState.new(LoadFile::Load, LinkState.new(
+ LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
document_filename: pop.document_filename,
inherited_block_names: peek.inherited_block_names,
inherited_dependencies: peek.inherited_dependencies,
inherited_lines: peek.inherited_lines
))
@@ -1703,10 +2034,28 @@
true
rescue TTY::Reader::InputInterrupt
exit 1
end
+ # Prompts the user to enter a path or name to substitute into the wildcard expression.
+ # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
+ #
+ # @param filespec [String] the wildcard expression to be substituted
+ # @return [String, nil] the resolved path or substituted expression, or nil if interrupted
+ def prompt_for_filespec_with_wildcard(filespec)
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
+ puts @delegate_object[:prompt_enter_filespec]
+
+ begin
+ input = gets.chomp
+ PathUtils.resolve_path_or_substitute(input, filespec)
+ rescue Interrupt
+ puts "\nOperation interrupted. Returning nil."
+ nil
+ end
+ end
+
##
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
#
# @param opts [Hash] A hash containing various options for the menu.
@@ -1746,42 +2095,51 @@
sel == MenuOptions::YES
rescue TTY::Reader::InputInterrupt
exit 1
end
- def prompt_select_continue
- sel = @prompt.select(
- string_send_color(@delegate_object[:prompt_after_script_execution],
+ # public
+
+ def prompt_select_code_filename(filenames)
+ @prompt.select(
+ string_send_color(@delegate_object[:prompt_select_code_file],
:prompt_color_after_script_execution),
filter: true,
quiet: true
) do |menu|
- menu.choice @delegate_object[:prompt_yes]
- menu.choice @delegate_object[:prompt_exit]
+ filenames.each do |filename|
+ menu.choice filename
+ end
end
- sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
rescue TTY::Reader::InputInterrupt
exit 1
end
- # public
-
- def prompt_select_code_filename(filenames)
- @prompt.select(
- string_send_color(@delegate_object[:prompt_select_code_file],
+ def prompt_select_continue
+ sel = @prompt.select(
+ string_send_color(@delegate_object[:prompt_after_script_execution],
:prompt_color_after_script_execution),
filter: true,
quiet: true
) do |menu|
- filenames.each do |filename|
- menu.choice filename
- end
+ menu.choice @delegate_object[:prompt_yes]
+ menu.choice @delegate_object[:prompt_exit]
end
+ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
rescue TTY::Reader::InputInterrupt
exit 1
end
+ # user prompt to exit if the menu will be displayed again
+ #
+ def prompt_user_exit(block_name_from_cli:, selected:)
+ !block_name_from_cli &&
+ selected[:shell] == BlockType::BASH &&
+ @delegate_object[:pause_after_script_execution] &&
+ prompt_select_continue == MenuState::EXIT
+ end
+
# Handles the processing of a link block in Markdown Execution.
# It loads YAML data from the link_block_body content, pushes the state to history,
# sets environment variables, and decides on the next block to load.
#
# @param link_block_body [Array<String>] The body content as an array of strings.
@@ -1810,53 +2168,83 @@
dependencies = {}
end
# load key and values from link block into current environment
#
- if link_block_data[LinkKeys::Vars]
+ if link_block_data[LinkKeys::VARS]
code_lines.push BashCommentFormatter.format_comment(selected[:oname])
- (link_block_data[LinkKeys::Vars] || []).each do |(key, value)|
+ (link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
ENV[key] = value.to_s
code_lines.push(assign_key_value_in_bash(key, value))
end
end
- ## append blocks loaded, apply LinkKeys::Eval
+ ## append blocks loaded, apply LinkKeys::EVAL
#
- if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
+ if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
load_filespec = load_filespec_from_expression(load_expr)
code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
end
# if an eval link block, evaluate code_lines and return its standard output
#
- if link_block_data.fetch(LinkKeys::Eval,
- false) || link_block_data.fetch(LinkKeys::Exec, false)
+ if link_block_data.fetch(LinkKeys::EVAL,
+ false) || link_block_data.fetch(LinkKeys::EXEC, false)
code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
end
next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
- if link_block_data[LinkKeys::Return]
+ if link_block_data[LinkKeys::RETURN]
pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
dependencies, selected)
else
link_history_push_and_next(
curr_block_name: selected[:oname],
curr_document_filename: @delegate_object[:filename],
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
- next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
- nil) || link_block_data[LinkKeys::Block] || '',
+ next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK,
+ nil) || link_block_data[LinkKeys::BLOCK] || '',
next_document_filename: next_document_filename,
- next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
+ next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
)
end
end
+ # Handle expression with wildcard characters
+ # allow user to select or enter
+ def puts_gets_oprompt_(filespec)
+ puts format(@delegate_object[:prompt_show_expr_format],
+ { expr: filespec })
+ puts @delegate_object[:prompt_enter_filespec]
+ gets.chomp
+ end
+
+ # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
+ # @param selected [Hash] Selected item from the menu containing a YAML body.
+ # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
+ # @return [LoadFileLinkState] An instance indicating the next action for loading files.
+ def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
+ obj = {}
+ data = YAML.load(selected[:body].join("\n"))
+ (data || []).each do |key, value|
+ sym_key = key.to_sym
+ obj[sym_key] = value
+
+ print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
+ end
+
+ link_state.block_name = nil
+ OpenStruct.new(options: obj,
+ load_file_link_state: LoadFileLinkState.new(
+ LoadFile::REUSE, link_state
+ ))
+ end
+
# Check if the delegate object responds to a given method.
# @param method_name [Symbol] The name of the method to check.
# @param include_private [Boolean] Whether to include private methods in the check.
# @return [Boolean] true if the delegate object responds to the method, false otherwise.
def respond_to?(method_name, include_private = false)
@@ -1888,296 +2276,51 @@
return unless (@delegate_object[exception_sym]).positive?
exit @delegate_object[exception_sym]
end
- def save_to_file(required_lines:, selected:)
- write_command_file(required_lines: required_lines, selected: selected)
- @fout.fout "File saved: #{@run_state.saved_filespec}"
- end
+ # allow user to select or enter
+ def save_filespec_from_expression(expression)
+ # Process expression with embedded formatting
+ formatted = formatted_expression(expression)
- def block_state_for_name_from_cli(block_name)
- SelectedBlockMenuState.new(
- @dml_blocks_in_file.find do |item|
- item[:oname] == block_name
- end&.merge(
- block_name_from_cli: true,
- block_name_from_ui: false
- ),
- MenuState::CONTINUE
- )
+ # Handle wildcards or direct file specification
+ if contains_wildcards?(formatted)
+ save_filespec_wildcard_expansion(formatted)
+ else
+ formatted
+ end
end
- # Select and execute a code block from a Markdown document.
- #
- # This method allows the user to interactively select a code block from a
- # Markdown document, obtain approval, and execute the chosen block of code.
- #
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
- def document_menu_loop
- @menu_base_options = @delegate_object
- @dml_link_state = LinkState.new(
- block_name: @delegate_object[:block_name],
- document_filename: @delegate_object[:filename]
- )
- @run_state.block_name_from_cli = @dml_link_state.block_name.present?
- @cli_block_name = @dml_link_state.block_name
- @dml_now_using_cli = @run_state.block_name_from_cli
- @dml_menu_default_dname = nil
- @dml_block_state = SelectedBlockMenuState.new
-
- @run_state.batch_random = Random.new.rand
- @run_state.batch_index = 0
-
- InputSequencer.new(
- @delegate_object[:filename],
- @delegate_object[:input_cli_rest]
- ).run do |msg, data|
- case msg
- when :parse_document # once for each menu
- # puts "@ - parse document #{data}"
- ii_parse_document(data)
-
- when :display_menu
- # warn "@ - display menu:"
- # ii_display_menu
- @dml_block_state = SelectedBlockMenuState.new
- @delegate_object[:block_name] = nil
-
- when :user_choice
- if @dml_link_state.block_name.present?
- # @prior_block_was_link = true
- @dml_block_state.block = @dml_blocks_in_file.find do |item|
- item[:oname] == @dml_link_state.block_name
- end
- @dml_link_state.block_name = nil
- else
- # puts "? - Select a block to execute (or type #{$texit} to exit):"
- break if ii_user_choice == :break # into @dml_block_state
- break if @dml_block_state.block.nil? # no block matched
- end
- # puts "! - Executing block: #{data}"
- # @dml_block_state.block[:oname]
- @dml_block_state.block&.fetch(:oname, nil)
-
- when :execute_block
- block_name = data
- if block_name == '* Back' ####
- debounce_reset
- @menu_user_clicked_back_link = true
- load_file_link_state = pop_link_history_and_trigger_load
- @dml_link_state = load_file_link_state.link_state
-
- InputSequencer.merge_link_state(
- @dml_link_state,
- InputSequencer.next_link_state(
- block_name: @dml_link_state.block_name,
- document_filename: @dml_link_state.document_filename,
- prior_block_was_link: true
- )
- )
-
- else
- @dml_block_state = block_state_for_name_from_cli(block_name)
- if @dml_block_state.block[:shell] == BlockType::OPTS
- debounce_reset
- link_state = LinkState.new
- options_state = read_show_options_and_trigger_reuse(
- selected: @dml_block_state.block,
- link_state: link_state
- )
-
- @menu_base_options.merge!(options_state.options)
- @delegate_object.merge!(options_state.options)
- options_state.load_file_link_state.link_state
- else
- ii_execute_block(block_name)
-
- if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
- selected: @dml_block_state.block)
- return :break
- end
-
- ## order of block name processing: link block, cli, from user
- #
- @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
- HashDelegator.next_link_state(
- block_name: @dml_link_state.block_name,
- block_name_from_cli: !@dml_link_state.block_name.present?,
- block_state: @dml_block_state,
- was_using_cli: @dml_now_using_cli
- )
-
- if !@dml_block_state.block[:block_name_from_ui] && cli_break
- # &bsp '!block_name_from_ui + cli_break -> break'
- return :break
- end
-
- InputSequencer.next_link_state(
- block_name: @dml_link_state.block_name,
- prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
- )
- end
- end
-
- when :exit?
- data == $texit
- when :stay?
- data == $stay
+ # Handle expression with wildcard characters
+ # allow user to select or enter
+ def save_filespec_wildcard_expansion(filespec)
+ files = find_files(filespec)
+ case files.count
+ when 0
+ prompt_for_filespec_with_wildcard(filespec)
+ else
+ ## user selects from existing files or other
+ # input into path with wildcard for easy entry
+ #
+ name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
+ case name
+ when @delegate_object[:prompt_filespec_back]
+ # do nothing
+ when @delegate_object[:prompt_filespec_other]
+ prompt_for_filespec_with_wildcard(filespec)
else
- raise "Invalid message: #{msg}"
+ name
end
end
- rescue StandardError
- HashDelegator.error_handler('document_menu_loop',
- { abort: true })
end
- def ii_parse_document(_document_filename)
- @run_state.batch_index += 1
- @run_state.in_own_window = false
-
- # &bsp 'loop', block_name_from_cli, @cli_block_name
- @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
- now_using_cli: @dml_now_using_cli,
- link_state: @dml_link_state)
+ def save_to_file(required_lines:, selected:)
+ write_command_file(required_lines: required_lines, selected: selected)
+ @fout.fout "File saved: #{@run_state.saved_filespec}"
end
- def ii_user_choice
- @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
- menu_blocks: @dml_menu_blocks,
- default: @dml_menu_default_dname)
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
- if !@dml_block_state
- HashDelegator.error_handler('block_state missing', { abort: true })
- elsif @dml_block_state.state == MenuState::EXIT
- # &bsp 'load_cli_or_user_selected_block -> break'
- :break
- end
- end
-
- def ii_execute_block(block_name)
- @dml_block_state = block_state_for_name_from_cli(block_name)
-
- dump_and_warn_block_state(selected: @dml_block_state.block)
- @dml_link_state, @dml_menu_default_dname = \
- exec_bash_next_state(
- selected: @dml_block_state.block,
- mdoc: @dml_mdoc,
- link_state: @dml_link_state,
- block_source: {
- document_filename: @delegate_object[:filename],
- time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
- }
- )
- end
-
- def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
- lfls = execute_shell_type(
- selected: selected,
- mdoc: mdoc,
- link_state: link_state,
- block_source: block_source
- )
-
- # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
- [lfls.link_state,
- lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
- end
-
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
- block_name_from_cli, now_using_cli = \
- manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
- now_using_cli: now_using_cli,
- link_state: link_state)
- set_delob_filename_block_name(link_state: link_state,
- block_name_from_cli: block_name_from_cli)
-
- # update @delegate_object and @menu_base_options in auto_load
- #
- blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
- dump_delobj(blocks_in_file, menu_blocks, link_state)
-
- [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
- end
-
- # user prompt to exit if the menu will be displayed again
- #
- def prompt_user_exit(block_name_from_cli:, selected:)
- !block_name_from_cli &&
- selected[:shell] == BlockType::BASH &&
- @delegate_object[:pause_after_script_execution] &&
- prompt_select_continue == MenuState::EXIT
- end
-
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
- if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
- # &bsp 'pause cli control, allow user to select block'
- block_name_from_cli = false
- now_using_cli = false
- @menu_base_options[:block_name] = \
- @delegate_object[:block_name] = \
- link_state.block_name = \
- @cli_block_name = nil
- end
-
- @delegate_object = @menu_base_options.dup
- @menu_user_clicked_back_link = false
- [block_name_from_cli, now_using_cli]
- end
-
- # Update the block name in the link state and delegate object.
- #
- # This method updates the block name based on whether it was specified
- # through the CLI or derived from the link state.
- #
- # @param link_state [LinkState] The current link state object.
- # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
- def set_delob_filename_block_name(link_state:, block_name_from_cli:)
- @delegate_object[:filename] = link_state.document_filename
- link_state.block_name = @delegate_object[:block_name] =
- block_name_from_cli ? @cli_block_name : link_state.block_name
- end
-
- # Outputs warnings based on the delegate object's configuration
- #
- # @param delegate_object [Hash] The delegate object containing configuration flags.
- # @param blocks_in_file [Hash] Hash of blocks present in the file.
- # @param menu_blocks [Hash] Hash of menu blocks.
- # @param link_state [LinkState] Current state of the link.
- def dump_delobj(blocks_in_file, menu_blocks, link_state)
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
-
- if @delegate_object[:dump_blocks_in_file]
- warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
- label: 'blocks_in_file')
- end
-
- if @delegate_object[:dump_menu_blocks]
- warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
- label: 'menu_blocks')
- end
-
- warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
- warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
- return unless @delegate_object[:dump_inherited_lines]
-
- warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
- end
-
- def dump_and_warn_block_state(selected:)
- if selected.nil?
- Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
- { abort: true })
- end
-
- return unless @delegate_object[:dump_selected_block]
-
- warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
- end
-
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
def select_option_with_metadata(prompt_text, names, opts = {})
## configure to environment
#
unless opts[:select_page_height].positive?
@@ -2187,24 +2330,24 @@
# crashes if all menu options are disabled
selection = @prompt.select(prompt_text,
names,
opts.merge(filter: true))
- item = names.find do |item|
+ selected_name = names.find do |item|
if item.instance_of?(Hash)
item[:dname] == selection
else
item == selection
end
end
- item = { dname: item } if item.instance_of?(String)
- unless item
+ selected_name = { dname: selected_name } if selected_name.instance_of?(String)
+ unless selected_name
HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
exit 1
end
- item.merge(
+ selected_name.merge(
if selection == menu_chrome_colored_option(:menu_option_back_name)
{ option: selection, shell: BlockType::LINK }
elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
{ option: selection }
else
@@ -2215,10 +2358,39 @@
exit 1
rescue StandardError
HashDelegator.error_handler('select_option_with_metadata')
end
+ # Update the block name in the link state and delegate object.
+ #
+ # This method updates the block name based on whether it was specified
+ # through the CLI or derived from the link state.
+ #
+ # @param link_state [LinkState] The current link state object.
+ # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
+ def set_delob_filename_block_name(link_state:, block_name_from_cli:)
+ @delegate_object[:filename] = link_state.document_filename
+ link_state.block_name = @delegate_object[:block_name] =
+ block_name_from_cli ? @cli_block_name : link_state.block_name
+ end
+
+ def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
+ block_name_from_cli, now_using_cli = \
+ manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
+ now_using_cli: now_using_cli,
+ link_state: link_state)
+ set_delob_filename_block_name(link_state: link_state,
+ block_name_from_cli: block_name_from_cli)
+
+ # update @delegate_object and @menu_base_options in auto_load
+ #
+ blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
+ dump_delobj(blocks_in_file, menu_blocks, link_state)
+
+ [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
+ end
+
def set_environment_variables_for_block(selected)
code_lines = []
YAML.load(selected[:body].join("\n"))&.each do |key, value|
ENV[key] = value.to_s
@@ -2350,31 +2522,10 @@
# &bsp 'line is not recognized for block state'
end
end
- # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
- # @param selected [Hash] Selected item from the menu containing a YAML body.
- # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
- # @return [LoadFileLinkState] An instance indicating the next action for loading files.
- def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
- obj = {}
- data = YAML.load(selected[:body].join("\n"))
- (data || []).each do |key, value|
- sym_key = key.to_sym
- obj[sym_key] = value
-
- print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
- end
-
- link_state.block_name = nil
- OpenStruct.new(options: obj,
- load_file_link_state: LoadFileLinkState.new(
- LoadFile::Reuse, link_state
- ))
- end
-
def wait_for_stream_processing
@process_mutex.synchronize do
@process_cv.wait(@process_mutex)
end
end
@@ -2445,18 +2596,38 @@
)
rescue StandardError
HashDelegator.error_handler('write_command_file')
end
+ # Ensure the directory exists before writing the file
+ def write_file_with_directory_creation(save_filespec, content)
+ directory = File.dirname(save_filespec)
+
+ begin
+ FileUtils.mkdir_p(directory)
+ File.write(save_filespec, content)
+ rescue Errno::EACCES
+ warn "Permission denied: Unable to write to file '#{save_filespec}'"
+ nil
+ rescue Errno::EROFS
+ warn "Read-only file system: Unable to write to file '#{save_filespec}'"
+ nil
+ rescue StandardError => err
+ warn "An error occurred while writing to file '#{save_filespec}': #{err.message}"
+ nil
+ end
+ end
+
+ # return next document file name
def write_inherited_lines_to_file(link_state, link_block_data)
- save_expr = link_block_data.fetch(LinkKeys::Save, '')
+ save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
if save_expr.present?
save_filespec = save_filespec_from_expression(save_expr)
File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
@delegate_object[:filename]
else
- link_block_data[LinkKeys::File] || @delegate_object[:filename]
+ link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
end
end
end
class HashDelegator < HashDelegatorParent
@@ -2488,25 +2659,10 @@
obj
end
def self.next_link_state(*args, **kwargs, &block)
super
- # result = super
-
- # @logger ||= StdOutErrLogger.new
- # @logger.unknown(
- # HashDelegator.clean_hash_recursively(
- # { "HashDelegator.next_link_state":
- # { 'args': args,
- # 'at': Time.now.strftime('%FT%TZ'),
- # 'for': /[^\/]+:\d+/.match(caller.first)[0],
- # 'kwargs': kwargs,
- # 'return': result } }
- # )
- # )
-
- # result
end
end
end
return if $PROGRAM_NAME != __FILE__
@@ -2593,25 +2749,25 @@
c.execute_required_lines
end
# Test case for empty body
def test_push_link_history_and_trigger_load_with_empty_body
- assert_equal LoadFile::Reuse,
+ assert_equal LoadFile::REUSE,
@hd.push_link_history_and_trigger_load.load_file
end
# Test case for non-empty body without 'file' key
def test_push_link_history_and_trigger_load_without_file_key
body = ["vars:\n KEY: VALUE"]
- assert_equal LoadFile::Reuse,
+ assert_equal LoadFile::REUSE,
@hd.push_link_history_and_trigger_load(link_block_body: body).load_file
end
# Test case for non-empty body with 'file' key
def test_push_link_history_and_trigger_load_with_file_key
body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
- expected_result = LoadFileLinkState.new(LoadFile::Load,
+ expected_result = LoadFileLinkState.new(LoadFile::LOAD,
LinkState.new(block_name: 'sample_block',
document_filename: 'sample_file',
inherited_dependencies: {},
inherited_lines: ['# ', 'KEY="VALUE"']))
assert_equal expected_result,
@@ -2645,12 +2801,13 @@
def test_safeval_successful_evaluation
assert_equal 4, HashDelegator.safeval('2 + 2')
end
def test_safeval_rescue_from_error
- HashDelegator.stubs(:error_handler).with('safeval')
- assert_nil HashDelegator.safeval('invalid code')
+ assert_raises(SystemExit) do
+ HashDelegator.safeval('invalid_code_raises_exception')
+ end
end
def test_set_fcb_title
# sample input and output data for testing default_block_title_from_body method
input_output_data = [
@@ -3052,11 +3209,11 @@
result = @hd.pop_link_history_and_trigger_load
# Asserting the result is an instance of LoadFileLinkState
assert_instance_of LoadFileLinkState, result
- assert_equal LoadFile::Load, result.load_file
+ assert_equal LoadFile::LOAD, result.load_file
assert_nil result.link_state.block_name
end
end
class TestHashDelegatorHandleBlockState < Minitest::Test
@@ -3437,8 +3594,64 @@
# Test formatting a string containing UTF-8 characters
def test_format_utf8_characters
input = 'Unicode test: Δ, ΓΆ, π», and π are fun!'
expected = '# Unicode test: Δ, ΓΆ, π», and π are fun!'
assert_equal expected, BashCommentFormatter.format_comment(input)
+ end
+ end
+
+ class PromptForFilespecWithWildcardTest < Minitest::Test
+ def setup
+ @delegate_object = {
+ prompt_show_expr_format: 'Current expression: %{expr}',
+ prompt_enter_filespec: 'Please enter a filespec:'
+ }
+ @original_stdin = $stdin
+ end
+
+ def teardown
+ $stdin = @original_stdin
+ end
+
+ def test_prompt_for_filespec_with_normal_input
+ $stdin = StringIO.new("test_input\n")
+ result = prompt_for_filespec_with_wildcard('*.txt')
+ assert_equal 'resolved_path_or_substituted_value', result
+ end
+
+ def test_prompt_for_filespec_with_interruption
+ $stdin = StringIO.new
+ # rubocop disable:Lint/NestedMethodDefinition
+ def $stdin.gets; raise Interrupt; end
+ # rubocop enable:Lint/NestedMethodDefinition
+
+ result = prompt_for_filespec_with_wildcard('*.txt')
+ assert_nil result
+ end
+
+ def test_prompt_for_filespec_with_empty_input
+ $stdin = StringIO.new("\n")
+ result = prompt_for_filespec_with_wildcard('*.txt')
+ assert_equal 'resolved_path_or_substituted_value', result
+ end
+
+ private
+
+ def prompt_for_filespec_with_wildcard(filespec)
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
+ puts @delegate_object[:prompt_enter_filespec]
+
+ begin
+ input = gets.chomp
+ PathUtils.resolve_path_or_substitute(input, filespec)
+ rescue Interrupt
+ nil
+ end
+ end
+
+ module PathUtils
+ def self.resolve_path_or_substitute(input, filespec)
+ 'resolved_path_or_substituted_value' # Placeholder implementation
+ end
end
end
end # module MarkdownExec