14 Commits

Author SHA1 Message Date
brent saner
bae0abe960 v1.9.3
IMPROVED:
* Better documentation for remap
2025-08-12 00:06:51 -04:00
brent saner
368ae0cb8e v1.9.2
FIX:
* Yeah so the ReMap.Map* stuff was kind of broken hard. It's fixed now.
2025-08-04 04:26:52 +00:00
brent saner
154170c0e5 v1.9.1
ADDED:
* remap package
2025-08-02 09:39:06 -04:00
brent saner
d9bd928edb v1.9.0
ADD:
* `iox` subpackage

FIX:
* `logging` now has a way to return logWritier directly
* added significant `io.*` interface compat to logWriter -- allowing a `logging.Logger` to essentially be used for a large amount of io interaction in other libraries.
2025-07-31 03:45:32 -04:00
brent saner
dc2ed32352 v1.8.1
FIXED:
* Whoops, bit premature on 1.8.0; broke some Linux logging.
2025-02-10 13:20:36 -05:00
brent saner
e734e847c4 v1.8.0
ADDED:
* Basic macOS support (and BSD support, etc.)
* macOS has its own proprietary logging, ULS ("Unified Logging System"),
  but there doesn't seem to be native Golang support. So lolbai;
  your only options are syslog, stdlog, null log, filelog, and the
  "meta" logs (multilog, default log- which should use syslog).
2025-02-10 13:01:46 -05:00
brent saner
2203de4e32 need some general *nix errs too 2025-02-10 12:54:12 -05:00
brent saner
a0c6df14aa more general syslog support 2025-02-10 12:35:40 -05:00
brent saner
fd720f2b34 v1.7.2
FIXED:
* multierr race condition fix/now fully supports multithreading
2025-01-04 02:29:49 -05:00
brent saner
3c543a05e7 v1.7.1
FIXED:
* bitmask.MaskBit.ClearFlag now works properly. Whoooops, how long was
  that typo there?
2024-11-07 03:44:54 -05:00
brent saner
e5191383a7 v1.7.0
ADDED:
* logging.Logger objects now are able to return a stdlib *log.Logger.
2024-06-19 18:57:26 -04:00
brent saner
ae49f42c0c v1.6.0
Added bitmask/MaskBit.Copy()
2024-04-14 01:54:59 -04:00
brent saner
b87934e8a9 v1.5.0
Added structutils
2024-04-13 13:04:17 -04:00
70d6c2cbb3 updating logging TODO 2023-07-12 23:16:31 -04:00
43 changed files with 1783 additions and 141 deletions

View File

@@ -2,8 +2,8 @@ package bitmask
import ( import (
"bytes" "bytes"
"errors"
"encoding/binary" "encoding/binary"
"errors"
"math/bits" "math/bits"
) )
@@ -56,7 +56,7 @@ func (m *MaskBit) AddFlag(flag MaskBit) {
// ClearFlag removes MaskBit flag from m. // ClearFlag removes MaskBit flag from m.
func (m *MaskBit) ClearFlag(flag MaskBit) { func (m *MaskBit) ClearFlag(flag MaskBit) {
*m &= flag *m &^= flag
return return
} }
@@ -109,6 +109,15 @@ func (m *MaskBit) Bytes(trim bool) (b []byte) {
return return
} }
// Copy returns a pointer to a (new) copy of a MaskBit.
func (m *MaskBit) Copy() (newM *MaskBit) {
newM = new(MaskBit)
*newM = *m
return
}
// Value returns the current raw uint value of a MaskBit. // Value returns the current raw uint value of a MaskBit.
func (m *MaskBit) Value() (v uint) { func (m *MaskBit) Value() (v uint) {

15
go.mod
View File

@@ -1,10 +1,15 @@
module r00t2.io/goutils module r00t2.io/goutils
go 1.16 go 1.24.5
require ( require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/coreos/go-systemd/v22 v22.5.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.6.0
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e golang.org/x/sys v0.34.0
r00t2.io/sysutils v1.1.1 r00t2.io/sysutils v1.14.0
)
require (
github.com/djherbis/times v1.6.0 // indirect
golang.org/x/sync v0.16.0 // indirect
) )

21
go.sum
View File

@@ -1,8 +1,13 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
r00t2.io/sysutils v1.1.1 h1:q2P5u50HIIRk6muCPo1Gpapy6sNT4oaB1l2O/C/mi3A= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=

4
iox/docs.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package iox includes extensions to the stdlib `io` module.
*/
package iox

9
iox/errs.go Normal file
View File

@@ -0,0 +1,9 @@
package iox
import (
`errors`
)
var (
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
)

41
iox/funcs.go Normal file
View File

@@ -0,0 +1,41 @@
package iox
import (
`io`
)
/*
CopyBufN is a mix between io.CopyN and io.CopyBuffer.
Despite what the docs may suggest, io.CopyN does NOT *read* n bytes from src AND write n bytes to dst.
Instead, it always reads 32 KiB from src, and writes n bytes to dst.
There are, of course, cases where this is deadfully undesired.
One can, of course, use io.CopyBuffer, but this is a bit annoying since you then have to provide a buffer yourself.
This convenience-wraps io.CopyBuffer to have a similar signature to io.CopyN but properly uses n for both reading and writing.
*/
func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
var b []byte
if n <= 0 {
err = ErrBufTooSmall
return
}
b = make([]byte, n)
written, err = io.CopyBuffer(dst, src, b)
return
}
// CopyBufWith allows for specifying a buffer allocator function, otherwise acts as CopyBufN.
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
written, err = io.CopyBuffer(dst, src, bufFunc())
return
}

8
iox/types.go Normal file
View File

@@ -0,0 +1,8 @@
package iox
type (
// RuneWriter matches the behavior of *(bytes.Buffer).WriteRune and *(bufio.Writer).WriteRune
RuneWriter interface {
WriteRune(r rune) (n int, err error)
}
)

View File

@@ -1,9 +1,21 @@
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
-- https://developer.apple.com/documentation/os/logging
-- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code
-- no native Go support (yet)?
--- https://developer.apple.com/forums/thread/773369
- add a `log/slog` logging.Logger?
- Implement code line/func/etc. (only for debug?): - Implement code line/func/etc. (only for debug?):
https://stackoverflow.com/a/24809646 https://stackoverflow.com/a/24809646
https://golang.org/pkg/runtime/#Caller https://golang.org/pkg/runtime/#Caller
-- log.LlongFile and log.Lshortfile flags don't currently work properly for StdLogger/FileLogger; they refer to the file in logging package rather than the caller. -- log.LlongFile and log.Lshortfile flags don't currently work properly for StdLogger/FileLogger; they refer to the file in logging package rather than the caller.
-- ZeroLog seems to be able to do it, take a peek there.
- Suport remote loggers? (eventlog, syslog, systemd) - StdLogger2; where stdout and stderr are both logged to depending on severity level.
- make configurable via OR bitmask
- Suport remote loggers? (eventlog, syslog, journald)
- JSON logger? YAML logger? XML logger? - JSON logger? YAML logger? XML logger?

View File

@@ -12,3 +12,16 @@ const (
// appendFlags are the flags used for testing the file (and opening/writing). // appendFlags are the flags used for testing the file (and opening/writing).
appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY
) )
const PriorityNone logPrio = 0
const (
PriorityEmergency logPrio = 1 << iota
PriorityAlert
PriorityCritical
PriorityError
PriorityWarning
PriorityNotice
PriorityInformational
PriorityDebug
)
const PriorityAll logPrio = PriorityEmergency | PriorityAlert | PriorityCritical | PriorityError | PriorityWarning | PriorityNotice | PriorityInformational | PriorityDebug

9
logging/consts_darwin.go Normal file
View File

@@ -0,0 +1,9 @@
package logging
var (
// defLogPaths indicates default log paths.
defLogPaths = []string{
"/var/log/golang/program.log",
"~/Library/Logs/Golang/program.log",
}
)

View File

@@ -1,32 +1,5 @@
package logging package logging
import (
`log/syslog`
`r00t2.io/goutils/bitmask`
)
const (
// devlog is the path to the syslog char device.
devlog string = "/dev/log"
// syslogFacility is the facility to use; it's a little like a context or scope if you think of it in those terms.
syslogFacility syslog.Priority = syslog.LOG_USER
)
// Flags for logger configuration. These are used internally.
const (
// LogUndefined indicates an undefined Logger type.
LogUndefined bitmask.MaskBit = 1 << iota
// LogJournald flags a SystemDLogger Logger type.
LogJournald
// LogSyslog flags a SyslogLogger Logger type.
LogSyslog
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.
LogStdout
)
var ( var (
// defLogPaths indicates default log paths. // defLogPaths indicates default log paths.
defLogPaths = []string{ defLogPaths = []string{

34
logging/consts_nix.go Normal file
View File

@@ -0,0 +1,34 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
// I mean maybe it works for plan9 and ios, I don't know.
package logging
import (
"log/syslog"
"r00t2.io/goutils/bitmask"
)
const (
// devlog is the path to the syslog char device.
devlog string = "/dev/log"
// syslogFacility is the facility to use; it's a little like a context or scope if you think of it in those terms.
syslogFacility syslog.Priority = syslog.LOG_USER
)
// Flags for logger configuration. These are used internally.
// LogUndefined indicates an undefined Logger type.
const LogUndefined bitmask.MaskBit = iota
const (
// LogJournald flags a SystemDLogger Logger type.
LogJournald = 1 << iota
// LogSyslog flags a SyslogLogger Logger type.
LogSyslog
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.
LogStdout
)

View File

@@ -7,13 +7,16 @@ These particular loggers (logging.Logger) available are:
StdLogger StdLogger
FileLogger FileLogger
SystemDLogger (Linux only) SystemDLogger (Linux only)
SyslogLogger (Linux only) SyslogLogger (Linux/macOS/other *NIX-like only)
WinLogger (Windows only) WinLogger (Windows only)
There is a seventh type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call. There is a seventh type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call.
As you may have guessed, NullLogger doesn't actually log anything but is fully "functional" as a logging.Logger. (This is similar to stdlib's io.MultiWriter()'s return value, but with priority awareness and fmt string support).
Note that for some Loggers, the prefix may be modified - "literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix. As you may have guessed, NullLogger doesn't actually log anything but is fully "functional" as a logging.Logger (similar to io.discard/io.Discard()'s return).
Note that for some Loggers, the prefix may be modified after the Logger has already initialized.
"Literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix by default.
If this is undesired (unlikely), you will need to modify (Logger).Prefix and run (Logger).Logger.SetPrefix(yourPrefixHere) for the respective logger. If this is undesired (unlikely), you will need to modify (Logger).Prefix and run (Logger).Logger.SetPrefix(yourPrefixHere) for the respective logger.
Every logging.Logger type has the following methods that correspond to certain "levels". Every logging.Logger type has the following methods that correspond to certain "levels".
@@ -45,5 +48,17 @@ logging.Logger types also have the following methods:
In some cases, Logger.Setup and Logger.Shutdown are no-ops. In other cases, they perform necessary initialization/cleanup and closing of the logger. In some cases, Logger.Setup and Logger.Shutdown are no-ops. In other cases, they perform necessary initialization/cleanup and closing of the logger.
It is recommended to *always* run Setup and Shutdown before and after using, respectively, regardless of the actual logging.Logger type. It is recommended to *always* run Setup and Shutdown before and after using, respectively, regardless of the actual logging.Logger type.
Lastly, all logging.Loggers have a ToLogger() method. This returns a *log.Logger (from stdlib log), which also conforms to io.Writer inherently.
In addition. all have a ToRaw() method, which extends a Logger even further and returns an unexported type (*logging.logWriter) compatible with:
- io.ByteWriter
- io.Writer
- io.WriteCloser (Shutdown() on the Logger backend is called during Close(), rendering the underlying Logger unsafe to use afterwards)
- io.StringWriter
and, if stdlib io ever defines an e.g. RuneWriter (WriteRune(r rune) (n int, err error)), it will conform to that too (see (r00t2.io/goutils/iox).RuneWriter).
Obviously this and io.ByteWriter are fairly silly, as they're intended to be high-speed throughput-optimized methods, but if you wanted to e.g.
log every single byte on a wire as a separate log message, go ahead; I'm not your dad.
*/ */
package logging package logging

View File

@@ -1,7 +1,7 @@
package logging package logging
import ( import (
`errors` "errors"
) )
var ( var (
@@ -12,6 +12,8 @@ var (
exists with too restrictive perms to write/append to, and/or could not be created. exists with too restrictive perms to write/append to, and/or could not be created.
*/ */
ErrInvalidFile error = errors.New("a FileLogger was requested but the file does not exist and cannot be created") ErrInvalidFile error = errors.New("a FileLogger was requested but the file does not exist and cannot be created")
// ErrInvalidRune is returned if a rune was expected but it is not a valid UTF-8 codepoint.
ErrInvalidRune error = errors.New("specified rune is not valid UTF-8 codepoint")
// ErrNoEntry indicates that the user attempted to MultiLogger.RemoveLogger a Logger but one by that identifier does not exist. // ErrNoEntry indicates that the user attempted to MultiLogger.RemoveLogger a Logger but one by that identifier does not exist.
ErrNoEntry error = errors.New("the Logger specified to be removed does not exist") ErrNoEntry error = errors.New("the Logger specified to be removed does not exist")
) )

View File

@@ -1,18 +1,10 @@
package logging package logging
import ( import (
`errors` "errors"
`fmt`
) )
var ( var (
// ErrNoSysD indicates that the user attempted to add a SystemDLogger to a MultiLogger but systemd is unavailable. // ErrNoSysD indicates that the user attempted to add a SystemDLogger to a MultiLogger but systemd is unavailable.
ErrNoSysD error = errors.New("a systemd (journald) Logger was requested but systemd is unavailable on this system") ErrNoSysD error = errors.New("a systemd (journald) Logger was requested but systemd is unavailable on this system")
// ErrNoSyslog indicates that the user attempted to add a SyslogLogger to a MultiLogger but syslog's logger device is unavailable.
ErrNoSyslog error = errors.New("a Syslog Logger was requested but Syslog is unavailable on this system")
/*
ErrInvalidDevLog indicates that the user attempted to add a SyslogLogger to a MultiLogger but
the Syslog char device file is... not actually a char device file.
*/
ErrInvalidDevLog error = errors.New(fmt.Sprintf("a Syslog Logger was requested but %v is not a valid logger handle", devlog))
) )

19
logging/errs_nix.go Normal file
View File

@@ -0,0 +1,19 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"errors"
"fmt"
)
var (
// ErrNoSyslog indicates that the user attempted to add a SyslogLogger to a MultiLogger but syslog's logger device is unavailable.
ErrNoSyslog error = errors.New("a Syslog Logger was requested but Syslog is unavailable on this system")
/*
ErrInvalidDevLog indicates that the user attempted to add a SyslogLogger to a MultiLogger but
the Syslog char device file is... not actually a char device file.
*/
ErrInvalidDevLog error = errors.New(fmt.Sprintf("a Syslog Logger was requested but %v is not a valid logger handle", devlog))
)

View File

@@ -1,9 +1,33 @@
package logging package logging
import ( import (
"log"
"os" "os"
) )
/*
ToLog returns a stdlib *log.Logger from a logging.Logger. It simply wraps the (logging.Logger).ToLogger() methods.
prio is an OR'd logPrio of the Priority* constants.
*/
func ToLog(l Logger, prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = l.ToLogger(prio)
return
}
// ToRaw returns a *logWriter from a logging.Logger. It is an alternative to the (logging.Logger).ToRaw() methods.
func ToRaw(l Logger, prio logPrio) (raw *logWriter) {
raw = &logWriter{
backend: l,
prio: prio,
}
return
}
// testOpen attempts to open a file for writing to test for suitability as a LogFile path. // testOpen attempts to open a file for writing to test for suitability as a LogFile path.
func testOpen(path string) (success bool, err error) { func testOpen(path string) (success bool, err error) {

View File

@@ -220,6 +220,22 @@ func (l *FileLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *FileLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *FileLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}
// renderWrite prepares/formats a log message to be written to this FileLogger. // renderWrite prepares/formats a log message to be written to this FileLogger.
func (l *FileLogger) renderWrite(msg, prio string) { func (l *FileLogger) renderWrite(msg, prio string) {

View File

@@ -5,7 +5,7 @@ import (
`os` `os`
`path` `path`
sysd `github.com/coreos/go-systemd/journal` sysd `github.com/coreos/go-systemd/v22/journal`
`r00t2.io/goutils/bitmask` `r00t2.io/goutils/bitmask`
`r00t2.io/sysutils/paths` `r00t2.io/sysutils/paths`
) )

23
logging/funcs_logprio.go Normal file
View File

@@ -0,0 +1,23 @@
package logging
import (
`r00t2.io/goutils/bitmask`
)
// HasFlag provides a wrapper for functionality to the underlying bitmask.MaskBit.
func (l *logPrio) HasFlag(prio logPrio) (hasFlag bool) {
var m *bitmask.MaskBit
var p *bitmask.MaskBit
if l == nil {
return
}
m = bitmask.NewMaskBitExplicit(uint(*l))
p = bitmask.NewMaskBitExplicit(uint(prio))
hasFlag = m.HasFlag(*p)
return
}

209
logging/funcs_logwriter.go Normal file
View File

@@ -0,0 +1,209 @@
package logging
import (
"unicode/utf8"
"r00t2.io/goutils/multierr"
)
/*
Close calls Logger.Shutdown() on the underlying Logger.
The Logger *must not be used* after this; it will need to be re-initialized with Logger.Setup()
or a new Logger (and thuse new logWriter) must be created to replace it.
It (along with logWriter.Write()) conforms to WriteCloser().
*/
func (l *logWriter) Close() (err error) {
if err = l.backend.Shutdown(); err != nil {
return
}
return
}
/*
Write writes bytes b to the underlying Logger's priority level if the logWriter's priority level(s) match.
It conforms to io.Writer. n will *always* == len(b) on success, because otherwise n would technically be >= len(b)
(if multiple priorities are enabled), which is undefined behavior per io.Writer.
b is converted to a string to normalize to the underlying Logger.
*/
func (l *logWriter) Write(b []byte) (n int, err error) {
var s string
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
if b == nil {
return
}
s = string(b)
if l.prio.HasFlag(PriorityEmergency) {
if err = l.backend.Emerg(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityAlert) {
if err = l.backend.Alert(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityCritical) {
if err = l.backend.Crit(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityError) {
if err = l.backend.Err(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityWarning) {
if err = l.backend.Warning(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityNotice) {
if err = l.backend.Notice(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityInformational) {
if err = l.backend.Info(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityDebug) {
if err = l.backend.Debug(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
n = len(b)
return
}
/*
WriteByte conforms a logWriter to an io.ByteWriter. (It just wraps logWriter.Write().)
You should probably never use this; the logging overhead/prefix is going to be more data than the single byte itself.
c is converted to a string to normalize to the underlying Logger.
*/
func (l *logWriter) WriteByte(c byte) (err error) {
if _, err = l.Write([]byte{c}); err != nil {
return
}
return
}
/*
WriteRune follows the same signature of (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune(); thus if `io` ever defines an io.RuneWriter interface, here ya go.
n will *always* be equal to (unicode/utf8).RuneLen(r), unless r is an "invalid rune" -- in which case n will be 0 and err will be ErrInvalidRune..
*/
func (l *logWriter) WriteRune(r rune) (n int, err error) {
var b []byte
n = utf8.RuneLen(r)
if n < 0 {
err = ErrInvalidRune
n = 0
return
}
b = make([]byte, n)
utf8.EncodeRune(b, r)
if n, err = l.Write(b); err != nil {
return
}
return
}
/*
WriteString writes string s to the underlying Logger's priority level if the logWriter's priority level(s) match.
It conforms to io.StringWriter. n will *always* == len(s) on success, because otherwise n would technically be >= len(s)
(if multiple priorities are enabled), which is undefined behavior per io.StringWriter.
*/
func (l *logWriter) WriteString(s string) (n int, err error) {
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
if l.prio.HasFlag(PriorityEmergency) {
if err = l.backend.Emerg(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityAlert) {
if err = l.backend.Alert(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityCritical) {
if err = l.backend.Crit(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityError) {
if err = l.backend.Err(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityWarning) {
if err = l.backend.Warning(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityNotice) {
if err = l.backend.Notice(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityInformational) {
if err = l.backend.Info(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityDebug) {
if err = l.backend.Debug(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
n = len(s)
return
}

View File

@@ -3,6 +3,7 @@ package logging
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"sync" "sync"
"r00t2.io/goutils/multierr" "r00t2.io/goutils/multierr"
@@ -370,3 +371,19 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (m *MultiLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(m.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (m *MultiLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: m, prio: prio}
return
}

View File

@@ -1,20 +1,17 @@
package logging package logging
import ( import (
`os` sysd "github.com/coreos/go-systemd/v22/journal"
"github.com/google/uuid"
sysd `github.com/coreos/go-systemd/journal`
`github.com/google/uuid`
`r00t2.io/sysutils/paths`
) )
/* /*
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger. AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
identifier is a string to use to identify the added Logger in MultiLogger.Loggers. identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
If empty, one will be automatically generated. If empty, one will be automatically generated.
See the documentation for GetLogger for details on other arguments. See the documentation for GetLogger for details on other arguments.
*/ */
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) { func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
@@ -40,10 +37,10 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths
} }
/* /*
AddSysdLogger adds a SystemDLogger to a MultiLogger. AddSysdLogger adds a SystemDLogger to a MultiLogger.
identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers. identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers.
If empty, one will be automatically generated. If empty, one will be automatically generated.
*/ */
func (m *MultiLogger) AddSysdLogger(identifier string) (err error) { func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
@@ -80,55 +77,3 @@ func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
return return
} }
/*
AddSyslogLogger adds a SyslogLogger to a MultiLogger.
identifier is a string to use to identify the added SyslogLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.
*/
func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) {
var exists bool
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog
var prefix string
if identifier == "" {
identifier = uuid.New().String()
}
if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
} else if !hasSyslog {
err = ErrNoSyslog
return
}
if stat.Mode().IsRegular() {
err = ErrInvalidDevLog
return
}
m.Loggers[identifier] = &SyslogLogger{
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
}
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
return
}

View File

@@ -0,0 +1,63 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"os"
"github.com/google/uuid"
"r00t2.io/sysutils/paths"
)
/*
AddSyslogLogger adds a SyslogLogger to a MultiLogger.
identifier is a string to use to identify the added SyslogLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.
*/
func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) {
var exists bool
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog
var prefix string
if identifier == "" {
identifier = uuid.New().String()
}
if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
} else if !hasSyslog {
err = ErrNoSyslog
return
}
if stat.Mode().IsRegular() {
err = ErrInvalidDevLog
return
}
m.Loggers[identifier] = &SyslogLogger{
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
}
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
return
}

View File

@@ -0,0 +1,41 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
// Linux is excluded because it has its own.
package logging
import (
"github.com/google/uuid"
)
/*
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
If empty, one will be automatically generated.
See the documentation for GetLogger for details on other arguments.
*/
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
var l Logger
var exists bool
if identifier == "" {
identifier = uuid.New().String()
}
if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}
if l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...); err != nil {
return
}
m.Loggers[identifier] = l
return
}

View File

@@ -1,5 +1,9 @@
package logging package logging
import (
"log"
)
// Setup does nothing at all; it's here for interface compat. 🙃 // Setup does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Setup() (err error) { func (l *NullLogger) Setup() (err error) {
return return
@@ -72,3 +76,19 @@ func (l *NullLogger) Notice(s string, v ...interface{}) (err error) {
func (l *NullLogger) Warning(s string, v ...interface{}) (err error) { func (l *NullLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger (that doesn't actually write to anything).
func (l *NullLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(&nullWriter{}, "", 0)
return
}
// ToRaw returns a *logWriter. (This is a little less efficient than using ToLogger's log.Logger as an io.Writer if that's all you need.)
func (l *NullLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@@ -0,0 +1,58 @@
package logging
import (
"unicode/utf8"
)
// Close conforms a nullWriter to an io.WriteCloser. It obviously does nothing, and will always return with err == nil.
func (nw *nullWriter) Close() (err error) {
// NO-OP
return
}
// Write conforms a nullWriter to an io.Writer, but it writes... nothing. To avoid errors, however, in downstream code it pretends it does (n will *always* == len(b)).
func (nw *nullWriter) Write(b []byte) (n int, err error) {
if b == nil {
return
}
n = len(b)
return
}
// WriteByte conforms to an io.ByteWriter but again... nothing is actually written anywhere.
func (nw *nullWriter) WriteByte(c byte) (err error) {
// NO-OP
_ = c
return
}
/*
WriteRune conforms to the other Loggers. It WILL return the proper value for n (matching (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune() signatures,
and it WILL return an ErrInvalidRune if r is not a valid rune, but otherwise it will no-op.
*/
func (nw *nullWriter) WriteRune(r rune) (n int, err error) {
n = utf8.RuneLen(r)
if n < 0 {
err = ErrInvalidRune
n = 0
return
}
return
}
// WriteString conforms to an io.StringWriter but nothing is actually written. (n will *always* == len(s))
func (nw *nullWriter) WriteString(s string) (n int, err error) {
n = len(s)
return
}

138
logging/funcs_oldnix.go Normal file
View File

@@ -0,0 +1,138 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
// Linux is excluded because it has its own.
package logging
import (
native "log"
"os"
"path"
"r00t2.io/goutils/bitmask"
"r00t2.io/sysutils/paths"
)
var (
_ = native.Logger{}
_ = os.Interrupt
)
/*
GetLogger returns an instance of Logger that best suits your system's capabilities.
If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written.
If prefix is "\x00" (a null byte), then the default logging prefix will be used. If anything else, even an empty string,
is specified then that will be used instead for the prefix.
logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and
https://pkg.go.dev/log#pkg-constants for details.
logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to,
it will be used (assuming you have no higher-level loggers available). Only the first logPaths entry that "works" will be used, later entries will be ignored.
If you want to log to multiple files simultaneously, use a MultiLogger instead.
If you call GetLogger, you will only get a single ("best") logger your system supports.
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
use GetMultiLogger.
*/
func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
var logPath string
var logFlags bitmask.MaskBit
var currentPrefix string
// Configure system-supported logger(s).
// If we can detect syslog, use that. If not, try to use a file logger (+ stdout).
// Last ditch, stdout.
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
}
if hasSyslog && !stat.Mode().IsRegular() {
logFlags.AddFlag(LogSyslog)
} else {
var exists bool
var success bool
var ckLogPaths []string
logFlags.AddFlag(LogStdout)
ckLogPaths = defLogPaths
if logPaths != nil {
ckLogPaths = logPaths
}
for _, p := range ckLogPaths {
if exists, _ = paths.RealPathExists(&p); exists {
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
} else {
dirPath := path.Dir(p)
if err = paths.MakeDirIfNotExist(dirPath); err != nil {
continue
}
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
}
}
}
if logFlags.HasFlag(LogSyslog) {
logger = &SyslogLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
}
} else {
if logFlags.HasFlag(LogFile) {
logger = &FileLogger{
StdLogger: StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
},
Path: logPath,
}
} else {
logger = &StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
}
}
}
if prefix != "\x00" {
if err = logger.SetPrefix(prefix); err != nil {
return
}
}
if err = logger.Setup(); err != nil {
return
}
if currentPrefix, err = logger.GetPrefix(); err != nil {
return
}
logger.Debug("logger initialized of type %T with prefix %v", logger, currentPrefix)
return
}

View File

@@ -235,6 +235,22 @@ func (l *StdLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *StdLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *StdLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}
// renderWrite prepares/formats a log message to be written to this StdLogger. // renderWrite prepares/formats a log message to be written to this StdLogger.
func (l *StdLogger) renderWrite(msg, prio string) { func (l *StdLogger) renderWrite(msg, prio string) {

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/coreos/go-systemd/journal" "github.com/coreos/go-systemd/v22/journal"
) )
/* /*
@@ -223,3 +223,19 @@ func (l *SystemDLogger) renderWrite(msg string, prio journal.Priority) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *SystemDLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *SystemDLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@@ -1,3 +1,6 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging package logging
import ( import (
@@ -266,3 +269,19 @@ func (l *SyslogLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *SyslogLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *SyslogLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@@ -3,6 +3,7 @@ package logging
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"syscall" "syscall"
@@ -342,3 +343,19 @@ func (l *WinLogger) Warning(s string, v ...interface{}) (err error) {
return return
} }
// ToLogger returns a stdlib log.Logger.
func (l *WinLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *WinLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@@ -3,8 +3,12 @@ package logging
import ( import (
"log" "log"
"os" "os"
"r00t2.io/goutils/bitmask"
) )
type logPrio bitmask.MaskBit
/* /*
Logger is one of the various loggers offered by this module. Logger is one of the various loggers offered by this module.
*/ */
@@ -23,6 +27,8 @@ type Logger interface {
GetPrefix() (p string, err error) GetPrefix() (p string, err error)
Setup() (err error) Setup() (err error)
Shutdown() (err error) Shutdown() (err error)
ToLogger(prio logPrio) (stdLibLog *log.Logger)
ToRaw(prio logPrio) (raw *logWriter)
} }
/* /*
@@ -105,3 +111,12 @@ type MultiLogger struct {
*/ */
Loggers map[string]Logger Loggers map[string]Logger
} }
// logWriter is used as a log.Logger and is returned by <Logger>.ToLogger.
type logWriter struct {
backend Logger
prio logPrio
}
// nullWriter is used as a shortcut by NullLogger.ToLogger.
type nullWriter struct{}

View File

@@ -1,27 +1,9 @@
package logging package logging
import (
`log/syslog`
)
/* /*
SystemDLogger (yes, I'm aware it's actually written as "systemd") writes to journald on systemd-enabled systems. SystemDLogger (yes, I'm aware it's actually written as "systemd") writes to journald on systemd-enabled systems.
*/ */
type SystemDLogger struct { type SystemDLogger struct {
EnableDebug bool EnableDebug bool
Prefix string Prefix string
} }
// SyslogLogger writes to syslog on syslog-enabled systems.
type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
}

22
logging/types_nix.go Normal file
View File

@@ -0,0 +1,22 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"log/syslog"
)
// SyslogLogger writes to syslog on syslog-enabled systems.
type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
}

View File

@@ -69,6 +69,9 @@ func (e *MultiError) Error() (errStr string) {
numErrs = len(e.Errors) numErrs = len(e.Errors)
} }
e.lock.Lock()
defer e.lock.Unlock()
for idx, err := range e.Errors { for idx, err := range e.Errors {
if (idx + 1) < numErrs { if (idx + 1) < numErrs {
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep) errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
@@ -87,6 +90,9 @@ func (e *MultiError) AddError(err error) {
return return
} }
e.lock.Lock()
defer e.lock.Unlock()
e.Errors = append(e.Errors, err) e.Errors = append(e.Errors, err)
} }

View File

@@ -1,9 +1,14 @@
package multierr package multierr
import (
`sync`
)
// MultiError is a type of error.Error that can contain multiple errors. // MultiError is a type of error.Error that can contain multiple errors.
type MultiError struct { type MultiError struct {
// Errors is a slice of errors to combine/concatenate when .Error() is called. // Errors is a slice of errors to combine/concatenate when .Error() is called.
Errors []error `json:"errors"` Errors []error `json:"errors"`
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n". // ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
ErrorSep string `json:"separator"` ErrorSep string `json:"separator"`
lock sync.Mutex
} }

4
remap/doc.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package remap provides convenience functions around regular expressions, primarily offering maps for named capture groups.
*/
package remap

437
remap/funcs_remap.go Normal file
View File

@@ -0,0 +1,437 @@
package remap
/*
Map returns a map[string][]<match bytes> for regexes with named capture groups matched in bytes b.
Note that this supports non-unique group names; [regexp.Regexp] allows for patterns with multiple groups
using the same group name (though your IDE might complain; I know GoLand does).
Each match for each group is in a slice keyed under that group name, with that slice
ordered by the indexing done by the regex match itself.
In summary, the parameters are as follows:
# inclNoMatch
If true, then attempt to return a non-nil matches (as long as b isn't nil).
Group keys will be populated and explicitly defined as nil.
For example, if a pattern
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but b does not match then matches will be:
map[string][][]byte{
"g1": nil,
"g2": nil,
}
# inclNoMatchStrict
If true (and inclNoMatch is true), instead of a single nil the group's values will be
a slice of nil values explicitly matching the number of times the group name is specified
in the pattern.
For example, if a pattern:
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but b does not match then matches will be:
map[string][][]byte{
"g1": [][]byte{
nil,
nil,
},
"g2": [][]byte{
nil,
},
}
# mustMatch
If true, matches will be nil if the entirety of b does not match the pattern (and thus
no capture groups matched) (overrides inclNoMatch) -- explicitly:
matches == nil
Otherwise if false (and assuming inclNoMatch is false), matches will be:
map[string][][]byte{}{}
# Condition Tree
In detail, matches and/or its values may be nil or empty under the following condition tree:
IF b is nil:
THEN matches will always be nil
ELSE:
IF all of b does not match pattern
IF mustMuch is true
THEN matches == nil
ELSE
THEN matches == map[string][][]byte{} (non-nil but empty)
ELSE IF pattern has no named capture groups
IF inclNoMatch is true
THEN matches == map[string][][]byte{} (non-nil but empty)
ELSE
THEN matches == nil
ELSE
IF there are no named group matches
IF inclNoMatch is true
THEN matches is non-nil; matches[<group name>, ...] is/are defined but nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches == nil
ELSE
IF <group name> does not have a match
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == [][]byte{nil[, nil...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches[<group name>] is not defined (_, ok = matches[<group name>]; ok == false)
ELSE
matches[<group name>] == []{<match>[, <match>...]}
*/
func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][][]byte) {
var ok bool
var mIdx int
var match []byte
var grpNm string
var names []string
var matchBytes [][]byte
var tmpMap map[string][][]byte = make(map[string][][]byte)
if b == nil {
return
}
names = r.Regexp.SubexpNames()
matchBytes = r.Regexp.FindSubmatch(b)
if matchBytes == nil {
// b does not match pattern
if !mustMatch {
matches = make(map[string][][]byte)
}
return
}
if names == nil || len(names) == 0 || len(names) == 1 {
/*
no named capture groups;
technically only the last condition would be the case.
*/
if inclNoMatch {
matches = make(map[string][][]byte)
}
return
}
names = names[1:]
if len(matchBytes) == 0 || len(matchBytes) == 1 {
/*
no submatches whatsoever.
*technically* I don't think this condition can actually be reached.
This is more of a safe-return before we re-slice.
*/
matches = make(map[string][][]byte)
if inclNoMatch {
if len(names) >= 1 {
for _, grpNm = range names {
matches[grpNm] = nil
}
}
}
return
}
matchBytes = matchBytes[1:]
for mIdx, match = range matchBytes {
grpNm = names[mIdx]
/*
Thankfully, it's actually a build error if a pattern specifies a named
capture group with an empty name.
So we don't need to worry about accounting for that,
and can just skip over grpNm == "" (which is an *unnamed* capture group).
*/
if grpNm == "" {
continue
}
if match == nil {
// group did not match
if !inclNoMatch {
continue
}
if _, ok = tmpMap[grpNm]; !ok {
if !inclNoMatchStrict {
tmpMap[grpNm] = nil
} else {
tmpMap[grpNm] = [][]byte{nil}
}
} else {
if inclNoMatchStrict {
tmpMap[grpNm] = append(tmpMap[grpNm], nil)
}
}
continue
}
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = make([][]byte, 0)
}
tmpMap[grpNm] = append(tmpMap[grpNm], match)
}
// This *technically* should be completely handled above.
if inclNoMatch {
for _, grpNm = range names {
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = nil
}
}
}
if len(tmpMap) > 0 {
matches = tmpMap
}
return
}
/*
MapString is exactly like ReMap.Map(), but operates on (and returns) strings instead.
(matches will always be nil if s == “.)
A small deviation, though; empty strings instead of nils (because duh) will occupy slice placeholders (if `inclNoMatchStrict` is specified).
This unfortunately *does not provide any indication* if an empty string positively matched the pattern (a "hit") or if it was simply
not matched at all (a "miss"). If you need definitive determination between the two conditions, it is instead recommended to either
*not* use inclNoMatchStrict or to use ReMap.Map() instead and convert any non-nil values to strings after.
Particularly:
# inclNoMatch
If true, then attempt to return a non-nil matches (as long as s isn't empty).
Group keys will be populated and explicitly defined as nil.
For example, if a pattern
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but s does not match then matches will be:
map[string][]string{
"g1": nil,
"g2": nil,
}
# inclNoMatchStrict
If true (and inclNoMatch is true), instead of a single nil the group's values will be
a slice of eempty string values explicitly matching the number of times the group name is specified
in the pattern.
For example, if a pattern:
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but s does not match then matches will be:
map[string][]string{
"g1": []string{
"",
"",
},
"g2": []string{
"",
},
}
# mustMatch
If true, matches will be nil if the entirety of s does not match the pattern (and thus
no capture groups matched) (overrides inclNoMatch) -- explicitly:
matches == nil
Otherwise if false (and assuming inclNoMatch is false), matches will be:
map[string][]string{}{}
# Condition Tree
In detail, matches and/or its values may be nil or empty under the following condition tree:
IF s is empty:
THEN matches will always be nil
ELSE:
IF all of s does not match pattern
IF mustMuch is true
THEN matches == nil
ELSE
THEN matches == map[string][]string{} (non-nil but empty)
ELSE IF pattern has no named capture groups
IF inclNoMatch is true
THEN matches == map[string][]string{} (non-nil but empty)
ELSE
THEN matches == nil
ELSE
IF there are no named group matches
IF inclNoMatch is true
THEN matches is non-nil; matches[<group name>, ...] is/are defined but nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches == nil
ELSE
IF <group name> does not have a match
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == []string{""[, ""...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches[<group name>] is not defined (_, ok = matches[<group name>]; ok == false)
ELSE
matches[<group name>] == []{<match>[, <match>...]}
*/
func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][]string) {
var ok bool
var endIdx int
var startIdx int
var chunkIdx int
var grpNm string
var names []string
var matchStr string
var idxChunks [][]int
var matchIndices []int
var chunkIndices []int // always 2 elements; start pos and end pos
var tmpMap map[string][]string = make(map[string][]string)
/*
OK so this is a bit of a deviation.
It's not as straightforward as above, because there isn't an explicit way
like above to determine if a patterb was *matched as an empty string* vs.
*not matched*.
So instead do roundabout index-y things.
*/
if s == "" {
return
}
names = r.Regexp.SubexpNames()
matchIndices = r.Regexp.FindStringSubmatchIndex(s)
if matchIndices == nil {
// s does not match pattern
if !mustMatch {
matches = make(map[string][]string)
}
return
}
if names == nil || len(names) == 0 || len(names) == 1 {
/*
no named capture groups;
technically only the last condition would be the case.
*/
if inclNoMatch {
matches = make(map[string][]string)
}
return
}
names = names[1:]
if len(matchIndices) == 0 || len(matchIndices) == 1 {
/*
no submatches whatsoever.
*technically* I don't think this condition can actually be reached.
This is more of a safe-return before we chunk the indices.
*/
matches = make(map[string][]string)
if inclNoMatch {
if len(names) >= 1 {
for _, grpNm = range names {
matches[grpNm] = nil
}
}
}
return
}
/*
The reslice starts at 2 because they're in pairs: []int{<start>, <end>, <start>, <end>, ...}
and the first *pair* is the entire pattern match.
Thus the len(matchIndices) == 2*len(names).
Keep in mind that since the first element of names is removed,
the first pair here is also removed.
*/
matchIndices = matchIndices[2:]
idxChunks = make([][]int, len(names))
for startIdx = 0; startIdx < len(idxChunks); startIdx += 2 {
endIdx = startIdx + 2
grpNm = names[chunkIdx]
/*
Thankfully, it's actually a build error if a pattern specifies a named
capture group with an empty name.
So we don't need to worry about accounting for that,
and can just skip over grpNm == "" (which is an *unnamed* capture group).
*/
if grpNm == "" {
continue
}
// This technically should never happen.
if endIdx > len(matchIndices) {
endIdx = len(matchIndices)
}
chunkIndices = matchIndices[startIdx:endIdx]
if chunkIndices[0] == -1 || chunkIndices[1] == -1 {
// group did not match
if !inclNoMatch {
continue
}
if _, ok = tmpMap[grpNm]; !ok {
if !inclNoMatchStrict {
tmpMap[grpNm] = nil
} else {
tmpMap[grpNm] = []string{""}
}
} else {
if inclNoMatchStrict {
tmpMap[grpNm] = append(tmpMap[grpNm], "")
}
}
continue
}
matchStr = s[chunkIndices[0]:chunkIndices[1]]
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = make([]string, 0)
}
tmpMap[grpNm] = append(tmpMap[grpNm], matchStr)
chunkIdx++
}
// This *technically* should be completely handled above.
if inclNoMatch {
for _, grpNm = range names {
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = nil
}
}
}
if len(tmpMap) > 0 {
matches = tmpMap
}
return
}

27
remap/types.go Normal file
View File

@@ -0,0 +1,27 @@
package remap
import (
"regexp"
)
type (
// ReMap provides some map-related functions around a regexp.Regexp.
ReMap struct {
*regexp.Regexp
}
// TODO?
/*
ExplicitStringMatch is used with ReMap.MapStringExplicit to indicate if a
capture group result is a hit (a group matched, but e.g. the match value is empty string)
or not (a group did not match).
*/
/*
ExplicitStringMatch struct {
Group string
IsMatch bool
Value string
}
*/
)

5
structutils/consts.go Normal file
View File

@@ -0,0 +1,5 @@
package structutils
const (
TagMapTrim tagMapOpt = iota
)

362
structutils/funcs.go Normal file
View File

@@ -0,0 +1,362 @@
/*
GoUtils - a library to assist with various Golang-related functions
Copyright (C) 2020 Brent Saner
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package structutils
import (
`reflect`
`strings`
)
/*
TagToBoolMap takes struct field `field` and tag name `tagName`,
optionally with options `opts`, and returns a map of the tag values.
The tag value string is assumed to be in the form of:
option[,option,option...]
and returns a map[string]bool (map[option]true).
If field does not have tag tagName, m will be nil.
See the TagMap* constants for opts.
*/
func TagToBoolMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (m map[string]bool) {
var s string
var optSplit []string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
s = field.Tag.Get(tagName)
if strings.TrimSpace(s) == "" {
return
}
optSplit = strings.Split(s, ",")
if optSplit == nil || len(optSplit) == 0 {
return
}
m = make(map[string]bool)
for _, o := range optSplit {
if tagOpts[TagMapTrim] {
o = strings.TrimSpace(o)
}
m[o] = true
}
return
}
/*
TagToBoolMapWithValue is like TagToBoolMap but additionally assumes the first value is an "identifier".
The tag value string is assumed to be in the form of:
value,option[,option,option...]
and returns a map[string]bool (map[option]true) with the value.
*/
func TagToBoolMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, m map[string]bool) {
var s string
var optSplit []string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
s = field.Tag.Get(tagName)
if strings.TrimSpace(s) == "" {
return
}
optSplit = strings.Split(s, ",")
if optSplit == nil || len(optSplit) == 0 {
return
}
m = make(map[string]bool)
for idx, o := range optSplit {
if idx == 0 {
if tagOpts[TagMapTrim] {
o = strings.TrimSpace(o)
}
value = o
continue
}
if tagOpts[TagMapTrim] {
o = strings.TrimSpace(o)
}
m[o] = true
}
return
}
/*
TagToMixedMap combines TagToBoolMap and TagToStringMap.
It takes struct field `field` and tag name `tagName`,
and returns all single-value options in mapBool, and all key/value options in mapString.
If field does not have tag tagName, m will be nil.
See the TagMap* constants for opts.
*/
func TagToMixedMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (mapBool map[string]bool, mapString map[string]string) {
var s string
var valStr string
var split []string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
s = field.Tag.Get(tagName)
if strings.TrimSpace(s) == "" {
return
}
split = strings.Split(s, ",")
if split == nil || len(split) == 0 {
return
}
mapBool = make(map[string]bool)
mapString = make(map[string]string)
for _, valStr = range split {
if strings.Contains(valStr, "=") {
kvSplit = strings.SplitN(valStr, "=", 2)
if kvSplit == nil || len(kvSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = kvSplit[1]
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
mapString[k] = v
} else {
if tagOpts[TagMapTrim] {
valStr = strings.TrimSpace(valStr)
}
mapBool[valStr] = true
}
}
return
}
/*
TagToMixedMapWithValue combines TagToBoolMapWithValue and TagToStringMapWithValue.
It takes struct field `field` and tag name `tagName`,
and returns all single-value options in mapBool, and all key/value options in mapString
along with the first single-value option as value..
If field does not have tag tagName, m will be nil.
See the TagMap* constants for opts.
*/
func TagToMixedMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, mapBool map[string]bool, mapString map[string]string) {
var s string
var idx int
var valStr string
var split []string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
s = field.Tag.Get(tagName)
if strings.TrimSpace(s) == "" {
return
}
split = strings.Split(s, ",")
if split == nil || len(split) == 0 {
return
}
mapBool = make(map[string]bool)
mapString = make(map[string]string)
for idx, valStr = range split {
if idx == 0 {
if tagOpts[TagMapTrim] {
valStr = strings.TrimSpace(valStr)
}
value = valStr
continue
}
if strings.Contains(valStr, "=") {
kvSplit = strings.SplitN(valStr, "=", 2)
if kvSplit == nil || len(kvSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = kvSplit[1]
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
mapString[k] = v
} else {
if tagOpts[TagMapTrim] {
valStr = strings.TrimSpace(valStr)
}
mapBool[valStr] = true
}
}
return
}
/*
TagToStringMap takes struct field `field` and tag name `tagName`,
optionally with options `opts`, and returns a map of the tag values.
The tag value string is assumed to be in the form of:
key=value[,key=value,key=value...]
and returns a map[string]string (map[key]value).
It is proccessed in order; later duplicate keys overwrite previous ones.
If field does not have tag tagName, m will be nil.
If only a key is provided with no value, the value in the map will be an empty string.
(e.g. "foo,bar=baz" => map[string]string{"foo": "", "bar: "baz"}
See the TagMap* constants for opts.
*/
func TagToStringMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (m map[string]string) {
var s string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
s = field.Tag.Get(tagName)
if strings.TrimSpace(s) == "" {
return
}
kvSplit = strings.Split(s, ",")
if kvSplit == nil || len(kvSplit) == 0 {
return
}
for _, kv := range kvSplit {
valSplit = strings.SplitN(kv, "=", 2)
if valSplit == nil || len(valSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = valSplit[1]
// It's not possible to have more than 2.
}
if m == nil {
m = make(map[string]string)
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
m[k] = v
}
return
}
/*
TagToStringMapWithValue is like TagToStringMap but additionally assumes the first value is an "identifier".
The tag value string is assumed to be in the form of:
value,key=value[,key=value,key=value...]
and returns a map[string]string (map[key]value) with the value.
*/
func TagToStringMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, m map[string]string) {
var s string
var kvSplit []string
var valSplit []string
var k string
var v string
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
s = field.Tag.Get(tagName)
if strings.TrimSpace(s) == "" {
return
}
kvSplit = strings.Split(s, ",")
if kvSplit == nil || len(kvSplit) == 0 {
return
}
for idx, kv := range kvSplit {
if idx == 0 {
if tagOpts[TagMapTrim] {
kv = strings.TrimSpace(kv)
}
value = kv
continue
}
valSplit = strings.SplitN(kv, "=", 2)
if valSplit == nil || len(valSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = valSplit[1]
// It's not possible to have more than 2.
}
if m == nil {
m = make(map[string]string)
}
if tagOpts[TagMapTrim] {
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
}
m[k] = v
}
return
}
func getTagMapOpts(opts []tagMapOpt) (optMap map[tagMapOpt]bool) {
optMap = make(map[tagMapOpt]bool)
if opts == nil {
return
}
return
}

5
structutils/types.go Normal file
View File

@@ -0,0 +1,5 @@
package structutils
type (
tagMapOpt uint8
)