<commit_before>require "shelly/version"

require 'readline'
require 'abbrev'

module Shelly
  # ============================
  # = Simple Command Structure =
  # ============================
  class Command
    attr_accessor :name, :description, :usage, :body
    
    def [](argstring)
      shell = Shelly::Interpreter.get_instance
      self.body[shell, argstring]
    end
  end
  
  # ===================
  # = The Interpreter =
  # ===================
  class Interpreter
    attr_accessor :prefix, :suffix, :prompt, :quote_input, :complete, :custom_complete
    
    def initialize
      self.quote_input = false
      self.prefix = nil
      self.suffix = ''
      self.complete = :filenames
      self.custom_complete = Proc.new { |word| [] }
      self.prompt = Proc.new do |c|
        "[Shelly: #{self.prefix}]#{c ? '|' : '>'} "
      end
      @commands = {}
      @exit = false
      @running = false
      @config_file_loaded = false
    end
    
    def add_command(cmd)
      command_names = @commands.keys
      if command_names.include?(cmd.name)
        raise ArgumentError.new "Already have a command named '#{cmd.name}'."
      end
      
      if cmd.name =~ /^[a-z0-9]+$/i
        @commands[cmd.name] = cmd
      else
        raise ArgumentError.new "Command names must be alphanumeric (given: '#{cmd.name}')."
      end
    end
    
    def load_config_file(filename)
      return if @config_file_loaded
      @config_file_loaded = true
      
      if File.exists? filename
        load filename, true
      end
    end
    
    def run!
      return if @running
      @running = true
      
      unless self.prefix
        raise 'Must set a prefix before running the interpreter'
      end
      
      # Setup the prompt
      if STDIN.tty?
        # Setup Readline
        Readline.completion_append_character = ''
        if self.complete == :none
          Readline.completion_proc = Proc.new do |s|
            []
          end
        else
          Readline.completion_proc = completion_proc
        end
        
        get_line = Proc.new do |c|
          Readline.readline(self.prompt[c], true).chomp
        end
        
        # Setup the terminal
        stty_save = `stty -g`.chomp
        cleanup = Proc.new { system('stty', stty_save) }
      else
        get_line = Proc.new { |c| STDIN.gets.chomp }
        cleanup  = Proc.new {}
      end
      
      # Set up command abbreviations
      command_names = Abbrev.abbrev(@commands.keys)
      
      # Do it!
      full_line = ''
      begin
        while (!@exit) and line = get_line[! full_line.empty?]
          full_line += line
          if full_line =~ /^(.*)\\$/
            # Line ends with a backslash, so wait for more input
            full_line = $1
            next
          elsif full_line =~ /^\\([a-zA-Z0-9]+)\s*(.*)$/
            # Special command. Look it up and execute it
            command_name = $1
            argstring = $2
            if command_names.keys.include?(command_name)
              command = @commands[command_names[command_name]]
              command[argstring]
            else
              STDERR.puts "Unknown special command '#{command_name}'"
            end
          elsif full_line =~ /^!(.*)$/
            # Shell command
            command = $1.strip
            system(command) unless command.empty?
          else
            # Run it!
            if self.quote_input
              full_line = full_line.shellescape
            end
            system("#{self.prefix} #{full_line} #{self.suffix}")
          end
          full_line = ''
        end
        puts 'Bye.'
      rescue
        puts # Make sure to output a newline at the end
      end
      
      cleanup[]
    end
    
    def exit!
      @exit = true
    end
    
    private
    def complete_filenames(partial)
      full_path = File.expand_path(partial)
      full_path += '/' if File.directory? full_path and partial.end_with? '/'
      completions = Dir[full_path + '*']
      if completions.length == 1
        result = completions[0]
        if File.directory? result
          result += '/'
        else
          result += ' '
        end<commit_msg>Fix filename completion for the empty string.<commit_after>require "shelly/version"

require 'readline'
require 'abbrev'

module Shelly
  # ============================
  # = Simple Command Structure =
  # ============================
  class Command
    attr_accessor :name, :description, :usage, :body
    
    def [](argstring)
      shell = Shelly::Interpreter.get_instance
      self.body[shell, argstring]
    end
  end
  
  # ===================
  # = The Interpreter =
  # ===================
  class Interpreter
    attr_accessor :prefix, :suffix, :prompt, :quote_input, :complete, :custom_complete
    
    def initialize
      self.quote_input = false
      self.prefix = nil
      self.suffix = ''
      self.complete = :filenames
      self.custom_complete = Proc.new { |word| [] }
      self.prompt = Proc.new do |c|
        "[Shelly: #{self.prefix}]#{c ? '|' : '>'} "
      end
      @commands = {}
      @exit = false
      @running = false
      @config_file_loaded = false
    end
    
    def add_command(cmd)
      command_names = @commands.keys
      if command_names.include?(cmd.name)
        raise ArgumentError.new "Already have a command named '#{cmd.name}'."
      end
      
      if cmd.name =~ /^[a-z0-9]+$/i
        @commands[cmd.name] = cmd
      else
        raise ArgumentError.new "Command names must be alphanumeric (given: '#{cmd.name}')."
      end
    end
    
    def load_config_file(filename)
      return if @config_file_loaded
      @config_file_loaded = true
      
      if File.exists? filename
        load filename, true
      end
    end
    
    def run!
      return if @running
      @running = true
      
      unless self.prefix
        raise 'Must set a prefix before running the interpreter'
      end
      
      # Setup the prompt
      if STDIN.tty?
        # Setup Readline
        Readline.completion_append_character = ''
        if self.complete == :none
          Readline.completion_proc = Proc.new do |s|
            []
          end
        else
          Readline.completion_proc = completion_proc
        end
        
        get_line = Proc.new do |c|
          Readline.readline(self.prompt[c], true).chomp
        end
        
        # Setup the terminal
        stty_save = `stty -g`.chomp
        cleanup = Proc.new { system('stty', stty_save) }
      else
        get_line = Proc.new { |c| STDIN.gets.chomp }
        cleanup  = Proc.new {}
      end
      
      # Set up command abbreviations
      command_names = Abbrev.abbrev(@commands.keys)
      
      # Do it!
      full_line = ''
      begin
        while (!@exit) and line = get_line[! full_line.empty?]
          full_line += line
          if full_line =~ /^(.*)\\$/
            # Line ends with a backslash, so wait for more input
            full_line = $1
            next
          elsif full_line =~ /^\\([a-zA-Z0-9]+)\s*(.*)$/
            # Special command. Look it up and execute it
            command_name = $1
            argstring = $2
            if command_names.keys.include?(command_name)
              command = @commands[command_names[command_name]]
              command[argstring]
            else
              STDERR.puts "Unknown special command '#{command_name}'"
            end
          elsif full_line =~ /^!(.*)$/
            # Shell command
            command = $1.strip
            system(command) unless command.empty?
          else
            # Run it!
            if self.quote_input
              full_line = full_line.shellescape
            end
            system("#{self.prefix} #{full_line} #{self.suffix}")
          end
          full_line = ''
        end
        puts 'Bye.'
      rescue
        puts # Make sure to output a newline at the end
      end
      
      cleanup[]
    end
    
    def exit!
      @exit = true
    end
    
    private
    def complete_filenames(partial)
      return Dir['*'] if partial == '' # empty string expands to all files in the current directory
      full_path = File.expand_path(partial)
      full_path += '/' if File.directory? full_path and partial.end_with? '/'
      completions = Dir[full_path + '*']
      if completions.length == 1
        result = completions[0]
        if File.directory? result
          result += '/'
        else
          result += ' '
        end
{'fraction_non_alphanumeric': 0.12069372345445965, 'fraction_numerical': 0.0023596035865974517, 'mean_word_length': 1.726600192988099, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5344004', 'n_tokens_mistral': 2522, 'n_tokens_neox': 2466, 'n_words': 627}