Compare commits

...

11 Commits

Author SHA1 Message Date
2b6ae03d1a config: add a Require message.
Also update explanation of the intended use-case for this package.
2020-03-02 17:36:29 -08:00
ef0f14a512 Add global config package.
This is a simple config system that I find myself wanting a lot.
2020-03-02 17:11:55 -08:00
6ae393ebf2 Have to explicitly allow darwin/amd64 in the build tag. 2020-01-16 06:19:21 -08:00
76d88c220d Add go mod, update ftime.
+ Stat_t on Darwin amd64 now uses the same struct fields
  for file times as Lunix, not the BSD ones. I need to
  follow up on this.
2020-01-16 06:13:31 -08:00
Kyle Isom
40e015373f Point to goutils-pkg. 2020-01-16 06:13:18 -08:00
50c226b726 Add parts. 2019-01-08 12:46:53 -08:00
070ffb9dff Cleanup testio. 2018-12-17 18:16:52 -08:00
5ac05bd298 Add debug printer. 2018-12-17 18:12:51 -08:00
cf1edf2d31 Add simple proxy. 2018-12-12 14:57:08 -08:00
Kyle Isom
03e8958dd7 Merge pull request #6 from golint-fixer/master
Fix golint import path
2018-10-31 18:31:23 -07:00
golint fixer
6cef585071 Fix golint import path 2018-10-25 09:37:10 -05:00
15 changed files with 587 additions and 3 deletions

View File

@@ -4,7 +4,7 @@ go:
- tip - tip
- 1.9 - 1.9
script: script:
- go get github.com/golang/lint/golint - go get golang.org/x/lint/golint
- go get golang.org/x/tools/cmd/cover - go get golang.org/x/tools/cmd/cover
- go get github.com/kisom/goutils/... - go get github.com/kisom/goutils/...
- go test -cover github.com/kisom/goutils/... - go test -cover github.com/kisom/goutils/...

View File

@@ -7,6 +7,9 @@ Note that for packaging purposes, the goutils-pkg repo should be used: it
pins the library versions to working copies and vendors all depdencies. See pins the library versions to working copies and vendors all depdencies. See
https://github.com/kisom/goutils-pkg for more details. https://github.com/kisom/goutils-pkg for more details.
The goutils-pkg repo [1] has stable versions of the command line
utilities here, along with a vendored snapshot of any dependencies.
Contents: Contents:
ahash/ Provides hashes from string algorithm specifiers. ahash/ Provides hashes from string algorithm specifiers.
@@ -62,3 +65,5 @@ Each program should have a small README in the directory with more
information. information.
All code here is licensed under the MIT license. All code here is licensed under the MIT license.
[1] https://github.com/kisom/goutils-pkg/

9
cmd/parts/README Normal file
View File

@@ -0,0 +1,9 @@
parts: simple parts database for electronic components
Usage: parts [id] -- query the database for a part
parts [-c class] [id] [description] -- store a part in the database
Options:
-f path Path to parts database (default is
/home/kyle/.parts.json).

142
cmd/parts/main.go Normal file
View File

@@ -0,0 +1,142 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/kisom/goutils/die"
)
const dbVersion = "1"
var dbFile = filepath.Join(os.Getenv("HOME"), ".parts.json")
var partsDB = &database{Version: dbVersion}
type part struct {
Name string `json:"name"`
Description string `json:"description"`
Class string `json:"class,omitempty"`
}
func (p part) String() string {
return fmt.Sprintf("%s: %s", p.Name, p.Description)
}
type database struct {
Version string `json:"version"`
LastUpdate int64 `json:"json"`
Parts map[string]part `json:"parts"`
}
func help(w io.Writer) {
fmt.Fprintf(w, `Usage: parts [id] -- query the database for a part
parts [-c class] [id] [description] -- store a part in the database
Options:
-f path Path to parts database (default is
%s).
`, dbFile)
}
func loadDatabase() {
data, err := ioutil.ReadFile(dbFile)
if err != nil && os.IsNotExist(err) {
partsDB = &database{
Version: dbVersion,
Parts: map[string]part{},
}
return
}
die.If(err)
err = json.Unmarshal(data, partsDB)
die.If(err)
}
func findPart(partName string) {
partName = strings.ToLower(partName)
for name, part := range partsDB.Parts {
if strings.Contains(strings.ToLower(name), partName) {
fmt.Println(part.String())
}
}
}
func writeDB() {
data, err := json.Marshal(partsDB)
die.If(err)
err = ioutil.WriteFile(dbFile, data, 0644)
die.If(err)
}
func storePart(name, class, description string) {
p, exists := partsDB.Parts[name]
if exists {
fmt.Printf("warning: replacing part %s\n", name)
fmt.Printf("\t%s\n", p.String())
}
partsDB.Parts[name] = part{
Name: name,
Class: class,
Description: description,
}
writeDB()
}
func listParts() {
parts := make([]string, 0, len(partsDB.Parts))
for partName := range partsDB.Parts {
parts = append(parts, partName)
}
sort.Strings(parts)
for _, partName := range parts {
fmt.Println(partsDB.Parts[partName].String())
}
}
func main() {
var class string
var helpFlag bool
flag.StringVar(&class, "c", "", "device class")
flag.StringVar(&dbFile, "f", dbFile, "`path` to database")
flag.BoolVar(&helpFlag, "h", false, "Print a help message.")
flag.Parse()
if helpFlag {
help(os.Stdout)
return
}
loadDatabase()
switch flag.NArg() {
case 0:
help(os.Stdout)
return
case 1:
partName := flag.Arg(0)
if partName == "list" {
listParts()
} else {
findPart(flag.Arg(0))
}
return
default:
description := strings.Join(flag.Args()[1:], " ")
storePart(flag.Arg(0), class, description)
return
}
}

46
cmd/sprox/main.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"flag"
"io"
"log"
"net"
"github.com/kisom/goutils/die"
)
func proxy(conn net.Conn, inside string) error {
proxyConn, err := net.Dial("tcp", inside)
if err != nil {
return err
}
defer proxyConn.Close()
defer conn.Close()
go func() {
io.Copy(conn, proxyConn)
}()
_, err = io.Copy(proxyConn, conn)
return err
}
func main() {
var outside, inside string
flag.StringVar(&outside, "f", "8080", "outside port")
flag.StringVar(&inside, "p", "4000", "inside port")
flag.Parse()
l, err := net.Listen("tcp", "0.0.0.0:"+outside)
die.If(err)
for {
conn, err := l.Accept()
if err != nil {
log.Println(err)
continue
}
go proxy(conn, "127.0.0.1:"+inside)
}
}

110
config/config.go Normal file
View File

@@ -0,0 +1,110 @@
// Package config implements a simple global configuration system that
// supports a file with key=value pairs and environment variables. Note
// that the config system is global.
//
// This package is intended to be used for small daemons: some configuration
// file is optionally populated at program start, then this is used to
// transparently look up configuration values from either that file or the
// environment.
package config
import (
"bufio"
"log"
"os"
"strings"
)
// NB: Rather than define a singleton type, everything is defined at
// the top-level
var (
vars = map[string]string{}
prefix = ""
)
// SetEnvPrefix sets the prefix for all environment variables; it's
// assumed to not be needed for files.
func SetEnvPrefix(pfx string) {
prefix = pfx
}
func addLine(line string) {
if strings.HasPrefix(line, "#") || line == "" {
return
}
lineParts := strings.SplitN(line, "=", 2)
if len(lineParts) != 2 {
log.Print("skipping line: ", line)
return // silently ignore empty keys
}
lineParts[0] = strings.TrimSpace(lineParts[0])
lineParts[1] = strings.TrimSpace(lineParts[1])
vars[lineParts[0]] = lineParts[1]
}
// LoadFile scans the file at path for key=value pairs and adds them
// to the configuration.
func LoadFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
addLine(line)
}
if err = scanner.Err(); err != nil {
return err
}
return nil
}
// Get retrieves a value from either a configuration file or the
// environment. Note that values from a file will override environment
// variables.
func Get(key string) string {
if v, ok := vars[key]; ok {
return v
}
return os.Getenv(prefix + key)
}
// GetDefault retrieves a value from either a configuration file or
// the environment. Note that value from a file will override
// environment variables. If a value isn't found (e.g. Get returns an
// empty string), the default value will be used.
func GetDefault(key, def string) string {
if v := Get(key); v != "" {
return v
}
return def
}
// Require retrieves a value from either a configuration file or the
// environment. If the key isn't present, it will call log.Fatal, printing
// the missing key.
func Require(key string) string {
if v, ok := vars[key]; ok {
return v
}
v, ok := os.LookupEnv(prefix + key)
if !ok {
var envMessage string
if prefix != "" {
envMessage = " (note: looked for the key " + prefix + key
envMessage += " in the local env)"
}
log.Fatalf("missing required configuration value %s%s", key, envMessage)
}
return v
}

66
config/config_test.go Normal file
View File

@@ -0,0 +1,66 @@
package config
import (
"os"
"testing"
)
const (
testFilePath = "testdata/test.env"
// Keys
kOrder = "ORDER"
kSpecies = "SPECIES"
kName = "COMMON_NAME"
// Env
eOrder = "corvus"
eSpecies = "corvus corax"
eName = "northern raven"
// File
fOrder = "stringiformes"
fSpecies = "strix aluco"
// Name isn't set in the file to test fall through.
)
func init() {
os.Setenv(kOrder, eOrder)
os.Setenv(kSpecies, eSpecies)
os.Setenv(kName, eName)
}
func TestLoadEnvOnly(t *testing.T) {
order := Get(kOrder)
species := Get(kSpecies)
if order != eOrder {
t.Errorf("want %s, have %s", eOrder, order)
}
if species != eSpecies {
t.Errorf("want %s, have %s", eSpecies, species)
}
}
func TestLoadFile(t *testing.T) {
err := LoadFile(testFilePath)
if err != nil {
t.Fatal(err)
}
order := Get(kOrder)
species := Get(kSpecies)
name := Get(kName)
if order != fOrder {
t.Errorf("want %s, have %s", fOrder, order)
}
if species != fSpecies {
t.Errorf("want %s, have %s", fSpecies, species)
}
if name != eName {
t.Errorf("want %s, have %s", eName, name)
}
}

2
config/testdata/test.env vendored Normal file
View File

@@ -0,0 +1,2 @@
ORDER=stringiformes
SPECIES=strix aluco

76
dbg/dbg.go Normal file
View File

@@ -0,0 +1,76 @@
// Package dbg implements a debug printer.
package dbg
import (
"fmt"
"io"
"os"
)
// A DebugPrinter is a drop-in replacement for fmt.Print*, and also acts as
// an io.WriteCloser when enabled.
type DebugPrinter struct {
// If Enabled is false, the print statements won't do anything.
Enabled bool
out io.WriteCloser
}
// Close satisfies the Closer interface.
func (dbg *DebugPrinter) Close() error {
return dbg.out.Close()
}
// Write satisfies the Writer interface.
func (dbg *DebugPrinter) Write(p []byte) (int, error) {
if dbg.Enabled {
return dbg.out.Write(p)
}
return 0, nil
}
// New returns a new DebugPrinter on os.Stdout.
func New() *DebugPrinter {
return &DebugPrinter{
out: os.Stdout,
}
}
// ToFile sets up a new DebugPrinter to a file, truncating it if it exists.
func ToFile(path string) (*DebugPrinter, error) {
file, err := os.Create(path)
if err != nil {
return nil, err
}
return &DebugPrinter{
out: file,
}, nil
}
// To sets up a new DebugPrint to an io.WriteCloser.
func To(w io.WriteCloser) *DebugPrinter {
return &DebugPrinter{
out: w,
}
}
// Print calls fmt.Print if Enabled is true.
func (dbg DebugPrinter) Print(v ...interface{}) {
if dbg.Enabled {
fmt.Fprint(dbg.out, v...)
}
}
// Println calls fmt.Println if Enabled is true.
func (dbg DebugPrinter) Println(v ...interface{}) {
if dbg.Enabled {
fmt.Fprintln(dbg.out, v...)
}
}
// Printf calls fmt.Printf if Enabled is true.
func (dbg DebugPrinter) Printf(format string, v ...interface{}) {
if dbg.Enabled {
fmt.Fprintf(dbg.out, format, v...)
}
}

120
dbg/dbg_test.go Normal file
View File

@@ -0,0 +1,120 @@
package dbg
import (
"io/ioutil"
"os"
"testing"
"github.com/kisom/goutils/testio"
"github.com/stretchr/testify/require"
)
func TestNew(t *testing.T) {
buf := testio.NewBufCloser(nil)
dbg := New()
dbg.out = buf
dbg.Print("hello")
dbg.Println("hello")
dbg.Printf("hello %s", "world")
require.Equal(t, 0, buf.Len())
dbg.Enabled = true
dbg.Print("hello") // +5
dbg.Println("hello") // +6
dbg.Printf("hello %s", "world") // +11
require.Equal(t, 22, buf.Len())
err := dbg.Close()
require.NoError(t, err)
}
func TestTo(t *testing.T) {
buf := testio.NewBufCloser(nil)
dbg := To(buf)
dbg.Print("hello")
dbg.Println("hello")
dbg.Printf("hello %s", "world")
require.Equal(t, 0, buf.Len())
dbg.Enabled = true
dbg.Print("hello") // +5
dbg.Println("hello") // +6
dbg.Printf("hello %s", "world") // +11
require.Equal(t, 22, buf.Len())
err := dbg.Close()
require.NoError(t, err)
}
func TestToFile(t *testing.T) {
testFile, err := ioutil.TempFile("", "dbg")
require.NoError(t, err)
err = testFile.Close()
require.NoError(t, err)
testFileName := testFile.Name()
defer os.Remove(testFileName)
dbg, err := ToFile(testFileName)
require.NoError(t, err)
dbg.Print("hello")
dbg.Println("hello")
dbg.Printf("hello %s", "world")
stat, err := os.Stat(testFileName)
require.NoError(t, err)
require.EqualValues(t, 0, stat.Size())
dbg.Enabled = true
dbg.Print("hello") // +5
dbg.Println("hello") // +6
dbg.Printf("hello %s", "world") // +11
stat, err = os.Stat(testFileName)
require.NoError(t, err)
require.EqualValues(t, 22, stat.Size())
err = dbg.Close()
require.NoError(t, err)
}
func TestWriting(t *testing.T) {
data := []byte("hello, world")
buf := testio.NewBufCloser(nil)
dbg := To(buf)
n, err := dbg.Write(data)
require.NoError(t, err)
require.EqualValues(t, 0, n)
dbg.Enabled = true
n, err = dbg.Write(data)
require.NoError(t, err)
require.EqualValues(t, 12, n)
err = dbg.Close()
require.NoError(t, err)
}
func TestToFileError(t *testing.T) {
testFile, err := ioutil.TempFile("", "dbg")
require.NoError(t, err)
err = testFile.Chmod(0400)
require.NoError(t, err)
err = testFile.Close()
require.NoError(t, err)
testFileName := testFile.Name()
_, err = ToFile(testFileName)
require.Error(t, err)
err = os.Remove(testFileName)
require.NoError(t, err)
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/kisom/goutils
go 1.13

View File

@@ -1,4 +1,4 @@
// +build freebsd darwin netbsd // +build freebsd darwin,386 netbsd
package lib package lib

View File

@@ -1,4 +1,4 @@
// +build unix linux openbsd // +build unix linux openbsd darwin,amd64
package lib package lib

Binary file not shown.

View File

@@ -194,6 +194,11 @@ func (buf *BufCloser) Bytes() []byte {
return buf.buf.Bytes() return buf.buf.Bytes()
} }
// Len returns the length of the buffer.
func (buf *BufCloser) Len() int {
return buf.buf.Len()
}
// NewBufCloser creates and initializes a new BufCloser using buf as // NewBufCloser creates and initializes a new BufCloser using buf as
// its initial contents. It is intended to prepare a BufCloser to read // its initial contents. It is intended to prepare a BufCloser to read
// existing data. It can also be used to size the internal buffer for // existing data. It can also be used to size the internal buffer for