Skip to content

Instantly share code, notes, and snippets.

Last active Jan 9, 2020
What would you like to do?
Simple script for concatenating minecraft log files
package main
import (
// Represents the filename used by Minecraft for the log files
var logFileRegex = regexp.MustCompile("^\\d{4}-\\d{2}-\\d{2}-(\\d+)\\.log\\.gz$")
// Check for invalid of help args
func checkArgs() {
if len(os.Args) > 2 {
printError("invalid number of arguments")
if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-h" {
fmt.Println("Usage: mc-log <logs-path>")
func main() {
path := os.Args[1]
logs, err := getLogMap(path)
if err != nil {
printError("error getting logs list: %s", err)
for dateStr, logFileNumbers := range logs {
if err := appendAndRemove(path, dateStr, logFileNumbers); err != nil {
printError("error appending logs from %s: %s", dateStr, err)
// getLogMap returns a map that represents the logs in the path provided.
// In this map, a string of the date like this "YYYY-MM-DD" is used as the key,
// and a slice of ints that represent each log file in order is the value.
// The ints of the value are got from the number represented by $ in the string "YYYY-MM-DD-$.log.gz"
func getLogMap(path string) (map[string][]int, error) {
logFiles, err := ioutil.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("error listing logs directory: %s", err)
logMap := make(map[string][]int, len(logFiles))
for _, logFile := range logFiles {
if !logFileRegex.MatchString(logFile.Name()) {
n, err := strconv.Atoi(logFileRegex.FindStringSubmatch(logFile.Name())[1])
if err != nil {
return nil, fmt.Errorf("cannot parse log number in file %s: %s", logFile.Name(), err)
logMap[logFile.Name()[:10]] = append(logMap[logFile.Name()[:10]], n)
return logMap, nil
// appendAndRemove takes the path to the log files, a string of the date of the logs (YYYY-MM-DD) and
// a slice of the log numbers (the one marked by the $ symbol in "YYYY-MM-DD-$.log.gz"), and
// appends all the logs to a file named YYYY-MM-DD.log
func appendAndRemove(logsPath, dateStr string, logNumbers []int) error {
destination := filepath.Join(logsPath, dateStr+".log")
fDestination, err := os.OpenFile(destination, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
return fmt.Errorf("error opening or creating file %s: %s", destination, err)
defer fDestination.Close()
bufFDestination := bufio.NewWriterSize(fDestination, 128*1024)
for _, logNumber := range logNumbers {
originPath := filepath.Join(logsPath, fmt.Sprintf("%s-%d.log.gz", dateStr, logNumber))
if err = decompressTo(originPath, bufFDestination); err != nil {
return err
if err = os.Remove(originPath); err != nil {
return fmt.Errorf("error removing file already saved (%s): %s", originPath, err)
if err = bufFDestination.Flush(); err != nil {
return fmt.Errorf("error flushing writer buffer in file %s: %s", destination, err)
if err = fDestination.Close(); err != nil {
return fmt.Errorf("error closing file %s: %s", destination, err)
return nil
// decompressTo decompress the file in the path provided and writes it to the writer provided.
func decompressTo(path string, w io.Writer) error {
fOrigin, err := os.Open(path)
if err != nil {
return fmt.Errorf("error opening file %s: %s", path, err)
defer fOrigin.Close()
bufFOrigin, err := gzip.NewReader(bufio.NewReaderSize(fOrigin, 128*1024))
if _, err = io.Copy(w, bufFOrigin); err != nil {
return fmt.Errorf("error decompressing and copying file %s: %s", path, err)
return nil
func printError(format string, a ...interface{}) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", a...)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment