ADD:
* `paths.Len()`
* `paths.LenSys()`
* `paths.Segment()`
* `paths.SegmentSys()`
* `paths.Strip()`
* `paths.StripSys()`

FIX:
* Naming conflics between `<stdlib>/path` and the `path` parameters
This commit is contained in:
brent saner 2025-07-31 03:27:04 -04:00
parent e5f7296d2e
commit 8260e4fa93
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
4 changed files with 194 additions and 37 deletions

8
go.mod
View File

@ -6,11 +6,11 @@ require (
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/djherbis/times v1.6.0 github.com/djherbis/times v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/shirou/gopsutil/v4 v4.25.5 github.com/shirou/gopsutil/v4 v4.25.6
golang.org/x/sync v0.15.0 golang.org/x/sync v0.16.0
golang.org/x/sys v0.33.0 golang.org/x/sys v0.34.0
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
r00t2.io/goutils v1.8.1 r00t2.io/goutils v1.9.0
) )
require ( require (

8
go.sum
View File

@ -21,6 +21,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
@ -31,6 +33,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -38,10 +42,14 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E= honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE= honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
r00t2.io/goutils v1.8.1 h1:TQcUycPKsYn0QI4uCqb56utmvu/vVSxlblBg98iXStg= r00t2.io/goutils v1.8.1 h1:TQcUycPKsYn0QI4uCqb56utmvu/vVSxlblBg98iXStg=
r00t2.io/goutils v1.8.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= r00t2.io/goutils v1.8.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/goutils v1.9.0 h1:iEwa9LinCzabpTD03/2oUrFE3QinxszTzL48pBV9cD4=
r00t2.io/goutils v1.9.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=

View File

@ -4,6 +4,10 @@ import (
"io/fs" "io/fs"
) )
const (
GenericSeparator rune = '/'
)
// Mostly just for reference. // Mostly just for reference.
const ( const (
// ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular // ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular

View File

@ -23,6 +23,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"math"
"os" "os"
"os/user" "os/user"
"path" "path"
@ -42,7 +43,7 @@ import (
ExpandHome will take a tilde(~)-prefixed path and resolve it to the actual path in-place. ExpandHome will take a tilde(~)-prefixed path and resolve it to the actual path in-place.
"Nested" user paths (~someuser/somechroot/~someotheruser) are not supported as home directories are expected to be absolute paths. "Nested" user paths (~someuser/somechroot/~someotheruser) are not supported as home directories are expected to be absolute paths.
*/ */
func ExpandHome(path *string) (err error) { func ExpandHome(p *string) (err error) {
var unameSplit []string var unameSplit []string
var uname string var uname string
@ -51,10 +52,10 @@ func ExpandHome(path *string) (err error) {
// Props to this guy. // Props to this guy.
// https://stackoverflow.com/a/43578461/733214 // https://stackoverflow.com/a/43578461/733214
if len(*path) == 0 { if len(*p) == 0 {
err = errors.New("empty path") err = errors.New("empty path")
return return
} else if (*path)[0] != '~' { } else if (*p)[0] != '~' {
return return
} }
@ -70,7 +71,7 @@ func ExpandHome(path *string) (err error) {
} }
*/ */
// K but do it smarter. // K but do it smarter.
unameSplit = strings.SplitN(*path, string(os.PathSeparator), 2) unameSplit = strings.SplitN(*p, string(os.PathSeparator), 2)
if len(unameSplit) != 2 { if len(unameSplit) != 2 {
unameSplit = append(unameSplit, "") unameSplit = append(unameSplit, "")
} }
@ -86,7 +87,7 @@ func ExpandHome(path *string) (err error) {
} }
} }
*path = filepath.Join(u.HomeDir, unameSplit[1]) *p = filepath.Join(u.HomeDir, unameSplit[1])
return return
} }
@ -108,9 +109,9 @@ potentially returning an inaccurate result.
This is a thin wrapper around GetFirstWithRef. This is a thin wrapper around GetFirstWithRef.
*/ */
func GetFirst(paths []string) (content []byte, isDir, ok bool) { func GetFirst(p []string) (content []byte, isDir, ok bool) {
content, isDir, ok, _ = GetFirstWithRef(paths) content, isDir, ok, _ = GetFirstWithRef(p)
return return
} }
@ -119,12 +120,12 @@ func GetFirst(paths []string) (content []byte, isDir, ok bool) {
GetFirstWithRef is the file equivalent of envs.GetFirstWithRef. GetFirstWithRef is the file equivalent of envs.GetFirstWithRef.
It behaves exactly like GetFirst, but with an additional returned value, idx, It behaves exactly like GetFirst, but with an additional returned value, idx,
which specifies the index in paths in which a path was found. which specifies the index in p in which a path was found.
As always, results are not guaranteed due to permissions, etc. As always, results are not guaranteed due to permissions, etc.
potentially returning an inaccurate result. potentially returning an inaccurate result.
*/ */
func GetFirstWithRef(paths []string) (content []byte, isDir, ok bool, idx int) { func GetFirstWithRef(p []string) (content []byte, isDir, ok bool, idx int) {
var locPaths []string var locPaths []string
var exists bool var exists bool
@ -133,11 +134,11 @@ func GetFirstWithRef(paths []string) (content []byte, isDir, ok bool, idx int) {
idx = -1 idx = -1
// We have to be a little less cavalier about this. // We have to be a little less cavalier about this.
if paths == nil { if p == nil {
return return
} }
locPaths = make([]string, len(paths)) locPaths = make([]string, len(p))
locPaths = paths[:] // Create an explicit copy so we don't modify paths. locPaths = p[:] // Create an explicit copy so we don't modify p.
for i, p := range locPaths { for i, p := range locPaths {
if exists, stat, err = RealPathExistsStat(&p); err != nil { if exists, stat, err = RealPathExistsStat(&p); err != nil {
err = nil err = nil
@ -160,6 +161,30 @@ func GetFirstWithRef(paths []string) (content []byte, isDir, ok bool, idx int) {
return return
} }
/*
Len returns the number of path segments in p, as split with the same param signature to Segment.
See Segment for details on abs and strict.
*/
func Len(p string, abs, strict bool) (segments int) {
segments = len(Segment(p, abs, strict))
return
}
/*
LenSys returns the number of path segments in p, as split with the same param signature to SegmentSys.
See Segment for details on abs and strict.
*/
func LenSys(p string, abs, strict bool) (segments int) {
segments = len(SegmentSys(p, abs, strict))
return
}
/* /*
MakeDirIfNotExist will create a directory at a given path if it doesn't exist. MakeDirIfNotExist will create a directory at a given path if it doesn't exist.
@ -167,11 +192,11 @@ See also the documentation for RealPath.
This is a bit more sane option than os.MkdirAll as it will normalize paths a little better. This is a bit more sane option than os.MkdirAll as it will normalize paths a little better.
*/ */
func MakeDirIfNotExist(path string) (err error) { func MakeDirIfNotExist(p string) (err error) {
var stat os.FileInfo var stat os.FileInfo
var exists bool var exists bool
var locPath string = path var locPath string = p
if exists, stat, err = RealPathExistsStat(&locPath); err != nil { if exists, stat, err = RealPathExistsStat(&locPath); err != nil {
if !exists { if !exists {
@ -208,14 +233,16 @@ It is recommended to check err (if not nil) for an invalid path error. If this i
path syntax/string itself is not supported on the runtime OS. This can be done via: path syntax/string itself is not supported on the runtime OS. This can be done via:
if errors.Is(err, fs.ErrInvalid) {...} if errors.Is(err, fs.ErrInvalid) {...}
*/
func RealPath(path *string) (err error) {
if err = ExpandHome(path); err != nil { RealPath is simply a wrapper around ExpandHome(path) and filepath.Abs(*path).
*/
func RealPath(p *string) (err error) {
if err = ExpandHome(p); err != nil {
return return
} }
if *path, err = filepath.Abs(*path); err != nil { if *p, err = filepath.Abs(*p); err != nil {
return return
} }
@ -225,25 +252,25 @@ func RealPath(path *string) (err error) {
/* /*
RealPathJoin combines RealPath with (path).Join. RealPathJoin combines RealPath with (path).Join.
If dst is nil, then rootPath will be updated with the new value. If dst is nil, then p will be updated with the new value.
You probably don't want that. You probably don't want that.
*/ */
func RealPathJoin(rootPath, dst *string, subPaths ...string) (err error) { func RealPathJoin(p, dst *string, subPaths ...string) (err error) {
var newPath string var newPath string
var realDst *string var realDst *string
if err = RealPath(rootPath); err != nil { if err = RealPath(p); err != nil {
return return
} }
if dst == nil { if dst == nil {
realDst = rootPath realDst = p
} else { } else {
realDst = dst realDst = dst
} }
newPath = path.Join(append([]string{*rootPath}, subPaths...)...) newPath = path.Join(append([]string{*p}, subPaths...)...)
if err = RealPath(&newPath); err != nil { if err = RealPath(&newPath); err != nil {
return return
} }
@ -259,22 +286,22 @@ RealPathJoinSys combines RealPath with (path/filepath).Join.
If dst is nil, then path will be updated with the new value. If dst is nil, then path will be updated with the new value.
You probably don't want that. You probably don't want that.
*/ */
func RealPathJoinSys(path, dst *string, subPaths ...string) (err error) { func RealPathJoinSys(p, dst *string, subPaths ...string) (err error) {
var newPath string var newPath string
var realDst *string var realDst *string
if err = RealPath(path); err != nil { if err = RealPath(p); err != nil {
return return
} }
if dst == nil { if dst == nil {
realDst = path realDst = p
} else { } else {
realDst = dst realDst = dst
} }
newPath = filepath.Join(append([]string{*path}, subPaths...)...) newPath = filepath.Join(append([]string{*p}, subPaths...)...)
if err = RealPath(&newPath); err != nil { if err = RealPath(&newPath); err != nil {
return return
} }
@ -300,13 +327,13 @@ See also the documentation for RealPath.
In those cases, it may be preferable to use RealPathExistsStat and checking stat for nil. In those cases, it may be preferable to use RealPathExistsStat and checking stat for nil.
*/ */
func RealPathExists(path *string) (exists bool, err error) { func RealPathExists(p *string) (exists bool, err error) {
if err = RealPath(path); err != nil { if err = RealPath(p); err != nil {
return return
} }
if _, err = os.Stat(*path); err != nil { if _, err = os.Stat(*p); err != nil {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
err = nil err = nil
} }
@ -325,13 +352,13 @@ for the path (assuming it exists).
If stat is nil, it is highly recommended to check err via the methods suggested If stat is nil, it is highly recommended to check err via the methods suggested
in the documentation for RealPath and RealPathExists. in the documentation for RealPath and RealPathExists.
*/ */
func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error) { func RealPathExistsStat(p *string) (exists bool, stat os.FileInfo, err error) {
if exists, err = RealPathExists(path); err != nil { if exists, err = RealPathExists(p); err != nil {
return return
} }
if stat, err = os.Stat(*path); err != nil { if stat, err = os.Stat(*p); err != nil {
return return
} }
@ -498,6 +525,124 @@ func SearchFsPathsAsync(matcher FsSearchCriteriaAsync) {
return return
} }
/*
Segment returns path p's segments as a slice of strings, using GenericSeparator as a separator.
If abs is true, the placeholder leading prefix(es) (if any) of GenericSeparator will be kept in-place;
otherwise it/they will be trimmed out.
e.g.:
abs == true: //foo/bar/baz => []string{"", "", "foo", "bar", "baz"}
abs == false: /foo/bar/baz => []string{"foo", "bar", "baz"}
If strict is true, any trailing GenericSeparator will be kept in-place;
otherwise they will be trimmed out.
e.g. (assuming abs == false):
strict == true: /foo/bar/baz// => []string{"foo", "bar", "baz", "", ""}
strict == false: /foo/bar/baz/ => []string{"foo", "bar", "baz"}
It is recommended to call RealPath for path's ptr first for normalization.
*/
func Segment(p string, abs, strict bool) (segments []string) {
if !abs {
p = strings.TrimLeft(p, string(GenericSeparator))
}
if !strict {
p = strings.TrimRight(p, string(GenericSeparator))
}
segments = strings.Split(p, string(GenericSeparator))
return
}
// SegmentSys is exactly like Segment, except using os.PathSeparator instead of GenericSeparator.
func SegmentSys(p string, abs, strict bool) (segments []string) {
if !abs {
p = strings.TrimLeft(p, string(os.PathSeparator))
}
if !strict {
p = strings.TrimRight(p, string(os.PathSeparator))
}
segments = strings.Split(p, string(os.PathSeparator))
return
}
/*
Strip is like Segment but trims out the leading n number of segments and reassembles the path using path.Join.
n may be negative, in which case the *trailing* n number of segments will be trimmed out.
(i.e. n == -1, p == `foo/bar/baz/quux` would be `foo/bar/baz`, not `bar/baz/quux`)
If you require more traditional slicing (e.g. with interval),
you may want to use path.Join with a sliced result of Segment instead.
e.g.: *only* the *last* n segments: path.Join(Segment(p, ...)[Len(p, ...)-n:]...)
If n == 0 or int(math.Abs(float64(n))) >= len(Segment(p, ...)), no transformation will be done.
e.g.
n == 2: foo/bar/baz/foobar/quux => baz/foobar/quux
n == -2: foo/bar/baz/foobar/quux => foo/bar/baz
*/
func Strip(p string, abs, strict bool, n int) (slicedPath string) {
var pLen int
var absN int
var segments []string
segments = Segment(p, abs, strict)
pLen = len(segments)
absN = int(math.Abs(float64(n)))
if n == 0 || absN >= pLen {
slicedPath = p
return
}
if n > 0 {
segments = segments[n:]
} else {
segments = segments[:pLen-absN]
}
slicedPath = path.Join(segments...)
return
}
// StripSys is exactly like Strip but using (path/filepath).Join and SegmentSys.
func StripSys(p string, abs, strict bool, n int) (slicedPath string) {
var pLen int
var absN int
var segments []string
segments = SegmentSys(p, abs, strict)
pLen = len(segments)
absN = int(math.Abs(float64(n)))
if n == 0 || absN >= pLen {
slicedPath = p
return
}
if n > 0 {
segments = segments[n:]
} else {
segments = segments[:pLen-absN]
}
slicedPath = filepath.Join(segments...)
return
}
/* /*
filterTimes checks a times.Timespec of a file using: filterTimes checks a times.Timespec of a file using:
- an age specified by the caller - an age specified by the caller