Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
688abd0874
|
||
|
|
a1f87d6b51
|
||
|
|
07951f1f03
|
||
|
|
bae0abe960
|
||
|
|
368ae0cb8e
|
||
|
|
154170c0e5
|
||
|
|
d9bd928edb
|
||
|
|
dc2ed32352
|
||
|
|
e734e847c4
|
||
|
|
2203de4e32
|
||
|
|
a0c6df14aa
|
||
|
|
fd720f2b34
|
||
|
|
3c543a05e7
|
||
|
|
e5191383a7
|
||
|
|
ae49f42c0c
|
||
|
|
b87934e8a9
|
||
|
70d6c2cbb3
|
19
.encoding.TODO/bit/docs.go
Normal file
19
.encoding.TODO/bit/docs.go
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Package bit aims to provide feature parity with stdlib's [encoding/hex].
|
||||
|
||||
It's a ludicrous tragedy that hex/base16, base32, base64 all have libraries for converting
|
||||
to/from string representations... but there's nothing for binary ('01010001' etc.) whatsoever.
|
||||
|
||||
This package also provides some extra convenience functions and types in an attempt to provide
|
||||
an abstracted bit-level fidelity in Go. A [Bit] is a bool type, in which that underlying bool
|
||||
being false represents a 0 and that underlying bool being true represents a 1.
|
||||
|
||||
Note that a [Bit] or arbitrary-length or non-octal-aligned [][Bit] may take up more bytes in memory
|
||||
than expected; a [Bit] will actually always occupy a single byte -- thus representing
|
||||
`00000000 00000000` as a [][Bit] or [16][Bit] will actually occupy *sixteen bytes* in memory,
|
||||
NOT 2 bytes (nor, obviously, [2][Byte])!
|
||||
It is recommended instead to use a [Bits] instead of a [Bit] slice or array, as it will try to properly align to the
|
||||
smallest memory allocation possible (at the cost of a few extra CPU cycles on adding/removing one or more [Bit]).
|
||||
It will properly retain any appended, prepended, leading, or trailing bits that do not currently align to a byte.
|
||||
*/
|
||||
package bit
|
||||
14
.encoding.TODO/bit/funcs.go
Normal file
14
.encoding.TODO/bit/funcs.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package bit
|
||||
|
||||
// TODO: Provide analogues of encoding/hex, encoding/base64, etc. functions etc.
|
||||
|
||||
/*
|
||||
TODO: Also provide interfaces for the following:
|
||||
|
||||
* https://pkg.go.dev/encoding#BinaryAppender
|
||||
* https://pkg.go.dev/encoding#BinaryMarshaler
|
||||
* https://pkg.go.dev/encoding#BinaryUnmarshaler
|
||||
* https://pkg.go.dev/encoding#TextAppender
|
||||
* https://pkg.go.dev/encoding#TextMarshaler
|
||||
* https://pkg.go.dev/encoding#TextUnmarshaler
|
||||
*/
|
||||
34
.encoding.TODO/bit/types.go
Normal file
34
.encoding.TODO/bit/types.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package bit
|
||||
|
||||
type (
|
||||
// Bit aims to provide a native-like type for a single bit (Golang operates on the smallest fidelity level of *byte*/uint8).
|
||||
Bit bool
|
||||
|
||||
// Bits is an arbitrary length of bits.
|
||||
Bits struct {
|
||||
/*
|
||||
leading is a series of Bit that do not cleanly align to the beginning of Bits.b.
|
||||
They will always be the bits at the *beginning* of the sequence.
|
||||
len(Bits.leading) will *never* be more than 7;
|
||||
it's converted into a byte, prepended to Bits.b, and cleared if it reaches that point.
|
||||
*/
|
||||
leading []Bit
|
||||
// b is the condensed/memory-aligned alternative to an [][8]Bit (or []Bit, or [][]Bit, etc.).
|
||||
b []byte
|
||||
/*
|
||||
remaining is a series of Bit that do not cleanly align to the end of Bits.b.
|
||||
They will always be the bits at the *end* of the sequence.
|
||||
len(Bits.remaining) will *never* be more than 7;
|
||||
it's converted into a byte, appended to Bits.b, and cleared if it reaches that point.
|
||||
*/
|
||||
remaining []Bit
|
||||
// fixedLen, if 0, represents a "slice". If >= 1, it represents an "array".
|
||||
fixedLen uint
|
||||
}
|
||||
|
||||
// Byte is this package's representation of a byte. It's primarily for convenience.
|
||||
Byte byte
|
||||
|
||||
// Bytes is defined as a type for convenience single-call functions.
|
||||
Bytes []Byte
|
||||
)
|
||||
@@ -2,8 +2,8 @@ package bitmask
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
@@ -34,11 +34,47 @@ func NewMaskBitExplicit(value uint) (m *MaskBit) {
|
||||
return
|
||||
}
|
||||
|
||||
// HasFlag is true if m has MaskBit flag set/enabled.
|
||||
/*
|
||||
HasFlag is true if m has MaskBit flag set/enabled.
|
||||
THIS WILL RETURN FALSE FOR OR'd FLAGS.
|
||||
For example:
|
||||
|
||||
flagA MaskBit = 0x01
|
||||
flagB MaskBit = 0x02
|
||||
flagComposite = flagA | flagB
|
||||
|
||||
m *MaskBit = NewMaskBitExplicit(uint(flagA))
|
||||
|
||||
m.HasFlag(flagComposite) will return false even though flagComposite is an OR
|
||||
that contains flagA.
|
||||
Use [MaskBit.IsOneOf] instead if you do not desire this behavior,
|
||||
and instead want to test composite flag *membership*.
|
||||
(MaskBit.IsOneOf will also return true for non-composite equality.)
|
||||
*/
|
||||
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
|
||||
|
||||
var b MaskBit = *m
|
||||
|
||||
if b&flag == flag {
|
||||
r = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsOneOf is like a "looser" form of [MaskBit.HasFlag]
|
||||
in that it allows for testing composite membership.
|
||||
|
||||
See [MaskBit.HasFlag] for more information.
|
||||
|
||||
If composite is *not* an OR'd MaskBit (i.e.
|
||||
it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.),
|
||||
then IsOneOf will behave exactly like HasFlag.
|
||||
*/
|
||||
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) {
|
||||
|
||||
var b MaskBit = *m
|
||||
|
||||
if b&flag != 0 {
|
||||
r = true
|
||||
}
|
||||
@@ -56,7 +92,7 @@ func (m *MaskBit) AddFlag(flag MaskBit) {
|
||||
// ClearFlag removes MaskBit flag from m.
|
||||
func (m *MaskBit) ClearFlag(flag MaskBit) {
|
||||
|
||||
*m &= flag
|
||||
*m &^= flag
|
||||
|
||||
return
|
||||
}
|
||||
@@ -109,6 +145,15 @@ func (m *MaskBit) Bytes(trim bool) (b []byte) {
|
||||
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.
|
||||
func (m *MaskBit) Value() (v uint) {
|
||||
|
||||
|
||||
15
go.mod
15
go.mod
@@ -1,10 +1,15 @@
|
||||
module r00t2.io/goutils
|
||||
|
||||
go 1.16
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/google/uuid v1.3.0
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
||||
r00t2.io/sysutils v1.1.1
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/sys v0.34.0
|
||||
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
21
go.sum
@@ -1,8 +1,13 @@
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
r00t2.io/sysutils v1.1.1 h1:q2P5u50HIIRk6muCPo1Gpapy6sNT4oaB1l2O/C/mi3A=
|
||||
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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
4
iox/docs.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package iox includes extensions to the stdlib `io` module.
|
||||
*/
|
||||
package iox
|
||||
9
iox/errs.go
Normal file
9
iox/errs.go
Normal 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
41
iox/funcs.go
Normal 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
8
iox/types.go
Normal 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)
|
||||
}
|
||||
)
|
||||
14
logging/TODO
14
logging/TODO
@@ -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?):
|
||||
https://stackoverflow.com/a/24809646
|
||||
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.
|
||||
-- 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?
|
||||
|
||||
|
||||
@@ -12,3 +12,16 @@ const (
|
||||
// appendFlags are the flags used for testing the file (and opening/writing).
|
||||
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
9
logging/consts_darwin.go
Normal 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",
|
||||
}
|
||||
)
|
||||
@@ -1,32 +1,5 @@
|
||||
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 (
|
||||
// defLogPaths indicates default log paths.
|
||||
defLogPaths = []string{
|
||||
|
||||
34
logging/consts_nix.go
Normal file
34
logging/consts_nix.go
Normal 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
|
||||
)
|
||||
@@ -7,13 +7,16 @@ These particular loggers (logging.Logger) available are:
|
||||
StdLogger
|
||||
FileLogger
|
||||
SystemDLogger (Linux only)
|
||||
SyslogLogger (Linux only)
|
||||
SyslogLogger (Linux/macOS/other *NIX-like 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`errors`
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -12,6 +12,8 @@ var (
|
||||
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")
|
||||
// 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 error = errors.New("the Logger specified to be removed does not exist")
|
||||
)
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// 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")
|
||||
// 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
19
logging/errs_nix.go
Normal 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))
|
||||
)
|
||||
@@ -1,9 +1,33 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
"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.
|
||||
func testOpen(path string) (success bool, err error) {
|
||||
|
||||
|
||||
@@ -220,6 +220,22 @@ func (l *FileLogger) Warning(s string, v ...interface{}) (err error) {
|
||||
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.
|
||||
func (l *FileLogger) renderWrite(msg, prio string) {
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
`os`
|
||||
`path`
|
||||
|
||||
sysd `github.com/coreos/go-systemd/journal`
|
||||
sysd `github.com/coreos/go-systemd/v22/journal`
|
||||
`r00t2.io/goutils/bitmask`
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
23
logging/funcs_logprio.go
Normal file
23
logging/funcs_logprio.go
Normal 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
209
logging/funcs_logwriter.go
Normal 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
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package logging
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"r00t2.io/goutils/multierr"
|
||||
@@ -370,3 +371,19 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`os`
|
||||
|
||||
sysd `github.com/coreos/go-systemd/journal`
|
||||
`github.com/google/uuid`
|
||||
`r00t2.io/sysutils/paths`
|
||||
sysd "github.com/coreos/go-systemd/v22/journal"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -80,55 +77,3 @@ func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
63
logging/funcs_multilogger_mgr_nix.go
Normal file
63
logging/funcs_multilogger_mgr_nix.go
Normal 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
|
||||
}
|
||||
41
logging/funcs_multilogger_mgr_oldnix.go
Normal file
41
logging/funcs_multilogger_mgr_oldnix.go
Normal 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
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// Setup does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Setup() (err error) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
58
logging/funcs_nullwriter.go
Normal file
58
logging/funcs_nullwriter.go
Normal 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
138
logging/funcs_oldnix.go
Normal 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
|
||||
}
|
||||
@@ -235,6 +235,22 @@ func (l *StdLogger) Warning(s string, v ...interface{}) (err error) {
|
||||
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.
|
||||
func (l *StdLogger) renderWrite(msg, prio string) {
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build !(windows || plan9 || wasip1 || js || ios)
|
||||
// +build !windows,!plan9,!wasip1,!js,!ios
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
@@ -266,3 +269,19 @@ func (l *SyslogLogger) Warning(s string, v ...interface{}) (err error) {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package logging
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
@@ -342,3 +343,19 @@ func (l *WinLogger) Warning(s string, v ...interface{}) (err error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package logging
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"r00t2.io/goutils/bitmask"
|
||||
)
|
||||
|
||||
type logPrio bitmask.MaskBit
|
||||
|
||||
/*
|
||||
Logger is one of the various loggers offered by this module.
|
||||
*/
|
||||
@@ -23,6 +27,8 @@ type Logger interface {
|
||||
GetPrefix() (p string, err error)
|
||||
Setup() (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
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`log/syslog`
|
||||
)
|
||||
|
||||
/*
|
||||
SystemDLogger (yes, I'm aware it's actually written as "systemd") writes to journald on systemd-enabled systems.
|
||||
*/
|
||||
@@ -11,17 +7,3 @@ type SystemDLogger struct {
|
||||
EnableDebug bool
|
||||
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
22
logging/types_nix.go
Normal 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
|
||||
}
|
||||
@@ -69,6 +69,9 @@ func (e *MultiError) Error() (errStr string) {
|
||||
numErrs = len(e.Errors)
|
||||
}
|
||||
|
||||
e.lock.Lock()
|
||||
defer e.lock.Unlock()
|
||||
|
||||
for idx, err := range e.Errors {
|
||||
if (idx + 1) < numErrs {
|
||||
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
|
||||
@@ -87,6 +90,9 @@ func (e *MultiError) AddError(err error) {
|
||||
return
|
||||
}
|
||||
|
||||
e.lock.Lock()
|
||||
defer e.lock.Unlock()
|
||||
|
||||
e.Errors = append(e.Errors, err)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package multierr
|
||||
|
||||
import (
|
||||
`sync`
|
||||
)
|
||||
|
||||
// MultiError is a type of error.Error that can contain multiple errors.
|
||||
type MultiError struct {
|
||||
// Errors is a slice of errors to combine/concatenate when .Error() is called.
|
||||
Errors []error `json:"errors"`
|
||||
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
|
||||
ErrorSep string `json:"separator"`
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
4
remap/doc.go
Normal file
4
remap/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package remap provides convenience functions around regular expressions, primarily offering maps for named capture groups.
|
||||
*/
|
||||
package remap
|
||||
488
remap/funcs_remap.go
Normal file
488
remap/funcs_remap.go
Normal file
@@ -0,0 +1,488 @@
|
||||
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
|
||||
/*
|
||||
A slice of indices or index pairs.
|
||||
For each element `e` in idxChunks,
|
||||
* if `e` is nil, no group match.
|
||||
* if len(e) == 1, only a single character was matched.
|
||||
* otherwise len(e) == 2, the start and end of the match.
|
||||
*/
|
||||
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 pattern was *matched as an empty string* vs.
|
||||
*not matched*.
|
||||
|
||||
So instead do roundabout index-y things.
|
||||
*/
|
||||
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
/*
|
||||
I'm not entirely sure how serious they are about "the slice should not be modified"...
|
||||
|
||||
DO NOT sort or dedupe `names`! If the same name for groups is duplicated,
|
||||
it will be duplicated here in proper order and the ordering is tied to
|
||||
the ordering of matchIndices.
|
||||
*/
|
||||
names = r.Regexp.SubexpNames()[:]
|
||||
matchIndices = r.Regexp.FindStringSubmatchIndex(s)
|
||||
|
||||
if matchIndices == nil {
|
||||
// s does not match pattern at all.
|
||||
if !mustMatch {
|
||||
matches = make(map[string][]string)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if names == nil || len(names) <= 1 {
|
||||
/*
|
||||
No named capture groups;
|
||||
technically only the last condition would be the case,
|
||||
as (regexp.Regexp).SubexpNames() will ALWAYS at the LEAST
|
||||
return a `[]string{""}`.
|
||||
*/
|
||||
if inclNoMatch {
|
||||
matches = make(map[string][]string)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(matchIndices) == 0 || len(matchIndices) == 1 {
|
||||
/*
|
||||
No (sub)matches whatsoever.
|
||||
*technically* I don't think this condition can actually be reached;
|
||||
matchIndices should ALWAYS either be `nil` or len will be at LEAST 2,
|
||||
and modulo 2 thereafter since they're PAIRS of indices...
|
||||
Why they didn't just return a [][]int or [][2]int or something
|
||||
instead of an []int, who knows.
|
||||
But we're correcting that poor design.
|
||||
This is more of a safe-return before we chunk the indices.
|
||||
*/
|
||||
matches = make(map[string][]string)
|
||||
if inclNoMatch {
|
||||
for _, grpNm = range names {
|
||||
if grpNm != "" {
|
||||
matches[grpNm] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
A reslice of `matchIndices` could technically start at 2 (as long as `names` is sliced [1:])
|
||||
because they're in pairs: []int{<start>, <end>, <start>, <end>, ...}
|
||||
and the first pair is the entire pattern match (un-resliced names[0]).
|
||||
Thus the len(matchIndices) == 2*len(names), *even* if you
|
||||
Keep in mind that since the first element of names is removed,
|
||||
the first pair here is skipped.
|
||||
This provides a bit more consistent readability, though.
|
||||
*/
|
||||
idxChunks = make([][]int, len(names))
|
||||
chunkIdx = 0
|
||||
endIdx = 0
|
||||
for startIdx = 0; endIdx < len(matchIndices); startIdx += 2 {
|
||||
endIdx = startIdx + 2
|
||||
// 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
|
||||
chunkIndices = nil
|
||||
} else {
|
||||
if chunkIndices[0] == chunkIndices[1] {
|
||||
chunkIndices = []int{chunkIndices[0]}
|
||||
} else {
|
||||
chunkIndices = matchIndices[startIdx:endIdx]
|
||||
}
|
||||
}
|
||||
idxChunks[chunkIdx] = chunkIndices
|
||||
chunkIdx++
|
||||
}
|
||||
|
||||
// Now associate with names and pull the string sequence.
|
||||
for chunkIdx, chunkIndices = range idxChunks {
|
||||
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 either an *unnamed* capture group
|
||||
OR the first element in `names`, which is always
|
||||
the entire match).
|
||||
*/
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if chunkIndices == nil || len(chunkIndices) == 0 {
|
||||
// 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
|
||||
}
|
||||
|
||||
switch len(chunkIndices) {
|
||||
case 1:
|
||||
// Single character
|
||||
matchStr = string(s[chunkIndices[0]])
|
||||
case 2:
|
||||
// Multiple characters
|
||||
matchStr = s[chunkIndices[0]:chunkIndices[1]]
|
||||
}
|
||||
|
||||
if _, ok = tmpMap[grpNm]; !ok {
|
||||
tmpMap[grpNm] = make([]string, 0)
|
||||
}
|
||||
tmpMap[grpNm] = append(tmpMap[grpNm], matchStr)
|
||||
}
|
||||
|
||||
// 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
27
remap/types.go
Normal 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
5
structutils/consts.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package structutils
|
||||
|
||||
const (
|
||||
TagMapTrim tagMapOpt = iota
|
||||
)
|
||||
362
structutils/funcs.go
Normal file
362
structutils/funcs.go
Normal 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
5
structutils/types.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package structutils
|
||||
|
||||
type (
|
||||
tagMapOpt uint8
|
||||
)
|
||||
Reference in New Issue
Block a user