summaryrefslogtreecommitdiff
path: root/lib/pry/history.rb
blob: 5f9a58c38bb9188d994c446a75fdd62ab7025ed0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
# frozen_string_literal: true

class Pry
  # The History class is responsible for maintaining the user's input history,
  # both internally and within Readline.
  class History
    def self.default_file
      history_file =
        if ENV.key?('XDG_DATA_HOME') && ENV['XDG_DATA_HOME'] != ''
          # See XDG Base Directory Specification at
          # https://standards.freedesktop.org/basedir-spec/basedir-spec-0.8.html
          ENV['XDG_DATA_HOME'] + '/pry/pry_history'
        elsif File.exist?(File.expand_path('~/.pry_history'))
          '~/.pry_history'
        else
          '~/.local/share/pry/pry_history'
        end
      File.expand_path(history_file)
    end

    attr_accessor :loader, :saver

    # @return [Fixnum] Number of lines in history when Pry first loaded.
    attr_reader :original_lines

    # @return [Integer] total number of lines, including original lines
    attr_reader :history_line_count

    def initialize(options = {})
      @history = options[:history] || []
      @history_line_count = @history.count
      @file_path = options[:file_path]
      @original_lines = 0
      @loader = method(:read_from_file)
      @saver = method(:save_to_file)
    end

    # Load the input history using `History.loader`.
    # @return [Integer] The number of lines loaded
    def load
      @loader.call do |line|
        next if invalid_readline_line?(line)

        @history << line.chomp
        @original_lines += 1
        @history_line_count += 1
      end
    end

    # Add a line to the input history, ignoring blank and duplicate lines.
    # @param [String] line
    # @return [String] The same line that was passed in
    def push(line)
      return line if line.empty? || invalid_readline_line?(line)

      begin
        last_line = @history[-1]
      rescue IndexError
        last_line = nil
      end

      return line if line == last_line

      @history << line
      @history_line_count += 1
      @saver.call(line) if !should_ignore?(line) && Pry.config.history_save

      line
    end
    alias << push

    # Clear this session's history. This won't affect the contents of the
    # history file.
    def clear
      @history.clear
      @history_line_count = 0
      @original_lines = 0
    end

    # @return [Fixnum] The number of lines in history from just this session.
    def session_line_count
      @history_line_count - @original_lines
    end

    # Return an Array containing all stored history.
    # @return [Array<String>] An Array containing all lines of history loaded
    #   or entered by the user in the current session.
    def to_a
      @history.to_a
    end

    # Filter the history with the histignore options
    # @return [Array<String>] An array containing all the lines that are not
    #   included in the histignore.
    def filter(history)
      history.select { |l| l unless should_ignore?(l) }
    end

    private

    # Check if the line match any option in the histignore
    # [Pry.config.history_ignorelist]
    # @return [Boolean] a boolean that notifies if the line was found in the
    #   histignore array.
    def should_ignore?(line)
      hist_ignore = Pry.config.history_ignorelist
      return false if hist_ignore.nil? || hist_ignore.empty?

      hist_ignore.any? { |p| line.to_s.match(p) }
    end

    # The default loader. Yields lines from `Pry.config.history_file`.
    def read_from_file
      path = history_file_path

      File.foreach(path) { |line| yield(line) } if File.exist?(path)
    rescue SystemCallError => error
      warn "Unable to read history file: #{error.message}"
    end

    # The default saver. Appends the given line to `Pry.config.history_file`.
    def save_to_file(line)
      history_file.puts line if history_file
    end

    # The history file, opened for appending.
    def history_file
      if defined?(@history_file)
        @history_file
      else
        unless File.exist?(history_file_path)
          FileUtils.mkdir_p(File.dirname(history_file_path))
        end
        @history_file = File.open(history_file_path, 'a', 0o600).tap do |file|
          file.sync = true
        end
      end
    rescue SystemCallError => error
      warn "Unable to write history file: #{error.message}"
      @history_file = false
    end

    def history_file_path
      File.expand_path(@file_path || Pry.config.history_file)
    end

    def invalid_readline_line?(line)
      # `Readline::HISTORY << line` raises an `ArgumentError` if `line`
      # includes a null byte
      line.include?("\0")
    end
  end
end