9 Commits

Author SHA1 Message Date
e5c1beb937 refactor search function. rename -being and -end to -after and -before 2021-06-05 22:41:34 +02:00
a2c4c99af4 WIP: implement reverse search 2021-06-05 19:59:47 +02:00
e292b4ce74 Liner WIP 2021-06-05 17:41:58 +02:00
a8f35951f1 Replace previous PROMPT_COMMAND in bashrc with more convenient option 2021-06-05 15:42:33 +02:00
b9e3a3629b Fix bug in cwd search and fix go-staticcheck regexp complaint 2021-06-05 15:05:18 +02:00
16752411e1 Add 'nolog' subcommand
The nolog subcommand instructs the user on how to disable logging
for the current shell. This is not (easily) doable automatically
since we can't affect a parent's environment without insane hacks.

Fixes #4
2021-06-05 14:49:25 +02:00
c480519fca Resolve CWD arguments as absolute path
Previously when calling e.g 'hs9001 search -cwd .' we would search
for a litteral '.' in the database. Now we resolve that argument
relative to the CWD.
2021-06-05 14:28:02 +02:00
70e66f47ba CLI: Rename -workdir to -cwd so we have to type less 2021-06-05 14:27:25 +02:00
216e59747c Add 'version' command 2021-05-16 18:38:50 +02:00
6 changed files with 179 additions and 38 deletions

6
Makefile Normal file
View File

@ -0,0 +1,6 @@
GIT_COMMIT=$(shell git rev-list -1 HEAD)
GIT_TAG=$(shell git tag --sort="-version:refname" | head -n 1)
all:
go build -ldflags "-X main.GitCommit=${GIT_COMMIT} -X main.GitTag=${GIT_TAG}"

View File

@ -34,9 +34,7 @@ apk add hs9001
Add this to .bashrc Add this to .bashrc
``` ```
if [ -n "$PS1" ] ; then eval "$(hs9001 bash-enable)"
PROMPT_COMMAND='hs9001 add -ret $? "$(history 1)"'
fi
``` ```
By default, every system user gets his own database. You can override this by setting the environment variable By default, every system user gets his own database. You can override this by setting the environment variable
for all users that should write to your unified database. for all users that should write to your unified database.

1
go.mod
View File

@ -3,6 +3,7 @@ module hs9001
go 1.16 go 1.16
require ( require (
github.com/peterh/liner v1.2.1
github.com/tj/go-naturaldate v1.3.0 github.com/tj/go-naturaldate v1.3.0
modernc.org/sqlite v1.10.0 modernc.org/sqlite v1.10.0
) )

5
go.sum
View File

@ -8,8 +8,12 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
@ -50,7 +54,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

63
history.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"database/sql"
"io"
"log"
"strings"
)
type history struct {
conn *sql.DB
}
func (h *history) GetHistoryByPrefix(prefix string) (ph []string) {
opts := searchopts{}
opts.order = "DESC"
cmdqry := prefix + "%"
opts.command = &cmdqry
results := search(h.conn, opts)
for e := results.Front(); e != nil; e = e.Next() {
entry, ok := e.Value.(*HistoryEntry)
if !ok {
log.Panic("Failed to retrieve entries")
}
ph = append(ph, entry.cmd)
}
return
}
func (h *history) GetHistoryByPattern(pattern string) (ph []string, pos []int) {
opts := searchopts{}
opts.order = "DESC"
cmdqry := "%" + pattern + "%"
opts.command = &cmdqry
results := search(h.conn, opts)
for e := results.Front(); e != nil; e = e.Next() {
entry, ok := e.Value.(*HistoryEntry)
if !ok {
log.Panic("Failed to retrieve entries")
}
ph = append(ph, entry.cmd)
pos = append(pos, strings.Index(strings.ToLower(entry.cmd), strings.ToLower(pattern)))
}
return
}
func (h *history) ReadHistory(r io.Reader) (num int, err error) {
panic("not implemented")
}
func (h *history) WriteHistory(w io.Writer) (num int, err error) {
panic("not implemented")
}
func (h *history) AppendHistory(item string) {
panic("not implemented")
}
func (h *history) ClearHistory() {
panic("not implemented")
}
func (h *history) RLock() {
//noop
}
func (h *history) RUnlock() {
//noop
}

138
main.go
View File

@ -10,9 +10,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
"github.com/peterh/liner"
"github.com/tj/go-naturaldate" "github.com/tj/go-naturaldate"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
@ -27,6 +29,9 @@ type HistoryEntry struct {
timestamp time.Time timestamp time.Time
} }
var GitTag string
var GitCommit string
func databaseLocation() string { func databaseLocation() string {
envOverride := os.Getenv("HS9001_DB_PATH") envOverride := os.Getenv("HS9001_DB_PATH")
if envOverride != "" { if envOverride != "" {
@ -145,28 +150,47 @@ func importFromStdin(conn *sql.DB) {
} }
} }
func search(conn *sql.DB, q string, workdir string, beginTime time.Time, endTime time.Time, retval int) list.List { type searchopts struct {
command *string
workdir *string
after *time.Time
before *time.Time
retval *int
order string
}
func search(conn *sql.DB, opts searchopts) list.List {
args := make([]interface{}, 0)
var sb strings.Builder var sb strings.Builder
sb.WriteString("SELECT id, command, workdir, user, hostname, retval ") sb.WriteString("SELECT id, command, workdir, user, hostname, retval ")
sb.WriteString("FROM history ") sb.WriteString("FROM history ")
sb.WriteString("WHERE timestamp BETWEEN datetime(?, 'unixepoch') ") sb.WriteString("WHERE 1=1 ") //1=1 so we can append as many AND foo as we want, or none
sb.WriteString("AND datetime(?, 'unixepoch') ")
sb.WriteString("AND command LIKE ? ") if opts.command != nil {
sb.WriteString("AND workdir LIKE ? ") sb.WriteString("AND command LIKE ? ")
if retval != -9001 { args = append(args, opts.command)
sb.WriteString("AND retval = ? ")
} }
sb.WriteString("ORDER BY timestamp ASC ") if opts.workdir != nil {
sb.WriteString("AND workdir LIKE ? ")
args = append(args, opts.workdir)
}
if opts.after != nil {
sb.WriteString("AND timestamp > datetime(?, 'unixepoch') ")
args = append(args, opts.after.Unix())
}
if opts.before != nil {
sb.WriteString("AND timestamp < datetime(?, 'unixepoch') ")
args = append(args, opts.before.Unix())
}
if opts.retval != nil {
sb.WriteString("AND retval = ? ")
args = append(args, opts.retval)
}
sb.WriteString("ORDER BY timestamp ")
sb.WriteString("ASC ")
queryStmt := sb.String() queryStmt := sb.String()
args := make([]interface{}, 0)
args = append(args, beginTime.Unix())
args = append(args, endTime.Unix())
args = append(args, q)
args = append(args, workdir)
if retval != -9001 {
args = append(args, retval)
}
rows, err := conn.Query(queryStmt, args...) rows, err := conn.Query(queryStmt, args...)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
@ -231,7 +255,7 @@ func exists(path string) (bool, error) {
} }
func printUsage() { func printUsage() {
fmt.Fprintf(os.Stderr, "Usage: ./hs9001 <add/search/import>\n") fmt.Fprintf(os.Stderr, "Usage: ./hs9001 <add/search/import/nolog/bash-enable>\n")
} }
func main() { func main() {
@ -263,6 +287,29 @@ func main() {
migrateDatabase(conn, fetchDBVersion(conn)) migrateDatabase(conn, fetchDBVersion(conn))
switch cmd { switch cmd {
case "bash-ctrlr":
line := liner.NewLiner()
defer line.Close()
line.SetCtrlCAborts(true)
line.SetHistoryProvider(&history{conn: conn})
rdlineline := os.Getenv("READLINE_LINE")
rdlinepos := os.Getenv("READLINE_POS")
rdlineposint, _ := strconv.Atoi(rdlinepos)
if name, err := line.PromptWithSuggestionReverse("", rdlineline, rdlineposint); err == nil {
fmt.Fprintf(os.Stderr, "%s\n", name)
}
case "bash-enable":
fmt.Printf(`
if [ -n "$PS1" ] ; then
PROMPT_COMMAND='hs9001 add -ret $? "$(history 1)"'
bind -x '"\C-r": " READLINE_LINE=$(hs9001 bash-ctrlr 3>&1 1>&2 2>&3) READLINE_POINT=0"'
fi
`)
case "bash-disable":
fmt.Printf("unset PROMPT_COMMAND\n")
case "add": case "add":
var ret int var ret int
addCmd.IntVar(&ret, "ret", 0, "Return value of the command to add") addCmd.IntVar(&ret, "ret", 0, "Return value of the command to add")
@ -277,7 +324,7 @@ func main() {
} }
historycmd := args[0] historycmd := args[0]
var rgx = regexp.MustCompile("\\s+\\d+\\s+(.*)") var rgx = regexp.MustCompile(`\s+\d+\s+(.*)`)
rs := rgx.FindStringSubmatch(historycmd) rs := rgx.FindStringSubmatch(historycmd)
if len(rs) == 2 { if len(rs) == 2 {
add(conn, NewHistoryEntry(rs[1], ret)) add(conn, NewHistoryEntry(rs[1], ret))
@ -286,13 +333,13 @@ func main() {
fallthrough fallthrough
case "delete": case "delete":
var workDir string var workDir string
var beginTime string var afterTime string
var endTime string var beforeTime string
var distinct bool = true var distinct bool = true
var retVal int var retVal int
searchCmd.StringVar(&workDir, "workdir", "%", "Search only within this workdir") searchCmd.StringVar(&workDir, "cwd", "", "Search only within this workdir")
searchCmd.StringVar(&beginTime, "begin", "50 years ago", "Start searching from this timeframe") searchCmd.StringVar(&afterTime, "after", "", "Start searching from this timeframe")
searchCmd.StringVar(&endTime, "end", "now", "End searching from this timeframe") searchCmd.StringVar(&beforeTime, "before", "", "End searching from this timeframe")
searchCmd.BoolVar(&distinct, "distinct", true, "Remove consecutive duplicate commands from output") searchCmd.BoolVar(&distinct, "distinct", true, "Remove consecutive duplicate commands from output")
searchCmd.IntVar(&retVal, "ret", -9001, "Only query commands that returned with this exit code. -9001=all (default)") searchCmd.IntVar(&retVal, "ret", -9001, "Only query commands that returned with this exit code. -9001=all (default)")
@ -300,18 +347,39 @@ func main() {
args := searchCmd.Args() args := searchCmd.Args()
beginTimestamp, err := naturaldate.Parse(beginTime, time.Now())
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error())
}
endTimeStamp, err := naturaldate.Parse(endTime, time.Now())
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error())
}
q := strings.Join(args, " ") q := strings.Join(args, " ")
results := search(conn, "%"+q+"%", workDir, beginTimestamp, endTimeStamp, retVal)
opts := searchopts{}
opts.order = "ASC"
if q != "" {
cmd := "%" + q + "%"
opts.command = &cmd
}
if workDir != "" {
wd, err := filepath.Abs(workDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed parse working directory path: %s\n", err.Error())
}
opts.workdir = &wd
}
if afterTime != "" {
afterTimestamp, err := naturaldate.Parse(afterTime, time.Now())
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error())
}
opts.after = &afterTimestamp
}
if beforeTime != "" {
beforeTimestamp, err := naturaldate.Parse(beforeTime, time.Now())
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error())
}
opts.before = &beforeTimestamp
}
if retVal != -9001 {
opts.retval = &retVal
}
results := search(conn, opts)
previousCmd := "" previousCmd := ""
for e := results.Front(); e != nil; e = e.Next() { for e := results.Front(); e != nil; e = e.Next() {
@ -354,6 +422,8 @@ func main() {
os.Exit(23) os.Exit(23)
case "import": case "import":
importFromStdin(conn) importFromStdin(conn)
case "version":
fmt.Fprintf(os.Stdout, "Git Tag: %s\nGit Commit: %s\n", GitTag, GitCommit)
default: default:
fmt.Fprintf(os.Stderr, "Error: Unknown subcommand '%s' supplied\n\n", cmd) fmt.Fprintf(os.Stderr, "Error: Unknown subcommand '%s' supplied\n\n", cmd)
printUsage() printUsage()