ADDED:
* `stringsx` package
** `stringsx.Indent()`, to indent/prefix multiline strings
** `stringsx.Redact()`, to mask strings
** `stringsx.TrimLines()`, like strings.TrimSpace() but multiline
** `stringsx.TrimSpaceLeft()`, like strings.TrimSpace() but only to the
    left of a string.
** `stringsx.TrimSpaceRight()`, like strings.TrimSpace() but only to the
    right of a string.
This commit is contained in:
brent saner
2025-11-14 01:02:59 -05:00
parent e101758187
commit 5781234a37
7 changed files with 724 additions and 0 deletions

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.24.5
require ( require (
github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/go-systemd/v22 v22.5.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/sys v0.34.0 golang.org/x/sys v0.34.0
r00t2.io/sysutils v1.14.0 r00t2.io/sysutils v1.14.0
) )

3
go.sum
View File

@@ -5,9 +5,12 @@ github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYC
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.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 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
r00t2.io/sysutils v1.14.0 h1:Lrio3uPi9CuUdg+sg3WkVV1CK/qcOpV9GdFCGFG1KJs=
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw= r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=

5
stringsx/TODO Normal file
View File

@@ -0,0 +1,5 @@
- Banner struct, with .Format(s string) method
-- draw border around multiline s
-- i have a version in python somewhere that does this, should dig that up
- create bytesx package that duplicates the functions here?

11
stringsx/consts.go Normal file
View File

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

4
stringsx/doc.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package stringsx aims to extend functionality of the stdlib [strings] module.
*/
package stringsx

249
stringsx/funcs.go Normal file
View File

@@ -0,0 +1,249 @@
package stringsx
import (
`strings`
`unicode`
)
/*
Indent takes string s and indents it with string `indent` `level` times.
If indent is an empty string, [DefIndentStr] will be used.
If ws is true, lines consisting of only whitespace will be indented as well.
(To then trim any extraneous trailing space, you may want to use [TrimSpaceRight]
or [TrimLines].)
If empty is true, lines with no content will be replaced with lines that purely
consist of (indent * level) (otherwise they will be left as empty lines).
This function can also be used to prefix lines with arbitrary strings as well.
e.g:
Indent("foo\nbar\nbaz\n", "# ", 1, false, false)
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) {
var i string
var nl string
var endsNewline bool
var sb strings.Builder
var lineStripped string
if indent == "" {
indent = DefIndentStr
}
// This condition functionally won't do anything, so just return the input as-is.
if level == 0 {
indented = s
return
}
i = strings.Repeat(indent, int(level))
// This condition functionally won't do anything, so just return the input as-is.
if s == "" {
if empty {
indented = i
}
return
}
for line := range strings.Lines(s) {
lineStripped = strings.TrimSpace(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
}
// non-empty/non-whitespace-only line.
sb.WriteString(i + line)
}
// If it ends with a trailing newline and nothing after, strings.Lines() will skip the last (empty) line.
if endsNewline && empty {
nl = getNewLine(s)
sb.WriteString(i)
}
indented = sb.String()
return
}
/*
Redact provides a "masked" version of string s (e.g. `my_terrible_password` -> `my****************rd`).
maskStr is the character or sequence of characters
to repeat for every masked character of s.
If an empty string, the default [DefMaskStr] will be used.
(maskStr does not need to be a single character.
It is recommended to use a multi-char mask to help obfuscate a string's length.)
leading specifies the number of leading characters of s to leave *unmasked*.
If 0, no leading characters will be unmasked.
trailing specifies the number of trailing characters of s to leave *unmasked*.
if 0, no trailing characters will be unmasked.
newlines, if true, will preserve newline characters - otherwise
they will be treated as regular characters.
As a safety precaution, if:
len(s) <= (leading + trailing)
then the entire string will be *masked* and no unmasking will be performed.
*/
func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted string) {
var nl string
var numMasked int
var sb strings.Builder
var endIdx int = int(leading)
// This condition functionally won't do anything, so just return the input as-is.
if s == "" {
return
}
if maskStr == "" {
maskStr = DefMaskStr
}
if newlines {
for line := range strings.Lines(s) {
nl = getNewLine(line)
sb.WriteString(
Redact(
strings.TrimSuffix(line, nl), maskStr, leading, trailing, false,
),
)
sb.WriteString(nl)
}
} else {
if len(s) <= int(leading+trailing) {
redacted = strings.Repeat(maskStr, len(s))
return
}
if leading == 0 && trailing == 0 {
redacted = strings.Repeat(maskStr, len(s))
return
}
numMasked = len(s) - int(leading+trailing)
endIdx = endIdx + numMasked
if leading > 0 {
sb.WriteString(s[:int(leading)])
}
sb.WriteString(strings.Repeat(maskStr, numMasked))
if trailing > 0 {
sb.WriteString(s[endIdx:])
}
}
redacted = sb.String()
return
}
/*
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
The first encountered linebreak (`\n` vs. `\r\n`) are assumed to be
the canonical linebreak for the rest of s.
left, if true, performs a [TrimSpaceLeft] on each line (retaining the newline).
right, if true, performs a [TrimSpaceRight] on each line (retaining the newline).
*/
func TrimLines(s string, left, right bool) (trimmed string) {
var sl string
var nl string
var sb strings.Builder
// These conditions functionally won't do anything, so just return the input as-is.
if s == "" {
return
}
if !left && !right {
trimmed = s
return
}
for line := range strings.Lines(s) {
nl = getNewLine(line)
sl = strings.TrimSuffix(line, nl)
if left && right {
sl = strings.TrimSpace(sl)
} else if left {
sl = TrimSpaceLeft(sl)
} else if right {
sl = TrimSpaceRight(sl)
}
sb.WriteString(sl + nl)
}
trimmed = sb.String()
return
}
// TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string s.
func TrimSpaceLeft(s string) (trimmed string) {
trimmed = strings.TrimLeftFunc(s, unicode.IsSpace)
return
}
/*
TrimSpaceRight is like [strings.TrimSpace] but only removes trailing whitespace from string s.
*/
func TrimSpaceRight(s string) (trimmed string) {
trimmed = strings.TrimRightFunc(s, unicode.IsSpace)
return
}
// getNewLine is too unpredictable to be used outside of this package so it isn't exported.
func getNewLine(s string) (nl string) {
if strings.HasSuffix(s, "\r\n") {
nl = "\r\n"
} else if strings.HasSuffix(s, "\n") {
nl = "\n"
}
return
}

451
stringsx/funcs_test.go Normal file
View File

@@ -0,0 +1,451 @@
package stringsx
import (
"testing"
)
type (
testIndentSet struct {
name string
orig string
indent string
lvl uint
ws bool
empty bool
tgt string
}
testRedactSet struct {
name string
orig string
leading uint
trailing uint
tgt string
newline bool
mask string // defaults to DefMaskStr.
}
testTrimLinesSet struct {
name string
orig string
left bool
right bool
tgt string
}
testTrimSet struct {
name string
orig string
tgt string
}
)
func TestIndent(t *testing.T) {
var out string
var tests []testIndentSet = []testIndentSet{
testIndentSet{
name: "standard, no trailing newline",
orig: "foo\nbar\nbaz",
indent: "",
lvl: 1,
ws: false,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz",
},
testIndentSet{
name: "standard, trailing newline",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: false,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n",
},
testIndentSet{
name: "standard, trailing newline with empty",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: false,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t",
},
testIndentSet{
name: "standard, trailing newline with ws",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: true,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n",
},
testIndentSet{
name: "standard, trailing newline with ws and empty",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: true,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t",
},
testIndentSet{
name: "standard, trailing ws newline with empty",
orig: "foo\nbar\nbaz\n ",
indent: "",
lvl: 1,
ws: false,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n ",
},
testIndentSet{
name: "standard, trailing ws newline with ws",
orig: "foo\nbar\nbaz\n ",
indent: "",
lvl: 1,
ws: true,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n\t ",
},
testIndentSet{
name: "standard, trailing ws newline with ws and empty",
orig: "foo\nbar\nbaz\n \n",
indent: "",
lvl: 1,
ws: true,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t \n\t",
},
testIndentSet{
name: "comment",
orig: "foo\nbar\nbaz",
indent: "# ",
lvl: 1,
ws: false,
empty: false,
tgt: "# foo\n# bar\n# baz",
},
}
for idx, ts := range tests {
out = Indent(ts.orig, ts.indent, ts.lvl, ts.ws, ts.empty)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestRedact(t *testing.T) {
var out string
var tests []testRedactSet = []testRedactSet{
testRedactSet{
name: "empty in, empty out",
orig: "",
leading: 0,
trailing: 0,
tgt: "",
},
testRedactSet{
name: "standard",
orig: "password",
leading: 0,
trailing: 0,
tgt: "************************",
},
testRedactSet{
name: "standard with newline",
orig: "pass\nword",
leading: 0,
trailing: 0,
tgt: "************\n************",
newline: true,
},
testRedactSet{
name: "standard with Windows newline",
orig: "pass\r\nword",
leading: 0,
trailing: 0,
tgt: "************\r\n************",
newline: true,
},
testRedactSet{
name: "standard with newline without newlines",
orig: "pass\nword",
leading: 0,
trailing: 0,
tgt: "***************************",
},
testRedactSet{
name: "single leading",
orig: "password",
leading: 1,
trailing: 0,
tgt: "p*********************",
},
testRedactSet{
name: "single trailing",
orig: "password",
leading: 0,
trailing: 1,
tgt: "*********************d",
},
testRedactSet{
name: "three leading",
orig: "password",
leading: 3,
trailing: 0,
tgt: "pas***************",
},
testRedactSet{
name: "three trailing",
orig: "password",
leading: 0,
trailing: 3,
tgt: "***************ord",
},
testRedactSet{
name: "three leading and trailing",
orig: "password",
leading: 3,
trailing: 3,
tgt: "pas******ord",
},
testRedactSet{
name: "unmask overflow leading",
orig: "password",
leading: 5,
trailing: 4,
tgt: "************************",
},
testRedactSet{
name: "unmask overflow trailing",
orig: "password",
leading: 4,
trailing: 5,
tgt: "************************",
},
testRedactSet{
name: "single mask",
orig: "password",
leading: 0,
trailing: 0,
tgt: "********",
mask: "*",
},
testRedactSet{
name: "standard trailing newline with newlines",
orig: "password\n",
leading: 0,
trailing: 0,
tgt: "************************\n",
newline: true,
},
testRedactSet{
name: "standard trailing newline without newlines",
orig: "password\n",
leading: 0,
trailing: 0,
tgt: "***************************",
},
}
for idx, ts := range tests {
out = Redact(ts.orig, ts.mask, ts.leading, ts.trailing, ts.newline)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestTrimLines(t *testing.T) {
var out string
var tests []testTrimLinesSet = []testTrimLinesSet{
testTrimLinesSet{
name: "none",
orig: " foo \n bar \n baz ",
left: false,
right: false,
tgt: " foo \n bar \n baz ",
},
testTrimLinesSet{
name: "standard",
orig: " foo \n bar \n baz ",
left: true,
right: true,
tgt: "foo\nbar\nbaz",
},
testTrimLinesSet{
name: "left only",
orig: " foo \n bar \n baz ",
left: true,
right: false,
tgt: "foo \nbar \nbaz ",
},
testTrimLinesSet{
name: "right only",
orig: " foo \n bar \n baz ",
left: false,
right: true,
tgt: " foo\n bar\n baz",
},
testTrimLinesSet{
name: "standard, trailing newline",
orig: " foo \n bar \n baz \n",
left: true,
right: true,
tgt: "foo\nbar\nbaz\n",
},
testTrimLinesSet{
name: "left only, trailing newline",
orig: " foo \n bar \n baz \n",
left: true,
right: false,
tgt: "foo \nbar \nbaz \n",
},
testTrimLinesSet{
name: "right only, trailing newline",
orig: " foo \n bar \n baz \n",
left: false,
right: true,
tgt: " foo\n bar\n baz\n",
},
// Since there's no "non-space" boundary, both of these condition tests do the same thing.
testTrimLinesSet{
name: "left only, trailing newline and ws",
orig: " foo \n bar \n baz \n ",
left: true,
right: false,
tgt: "foo \nbar \nbaz \n",
},
testTrimLinesSet{
name: "right only, trailing newline and ws",
orig: " foo \n bar \n baz \n ",
left: false,
right: true,
tgt: " foo\n bar\n baz\n",
},
}
for idx, ts := range tests {
out = TrimLines(ts.orig, ts.left, ts.right)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestTrimSpaceLeft(t *testing.T) {
var out string
var tests []testTrimSet = []testTrimSet{
testTrimSet{
name: "standard",
orig: " foo ",
tgt: "foo ",
},
testTrimSet{
name: "tabs",
orig: "\t\tfoo\t\t",
tgt: "foo\t\t",
},
testTrimSet{
name: "newlines",
orig: "\n\nfoo\n\n",
tgt: "foo\n\n",
},
}
for idx, ts := range tests {
out = TrimSpaceLeft(ts.orig)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}
func TestTrimSpaceRight(t *testing.T) {
var out string
var tests []testTrimSet = []testTrimSet{
testTrimSet{
name: "standard",
orig: " foo ",
tgt: " foo",
},
testTrimSet{
name: "tabs",
orig: "\t\tfoo\t\t",
tgt: "\t\tfoo",
},
testTrimSet{
name: "newlines",
orig: "\n\nfoo\n\n",
tgt: "\n\nfoo",
},
}
for idx, ts := range tests {
out = TrimSpaceRight(ts.orig)
if out == ts.tgt {
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
} else {
t.Errorf(
"[%d] FAIL (%s): %#v (len %d):\n"+
"\t\t\texpected (len %d): %#v\n"+
"\t\t\tgot (len %d): %#v\n"+
"\t\t%#v",
idx, ts.name, ts.orig, len(ts.orig),
len(ts.tgt), ts.tgt,
len(out), out,
ts,
)
}
}
}