Skip to content

Commit

Permalink
feat: Redesign package APIs (#5)
Browse files Browse the repository at this point in the history
* feat: Add ability to enable/disable color

* feat: Redesign command package api

* chore: Clean up fatal tests

* feat: Completely redesign spinner package api

* feat: Redesign and refactor file api

* chore: Remove unused dependencies

* chore: Fix lint errors

* feat: Add ability for spinner to write debug messages

* feat: Spinner is now an io.Writer, remove debug writer and Debugf

* feat: Add UpdateMessage method to Spinner

* feat: Ellipses counts as part of max message length for spinner

* chore: Add additional docs
  • Loading branch information
cszatmary authored Jan 26, 2021
1 parent 229ef2f commit 4006d4d
Show file tree
Hide file tree
Showing 14 changed files with 1,167 additions and 271 deletions.
11 changes: 6 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
version: 2.1

cache_key: &cache_key goutil-deps-20210108-{{ checksum "go.sum" }}

jobs:
lint-test:
docker:
- image: circleci/golang:1.12
working_directory: ~/goutils
- image: cimg/go:1.15
steps:
- checkout
- restore_cache:
name: Restore dependency cache
keys:
- goutils-deps-{{ checksum "go.sum" }}
- *cache_key
- run:
name: Install dependencies
command: make setup
- save_cache:
name: Cache dependencies
key: goutils-deps-{{ checksum "go.sum" }}
key: *cache_key
paths:
- /go/pkg
- ~/go/pkg
- run:
name: Run linter
command: make lint
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# goutils

[![Go Reference](https://pkg.go.dev/badge/github.com/TouchBistro/goutils.svg)](https://pkg.go.dev/github.com/TouchBistro/goutils)

A collection of useful go utilities. See [the docs](https://pkg.go.dev/github.com/TouchBistro/goutils) for more information.

## Installation

```
go get github.com/TouchBistro/goutils
```

## License

MIT © TouchBistro, see [LICENSE](LICENSE) for details.
20 changes: 18 additions & 2 deletions color/color.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package color provides functions for creating coloured strings.
package color

import (
Expand All @@ -19,17 +20,21 @@ const (

// Support for NO_COLOR env var
// https://no-color.org/
var noColor = false
var (
noColor = false
enabled bool
)

func init() {
// The standard says the value doesn't matter, only whether or not it's set
if _, ok := os.LookupEnv("NO_COLOR"); ok {
noColor = true
}
enabled = !noColor
}

func apply(str string, start, end int) string {
if noColor {
if !enabled {
return str
}

Expand All @@ -39,6 +44,17 @@ func apply(str string, start, end int) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[%dm", start, sanitized, end)
}

// SetEnabled sets whether color is enabled or disabled.
// If the NO_COLOR environment variable is set, this function will
// do nothing as NO_COLOR takes precedence.
func SetEnabled(e bool) {
// NO_COLOR overrides this
if noColor {
return
}
enabled = e
}

// Red creates a red colored string
func Red(str string) string {
return apply(str, ansiFgRed, ansiResetFg)
Expand Down
57 changes: 31 additions & 26 deletions color/color_test.go
Original file line number Diff line number Diff line change
@@ -1,82 +1,87 @@
package color
package color_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/TouchBistro/goutils/color"
)

func TestColors(t *testing.T) {
noColor = false

color.SetEnabled(true)
tests := []struct {
name string
colorFn func(string) string
input string
expected string
name string
colorFn func(string) string
input string
want string
}{
{
"Red() test",
Red,
color.Red,
"foo bar",
"\x1b[31mfoo bar\x1b[39m",
},
{
"Green() test",
Green,
color.Green,
"foo bar",
"\x1b[32mfoo bar\x1b[39m",
},
{
"Yellow() test",
Yellow,
color.Yellow,
"foo bar",
"\x1b[33mfoo bar\x1b[39m",
},
{
"Blue() test",
Blue,
color.Blue,
"foo bar",
"\x1b[34mfoo bar\x1b[39m",
},
{
"Magenta() test",
Magenta,
color.Magenta,
"foo bar",
"\x1b[35mfoo bar\x1b[39m",
},
{
"Cyan() test",
Cyan,
color.Cyan,
"foo bar",
"\x1b[36mfoo bar\x1b[39m",
},
{
"White() test",
White,
color.White,
"foo bar",
"\x1b[37mfoo bar\x1b[39m",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
received := tt.colorFn(tt.input)
assert.Equal(t, tt.expected, received)
got := tt.colorFn(tt.input)
if got != tt.want {
t.Errorf("got %s, want %s", got, tt.want)
}
})
}
}

func TestStripReset(t *testing.T) {
noColor = false

received := Red("foo \x1b[39mbar")
assert.Equal(t, "\x1b[31mfoo bar\x1b[39m", received)
color.SetEnabled(true)
got := color.Red("foo \x1b[39mbar")
want := "\x1b[31mfoo bar\x1b[39m"
if got != want {
t.Errorf("got %s, want %s", got, want)
}
}

func TestNoColor(t *testing.T) {
noColor = true

received := Red("foo bar")
assert.Equal(t, "foo bar", received)
func TestColorDisabled(t *testing.T) {
color.SetEnabled(false)
got := color.Red("foo bar")
want := "foo bar"
if got != want {
t.Errorf("got %s, want %s", got, want)
}
}
118 changes: 92 additions & 26 deletions command/command.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,113 @@
// Package command provides functionality for working with programs the host OS.
// It provides a high level API over os/exec for running commands, which is
// easier to use for common cases.
package command

import (
"fmt"
"io"
"os/exec"
"strings"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)

func IsCommandAvailable(command string) bool {
// IsAvailable checks if command is available on the system. This is done by
// checking if command exists within the user's PATH.
func IsAvailable(command string) bool {
_, err := exec.LookPath(command)
if err != nil {
log.WithFields(log.Fields{"error": err.Error(), "command": command}).Debug("Error looking up command.")
return false
return err == nil
}

// Command manages the configuration of a command
// that will be run in a child process.
type Command struct {
stdin io.Reader
stdout io.Writer
stderr io.Writer
env map[string]string
dir string
}

// New creates a command instance from the given options.
func New(opts ...Option) *Command {
c := &Command{}
for _, opt := range opts {
opt(c)
}
return true
return c
}

func Exec(cmdName string, args []string, id string, opts ...func(*exec.Cmd)) error {
cmd := exec.Command(cmdName, args...)
// Option is a function that takes a command and applies
// a configuration to it.
type Option func(*Command)

stdout := log.WithFields(log.Fields{
"id": id,
}).WriterLevel(log.DebugLevel)
defer stdout.Close()
// WithStdin sets the reader the the command's stdin should read from.
func WithStdin(stdin io.Reader) Option {
return func(c *Command) {
c.stdin = stdin
}
}

stderr := log.WithFields(log.Fields{
"id": id,
}).WriterLevel(log.DebugLevel)
defer stderr.Close()
// WithStdout sets the writer that the command's stdout
// should be written to.
func WithStdout(stdout io.Writer) Option {
return func(c *Command) {
c.stdout = stdout
}
}

cmd.Stdout = stdout
cmd.Stderr = stderr
// WithStderr sets the writer that the command's stderr
// should be written to.
func WithStderr(stderr io.Writer) Option {
return func(c *Command) {
c.stderr = stderr
}
}

for _, opt := range opts {
opt(cmd)
// WithEnv sets the environment variables for the process
// the command will be run in.
func WithEnv(env map[string]string) Option {
return func(c *Command) {
c.env = env
}
}

err := cmd.Run()
if err != nil {
argsStr := strings.Join(args, " ")
return errors.Wrapf(err, "Exec failed to run %s %s", cmdName, argsStr)
// WithDir sets the directory the command should be run in.
func WithDir(dir string) Option {
return func(c *Command) {
c.dir = dir
}
}

// Exec executes the named program with the given arguments.
func (c *Command) Exec(name string, args ...string) error {
cmd := exec.Command(name, args...)
if c.stdin != nil {
cmd.Stdin = c.stdin
}
if c.stdout != nil {
cmd.Stdout = c.stdout
}
if c.stderr != nil {
cmd.Stderr = c.stderr
}
if c.env != nil {
for k, v := range c.env {
cmd.Env = append(cmd.Env, k+"="+v)
}
}
if c.dir != "" {
cmd.Dir = c.dir
}

if err := cmd.Run(); err != nil {
argsStr := strings.Join(args, " ")
return fmt.Errorf("command: failed to run '%s %s': %w", name, argsStr, err)
}
return nil
}

// Exec executes the named program with the given arguments.
// This is a shorthand for when the default command options wish to be used.
func Exec(name string, args ...string) error {
return New().Exec(name, args...)
}
Loading

0 comments on commit 4006d4d

Please sign in to comment.