mirror of
				https://github.com/quitesimpleorg/hs9001.git
				synced 2025-10-31 19:49:30 +01:00 
			
		
		
		
	Implement bash CTRL-R reverse history
Fork customized liner into own repo bash. Rename -being and -end options to -after and -before.
This commit is contained in:
		
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,7 +3,7 @@ module hs9001 | ||||
| go 1.16 | ||||
|  | ||||
| require ( | ||||
| 	github.com/peterh/liner v1.2.1 | ||||
| 	github.com/mattn/go-runewidth v0.0.13 | ||||
| 	github.com/tj/go-naturaldate v1.3.0 | ||||
| 	modernc.org/sqlite v1.10.0 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @@ -8,16 +8,16 @@ 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/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | ||||
| 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-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= | ||||
| github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||
| 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/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/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/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
|   | ||||
							
								
								
									
										63
									
								
								history.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								history.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										20
									
								
								liner/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								liner/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| Copyright © 2012 Peter Harris | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a | ||||
| copy of this software and associated documentation files (the "Software"), | ||||
| to deal in the Software without restriction, including without limitation | ||||
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||||
| and/or sell copies of the Software, and to permit persons to whom the | ||||
| Software is furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice (including the next | ||||
| paragraph) shall be included in all copies or substantial portions of the | ||||
| Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL | ||||
| THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||||
| FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||||
| DEALINGS IN THE SOFTWARE. | ||||
							
								
								
									
										196
									
								
								liner/common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								liner/common.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | ||||
| /* | ||||
| Package liner implements a simple command line editor, inspired by linenoise | ||||
| (https://github.com/antirez/linenoise/). This package supports WIN32 in | ||||
| addition to the xterm codes supported by everything else. | ||||
| */ | ||||
| package liner | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"container/ring" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| type commonState struct { | ||||
| 	terminalSupported bool | ||||
| 	outputRedirected  bool | ||||
| 	inputRedirected   bool | ||||
| 	historyProvider   HistoryProvider | ||||
| 	completer         WordCompleter | ||||
| 	columns           int | ||||
| 	killRing          *ring.Ring | ||||
| 	ctrlCAborts       bool | ||||
| 	r                 *bufio.Reader | ||||
| 	tabStyle          TabStyle | ||||
| 	multiLineMode     bool | ||||
| 	cursorRows        int | ||||
| 	maxRows           int | ||||
| 	shouldRestart     ShouldRestart | ||||
| 	noBeep            bool | ||||
| 	needRefresh       bool | ||||
| } | ||||
|  | ||||
| type HistoryProvider interface { | ||||
| 	ReadHistory(r io.Reader) (num int, err error) | ||||
| 	WriteHistory(w io.Writer) (num int, err error) | ||||
| 	AppendHistory(item string) | ||||
| 	ClearHistory() | ||||
| 	GetHistoryByPrefix(prefix string) (ph []string) | ||||
| 	GetHistoryByPattern(pattern string) (ph []string, pos []int) | ||||
| 	RLock() | ||||
| 	RUnlock() | ||||
| } | ||||
|  | ||||
| // TabStyle is used to select how tab completions are displayed. | ||||
| type TabStyle int | ||||
|  | ||||
| // Two tab styles are currently available: | ||||
| // | ||||
| // TabCircular cycles through each completion item and displays it directly on | ||||
| // the prompt | ||||
| // | ||||
| // TabPrints prints the list of completion items to the screen after a second | ||||
| // tab key is pressed. This behaves similar to GNU readline and BASH (which | ||||
| // uses readline) | ||||
| const ( | ||||
| 	TabCircular TabStyle = iota | ||||
| 	TabPrints | ||||
| ) | ||||
|  | ||||
| // ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C | ||||
| // if SetCtrlCAborts(true) has been called on the State | ||||
| var ErrPromptAborted = errors.New("prompt aborted") | ||||
|  | ||||
| // ErrNotTerminalOutput is returned from Prompt or PasswordPrompt if the | ||||
| // platform is normally supported, but stdout has been redirected | ||||
| var ErrNotTerminalOutput = errors.New("standard output is not a terminal") | ||||
|  | ||||
| // ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the | ||||
| // prompt contains any unprintable runes (including substrings that could | ||||
| // be colour codes on some platforms). | ||||
| var ErrInvalidPrompt = errors.New("invalid prompt") | ||||
|  | ||||
| // ErrInternal is returned when liner experiences an error that it cannot | ||||
| // handle. For example, if the number of colums becomes zero during an | ||||
| // active call to Prompt | ||||
| var ErrInternal = errors.New("liner: internal error") | ||||
|  | ||||
| // KillRingMax is the max number of elements to save on the killring. | ||||
| const KillRingMax = 60 | ||||
|  | ||||
| // HistoryLimit is the maximum number of entries saved in the scrollback history. | ||||
| const HistoryLimit = 1000 | ||||
|  | ||||
| func (s *State) ReadHistory(r io.Reader) (num int, err error) { | ||||
| 	return s.historyProvider.ReadHistory(r) | ||||
| } | ||||
| func (s *State) WriteHistory(w io.Writer) (num int, err error) { | ||||
| 	return s.historyProvider.WriteHistory(w) | ||||
| } | ||||
| func (s *State) AppendHistory(item string) { | ||||
| 	s.historyProvider.AppendHistory(item) | ||||
| } | ||||
| func (s *State) ClearHistory() { | ||||
| 	s.historyProvider.ClearHistory() | ||||
| } | ||||
| func (s *State) getHistoryByPrefix(prefix string) (ph []string) { | ||||
| 	return s.historyProvider.GetHistoryByPrefix(prefix) | ||||
| } | ||||
| func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) { | ||||
| 	return s.historyProvider.GetHistoryByPattern(pattern) | ||||
| } | ||||
|  | ||||
| // SetHistoryProvider allows you to set a custom provider | ||||
| // for reading, writing and searching history. | ||||
| // By implementing the HistoryProvider interface | ||||
| // and injecting your custom implementation here. | ||||
| func (s *State) SetHistoryProvider(f HistoryProvider) { | ||||
| 	s.historyProvider = f | ||||
| } | ||||
|  | ||||
| // Completer takes the currently edited line content at the left of the cursor | ||||
| // and returns a list of completion candidates. | ||||
| // If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed | ||||
| // to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!". | ||||
| type Completer func(line string) []string | ||||
|  | ||||
| // WordCompleter takes the currently edited line with the cursor position and | ||||
| // returns the completion candidates for the partial word to be completed. | ||||
| // If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed | ||||
| // to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!". | ||||
| type WordCompleter func(line string, pos int) (head string, completions []string, tail string) | ||||
|  | ||||
| // SetCompleter sets the completion function that Liner will call to | ||||
| // fetch completion candidates when the user presses tab. | ||||
| func (s *State) SetCompleter(f Completer) { | ||||
| 	if f == nil { | ||||
| 		s.completer = nil | ||||
| 		return | ||||
| 	} | ||||
| 	s.completer = func(line string, pos int) (string, []string, string) { | ||||
| 		return "", f(string([]rune(line)[:pos])), string([]rune(line)[pos:]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SetWordCompleter sets the completion function that Liner will call to | ||||
| // fetch completion candidates when the user presses tab. | ||||
| func (s *State) SetWordCompleter(f WordCompleter) { | ||||
| 	s.completer = f | ||||
| } | ||||
|  | ||||
| // SetTabCompletionStyle sets the behvavior when the Tab key is pressed | ||||
| // for auto-completion.  TabCircular is the default behavior and cycles | ||||
| // through the list of candidates at the prompt.  TabPrints will print | ||||
| // the available completion candidates to the screen similar to BASH | ||||
| // and GNU Readline | ||||
| func (s *State) SetTabCompletionStyle(tabStyle TabStyle) { | ||||
| 	s.tabStyle = tabStyle | ||||
| } | ||||
|  | ||||
| // ModeApplier is the interface that wraps a representation of the terminal | ||||
| // mode. ApplyMode sets the terminal to this mode. | ||||
| type ModeApplier interface { | ||||
| 	ApplyMode() error | ||||
| } | ||||
|  | ||||
| // SetCtrlCAborts sets whether Prompt on a supported terminal will return an | ||||
| // ErrPromptAborted when Ctrl-C is pressed. The default is false (will not | ||||
| // return when Ctrl-C is pressed). Unsupported terminals typically raise SIGINT | ||||
| // (and Prompt does not return) regardless of the value passed to SetCtrlCAborts. | ||||
| func (s *State) SetCtrlCAborts(aborts bool) { | ||||
| 	s.ctrlCAborts = aborts | ||||
| } | ||||
|  | ||||
| // SetMultiLineMode sets whether line is auto-wrapped. The default is false (single line). | ||||
| func (s *State) SetMultiLineMode(mlmode bool) { | ||||
| 	s.multiLineMode = mlmode | ||||
| } | ||||
|  | ||||
| // ShouldRestart is passed the error generated by readNext and returns true if | ||||
| // the the read should be restarted or false if the error should be returned. | ||||
| type ShouldRestart func(err error) bool | ||||
|  | ||||
| // SetShouldRestart sets the restart function that Liner will call to determine | ||||
| // whether to retry the call to, or return the error returned by, readNext. | ||||
| func (s *State) SetShouldRestart(f ShouldRestart) { | ||||
| 	s.shouldRestart = f | ||||
| } | ||||
|  | ||||
| // SetBeep sets whether liner should beep the terminal at various times (output | ||||
| // ASCII BEL, 0x07). Default is true (will beep). | ||||
| func (s *State) SetBeep(beep bool) { | ||||
| 	s.noBeep = !beep | ||||
| } | ||||
|  | ||||
| func (s *State) promptUnsupported(p string) (string, error) { | ||||
| 	if !s.inputRedirected || !s.terminalSupported { | ||||
| 		fmt.Print(p) | ||||
| 	} | ||||
| 	linebuf, _, err := s.r.ReadLine() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return string(linebuf), nil | ||||
| } | ||||
							
								
								
									
										391
									
								
								liner/input.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								liner/input.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,391 @@ | ||||
| package liner | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type nexter struct { | ||||
| 	r   rune | ||||
| 	err error | ||||
| } | ||||
|  | ||||
| // State represents an open terminal | ||||
| type State struct { | ||||
| 	commonState | ||||
| 	origMode    termios | ||||
| 	defaultMode termios | ||||
| 	next        <-chan nexter | ||||
| 	winch       chan os.Signal | ||||
| 	pending     []rune | ||||
| 	useCHA      bool | ||||
| } | ||||
|  | ||||
| // NewLiner initializes a new *State, and sets the terminal into raw mode. To | ||||
| // restore the terminal to its previous state, call State.Close(). | ||||
| func NewLiner() *State { | ||||
| 	var s State | ||||
| 	s.r = bufio.NewReader(os.Stdin) | ||||
|  | ||||
| 	s.terminalSupported = TerminalSupported() | ||||
| 	if m, err := TerminalMode(); err == nil { | ||||
| 		s.origMode = *m.(*termios) | ||||
| 	} else { | ||||
| 		s.inputRedirected = true | ||||
| 	} | ||||
| 	if _, err := getMode(syscall.Stdout); err != 0 { | ||||
| 		s.outputRedirected = true | ||||
| 	} | ||||
| 	if s.inputRedirected && s.outputRedirected { | ||||
| 		s.terminalSupported = false | ||||
| 	} | ||||
| 	if s.terminalSupported && !s.inputRedirected && !s.outputRedirected { | ||||
| 		mode := s.origMode | ||||
| 		mode.Iflag &^= icrnl | inpck | istrip | ixon | ||||
| 		mode.Cflag |= cs8 | ||||
| 		mode.Lflag &^= syscall.ECHO | icanon | iexten | ||||
| 		mode.ApplyMode() | ||||
|  | ||||
| 		winch := make(chan os.Signal, 1) | ||||
| 		signal.Notify(winch, syscall.SIGWINCH) | ||||
| 		s.winch = winch | ||||
|  | ||||
| 		s.checkOutput() | ||||
| 	} | ||||
|  | ||||
| 	if !s.outputRedirected { | ||||
| 		s.outputRedirected = !s.getColumns() | ||||
| 	} | ||||
|  | ||||
| 	return &s | ||||
| } | ||||
|  | ||||
| var errTimedOut = errors.New("timeout") | ||||
|  | ||||
| func (s *State) startPrompt() { | ||||
| 	if s.terminalSupported { | ||||
| 		if m, err := TerminalMode(); err == nil { | ||||
| 			s.defaultMode = *m.(*termios) | ||||
| 			mode := s.defaultMode | ||||
| 			mode.Lflag &^= isig | ||||
| 			mode.ApplyMode() | ||||
| 		} | ||||
| 	} | ||||
| 	s.restartPrompt() | ||||
| } | ||||
|  | ||||
| func (s *State) inputWaiting() bool { | ||||
| 	return len(s.next) > 0 | ||||
| } | ||||
|  | ||||
| func (s *State) restartPrompt() { | ||||
| 	next := make(chan nexter, 200) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			var n nexter | ||||
| 			n.r, _, n.err = s.r.ReadRune() | ||||
| 			next <- n | ||||
| 			// Shut down nexter loop when an end condition has been reached | ||||
| 			if n.err != nil || n.r == '\n' || n.r == '\r' || n.r == ctrlC || n.r == ctrlD { | ||||
| 				close(next) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	s.next = next | ||||
| } | ||||
|  | ||||
| func (s *State) stopPrompt() { | ||||
| 	if s.terminalSupported { | ||||
| 		s.defaultMode.ApplyMode() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *State) nextPending(timeout <-chan time.Time) (rune, error) { | ||||
| 	select { | ||||
| 	case thing, ok := <-s.next: | ||||
| 		if !ok { | ||||
| 			return 0, ErrInternal | ||||
| 		} | ||||
| 		if thing.err != nil { | ||||
| 			return 0, thing.err | ||||
| 		} | ||||
| 		s.pending = append(s.pending, thing.r) | ||||
| 		return thing.r, nil | ||||
| 	case <-timeout: | ||||
| 		rv := s.pending[0] | ||||
| 		s.pending = s.pending[1:] | ||||
| 		return rv, errTimedOut | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *State) readNext() (interface{}, error) { | ||||
| 	if len(s.pending) > 0 { | ||||
| 		rv := s.pending[0] | ||||
| 		s.pending = s.pending[1:] | ||||
| 		return rv, nil | ||||
| 	} | ||||
| 	var r rune | ||||
| 	select { | ||||
| 	case thing, ok := <-s.next: | ||||
| 		if !ok { | ||||
| 			return 0, ErrInternal | ||||
| 		} | ||||
| 		if thing.err != nil { | ||||
| 			return nil, thing.err | ||||
| 		} | ||||
| 		r = thing.r | ||||
| 	case <-s.winch: | ||||
| 		s.getColumns() | ||||
| 		return winch, nil | ||||
| 	} | ||||
| 	if r != esc { | ||||
| 		return r, nil | ||||
| 	} | ||||
| 	s.pending = append(s.pending, r) | ||||
|  | ||||
| 	// Wait at most 50 ms for the rest of the escape sequence | ||||
| 	// If nothing else arrives, it was an actual press of the esc key | ||||
| 	timeout := time.After(50 * time.Millisecond) | ||||
| 	flag, err := s.nextPending(timeout) | ||||
| 	if err != nil { | ||||
| 		if err == errTimedOut { | ||||
| 			return flag, nil | ||||
| 		} | ||||
| 		return unknown, err | ||||
| 	} | ||||
|  | ||||
| 	switch flag { | ||||
| 	case '[': | ||||
| 		code, err := s.nextPending(timeout) | ||||
| 		if err != nil { | ||||
| 			if err == errTimedOut { | ||||
| 				return code, nil | ||||
| 			} | ||||
| 			return unknown, err | ||||
| 		} | ||||
| 		switch code { | ||||
| 		case 'A': | ||||
| 			s.pending = s.pending[:0] // escape code complete | ||||
| 			return up, nil | ||||
| 		case 'B': | ||||
| 			s.pending = s.pending[:0] // escape code complete | ||||
| 			return down, nil | ||||
| 		case 'C': | ||||
| 			s.pending = s.pending[:0] // escape code complete | ||||
| 			return right, nil | ||||
| 		case 'D': | ||||
| 			s.pending = s.pending[:0] // escape code complete | ||||
| 			return left, nil | ||||
| 		case 'F': | ||||
| 			s.pending = s.pending[:0] // escape code complete | ||||
| 			return end, nil | ||||
| 		case 'H': | ||||
| 			s.pending = s.pending[:0] // escape code complete | ||||
| 			return home, nil | ||||
| 		case 'Z': | ||||
| 			s.pending = s.pending[:0] // escape code complete | ||||
| 			return shiftTab, nil | ||||
| 		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': | ||||
| 			num := []rune{code} | ||||
| 			for { | ||||
| 				code, err := s.nextPending(timeout) | ||||
| 				if err != nil { | ||||
| 					if err == errTimedOut { | ||||
| 						return code, nil | ||||
| 					} | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				switch code { | ||||
| 				case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': | ||||
| 					num = append(num, code) | ||||
| 				case ';': | ||||
| 					// Modifier code to follow | ||||
| 					// This only supports Ctrl-left and Ctrl-right for now | ||||
| 					x, _ := strconv.ParseInt(string(num), 10, 32) | ||||
| 					if x != 1 { | ||||
| 						// Can't be left or right | ||||
| 						rv := s.pending[0] | ||||
| 						s.pending = s.pending[1:] | ||||
| 						return rv, nil | ||||
| 					} | ||||
| 					num = num[:0] | ||||
| 					for { | ||||
| 						code, err = s.nextPending(timeout) | ||||
| 						if err != nil { | ||||
| 							if err == errTimedOut { | ||||
| 								rv := s.pending[0] | ||||
| 								s.pending = s.pending[1:] | ||||
| 								return rv, nil | ||||
| 							} | ||||
| 							return nil, err | ||||
| 						} | ||||
| 						switch code { | ||||
| 						case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': | ||||
| 							num = append(num, code) | ||||
| 						case 'C', 'D': | ||||
| 							// right, left | ||||
| 							mod, _ := strconv.ParseInt(string(num), 10, 32) | ||||
| 							if mod != 5 { | ||||
| 								// Not bare Ctrl | ||||
| 								rv := s.pending[0] | ||||
| 								s.pending = s.pending[1:] | ||||
| 								return rv, nil | ||||
| 							} | ||||
| 							s.pending = s.pending[:0] // escape code complete | ||||
| 							if code == 'C' { | ||||
| 								return wordRight, nil | ||||
| 							} | ||||
| 							return wordLeft, nil | ||||
| 						default: | ||||
| 							// Not left or right | ||||
| 							rv := s.pending[0] | ||||
| 							s.pending = s.pending[1:] | ||||
| 							return rv, nil | ||||
| 						} | ||||
| 					} | ||||
| 				case '~': | ||||
| 					s.pending = s.pending[:0] // escape code complete | ||||
| 					x, _ := strconv.ParseInt(string(num), 10, 32) | ||||
| 					switch x { | ||||
| 					case 2: | ||||
| 						return insert, nil | ||||
| 					case 3: | ||||
| 						return del, nil | ||||
| 					case 5: | ||||
| 						return pageUp, nil | ||||
| 					case 6: | ||||
| 						return pageDown, nil | ||||
| 					case 1, 7: | ||||
| 						return home, nil | ||||
| 					case 4, 8: | ||||
| 						return end, nil | ||||
| 					case 15: | ||||
| 						return f5, nil | ||||
| 					case 17: | ||||
| 						return f6, nil | ||||
| 					case 18: | ||||
| 						return f7, nil | ||||
| 					case 19: | ||||
| 						return f8, nil | ||||
| 					case 20: | ||||
| 						return f9, nil | ||||
| 					case 21: | ||||
| 						return f10, nil | ||||
| 					case 23: | ||||
| 						return f11, nil | ||||
| 					case 24: | ||||
| 						return f12, nil | ||||
| 					default: | ||||
| 						return unknown, nil | ||||
| 					} | ||||
| 				default: | ||||
| 					// unrecognized escape code | ||||
| 					rv := s.pending[0] | ||||
| 					s.pending = s.pending[1:] | ||||
| 					return rv, nil | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	case 'O': | ||||
| 		code, err := s.nextPending(timeout) | ||||
| 		if err != nil { | ||||
| 			if err == errTimedOut { | ||||
| 				return code, nil | ||||
| 			} | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		s.pending = s.pending[:0] // escape code complete | ||||
| 		switch code { | ||||
| 		case 'c': | ||||
| 			return wordRight, nil | ||||
| 		case 'd': | ||||
| 			return wordLeft, nil | ||||
| 		case 'H': | ||||
| 			return home, nil | ||||
| 		case 'F': | ||||
| 			return end, nil | ||||
| 		case 'P': | ||||
| 			return f1, nil | ||||
| 		case 'Q': | ||||
| 			return f2, nil | ||||
| 		case 'R': | ||||
| 			return f3, nil | ||||
| 		case 'S': | ||||
| 			return f4, nil | ||||
| 		default: | ||||
| 			return unknown, nil | ||||
| 		} | ||||
| 	case 'b': | ||||
| 		s.pending = s.pending[:0] // escape code complete | ||||
| 		return altB, nil | ||||
| 	case 'd': | ||||
| 		s.pending = s.pending[:0] // escape code complete | ||||
| 		return altD, nil | ||||
| 	case bs: | ||||
| 		s.pending = s.pending[:0] // escape code complete | ||||
| 		return altBs, nil | ||||
| 	case 'f': | ||||
| 		s.pending = s.pending[:0] // escape code complete | ||||
| 		return altF, nil | ||||
| 	case 'y': | ||||
| 		s.pending = s.pending[:0] // escape code complete | ||||
| 		return altY, nil | ||||
| 	default: | ||||
| 		rv := s.pending[0] | ||||
| 		s.pending = s.pending[1:] | ||||
| 		return rv, nil | ||||
| 	} | ||||
|  | ||||
| 	// not reached | ||||
| 	return r, nil | ||||
| } | ||||
|  | ||||
| // Close returns the terminal to its previous mode | ||||
| func (s *State) Close() error { | ||||
| 	signal.Stop(s.winch) | ||||
| 	if !s.inputRedirected { | ||||
| 		s.origMode.ApplyMode() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // TerminalSupported returns true if the current terminal supports | ||||
| // line editing features, and false if liner will use the 'dumb' | ||||
| // fallback for input. | ||||
| // Note that TerminalSupported does not check all factors that may | ||||
| // cause liner to not fully support the terminal (such as stdin redirection) | ||||
| func TerminalSupported() bool { | ||||
| 	bad := map[string]bool{"": true, "dumb": true, "cons25": true} | ||||
| 	return !bad[strings.ToLower(os.Getenv("TERM"))] | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	getTermios = syscall.TCGETS | ||||
| 	setTermios = syscall.TCSETS | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	icrnl  = syscall.ICRNL | ||||
| 	inpck  = syscall.INPCK | ||||
| 	istrip = syscall.ISTRIP | ||||
| 	ixon   = syscall.IXON | ||||
| 	opost  = syscall.OPOST | ||||
| 	cs8    = syscall.CS8 | ||||
| 	isig   = syscall.ISIG | ||||
| 	icanon = syscall.ICANON | ||||
| 	iexten = syscall.IEXTEN | ||||
| ) | ||||
|  | ||||
| type termios struct { | ||||
| 	syscall.Termios | ||||
| } | ||||
|  | ||||
| const cursorColumn = false | ||||
							
								
								
									
										1075
									
								
								liner/line.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1075
									
								
								liner/line.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										74
									
								
								liner/output.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								liner/output.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| package liner | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"unsafe" | ||||
| ) | ||||
|  | ||||
| func (s *State) cursorPos(x int) { | ||||
| 	if s.useCHA { | ||||
| 		// 'G' is "Cursor Character Absolute (CHA)" | ||||
| 		fmt.Printf("\x1b[%dG", x+1) | ||||
| 	} else { | ||||
| 		// 'C' is "Cursor Forward (CUF)" | ||||
| 		fmt.Print("\r") | ||||
| 		if x > 0 { | ||||
| 			fmt.Printf("\x1b[%dC", x) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *State) eraseLine() { | ||||
| 	fmt.Print("\x1b[0K") | ||||
| } | ||||
|  | ||||
| func (s *State) eraseScreen() { | ||||
| 	fmt.Print("\x1b[H\x1b[2J") | ||||
| } | ||||
|  | ||||
| func (s *State) moveUp(lines int) { | ||||
| 	fmt.Printf("\x1b[%dA", lines) | ||||
| } | ||||
|  | ||||
| func (s *State) moveDown(lines int) { | ||||
| 	fmt.Printf("\x1b[%dB", lines) | ||||
| } | ||||
|  | ||||
| func (s *State) emitNewLine() { | ||||
| 	fmt.Print("\n") | ||||
| } | ||||
|  | ||||
| type winSize struct { | ||||
| 	row, col       uint16 | ||||
| 	xpixel, ypixel uint16 | ||||
| } | ||||
|  | ||||
| func (s *State) getColumns() bool { | ||||
| 	var ws winSize | ||||
| 	ok, _, _ := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdout), | ||||
| 		syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws))) | ||||
| 	if int(ok) < 0 { | ||||
| 		return false | ||||
| 	} | ||||
| 	s.columns = int(ws.col) | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (s *State) checkOutput() { | ||||
| 	// xterm is known to support CHA | ||||
| 	if strings.Contains(strings.ToLower(os.Getenv("TERM")), "xterm") { | ||||
| 		s.useCHA = true | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// The test for functional ANSI CHA is unreliable (eg the Windows | ||||
| 	// telnet command does not support reading the cursor position with | ||||
| 	// an ANSI DSR request, despite setting TERM=ansi) | ||||
|  | ||||
| 	// Assume CHA isn't supported (which should be safe, although it | ||||
| 	// does result in occasional visible cursor jitter) | ||||
| 	s.useCHA = false | ||||
| } | ||||
							
								
								
									
										35
									
								
								liner/unixmode.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								liner/unixmode.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| package liner | ||||
|  | ||||
| import ( | ||||
| 	"syscall" | ||||
| 	"unsafe" | ||||
| ) | ||||
|  | ||||
| func (mode *termios) ApplyMode() error { | ||||
| 	_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), setTermios, uintptr(unsafe.Pointer(mode))) | ||||
|  | ||||
| 	if errno != 0 { | ||||
| 		return errno | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // TerminalMode returns the current terminal input mode as an InputModeSetter. | ||||
| // | ||||
| // This function is provided for convenience, and should | ||||
| // not be necessary for most users of liner. | ||||
| func TerminalMode() (ModeApplier, error) { | ||||
| 	mode, errno := getMode(syscall.Stdin) | ||||
|  | ||||
| 	if errno != 0 { | ||||
| 		return nil, errno | ||||
| 	} | ||||
| 	return mode, nil | ||||
| } | ||||
|  | ||||
| func getMode(handle int) (*termios, syscall.Errno) { | ||||
| 	var mode termios | ||||
| 	_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(handle), getTermios, uintptr(unsafe.Pointer(&mode))) | ||||
|  | ||||
| 	return &mode, errno | ||||
| } | ||||
							
								
								
									
										90
									
								
								liner/width.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								liner/width.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| package liner | ||||
|  | ||||
| import ( | ||||
| 	"unicode" | ||||
|  | ||||
| 	"github.com/mattn/go-runewidth" | ||||
| ) | ||||
|  | ||||
| // These character classes are mostly zero width (when combined). | ||||
| // A few might not be, depending on the user's font. Fixing this | ||||
| // is non-trivial, given that some terminals don't support | ||||
| // ANSI DSR/CPR | ||||
| var zeroWidth = []*unicode.RangeTable{ | ||||
| 	unicode.Mn, | ||||
| 	unicode.Me, | ||||
| 	unicode.Cc, | ||||
| 	unicode.Cf, | ||||
| } | ||||
|  | ||||
| // countGlyphs considers zero-width characters to be zero glyphs wide, | ||||
| // and members of Chinese, Japanese, and Korean scripts to be 2 glyphs wide. | ||||
| func countGlyphs(s []rune) int { | ||||
| 	n := 0 | ||||
| 	for _, r := range s { | ||||
| 		// speed up the common case | ||||
| 		if r < 127 { | ||||
| 			n++ | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		n += runewidth.RuneWidth(r) | ||||
| 	} | ||||
| 	return n | ||||
| } | ||||
|  | ||||
| func countMultiLineGlyphs(s []rune, columns int, start int) int { | ||||
| 	n := start | ||||
| 	for _, r := range s { | ||||
| 		if r < 127 { | ||||
| 			n++ | ||||
| 			continue | ||||
| 		} | ||||
| 		switch runewidth.RuneWidth(r) { | ||||
| 		case 0: | ||||
| 		case 1: | ||||
| 			n++ | ||||
| 		case 2: | ||||
| 			n += 2 | ||||
| 			// no room for a 2-glyphs-wide char in the ending | ||||
| 			// so skip a column and display it at the beginning | ||||
| 			if n%columns == 1 { | ||||
| 				n++ | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return n | ||||
| } | ||||
|  | ||||
| func getPrefixGlyphs(s []rune, num int) []rune { | ||||
| 	p := 0 | ||||
| 	for n := 0; n < num && p < len(s); p++ { | ||||
| 		// speed up the common case | ||||
| 		if s[p] < 127 { | ||||
| 			n++ | ||||
| 			continue | ||||
| 		} | ||||
| 		if !unicode.IsOneOf(zeroWidth, s[p]) { | ||||
| 			n++ | ||||
| 		} | ||||
| 	} | ||||
| 	for p < len(s) && unicode.IsOneOf(zeroWidth, s[p]) { | ||||
| 		p++ | ||||
| 	} | ||||
| 	return s[:p] | ||||
| } | ||||
|  | ||||
| func getSuffixGlyphs(s []rune, num int) []rune { | ||||
| 	p := len(s) | ||||
| 	for n := 0; n < num && p > 0; p-- { | ||||
| 		// speed up the common case | ||||
| 		if s[p-1] < 127 { | ||||
| 			n++ | ||||
| 			continue | ||||
| 		} | ||||
| 		if !unicode.IsOneOf(zeroWidth, s[p-1]) { | ||||
| 			n++ | ||||
| 		} | ||||
| 	} | ||||
| 	return s[p:] | ||||
| } | ||||
							
								
								
									
										135
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								main.go
									
									
									
									
									
								
							| @@ -10,10 +10,12 @@ import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/peterh/liner" | ||||
| 	"hs9001/liner" | ||||
|  | ||||
| 	"github.com/tj/go-naturaldate" | ||||
| 	_ "modernc.org/sqlite" | ||||
| ) | ||||
| @@ -149,28 +151,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 | ||||
| 	sb.WriteString("SELECT id, command, workdir, user, hostname, retval ") | ||||
| 	sb.WriteString("FROM history ") | ||||
| 	sb.WriteString("WHERE timestamp BETWEEN datetime(?, 'unixepoch') ") | ||||
| 	sb.WriteString("AND datetime(?, 'unixepoch') ") | ||||
| 	sb.WriteString("WHERE 1=1 ") //1=1 so we can append as many AND foo as we want, or none | ||||
|  | ||||
| 	if opts.command != nil { | ||||
| 		sb.WriteString("AND command LIKE ? ") | ||||
| 	sb.WriteString("AND workdir LIKE ? ") | ||||
| 	if retval != -9001 { | ||||
| 		sb.WriteString("AND retval = ? ") | ||||
| 		args = append(args, opts.command) | ||||
| 	} | ||||
| 	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() | ||||
| 	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...) | ||||
| 	if err != nil { | ||||
| 		log.Panic(err) | ||||
| @@ -271,34 +292,22 @@ func main() { | ||||
| 		line := liner.NewLiner() | ||||
| 		defer line.Close() | ||||
| 		line.SetCtrlCAborts(true) | ||||
| 		line.SetCompleter(func(line string) (c []string) { | ||||
| 			beginTimestamp, err := naturaldate.Parse("50 years ago", time.Now()) | ||||
| 			if err != nil { | ||||
| 				fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) | ||||
| 			} | ||||
| 			endTimeStamp, err := naturaldate.Parse("now", time.Now()) | ||||
| 			if err != nil { | ||||
| 				fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) | ||||
| 			} | ||||
| 			results := search(conn, "%"+line+"%", "%", beginTimestamp, endTimeStamp, -9001) | ||||
| 			for e := results.Front(); e != nil; e = e.Next() { | ||||
| 				entry, ok := e.Value.(*HistoryEntry) | ||||
| 				if !ok { | ||||
| 					log.Panic("Failed to retrieve entries") | ||||
| 				} | ||||
| 				c = append(c, entry.cmd) | ||||
| 			} | ||||
| 			return | ||||
| 		}) | ||||
| 		line.SetHistoryProvider(&history{conn: conn}) | ||||
| 		line.SetMultiLineMode(true) | ||||
|  | ||||
| 		if name, err := line.Prompt("What is your command? "); err == nil { | ||||
| 			log.Print("Got: ", name) | ||||
| 		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": | ||||
| @@ -326,13 +335,13 @@ func main() { | ||||
| 		fallthrough | ||||
| 	case "delete": | ||||
| 		var workDir string | ||||
| 		var beginTime string | ||||
| 		var endTime string | ||||
| 		var afterTime string | ||||
| 		var beforeTime string | ||||
| 		var distinct bool = true | ||||
| 		var retVal int | ||||
| 		searchCmd.StringVar(&workDir, "cwd", "%", "Search only within this workdir") | ||||
| 		searchCmd.StringVar(&beginTime, "begin", "50 years ago", "Start searching from this timeframe") | ||||
| 		searchCmd.StringVar(&endTime, "end", "now", "End searching from this timeframe") | ||||
| 		searchCmd.StringVar(&workDir, "cwd", "", "Search only within this workdir") | ||||
| 		searchCmd.StringVar(&afterTime, "after", "", "Start 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.IntVar(&retVal, "ret", -9001, "Only query commands that returned with this exit code. -9001=all (default)") | ||||
|  | ||||
| @@ -340,25 +349,39 @@ func main() { | ||||
|  | ||||
| 		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, " ") | ||||
| 		if workDir != "%" { | ||||
| 			workDir, err = filepath.Abs(workDir) | ||||
|  | ||||
| 		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 | ||||
| 		} | ||||
|  | ||||
| 		results := search(conn, "%"+q+"%", workDir, beginTimestamp, endTimeStamp, retVal) | ||||
| 		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 := "" | ||||
| 		for e := results.Front(); e != nil; e = e.Next() { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 lawl
					lawl