added Len*, Segment*, Strip* functions to paths

This commit is contained in:
Brent S. 2025-07-31 02:23:57 -04:00
parent e5f7296d2e
commit 431974283a
Signed by untrusted user: bts.work
GPG Key ID: 004FD489E0203EEE
2 changed files with 182 additions and 33 deletions

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