Quote initial commands to make it possible to easily use hishtory to find matching entries for already typed commands that contain flags

This commit is contained in:
David Dworken
2024-09-15 20:43:34 -07:00
parent 3987b355ea
commit df2be5cfe2
4 changed files with 64 additions and 15 deletions

View File

@ -55,7 +55,7 @@ var tqueryCmd = &cobra.Command{
if os.Getenv("HISHTORY_SHELL_NAME") != "" { if os.Getenv("HISHTORY_SHELL_NAME") != "" {
shellName = os.Getenv("HISHTORY_SHELL_NAME") shellName = os.Getenv("HISHTORY_SHELL_NAME")
} }
lib.CheckFatalError(tui.TuiQuery(ctx, shellName, strings.Join(args, " "))) lib.CheckFatalError(tui.TuiQuery(ctx, shellName, args))
}, },
} }

View File

@ -965,7 +965,9 @@ func splitEscaped(query string, separator rune, maxSplit int) []string {
isInSingleQuotedString := false isInSingleQuotedString := false
for i := 0; i < len(runeQuery); i++ { for i := 0; i < len(runeQuery); i++ {
if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator && !isInSingleQuotedString && !isInDoubleQuotedString { if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator && !isInSingleQuotedString && !isInDoubleQuotedString {
tokens = append(tokens, string(token)) if 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) {
@ -982,13 +984,20 @@ func splitEscaped(query string, separator rune, maxSplit int) []string {
} else if runeQuery[i] == '\'' && !isInDoubleQuotedString && !heuristicIgnoreUnclosedQuote(isInSingleQuotedString, '\'', runeQuery, i) { } else if runeQuery[i] == '\'' && !isInDoubleQuotedString && !heuristicIgnoreUnclosedQuote(isInSingleQuotedString, '\'', runeQuery, i) {
isInSingleQuotedString = !isInSingleQuotedString isInSingleQuotedString = !isInSingleQuotedString
} else { } else {
if (isInSingleQuotedString || isInDoubleQuotedString) && separator == ' ' && runeQuery[i] == ':' { if (isInSingleQuotedString || isInDoubleQuotedString) && separator == ' ' {
token = append(token, '\\') if runeQuery[i] == ':' {
token = append(token, '\\')
}
if runeQuery[i] == '-' && len(token) == 0 {
token = append(token, '\\')
}
} }
token = append(token, runeQuery[i]) token = append(token, runeQuery[i])
} }
} }
tokens = append(tokens, string(token)) if string(token) != "" {
tokens = append(tokens, string(token))
}
return tokens return tokens
} }

View File

@ -306,8 +306,8 @@ func TestSplitEscaped(t *testing.T) {
{"'foo\"bar", ' ', -1, []string{"'foo\"bar"}}, {"'foo\"bar", ' ', -1, []string{"'foo\"bar"}},
{"\"foo'\\\"bar\"", ' ', -1, []string{"foo'\"bar"}}, {"\"foo'\\\"bar\"", ' ', -1, []string{"foo'\"bar"}},
{"'foo\"\\'bar'", ' ', -1, []string{"foo\"'bar"}}, {"'foo\"\\'bar'", ' ', -1, []string{"foo\"'bar"}},
{"''", ' ', -1, []string{""}}, {"''", ' ', -1, nil},
{"\"\"", ' ', -1, []string{""}}, {"\"\"", ' ', -1, nil},
{"\\\"", ' ', -1, []string{"\""}}, {"\\\"", ' ', -1, []string{"\""}},
{"\\'", ' ', -1, []string{"'"}}, {"\\'", ' ', -1, []string{"'"}},
// Tests the behavior of quotes with // Tests the behavior of quotes with
@ -324,6 +324,11 @@ func TestSplitEscaped(t *testing.T) {
{"foo:bar", ' ', -1, []string{"foo:bar"}}, {"foo:bar", ' ', -1, []string{"foo:bar"}},
{"'foo:bar'", ' ', -1, []string{"foo\\:bar"}}, {"'foo:bar'", ' ', -1, []string{"foo\\:bar"}},
{"\"foo:bar\"", ' ', -1, []string{"foo\\:bar"}}, {"\"foo:bar\"", ' ', -1, []string{"foo\\:bar"}},
// Tests for quoting dashes
{"'-foo'", ' ', -1, []string{"\\-foo"}},
{"'--foo'", ' ', -1, []string{"\\--foo"}},
{"bar '--foo'", ' ', -1, []string{"bar", "\\--foo"}},
{"bar 'foo-baz'", ' ', -1, []string{"bar", "foo-baz"}},
} }
for _, tc := range testcases { for _, tc := range testcases {
actual := splitEscaped(tc.input, tc.char, tc.limit) actual := splitEscaped(tc.input, tc.char, tc.limit)

View File

@ -3,6 +3,7 @@ package tui
import ( import (
"context" "context"
_ "embed" // for embedding config.sh _ "embed" // for embedding config.sh
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -865,21 +866,55 @@ func configureColorProfile(ctx context.Context) {
} }
} }
func TuiQuery(ctx context.Context, shellName, initialQuery string) error { func buildInitialQueryWithSearchEscaping(initialQueryArray []string) (string, error) {
var initialQuery string
for i, queryChunk := range initialQueryArray {
if i != 0 {
initialQuery += " "
}
if strings.HasPrefix(queryChunk, "-") {
quoted, err := json.Marshal(queryChunk)
if err != nil {
return "", fmt.Errorf("failed to marshal query chunk for escaping: %w", err)
}
initialQuery += string(quoted)
} else {
initialQuery += queryChunk
}
}
return initialQuery, nil
}
func splitQueryArray(initialQueryArray []string) []string {
var splitQueryArray []string
for _, queryChunk := range initialQueryArray {
splitQueryArray = append(splitQueryArray, strings.Split(queryChunk, " ")...)
}
return splitQueryArray
}
func TuiQuery(ctx context.Context, shellName string, initialQueryArray []string) error {
initialQueryArray = splitQueryArray(initialQueryArray)
initialQueryWithEscaping, err := buildInitialQueryWithSearchEscaping(initialQueryArray)
if err != nil {
return err
}
loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap() loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap()
configureColorProfile(ctx) configureColorProfile(ctx)
p := tea.NewProgram(initialModel(ctx, shellName, initialQuery), tea.WithOutput(os.Stderr)) p := tea.NewProgram(initialModel(ctx, shellName, initialQueryWithEscaping), tea.WithOutput(os.Stderr))
// Async: Get the initial set of rows // Async: Get the initial set of rows
go func() { go func() {
LAST_DISPATCHED_QUERY_ID++ LAST_DISPATCHED_QUERY_ID++
queryId := LAST_DISPATCHED_QUERY_ID queryId := LAST_DISPATCHED_QUERY_ID
LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now() LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now()
conf := hctx.GetConf(ctx) conf := hctx.GetConf(ctx)
rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQuery, PADDED_NUM_ENTRIES) rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQueryWithEscaping, PADDED_NUM_ENTRIES)
if err == nil || initialQuery == "" { if err == nil || initialQueryWithEscaping == "" {
p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil}) p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil})
} else { } else {
// initialQuery is likely invalid in some way, let's just drop it // The initial query is likely invalid in some way, let's just drop it
emptyQuery := "" emptyQuery := ""
rows, entries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, shellName, conf.DefaultFilter, emptyQuery, PADDED_NUM_ENTRIES) rows, entries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, shellName, conf.DefaultFilter, emptyQuery, PADDED_NUM_ENTRIES)
p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: &emptyQuery}) p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: &emptyQuery})
@ -913,13 +948,13 @@ func TuiQuery(ctx context.Context, shellName, initialQuery string) error {
p.Send(bannerMsg{banner: string(banner)}) p.Send(bannerMsg{banner: string(banner)})
}() }()
// Blocking: Start the TUI // Blocking: Start the TUI
_, err := p.Run() _, err = p.Run()
if err != nil { if err != nil {
return err return err
} }
if SELECTED_COMMAND == "" && os.Getenv("HISHTORY_TERM_INTEGRATION") != "" { if SELECTED_COMMAND == "" && os.Getenv("HISHTORY_TERM_INTEGRATION") != "" {
// Print out the initialQuery instead so that we don't clear the terminal // Print out the initialQuery instead so that we don't clear the terminal (note that we don't use the escaped one here)
SELECTED_COMMAND = initialQuery SELECTED_COMMAND = strings.Join(initialQueryArray, " ")
} }
fmt.Printf("%s\n", SELECTED_COMMAND) fmt.Printf("%s\n", SELECTED_COMMAND)
return nil return nil