zsh-history-merge

A very hacky merge script for zsh history from multiple files

git clone https://code.pdelong.com/zsh-history-merge.git

  1package main
  2
  3import (
  4	"bufio"
  5	"bytes"
  6	"fmt"
  7	"os"
  8	"regexp"
  9	"sort"
 10	"strconv"
 11)
 12
 13var commandStartRegex = regexp.MustCompile(`^: (\d+):(\d+);(.*)$`)
 14
 15type HistoryEntry struct {
 16	Timestamp int32
 17	Runtime   int32
 18	Command   string
 19}
 20
 21// ScanLines is a split function for a Scanner that returns each line of
 22// text, stripped of any trailing end-of-line marker. The returned line may
 23// be empty. The end-of-line marker is one optional carriage return followed
 24// 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 no
 26// newline.
 27func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
 28	if atEOF && len(data) == 0 {
 29		return 0, nil, nil
 30	}
 31
 32	if i := bytes.IndexByte(data, '\n'); i >= 0 {
 33		// We have a full newline-terminated line.
 34		return i + 1, data[0:i], nil
 35	}
 36	// If we're at EOF, we have a final, non-terminated line. Return it.
 37	if atEOF {
 38		return len(data), data, nil
 39	}
 40	// Request more data.
 41	return 0, nil, nil
 42}
 43
 44func readFile(filename string) ([]HistoryEntry, error) {
 45	f, err := os.Open(filename)
 46	if err != nil {
 47		return nil, err
 48	}
 49
 50	s := bufio.NewScanner(f)
 51	s.Split(ScanLines)
 52	found := false
 53	commands := make([]HistoryEntry, 0)
 54	var currentCommand HistoryEntry
 55	for s.Scan() {
 56		line := s.Text()
 57
 58		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" + line
 64		} else {
 65			if found {
 66				commands = append(commands, currentCommand)
 67			}
 68			found = true
 69			currentCommand = HistoryEntry{}
 70
 71			timestamp, err := strconv.Atoi(m[1])
 72			if err != nil {
 73				return nil, err
 74			}
 75
 76			runtime, err := strconv.Atoi(m[2])
 77			if err != nil {
 78				return nil, err
 79			}
 80
 81			currentCommand.Timestamp = int32(timestamp)
 82			currentCommand.Runtime = int32(runtime)
 83			currentCommand.Command = m[3]
 84		}
 85	}
 86
 87	if err := s.Err(); err != nil {
 88		return nil, err
 89	}
 90
 91	// Grab the last one
 92	if found {
 93		commands = append(commands, currentCommand)
 94	}
 95
 96	sort.SliceStable(commands, func(i, j int) bool {
 97		return commands[i].Timestamp < commands[j].Timestamp
 98	})
 99
100	return commands, nil
101}
102
103func Merge(one, two []HistoryEntry) []HistoryEntry {
104	ret := make([]HistoryEntry, 0)
105
106	oneIndex := 0
107	twoIndex := 0
108
109	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	}
128
129	if oneIndex >= len(one) && twoIndex >= len(two) {
130		return ret
131	}
132
133	if oneIndex >= len(one) {
134		ret = append(ret, two[twoIndex:]...)
135	} else if twoIndex >= len(two) {
136		ret = append(ret, one[oneIndex:]...)
137	}
138
139	return ret
140}
141
142func main() {
143	if len(os.Args) < 2 {
144		fmt.Println("usage: zsh_history_merge [FILES...]")
145		os.Exit(1)
146	}
147
148	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		}
154
155		fileCommands[i] = commands
156	}
157
158	mergeCommands := make([]HistoryEntry, 0)
159	for _, commands := range fileCommands {
160		mergeCommands = Merge(mergeCommands, commands)
161	}
162
163	for _, entry := range mergeCommands {
164		fmt.Printf(": %d:%d;%s\n", entry.Timestamp, entry.Runtime, entry.Command)
165	}
166}