Rich Enhanced Shell History - Contextual shell history for zsh and bash
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
resh/evaluate/resh-evaluate.go

318 lines
8.6 KiB

package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"github.com/curusarn/resh/common"
)
// Version from git set during build
var Version string
// Revision from git set during build
var Revision string
func main() {
usr, _ := user.Current()
dir := usr.HomeDir
historyPath := filepath.Join(dir, ".resh_history.json")
historyPathBatchMode := filepath.Join(dir, "resh_history.json")
sanitizedHistoryPath := filepath.Join(dir, "resh_history_sanitized.json")
// tmpPath := "/tmp/resh-evaluate-tmp.json"
showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit")
input := flag.String("input", "",
"Input file (default: "+historyPath+"OR"+sanitizedHistoryPath+
" depending on --sanitized-input option)")
// outputDir := flag.String("output", "/tmp/resh-evaluate", "Output directory")
sanitizedInput := flag.Bool("sanitized-input", false,
"Handle input as sanitized (also changes default value for input argument)")
plottingScript := flag.String("plotting-script", "resh-evaluate-plot.py", "Script to use for plotting")
inputDataRoot := flag.String("input-data-root", "",
"Input data root, enables batch mode, looks for files matching --input option")
flag.Parse()
// handle show{Version,Revision} options
if *showVersion == true {
fmt.Println(Version)
os.Exit(0)
}
if *showRevision == true {
fmt.Println(Revision)
os.Exit(0)
}
// handle batch mode
batchMode := false
if *inputDataRoot != "" {
batchMode = true
}
// set default input
if *input == "" {
if *sanitizedInput {
*input = sanitizedHistoryPath
} else if batchMode {
*input = historyPathBatchMode
} else {
*input = historyPath
}
}
evaluator := evaluator{sanitizedInput: *sanitizedInput, maxCandidates: 50, BatchMode: batchMode}
if batchMode {
err := evaluator.initBatchMode(*input, *inputDataRoot)
if err != nil {
log.Fatal("Evaluator initBatchMode() error:", err)
}
} else {
err := evaluator.init(*input)
if err != nil {
log.Fatal("Evaluator init() error:", err)
}
}
var strategies []strategy
// dummy := strategyDummy{}
// strategies = append(strategies, &dummy)
recent := strategyRecent{}
strategies = append(strategies, &recent)
for _, strat := range strategies {
err := evaluator.evaluate(strat)
if err != nil {
log.Println("Evaluator evaluate() error:", err)
}
}
evaluator.calculateStatsAndPlot(*plottingScript)
}
type strategy interface {
GetTitleAndDescription() (string, string)
GetCandidates() []string
AddHistoryRecord(record *common.Record) error
ResetHistory() error
}
type matchJSON struct {
Match bool
Distance int
CharsRecalled int
}
type strategyJSON struct {
Title string
Description string
Matches []matchJSON
}
type deviceRecords struct {
Name string
Records []common.Record
}
type userRecords struct {
Name string
Devices []deviceRecords
}
type evaluator struct {
sanitizedInput bool
BatchMode bool
maxCandidates int
UsersRecords []userRecords
Strategies []strategyJSON
}
func (e *evaluator) initBatchMode(input string, inputDataRoot string) error {
e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot)
e.processRecords()
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.processRecords()
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)
}
}
// enrich records and add them to serializable structure
func (e *evaluator) processRecords() {
for i := range e.UsersRecords {
for j := range e.UsersRecords[i].Devices {
for k, record := range e.UsersRecords[i].Devices[j].Records {
// 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")
}
e.UsersRecords[i].Devices[j].Records[k].Enrich()
// device.Records = append(device.Records, record)
}
}
}
}
func (e *evaluator) evaluate(strategy strategy) error {
title, description := strategy.GetTitleAndDescription()
strategyData := strategyJSON{Title: title, Description: description}
for _, record := range e.UsersRecords[0].Devices[0].Records {
candidates := strategy.GetCandidates()
matchFound := false
for i, candidate := range candidates {
// make an option (--calculate-total) to turn this on/off ?
// if i >= e.maxCandidates {
// break
// }
if candidate == record.CmdLine {
match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength}
strategyData.Matches = append(strategyData.Matches, match)
matchFound = true
break
}
}
if matchFound == false {
strategyData.Matches = append(strategyData.Matches, matchJSON{})
}
err := strategy.AddHistoryRecord(&record)
if err != nil {
log.Println("Error while evauating", err)
return err
}
}
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) []common.Record {
file, err := os.Open(fname)
if err != nil {
log.Fatal("Open() resh history file error:", err)
}
defer file.Close()
var records []common.Record
scanner := bufio.NewScanner(file)
for scanner.Scan() {
record := common.Record{}
fallbackRecord := common.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 = common.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.")
}
records = append(records, record)
}
return records
}