mirror of
https://github.com/ddworken/hishtory.git
synced 2024-11-26 10:13:44 +01:00
no-op refactoring: Move history entry building code from lib.go to cmd file for saving history entries
This commit is contained in:
parent
fe41687fd0
commit
2490082088
@ -1,12 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ddworken/hishtory/client/data"
|
||||
@ -86,20 +91,20 @@ func presaveHistoryEntry(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Build the basic entry with metadata retrieved from runtime
|
||||
entry, err := lib.BuildPreArgsHistoryEntry(ctx)
|
||||
entry, err := buildPreArgsHistoryEntry(ctx)
|
||||
lib.CheckFatalError(err)
|
||||
if entry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Augment it with os.Args
|
||||
entry.Command = lib.TrimTrailingWhitespace(os.Args[3])
|
||||
entry.Command = trimTrailingWhitespace(os.Args[3])
|
||||
if strings.HasPrefix(" ", entry.Command) {
|
||||
// Don't save commands that start with a space
|
||||
return
|
||||
}
|
||||
fmt.Println(entry.Command)
|
||||
startTime, err := lib.ParseCrossPlatformInt(os.Args[4])
|
||||
startTime, err := parseCrossPlatformInt(os.Args[4])
|
||||
lib.CheckFatalError(err)
|
||||
entry.StartTime = time.Unix(startTime, 0)
|
||||
entry.EndTime = time.Unix(0, 0)
|
||||
@ -124,7 +129,7 @@ func saveHistoryEntry(ctx *context.Context) {
|
||||
hctx.GetLogger().Infof("Skipping saving a history entry because hishtory is disabled\n")
|
||||
return
|
||||
}
|
||||
entry, err := lib.BuildHistoryEntry(ctx, os.Args)
|
||||
entry, err := buildHistoryEntry(ctx, os.Args)
|
||||
lib.CheckFatalError(err)
|
||||
if entry == nil {
|
||||
hctx.GetLogger().Infof("Skipping saving a history entry because we did not build a history entry (was the command prefixed with a space and/or empty?)\n")
|
||||
@ -213,3 +218,294 @@ func init() {
|
||||
rootCmd.AddCommand(saveHistoryEntryCmd)
|
||||
rootCmd.AddCommand(presaveHistoryEntryCmd)
|
||||
}
|
||||
|
||||
func buildPreArgsHistoryEntry(ctx *context.Context) (*data.HistoryEntry, error) {
|
||||
var entry data.HistoryEntry
|
||||
|
||||
// user
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.LocalUsername = user.Username
|
||||
|
||||
// cwd and homedir
|
||||
cwd, homedir, err := getCwd(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.CurrentWorkingDirectory = cwd
|
||||
entry.HomeDirectory = homedir
|
||||
|
||||
// hostname
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.Hostname = hostname
|
||||
|
||||
// device ID
|
||||
config := hctx.GetConf(ctx)
|
||||
entry.DeviceId = config.DeviceId
|
||||
|
||||
// custom columns
|
||||
cc, err := buildCustomColumns(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.CustomColumns = cc
|
||||
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
func buildHistoryEntry(ctx *context.Context, args []string) (*data.HistoryEntry, error) {
|
||||
if len(args) < 6 {
|
||||
hctx.GetLogger().Warnf("buildHistoryEntry called with args=%#v, which has too few entries! This can happen in specific edge cases for newly opened terminals and is likely not a problem.", args)
|
||||
return nil, nil
|
||||
}
|
||||
shell := args[2]
|
||||
|
||||
entry, err := buildPreArgsHistoryEntry(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// exitCode
|
||||
exitCode, err := strconv.Atoi(args[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.ExitCode = exitCode
|
||||
|
||||
// start time
|
||||
seconds, err := parseCrossPlatformInt(args[5])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse start time %s as int: %v", args[5], err)
|
||||
}
|
||||
entry.StartTime = time.Unix(seconds, 0)
|
||||
|
||||
// end time
|
||||
entry.EndTime = time.Now()
|
||||
|
||||
// command
|
||||
if shell == "bash" {
|
||||
cmd, err := getLastCommand(args[4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
shouldBeSkipped, err := shouldSkipHiddenCommand(ctx, args[4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if command was hidden: %v", err)
|
||||
}
|
||||
if shouldBeSkipped || strings.HasPrefix(cmd, " ") {
|
||||
// Don't save commands that start with a space
|
||||
return nil, nil
|
||||
}
|
||||
cmd, err = maybeSkipBashHistTimePrefix(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.Command = cmd
|
||||
} else if shell == "zsh" || shell == "fish" {
|
||||
cmd := trimTrailingWhitespace(args[4])
|
||||
if strings.HasPrefix(cmd, " ") {
|
||||
// Don't save commands that start with a space
|
||||
return nil, nil
|
||||
}
|
||||
entry.Command = cmd
|
||||
} else {
|
||||
return nil, fmt.Errorf("tried to save a hishtory entry from an unsupported shell=%#v", shell)
|
||||
}
|
||||
if strings.TrimSpace(entry.Command) == "" {
|
||||
// Skip recording empty commands where the user just hits enter in their terminal
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func trimTrailingWhitespace(s string) string {
|
||||
return strings.TrimSuffix(strings.TrimSuffix(s, "\n"), " ")
|
||||
}
|
||||
|
||||
func buildCustomColumns(ctx *context.Context) (data.CustomColumns, error) {
|
||||
ccs := data.CustomColumns{}
|
||||
config := hctx.GetConf(ctx)
|
||||
for _, cc := range config.CustomColumns {
|
||||
cmd := exec.Command("bash", "-c", cc.ColumnCommand)
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute custom command named %v (stdout=%#v, stderr=%#v)", cc.ColumnName, stdout.String(), stderr.String())
|
||||
}
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
// Log a warning, but don't crash. This way commands can exit with a different status and still work.
|
||||
hctx.GetLogger().Warnf("failed to execute custom command named %v (stdout=%#v, stderr=%#v)", cc.ColumnName, stdout.String(), stderr.String())
|
||||
}
|
||||
ccv := data.CustomColumn{
|
||||
Name: cc.ColumnName,
|
||||
Val: strings.TrimSpace(stdout.String()),
|
||||
}
|
||||
ccs = append(ccs, ccv)
|
||||
}
|
||||
return ccs, nil
|
||||
}
|
||||
|
||||
func buildRegexFromTimeFormat(timeFormat string) string {
|
||||
expectedRegex := ""
|
||||
lastCharWasPercent := false
|
||||
for _, char := range timeFormat {
|
||||
if lastCharWasPercent {
|
||||
if char == '%' {
|
||||
expectedRegex += regexp.QuoteMeta(string(char))
|
||||
lastCharWasPercent = false
|
||||
continue
|
||||
} else if char == 't' {
|
||||
expectedRegex += "\t"
|
||||
} else if char == 'F' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%Y-%m-%d")
|
||||
} else if char == 'Y' {
|
||||
expectedRegex += "[0-9]{4}"
|
||||
} else if char == 'G' {
|
||||
expectedRegex += "[0-9]{4}"
|
||||
} else if char == 'g' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'C' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'u' || char == 'w' {
|
||||
expectedRegex += "[0-9]"
|
||||
} else if char == 'm' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'd' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'D' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%m/%d/%y")
|
||||
} else if char == 'T' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%H:%M:%S")
|
||||
} else if char == 'H' || char == 'I' || char == 'U' || char == 'V' || char == 'W' || char == 'y' || char == 'Y' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'M' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'j' {
|
||||
expectedRegex += "[0-9]{3}"
|
||||
} else if char == 'S' || char == 'm' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'c' {
|
||||
// Note: Specific to the POSIX locale
|
||||
expectedRegex += buildRegexFromTimeFormat("%a %b %e %H:%M:%S %Y")
|
||||
} else if char == 'a' {
|
||||
// Note: Specific to the POSIX locale
|
||||
expectedRegex += "(Sun|Mon|Tue|Wed|Thu|Fri|Sat)"
|
||||
} else if char == 'b' || char == 'h' {
|
||||
// Note: Specific to the POSIX locale
|
||||
expectedRegex += "(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
|
||||
} else if char == 'e' || char == 'k' || char == 'l' {
|
||||
expectedRegex += "[0-9 ]{2}"
|
||||
} else if char == 'n' {
|
||||
expectedRegex += "\n"
|
||||
} else if char == 'p' {
|
||||
expectedRegex += "(AM|PM)"
|
||||
} else if char == 'P' {
|
||||
expectedRegex += "(am|pm)"
|
||||
} else if char == 's' {
|
||||
expectedRegex += "\\d+"
|
||||
} else if char == 'z' {
|
||||
expectedRegex += "[+-][0-9]{4}"
|
||||
} else if char == 'r' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%I:%M:%S %p")
|
||||
} else if char == 'R' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%H:%M")
|
||||
} else if char == 'x' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%m/%d/%y")
|
||||
} else if char == 'X' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%H:%M:%S")
|
||||
} else {
|
||||
panic(fmt.Sprintf("buildRegexFromTimeFormat doesn't support %%%v, please open a bug against github.com/ddworken/hishtory", string(char)))
|
||||
}
|
||||
} else if char != '%' {
|
||||
expectedRegex += regexp.QuoteMeta(string(char))
|
||||
}
|
||||
lastCharWasPercent = false
|
||||
if char == '%' {
|
||||
lastCharWasPercent = true
|
||||
}
|
||||
}
|
||||
return expectedRegex
|
||||
}
|
||||
|
||||
func maybeSkipBashHistTimePrefix(cmdLine string) (string, error) {
|
||||
format := os.Getenv("HISTTIMEFORMAT")
|
||||
if format == "" {
|
||||
return cmdLine, nil
|
||||
}
|
||||
re, err := regexp.Compile("^" + buildRegexFromTimeFormat(format))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse regex for HISTTIMEFORMAT variable: %v", err)
|
||||
}
|
||||
return re.ReplaceAllLiteralString(cmdLine, ""), nil
|
||||
}
|
||||
|
||||
func parseCrossPlatformInt(data string) (int64, error) {
|
||||
data = strings.TrimSuffix(data, "N")
|
||||
return strconv.ParseInt(data, 10, 64)
|
||||
}
|
||||
|
||||
func getLastCommand(history string) (string, error) {
|
||||
split := strings.SplitN(strings.TrimSpace(history), " ", 2)
|
||||
if len(split) <= 1 {
|
||||
return "", fmt.Errorf("got unexpected bash history line: %#v, please open a bug at github.com/ddworken/hishtory", history)
|
||||
}
|
||||
split = strings.SplitN(split[1], " ", 2)
|
||||
if len(split) <= 1 {
|
||||
return "", fmt.Errorf("got unexpected bash history line: %#v, please open a bug at github.com/ddworken/hishtory", history)
|
||||
}
|
||||
return split[1], nil
|
||||
}
|
||||
|
||||
func shouldSkipHiddenCommand(ctx *context.Context, historyLine string) (bool, error) {
|
||||
config := hctx.GetConf(ctx)
|
||||
if config.LastSavedHistoryLine == historyLine {
|
||||
return true, nil
|
||||
}
|
||||
config.LastSavedHistoryLine = historyLine
|
||||
err := hctx.SetConfig(config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func getCwd(ctx *context.Context) (string, string, error) {
|
||||
cwd, err := getCwdWithoutSubstitution()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get cwd for last command: %v", err)
|
||||
}
|
||||
homedir := hctx.GetHome(ctx)
|
||||
if cwd == homedir {
|
||||
return "~/", homedir, nil
|
||||
}
|
||||
if strings.HasPrefix(cwd, homedir) {
|
||||
return strings.Replace(cwd, homedir, "~", 1), homedir, nil
|
||||
}
|
||||
return cwd, homedir, nil
|
||||
}
|
||||
|
||||
func getCwdWithoutSubstitution() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
return cwd, nil
|
||||
}
|
||||
// Fall back to the syscall to see if that works, as an attempt to
|
||||
// fix github.com/ddworken/hishtory/issues/69
|
||||
if syscall.ImplementsGetwd {
|
||||
cwd, err = syscall.Getwd()
|
||||
if err == nil {
|
||||
return cwd, nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
227
client/cmd/saveHistoryEntry_test.go
Normal file
227
client/cmd/saveHistoryEntry_test.go
Normal file
@ -0,0 +1,227 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ddworken/hishtory/client/hctx"
|
||||
"github.com/ddworken/hishtory/client/lib"
|
||||
"github.com/ddworken/hishtory/shared/testutils"
|
||||
)
|
||||
|
||||
func TestBuildHistoryEntry(t *testing.T) {
|
||||
defer testutils.BackupAndRestore(t)()
|
||||
defer testutils.RunTestServer()()
|
||||
testutils.Check(t, lib.Setup("", false))
|
||||
|
||||
// Test building an actual entry for bash
|
||||
entry, err := buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", " 123 ls /foo ", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry.ExitCode != 120 {
|
||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||
}
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve user: %v", err)
|
||||
}
|
||||
if entry.LocalUsername != user.Username {
|
||||
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
|
||||
}
|
||||
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
|
||||
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
|
||||
}
|
||||
if !strings.HasPrefix(entry.HomeDirectory, "/") {
|
||||
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
|
||||
}
|
||||
if entry.Command != "ls /foo" {
|
||||
t.Fatalf("history entry has unexpected command: %v", entry.Command)
|
||||
}
|
||||
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
|
||||
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if entry.StartTime.Unix() != 1641774958 {
|
||||
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
|
||||
}
|
||||
|
||||
// Test building an entry for zsh
|
||||
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", "ls /foo\n", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry.ExitCode != 120 {
|
||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||
}
|
||||
if entry.LocalUsername != user.Username {
|
||||
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
|
||||
}
|
||||
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
|
||||
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
|
||||
}
|
||||
if !strings.HasPrefix(entry.HomeDirectory, "/") {
|
||||
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
|
||||
}
|
||||
if entry.Command != "ls /foo" {
|
||||
t.Fatalf("history entry has unexpected command: %v", entry.Command)
|
||||
}
|
||||
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
|
||||
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if entry.StartTime.Unix() != 1641774958 {
|
||||
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
|
||||
}
|
||||
|
||||
// Test building an entry for fish
|
||||
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "fish", "120", "ls /foo\n", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry.ExitCode != 120 {
|
||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||
}
|
||||
if entry.LocalUsername != user.Username {
|
||||
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
|
||||
}
|
||||
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
|
||||
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
|
||||
}
|
||||
if !strings.HasPrefix(entry.HomeDirectory, "/") {
|
||||
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
|
||||
}
|
||||
if entry.Command != "ls /foo" {
|
||||
t.Fatalf("history entry has unexpected command: %v", entry.Command)
|
||||
}
|
||||
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
|
||||
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if entry.StartTime.Unix() != 1641774958 {
|
||||
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
|
||||
}
|
||||
|
||||
// Test building an entry that is empty, and thus not saved
|
||||
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", " \n", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry != nil {
|
||||
t.Fatalf("expected history entry to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) {
|
||||
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
|
||||
defer testutils.BackupAndRestore(t)()
|
||||
defer testutils.RunTestServer()()
|
||||
testutils.Check(t, lib.Setup("", false))
|
||||
|
||||
testcases := []struct {
|
||||
input, histtimeformat, expectedCommand string
|
||||
}{
|
||||
{" 123 ls /foo ", "", "ls /foo"},
|
||||
{" 2389 [2022-09-28 04:38:32 +0000] echo", "", "[2022-09-28 04:38:32 +0000] echo"},
|
||||
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[%F %T %z] ", "echo"},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
conf := hctx.GetConf(hctx.MakeContext())
|
||||
conf.LastSavedHistoryLine = ""
|
||||
testutils.Check(t, hctx.SetConfig(conf))
|
||||
|
||||
os.Setenv("HISTTIMEFORMAT", tc.histtimeformat)
|
||||
entry, err := buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", tc.input, "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry == nil {
|
||||
t.Fatalf("entry is unexpectedly nil")
|
||||
}
|
||||
if entry.Command != tc.expectedCommand {
|
||||
t.Fatalf("buildHistoryEntry(%#v) returned %#v (expected=%#v)", tc.input, entry.Command, tc.expectedCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCrossPlatformInt(t *testing.T) {
|
||||
res, err := parseCrossPlatformInt("123")
|
||||
testutils.Check(t, err)
|
||||
if res != 123 {
|
||||
t.Fatalf("failed to parse cross platform int %d", res)
|
||||
}
|
||||
res, err = parseCrossPlatformInt("123N")
|
||||
testutils.Check(t, err)
|
||||
if res != 123 {
|
||||
t.Fatalf("failed to parse cross platform int %d", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRegexFromTimeFormat(t *testing.T) {
|
||||
testcases := []struct {
|
||||
formatString, regex string
|
||||
}{
|
||||
{"%Y ", "[0-9]{4} "},
|
||||
{"%F %T ", "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} "},
|
||||
{"%F%T", "[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
|
||||
{"%%", "%"},
|
||||
{"%%%%", "%%"},
|
||||
{"%%%Y", "%[0-9]{4}"},
|
||||
{"%%%F%T", "%[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
if regex := buildRegexFromTimeFormat(tc.formatString); regex != tc.regex {
|
||||
t.Fatalf("building a regex for %#v returned %#v (expected=%#v)", tc.formatString, regex, tc.regex)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestGetLastCommand(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input, expectedOutput string
|
||||
}{
|
||||
{" 0 ls", "ls"},
|
||||
{" 33 ls", "ls"},
|
||||
{" 33 ls --aaaa foo bar ", "ls --aaaa foo bar"},
|
||||
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[2022-09-28 04:38:32 +0000] echo"},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
actualOutput, err := getLastCommand(tc.input)
|
||||
testutils.Check(t, err)
|
||||
if actualOutput != tc.expectedOutput {
|
||||
t.Fatalf("getLastCommand(%#v) returned %#v (expected=%#v)", tc.input, actualOutput, tc.expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeSkipBashHistTimePrefix(t *testing.T) {
|
||||
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
|
||||
|
||||
testcases := []struct {
|
||||
env, cmdLine, expected string
|
||||
}{
|
||||
{"%F %T ", "2019-07-12 13:02:31 sudo apt update", "sudo apt update"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls a b", "ls a b"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls a ", "ls a "},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls a", "ls a"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls", "ls"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls -Slah", "ls -Slah"},
|
||||
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
|
||||
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
|
||||
{"%Y", "2019ls -Slah", "ls -Slah"},
|
||||
{"%Y%Y", "20192020ls -Slah", "ls -Slah"},
|
||||
{"%Y%Y", "20192020ls -Slah20192020", "ls -Slah20192020"},
|
||||
{"", "ls -Slah", "ls -Slah"},
|
||||
{"[%F %T] ", "[2019-07-12 13:02:31] sudo apt update", "sudo apt update"},
|
||||
{"[%F a %T] ", "[2019-07-12 a 13:02:31] sudo apt update", "sudo apt update"},
|
||||
{"aaa ", "aaa sudo apt update", "sudo apt update"},
|
||||
{"%c ", "Sun Aug 19 02:56:02 2012 sudo apt update", "sudo apt update"},
|
||||
{"%c ", "Sun Aug 19 02:56:02 2012 ls", "ls"},
|
||||
{"[%c] ", "[Sun Aug 19 02:56:02 2012] ls", "ls"},
|
||||
{"[%c %t] ", "[Sun Aug 19 02:56:02 2012 ] ls", "ls"},
|
||||
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]ls", "ls"},
|
||||
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]foo", "foo"},
|
||||
{"[%c %t", "[Sun Aug 19 02:56:02 2012 foo", "foo"},
|
||||
{"[%F %T %z]", "[2022-09-28 04:17:06 +0000]foo", "foo"},
|
||||
{"[%F %T %z] ", "[2022-09-28 04:17:06 +0000] foo", "foo"},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
os.Setenv("HISTTIMEFORMAT", tc.env)
|
||||
stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine)
|
||||
testutils.Check(t, err)
|
||||
if stripped != tc.expected {
|
||||
t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine)
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@ -52,316 +51,6 @@ var GitCommit string = "Unknown"
|
||||
// 256KB ought to be enough for any reasonable cmd
|
||||
var maxSupportedLineLengthForImport = 256_000
|
||||
|
||||
func getCwd(ctx *context.Context) (string, string, error) {
|
||||
cwd, err := getCwdWithoutSubstitution()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get cwd for last command: %v", err)
|
||||
}
|
||||
homedir := hctx.GetHome(ctx)
|
||||
if cwd == homedir {
|
||||
return "~/", homedir, nil
|
||||
}
|
||||
if strings.HasPrefix(cwd, homedir) {
|
||||
return strings.Replace(cwd, homedir, "~", 1), homedir, nil
|
||||
}
|
||||
return cwd, homedir, nil
|
||||
}
|
||||
|
||||
func getCwdWithoutSubstitution() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
return cwd, nil
|
||||
}
|
||||
// Fall back to the syscall to see if that works, as an attempt to
|
||||
// fix github.com/ddworken/hishtory/issues/69
|
||||
if syscall.ImplementsGetwd {
|
||||
cwd, err = syscall.Getwd()
|
||||
if err == nil {
|
||||
return cwd, nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func BuildPreArgsHistoryEntry(ctx *context.Context) (*data.HistoryEntry, error) {
|
||||
var entry data.HistoryEntry
|
||||
|
||||
// user
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.LocalUsername = user.Username
|
||||
|
||||
// cwd and homedir
|
||||
cwd, homedir, err := getCwd(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.CurrentWorkingDirectory = cwd
|
||||
entry.HomeDirectory = homedir
|
||||
|
||||
// hostname
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.Hostname = hostname
|
||||
|
||||
// device ID
|
||||
config := hctx.GetConf(ctx)
|
||||
entry.DeviceId = config.DeviceId
|
||||
|
||||
// custom columns
|
||||
cc, err := buildCustomColumns(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.CustomColumns = cc
|
||||
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
func BuildHistoryEntry(ctx *context.Context, args []string) (*data.HistoryEntry, error) {
|
||||
if len(args) < 6 {
|
||||
hctx.GetLogger().Warnf("BuildHistoryEntry called with args=%#v, which has too few entries! This can happen in specific edge cases for newly opened terminals and is likely not a problem.", args)
|
||||
return nil, nil
|
||||
}
|
||||
shell := args[2]
|
||||
|
||||
entry, err := BuildPreArgsHistoryEntry(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// exitCode
|
||||
exitCode, err := strconv.Atoi(args[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
entry.ExitCode = exitCode
|
||||
|
||||
// start time
|
||||
seconds, err := ParseCrossPlatformInt(args[5])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse start time %s as int: %v", args[5], err)
|
||||
}
|
||||
entry.StartTime = time.Unix(seconds, 0)
|
||||
|
||||
// end time
|
||||
entry.EndTime = time.Now()
|
||||
|
||||
// command
|
||||
if shell == "bash" {
|
||||
cmd, err := getLastCommand(args[4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build history entry: %v", err)
|
||||
}
|
||||
shouldBeSkipped, err := shouldSkipHiddenCommand(ctx, args[4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if command was hidden: %v", err)
|
||||
}
|
||||
if shouldBeSkipped || strings.HasPrefix(cmd, " ") {
|
||||
// Don't save commands that start with a space
|
||||
return nil, nil
|
||||
}
|
||||
cmd, err = maybeSkipBashHistTimePrefix(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.Command = cmd
|
||||
} else if shell == "zsh" || shell == "fish" {
|
||||
cmd := TrimTrailingWhitespace(args[4])
|
||||
if strings.HasPrefix(cmd, " ") {
|
||||
// Don't save commands that start with a space
|
||||
return nil, nil
|
||||
}
|
||||
entry.Command = cmd
|
||||
} else {
|
||||
return nil, fmt.Errorf("tried to save a hishtory entry from an unsupported shell=%#v", shell)
|
||||
}
|
||||
if strings.TrimSpace(entry.Command) == "" {
|
||||
// Skip recording empty commands where the user just hits enter in their terminal
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func TrimTrailingWhitespace(s string) string {
|
||||
return strings.TrimSuffix(strings.TrimSuffix(s, "\n"), " ")
|
||||
}
|
||||
|
||||
func buildCustomColumns(ctx *context.Context) (data.CustomColumns, error) {
|
||||
ccs := data.CustomColumns{}
|
||||
config := hctx.GetConf(ctx)
|
||||
for _, cc := range config.CustomColumns {
|
||||
cmd := exec.Command("bash", "-c", cc.ColumnCommand)
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute custom command named %v (stdout=%#v, stderr=%#v)", cc.ColumnName, stdout.String(), stderr.String())
|
||||
}
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
// Log a warning, but don't crash. This way commands can exit with a different status and still work.
|
||||
hctx.GetLogger().Warnf("failed to execute custom command named %v (stdout=%#v, stderr=%#v)", cc.ColumnName, stdout.String(), stderr.String())
|
||||
}
|
||||
ccv := data.CustomColumn{
|
||||
Name: cc.ColumnName,
|
||||
Val: strings.TrimSpace(stdout.String()),
|
||||
}
|
||||
ccs = append(ccs, ccv)
|
||||
}
|
||||
return ccs, nil
|
||||
}
|
||||
|
||||
func stripZshWeirdness(cmd string) string {
|
||||
// Zsh has this weird behavior where sometimes commands are saved in the hishtory file
|
||||
// with a weird prefix. I've never been able to figure out why this happens, but we
|
||||
// can at least strip it.
|
||||
firstCommandBugRegex := regexp.MustCompile(`: \d+:\d;(.*)`)
|
||||
matches := firstCommandBugRegex.FindStringSubmatch(cmd)
|
||||
if len(matches) == 2 {
|
||||
return matches[1]
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func isBashWeirdness(cmd string) bool {
|
||||
// Bash has this weird behavior where the it has entries like `#1664342754` in the
|
||||
// history file. We want to skip these.
|
||||
firstCommandBugRegex := regexp.MustCompile(`^#\d+\s+$`)
|
||||
return firstCommandBugRegex.MatchString(cmd)
|
||||
}
|
||||
|
||||
func buildRegexFromTimeFormat(timeFormat string) string {
|
||||
expectedRegex := ""
|
||||
lastCharWasPercent := false
|
||||
for _, char := range timeFormat {
|
||||
if lastCharWasPercent {
|
||||
if char == '%' {
|
||||
expectedRegex += regexp.QuoteMeta(string(char))
|
||||
lastCharWasPercent = false
|
||||
continue
|
||||
} else if char == 't' {
|
||||
expectedRegex += "\t"
|
||||
} else if char == 'F' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%Y-%m-%d")
|
||||
} else if char == 'Y' {
|
||||
expectedRegex += "[0-9]{4}"
|
||||
} else if char == 'G' {
|
||||
expectedRegex += "[0-9]{4}"
|
||||
} else if char == 'g' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'C' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'u' || char == 'w' {
|
||||
expectedRegex += "[0-9]"
|
||||
} else if char == 'm' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'd' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'D' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%m/%d/%y")
|
||||
} else if char == 'T' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%H:%M:%S")
|
||||
} else if char == 'H' || char == 'I' || char == 'U' || char == 'V' || char == 'W' || char == 'y' || char == 'Y' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'M' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'j' {
|
||||
expectedRegex += "[0-9]{3}"
|
||||
} else if char == 'S' || char == 'm' {
|
||||
expectedRegex += "[0-9]{2}"
|
||||
} else if char == 'c' {
|
||||
// Note: Specific to the POSIX locale
|
||||
expectedRegex += buildRegexFromTimeFormat("%a %b %e %H:%M:%S %Y")
|
||||
} else if char == 'a' {
|
||||
// Note: Specific to the POSIX locale
|
||||
expectedRegex += "(Sun|Mon|Tue|Wed|Thu|Fri|Sat)"
|
||||
} else if char == 'b' || char == 'h' {
|
||||
// Note: Specific to the POSIX locale
|
||||
expectedRegex += "(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
|
||||
} else if char == 'e' || char == 'k' || char == 'l' {
|
||||
expectedRegex += "[0-9 ]{2}"
|
||||
} else if char == 'n' {
|
||||
expectedRegex += "\n"
|
||||
} else if char == 'p' {
|
||||
expectedRegex += "(AM|PM)"
|
||||
} else if char == 'P' {
|
||||
expectedRegex += "(am|pm)"
|
||||
} else if char == 's' {
|
||||
expectedRegex += "\\d+"
|
||||
} else if char == 'z' {
|
||||
expectedRegex += "[+-][0-9]{4}"
|
||||
} else if char == 'r' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%I:%M:%S %p")
|
||||
} else if char == 'R' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%H:%M")
|
||||
} else if char == 'x' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%m/%d/%y")
|
||||
} else if char == 'X' {
|
||||
expectedRegex += buildRegexFromTimeFormat("%H:%M:%S")
|
||||
} else {
|
||||
panic(fmt.Sprintf("buildRegexFromTimeFormat doesn't support %%%v, please open a bug against github.com/ddworken/hishtory", string(char)))
|
||||
}
|
||||
} else if char != '%' {
|
||||
expectedRegex += regexp.QuoteMeta(string(char))
|
||||
}
|
||||
lastCharWasPercent = false
|
||||
if char == '%' {
|
||||
lastCharWasPercent = true
|
||||
}
|
||||
}
|
||||
return expectedRegex
|
||||
}
|
||||
|
||||
func maybeSkipBashHistTimePrefix(cmdLine string) (string, error) {
|
||||
format := os.Getenv("HISTTIMEFORMAT")
|
||||
if format == "" {
|
||||
return cmdLine, nil
|
||||
}
|
||||
re, err := regexp.Compile("^" + buildRegexFromTimeFormat(format))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse regex for HISTTIMEFORMAT variable: %v", err)
|
||||
}
|
||||
return re.ReplaceAllLiteralString(cmdLine, ""), nil
|
||||
}
|
||||
|
||||
func ParseCrossPlatformInt(data string) (int64, error) {
|
||||
data = strings.TrimSuffix(data, "N")
|
||||
return strconv.ParseInt(data, 10, 64)
|
||||
}
|
||||
|
||||
func getLastCommand(history string) (string, error) {
|
||||
split := strings.SplitN(strings.TrimSpace(history), " ", 2)
|
||||
if len(split) <= 1 {
|
||||
return "", fmt.Errorf("got unexpected bash history line: %#v, please open a bug at github.com/ddworken/hishtory", history)
|
||||
}
|
||||
split = strings.SplitN(split[1], " ", 2)
|
||||
if len(split) <= 1 {
|
||||
return "", fmt.Errorf("got unexpected bash history line: %#v, please open a bug at github.com/ddworken/hishtory", history)
|
||||
}
|
||||
return split[1], nil
|
||||
}
|
||||
|
||||
func shouldSkipHiddenCommand(ctx *context.Context, historyLine string) (bool, error) {
|
||||
config := hctx.GetConf(ctx)
|
||||
if config.LastSavedHistoryLine == historyLine {
|
||||
return true, nil
|
||||
}
|
||||
config.LastSavedHistoryLine = historyLine
|
||||
err := hctx.SetConfig(config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func Setup(userSecret string, isOffline bool) error {
|
||||
if userSecret == "" {
|
||||
userSecret = uuid.Must(uuid.NewRandom()).String()
|
||||
@ -532,6 +221,25 @@ func CheckFatalError(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func stripZshWeirdness(cmd string) string {
|
||||
// Zsh has this weird behavior where sometimes commands are saved in the hishtory file
|
||||
// with a weird prefix. I've never been able to figure out why this happens, but we
|
||||
// can at least strip it.
|
||||
firstCommandBugRegex := regexp.MustCompile(`: \d+:\d;(.*)`)
|
||||
matches := firstCommandBugRegex.FindStringSubmatch(cmd)
|
||||
if len(matches) == 2 {
|
||||
return matches[1]
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func isBashWeirdness(cmd string) bool {
|
||||
// Bash has this weird behavior where the it has entries like `#1664342754` in the
|
||||
// history file. We want to skip these.
|
||||
firstCommandBugRegex := regexp.MustCompile(`^#\d+\s+$`)
|
||||
return firstCommandBugRegex.MatchString(cmd)
|
||||
}
|
||||
|
||||
func ImportHistory(ctx *context.Context, shouldReadStdin, force bool) (int, error) {
|
||||
config := hctx.GetConf(ctx)
|
||||
if config.HaveCompletedInitialImport && !force {
|
||||
|
@ -2,10 +2,8 @@ package lib
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -62,129 +60,6 @@ func TestSetupOffline(t *testing.T) {
|
||||
t.Fatalf("hishtory config should have been offline, actual=%#v", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHistoryEntry(t *testing.T) {
|
||||
defer testutils.BackupAndRestore(t)()
|
||||
defer testutils.RunTestServer()()
|
||||
testutils.Check(t, Setup("", false))
|
||||
|
||||
// Test building an actual entry for bash
|
||||
entry, err := BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", " 123 ls /foo ", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry.ExitCode != 120 {
|
||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||
}
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve user: %v", err)
|
||||
}
|
||||
if entry.LocalUsername != user.Username {
|
||||
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
|
||||
}
|
||||
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
|
||||
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
|
||||
}
|
||||
if !strings.HasPrefix(entry.HomeDirectory, "/") {
|
||||
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
|
||||
}
|
||||
if entry.Command != "ls /foo" {
|
||||
t.Fatalf("history entry has unexpected command: %v", entry.Command)
|
||||
}
|
||||
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
|
||||
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if entry.StartTime.Unix() != 1641774958 {
|
||||
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
|
||||
}
|
||||
|
||||
// Test building an entry for zsh
|
||||
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", "ls /foo\n", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry.ExitCode != 120 {
|
||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||
}
|
||||
if entry.LocalUsername != user.Username {
|
||||
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
|
||||
}
|
||||
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
|
||||
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
|
||||
}
|
||||
if !strings.HasPrefix(entry.HomeDirectory, "/") {
|
||||
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
|
||||
}
|
||||
if entry.Command != "ls /foo" {
|
||||
t.Fatalf("history entry has unexpected command: %v", entry.Command)
|
||||
}
|
||||
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
|
||||
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if entry.StartTime.Unix() != 1641774958 {
|
||||
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
|
||||
}
|
||||
|
||||
// Test building an entry for fish
|
||||
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "fish", "120", "ls /foo\n", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry.ExitCode != 120 {
|
||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||
}
|
||||
if entry.LocalUsername != user.Username {
|
||||
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
|
||||
}
|
||||
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
|
||||
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
|
||||
}
|
||||
if !strings.HasPrefix(entry.HomeDirectory, "/") {
|
||||
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
|
||||
}
|
||||
if entry.Command != "ls /foo" {
|
||||
t.Fatalf("history entry has unexpected command: %v", entry.Command)
|
||||
}
|
||||
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
|
||||
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
|
||||
}
|
||||
if entry.StartTime.Unix() != 1641774958 {
|
||||
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
|
||||
}
|
||||
|
||||
// Test building an entry that is empty, and thus not saved
|
||||
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", " \n", "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry != nil {
|
||||
t.Fatalf("expected history entry to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) {
|
||||
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
|
||||
defer testutils.BackupAndRestore(t)()
|
||||
defer testutils.RunTestServer()()
|
||||
testutils.Check(t, Setup("", false))
|
||||
|
||||
testcases := []struct {
|
||||
input, histtimeformat, expectedCommand string
|
||||
}{
|
||||
{" 123 ls /foo ", "", "ls /foo"},
|
||||
{" 2389 [2022-09-28 04:38:32 +0000] echo", "", "[2022-09-28 04:38:32 +0000] echo"},
|
||||
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[%F %T %z] ", "echo"},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
conf := hctx.GetConf(hctx.MakeContext())
|
||||
conf.LastSavedHistoryLine = ""
|
||||
testutils.Check(t, hctx.SetConfig(conf))
|
||||
|
||||
os.Setenv("HISTTIMEFORMAT", tc.histtimeformat)
|
||||
entry, err := BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", tc.input, "1641774958"})
|
||||
testutils.Check(t, err)
|
||||
if entry == nil {
|
||||
t.Fatalf("entry is unexpectedly nil")
|
||||
}
|
||||
if entry.Command != tc.expectedCommand {
|
||||
t.Fatalf("BuildHistoryEntry(%#v) returned %#v (expected=%#v)", tc.input, entry.Command, tc.expectedCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersist(t *testing.T) {
|
||||
defer testutils.BackupAndRestore(t)()
|
||||
testutils.Check(t, hctx.InitConfig())
|
||||
@ -317,99 +192,6 @@ func TestAddToDbIfNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCrossPlatformInt(t *testing.T) {
|
||||
res, err := ParseCrossPlatformInt("123")
|
||||
testutils.Check(t, err)
|
||||
if res != 123 {
|
||||
t.Fatalf("failed to parse cross platform int %d", res)
|
||||
}
|
||||
res, err = ParseCrossPlatformInt("123N")
|
||||
testutils.Check(t, err)
|
||||
if res != 123 {
|
||||
t.Fatalf("failed to parse cross platform int %d", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRegexFromTimeFormat(t *testing.T) {
|
||||
testcases := []struct {
|
||||
formatString, regex string
|
||||
}{
|
||||
{"%Y ", "[0-9]{4} "},
|
||||
{"%F %T ", "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} "},
|
||||
{"%F%T", "[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
|
||||
{"%%", "%"},
|
||||
{"%%%%", "%%"},
|
||||
{"%%%Y", "%[0-9]{4}"},
|
||||
{"%%%F%T", "%[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
if regex := buildRegexFromTimeFormat(tc.formatString); regex != tc.regex {
|
||||
t.Fatalf("building a regex for %#v returned %#v (expected=%#v)", tc.formatString, regex, tc.regex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLastCommand(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input, expectedOutput string
|
||||
}{
|
||||
{" 0 ls", "ls"},
|
||||
{" 33 ls", "ls"},
|
||||
{" 33 ls --aaaa foo bar ", "ls --aaaa foo bar"},
|
||||
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[2022-09-28 04:38:32 +0000] echo"},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
actualOutput, err := getLastCommand(tc.input)
|
||||
testutils.Check(t, err)
|
||||
if actualOutput != tc.expectedOutput {
|
||||
t.Fatalf("getLastCommand(%#v) returned %#v (expected=%#v)", tc.input, actualOutput, tc.expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeSkipBashHistTimePrefix(t *testing.T) {
|
||||
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
|
||||
|
||||
testcases := []struct {
|
||||
env, cmdLine, expected string
|
||||
}{
|
||||
{"%F %T ", "2019-07-12 13:02:31 sudo apt update", "sudo apt update"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls a b", "ls a b"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls a ", "ls a "},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls a", "ls a"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls", "ls"},
|
||||
{"%F %T ", "2019-07-12 13:02:31 ls -Slah", "ls -Slah"},
|
||||
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
|
||||
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
|
||||
{"%Y", "2019ls -Slah", "ls -Slah"},
|
||||
{"%Y%Y", "20192020ls -Slah", "ls -Slah"},
|
||||
{"%Y%Y", "20192020ls -Slah20192020", "ls -Slah20192020"},
|
||||
{"", "ls -Slah", "ls -Slah"},
|
||||
{"[%F %T] ", "[2019-07-12 13:02:31] sudo apt update", "sudo apt update"},
|
||||
{"[%F a %T] ", "[2019-07-12 a 13:02:31] sudo apt update", "sudo apt update"},
|
||||
{"aaa ", "aaa sudo apt update", "sudo apt update"},
|
||||
{"%c ", "Sun Aug 19 02:56:02 2012 sudo apt update", "sudo apt update"},
|
||||
{"%c ", "Sun Aug 19 02:56:02 2012 ls", "ls"},
|
||||
{"[%c] ", "[Sun Aug 19 02:56:02 2012] ls", "ls"},
|
||||
{"[%c %t] ", "[Sun Aug 19 02:56:02 2012 ] ls", "ls"},
|
||||
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]ls", "ls"},
|
||||
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]foo", "foo"},
|
||||
{"[%c %t", "[Sun Aug 19 02:56:02 2012 foo", "foo"},
|
||||
{"[%F %T %z]", "[2022-09-28 04:17:06 +0000]foo", "foo"},
|
||||
{"[%F %T %z] ", "[2022-09-28 04:17:06 +0000] foo", "foo"},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
os.Setenv("HISTTIMEFORMAT", tc.env)
|
||||
stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine)
|
||||
testutils.Check(t, err)
|
||||
if stripped != tc.expected {
|
||||
t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunks(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input []int
|
||||
|
Loading…
Reference in New Issue
Block a user