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") != "" {
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
for i := 0; i < len(runeQuery); i++ {
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]
splits++
} 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) {
isInSingleQuotedString = !isInSingleQuotedString
} else {
if (isInSingleQuotedString || isInDoubleQuotedString) && separator == ' ' && runeQuery[i] == ':' {
token = append(token, '\\')
if (isInSingleQuotedString || isInDoubleQuotedString) && separator == ' ' {
if runeQuery[i] == ':' {
token = append(token, '\\')
}
if runeQuery[i] == '-' && len(token) == 0 {
token = append(token, '\\')
}
}
token = append(token, runeQuery[i])
}
}
tokens = append(tokens, string(token))
if string(token) != "" {
tokens = append(tokens, string(token))
}
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"}},
{"''", ' ', -1, []string{""}},
{"\"\"", ' ', -1, []string{""}},
{"''", ' ', -1, nil},
{"\"\"", ' ', -1, nil},
{"\\\"", ' ', -1, []string{"\""}},
{"\\'", ' ', -1, []string{"'"}},
// 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"}},
// 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 {
actual := splitEscaped(tc.input, tc.char, tc.limit)

View File

@ -3,6 +3,7 @@ package tui
import (
"context"
_ "embed" // for embedding config.sh
"encoding/json"
"fmt"
"os"
"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()
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
go func() {
LAST_DISPATCHED_QUERY_ID++
queryId := LAST_DISPATCHED_QUERY_ID
LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now()
conf := hctx.GetConf(ctx)
rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQuery, PADDED_NUM_ENTRIES)
if err == nil || initialQuery == "" {
rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQueryWithEscaping, PADDED_NUM_ENTRIES)
if err == nil || initialQueryWithEscaping == "" {
p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil})
} 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 := ""
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})
@ -913,13 +948,13 @@ func TuiQuery(ctx context.Context, shellName, initialQuery string) error {
p.Send(bannerMsg{banner: string(banner)})
}()
// Blocking: Start the TUI
_, err := p.Run()
_, err = p.Run()
if err != nil {
return err
}
if SELECTED_COMMAND == "" && os.Getenv("HISHTORY_TERM_INTEGRATION") != "" {
// Print out the initialQuery instead so that we don't clear the terminal
SELECTED_COMMAND = initialQuery
// 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 = strings.Join(initialQueryArray, " ")
}
fmt.Printf("%s\n", SELECTED_COMMAND)
return nil