From 79d7f1c45ef8f883ad75e8bd23472d720ca0774f Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 26 Sep 2019 01:35:23 +0200 Subject: [PATCH] restructure - move strategies to pkg, split evaluate and move parts to pkg --- cmd/evaluate/main.go | 425 ++---------------- cmd/evaluate/strategy-dummy.go | 24 - pkg/histanal/histeval.go | 246 ++++++++++ pkg/histanal/histload.go | 179 ++++++++ .../strat/directory-sensitive.go | 15 +- pkg/strat/dummy.go | 24 + .../strat/dynamic-record-distance.go | 38 +- .../strat/frequent.go | 14 +- .../strat/markov-chain-cmd.go | 26 +- .../strat/markov-chain.go | 26 +- .../strategy-random.go => pkg/strat/random.go | 20 +- .../strat/recent-bash.go | 16 +- .../strategy-recent.go => pkg/strat/recent.go | 12 +- .../strat/record-distance.go | 28 +- pkg/strat/strat.go | 44 ++ 15 files changed, 624 insertions(+), 513 deletions(-) delete mode 100644 cmd/evaluate/strategy-dummy.go create mode 100644 pkg/histanal/histeval.go create mode 100644 pkg/histanal/histload.go rename cmd/evaluate/strategy-directory-sensitive.go => pkg/strat/directory-sensitive.go (63%) create mode 100644 pkg/strat/dummy.go rename cmd/evaluate/strategy-dynamic-record-distance.go => pkg/strat/dynamic-record-distance.go (58%) rename cmd/evaluate/strategy-frequent.go => pkg/strat/frequent.go (64%) rename cmd/evaluate/strategy-markov-chain-cmd.go => pkg/strat/markov-chain-cmd.go (75%) rename cmd/evaluate/strategy-markov-chain.go => pkg/strat/markov-chain.go (68%) rename cmd/evaluate/strategy-random.go => pkg/strat/random.go (62%) rename cmd/evaluate/strategy-recent-bash.go => pkg/strat/recent-bash.go (75%) rename cmd/evaluate/strategy-recent.go => pkg/strat/recent.go (59%) rename cmd/evaluate/strategy-record-distance.go => pkg/strat/record-distance.go (53%) create mode 100644 pkg/strat/strat.go diff --git a/cmd/evaluate/main.go b/cmd/evaluate/main.go index 6e013cc..7ae217f 100644 --- a/cmd/evaluate/main.go +++ b/cmd/evaluate/main.go @@ -1,23 +1,16 @@ package main import ( - "bufio" - "bytes" - "encoding/json" "flag" "fmt" - "io/ioutil" "log" - "math/rand" "os" - "os/exec" "os/user" "path/filepath" + "github.com/curusarn/resh/pkg/histanal" "github.com/curusarn/resh/pkg/records" - "github.com/jpillora/longestcommon" - - "github.com/schollz/progressbar" + "github.com/curusarn/resh/pkg/strat" ) // Version from git set during build @@ -81,27 +74,20 @@ func main() { } } - evaluator := evaluator{sanitizedInput: *sanitizedInput, maxCandidates: maxCandidates, - BatchMode: batchMode, skipFailedCmds: *skipFailedCmds, debugRecords: *debugRecords} + var evaluator histanal.HistEval if batchMode { - err := evaluator.initBatchMode(*input, *inputDataRoot) - if err != nil { - log.Fatal("Evaluator initBatchMode() error:", err) - } + evaluator = histanal.NewHistEvalBatchMode(*input, *inputDataRoot, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput) } else { - err := evaluator.init(*input) - if err != nil { - log.Fatal("Evaluator init() error:", err) - } + evaluator = histanal.NewHistEval(*input, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput) } - var simpleStrategies []ISimpleStrategy - var strategies []IStrategy + var simpleStrategies []strat.ISimpleStrategy + var strategies []strat.IStrategy // dummy := strategyDummy{} // simpleStrategies = append(simpleStrategies, &dummy) - simpleStrategies = append(simpleStrategies, &strategyRecent{}) + simpleStrategies = append(simpleStrategies, &strat.Recent{}) // frequent := strategyFrequent{} // frequent.init() @@ -111,401 +97,56 @@ func main() { // random.init() // simpleStrategies = append(simpleStrategies, &random) - directory := strategyDirectorySensitive{} - directory.init() + directory := strat.DirectorySensitive{} + directory.Init() simpleStrategies = append(simpleStrategies, &directory) - dynamicDistG := strategyDynamicRecordDistance{ - maxDepth: 3000, - distParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1, Git: 10}, - label: "10*pwd,10*realpwd,session,time,10*git", + dynamicDistG := strat.DynamicRecordDistance{ + MaxDepth: 3000, + DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1, Git: 10}, + Label: "10*pwd,10*realpwd,session,time,10*git", } - dynamicDistG.init() + dynamicDistG.Init() strategies = append(strategies, &dynamicDistG) - distanceStaticBest := strategyRecordDistance{ - maxDepth: 3000, - distParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1}, - label: "10*pwd,10*realpwd,session,time", + distanceStaticBest := strat.RecordDistance{ + MaxDepth: 3000, + DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1}, + Label: "10*pwd,10*realpwd,session,time", } strategies = append(strategies, &distanceStaticBest) - recentBash := strategyRecentBash{} - recentBash.init() + recentBash := strat.RecentBash{} + recentBash.Init() strategies = append(strategies, &recentBash) if *slow { - markovCmd := strategyMarkovChainCmd{order: 1} - markovCmd.init() + markovCmd := strat.MarkovChainCmd{Order: 1} + markovCmd.Init() - markovCmd2 := strategyMarkovChainCmd{order: 2} - markovCmd2.init() + markovCmd2 := strat.MarkovChainCmd{Order: 2} + markovCmd2.Init() - markov := strategyMarkovChain{order: 1} - markov.init() + markov := strat.MarkovChain{Order: 1} + markov.Init() - markov2 := strategyMarkovChain{order: 2} - markov2.init() + markov2 := strat.MarkovChain{Order: 2} + markov2.Init() simpleStrategies = append(simpleStrategies, &markovCmd2, &markovCmd, &markov2, &markov) } - for _, strat := range simpleStrategies { - strategies = append(strategies, NewSimpleStrategyWrapper(strat)) + for _, strategy := range simpleStrategies { + strategies = append(strategies, strat.NewSimpleStrategyWrapper(strategy)) } for _, strat := range strategies { - err := evaluator.evaluate(strat) + err := evaluator.Evaluate(strat) if err != nil { log.Println("Evaluator evaluate() error:", err) } } - evaluator.calculateStatsAndPlot(*plottingScript) -} - -type ISimpleStrategy interface { - GetTitleAndDescription() (string, string) - GetCandidates() []string - AddHistoryRecord(record *records.EnrichedRecord) error - ResetHistory() error -} - -type IStrategy interface { - GetTitleAndDescription() (string, string) - GetCandidates(r records.EnrichedRecord) []string - AddHistoryRecord(record *records.EnrichedRecord) error - ResetHistory() error -} - -type simpleStrategyWrapper struct { - strategy ISimpleStrategy -} - -// NewSimpleStrategyWrapper returns IStrategy created by wrapping given ISimpleStrategy -func NewSimpleStrategyWrapper(strategy ISimpleStrategy) *simpleStrategyWrapper { - return &simpleStrategyWrapper{strategy: strategy} -} - -func (s *simpleStrategyWrapper) GetTitleAndDescription() (string, string) { - return s.strategy.GetTitleAndDescription() -} - -func (s *simpleStrategyWrapper) GetCandidates(r records.EnrichedRecord) []string { - return s.strategy.GetCandidates() -} - -func (s *simpleStrategyWrapper) AddHistoryRecord(r *records.EnrichedRecord) error { - return s.strategy.AddHistoryRecord(r) -} - -func (s *simpleStrategyWrapper) ResetHistory() error { - return s.strategy.ResetHistory() -} - -type matchJSON struct { - Match bool - Distance int - CharsRecalled int -} - -type multiMatchItemJSON struct { - Distance int - CharsRecalled int -} - -type multiMatchJSON struct { - Match bool - Entries []multiMatchItemJSON -} - -type strategyJSON struct { - Title string - Description string - Matches []matchJSON - PrefixMatches []multiMatchJSON -} - -type deviceRecords struct { - Name string - Records []records.EnrichedRecord -} - -type userRecords struct { - Name string - Devices []deviceRecords -} - -type evaluator struct { - sanitizedInput bool - BatchMode bool - maxCandidates int - skipFailedCmds bool - debugRecords float64 - UsersRecords []userRecords - Strategies []strategyJSON -} - -func (e *evaluator) initBatchMode(input string, inputDataRoot string) error { - e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot) - e.preprocessRecords() - return nil -} - -func (e *evaluator) init(inputPath string) error { - records := e.loadHistoryRecords(inputPath) - device := deviceRecords{Records: records} - user := userRecords{} - user.Devices = append(user.Devices, device) - e.UsersRecords = append(e.UsersRecords, user) - e.preprocessRecords() - return nil -} - -func (e *evaluator) calculateStatsAndPlot(scriptName string) { - evalJSON, err := json.Marshal(e) - if err != nil { - log.Fatal("json marshal error", err) - } - buffer := bytes.Buffer{} - buffer.Write(evalJSON) - // run python script to stat and plot/ - cmd := exec.Command(scriptName) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = &buffer - err = cmd.Run() - if err != nil { - log.Printf("Command finished with error: %v", err) - } -} - -func (e *evaluator) preprocessDeviceRecords(device deviceRecords) deviceRecords { - sessionIDs := map[string]uint64{} - var nextID uint64 - nextID = 1 // start with 1 because 0 won't get saved to json - for k, record := range device.Records { - id, found := sessionIDs[record.SessionID] - if found == false { - id = nextID - sessionIDs[record.SessionID] = id - nextID++ - } - device.Records[k].SeqSessionID = id - // assert - if record.Sanitized != e.sanitizedInput { - if e.sanitizedInput { - log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") - } - log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") - } - device.Records[k].SeqSessionID = id - if e.debugRecords > 0 && rand.Float64() < e.debugRecords { - device.Records[k].DebugThisRecord = true - } - } - // sort.SliceStable(device.Records, func(x, y int) bool { - // if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID { - // return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal - // } - // return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID - // }) - - // iterate from back and mark last record of each session - sessionIDSet := map[string]bool{} - for i := len(device.Records) - 1; i >= 0; i-- { - var record *records.EnrichedRecord - record = &device.Records[i] - if sessionIDSet[record.SessionID] { - continue - } - sessionIDSet[record.SessionID] = true - record.LastRecordOfSession = true - } - return device -} - -// enrich records and add sequential session ID -func (e *evaluator) preprocessRecords() { - for i := range e.UsersRecords { - for j := range e.UsersRecords[i].Devices { - e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) - } - } -} - -func (e *evaluator) evaluate(strategy IStrategy) error { - title, description := strategy.GetTitleAndDescription() - log.Println("Evaluating strategy:", title, "-", description) - strategyData := strategyJSON{Title: title, Description: description} - for i := range e.UsersRecords { - for j := range e.UsersRecords[i].Devices { - bar := progressbar.New(len(e.UsersRecords[i].Devices[j].Records)) - var prevRecord records.EnrichedRecord - for _, record := range e.UsersRecords[i].Devices[j].Records { - if e.skipFailedCmds && record.ExitCode != 0 { - continue - } - candidates := strategy.GetCandidates(records.Stripped(record)) - if record.DebugThisRecord { - log.Println() - log.Println("===================================================") - log.Println("STRATEGY:", title, "-", description) - log.Println("===================================================") - log.Println("Previous record:") - if prevRecord.RealtimeBefore == 0 { - log.Println("== NIL") - } else { - rec, _ := prevRecord.ToString() - log.Println(rec) - } - log.Println("---------------------------------------------------") - log.Println("Recommendations for:") - rec, _ := record.ToString() - log.Println(rec) - log.Println("---------------------------------------------------") - for i, candidate := range candidates { - if i > 10 { - break - } - log.Println(string(candidate)) - } - log.Println("===================================================") - } - - matchFound := false - longestPrefixMatchLength := 0 - multiMatch := multiMatchJSON{} - for i, candidate := range candidates { - // make an option (--calculate-total) to turn this on/off ? - // if i >= e.maxCandidates { - // break - // } - commonPrefixLength := len(longestcommon.Prefix([]string{candidate, record.CmdLine})) - if commonPrefixLength > longestPrefixMatchLength { - longestPrefixMatchLength = commonPrefixLength - prefixMatch := multiMatchItemJSON{Distance: i + 1, CharsRecalled: commonPrefixLength} - multiMatch.Match = true - multiMatch.Entries = append(multiMatch.Entries, prefixMatch) - } - if candidate == record.CmdLine { - match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength} - matchFound = true - strategyData.Matches = append(strategyData.Matches, match) - strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) - break - } - } - if matchFound == false { - strategyData.Matches = append(strategyData.Matches, matchJSON{}) - strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) - } - err := strategy.AddHistoryRecord(&record) - if err != nil { - log.Println("Error while evauating", err) - return err - } - bar.Add(1) - prevRecord = record - } - strategy.ResetHistory() - fmt.Println() - } - } - e.Strategies = append(e.Strategies, strategyData) - return nil -} - -func (e *evaluator) loadHistoryRecordsBatchMode(fname string, dataRootPath string) []userRecords { - var records []userRecords - info, err := os.Stat(dataRootPath) - if err != nil { - log.Fatal("Error: Directory", dataRootPath, "does not exist - exiting! (", err, ")") - } - if info.IsDir() == false { - log.Fatal("Error:", dataRootPath, "is not a directory - exiting!") - } - users, err := ioutil.ReadDir(dataRootPath) - if err != nil { - log.Fatal("Could not read directory:", dataRootPath) - } - fmt.Println("Listing users in <", dataRootPath, ">...") - for _, user := range users { - userRecords := userRecords{Name: user.Name()} - userFullPath := filepath.Join(dataRootPath, user.Name()) - if user.IsDir() == false { - log.Println("Warn: Unexpected file (not a directory) <", userFullPath, "> - skipping.") - continue - } - fmt.Println() - fmt.Printf("*- %s\n", user.Name()) - devices, err := ioutil.ReadDir(userFullPath) - if err != nil { - log.Fatal("Could not read directory:", userFullPath) - } - for _, device := range devices { - deviceRecords := deviceRecords{Name: device.Name()} - deviceFullPath := filepath.Join(userFullPath, device.Name()) - if device.IsDir() == false { - log.Println("Warn: Unexpected file (not a directory) <", deviceFullPath, "> - skipping.") - continue - } - fmt.Printf(" \\- %s\n", device.Name()) - files, err := ioutil.ReadDir(deviceFullPath) - if err != nil { - log.Fatal("Could not read directory:", deviceFullPath) - } - for _, file := range files { - fileFullPath := filepath.Join(deviceFullPath, file.Name()) - if file.Name() == fname { - fmt.Printf(" \\- %s - loading ...", file.Name()) - // load the data - deviceRecords.Records = e.loadHistoryRecords(fileFullPath) - fmt.Println(" OK ✓") - } else { - fmt.Printf(" \\- %s - skipped\n", file.Name()) - } - } - userRecords.Devices = append(userRecords.Devices, deviceRecords) - } - records = append(records, userRecords) - } - return records -} - -func (e *evaluator) loadHistoryRecords(fname string) []records.EnrichedRecord { - file, err := os.Open(fname) - if err != nil { - log.Fatal("Open() resh history file error:", err) - } - defer file.Close() - - var recs []records.EnrichedRecord - scanner := bufio.NewScanner(file) - for scanner.Scan() { - record := records.Record{} - fallbackRecord := records.FallbackRecord{} - line := scanner.Text() - err = json.Unmarshal([]byte(line), &record) - if err != nil { - err = json.Unmarshal([]byte(line), &fallbackRecord) - if err != nil { - log.Println("Line:", line) - log.Fatal("Decoding error:", err) - } - record = records.ConvertRecord(&fallbackRecord) - } - if e.sanitizedInput == false { - if record.CmdLength != 0 { - log.Fatal("Assert failed - 'cmdLength' is set in raw data. Maybe you want to use '--sanitized-input' option?") - } - record.CmdLength = len(record.CmdLine) - } - if record.CmdLength == 0 { - log.Fatal("Assert failed - 'cmdLength' is unset in the data. This should not happen.") - } - recs = append(recs, record.Enrich()) - } - return recs + evaluator.CalculateStatsAndPlot(*plottingScript) } diff --git a/cmd/evaluate/strategy-dummy.go b/cmd/evaluate/strategy-dummy.go deleted file mode 100644 index 9d20779..0000000 --- a/cmd/evaluate/strategy-dummy.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "github.com/curusarn/resh/pkg/records" - -type strategyDummy struct { - history []string -} - -func (s *strategyDummy) GetTitleAndDescription() (string, string) { - return "dummy", "Return empty candidate list" -} - -func (s *strategyDummy) GetCandidates() []string { - return nil -} - -func (s *strategyDummy) AddHistoryRecord(record *records.EnrichedRecord) error { - s.history = append(s.history, record.CmdLine) - return nil -} - -func (s *strategyDummy) ResetHistory() error { - return nil -} diff --git a/pkg/histanal/histeval.go b/pkg/histanal/histeval.go new file mode 100644 index 0000000..4d19779 --- /dev/null +++ b/pkg/histanal/histeval.go @@ -0,0 +1,246 @@ +package histanal + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "math/rand" + "os" + "os/exec" + + "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/pkg/strat" + "github.com/jpillora/longestcommon" + + "github.com/schollz/progressbar" +) + +type matchJSON struct { + Match bool + Distance int + CharsRecalled int +} + +type multiMatchItemJSON struct { + Distance int + CharsRecalled int +} + +type multiMatchJSON struct { + Match bool + Entries []multiMatchItemJSON +} + +type strategyJSON struct { + Title string + Description string + Matches []matchJSON + PrefixMatches []multiMatchJSON +} + +// HistEval evaluates history +type HistEval struct { + HistLoad + BatchMode bool + maxCandidates int + Strategies []strategyJSON +} + +// NewHistEval constructs new HistEval +func NewHistEval(inputPath string, + maxCandidates int, skipFailedCmds bool, + debugRecords float64, sanitizedInput bool) HistEval { + + e := HistEval{ + HistLoad: HistLoad{ + skipFailedCmds: skipFailedCmds, + debugRecords: debugRecords, + sanitizedInput: sanitizedInput, + }, + maxCandidates: maxCandidates, + BatchMode: false, + } + records := e.loadHistoryRecords(inputPath) + device := deviceRecords{Records: records} + user := userRecords{} + user.Devices = append(user.Devices, device) + e.UsersRecords = append(e.UsersRecords, user) + e.preprocessRecords() + return e +} + +// NewHistEvalBatchMode constructs new HistEval in batch mode +func NewHistEvalBatchMode(input string, inputDataRoot string, + maxCandidates int, skipFailedCmds bool, + debugRecords float64, sanitizedInput bool) HistEval { + + e := HistEval{ + HistLoad: HistLoad{ + skipFailedCmds: skipFailedCmds, + debugRecords: debugRecords, + sanitizedInput: sanitizedInput, + }, + maxCandidates: maxCandidates, + BatchMode: false, + } + e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot) + e.preprocessRecords() + return e +} + +func (e *HistEval) preprocessDeviceRecords(device deviceRecords) deviceRecords { + sessionIDs := map[string]uint64{} + var nextID uint64 + nextID = 1 // start with 1 because 0 won't get saved to json + for k, record := range device.Records { + id, found := sessionIDs[record.SessionID] + if found == false { + id = nextID + sessionIDs[record.SessionID] = id + nextID++ + } + device.Records[k].SeqSessionID = id + // assert + if record.Sanitized != e.sanitizedInput { + if e.sanitizedInput { + log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") + } + log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") + } + device.Records[k].SeqSessionID = id + if e.debugRecords > 0 && rand.Float64() < e.debugRecords { + device.Records[k].DebugThisRecord = true + } + } + // sort.SliceStable(device.Records, func(x, y int) bool { + // if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID { + // return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal + // } + // return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID + // }) + + // iterate from back and mark last record of each session + sessionIDSet := map[string]bool{} + for i := len(device.Records) - 1; i >= 0; i-- { + var record *records.EnrichedRecord + record = &device.Records[i] + if sessionIDSet[record.SessionID] { + continue + } + sessionIDSet[record.SessionID] = true + record.LastRecordOfSession = true + } + return device +} + +// enrich records and add sequential session ID +func (e *HistEval) preprocessRecords() { + for i := range e.UsersRecords { + for j := range e.UsersRecords[i].Devices { + e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) + } + } +} + +// Evaluate a given strategy +func (e *HistEval) Evaluate(strategy strat.IStrategy) error { + title, description := strategy.GetTitleAndDescription() + log.Println("Evaluating strategy:", title, "-", description) + strategyData := strategyJSON{Title: title, Description: description} + for i := range e.UsersRecords { + for j := range e.UsersRecords[i].Devices { + bar := progressbar.New(len(e.UsersRecords[i].Devices[j].Records)) + var prevRecord records.EnrichedRecord + for _, record := range e.UsersRecords[i].Devices[j].Records { + if e.skipFailedCmds && record.ExitCode != 0 { + continue + } + candidates := strategy.GetCandidates(records.Stripped(record)) + if record.DebugThisRecord { + log.Println() + log.Println("===================================================") + log.Println("STRATEGY:", title, "-", description) + log.Println("===================================================") + log.Println("Previous record:") + if prevRecord.RealtimeBefore == 0 { + log.Println("== NIL") + } else { + rec, _ := prevRecord.ToString() + log.Println(rec) + } + log.Println("---------------------------------------------------") + log.Println("Recommendations for:") + rec, _ := record.ToString() + log.Println(rec) + log.Println("---------------------------------------------------") + for i, candidate := range candidates { + if i > 10 { + break + } + log.Println(string(candidate)) + } + log.Println("===================================================") + } + + matchFound := false + longestPrefixMatchLength := 0 + multiMatch := multiMatchJSON{} + for i, candidate := range candidates { + // make an option (--calculate-total) to turn this on/off ? + // if i >= e.maxCandidates { + // break + // } + commonPrefixLength := len(longestcommon.Prefix([]string{candidate, record.CmdLine})) + if commonPrefixLength > longestPrefixMatchLength { + longestPrefixMatchLength = commonPrefixLength + prefixMatch := multiMatchItemJSON{Distance: i + 1, CharsRecalled: commonPrefixLength} + multiMatch.Match = true + multiMatch.Entries = append(multiMatch.Entries, prefixMatch) + } + if candidate == record.CmdLine { + match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength} + matchFound = true + strategyData.Matches = append(strategyData.Matches, match) + strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) + break + } + } + if matchFound == false { + strategyData.Matches = append(strategyData.Matches, matchJSON{}) + strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) + } + err := strategy.AddHistoryRecord(&record) + if err != nil { + log.Println("Error while evauating", err) + return err + } + bar.Add(1) + prevRecord = record + } + strategy.ResetHistory() + fmt.Println() + } + } + e.Strategies = append(e.Strategies, strategyData) + return nil +} + +// CalculateStatsAndPlot results +func (e *HistEval) CalculateStatsAndPlot(scriptName string) { + evalJSON, err := json.Marshal(e) + if err != nil { + log.Fatal("json marshal error", err) + } + buffer := bytes.Buffer{} + buffer.Write(evalJSON) + // run python script to stat and plot/ + cmd := exec.Command(scriptName) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = &buffer + err = cmd.Run() + if err != nil { + log.Printf("Command finished with error: %v", err) + } +} diff --git a/pkg/histanal/histload.go b/pkg/histanal/histload.go new file mode 100644 index 0000000..c8f253b --- /dev/null +++ b/pkg/histanal/histload.go @@ -0,0 +1,179 @@ +package histanal + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math/rand" + "os" + "path/filepath" + + "github.com/curusarn/resh/pkg/records" +) + +type deviceRecords struct { + Name string + Records []records.EnrichedRecord +} + +type userRecords struct { + Name string + Devices []deviceRecords +} + +// HistLoad loads history +type HistLoad struct { + UsersRecords []userRecords + skipFailedCmds bool + sanitizedInput bool + debugRecords float64 +} + +func (e *HistLoad) preprocessDeviceRecords(device deviceRecords) deviceRecords { + sessionIDs := map[string]uint64{} + var nextID uint64 + nextID = 1 // start with 1 because 0 won't get saved to json + for k, record := range device.Records { + id, found := sessionIDs[record.SessionID] + if found == false { + id = nextID + sessionIDs[record.SessionID] = id + nextID++ + } + device.Records[k].SeqSessionID = id + // assert + if record.Sanitized != e.sanitizedInput { + if e.sanitizedInput { + log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") + } + log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") + } + device.Records[k].SeqSessionID = id + if e.debugRecords > 0 && rand.Float64() < e.debugRecords { + device.Records[k].DebugThisRecord = true + } + } + // sort.SliceStable(device.Records, func(x, y int) bool { + // if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID { + // return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal + // } + // return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID + // }) + + // iterate from back and mark last record of each session + sessionIDSet := map[string]bool{} + for i := len(device.Records) - 1; i >= 0; i-- { + var record *records.EnrichedRecord + record = &device.Records[i] + if sessionIDSet[record.SessionID] { + continue + } + sessionIDSet[record.SessionID] = true + record.LastRecordOfSession = true + } + return device +} + +// enrich records and add sequential session ID +func (e *HistLoad) preprocessRecords() { + for i := range e.UsersRecords { + for j := range e.UsersRecords[i].Devices { + e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) + } + } +} + +func (e *HistLoad) loadHistoryRecordsBatchMode(fname string, dataRootPath string) []userRecords { + var records []userRecords + info, err := os.Stat(dataRootPath) + if err != nil { + log.Fatal("Error: Directory", dataRootPath, "does not exist - exiting! (", err, ")") + } + if info.IsDir() == false { + log.Fatal("Error:", dataRootPath, "is not a directory - exiting!") + } + users, err := ioutil.ReadDir(dataRootPath) + if err != nil { + log.Fatal("Could not read directory:", dataRootPath) + } + fmt.Println("Listing users in <", dataRootPath, ">...") + for _, user := range users { + userRecords := userRecords{Name: user.Name()} + userFullPath := filepath.Join(dataRootPath, user.Name()) + if user.IsDir() == false { + log.Println("Warn: Unexpected file (not a directory) <", userFullPath, "> - skipping.") + continue + } + fmt.Println() + fmt.Printf("*- %s\n", user.Name()) + devices, err := ioutil.ReadDir(userFullPath) + if err != nil { + log.Fatal("Could not read directory:", userFullPath) + } + for _, device := range devices { + deviceRecords := deviceRecords{Name: device.Name()} + deviceFullPath := filepath.Join(userFullPath, device.Name()) + if device.IsDir() == false { + log.Println("Warn: Unexpected file (not a directory) <", deviceFullPath, "> - skipping.") + continue + } + fmt.Printf(" \\- %s\n", device.Name()) + files, err := ioutil.ReadDir(deviceFullPath) + if err != nil { + log.Fatal("Could not read directory:", deviceFullPath) + } + for _, file := range files { + fileFullPath := filepath.Join(deviceFullPath, file.Name()) + if file.Name() == fname { + fmt.Printf(" \\- %s - loading ...", file.Name()) + // load the data + deviceRecords.Records = e.loadHistoryRecords(fileFullPath) + fmt.Println(" OK ✓") + } else { + fmt.Printf(" \\- %s - skipped\n", file.Name()) + } + } + userRecords.Devices = append(userRecords.Devices, deviceRecords) + } + records = append(records, userRecords) + } + return records +} + +func (e *HistLoad) loadHistoryRecords(fname string) []records.EnrichedRecord { + file, err := os.Open(fname) + if err != nil { + log.Fatal("Open() resh history file error:", err) + } + defer file.Close() + + var recs []records.EnrichedRecord + scanner := bufio.NewScanner(file) + for scanner.Scan() { + record := records.Record{} + fallbackRecord := records.FallbackRecord{} + line := scanner.Text() + err = json.Unmarshal([]byte(line), &record) + if err != nil { + err = json.Unmarshal([]byte(line), &fallbackRecord) + if err != nil { + log.Println("Line:", line) + log.Fatal("Decoding error:", err) + } + record = records.ConvertRecord(&fallbackRecord) + } + if e.sanitizedInput == false { + if record.CmdLength != 0 { + log.Fatal("Assert failed - 'cmdLength' is set in raw data. Maybe you want to use '--sanitized-input' option?") + } + record.CmdLength = len(record.CmdLine) + } + if record.CmdLength == 0 { + log.Fatal("Assert failed - 'cmdLength' is unset in the data. This should not happen.") + } + recs = append(recs, record.Enrich()) + } + return recs +} diff --git a/cmd/evaluate/strategy-directory-sensitive.go b/pkg/strat/directory-sensitive.go similarity index 63% rename from cmd/evaluate/strategy-directory-sensitive.go rename to pkg/strat/directory-sensitive.go index a05deac..87edaec 100644 --- a/cmd/evaluate/strategy-directory-sensitive.go +++ b/pkg/strat/directory-sensitive.go @@ -1,25 +1,25 @@ -package main +package strat import "github.com/curusarn/resh/pkg/records" -type strategyDirectorySensitive struct { +type DirectorySensitive struct { history map[string][]string lastPwd string } -func (s *strategyDirectorySensitive) init() { +func (s *DirectorySensitive) Init() { s.history = map[string][]string{} } -func (s *strategyDirectorySensitive) GetTitleAndDescription() (string, string) { +func (s *DirectorySensitive) GetTitleAndDescription() (string, string) { return "directory sensitive (recent)", "Use recent commands executed is the same directory" } -func (s *strategyDirectorySensitive) GetCandidates() []string { +func (s *DirectorySensitive) GetCandidates() []string { return s.history[s.lastPwd] } -func (s *strategyDirectorySensitive) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *DirectorySensitive) AddHistoryRecord(record *records.EnrichedRecord) error { // work on history for PWD pwd := record.Pwd // remove previous occurance of record @@ -34,7 +34,8 @@ func (s *strategyDirectorySensitive) AddHistoryRecord(record *records.EnrichedRe return nil } -func (s *strategyDirectorySensitive) ResetHistory() error { +func (s *DirectorySensitive) ResetHistory() error { + s.Init() s.history = map[string][]string{} return nil } diff --git a/pkg/strat/dummy.go b/pkg/strat/dummy.go new file mode 100644 index 0000000..b21a60d --- /dev/null +++ b/pkg/strat/dummy.go @@ -0,0 +1,24 @@ +package strat + +import "github.com/curusarn/resh/pkg/records" + +type Dummy struct { + history []string +} + +func (s *Dummy) GetTitleAndDescription() (string, string) { + return "dummy", "Return empty candidate list" +} + +func (s *Dummy) GetCandidates() []string { + return nil +} + +func (s *Dummy) AddHistoryRecord(record *records.EnrichedRecord) error { + s.history = append(s.history, record.CmdLine) + return nil +} + +func (s *Dummy) ResetHistory() error { + return nil +} diff --git a/cmd/evaluate/strategy-dynamic-record-distance.go b/pkg/strat/dynamic-record-distance.go similarity index 58% rename from cmd/evaluate/strategy-dynamic-record-distance.go rename to pkg/strat/dynamic-record-distance.go index 0a107e9..4a3be10 100644 --- a/cmd/evaluate/strategy-dynamic-record-distance.go +++ b/pkg/strat/dynamic-record-distance.go @@ -1,4 +1,4 @@ -package main +package strat import ( "math" @@ -8,14 +8,14 @@ import ( "github.com/curusarn/resh/pkg/records" ) -type strategyDynamicRecordDistance struct { +type DynamicRecordDistance struct { history []records.EnrichedRecord - distParams records.DistParams + DistParams records.DistParams pwdHistogram map[string]int realPwdHistogram map[string]int gitOriginHistogram map[string]int - maxDepth int - label string + MaxDepth int + Label string } type strDynDistEntry struct { @@ -23,36 +23,36 @@ type strDynDistEntry struct { distance float64 } -func (s *strategyDynamicRecordDistance) init() { +func (s *DynamicRecordDistance) Init() { s.history = nil s.pwdHistogram = map[string]int{} s.realPwdHistogram = map[string]int{} s.gitOriginHistogram = map[string]int{} } -func (s *strategyDynamicRecordDistance) GetTitleAndDescription() (string, string) { - return "dynamic record distance (depth:" + strconv.Itoa(s.maxDepth) + ";" + s.label + ")", "Use TF-IDF record distance to recommend commands" +func (s *DynamicRecordDistance) GetTitleAndDescription() (string, string) { + return "dynamic record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use TF-IDF record distance to recommend commands" } -func (s *strategyDynamicRecordDistance) idf(count int) float64 { +func (s *DynamicRecordDistance) idf(count int) float64 { return math.Log(float64(len(s.history)) / float64(count)) } -func (s *strategyDynamicRecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { +func (s *DynamicRecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { if len(s.history) == 0 { return nil } var mapItems []strDynDistEntry for i, record := range s.history { - if s.maxDepth != 0 && i > s.maxDepth { + if s.MaxDepth != 0 && i > s.MaxDepth { break } distParams := records.DistParams{ - Pwd: s.distParams.Pwd * s.idf(s.pwdHistogram[strippedRecord.PwdAfter]), - RealPwd: s.distParams.RealPwd * s.idf(s.realPwdHistogram[strippedRecord.RealPwdAfter]), - Git: s.distParams.Git * s.idf(s.gitOriginHistogram[strippedRecord.GitOriginRemote]), - Time: s.distParams.Time, - SessionID: s.distParams.SessionID, + Pwd: s.DistParams.Pwd * s.idf(s.pwdHistogram[strippedRecord.PwdAfter]), + RealPwd: s.DistParams.RealPwd * s.idf(s.realPwdHistogram[strippedRecord.RealPwdAfter]), + Git: s.DistParams.Git * s.idf(s.gitOriginHistogram[strippedRecord.GitOriginRemote]), + Time: s.DistParams.Time, + SessionID: s.DistParams.SessionID, } distance := record.DistanceTo(strippedRecord, distParams) mapItems = append(mapItems, strDynDistEntry{record.CmdLine, distance}) @@ -70,7 +70,7 @@ func (s *strategyDynamicRecordDistance) GetCandidates(strippedRecord records.Enr return hist } -func (s *strategyDynamicRecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *DynamicRecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { // append record to front s.history = append([]records.EnrichedRecord{*record}, s.history...) s.pwdHistogram[record.Pwd]++ @@ -79,7 +79,7 @@ func (s *strategyDynamicRecordDistance) AddHistoryRecord(record *records.Enriche return nil } -func (s *strategyDynamicRecordDistance) ResetHistory() error { - s.init() +func (s *DynamicRecordDistance) ResetHistory() error { + s.Init() return nil } diff --git a/cmd/evaluate/strategy-frequent.go b/pkg/strat/frequent.go similarity index 64% rename from cmd/evaluate/strategy-frequent.go rename to pkg/strat/frequent.go index f081742..6525b9f 100644 --- a/cmd/evaluate/strategy-frequent.go +++ b/pkg/strat/frequent.go @@ -1,4 +1,4 @@ -package main +package strat import ( "sort" @@ -6,7 +6,7 @@ import ( "github.com/curusarn/resh/pkg/records" ) -type strategyFrequent struct { +type Frequent struct { history map[string]int } @@ -15,15 +15,15 @@ type strFrqEntry struct { count int } -func (s *strategyFrequent) init() { +func (s *Frequent) init() { s.history = map[string]int{} } -func (s *strategyFrequent) GetTitleAndDescription() (string, string) { +func (s *Frequent) GetTitleAndDescription() (string, string) { return "frequent", "Use frequent commands" } -func (s *strategyFrequent) GetCandidates() []string { +func (s *Frequent) GetCandidates() []string { var mapItems []strFrqEntry for cmdLine, count := range s.history { mapItems = append(mapItems, strFrqEntry{cmdLine, count}) @@ -36,12 +36,12 @@ func (s *strategyFrequent) GetCandidates() []string { return hist } -func (s *strategyFrequent) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *Frequent) AddHistoryRecord(record *records.EnrichedRecord) error { s.history[record.CmdLine]++ return nil } -func (s *strategyFrequent) ResetHistory() error { +func (s *Frequent) ResetHistory() error { s.init() return nil } diff --git a/cmd/evaluate/strategy-markov-chain-cmd.go b/pkg/strat/markov-chain-cmd.go similarity index 75% rename from cmd/evaluate/strategy-markov-chain-cmd.go rename to pkg/strat/markov-chain-cmd.go index 7c4ae7f..1bd1ded 100644 --- a/cmd/evaluate/strategy-markov-chain-cmd.go +++ b/pkg/strat/markov-chain-cmd.go @@ -1,4 +1,4 @@ -package main +package strat import ( "sort" @@ -8,8 +8,8 @@ import ( "github.com/mb-14/gomarkov" ) -type strategyMarkovChainCmd struct { - order int +type MarkovChainCmd struct { + Order int history []strMarkCmdHistoryEntry historyCmds []string } @@ -24,24 +24,24 @@ type strMarkCmdEntry struct { transProb float64 } -func (s *strategyMarkovChainCmd) init() { +func (s *MarkovChainCmd) Init() { s.history = nil s.historyCmds = nil } -func (s *strategyMarkovChainCmd) GetTitleAndDescription() (string, string) { - return "command-based markov chain (order " + strconv.Itoa(s.order) + ")", "Use command-based markov chain to recommend commands" +func (s *MarkovChainCmd) GetTitleAndDescription() (string, string) { + return "command-based markov chain (order " + strconv.Itoa(s.Order) + ")", "Use command-based markov chain to recommend commands" } -func (s *strategyMarkovChainCmd) GetCandidates() []string { - if len(s.history) < s.order { +func (s *MarkovChainCmd) GetCandidates() []string { + if len(s.history) < s.Order { var hist []string for _, item := range s.history { hist = append(hist, item.cmdLine) } return hist } - chain := gomarkov.NewChain(s.order) + chain := gomarkov.NewChain(s.Order) chain.Add(s.historyCmds) @@ -52,7 +52,7 @@ func (s *strategyMarkovChainCmd) GetCandidates() []string { continue } cmdsSet[cmd] = true - prob, _ := chain.TransitionProbability(cmd, s.historyCmds[len(s.historyCmds)-s.order:]) + prob, _ := chain.TransitionProbability(cmd, s.historyCmds[len(s.historyCmds)-s.Order:]) entries = append(entries, strMarkCmdEntry{cmd: cmd, transProb: prob}) } sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) @@ -78,14 +78,14 @@ func (s *strategyMarkovChainCmd) GetCandidates() []string { return hist } -func (s *strategyMarkovChainCmd) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *MarkovChainCmd) AddHistoryRecord(record *records.EnrichedRecord) error { s.history = append(s.history, strMarkCmdHistoryEntry{cmdLine: record.CmdLine, cmd: record.Command}) s.historyCmds = append(s.historyCmds, record.Command) // s.historySet[record.CmdLine] = true return nil } -func (s *strategyMarkovChainCmd) ResetHistory() error { - s.init() +func (s *MarkovChainCmd) ResetHistory() error { + s.Init() return nil } diff --git a/cmd/evaluate/strategy-markov-chain.go b/pkg/strat/markov-chain.go similarity index 68% rename from cmd/evaluate/strategy-markov-chain.go rename to pkg/strat/markov-chain.go index 25876b9..07006e0 100644 --- a/cmd/evaluate/strategy-markov-chain.go +++ b/pkg/strat/markov-chain.go @@ -1,4 +1,4 @@ -package main +package strat import ( "sort" @@ -8,8 +8,8 @@ import ( "github.com/mb-14/gomarkov" ) -type strategyMarkovChain struct { - order int +type MarkovChain struct { + Order int history []string } @@ -18,19 +18,19 @@ type strMarkEntry struct { transProb float64 } -func (s *strategyMarkovChain) init() { +func (s *MarkovChain) Init() { s.history = nil } -func (s *strategyMarkovChain) GetTitleAndDescription() (string, string) { - return "markov chain (order " + strconv.Itoa(s.order) + ")", "Use markov chain to recommend commands" +func (s *MarkovChain) GetTitleAndDescription() (string, string) { + return "markov chain (order " + strconv.Itoa(s.Order) + ")", "Use markov chain to recommend commands" } -func (s *strategyMarkovChain) GetCandidates() []string { - if len(s.history) < s.order { +func (s *MarkovChain) GetCandidates() []string { + if len(s.history) < s.Order { return s.history } - chain := gomarkov.NewChain(s.order) + chain := gomarkov.NewChain(s.Order) chain.Add(s.history) @@ -41,7 +41,7 @@ func (s *strategyMarkovChain) GetCandidates() []string { continue } cmdLinesSet[cmdLine] = true - prob, _ := chain.TransitionProbability(cmdLine, s.history[len(s.history)-s.order:]) + prob, _ := chain.TransitionProbability(cmdLine, s.history[len(s.history)-s.Order:]) entries = append(entries, strMarkEntry{cmdLine: cmdLine, transProb: prob}) } sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) @@ -58,13 +58,13 @@ func (s *strategyMarkovChain) GetCandidates() []string { return hist } -func (s *strategyMarkovChain) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *MarkovChain) AddHistoryRecord(record *records.EnrichedRecord) error { s.history = append(s.history, record.CmdLine) // s.historySet[record.CmdLine] = true return nil } -func (s *strategyMarkovChain) ResetHistory() error { - s.init() +func (s *MarkovChain) ResetHistory() error { + s.Init() return nil } diff --git a/cmd/evaluate/strategy-random.go b/pkg/strat/random.go similarity index 62% rename from cmd/evaluate/strategy-random.go rename to pkg/strat/random.go index f8a59eb..27e959c 100644 --- a/cmd/evaluate/strategy-random.go +++ b/pkg/strat/random.go @@ -1,4 +1,4 @@ -package main +package strat import ( "math/rand" @@ -7,27 +7,27 @@ import ( "github.com/curusarn/resh/pkg/records" ) -type strategyRandom struct { - candidatesSize int +type Random struct { + CandidatesSize int history []string historySet map[string]bool } -func (s *strategyRandom) init() { +func (s *Random) Init() { s.history = nil s.historySet = map[string]bool{} } -func (s *strategyRandom) GetTitleAndDescription() (string, string) { +func (s *Random) GetTitleAndDescription() (string, string) { return "random", "Use random commands" } -func (s *strategyRandom) GetCandidates() []string { +func (s *Random) GetCandidates() []string { seed := time.Now().UnixNano() rand.Seed(seed) var candidates []string candidateSet := map[string]bool{} - for len(candidates) < s.candidatesSize && len(candidates)*2 < len(s.historySet) { + for len(candidates) < s.CandidatesSize && len(candidates)*2 < len(s.historySet) { x := rand.Intn(len(s.history)) candidate := s.history[x] if candidateSet[candidate] == false { @@ -39,13 +39,13 @@ func (s *strategyRandom) GetCandidates() []string { return candidates } -func (s *strategyRandom) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *Random) AddHistoryRecord(record *records.EnrichedRecord) error { s.history = append([]string{record.CmdLine}, s.history...) s.historySet[record.CmdLine] = true return nil } -func (s *strategyRandom) ResetHistory() error { - s.init() +func (s *Random) ResetHistory() error { + s.Init() return nil } diff --git a/cmd/evaluate/strategy-recent-bash.go b/pkg/strat/recent-bash.go similarity index 75% rename from cmd/evaluate/strategy-recent-bash.go rename to pkg/strat/recent-bash.go index 7a83632..c5319ce 100644 --- a/cmd/evaluate/strategy-recent-bash.go +++ b/pkg/strat/recent-bash.go @@ -1,23 +1,23 @@ -package main +package strat import "github.com/curusarn/resh/pkg/records" -type strategyRecentBash struct { +type RecentBash struct { histfile []string histfileSnapshot map[string][]string history map[string][]string } -func (s *strategyRecentBash) init() { +func (s *RecentBash) Init() { s.histfileSnapshot = map[string][]string{} s.history = map[string][]string{} } -func (s *strategyRecentBash) GetTitleAndDescription() (string, string) { +func (s *RecentBash) GetTitleAndDescription() (string, string) { return "recent (bash-like)", "Behave like bash" } -func (s *strategyRecentBash) GetCandidates(strippedRecord records.EnrichedRecord) []string { +func (s *RecentBash) GetCandidates(strippedRecord records.EnrichedRecord) []string { // populate the local history from histfile if s.histfileSnapshot[strippedRecord.SessionID] == nil { s.histfileSnapshot[strippedRecord.SessionID] = s.histfile @@ -25,7 +25,7 @@ func (s *strategyRecentBash) GetCandidates(strippedRecord records.EnrichedRecord return append(s.history[strippedRecord.SessionID], s.histfileSnapshot[strippedRecord.SessionID]...) } -func (s *strategyRecentBash) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *RecentBash) AddHistoryRecord(record *records.EnrichedRecord) error { // remove previous occurance of record for i, cmd := range s.history[record.SessionID] { if cmd == record.CmdLine { @@ -44,7 +44,7 @@ func (s *strategyRecentBash) AddHistoryRecord(record *records.EnrichedRecord) er return nil } -func (s *strategyRecentBash) ResetHistory() error { - s.init() +func (s *RecentBash) ResetHistory() error { + s.Init() return nil } diff --git a/cmd/evaluate/strategy-recent.go b/pkg/strat/recent.go similarity index 59% rename from cmd/evaluate/strategy-recent.go rename to pkg/strat/recent.go index 967480c..68c8eff 100644 --- a/cmd/evaluate/strategy-recent.go +++ b/pkg/strat/recent.go @@ -1,20 +1,20 @@ -package main +package strat import "github.com/curusarn/resh/pkg/records" -type strategyRecent struct { +type Recent struct { history []string } -func (s *strategyRecent) GetTitleAndDescription() (string, string) { +func (s *Recent) GetTitleAndDescription() (string, string) { return "recent", "Use recent commands" } -func (s *strategyRecent) GetCandidates() []string { +func (s *Recent) GetCandidates() []string { return s.history } -func (s *strategyRecent) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *Recent) AddHistoryRecord(record *records.EnrichedRecord) error { // remove previous occurance of record for i, cmd := range s.history { if cmd == record.CmdLine { @@ -26,7 +26,7 @@ func (s *strategyRecent) AddHistoryRecord(record *records.EnrichedRecord) error return nil } -func (s *strategyRecent) ResetHistory() error { +func (s *Recent) ResetHistory() error { s.history = nil return nil } diff --git a/cmd/evaluate/strategy-record-distance.go b/pkg/strat/record-distance.go similarity index 53% rename from cmd/evaluate/strategy-record-distance.go rename to pkg/strat/record-distance.go index d2b8696..816922e 100644 --- a/cmd/evaluate/strategy-record-distance.go +++ b/pkg/strat/record-distance.go @@ -1,4 +1,4 @@ -package main +package strat import ( "sort" @@ -7,11 +7,11 @@ import ( "github.com/curusarn/resh/pkg/records" ) -type strategyRecordDistance struct { +type RecordDistance struct { history []records.EnrichedRecord - distParams records.DistParams - maxDepth int - label string + DistParams records.DistParams + MaxDepth int + Label string } type strDistEntry struct { @@ -19,24 +19,24 @@ type strDistEntry struct { distance float64 } -func (s *strategyRecordDistance) init() { +func (s *RecordDistance) Init() { s.history = nil } -func (s *strategyRecordDistance) GetTitleAndDescription() (string, string) { - return "record distance (depth:" + strconv.Itoa(s.maxDepth) + ";" + s.label + ")", "Use record distance to recommend commands" +func (s *RecordDistance) GetTitleAndDescription() (string, string) { + return "record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use record distance to recommend commands" } -func (s *strategyRecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { +func (s *RecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { if len(s.history) == 0 { return nil } var mapItems []strDistEntry for i, record := range s.history { - if s.maxDepth != 0 && i > s.maxDepth { + if s.MaxDepth != 0 && i > s.MaxDepth { break } - distance := record.DistanceTo(strippedRecord, s.distParams) + distance := record.DistanceTo(strippedRecord, s.DistParams) mapItems = append(mapItems, strDistEntry{record.CmdLine, distance}) } sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance }) @@ -52,13 +52,13 @@ func (s *strategyRecordDistance) GetCandidates(strippedRecord records.EnrichedRe return hist } -func (s *strategyRecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { +func (s *RecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { // append record to front s.history = append([]records.EnrichedRecord{*record}, s.history...) return nil } -func (s *strategyRecordDistance) ResetHistory() error { - s.init() +func (s *RecordDistance) ResetHistory() error { + s.Init() return nil } diff --git a/pkg/strat/strat.go b/pkg/strat/strat.go new file mode 100644 index 0000000..d5b1b2c --- /dev/null +++ b/pkg/strat/strat.go @@ -0,0 +1,44 @@ +package strat + +import ( + "github.com/curusarn/resh/pkg/records" +) + +type ISimpleStrategy interface { + GetTitleAndDescription() (string, string) + GetCandidates() []string + AddHistoryRecord(record *records.EnrichedRecord) error + ResetHistory() error +} + +type IStrategy interface { + GetTitleAndDescription() (string, string) + GetCandidates(r records.EnrichedRecord) []string + AddHistoryRecord(record *records.EnrichedRecord) error + ResetHistory() error +} + +type simpleStrategyWrapper struct { + strategy ISimpleStrategy +} + +// NewSimpleStrategyWrapper returns IStrategy created by wrapping given ISimpleStrategy +func NewSimpleStrategyWrapper(strategy ISimpleStrategy) *simpleStrategyWrapper { + return &simpleStrategyWrapper{strategy: strategy} +} + +func (s *simpleStrategyWrapper) GetTitleAndDescription() (string, string) { + return s.strategy.GetTitleAndDescription() +} + +func (s *simpleStrategyWrapper) GetCandidates(r records.EnrichedRecord) []string { + return s.strategy.GetCandidates() +} + +func (s *simpleStrategyWrapper) AddHistoryRecord(r *records.EnrichedRecord) error { + return s.strategy.AddHistoryRecord(r) +} + +func (s *simpleStrategyWrapper) ResetHistory() error { + return s.strategy.ResetHistory() +}