summaryrefslogtreecommitdiff
path: root/lib/pry/history.rb
blob: 0e7b1f37e674b9490e303776d6a6dea0e85849c9 (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
154
155
156
157
158
159
class Pry
  # The History class is responsible for maintaining the user's input history,
  # both internally and within Readline.
  class History
    attr_accessor :loader, :saver, :pusher, :clearer

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

    def initialize(options={})
      @history = []
      @original_lines = 0
      @file_path = options[:file_path]
      restore_default_behavior
    end

    # Assign the default methods for loading, saving, pushing, and clearing.
    def restore_default_behavior
      Pry.config.input # force Readline to load if applicable

      @loader = method(:read_from_file)
      @saver  = method(:save_to_file)

      if defined?(Readline)
        @pusher  = method(:push_to_readline)
        @clearer = method(:clear_readline)
      else
        @pusher  = proc { }
        @clearer = proc { }
      end
    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)

        @pusher.call(line.chomp)
        @history << line.chomp
        @original_lines += 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)
      empty_or_invalid_line = line.empty? || invalid_readline_line?(line)

      unless empty_or_invalid_line || (@history.last && line == @history.last)
        @pusher.call(line)
        @history << line
        if !should_ignore?(line) && Pry.config.history.should_save
          @saver.call(line)
        end
      end
      line
    end
    alias << push

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

    # @return [Fixnum] The number of lines in history.
    def history_line_count
      @history.count
    end

    # @return [Fixnum] The number of lines in history from just this session.
    def session_line_count
      @history.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.dup
    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.histignore]
    # @return [Boolean] a boolean that notifies if the line was found in the
    #   histignore array.
    def should_ignore?(line)
      hist_ignore = Pry.config.history.histignore
      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.history.config.file`.
    def read_from_file
      path = history_file_path

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

    # The default pusher. Appends the given line to Readline::HISTORY.
    # @param [String] line
    def push_to_readline(line)
      Readline::HISTORY << line
    end

    # The default clearer. Clears Readline::HISTORY.
    def clear_readline
      Readline::HISTORY.shift until Readline::HISTORY.empty?
    end

    # The default saver. Appends the given line to `Pry.history.config.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
        FileUtils.mkdir_p(File.dirname(history_file_path))
        @history_file = File.open(history_file_path, 'a', 0600).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