persistent-shell-history 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *~
2
+ *.gem
3
+ .*swp
4
+ .bundle
5
+ Gemfile.lock
6
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in persistent-shel-history.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,34 @@
1
+ This is a quick job, to have a local database, to collect _all_ commands
2
+ from ~/.bash_history
3
+
4
+ To couple with this, I have set a crontab job:
5
+ */30 * * * * /home/vbatts/bin/bash_history.rb
6
+
7
+
8
+ and set in ~/.bashrc:
9
+ unset HISTFILESIZE
10
+ export HISTSIZE=10000
11
+ export HISTTIMEFORMAT="%F %T "
12
+ export HISTCONTROL="ignoreboth"
13
+
14
+
15
+ == USAGE
16
+ See the --help also,
17
+ Usage: bash_history [options]
18
+ --inspect inspect the data
19
+ -h, --history FILE use bash_history FILE instead of the default (~/.bash_history)
20
+ -d, --db FILE use database FILE instead of the default (~/.bash_history.db)
21
+ -l, --list list history
22
+ --fix fix times
23
+ --format FORMAT specify a different strftime format. (default is "%F %T")
24
+ -f, --find PAT find a command with pattern PAT
25
+
26
+
27
+ == LICENSE
28
+ Copyright (c) 2012 Vincent Batts, Raleigh, NC, USA
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
31
+
32
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ task :default => :build
4
+
data/TODOs ADDED
@@ -0,0 +1,17 @@
1
+ * add a ~/.persistent-shell-history.yaml
2
+ * move the db from gdbm to sqlite
3
+ - tables
4
+ ** hostname
5
+ -- id
6
+ -- name
7
+ ** command
8
+ -- id
9
+ -- value
10
+ ** record
11
+ -- id
12
+ -- timestamp
13
+ -- hostname_id
14
+ -- command_id
15
+ * migration from current Marshal version need only to check the value, for if
16
+ it starts with "\x04\b{"
17
+
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'persistent-shell-history/binary-history-store'
4
+ require 'persistent-shell-history/old-history-store'
5
+ require 'optparse'
6
+
7
+ options = {}
8
+ bh_options = {:archive_file => File.expand_path("~/.bash_history.db")}
9
+ opts = OptionParser.new do |opts|
10
+ opts.on('--inspect','inspect the data') do |o|
11
+ options[:inspect] = o
12
+ end
13
+ opts.on('-h','--history FILE','use bash_history FILE instead of the default (~/.bash_history)') do |o|
14
+ bh_options[:file] = o
15
+ end
16
+ opts.on('-d','--db FILE','use database FILE instead of the default (~/.bash_history.db)') do |o|
17
+ bh_options[:archive_file] = o
18
+ end
19
+ opts.on('--migrate','check-for and migrate, only') do |o|
20
+ options[:migrate] = o
21
+ end
22
+ opts.on('-l','--list','list history') do |o|
23
+ options[:list] = o
24
+ end
25
+ opts.on('--format FORMAT','specify a different strftime format. (default is "%F %T")') do |o|
26
+ bh_options[:time_format] = o
27
+ end
28
+ opts.on('-f','--find PAT','find a command with pattern PAT') do |o|
29
+ options[:find] = o
30
+ end
31
+ end
32
+
33
+ begin
34
+ opts.parse!(ARGV)
35
+ rescue => ex
36
+ puts ex
37
+ puts opts
38
+ end
39
+
40
+ def migrate(old_bashhistorystore)
41
+ require 'fileutils'
42
+ # migrate
43
+ temp_db = GDBM.new("#{old_bashhistorystore.archive}.#{$$}")
44
+ old_bashhistorystore.keys.each {|key|
45
+ h = old_bashhistorystore[key]
46
+ h[:time] = h[:time].map {|t| t.to_i }
47
+ temp_db[key] = Marshal.dump(h)
48
+ }
49
+ old_bashhistorystore.db.close()
50
+ temp_db.close()
51
+ ts = "#{Time.now.year}#{Time.now.month}#{Time.now.day}#{Time.now.hour}#{Time.now.min}"
52
+ old_filename = "#{old_bashhistorystore.archive}.old.#{ts}"
53
+ FileUtils.mv(old_bashhistorystore.archive, old_filename)
54
+ puts "archived [#{old_bashhistorystore.archive}] to [#{old_filename}] ..."
55
+ FileUtils.mv("#{old_bashhistorystore.archive}.#{$$}", old_bashhistorystore.archive)
56
+ end
57
+
58
+ # First check the database, for whether it is the old format,
59
+ # if so, convert it, and reopen it.
60
+ bh = Persistent::Shell::OldHistoryStore.new(bh_options)
61
+ migrate(bh) if bh.is_oldformat?
62
+ bh.db.close unless bh.db.closed?
63
+
64
+ exit(0) if options[:migrate]
65
+
66
+ # re-open the history storage
67
+ bh = Persistent::Shell::BinaryHistoryStore.new(bh_options)
68
+
69
+ # load the new bash_history into the database
70
+ unless (options[:inspect] or options[:find] or options[:list])
71
+ bh.load()
72
+ bh.db.reorganize()
73
+ end
74
+
75
+ if options[:inspect]
76
+ p bh
77
+ #p "storing #{bh.keys.count} commands"
78
+ end
79
+ if options[:find]
80
+ bh.find(options[:find]).sort_by {|x| x[:time] }.each do |val|
81
+ puts bh.fmt(val)
82
+ end
83
+ elsif options[:list]
84
+ bh.values_by_time.each do |val|
85
+ puts bh.fmt(val)
86
+ end
87
+ end
88
+
89
+ # vim: set sts=2 sw=2 et ai:
@@ -0,0 +1,9 @@
1
+ require 'persistent-shell-history/version'
2
+ require 'persistent-shell-history/history'
3
+
4
+ module Persistent
5
+ module Shell
6
+ end
7
+ end
8
+
9
+ # vim: set sts=2 sw=2 et ai:
@@ -0,0 +1,17 @@
1
+
2
+ module Persistent
3
+ module Shell
4
+
5
+ class AbstractHistoryStore
6
+ SCHEMA_VERSION = "1"
7
+
8
+ def commands; end
9
+ def db; end
10
+ def shema_match?; db.has_key? "schema_version" and db["schema_version"] == SCHEMA_VERSION; end
11
+ def shema_version; db["schema_version"] if db.has_key? "schema_version" ; end
12
+ end # class AbstractHistoryStore
13
+
14
+ end
15
+ end
16
+
17
+ # vim: set sts=2 sw=2 et ai:
@@ -0,0 +1,129 @@
1
+ require 'digest/md5'
2
+ require 'gdbm'
3
+ require 'yaml'
4
+
5
+ require 'persistent-shell-history/abstract-history-store'
6
+ require 'persistent-shell-history/history'
7
+ require 'persistent-shell-history/command'
8
+
9
+ if RUBY_VERSION >= "1.9" # assuming you're running Ruby ~1.9
10
+ Encoding.default_external = Encoding::UTF_8
11
+ Encoding.default_internal = Encoding::UTF_8
12
+ end
13
+
14
+ module Persistent
15
+ module Shell
16
+ class BinaryHistoryStore < AbstractHistoryStore
17
+ OPTIONS = {
18
+ :file => File.expand_path("~/.bash_history"),
19
+ :archive_file => File.expand_path("~/.bash_history.db"),
20
+ :time_format => "%F %T",
21
+ }
22
+
23
+ def initialize(opts = {})
24
+ @options = OPTIONS.merge(opts)
25
+ end
26
+
27
+ def archive; @options[:archive_file]; end
28
+ def archive=(arg); @options[:archive_file] = arg; end
29
+
30
+ def hist_file; @options[:file]; end
31
+ def hist_file=(arg); @options[:file] = arg; end
32
+
33
+ def time_format; @options[:time_format]; end
34
+ def time_format=(tf); @options[:time_format] = tf; end
35
+ def db
36
+ @db ||= GDBM.new(@options[:archive_file])
37
+ end
38
+ def [](key); _ml(db[key]); end
39
+ def keys; db.keys; end
40
+ def values; db.map {|k,v| _ml(v) }; end
41
+ def values_by_time
42
+ return db.map {|k,v|
43
+ data = _ml(v)
44
+ data[:time].map {|t|
45
+ data.merge(:time => t)
46
+ }
47
+ }.flatten.sort_by {|x|
48
+ x[:time]
49
+ }
50
+ end
51
+ def commands; values.map {|v| v[:cmd] }; end
52
+ def _md(data); Marshal.dump(data); end
53
+ def _ml(data); Marshal.load(data); end
54
+ def _md5(data); Digest::MD5.hexdigest(data); end
55
+
56
+ # display a formatted time commend
57
+ def fmt(cmd); " %s %s" % [Time.at(cmd[:time]).strftime(@options[:time_format]), cmd[:cmd]]; end
58
+
59
+ def find(pat)
60
+ return values.select {|v|
61
+ v if v[:cmd] =~ /#{pat}/
62
+ }.map {|v|
63
+ v[:time].map {|t|
64
+ v.merge(:time => t)
65
+ }
66
+ }.flatten
67
+ end
68
+
69
+ def load(filename = @options[:file])
70
+ open(filename) do |f|
71
+ f.each_line do |line|
72
+ if line =~ /^#(.*)$/
73
+ l = f.readline.chomp
74
+ key = _md5(l)
75
+ if db.has_key?(key)
76
+ times = _ml(db[key])[:time]
77
+ if times.kind_of? Array
78
+ times.push($1.to_i)
79
+ else
80
+ times = [times]
81
+ end
82
+ db[key] = _md({:cmd => l, :time => times.uniq })
83
+ else
84
+ db[key] = _md({:cmd => l, :time => [$1.to_i] })
85
+ end
86
+ else
87
+ key = _md5(line.chomp)
88
+ if db.has_key?(key)
89
+ times = _ml(db[key])[:time]
90
+ if times.kind_of? Array
91
+ times.push($1.to_i)
92
+ else
93
+ times = [times]
94
+ end
95
+ db[key] = _md({:cmd => l, :time => times.uniq })
96
+ else
97
+ db[key] = _md({:cmd => line.chomp, :time => [0] })
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ # returns a Persistent::Shell::History object from the current GDBM database.
105
+ # intended for marshalling to other history-stores
106
+ def to_history
107
+ history = History.new
108
+ values.each do |value|
109
+ value[:time].each do |t|
110
+ history << Command.new(value[:cmd], t.to_i)
111
+ end
112
+ end
113
+ return history
114
+ end
115
+
116
+ # create an output that looks like a regular ~/.bash_history file
117
+ def render(file)
118
+ File.open(file,'w+') do |f|
119
+ values.each do |v|
120
+ f.write("#" + v[:time].to_i.to_s + "\n") if v[:time] and not (v[:time].to_i == 0)
121
+ f.write(v[:cmd] + "\n")
122
+ end
123
+ end
124
+ end
125
+
126
+ end # class BinaryHistoryStore
127
+ end # Shell
128
+ end # Persistent
129
+
@@ -0,0 +1,21 @@
1
+
2
+ require 'digest/md5'
3
+ require 'json'
4
+
5
+ module Persistent
6
+ module Shell
7
+ class Command < Struct.new(:cmd, :time)
8
+ def md5
9
+ Digest::MD5.hexdigest(cmd)
10
+ end
11
+ def to_h
12
+ { :cmd => cmd, :time => time, }
13
+ end
14
+ def to_json(*a)
15
+ to_h.to_json(*a)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ # vim: set sts=2 sw=2 et ai:
@@ -0,0 +1,43 @@
1
+
2
+ require 'persistent-shell-history/command'
3
+ require 'json'
4
+
5
+ module Persistent
6
+ module Shell
7
+ # Abstract storage for command history
8
+ class History
9
+ def initialize()
10
+ @cmds = Array.new
11
+ end
12
+ def commands; @cmds; end
13
+ def commands=(cmds); @cmds = cmds; end
14
+ def <<(arg); @cmds << arg; end
15
+ def to_a; commands.map {|c| c.to_h }; end
16
+ def to_json(*a); commands.to_json(*a); end
17
+ end
18
+ class BashHistory < History
19
+ def initialize(filename = '~/.bash_history')
20
+ @filename = File.expand_path(filename)
21
+ end
22
+ def commands; (@cmds.nil? or @cmds.empty?) ? (@cmds = parse) : @cmds; end
23
+ def file; @filename; end
24
+ def file=(filename); @filename = File.expand_path(filename); end
25
+ def parse(filename = @filename)
26
+ cmds = Array.new
27
+ open(filename) do |f|
28
+ f.each_line do |line|
29
+ if line =~ /^#(.*)$/
30
+ l = f.readline.chomp
31
+ cmds << Command.new(l, $1)
32
+ else
33
+ cmds << Command.new(line, "0")
34
+ end
35
+ end
36
+ end
37
+ return cmds
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # vim: set sts=2 sw=2 et ai:
@@ -0,0 +1,141 @@
1
+
2
+ require 'digest/md5'
3
+ require 'gdbm'
4
+ require 'yaml'
5
+
6
+ require 'persistent-shell-history/abstract-history-store'
7
+ require 'persistent-shell-history/history'
8
+ require 'persistent-shell-history/command'
9
+
10
+ module Persistent
11
+ module Shell
12
+ class OldHistoryStore < AbstractHistoryStore
13
+ OPTIONS = {
14
+ :file => File.expand_path("~/.bash_history"),
15
+ :archive_file => File.expand_path("~/.bash_history.db"),
16
+ :time_format => "%F %T",
17
+ }
18
+
19
+ def initialize(opts = {})
20
+ @options = OPTIONS.merge(opts)
21
+ end
22
+
23
+ def archive; @options[:archive_file]; end
24
+ def archive=(arg); @options[:archive_file] = arg; end
25
+
26
+ def hist_file; @options[:file]; end
27
+ def hist_file=(arg); @options[:file] = arg; end
28
+
29
+ # check the archive, whether it's the old format.
30
+ # If so, it needs to be converted to the BinaryHistoryStore
31
+ def is_oldformat?
32
+ db.keys[0..5].each {|key|
33
+ begin
34
+ YAML.load(db[key])
35
+ rescue Psych::SyntaxError => ex
36
+ return false
37
+ rescue => ex
38
+ return false
39
+ end
40
+ }
41
+ return true
42
+ end
43
+
44
+ def time_format; @options[:time_format]; end
45
+ def time_format=(tf); @options[:time_format] = tf; end
46
+ def db
47
+ @db ||= GDBM.new(@options[:archive_file])
48
+ end
49
+ def [](key); _yl(db[key]); end
50
+ def keys; db.keys; end
51
+ def keys_to_i; keys.map {|i| i.to_i }; end
52
+ def values; db.map {|k,v| _yl(v) }; end
53
+ def values_by_time
54
+ return db.map {|k,v|
55
+ data = _yl(v)
56
+ data[:time].map {|t|
57
+ data.merge(:time => t)
58
+ }
59
+ }.flatten.sort_by {|x|
60
+ x[:time]
61
+ }
62
+ end
63
+ def commands; values.map {|v| v[:cmd] }; end
64
+ def _yd(data); YAML.dump(data); end
65
+ def _yl(data); YAML.load(data); end
66
+ def _md5(data); Digest::MD5.hexdigest(data); end
67
+
68
+ # display a formatted time commend
69
+ def fmt(cmd); " %s %s" % [cmd[:time].strftime(@options[:time_format]), cmd[:cmd]]; end
70
+
71
+ def find(pat)
72
+ return values.select {|v|
73
+ v if v[:cmd] =~ /#{pat}/
74
+ }.map {|v|
75
+ v[:time].map {|t|
76
+ v.merge(:time => t)
77
+ }
78
+ }.flatten
79
+ end
80
+
81
+ def load(filename = @options[:file])
82
+ open(filename) do |f|
83
+ f.each_line do |line|
84
+ if line =~ /^#(.*)$/
85
+ l = f.readline.chomp
86
+ key = _md5(l)
87
+ if db.has_key?(key)
88
+ times = _yl(db[key])[:time]
89
+ if times.kind_of? Array
90
+ times.push(Time.at($1.to_i))
91
+ else
92
+ times = [times]
93
+ end
94
+ db[key] = _yd({:cmd => l, :time => times.uniq })
95
+ else
96
+ db[key] = _yd({:cmd => l, :time => [Time.at($1.to_i)] })
97
+ end
98
+ else
99
+ key = _md5(line.chomp)
100
+ if db.has_key?(key)
101
+ times = _yl(db[key])[:time]
102
+ if times.kind_of? Array
103
+ times.push(Time.at($1.to_i))
104
+ else
105
+ times = [times]
106
+ end
107
+ db[key] = _yd({:cmd => l, :time => times.uniq })
108
+ else
109
+ db[key] = _yd({:cmd => line.chomp, :time => [Time.at(0)] })
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # returns a Persistent::Shell::History object from the current GDBM database.
117
+ # intended for marshalling to other history-stores
118
+ def to_history
119
+ history = History.new
120
+ values.each do |value|
121
+ value[:time].each do |t|
122
+ history << Command.new(value[:cmd], t.to_i)
123
+ end
124
+ end
125
+ return history
126
+ end
127
+
128
+ # create an output that looks like a regular ~/.bash_history file
129
+ def render(file)
130
+ File.open(file,'w+') do |f|
131
+ values.each do |v|
132
+ f.write("#" + v[:time].to_i.to_s + "\n") if v[:time] and not (v[:time].to_i == 0)
133
+ f.write(v[:cmd] + "\n")
134
+ end
135
+ end
136
+ end
137
+ end # class DataStore
138
+ end # module Shell
139
+ end # module Persistent
140
+
141
+ # vim: set sts=2 sw=2 et ai:
@@ -0,0 +1,7 @@
1
+ module Persistent
2
+ module Shell
3
+ VERSION = "0.0.3"
4
+ end
5
+ end
6
+
7
+ # vim: set sts=2 sw=2 et ai:
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "persistent-shell-history/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "persistent-shell-history"
7
+ s.version = Persistent::Shell::VERSION
8
+ s.authors = ["Vincent Batts"]
9
+ s.email = ["vbatts@hashbangbash.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{bash shell history collection}
12
+ s.description = %q{
13
+ This is a quick job, to have a local database, to collect _all_ commands
14
+ from ~/.bash_history
15
+ See README for other implementation helpers.
16
+ }
17
+
18
+ s.rubyforge_project = "persistent-shell-history"
19
+ s.add_dependency("json")
20
+ s.add_dependency("gdbm")
21
+ s.add_dependency("ffi")
22
+
23
+ s.add_dependency("activerecord")
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ["lib"]
29
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: persistent-shell-history
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Vincent Batts
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &11562800 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *11562800
25
+ - !ruby/object:Gem::Dependency
26
+ name: gdbm
27
+ requirement: &11562380 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *11562380
36
+ - !ruby/object:Gem::Dependency
37
+ name: ffi
38
+ requirement: &11561960 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *11561960
47
+ - !ruby/object:Gem::Dependency
48
+ name: activerecord
49
+ requirement: &11561540 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *11561540
58
+ description: ! '
59
+
60
+ This is a quick job, to have a local database, to collect _all_ commands
61
+
62
+ from ~/.bash_history
63
+
64
+ See README for other implementation helpers.
65
+
66
+ '
67
+ email:
68
+ - vbatts@hashbangbash.com
69
+ executables:
70
+ - bash_history.rb
71
+ extensions: []
72
+ extra_rdoc_files: []
73
+ files:
74
+ - .gitignore
75
+ - Gemfile
76
+ - README
77
+ - Rakefile
78
+ - TODOs
79
+ - bin/bash_history.rb
80
+ - lib/persistent-shell-history.rb
81
+ - lib/persistent-shell-history/abstract-history-store.rb
82
+ - lib/persistent-shell-history/binary-history-store.rb
83
+ - lib/persistent-shell-history/command.rb
84
+ - lib/persistent-shell-history/history.rb
85
+ - lib/persistent-shell-history/old-history-store.rb
86
+ - lib/persistent-shell-history/version.rb
87
+ - persistent-shell-history.gemspec
88
+ homepage: ''
89
+ licenses: []
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project: persistent-shell-history
108
+ rubygems_version: 1.8.17
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: bash shell history collection
112
+ test_files: []
113
+ has_rdoc: