1package main23import (4 "bufio"5 "bytes"6 "fmt"7 "os"8 "regexp"9 "sort"10 "strconv"11)1213var commandStartRegex = regexp.MustCompile(`^: (\d+):(\d+);(.*)$`)1415type HistoryEntry struct {16 Timestamp int3217 Runtime int3218 Command string19}2021// ScanLines is a split function for a Scanner that returns each line of22// text, stripped of any trailing end-of-line marker. The returned line may23// be empty. The end-of-line marker is one optional carriage return followed24// by one mandatory newline. In regular expression notation, it is `\r?\n`.25// The last non-empty line of input will be returned even if it has no26// newline.27func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {28 if atEOF && len(data) == 0 {29 return 0, nil, nil30 }3132 if i := bytes.IndexByte(data, '\n'); i >= 0 {33 // We have a full newline-terminated line.34 return i + 1, data[0:i], nil35 }36 // If we're at EOF, we have a final, non-terminated line. Return it.37 if atEOF {38 return len(data), data, nil39 }40 // Request more data.41 return 0, nil, nil42}4344func readFile(filename string) ([]HistoryEntry, error) {45 f, err := os.Open(filename)46 if err != nil {47 return nil, err48 }4950 s := bufio.NewScanner(f)51 s.Split(ScanLines)52 found := false53 commands := make([]HistoryEntry, 0)54 var currentCommand HistoryEntry55 for s.Scan() {56 line := s.Text()5758 m := commandStartRegex.FindStringSubmatch(line)59 if len(m) == 0 {60 if !found {61 return nil, fmt.Errorf("Found non-entry line as first line")62 }63 currentCommand.Command += "\n" + line64 } else {65 if found {66 commands = append(commands, currentCommand)67 }68 found = true69 currentCommand = HistoryEntry{}7071 timestamp, err := strconv.Atoi(m[1])72 if err != nil {73 return nil, err74 }7576 runtime, err := strconv.Atoi(m[2])77 if err != nil {78 return nil, err79 }8081 currentCommand.Timestamp = int32(timestamp)82 currentCommand.Runtime = int32(runtime)83 currentCommand.Command = m[3]84 }85 }8687 if err := s.Err(); err != nil {88 return nil, err89 }9091 // Grab the last one92 if found {93 commands = append(commands, currentCommand)94 }9596 sort.SliceStable(commands, func(i, j int) bool {97 return commands[i].Timestamp < commands[j].Timestamp98 })99100 return commands, nil101}102103func Merge(one, two []HistoryEntry) []HistoryEntry {104 ret := make([]HistoryEntry, 0)105106 oneIndex := 0107 twoIndex := 0108109 for oneIndex < len(one) && twoIndex < len(two) {110 if one[oneIndex] == two[twoIndex] {111 ret = append(ret, one[oneIndex])112 oneIndex++113 twoIndex++114 } else if one[oneIndex].Timestamp < two[twoIndex].Timestamp {115 ret = append(ret, one[oneIndex])116 oneIndex++117 } else if two[twoIndex].Timestamp < one[oneIndex].Timestamp {118 ret = append(ret, two[twoIndex])119 twoIndex++120 } else if one[oneIndex].Timestamp == two[twoIndex].Timestamp {121 ret = append(ret, one[oneIndex], two[twoIndex])122 oneIndex++123 twoIndex++124 } else {125 panic("This should be impossible")126 }127 }128129 if oneIndex >= len(one) && twoIndex >= len(two) {130 return ret131 }132133 if oneIndex >= len(one) {134 ret = append(ret, two[twoIndex:]...)135 } else if twoIndex >= len(two) {136 ret = append(ret, one[oneIndex:]...)137 }138139 return ret140}141142func main() {143 if len(os.Args) < 2 {144 fmt.Println("usage: zsh_history_merge [FILES...]")145 os.Exit(1)146 }147148 fileCommands := make([][]HistoryEntry, len(os.Args)-1)149 for i, arg := range os.Args[1:] {150 commands, err := readFile(arg)151 if err != nil {152 panic(err)153 }154155 fileCommands[i] = commands156 }157158 mergeCommands := make([]HistoryEntry, 0)159 for _, commands := range fileCommands {160 mergeCommands = Merge(mergeCommands, commands)161 }162163 for _, entry := range mergeCommands {164 fmt.Printf(": %d:%d;%s\n", entry.Timestamp, entry.Runtime, entry.Command)165 }166}