Add support for quoted searchs for exact matches, for #135 (#145)

* Add support for quoted searchs for exact matches, for #135

* Add support for quoting search queries

* Fix spliteEscaped so that it works with escaping dashes and colons in search queries
This commit is contained in:
David Dworken 2023-12-12 22:20:49 -08:00 committed by GitHub
parent 1be8e2cb47
commit 1b3fa944bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 149 additions and 11 deletions

View File

@ -38,7 +38,7 @@ Both support the same query format, see the below annotated queries:
|---|---| |---|---|
| `psql` | Find all commands containing `psql` | | `psql` | Find all commands containing `psql` |
| `psql db.example.com` | Find all commands containing `psql` and `db.example.com` | | `psql db.example.com` | Find all commands containing `psql` and `db.example.com` |
| `docker hostname:my-server` | Find all commands containing `docker` that were run on the computer with hostname `my-server` | | `"docker run" hostname:my-server` | Find all commands containing `docker run` that were run on the computer with hostname `my-server` |
| `nano user:root` | Find all commands containing `nano` that were run as `root` | | `nano user:root` | Find all commands containing `nano` that were run as `root` |
| `exit_code:127` | Find all commands that exited with code `127` | | `exit_code:127` | Find all commands that exited with code `127` |
| `service before:2022-02-01` | Find all commands containing `service` run before February 1st 2022 | | `service before:2022-02-01` | Find all commands containing `service` run before February 1st 2022 |
@ -74,7 +74,6 @@ If you would like to:
</details> </details>
<details> <details>
<summary>TUI key bindings</summary> <summary>TUI key bindings</summary>
The TUI (opened via `Control+R`) supports a number of key bindings: The TUI (opened via `Control+R`) supports a number of key bindings:

View File

@ -1864,6 +1864,35 @@ func testTui_search(t *testing.T, onlineStatus OnlineStatus) {
}) })
out = stripTuiCommandPrefix(t, out) out = stripTuiCommandPrefix(t, out)
testutils.CompareGoldens(t, out, "TestTui-InvalidSearchBecomesValid") testutils.CompareGoldens(t, out, "TestTui-InvalidSearchBecomesValid")
// Record a couple commands that we can use to test for supporting quoted searches
db := hctx.GetDb(hctx.MakeContext())
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("for i in 1")).Error)
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("for i in 2")).Error)
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("i for in")).Error)
out = tester.RunInteractiveShell(t, `hishtory export`)
testutils.CompareGoldens(t, out, "TestTui-ExportWithAdditionalEntries")
// Check the behavior when it is unquoted and fuzzy
out = stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"for SPACE i SPACE in",
}))
testutils.CompareGoldens(t, out, "TestTui-SearchUnquoted")
// Check the behavior when it is quoted and exact
out = stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"'\"'for SPACE i SPACE in'\"'",
}))
testutils.CompareGoldens(t, out, "TestTui-SearchQuoted")
// Check the behavior when it is backslashed
out = stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"for\\\\ SPACE i\\\\ SPACE in",
}))
testutils.CompareGoldens(t, out, "TestTui-SearchBackslash")
} }
func testTui_general(t *testing.T, onlineStatus OnlineStatus) { func testTui_general(t *testing.T, onlineStatus OnlineStatus) {

View File

@ -0,0 +1,5 @@
ls ~/
echo 'aaaaaa bbbb'
for i in 1
for i in 2
i for in

View File

@ -0,0 +1,27 @@
Search Query: > for\ i\ in
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 2 for i in 2 │
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 2 for i in 1 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help

View File

@ -0,0 +1,27 @@
Search Query: > "for i in"
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 2 for i in 2 │
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 2 for i in 1 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help

View File

@ -0,0 +1,27 @@
Search Query: > for i in
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:36 PDT 3s 2 i for in │
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 2 for i in 2 │
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 2 for i in 1 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help

View File

@ -978,19 +978,29 @@ func tokenize(query string) []string {
return splitEscaped(query, ' ', -1) return splitEscaped(query, ' ', -1)
} }
// TODO: Maybe add support for searching for the backslash character itself?
func splitEscaped(query string, separator rune, maxSplit int) []string { func splitEscaped(query string, separator rune, maxSplit int) []string {
var token []rune var token []rune
var tokens []string var tokens []string
splits := 1 splits := 1
runeQuery := []rune(query) runeQuery := []rune(query)
isInQuotedString := false
for i := 0; i < len(runeQuery); i++ { for i := 0; i < len(runeQuery); i++ {
if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator { if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator && !isInQuotedString {
tokens = append(tokens, string(token)) tokens = append(tokens, string(token))
token = token[:0] token = token[:0]
splits++ splits++
} else if runeQuery[i] == '\\' && i+1 < len(runeQuery) { } else if runeQuery[i] == '\\' && i+1 < len(runeQuery) {
token = append(token, runeQuery[i], runeQuery[i+1]) if runeQuery[i+1] == '-' || runeQuery[i+1] == ':' || runeQuery[i+1] == '\\' {
// Note that we need to keep the backslash before the dash to support searches like `ls \-Slah`.
// And we need it before the colon so that we can search for things like `foo\:bar`
// And we need it before the backslash so that we can search for literal backslashes.
token = append(token, runeQuery[i])
}
i++ i++
token = append(token, runeQuery[i])
} else if runeQuery[i] == '"' {
isInQuotedString = !isInQuotedString
} else { } else {
token = append(token, runeQuery[i]) token = append(token, runeQuery[i])
} }

View File

@ -257,7 +257,7 @@ func TestContainsUnescaped(t *testing.T) {
for _, tc := range testcases { for _, tc := range testcases {
actual := containsUnescaped(tc.input, tc.token) actual := containsUnescaped(tc.input, tc.token)
if !reflect.DeepEqual(actual, tc.expected) { if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("containsUnescaped failure for containsUnescaped(%#v, %#v), actual=%#v", tc.input, tc.token, actual) t.Fatalf("failure for containsUnescaped(%#v, %#v), actual=%#v", tc.input, tc.token, actual)
} }
} }
} }
@ -274,16 +274,30 @@ func TestSplitEscaped(t *testing.T) {
{"foo bar baz", ' ', 3, []string{"foo", "bar", "baz"}}, {"foo bar baz", ' ', 3, []string{"foo", "bar", "baz"}},
{"foo bar baz", ' ', 1, []string{"foo bar baz"}}, {"foo bar baz", ' ', 1, []string{"foo bar baz"}},
{"foo bar baz", ' ', -1, []string{"foo", "bar", "baz"}}, {"foo bar baz", ' ', -1, []string{"foo", "bar", "baz"}},
{"foo\\ bar baz", ' ', -1, []string{"foo\\ bar", "baz"}}, {"foo\\ bar baz", ' ', -1, []string{"foo bar", "baz"}},
{"foo\\bar baz", ' ', -1, []string{"foo\\bar", "baz"}}, {"foo\\bar baz", ' ', -1, []string{"foobar", "baz"}},
{"foo\\bar baz foob", ' ', 2, []string{"foo\\bar", "baz foob"}}, {"foo\\bar baz foob", ' ', 2, []string{"foobar", "baz foob"}},
{"foo\\ bar\\ baz", ' ', -1, []string{"foo\\ bar\\ baz"}}, {"foo\\ bar\\ baz", ' ', -1, []string{"foo bar baz"}},
{"foo\\ bar\\ baz", ' ', -1, []string{"foo\\ bar\\ ", "baz"}}, {"foo\\ bar\\ baz", ' ', -1, []string{"foo bar ", "baz"}},
{"\"foo bar\"", ' ', -1, []string{"foo bar"}},
{"\"foo bar\" \" \"", ' ', -1, []string{"foo bar", " "}},
{"\"foo bar baz\" and", ' ', -1, []string{"foo bar baz", "and"}},
{"\"foo bar baz\" and", ' ', -1, []string{"foo bar baz", "and"}},
{"\"foo bar baz", ' ', -1, []string{"foo bar baz"}},
{"\"foo bar baz\\\"\"", ' ', -1, []string{"foo bar baz\""}},
{"cwd:\"foo bar :baz\\\"\"", ':', -1, []string{"cwd", "foo bar :baz\""}},
{"cwd:\"foo bar :baz\\\"\"", ' ', -1, []string{"cwd:foo bar :baz\""}},
{"ls \\-foo", ' ', -1, []string{"ls", "\\-foo"}},
{"ls \\-foo \\a \\\\", ' ', -1, []string{"ls", "\\-foo", "a", "\\\\"}},
{"foo:bar", ':', -1, []string{"foo", "bar"}},
{"foo:bar", ' ', -1, []string{"foo:bar"}},
{"foo\\:bar", ':', -1, []string{"foo\\:bar"}},
{"foo\\:bar", ' ', -1, []string{"foo\\:bar"}},
} }
for _, tc := range testcases { for _, tc := range testcases {
actual := splitEscaped(tc.input, tc.char, tc.limit) actual := splitEscaped(tc.input, tc.char, tc.limit)
if !reflect.DeepEqual(actual, tc.expected) { if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("containsUnescaped failure for splitEscaped(%#v, %#v, %#v), actual=%#v", tc.input, string(tc.char), tc.limit, actual) t.Fatalf("failure for splitEscaped(%#v, %#v, %#v), actual=%#v", tc.input, string(tc.char), tc.limit, actual)
} }
} }
} }