Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
a9c60a3698 | |||
66bcf21d79 | |||
e847e5c3ce | |||
9a70c9696e |
27
cmd/log_format_test/main.go
Normal file
27
cmd/log_format_test/main.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"modify/logger"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize logger with DEBUG level
|
||||||
|
logger.Init(logger.LevelDebug)
|
||||||
|
|
||||||
|
// Test different log levels
|
||||||
|
logger.Info("This is an info message")
|
||||||
|
logger.Debug("This is a debug message")
|
||||||
|
logger.Warning("This is a warning message")
|
||||||
|
logger.Error("This is an error message")
|
||||||
|
logger.Trace("This is a trace message (not visible at DEBUG level)")
|
||||||
|
|
||||||
|
// Test with a goroutine
|
||||||
|
logger.SafeGo(func() {
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
logger.Info("Message from goroutine")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for goroutine to complete
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
@@ -1,12 +1,14 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -57,6 +59,7 @@ type Logger struct {
|
|||||||
useColors bool
|
useColors bool
|
||||||
callerOffset int
|
callerOffset int
|
||||||
defaultFields map[string]interface{}
|
defaultFields map[string]interface{}
|
||||||
|
showGoroutine bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -104,6 +107,7 @@ func New(out io.Writer, prefix string, flag int) *Logger {
|
|||||||
useColors: true,
|
useColors: true,
|
||||||
callerOffset: 0,
|
callerOffset: 0,
|
||||||
defaultFields: make(map[string]interface{}),
|
defaultFields: make(map[string]interface{}),
|
||||||
|
showGoroutine: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +143,20 @@ func (l *Logger) SetCallerOffset(offset int) {
|
|||||||
l.callerOffset = offset
|
l.callerOffset = offset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetShowGoroutine sets whether to include goroutine ID in log messages
|
||||||
|
func (l *Logger) SetShowGoroutine(show bool) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
l.showGoroutine = show
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowGoroutine returns whether goroutine ID is included in log messages
|
||||||
|
func (l *Logger) ShowGoroutine() bool {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
return l.showGoroutine
|
||||||
|
}
|
||||||
|
|
||||||
// WithField adds a field to the logger's context
|
// WithField adds a field to the logger's context
|
||||||
func (l *Logger) WithField(key string, value interface{}) *Logger {
|
func (l *Logger) WithField(key string, value interface{}) *Logger {
|
||||||
newLogger := &Logger{
|
newLogger := &Logger{
|
||||||
@@ -149,6 +167,7 @@ func (l *Logger) WithField(key string, value interface{}) *Logger {
|
|||||||
useColors: l.useColors,
|
useColors: l.useColors,
|
||||||
callerOffset: l.callerOffset,
|
callerOffset: l.callerOffset,
|
||||||
defaultFields: make(map[string]interface{}),
|
defaultFields: make(map[string]interface{}),
|
||||||
|
showGoroutine: l.showGoroutine,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy existing fields
|
// Copy existing fields
|
||||||
@@ -171,6 +190,7 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
|
|||||||
useColors: l.useColors,
|
useColors: l.useColors,
|
||||||
callerOffset: l.callerOffset,
|
callerOffset: l.callerOffset,
|
||||||
defaultFields: make(map[string]interface{}),
|
defaultFields: make(map[string]interface{}),
|
||||||
|
showGoroutine: l.showGoroutine,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy existing fields
|
// Copy existing fields
|
||||||
@@ -185,6 +205,17 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
|
|||||||
return newLogger
|
return newLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGoroutineID extracts the goroutine ID from the runtime stack
|
||||||
|
func GetGoroutineID() string {
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
n := runtime.Stack(buf, false)
|
||||||
|
// Format of first line is "goroutine N [state]:"
|
||||||
|
// We only need the N part
|
||||||
|
buf = buf[:n]
|
||||||
|
idField := bytes.Fields(bytes.Split(buf, []byte{':'})[0])[1]
|
||||||
|
return string(idField)
|
||||||
|
}
|
||||||
|
|
||||||
// formatMessage formats a log message with level, time, file, and line information
|
// formatMessage formats a log message with level, time, file, and line information
|
||||||
func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{}) string {
|
func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{}) string {
|
||||||
var msg string
|
var msg string
|
||||||
@@ -212,7 +243,25 @@ func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{
|
|||||||
|
|
||||||
var caller string
|
var caller string
|
||||||
if l.flag&log.Lshortfile != 0 || l.flag&log.Llongfile != 0 {
|
if l.flag&log.Lshortfile != 0 || l.flag&log.Llongfile != 0 {
|
||||||
_, file, line, ok := runtime.Caller(3 + l.callerOffset)
|
// Find the actual caller by scanning up the stack
|
||||||
|
// until we find a function outside the logger package
|
||||||
|
var file string
|
||||||
|
var line int
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
// Start at a reasonable depth and scan up to 10 frames
|
||||||
|
for depth := 4; depth < 15; depth++ {
|
||||||
|
_, file, line, ok = runtime.Caller(depth)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the caller is not in the logger package, we found our caller
|
||||||
|
if !strings.Contains(file, "logger/logger.go") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
file = "???"
|
file = "???"
|
||||||
line = 0
|
line = 0
|
||||||
@@ -221,9 +270,10 @@ func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{
|
|||||||
if l.flag&log.Lshortfile != 0 {
|
if l.flag&log.Lshortfile != 0 {
|
||||||
file = filepath.Base(file)
|
file = filepath.Base(file)
|
||||||
}
|
}
|
||||||
caller = fmt.Sprintf("%s:%d ", file, line)
|
caller = fmt.Sprintf("%-25s ", file+":"+strconv.Itoa(line))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format the timestamp with fixed width
|
||||||
var timeStr string
|
var timeStr string
|
||||||
if l.flag&(log.Ldate|log.Ltime|log.Lmicroseconds) != 0 {
|
if l.flag&(log.Ldate|log.Ltime|log.Lmicroseconds) != 0 {
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
@@ -235,12 +285,24 @@ func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{
|
|||||||
if l.flag&log.Lmicroseconds != 0 {
|
if l.flag&log.Lmicroseconds != 0 {
|
||||||
timeStr += fmt.Sprintf(".%06d", t.Nanosecond()/1000)
|
timeStr += fmt.Sprintf(".%06d", t.Nanosecond()/1000)
|
||||||
}
|
}
|
||||||
timeStr += " "
|
|
||||||
}
|
}
|
||||||
|
timeStr = fmt.Sprintf("%-15s ", timeStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s%s%s[%s%s%s]%s %s\n",
|
// Add goroutine ID if enabled, with fixed width
|
||||||
l.prefix, timeStr, caller, levelColor, levelNames[level], resetColor, fields, resetColor, msg)
|
var goroutineStr string
|
||||||
|
if l.showGoroutine {
|
||||||
|
goroutineID := GetGoroutineID()
|
||||||
|
goroutineStr = fmt.Sprintf("[g:%-4s] ", goroutineID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a colored level indicator with both brackets colored
|
||||||
|
levelStr := fmt.Sprintf("%s[%s]%s", levelColor, levelNames[level], levelColor)
|
||||||
|
// Add a space after the level and before the reset color
|
||||||
|
levelColumn := fmt.Sprintf("%s %s", levelStr, resetColor)
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s%s%s%s%s%s%s\n",
|
||||||
|
l.prefix, timeStr, caller, goroutineStr, levelColumn, msg, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
// log logs a message at the specified level
|
// log logs a message at the specified level
|
||||||
@@ -323,6 +385,16 @@ func Trace(format string, args ...interface{}) {
|
|||||||
DefaultLogger.Trace(format, args...)
|
DefaultLogger.Trace(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogPanic logs a panic error and its stack trace
|
||||||
|
func LogPanic(r interface{}) {
|
||||||
|
if DefaultLogger == nil {
|
||||||
|
Init(defaultLogLevel)
|
||||||
|
}
|
||||||
|
stack := make([]byte, 4096)
|
||||||
|
n := runtime.Stack(stack, false)
|
||||||
|
DefaultLogger.Error("PANIC: %v\n%s", r, stack[:n])
|
||||||
|
}
|
||||||
|
|
||||||
// SetLevel sets the log level for the default logger
|
// SetLevel sets the log level for the default logger
|
||||||
func SetLevel(level LogLevel) {
|
func SetLevel(level LogLevel) {
|
||||||
if DefaultLogger == nil {
|
if DefaultLogger == nil {
|
||||||
@@ -355,3 +427,19 @@ func WithFields(fields map[string]interface{}) *Logger {
|
|||||||
}
|
}
|
||||||
return DefaultLogger.WithFields(fields)
|
return DefaultLogger.WithFields(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetShowGoroutine enables or disables goroutine ID display in the default logger
|
||||||
|
func SetShowGoroutine(show bool) {
|
||||||
|
if DefaultLogger == nil {
|
||||||
|
Init(defaultLogLevel)
|
||||||
|
}
|
||||||
|
DefaultLogger.SetShowGoroutine(show)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowGoroutine returns whether goroutine ID is included in default logger's messages
|
||||||
|
func ShowGoroutine() bool {
|
||||||
|
if DefaultLogger == nil {
|
||||||
|
Init(defaultLogLevel)
|
||||||
|
}
|
||||||
|
return DefaultLogger.ShowGoroutine()
|
||||||
|
}
|
||||||
|
49
logger/panic_handler.go
Normal file
49
logger/panic_handler.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PanicHandler handles a panic and logs it
|
||||||
|
func PanicHandler() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
goroutineID := GetGoroutineID()
|
||||||
|
stackTrace := debug.Stack()
|
||||||
|
Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeGo launches a goroutine with panic recovery
|
||||||
|
// Usage: logger.SafeGo(func() { ... your code ... })
|
||||||
|
func SafeGo(f func()) {
|
||||||
|
go func() {
|
||||||
|
defer PanicHandler()
|
||||||
|
f()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeGoWithArgs launches a goroutine with panic recovery and passes arguments
|
||||||
|
// Usage: logger.SafeGoWithArgs(func(arg1, arg2 interface{}) { ... }, "value1", 42)
|
||||||
|
func SafeGoWithArgs(f func(...interface{}), args ...interface{}) {
|
||||||
|
go func() {
|
||||||
|
defer PanicHandler()
|
||||||
|
f(args...)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeExec executes a function with panic recovery
|
||||||
|
// Useful for code that should not panic
|
||||||
|
func SafeExec(f func()) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
goroutineID := GetGoroutineID()
|
||||||
|
stackTrace := debug.Stack()
|
||||||
|
Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace)
|
||||||
|
err = fmt.Errorf("panic recovered: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
f()
|
||||||
|
return nil
|
||||||
|
}
|
19
main.go
19
main.go
@@ -184,31 +184,32 @@ func main() {
|
|||||||
// Process each file
|
// Process each file
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(file string) {
|
logger.SafeGoWithArgs(func(args ...interface{}) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
logger.Debug("Processing file: %s", file)
|
fileToProcess := args[0].(string)
|
||||||
|
logger.Debug("Processing file: %s", fileToProcess)
|
||||||
|
|
||||||
// It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now
|
// It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now
|
||||||
modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr)
|
modCount, matchCount, err := processor.Process(proc, fileToProcess, pattern, luaExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to process file %s: %v", file, err)
|
logger.Error("Failed to process file %s: %v", fileToProcess, err)
|
||||||
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err)
|
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", fileToProcess, err)
|
||||||
stats.FailedFiles++
|
stats.FailedFiles++
|
||||||
} else {
|
} else {
|
||||||
if modCount > 0 {
|
if modCount > 0 {
|
||||||
logger.Info("Successfully processed file %s: %d modifications from %d matches",
|
logger.Info("Successfully processed file %s: %d modifications from %d matches",
|
||||||
file, modCount, matchCount)
|
fileToProcess, modCount, matchCount)
|
||||||
} else if matchCount > 0 {
|
} else if matchCount > 0 {
|
||||||
logger.Info("Found %d matches in file %s but made no modifications",
|
logger.Info("Found %d matches in file %s but made no modifications",
|
||||||
matchCount, file)
|
matchCount, fileToProcess)
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("No matches found in file: %s", file)
|
logger.Debug("No matches found in file: %s", fileToProcess)
|
||||||
}
|
}
|
||||||
stats.ProcessedFiles++
|
stats.ProcessedFiles++
|
||||||
stats.TotalMatches += matchCount
|
stats.TotalMatches += matchCount
|
||||||
stats.TotalModifications += modCount
|
stats.TotalModifications += modCount
|
||||||
}
|
}
|
||||||
}(file)
|
}, file)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
"github.com/antchfx/xmlquery"
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
@@ -62,6 +64,8 @@ func NewLuaState() (*lua.LState, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) {
|
func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) {
|
||||||
|
logger.Debug("Processing file %q with pattern %q", filename, pattern)
|
||||||
|
|
||||||
// Read file content
|
// Read file content
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -70,7 +74,15 @@ func Process(p Processor, filename string, pattern string, luaExpr string) (int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fullPath := filepath.Join(cwd, filename)
|
fullPath := filepath.Join(cwd, filename)
|
||||||
logger.Trace("Reading file content from: %s", fullPath)
|
logger.Trace("Reading file from: %s", fullPath)
|
||||||
|
|
||||||
|
stat, err := os.Stat(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to stat file %s: %v", fullPath, err)
|
||||||
|
return 0, 0, fmt.Errorf("error getting file info: %v", err)
|
||||||
|
}
|
||||||
|
logger.Debug("File size: %d bytes, modified: %s", stat.Size(), stat.ModTime().Format(time.RFC3339))
|
||||||
|
|
||||||
content, err := os.ReadFile(fullPath)
|
content, err := os.ReadFile(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to read file %s: %v", fullPath, err)
|
logger.Error("Failed to read file %s: %v", fullPath, err)
|
||||||
@@ -78,32 +90,108 @@ func Process(p Processor, filename string, pattern string, luaExpr string) (int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileContent := string(content)
|
fileContent := string(content)
|
||||||
logger.Trace("File %s read successfully, size: %d bytes", fullPath, len(content))
|
logger.Trace("File read successfully: %d bytes, hash: %x", len(content), md5sum(content))
|
||||||
|
|
||||||
|
// Detect and log file type
|
||||||
|
fileType := detectFileType(filename, fileContent)
|
||||||
|
if fileType != "" {
|
||||||
|
logger.Debug("Detected file type: %s", fileType)
|
||||||
|
}
|
||||||
|
|
||||||
// Process the content
|
// Process the content
|
||||||
logger.Debug("Processing content for file: %s", filename)
|
logger.Debug("Starting content processing with %s processor", getProcessorType(p))
|
||||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error processing content for file %s: %v", filename, err)
|
logger.Error("Processing error: %v", err)
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debug("Processing results: %d matches, %d modifications", matchCount, modCount)
|
||||||
|
|
||||||
// If we made modifications, save the file
|
// If we made modifications, save the file
|
||||||
if modCount > 0 {
|
if modCount > 0 {
|
||||||
logger.Info("Writing %d modifications to file: %s", modCount, filename)
|
// Calculate changes summary
|
||||||
|
changePercent := float64(len(modifiedContent)) / float64(len(fileContent)) * 100
|
||||||
|
logger.Info("File size change: %d → %d bytes (%.1f%%)",
|
||||||
|
len(fileContent), len(modifiedContent), changePercent)
|
||||||
|
|
||||||
|
logger.Debug("Writing modified content to %s", fullPath)
|
||||||
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to write to file %s: %v", fullPath, err)
|
logger.Error("Failed to write to file %s: %v", fullPath, err)
|
||||||
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
||||||
}
|
}
|
||||||
logger.Debug("File %s written successfully", filename)
|
logger.Debug("File written successfully, new hash: %x", md5sum([]byte(modifiedContent)))
|
||||||
|
} else if matchCount > 0 {
|
||||||
|
logger.Debug("No content modifications needed for %d matches", matchCount)
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("No modifications to write for file: %s", filename)
|
logger.Debug("No matches found in file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return modCount, matchCount, nil
|
return modCount, matchCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to get a short MD5 hash of content for logging
|
||||||
|
func md5sum(data []byte) []byte {
|
||||||
|
h := md5.New()
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum(nil)[:4] // Just use first 4 bytes for brevity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to detect basic file type from extension and content
|
||||||
|
func detectFileType(filename string, content string) string {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
|
||||||
|
switch ext {
|
||||||
|
case ".xml":
|
||||||
|
return "XML"
|
||||||
|
case ".json":
|
||||||
|
return "JSON"
|
||||||
|
case ".html", ".htm":
|
||||||
|
return "HTML"
|
||||||
|
case ".txt":
|
||||||
|
return "Text"
|
||||||
|
case ".go":
|
||||||
|
return "Go"
|
||||||
|
case ".js":
|
||||||
|
return "JavaScript"
|
||||||
|
case ".py":
|
||||||
|
return "Python"
|
||||||
|
case ".java":
|
||||||
|
return "Java"
|
||||||
|
case ".c", ".cpp", ".h":
|
||||||
|
return "C/C++"
|
||||||
|
default:
|
||||||
|
// Try content-based detection for common formats
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(content), "<?xml") {
|
||||||
|
return "XML"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(content), "{") ||
|
||||||
|
strings.HasPrefix(strings.TrimSpace(content), "[") {
|
||||||
|
return "JSON"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(content), "<!DOCTYPE html") ||
|
||||||
|
strings.HasPrefix(strings.TrimSpace(content), "<html") {
|
||||||
|
return "HTML"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get processor type name
|
||||||
|
func getProcessorType(p Processor) string {
|
||||||
|
switch p.(type) {
|
||||||
|
case *RegexProcessor:
|
||||||
|
return "Regex"
|
||||||
|
case *XMLProcessor:
|
||||||
|
return "XML"
|
||||||
|
case *JSONProcessor:
|
||||||
|
return "JSON"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ToLua converts a struct or map to a Lua table recursively
|
// ToLua converts a struct or map to a Lua table recursively
|
||||||
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
|
@@ -109,7 +109,7 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
|
|
||||||
previous := luaExpr
|
previous := luaExpr
|
||||||
luaExpr = BuildLuaScript(luaExpr)
|
luaExpr = BuildLuaScript(luaExpr)
|
||||||
logger.Debug("Changing Lua expression from: %s to: %s", previous, luaExpr)
|
logger.Debug("Transformed Lua expression: %q → %q", previous, luaExpr)
|
||||||
|
|
||||||
// Initialize Lua environment
|
// Initialize Lua environment
|
||||||
modificationCount := 0
|
modificationCount := 0
|
||||||
@@ -117,13 +117,15 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
// Process all regex matches
|
// Process all regex matches
|
||||||
result := content
|
result := content
|
||||||
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
|
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
|
||||||
logger.Debug("Found %d matches in the content", len(indices))
|
logger.Debug("Found %d matches in content of length %d", len(indices), len(content))
|
||||||
|
|
||||||
// We walk backwards because we're replacing something with something else that might be longer
|
// We walk backwards because we're replacing something with something else that might be longer
|
||||||
// And in the case it is longer than the original all indicces past that change will be fucked up
|
// And in the case it is longer than the original all indicces past that change will be fucked up
|
||||||
// By going backwards we fuck up all the indices to the end of the file that we don't care about
|
// By going backwards we fuck up all the indices to the end of the file that we don't care about
|
||||||
// Because there either aren't any (last match) or they're already modified (subsequent matches)
|
// Because there either aren't any (last match) or they're already modified (subsequent matches)
|
||||||
for i := len(indices) - 1; i >= 0; i-- {
|
for i := len(indices) - 1; i >= 0; i-- {
|
||||||
|
logger.Debug("Processing match %d of %d", i+1, len(indices))
|
||||||
|
|
||||||
L, err := NewLuaState()
|
L, err := NewLuaState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error creating Lua state: %v", err)
|
logger.Error("Error creating Lua state: %v", err)
|
||||||
@@ -133,10 +135,10 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
// Maybe we want to close them every iteration
|
// Maybe we want to close them every iteration
|
||||||
// We'll leave it as is for now
|
// We'll leave it as is for now
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
logger.Trace("Lua state created successfully")
|
logger.Trace("Lua state created successfully for match %d", i+1)
|
||||||
|
|
||||||
matchIndices := indices[i]
|
matchIndices := indices[i]
|
||||||
logger.Trace("Processing match indices: %v", matchIndices)
|
logger.Trace("Match indices: %v (match position %d-%d)", matchIndices, matchIndices[0], matchIndices[1])
|
||||||
|
|
||||||
// Why we're doing this whole song and dance of indices is to properly handle empty matches
|
// Why we're doing this whole song and dance of indices is to properly handle empty matches
|
||||||
// Plus it's a little cleaner to surgically replace our matches
|
// Plus it's a little cleaner to surgically replace our matches
|
||||||
@@ -146,21 +148,34 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
// As if concatenating in the middle of the array
|
// As if concatenating in the middle of the array
|
||||||
// Plus it supports lookarounds
|
// Plus it supports lookarounds
|
||||||
match := content[matchIndices[0]:matchIndices[1]]
|
match := content[matchIndices[0]:matchIndices[1]]
|
||||||
logger.Trace("Matched content: %s", match)
|
matchPreview := match
|
||||||
|
if len(match) > 50 {
|
||||||
|
matchPreview = match[:47] + "..."
|
||||||
|
}
|
||||||
|
logger.Trace("Matched content: %q (length: %d)", matchPreview, len(match))
|
||||||
|
|
||||||
groups := matchIndices[2:]
|
groups := matchIndices[2:]
|
||||||
if len(groups) <= 0 {
|
if len(groups) <= 0 {
|
||||||
logger.Warning("No capture groups for lua to chew on")
|
logger.Warning("No capture groups found for match %q and regex %q", matchPreview, pattern)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(groups)%2 == 1 {
|
if len(groups)%2 == 1 {
|
||||||
logger.Warning("Odd number of indices of groups, what the fuck?")
|
logger.Warning("Invalid number of group indices (%d), should be even: %v", len(groups), groups)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count how many valid groups we have
|
||||||
|
validGroups := 0
|
||||||
|
for j := 0; j < len(groups); j += 2 {
|
||||||
|
if groups[j] != -1 && groups[j+1] != -1 {
|
||||||
|
validGroups++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Debug("Found %d valid capture groups in match", validGroups)
|
||||||
|
|
||||||
for _, index := range groups {
|
for _, index := range groups {
|
||||||
if index == -1 {
|
if index == -1 {
|
||||||
// return "", 0, 0, fmt.Errorf("negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. Please check the regex pattern and input content.", matchIndices)
|
logger.Warning("Negative index encountered in match indices %v. This may indicate an issue with the regex pattern or an empty/optional capture group.", matchIndices)
|
||||||
logger.Warning("Negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. This is not an error but it's possibly not what you want.", matchIndices)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,55 +187,59 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
captureGroups := make([]*CaptureGroup, 0, len(groups)/2)
|
captureGroups := make([]*CaptureGroup, 0, len(groups)/2)
|
||||||
groupNames := compiledPattern.SubexpNames()[1:]
|
groupNames := compiledPattern.SubexpNames()[1:]
|
||||||
for i, name := range groupNames {
|
for i, name := range groupNames {
|
||||||
// if name == "" {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
start := groups[i*2]
|
start := groups[i*2]
|
||||||
end := groups[i*2+1]
|
end := groups[i*2+1]
|
||||||
if start == -1 || end == -1 {
|
if start == -1 || end == -1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value := content[start:end]
|
||||||
captureGroups = append(captureGroups, &CaptureGroup{
|
captureGroups = append(captureGroups, &CaptureGroup{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: content[start:end],
|
Value: value,
|
||||||
Range: [2]int{start, end},
|
Range: [2]int{start, end},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
for _, capture := range captureGroups {
|
// Include name info in log if available
|
||||||
logger.Trace("Capture group: %+v", *capture)
|
if name != "" {
|
||||||
|
logger.Trace("Capture group '%s': %q (pos %d-%d)", name, value, start, end)
|
||||||
|
} else {
|
||||||
|
logger.Trace("Capture group #%d: %q (pos %d-%d)", i+1, value, start, end)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.ToLua(L, captureGroups); err != nil {
|
if err := p.ToLua(L, captureGroups); err != nil {
|
||||||
logger.Error("Error setting Lua variables: %v", err)
|
logger.Error("Failed to set Lua variables: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.Trace("Lua variables set successfully")
|
logger.Trace("Set %d capture groups as Lua variables", len(captureGroups))
|
||||||
|
|
||||||
if err := L.DoString(luaExpr); err != nil {
|
if err := L.DoString(luaExpr); err != nil {
|
||||||
logger.Error("Error executing Lua code %s for groups %+v: %v", luaExpr, captureGroups, err)
|
logger.Error("Lua script execution failed: %v\nScript: %s\nCapture Groups: %+v",
|
||||||
|
err, luaExpr, captureGroups)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.Trace("Lua code executed successfully")
|
logger.Trace("Lua script executed successfully")
|
||||||
|
|
||||||
// Get modifications from Lua
|
// Get modifications from Lua
|
||||||
captureGroups, err = p.FromLuaCustom(L, captureGroups)
|
captureGroups, err = p.FromLuaCustom(L, captureGroups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error getting modifications: %v", err)
|
logger.Error("Failed to retrieve modifications from Lua: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
logger.Trace("Retrieved updated values from Lua")
|
||||||
|
|
||||||
replacement := ""
|
replacement := ""
|
||||||
replacementVar := L.GetGlobal("replacement")
|
replacementVar := L.GetGlobal("replacement")
|
||||||
if replacementVar.Type() != lua.LTNil {
|
if replacementVar.Type() != lua.LTNil {
|
||||||
replacement = replacementVar.String()
|
replacement = replacementVar.String()
|
||||||
|
logger.Debug("Using global replacement: %q", replacement)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if modification flag is set
|
// Check if modification flag is set
|
||||||
modifiedVal := L.GetGlobal("modified")
|
modifiedVal := L.GetGlobal("modified")
|
||||||
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
|
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
|
||||||
logger.Debug("No modifications made by Lua script")
|
logger.Debug("Skipping match - no modifications made by Lua script")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +247,26 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
commands := make([]ReplaceCommand, 0, len(captureGroups))
|
commands := make([]ReplaceCommand, 0, len(captureGroups))
|
||||||
// Apply the modifications to the original match
|
// Apply the modifications to the original match
|
||||||
replacement = match
|
replacement = match
|
||||||
|
|
||||||
|
// Count groups that were actually modified
|
||||||
|
modifiedGroups := 0
|
||||||
for _, capture := range captureGroups {
|
for _, capture := range captureGroups {
|
||||||
logger.Debug("Applying modification: %s", capture.Updated)
|
if capture.Value != capture.Updated {
|
||||||
|
modifiedGroups++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Debug("%d of %d capture groups were modified", modifiedGroups, len(captureGroups))
|
||||||
|
|
||||||
|
for _, capture := range captureGroups {
|
||||||
|
if capture.Value == capture.Updated {
|
||||||
|
logger.Trace("Capture group unchanged: %s", capture.Value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log what changed with context
|
||||||
|
logger.Debug("Modifying group %s: %q → %q",
|
||||||
|
capture.Name, capture.Value, capture.Updated)
|
||||||
|
|
||||||
// Indices of the group are relative to content
|
// Indices of the group are relative to content
|
||||||
// To relate them to match we have to subtract the match start index
|
// To relate them to match we have to subtract the match start index
|
||||||
// replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
|
// replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
|
||||||
@@ -240,21 +277,31 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort commands in reverse order for safe replacements
|
||||||
sort.Slice(commands, func(i, j int) bool {
|
sort.Slice(commands, func(i, j int) bool {
|
||||||
return commands[i].From > commands[j].From
|
return commands[i].From > commands[j].From
|
||||||
})
|
})
|
||||||
|
logger.Trace("Applying %d replacement commands in reverse order", len(commands))
|
||||||
|
|
||||||
for _, command := range commands {
|
for _, command := range commands {
|
||||||
|
logger.Trace("Replace pos %d-%d with %q", command.From, command.To, command.With)
|
||||||
replacement = replacement[:command.From] + command.With + replacement[command.To:]
|
replacement = replacement[:command.From] + command.With + replacement[command.To:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preview the replacement for logging
|
||||||
|
replacementPreview := replacement
|
||||||
|
if len(replacement) > 50 {
|
||||||
|
replacementPreview = replacement[:47] + "..."
|
||||||
|
}
|
||||||
|
logger.Debug("Replacing match %q with %q", matchPreview, replacementPreview)
|
||||||
|
|
||||||
modificationCount++
|
modificationCount++
|
||||||
result = result[:matchIndices[0]] + replacement + result[matchIndices[1]:]
|
result = result[:matchIndices[0]] + replacement + result[matchIndices[1]:]
|
||||||
logger.Debug("Modification count updated: %d", modificationCount)
|
logger.Debug("Match #%d processed, running modification count: %d", i+1, modificationCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("Process completed with %d modifications", modificationCount)
|
logger.Info("Regex processing complete: %d modifications from %d matches", modificationCount, len(indices))
|
||||||
return result, modificationCount, len(indices), nil
|
return result, modificationCount, len(indices), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io"
|
||||||
"modify/logger"
|
"modify/logger"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// Only modify logger in test mode
|
||||||
|
// This checks if we're running under 'go test'
|
||||||
|
if os.Getenv("GO_TESTING") == "1" || os.Getenv("TESTING") == "1" {
|
||||||
// Initialize logger with ERROR level for tests
|
// Initialize logger with ERROR level for tests
|
||||||
// to minimize noise in test output
|
// to minimize noise in test output
|
||||||
logger.Init(logger.LevelError)
|
logger.Init(logger.LevelError)
|
||||||
@@ -16,7 +19,8 @@ func init() {
|
|||||||
disableTestLogs := os.Getenv("ENABLE_TEST_LOGS") != "1"
|
disableTestLogs := os.Getenv("ENABLE_TEST_LOGS") != "1"
|
||||||
if disableTestLogs {
|
if disableTestLogs {
|
||||||
// Create a new logger that writes to nowhere
|
// Create a new logger that writes to nowhere
|
||||||
silentLogger := logger.New(ioutil.Discard, "", 0)
|
silentLogger := logger.New(io.Discard, "", 0)
|
||||||
logger.DefaultLogger = silentLogger
|
logger.DefaultLogger = silentLogger
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -81,3 +81,80 @@ func TestTalentsMechanicOutOfRange(t *testing.T) {
|
|||||||
t.Errorf("expected %s, got %s", actual, result)
|
t.Errorf("expected %s, got %s", actual, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIndexExplosions(t *testing.T) {
|
||||||
|
given := `<Talent identifier="quickfixer">
|
||||||
|
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
|
||||||
|
<Description tag="talentdescription.quickfixer">
|
||||||
|
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||||
|
<Replace tag="[duration]" value="10" color="gui.green"/>
|
||||||
|
</Description>
|
||||||
|
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
|
||||||
|
<AbilityGroupEffect abilityeffecttype="None">
|
||||||
|
<Abilities>
|
||||||
|
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="1"/>
|
||||||
|
</Abilities>
|
||||||
|
</AbilityGroupEffect>
|
||||||
|
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||||
|
<Conditions>
|
||||||
|
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
|
||||||
|
</Conditions>
|
||||||
|
<Abilities>
|
||||||
|
<CharacterAbilityApplyStatusEffects>
|
||||||
|
<StatusEffects>
|
||||||
|
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
|
||||||
|
<Affliction identifier="quickfixer" amount="10.0"/>
|
||||||
|
</StatusEffect>
|
||||||
|
</StatusEffects>
|
||||||
|
</CharacterAbilityApplyStatusEffects>
|
||||||
|
</Abilities>
|
||||||
|
</AbilityGroupEffect>
|
||||||
|
</Talent>`
|
||||||
|
|
||||||
|
actual := `<Talent identifier="quickfixer">
|
||||||
|
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
|
||||||
|
<Description tag="talentdescription.quickfixer">
|
||||||
|
<Replace tag="[amount]" value="30" color="gui.green"/>
|
||||||
|
<Replace tag="[duration]" value="20" color="gui.green"/>
|
||||||
|
</Description>
|
||||||
|
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
|
||||||
|
<AbilityGroupEffect abilityeffecttype="None">
|
||||||
|
<Abilities>
|
||||||
|
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="2"/>
|
||||||
|
</Abilities>
|
||||||
|
</AbilityGroupEffect>
|
||||||
|
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||||
|
<Conditions>
|
||||||
|
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
|
||||||
|
</Conditions>
|
||||||
|
<Abilities>
|
||||||
|
<CharacterAbilityApplyStatusEffects>
|
||||||
|
<StatusEffects>
|
||||||
|
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
|
||||||
|
<Affliction identifier="quickfixer" amount="20"/>
|
||||||
|
</StatusEffect>
|
||||||
|
</StatusEffects>
|
||||||
|
</CharacterAbilityApplyStatusEffects>
|
||||||
|
</Abilities>
|
||||||
|
</AbilityGroupEffect>
|
||||||
|
</Talent>`
|
||||||
|
|
||||||
|
p := &processor.RegexProcessor{}
|
||||||
|
result, mods, matches, err := p.ProcessContent(given, `<Talent identifier="quickfixer">!anyvalue="(?<movementspeed>!num)"!anyvalue="(?<duration>!num)"!anyvalue="(?<repairspeed>!num)"!anyamount="(?<durationv>!num)"`, "movementspeed=round(movementspeed*1.5, 2) duration=round(duration*2, 2) repairspeed=round(repairspeed*2, 2) durationv=duration")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches != 1 {
|
||||||
|
t.Errorf("Expected 1 match, got %d", matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mods != 1 {
|
||||||
|
t.Errorf("Expected 1 modification, got %d", mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != actual {
|
||||||
|
t.Errorf("expected %s, got %s", actual, result)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user