3 Commits

Author SHA1 Message Date
brent saner
6ddfcdb416 v1.13.0
ADDED:
* stringsx functions
2025-11-30 16:53:56 -05:00
brent saner
79f10b7611 v1.12.1
FIXED:
* Aaaannnddd need to make the Windows multilogger AddDefaultLogger
  use the right/matching parameters as well.
2025-11-22 17:19:41 -05:00
brent saner
01adbfc605 v1.12.0
FIXED:
* logging package on Windows had a non-conformant GetLogger().
  While this fix technically breaks API, this was a horribly broken
  thing so I'm including it as a minor bump instead of major and
  thus breaking SemVer. Too bad, so sad, deal with it; Go modules
  have versioning for a reason.
  The previous logging.GetLogger() behavior on Windows has been moved
  to logging.GetLoggerWindows().
2025-11-22 15:53:38 -05:00
8 changed files with 211 additions and 93 deletions

View File

@@ -1,3 +1,5 @@
- logging probably needs mutexes
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System". - 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/logging
-- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code -- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code

View File

@@ -21,7 +21,7 @@ import (
Only the first logPaths entry that "works" will be used, later entries will be ignored. Only the first logPaths entry that "works" will be used, later entries will be ignored.
Currently this will almost always return a WinLogger. Currently this will almost always return a WinLogger.
*/ */
func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logFlags int, logPaths ...string) (err error) { func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
var l Logger var l Logger
var exists bool var exists bool
@@ -36,9 +36,9 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID,
} }
if logPaths != nil { if logPaths != nil {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...) l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...)
} else { } else {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags) l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags)
} }
if err != nil { if err != nil {
return return

View File

@@ -10,32 +10,63 @@ import (
) )
/* /*
GetLogger returns an instance of Logger that best suits your system's capabilities. Note that this is a VERY generalized interface to the Windows Event Log. GetLogger returns an instance of Logger that best suits your system's capabilities.
Note that this is a VERY generalized interface to the Windows Event Log to conform with multiplatform compat.
You'd have a little more flexibility with [GetLoggerWindows] (this function wraps that one).
If you need more custom behavior than that, I recommend using [golang.org/x/sys/windows/svc/eventlog] directly
(or using another logging module).
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
The `prefix` correlates to the `source` parameter in [GetLoggerWindows], and this function inherently uses [DefaultEventID],
but otherwise it remains the same as [GetLoggerWindows] - refer to it for documentation on the other parameters.
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) {
if logger, err = GetLoggerWindows(enableDebug, prefix, DefaultEventID, logConfigFlags, logPaths...); err != nil {
return
}
return
}
/*
GetLoggerWindows returns an instance of Logger that best suits your system's capabilities.
This is a slightly less (but still quite) generalized interface to the Windows Event Log than [GetLogger].
If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event), If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
you will want to set up your own logger (golang.org/x/sys/windows/svc/eventlog). you will want to set up your own logger via [golang.org/x/sys/windows/svc/eventlog].
If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored). If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data)
are rendered and written (otherwise they are ignored).
A blank source will return an error as it's used as the source name. Other functions, struct fields, etc. will refer to this as the "prefix". A blank `source` will return an error as it's used as the source name.
Throughout the rest of this documentation you will see this referred to as the `prefix` to remain platform-agnostic.
A pointer to a WinEventID struct may be specified for eventIDs to map extended logging levels (as Windows only supports three levels natively). A pointer to a [WinEventID] struct may be specified for `eventIDs` to map extended logging levels
(as Windows only supports three levels natively).
If it is nil, a default one (DefaultEventID) will be used. If it is nil, a default one (DefaultEventID) will be used.
logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and `logConfigFlags` is the corresponding flag(s) OR'd for [StdLogger.LogFlags] (and/or the [StdLogger.LogFlags] for [FileLogger])
https://pkg.go.dev/log#pkg-constants for details. if either is selected. See [StdLogger.LogFlags] and [stdlib log's 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, `logPaths` is an (optional) list of strings to use as paths to test for writing.
it will be used (assuming you have no higher-level loggers available). 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. Only the first `logPaths` entry that "works" will be used, later entries will be ignored.
Currently this will almost always return a WinLogger. Currently this will almost always return a [WinLogger].
If you call GetLogger, you will only get a single ("best") logger your system supports. If you call [GetLoggerWindows], 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), If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
use GetMultiLogger. use [GetMultiLogger].
[stdlib log's constants]: https://pkg.go.dev/log#pkg-constants
*/ */
func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) { func GetLoggerWindows(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
var logPath string var logPath string
var logFlags bitmask.MaskBit var logFlags bitmask.MaskBit

View File

@@ -124,7 +124,7 @@ func TestDefaultLogger(t *testing.T) {
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error()) t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
} }
if l, err = GetLogger(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil { if l, err = GetLoggerWindows(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error()) t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
} }

View File

@@ -35,7 +35,7 @@ func TestMultiLogger(t *testing.T) {
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error()) t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
} }
if err = l.AddDefaultLogger("DefaultLogger", DefaultEventID, logFlags, tempfilePath); err != nil { if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error()) t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
} }

View File

@@ -4,8 +4,3 @@ const (
// DefMaskStr is the string used as the default maskStr if left empty in [Redact]. // DefMaskStr is the string used as the default maskStr if left empty in [Redact].
DefMaskStr string = "***" DefMaskStr string = "***"
) )
const (
// DefIndentStr is the string used as the default indent if left empty in [Indent].
DefIndentStr string = "\t"
)

View File

@@ -1,4 +1,17 @@
/* /*
Package stringsx aims to extend functionality of the stdlib [strings] module. Package stringsx aims to extend functionality of the stdlib [strings] module.
Note that if you need a way of mimicking Bash's shell quoting rules, [desertbit/shlex] or [buildkite/shellwords]
would be better options than [google/shlex] but this package does not attempt to reproduce
any of that functionality.
For line splitting, one should use [muesli/reflow/wordwrap].
Likewise for indentation, one should use [muesli/reflow/indent].
[desertbit/shlex]: https://pkg.go.dev/github.com/desertbit/go-shlex
[buildkite/shellwords]: https://pkg.go.dev/github.com/buildkite/shellwords
[google/shlex]: https://pkg.go.dev/github.com/google/shlex
[muesli/reflow/wordwrap]: https://pkg.go.dev/github.com/muesli/reflow/wordwrap
[muesli/reflow/indent]: https://pkg.go.dev/github.com/muesli/reflow/indent
*/ */
package stringsx package stringsx

View File

@@ -1,96 +1,170 @@
package stringsx package stringsx
import ( import (
`fmt`
`strings` `strings`
`unicode` `unicode`
) )
/* /*
Indent takes string s and indents it with string `indent` `level` times. LenSplit formats string `s` to break at, at most, every `width` characters.
If indent is an empty string, [DefIndentStr] will be used. Any existing newlines (e.g. \r\n) will be removed during a string/
substring/line's length calculation. (e.g. `foobarbaz\n` and `foobarbaz\r\n` are
both considered to be lines of length 9, not 10 and 11 respectively).
If ws is true, lines consisting of only whitespace will be indented as well. This also means that any newlines (\n or \r\n) are inherently removed from
(To then trim any extraneous trailing space, you may want to use [TrimSpaceRight] `out` (even if included in `wordWrap`; see below).
or [TrimLines].)
If empty is true, lines with no content will be replaced with lines that purely Note that if `s` is multiline (already contains newlines), they will be respected
consist of (indent * level) (otherwise they will be left as empty lines). as-is - that is, if a line ends with less than `width` chars and then has a newline,
it will be preserved as an empty element. That is to say:
This function can also be used to prefix lines with arbitrary strings as well. "foo\nbar\n\n" → []string{"foo", "bar", ""}
e.g: "foo\n\nbar\n" → []string{"foo", "", "bar"}
Indent("foo\nbar\nbaz\n", "# ", 1, false, false) This splitter is particularly simple. If you need wordwrapping, it should be done
with e.g. [github.com/muesli/reflow/wordwrap].
would yield:
# foo
# bar
# baz
<empty line>
thus allowing you to "comment out" multiple lines at once.
*/ */
func Indent(s, indent string, level uint, ws, empty bool) (indented string) { func LenSplit(s string, width uint) (out []string) {
var i string var end int
var nl string var line string
var endsNewline bool var lineRunes []rune
var sb strings.Builder
var lineStripped string
if indent == "" { if width == 0 {
indent = DefIndentStr out = []string{s}
}
// This condition functionally won't do anything, so just return the input as-is.
if level == 0 {
indented = s
return return
} }
i = strings.Repeat(indent, int(level)) for line = range strings.Lines(s) {
line = strings.TrimRight(line, "\n")
line = strings.TrimRight(line, "\r")
// This condition functionally won't do anything, so just return the input as-is. lineRunes = []rune(line)
if s == "" {
if empty {
indented = i
}
return
}
for line := range strings.Lines(s) { if uint(len(lineRunes)) <= width {
lineStripped = strings.TrimSpace(line) out = append(out, line)
nl = getNewLine(line)
endsNewline = nl != ""
// fmt.Printf("%#v => %#v\n", line, lineStripped)
if lineStripped == "" {
// fmt.Printf("WS/EMPTY LINE (%#v) (ws %v, empty %v): \n", s, ws, empty)
if line != (lineStripped + nl) {
// whitespace-only line
if ws {
sb.WriteString(i)
}
} else {
// empty line
if empty {
sb.WriteString(i)
}
}
sb.WriteString(line)
continue continue
} }
// non-empty/non-whitespace-only line.
sb.WriteString(i + line) for i := 0; i < len(lineRunes); i += int(width) {
end = i + int(width)
if end > len(lineRunes) {
end = len(lineRunes)
}
out = append(out, string(lineRunes[i:end]))
}
} }
// If it ends with a trailing newline and nothing after, strings.Lines() will skip the last (empty) line. return
if endsNewline && empty { }
nl = getNewLine(s)
sb.WriteString(i) /*
LenSplitStr wraps [LenSplit] but recombines into a new string with newlines.
It's mostly just a convenience wrapper.
All arguments remain the same as in [LenSplit] with an additional one,
`winNewLine`, which if true will use \r\n as the newline instead of \n.
*/
func LenSplitStr(s string, width uint, winNewline bool) (out string) {
var outSl []string = LenSplit(s, width)
if winNewline {
out = strings.Join(outSl, "\r\n")
} else {
out = strings.Join(outSl, "\n")
} }
indented = sb.String() return
}
/*
Pad pads each element in `s` to length `width` using `pad`.
If `pad` is empty, a single space (0x20) will be assumed.
Note that `width` operates on rune size, not byte size.
(In ASCII, they will be the same size.)
If a line in `s` is greater than or equal to `width`,
no padding will be performed.
If `leftPad` is true, padding will be applied to the "left" (beginning")
of each element instead of the "right" ("end").
*/
func Pad(s []string, width uint, pad string, leftPad bool) (out []string) {
var idx int
var padIdx int
var runeIdx int
var padLen uint
var elem string
var unpadLen uint
var tmpPadLen int
var padRunes []rune
var tmpPad []rune
if width == 0 {
out = s
return
}
out = make([]string, len(s))
// Easy; supported directly in fmt.
if pad == "" {
for idx, elem = range s {
if leftPad {
out[idx] = fmt.Sprintf("%*s", width, elem)
} else {
out[idx] = fmt.Sprintf("%-*s", width, elem)
}
}
return
}
// This gets a little more tricky.
padRunes = []rune(pad)
padLen = uint(len(padRunes))
for idx, elem = range s {
// First we need to know the number of runes in elem.
unpadLen = uint(len([]rune(elem)))
// If it's more than/equal to width, as-is.
if unpadLen >= width {
out[idx] = elem
} else {
// Otherwise, we need to construct/calculate a pad.
if (width-unpadLen)%padLen == 0 {
// Also easy enough.
if leftPad {
out[idx] = fmt.Sprintf("%s%s", strings.Repeat(pad, int((width-unpadLen)/padLen)), elem)
} else {
out[idx] = fmt.Sprintf("%s%s", elem, strings.Repeat(pad, int((width-unpadLen)/padLen)))
}
} else {
// This is where it gets a little hairy.
tmpPad = []rune{}
tmpPadLen = int(width - unpadLen)
idx = 0
padIdx = 0
for runeIdx = range tmpPadLen {
tmpPad[runeIdx] = padRunes[padIdx]
if uint(padIdx) >= padLen {
padIdx = 0
} else {
padIdx++
}
runeIdx++
}
if leftPad {
out[idx] = fmt.Sprintf("%s%s", string(tmpPad), elem)
} else {
out[idx] = fmt.Sprintf("%s%s", elem, string(tmpPad))
}
}
}
}
return return
} }
@@ -118,6 +192,9 @@ As a safety precaution, if:
len(s) <= (leading + trailing) len(s) <= (leading + trailing)
then the entire string will be *masked* and no unmasking will be performed. then the entire string will be *masked* and no unmasking will be performed.
Note that this DOES NOT do a string *replace*, it provides a masked version of `s` itself.
Wrap Redact with [strings.ReplaceAll] if you want to replace a certain value with a masked one.
*/ */
func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted string) { func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted string) {
@@ -218,7 +295,7 @@ func TrimLines(s string, left, right bool) (trimmed string) {
return return
} }
// TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string s. // TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string `s`.
func TrimSpaceLeft(s string) (trimmed string) { func TrimSpaceLeft(s string) (trimmed string) {
trimmed = strings.TrimLeftFunc(s, unicode.IsSpace) trimmed = strings.TrimLeftFunc(s, unicode.IsSpace)
@@ -236,7 +313,7 @@ func TrimSpaceRight(s string) (trimmed string) {
return return
} }
// getNewLine is too unpredictable to be used outside of this package so it isn't exported. // getNewLine is too unpredictable/nuanced to be used as part of a public API promise so it isn't exported.
func getNewLine(s string) (nl string) { func getNewLine(s string) (nl string) {
if strings.HasSuffix(s, "\r\n") { if strings.HasSuffix(s, "\r\n") {