From d54bece7057b1a07d8d194a2a4428aa760634e9f Mon Sep 17 00:00:00 2001 From: David Dworken Date: Wed, 7 Sep 2022 23:20:31 -0700 Subject: [PATCH] Add HomeDirectory to HistoryEntry so we can query with or without ~/ in the cwd atom --- client/client_test.go | 18 ++++++++++++++++++ client/data/data.go | 32 +++++++++++++++++--------------- client/lib/lib.go | 32 +++++++++++++++++++------------- client/lib/lib_test.go | 12 +++++++++++- 4 files changed, 65 insertions(+), 29 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 3c2b15a..daf93d9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -529,6 +529,24 @@ hishtory disable`) if strings.Count(out, "\n") != 4 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } + + // Search for a cwd based on the home directory + entry = data.MakeFakeHistoryEntry("foobar") + entry.HomeDirectory = "/home/david/" + entry.CurrentWorkingDirectory = "~/dir/" + manuallySubmitHistoryEntry(t, userSecret, entry) + out = tester.RunInteractiveShell(t, `hishtory export cwd:~/dir`) + expectedOutput := "foobar\n" + if diff := cmp.Diff(expectedOutput, out); diff != "" { + t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) + } + + // And search with the fully expanded path + out = tester.RunInteractiveShell(t, `hishtory export cwd:/home/david/dir`) + expectedOutput = "foobar\n" + if diff := cmp.Diff(expectedOutput, out); diff != "" { + t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) + } } func testUpdate(t *testing.T, tester shellTester) { diff --git a/client/data/data.go b/client/data/data.go index 74b14b9..de47f6d 100644 --- a/client/data/data.go +++ b/client/data/data.go @@ -30,6 +30,7 @@ type HistoryEntry struct { Hostname string `json:"hostname" gorm:"uniqueIndex:compositeindex"` Command string `json:"command" gorm:"uniqueIndex:compositeindex"` CurrentWorkingDirectory string `json:"current_working_directory" gorm:"uniqueIndex:compositeindex"` + HomeDirectory string `json:"home_directory" gorm:"uniqueIndex:compositeindex"` ExitCode int `json:"exit_code" gorm:"uniqueIndex:compositeindex"` StartTime time.Time `json:"start_time" gorm:"uniqueIndex:compositeindex"` EndTime time.Time `json:"end_time" gorm:"uniqueIndex:compositeindex"` @@ -141,11 +142,11 @@ func Search(db *gorm.DB, query string, limit int) ([]*HistoryEntry, error) { for _, token := range tokens { if strings.HasPrefix(token, "-") { if strings.Contains(token, ":") { - query, val, err := parseAtomizedToken(token[1:]) + query, v1, v2, err := parseAtomizedToken(token[1:]) if err != nil { return nil, err } - tx = tx.Where("NOT "+query, val) + tx = tx.Where("NOT "+query, v1, v2) } else { query, v1, v2, v3, err := parseNonAtomizedToken(token[1:]) if err != nil { @@ -154,11 +155,11 @@ func Search(db *gorm.DB, query string, limit int) ([]*HistoryEntry, error) { tx = tx.Where("NOT "+query, v1, v2, v3) } } else if strings.Contains(token, ":") { - query, val, err := parseAtomizedToken(token) + query, v1, v2, err := parseAtomizedToken(token) if err != nil { return nil, err } - tx = tx.Where(query, val) + tx = tx.Where(query, v1, v2) } else { query, v1, v2, v3, err := parseNonAtomizedToken(token) if err != nil { @@ -184,36 +185,35 @@ func parseNonAtomizedToken(token string) (string, interface{}, interface{}, inte return "(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken, nil } -func parseAtomizedToken(token string) (string, interface{}, error) { +func parseAtomizedToken(token string) (string, interface{}, interface{}, error) { splitToken := strings.SplitN(token, ":", 2) field := splitToken[0] val := splitToken[1] switch field { case "user": - return "(local_username = ?)", val, nil + return "(local_username = ?)", val, nil, nil case "host": fallthrough case "hostname": - return "(instr(hostname, ?) > 0)", val, nil + return "(instr(hostname, ?) > 0)", val, nil, nil case "cwd": - // TODO: Can I make this support querying via ~/ too? - return "(instr(current_working_directory, ?) > 0)", strings.TrimSuffix(val, "/"), nil + return "(instr(current_working_directory, ?) > 0 OR instr(REPLACE(current_working_directory, '~/', home_directory), ?) > 0)", strings.TrimSuffix(val, "/"), strings.TrimSuffix(val, "/"), nil case "exit_code": - return "(exit_code = ?)", val, nil + return "(exit_code = ?)", val, nil, nil case "before": t, err := parseTimeGenerously(val) if err != nil { - return "", nil, fmt.Errorf("failed to parse before:%s as a timestamp: %v", val, err) + return "", nil, nil, fmt.Errorf("failed to parse before:%s as a timestamp: %v", val, err) } - return "(CAST(strftime(\"%s\",start_time) AS INTEGER) < ?)", t.Unix(), nil + return "(CAST(strftime(\"%s\",start_time) AS INTEGER) < ?)", t.Unix(), nil, nil case "after": t, err := parseTimeGenerously(val) if err != nil { - return "", nil, fmt.Errorf("failed to parse after:%s as a timestamp: %v", val, err) + return "", nil, nil, fmt.Errorf("failed to parse after:%s as a timestamp: %v", val, err) } - return "(CAST(strftime(\"%s\",start_time) AS INTEGER) > ?)", t.Unix(), nil + return "(CAST(strftime(\"%s\",start_time) AS INTEGER) > ?)", t.Unix(), nil, nil default: - return "", nil, fmt.Errorf("search query contains unknown search atom %s", field) + return "", nil, nil, fmt.Errorf("search query contains unknown search atom %s", field) } } @@ -229,6 +229,7 @@ func EntryEquals(entry1, entry2 HistoryEntry) bool { entry1.Hostname == entry2.Hostname && entry1.Command == entry2.Command && entry1.CurrentWorkingDirectory == entry2.CurrentWorkingDirectory && + entry1.HomeDirectory == entry2.HomeDirectory && entry1.ExitCode == entry2.ExitCode && entry1.StartTime.Format(time.RFC3339) == entry2.StartTime.Format(time.RFC3339) && entry1.EndTime.Format(time.RFC3339) == entry2.EndTime.Format(time.RFC3339) @@ -240,6 +241,7 @@ func MakeFakeHistoryEntry(command string) HistoryEntry { Hostname: "localhost", Command: command, CurrentWorkingDirectory: "/tmp/", + HomeDirectory: "/home/david/", ExitCode: 2, StartTime: time.Now(), EndTime: time.Now(), diff --git a/client/lib/lib.go b/client/lib/lib.go index 44a5d9d..511e042 100644 --- a/client/lib/lib.go +++ b/client/lib/lib.go @@ -50,22 +50,22 @@ var TestConfigZshContents string var Version string = "Unknown" -func getCwd() (string, error) { +func getCwd() (string, string, error) { cwd, err := os.Getwd() if err != nil { - return "", fmt.Errorf("failed to get cwd for last command: %v", err) + return "", "", fmt.Errorf("failed to get cwd for last command: %v", err) } homedir, err := os.UserHomeDir() if err != nil { - return "", fmt.Errorf("failed to get user's home directory: %v", err) + return "", "", fmt.Errorf("failed to get user's home directory: %v", err) } if cwd == homedir { - return "~/", nil + return "~/", homedir, nil } if strings.HasPrefix(cwd, homedir) { - return strings.Replace(cwd, homedir, "~", 1), nil + return strings.Replace(cwd, homedir, "~", 1), homedir, nil } - return cwd, nil + return cwd, homedir, nil } func BuildHistoryEntry(args []string) (*data.HistoryEntry, error) { @@ -91,12 +91,13 @@ func BuildHistoryEntry(args []string) (*data.HistoryEntry, error) { } entry.LocalUsername = user.Username - // cwd - cwd, err := getCwd() + // cwd and homedir + cwd, homedir, err := getCwd() if err != nil { return nil, fmt.Errorf("failed to build history entry: %v", err) } entry.CurrentWorkingDirectory = cwd + entry.HomeDirectory = homedir // start time seconds, err := parseCrossPlatformInt(args[5]) @@ -122,7 +123,11 @@ func BuildHistoryEntry(args []string) (*data.HistoryEntry, error) { // Don't save commands that start with a space return nil, nil } - entry.Command = maybeSkipBashHistTimePrefix(cmd) + cmd, err = maybeSkipBashHistTimePrefix(cmd) + if err != nil { + return nil, err + } + entry.Command = cmd } else if shell == "zsh" { cmd := strings.TrimSuffix(strings.TrimSuffix(args[4], "\n"), " ") if strings.HasPrefix(cmd, " ") { @@ -214,16 +219,16 @@ func buildRegexFromTimeFormat(timeFormat string) string { return expectedRegex } -func maybeSkipBashHistTimePrefix(cmdLine string) string { +func maybeSkipBashHistTimePrefix(cmdLine string) (string, error) { format := os.Getenv("HISTTIMEFORMAT") if format == "" { - return cmdLine + return cmdLine, nil } re, err := regexp.Compile("^" + buildRegexFromTimeFormat(format)) if err != nil { - panic("TODO: bubble up this error") + return "", fmt.Errorf("failed to parse regex for HISTTIMEFORMAT variable: %v", err) } - return re.ReplaceAllLiteralString(cmdLine, "") + return re.ReplaceAllLiteralString(cmdLine, ""), nil } func parseCrossPlatformInt(data string) (int64, error) { @@ -314,6 +319,7 @@ func AddToDbIfNew(db *gorm.DB, entry data.HistoryEntry) { tx = tx.Where("hostname = ?", entry.Hostname) tx = tx.Where("command = ?", entry.Command) tx = tx.Where("current_working_directory = ?", entry.CurrentWorkingDirectory) + tx = tx.Where("home_directory = ?", entry.HomeDirectory) tx = tx.Where("exit_code = ?", entry.ExitCode) tx = tx.Where("start_time = ?", entry.StartTime) tx = tx.Where("end_time = ?", entry.EndTime) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 79e2638..c8aed71 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -52,6 +52,9 @@ func TestBuildHistoryEntry(t *testing.T) { if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") { t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory) } + if !strings.HasPrefix(entry.HomeDirectory, "/") { + t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory) + } if entry.Command != "ls /foo" { t.Fatalf("history entry has unexpected command: %v", entry.Command) } @@ -74,6 +77,9 @@ func TestBuildHistoryEntry(t *testing.T) { if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") { t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory) } + if !strings.HasPrefix(entry.HomeDirectory, "/") { + t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory) + } if entry.Command != "ls /foo" { t.Fatalf("history entry has unexpected command: %v", entry.Command) } @@ -243,7 +249,11 @@ func TestMaybeSkipBashHistTimePrefix(t *testing.T) { for _, tc := range testcases { os.Setenv("HISTTIMEFORMAT", tc.env) - if stripped := maybeSkipBashHistTimePrefix(tc.cmdLine); stripped != tc.expected { + stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine) + if err != nil { + t.Fatal(err) + } + if stripped != tc.expected { t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine) } }