futil

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 20, 2024 License: Apache-2.0 Imports: 20 Imported by: 13

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ExtractFilePerms = fs.FileMode(0600)
	ExtractDirPerms  = fs.FileMode(0700)
)
View Source
var DefaultDirPerms = 0700
View Source
var DefaultFilePerms = 0600
View Source
var UserHomeDir = os.UserHomeDir

Functions

func AbsDir

func AbsDir() string

AbsDir returns the absolute path to the current working directory. If unable to determine, returns empty string.

func Append added in v0.3.0

func Append[T string | []byte](path string, these ...T) error

Append appends one or more slices of bytes to the specified file. If the file does not exist, it is created. The file is opened in append and write-only mode.

func Cat

func Cat(path string) error

Cat just prints the contents of target file to stdout. If the file cannot be found or opened returns error. For performance, the entire file is loaded into memory before being written to stdout. Do not use this on anything where the size of the file is unknown and untrusted.

func CreateDir

func CreateDir(path string) error

CreateDir a new directory with the DefaultDirPerms creating any new directories as well (see os.MkdirAll)

func DirEntries

func DirEntries(path string) []string

DirEntries returns a slice of strings with all the files in the directory at that path joined to their path (as is usually wanted). Returns an empty slice if empty or path doesn't point to a directory.

func DirEntriesAddSlash

func DirEntriesAddSlash(entries []string) []string

DirEntriesAddSlash adds a filepath.Separator to the end of all entries passed that are directories.

func DirEntriesAddSlashPath

func DirEntriesAddSlashPath(path string) []string

DirEntriesAddSlashPath returns Entries passed to AddSlash so that all directories will have a trailing slash.

func DirIsEmpty

func DirIsEmpty(path string) bool

DirIsEmpty returns true if the directory at path either contains no files or only files with zero length. Directories are recursively checked. Returns false, however, if the path does not exist.

Example (Emptyfiles)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.DirIsEmpty(`testdata/emptyfiles`))
}
Output:

true
Example (Hiddenfiles)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.FileSize(`testdata/hiddenfiles/.foo`))
	fmt.Println(futil.DirIsEmpty(`testdata/hiddenfiles`))
}
Output:

5
false
Example (Notemptyfiles)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.FileSize(`testdata/notemptyfiles/README.md`))
	fmt.Println(futil.DirIsEmpty(`testdata/notemptyfiles`))
}
Output:

5
false
Example (Notexist)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.DirIsEmpty(`testdata/notexist`))
}
Output:

false

func DirName

func DirName() string

DirName returns the current working directory name or an empty string.

Example
package main

import (
	"fmt"
	"os"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	os.Chdir(`testdata`)
	fmt.Println(futil.DirName())
	os.Chdir(`..`)
}
Output:

testdata

func DupPerms

func DupPerms(orig, clone string) error

DupPerms duplicates the perms of one file or directory onto the second.

func Exists

func Exists(path string) bool

Exists returns true if the given path was absolutely found to exist on the system. A false return value means either the file does not exists or it was not able to determine if it exists or not. Use NotExists instead.

WARNING: do not use this function if a definitive check for the non-existence of a file is required since the possible indeterminate error state is a possibility. These checks are also not atomic on many file systems so avoid this usage for pseudo-semaphore designs and depend on file locks.

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.Exists("testdata/exists"))
	fmt.Println(futil.Exists("testdata"))
}
Output:

true
true

func ExtractEmbed

func ExtractEmbed(it embed.FS, root, target string) error

ExtractEmbed walks the embedded file system and duplicates its structure into the target directory beginning with root. Since embed.FS drops all permissions information from the original files all files and directories are written as read/write (0600) for the current effective user (0600 for file, 0700 for directories). This default can be changed by setting the package variables ExtractFilePerms and ExtractDirPerms. Note that each embedded file is full buffered into memory before writing.

Example
package main

import (
	"embed"
	"fmt"
	"os"
	"path/filepath"

	"github.com/rwxrob/bonzai/futil"
)

//go:embed all:testdata/testfs
var testfs embed.FS

func main() {

	// go:embed all:testdata/testfs
	// var testfs embed.FS
	defer os.RemoveAll("testdata/testfsout")

	if err := futil.ExtractEmbed(testfs,
		"testdata/testfs", "testdata/testfsout"); err != nil {
		fmt.Println(err)
	}

	stuff := []string{
		`foo`,
		`_notignored`,
		`.secret`,
		`dir`,
		`dir/README.md`,
	}

	for _, i := range stuff {
		f, err := os.Stat(filepath.Join("testdata/testfsout", i))
		if err != nil {
			fmt.Println(err)
			continue
		}
		fmt.Printf("%v %v\n", i, f.Mode())
	}

}
Output:

foo -rw-------
_notignored -rw-------
.secret -rw-------
dir drwx------
dir/README.md -rw-------
Example (Confirm_Default_Read)
package main

import (
	"fmt"
	"os"
)

func main() {

	// go:embed all:testdata/testfs
	// var testfs embed.FS

	foo, err := os.Open("testdata/testfs/foo")
	if err != nil {
		fmt.Println(err)
	}
	info, err := foo.Stat()
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(info.Mode())

}
Output:

-rwxr-xr-x

func Fetch

func Fetch(from, to string) error

Fetch fetches the specified file at the give "from" URL and saves it "to" the specified file path. The name is *not* inferred. If timeouts, status, and contexts are required use the net/http package instead. Will block until the entire file is downloaded. For more involved downloading needs consider the github.com/cavaliercoder/grab package.

Example
package main

import (
	"fmt"
	"net/http"

	ht "net/http/httptest"
	"os"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	// serve get
	handler := http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			fmt.Fprintf(w, `random file content`)
		})
	svr := ht.NewServer(handler)
	defer svr.Close()
	defer os.Remove(`testdata/file`)

	// not found
	handler = http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(404)
		})
	notfound := ht.NewServer(handler)
	defer notfound.Close()

	if err := futil.Fetch(svr.URL, `testdata/file`); err != nil {
		fmt.Println(err)
	}

	it, _ := os.ReadFile(`testdata/file`)
	fmt.Println(string(it))

	if err := futil.Fetch(notfound.URL, `testdata/file`); err != nil {
		fmt.Println(err)
	}

}
Output:

random file content
404 Not Found

func Field

func Field(path string, n int) []string

Field returns the field (as returned by strings.Fields) from each line of the file located at path (like awk '{print $1}'). Always returns a slice even if empty. If that field does not exist on a line, that line is omitted. Note that field count starts at 1 (not 0).

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.Field(`testdata/fieldtest`, 2))
}
Output:

[foo bar baz]

func FileIsEmpty

func FileIsEmpty(path string) bool

FileIsEmpty checks for files of zero length in an OS-agnostic way. If the file does not exist returns false.

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.FileIsEmpty(`testdata/overwritten`))
	fmt.Println(futil.FileIsEmpty(`testdata/ovewritten`))
	futil.Touch(`testdata/emptyfile`)
	fmt.Println(futil.FileIsEmpty(`testdata/emptyfile`))
}
Output:

false
false
true

func FileSize

func FileSize(path string) int64

FileSize returns the info.Size() of the file from os.Stat(path). Returns -1 if unable to determine.

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.FileSize(`testdata/headtail`))
}
Output:

24

func FindString

func FindString(path, regx string) (string, error)

FindString reads the file at path into a string, dynamically compiles the regx regular expression, and returns FindString on it returning an error if file does not exist, or if regular expression could not compile. Note that it is not an error to not find the string, which causes an empty string to be returned. See regexp.FindString.

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	str, err := futil.FindString(`testdata/headtail`, `thre+`)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(str)
}
Output:

three
func Head(path string, n int) ([]string, error)

Head is like the UNIX head command returning only that number of lines from the top of a file.

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	lines, err := futil.Head(`testdata/headtail`, 2)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(lines)

}
Output:

[one two]
Example (Over)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	lines, err := futil.Head(`testdata/headtail`, 20)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(lines)

}
Output:

[one two three four five]

func HereOrAbove

func HereOrAbove(name string) (string, error)

HereOrAbove returns the full path to the file or directory if it is found in the current working directory, or if not exists in any parent directory recursively. Returns ErrNotExist error with the name if not found.

Example (Above)
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	dir, _ := os.Getwd()
	defer func() { os.Chdir(dir) }()
	os.Chdir("testdata/adir")

	path, err := futil.HereOrAbove("anotherfile")
	if err != nil {
		fmt.Println(err)
	}
	d := strings.Split(path, string(filepath.Separator))
	fmt.Println(d[len(d)-2:])

}
Output:

[testdata anotherfile]
Example (Here)
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	dir, _ := os.Getwd()
	defer func() { os.Chdir(dir) }()
	os.Chdir("testdata/adir")

	path, err := futil.HereOrAbove("afile")
	if err != nil {
		fmt.Println(err)
	}
	d := strings.Split(path, string(filepath.Separator))
	fmt.Println(d[len(d)-2:])

}
Output:

[adir afile]

func IsDir

func IsDir(path string) bool

IsDir returns true if the indicated path is a directory. Returns false and logs if unable to determine.

func IsDirFS

func IsDirFS(fsys fs.FS, path string) bool

IsDirFS simply a shortcut for fs.Stat().IsDir(). Only returns true if the path is a directory. If not a directory (or an error prevented confirming it is a directory) then returns false.

func IsHardLink(path string) (bool, error)

IsHardLink attempts to determine if the file at the end of path is a unix/linux hard link by counting its number of links. On Windows always returns false.

func IsSymLink(path string) (bool, error)

IsSymLink returns true if the file at the base of path (not any intermediate directories) is itself a symbolic link.

func Isosec

func Isosec() string

Isosec returns the GMT current time in ISO8601 (RFC3339) without any punctuation or the T. This is frequently a very good unique suffix that has the added advantage of being chronologically sortable and more readable than the epoch. (Also see Second())

func LatestChange

func LatestChange(root string) (string, fs.FileInfo)

LatestChange walks the directory rooted at root looking at each file or directory within it recursively (including itself) and returns the time of the most recent change along with its full path. LastChange returns nil FileInfo and empty string if root is not a directory.

func ModTime

func ModTime(path string) time.Time

ModTime returns the [FileInfo.ModTime] from [Stat] as a convenience or returns a zero time if not. See time.IsZero.

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.ModTime("testdata/file").IsZero())
	fmt.Println(futil.ModTime("testdata/none"))
}
Output:

true
0001-01-01 00:00:00 +0000 UTC

func NameIsInt

func NameIsInt(path string) bool

NameIsInt returns true if the filepath.Base name of the passed path is a valid positive integer (including 0).

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.NameIsInt(`/some/1`))
	fmt.Println(futil.NameIsInt(`100`))
	fmt.Println(futil.NameIsInt(`-100`))
	fmt.Println(futil.NameIsInt(`/some/-1`))
	fmt.Println(futil.NameIsInt(`/some/`))
}
Output:

true
true
false
false
false

func NotExists

func NotExists(path string) bool

NotExists definitively returns true if the given path does not exist. See Exists as well.

func Overwrite

func Overwrite(path, buf string) error

Overwrite replaces the content of the target file at path with the string passed using the same file-level locking used by Go. File permissions are preserved if file exists.

Example
package main

import (
	"fmt"
	"os"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	err := futil.Overwrite(`testdata/overwritten`, `hello`)
	defer os.Remove(`testdata/overwritten`)
	if err != nil {
		fmt.Println(err)
	}
	futil.Cat(`testdata/overwritten`)
}
Output:

hello

func Preserve

func Preserve(target string) (string, error)

Preserve moves the target to a new name with an "~" isosec suffix usually in anticipation of eventual deletion or restoration for transactionally safe dealings with directories and files. Returns the name of the new file or directory. Does *not* return an error if the target doesn't exist (since there is nothing to preserve) and returns an empty string. Pair this with defer RevertIfMissing to provide transactional backups of files and directories.

func RelPaths

func RelPaths(it fs.FS, root string) []string

Paths returns a list of full paths to each of the directories or files from the root but with the path to the root stripped resulting in relative paths.

func Replace

func Replace(orig, url string) error

Replace replaces a file at a specified location with another successfully retrieved file from the specified URL or file path and duplicates the original files permissions. Only http and https URLs are currently supported. For security reasons, no backup copy of the replaced executable is kept. Also no checksum validation of the file is performed (which is fine in most cases where the connection has been secured with HTTPS).

Example
package main

import (
	"fmt"
	"net/http"

	ht "net/http/httptest"
	"os"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	// serve get
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `something random`)
	})
	svr := ht.NewServer(handler)
	defer svr.Close()

	// create a file to replace
	os.Create(`testdata/replaceme`)
	defer os.Remove(`testdata/replaceme`)
	os.Chmod(`testdata/replaceme`, 0400)

	// show info about control file
	info, err := os.Stat(`testdata/replaceme`)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(info.Mode())
	fmt.Println(info.Size())

	// replace it with local url
	futil.Replace(`testdata/replaceme`, svr.URL)

	// check that it is new
	info, err = os.Stat(`testdata/replaceme`)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(info.Mode())
	fmt.Println(info.Size())

}
Output:

-r--------
0
-r--------
16

func ReplaceAllString

func ReplaceAllString(path, regx, repl string) error

ReplaceAllString loads the file at path into buffer, compiles the regx, and replaces all matches with repl same as function of same name overwriting the target file at path. Returns and error if unable to compile the regular expression or read or overwrite the file.

Normally, it is better to pre-compile regular expressions. This function is designed for applications where the regular expression and replacement string are passed by the user at runtime.

func RevertIfMissing

func RevertIfMissing(target, backup string) error

RevertIfMissing is designed to be called from defer after calling a Preserve and storing the backup path to a variable. If the target is missing, backup is renamed back to the target. If the target is *not* missing, backup is removed. If the backup is an empty string no backup restoration is assumed and a nil error is returned with no action. This allows the output of Preserve to be passed directly to RevertIfMissing without complicating the defer.

func Tail

func Tail(path string, n int) ([]string, error)

Tail is like the UNIX tail command returning only that number of lines from the bottom of a file. If n is negative counts that many lines from the top of the file (effectively the line to start from).

Example
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	lines, err := futil.Tail(`testdata/headtail`, 2)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(lines)

}
Output:

[four five]
Example (Negative)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	lines, err := futil.Tail(`testdata/headtail`, -2)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(lines)

}
Output:

[three four five]
Example (Over)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	lines, err := futil.Tail(`testdata/headtail`, 20)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(lines)

}
Output:

[one two three four five]

func Tilde2Home

func Tilde2Home(dir string) string

Tilde2Home expands a Tilde (~) prefix into a proper os.UserHomeDir path. If it cannot find os.UserHomeDir simple returns unchanged path. Will not expand for specific users (~username).

func Touch

func Touch(path string) error

Touch creates a new file at path or updates the time stamp of existing. If a new file is needed creates it with DefaultFilePerms permissions (instead of 0666 as default os.Create does). If the directory does not exist all parent directories are created using DefaultDirPerms.

Example (Create)
package main

import (
	"fmt"
	"os"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	fmt.Println(futil.NotExists("testdata/foo"))
	futil.Touch("testdata/foo")
	fmt.Println(futil.Exists("testdata/foo"))
	os.Remove("testdata/foo")
}
Output:

true
true
Example (Update)
package main

import (
	"fmt"
	"log"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	// first create it and capture the time as a string
	futil.Touch("testdata/tmpfile")
	u1 := futil.ModTime("testdata/tmpfile")
	log.Print(u1)

	// touch it and capture the new time
	futil.Touch("testdata/tmpfile")
	u2 := futil.ModTime("testdata/tmpfile")
	log.Print(u2)

	// check that they are not equiv
	fmt.Println(u1 == u2)

}
Output:

false

func UserCacheDir

func UserCacheDir() (string, error)

UserCacheDir returns the path to the user's cache directory, checking [$XDG_CACHE_HOME] environment variable first. It defaults to joining UserHomeDir with `.cache` if the variable is unset on Unix systems (including darwin unlike pkg/os.UserCacheDir). On Windows it returns %LocalAppData%. It returns an error if UserHomeDir fails.

func UserConfigDir

func UserConfigDir() (string, error)

UserConfigDir returns the path to the user's config directory, checking [$XDG_CONFIG_HOME] environment variable first. It defaults to joining UserHomeDir with `.config` if the variable is unset on Unix systems. Note that this is unlike pkg/os.UserConfigDir by design for better intuitive consistency despite the defined "standards" from operating system vendors. This difference is particularly important given the prevalence of WSL2 usage requiring a clear distinction.

func UserStateDir

func UserStateDir() (string, error)

UserStateDir returns the path to the user's state directory, checking [XDG_STATE_HOME] environment variable first. It defaults to joining UserHomeDir with `.local/state` if the variable is unset. It returns an error if UserHomeDir fails.

Example
package main

import (
	"fmt"
	"os"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	originalEnv := os.Getenv("XDG_STATE_HOME")
	defer os.Setenv("XDG_STATE_HOME", originalEnv)
	os.Setenv("XDG_STATE_HOME", "/custom/state")

	originalHome := os.Getenv("HOME")
	defer os.Setenv("HOME", originalHome)
	os.Setenv("HOME", "/home/testuser")

	path, err := futil.UserStateDir()
	if err != nil {
		fmt.Println("error:", err)
	} else {
		fmt.Println(path)
	}

	os.Unsetenv("XDG_STATE_HOME")
	path, err = futil.UserStateDir()
	if err != nil {
		fmt.Println("error:", err)
	} else {
		fmt.Println(path)
	}

}
Output:

/custom/state
/home/testuser/.local/state

Types

type ErrExist

type ErrExist struct {
	N string
}

func (ErrExist) Error

func (e ErrExist) Error() string

type ErrNotExist

type ErrNotExist struct {
	N string
}

func (ErrNotExist) Error

func (e ErrNotExist) Error() string

type ErrorExists

type ErrorExists struct {
	P string
}

func (ErrorExists) Error

func (e ErrorExists) Error() string

type Multipart

type Multipart struct {
	Delimiter string
	Map       map[string]string
}

Multipart is meant to contain the delimited sections of output and can be marshalled into a single delimited string safely and automatically simply by using it in a string context.

func (Multipart) MarshalText

func (o Multipart) MarshalText() ([]byte, error)

MarshalText fulfills the encoding.TextMarshaler interface by delimiting each section of output with a unique delimiter line that contains a space and the key for each section. Order of sections is indeterminate officially (but consistent for testing, per Go). The special "break" delimiter is always the last line. The Delimiter is used if defined, otherwise one is automatically assigned.

func (Multipart) String

func (o Multipart) String() string

String fulfills the fmt.Stringer interface by calling MarshalText.

func (*Multipart) UnmarshalText

func (o *Multipart) UnmarshalText(text []byte) error

UnmarshalText fulfills the encoding.TextUnmarshaler interface by using its internal Delimiter or sensing the delimiter as the first text field (up to the first space) if not set and using that delimiter to parse the remaining data into the key/value pairs ending when either the end of text is encountered or the special "break" delimiter is read.

Example (Explicit)
package main

import (
	"fmt"

	"github.com/rwxrob/bonzai/futil"
)

func main() {
	out := futil.Multipart{
		Delimiter: `IMMADELIM`,
		Map:       map[string]string{`dummy`: `just checking`},
	}
	buf := `
random
ignored
lines
here
IMMADELIM stdout
some standard output

on multiple lines
IMMADELIM stderr
some standard err on single line
IMMADELIM exitval
-1
IMMADELIM break
`
	err := out.UnmarshalText([]byte(buf))
	if err != nil {
		fmt.Println(err)
	}
	fmt.Print(out)
}
Output:

IMMADELIM stdout
some standard output

on multiple lines
IMMADELIM stderr
some standard err on single line
IMMADELIM exitval
-1
IMMADELIM break

type PathEntry

type PathEntry struct {
	Path string
	Info fs.FileInfo
}

PathEntry contains the fully qualified path to a DirEntry and the returned FileInfo for that specific path. PathEntry saves the work of fetching this information a second time when functions in this package have already retrieved it.

func IntDirs

func IntDirs(target string) (paths []PathEntry, low, high int)

IntDirs returns all the directory entries within the target directory that have integer names. The lowest integer and highest integer values are also returned. Only positive integers are checked. This is useful when using directory names as database-friendly unique primary keys for other file system content.

IntDirs returns an empty slice and -1 values if no matches are found.

Errors looking up the FileInfo cause Info to be nil.

Example
package main

import (
	"fmt"
	"path/filepath"

	"github.com/rwxrob/bonzai/futil"
)

func main() {

	dirs, low, high := futil.IntDirs("testdata/ints")

	fmt.Println(low)
	fmt.Println(high)

	for _, d := range dirs {
		fmt.Printf("%v ", filepath.Base(d.Path))
	}

}
Output:

2
10
10 2 3 4 5 6 7 8 9

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL