bs

package module
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2021 License: MIT Imports: 13 Imported by: 0

README

bs

Overview

bs is a package to streamline building bash-like scripts.

The original intended use was inside a Magefile, but there's nothing stopping you from using this in a regular Go app.

Features include:

  • Echo/Warn/Verbose for outputting lines
    • Verbose defaults to only printing if MAGEFILE_VERBOSE is set to true (mirroring how Mage's mg.Verbose() works), but the specific env var that is checked can be changed by calling SetVerboseEnvVarName.
  • ANSI color support (which respects NO_COLOR env var)
  • Protect secrets from being visible on-screen or in logs via PushEchoFilter/PopEchoFilter
  • Read files to strings (or []byte)
  • Write or Append strings (or []byte) to files
  • Run commands via bash-like parsing of arguments, with support for redirecting stdin/out/err
  • Command variants that can return stdout as string, exit code as int, or Go error
  • Common File/Folder helpers, like Exists, IsFile, IsDir, Getwd, Chdir, Mkdir, MkdirAll, Remove, RemoveAll, etc
  • Global error handler that allows most bs commands to not require exlicit error handling
    • This mimics bash's set -e, where any error results in a panic
    • The default "panic" behavior is overridable via a call to SetErrorHandler.

Example

I've got an example magefile below, but first, here's what the output looks like:

example terminal output

And then here's that same output, but with mage's "verbose" flag set:

example terminal output with verbose output

And here's the corresponding magefile.go:

// +build mage

package main

import (
  "fmt"
  "regexp"
  "strings"

  "github.com/danbrakeley/bs"
  "github.com/magefile/mage/mg"
)

// BUILD COMMANDS

// Builds ardentd into the "local" folder.
func Build() {
  target := bs.ExeName("ardentd")

  bs.Echo("Running tests...")
  bs.Cmd("go test ./...").Run()

  bs.Echof("Building %s...", target)
  bs.MkdirAll("local/")
  bs.Cmdf("go build -o local/%s ./cmd/ardentd", target).Run()
}

// Removes all artifacts from previous builds.
// At the moment, this is accomplished by deleting the "local" folder.
func Clean() {
  bs.Echo("Deleting local...")
  bs.RemoveAll("local")
}

// Runs ardentd.
func Run() {
  mg.Deps(Build)

  target := bs.ExeName("ardentd")
  password := pgGetPass()
  bs.PushEchoFilter(password)
  defer bs.PopEchoFilter()
  pgURL := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s?sslmode=disable", PG_USERNAME, password, PG_PORT, PG_DBNAME)

  bs.Chdir("local")
  bs.Echo("Running...")
  bs.Cmdf("%s", target).Env(
    "ARDENT_HOST=127.0.0.1:8080",
    "ARDENT_PGURL="+pgURL,
    "ARDENT_VERBOSE=true",
  ).Run()
}

// POSTGRES COMMANDS

// Passes command to Postgres: help, start, stop, destroy
func PG(cmd string) {
  cmd = strings.ToLower(cmd)
  switch cmd {
  case "start":
    pgStart()
  case "stop":
    pgStop()
  case "destroy":
    pgDestroy()
  case "psql":
    pgPsql()
  default:
    if cmd != "help" {
      bs.Warnf(`Unrecognized command "%s"`, cmd)
    }
    bs.Echo("pg start   - Starts the postgres docker container. If the container didn't previously exist, it is created.")
    bs.Echo("pg stop    - Stops the postgres docker container.")
    bs.Echo("pg destroy - Destroys the postgres docker container (including data).")
    bs.Echo("pg psql    - Starts psql interactive shell against running postgres db.")
  }
}

const (
  PG_DOCKER_IMAGE   = "postgres:13-alpine"
  PG_CONTAINER_NAME = "psql-ardent"
  PG_DBNAME         = "ardent"
  PG_USERNAME       = "super"
  PG_PASS_FILE      = "pg.pass.local"
  PG_PORT           = "5444"
)

func pgStart() {
  existingContainer := bs.Cmd(`docker ps --filter "name=` + PG_CONTAINER_NAME + `" -q -a`).RunStr()
  if len(existingContainer) > 0 {
    bs.Cmd("docker start " + PG_CONTAINER_NAME).OutErr(nil).Run()
  } else {
    if !bs.Exists(PG_PASS_FILE) {
      bs.Warnf(`Please create a file named "%s" that contains the database password.`, PG_PASS_FILE)
      return
    }
    pgpass := pgGetPass()
    bs.PushEchoFilter(pgpass)
    defer bs.PopEchoFilter()
    bs.Cmd(
      "docker run --name " + PG_CONTAINER_NAME +
        " -e POSTGRES_PASSWORD=" + pgpass +
        " -e POSTGRES_USER=" + PG_USERNAME +
        " -e POSTGRES_DB=" + PG_DBNAME +
        " --publish " + PG_PORT + ":5432" +
        " --detach " + PG_DOCKER_IMAGE,
    ).Run()
  }

  bs.Cmd(`docker ps --filter "name=` + PG_CONTAINER_NAME + `" -a`).Run()
}

func pgStop() {
  bs.Cmd("docker stop " + PG_CONTAINER_NAME).OutErr(nil).Run()
  bs.Cmd(`docker ps --filter "name=` + PG_CONTAINER_NAME + `" -a`).Run()
}

func pgDestroy() {
  deletePhrase := bs.Ask(`This will delete all data. To continue, type "i've been warned" (without quotes): `)
  if deletePhrase == "i've been warned" {
    bs.Cmd("docker stop " + PG_CONTAINER_NAME).OutErr(nil).Run()
    bs.Cmd("docker rm " + PG_CONTAINER_NAME).OutErr(nil).Run()
  } else {
    bs.Warnf(`You typed "%s", which was not what was asked for, so nothing was deleted.`, deletePhrase)
  }
}

func pgPsql() {
  pgpass := pgGetPass()
  bs.PushEchoFilter(pgpass)
  defer bs.PopEchoFilter()
  bs.Cmdf(
    `docker exec -it --env PGPASSWORD=%s %s psql -h localhost -d %s -U %s`,
    pgpass, PG_CONTAINER_NAME, PG_DBNAME, PG_USERNAME,
  ).Run()
}

// GOOSE COMMANDS

// Calls "goose {cmd}", where {cmd} is one of: status, up, up-by-one, down, redo, reset, or version
func Goose(cmd string) {
  password := pgGetPass()
  bs.PushEchoFilter(password)
  defer bs.PopEchoFilter()
  bs.Cmdf("goose -dir sql %s", cmd).Env("GOOSE_DRIVER=postgres", "GOOSE_DBSTRING="+pgDSN(password)).Run()
}

// helpers

var regexpPassword = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)

func pgGetPass() string {
  str := strings.TrimSpace(strings.Split(bs.Read(PG_PASS_FILE), "\n")[0])
  if !regexpPassword.MatchString(str) {
    panic(fmt.Errorf(`Password is only allowed alphanumerics and underscores. Please change "%s" by hand to fix.`, PG_PASS_FILE))
  }
  return str
}

func pgDSN(password string) string {
  return fmt.Sprintf(
    "host=localhost port=%s user=%s password=%s dbname=%s sslmode=disable",
    PG_PORT, PG_USERNAME, password, PG_DBNAME,
  )
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Append

func Append(path string, contents string)

func AppendBytes

func AppendBytes(path string, b []byte)

func AppendBytesErr

func AppendBytesErr(path string, b []byte) error

func AppendErr

func AppendErr(path string, contents string) error

func Appendf

func Appendf(path string, format string, args ...interface{})

func Ask

func Ask(msg string) string

func Askf

func Askf(format string, args ...interface{}) string

func Chdir

func Chdir(dir string)

func Echo

func Echo(str string)

func Echof

func Echof(format string, args ...interface{})

func ExeName

func ExeName(path string) string

ExeName adds ".exe" to passed string if GOOS is windows

func Exists

func Exists(path string) bool

func Getwd

func Getwd() string

func IsDir

func IsDir(path string) bool

func IsFile

func IsFile(path string) bool

func IsVerbose added in v0.1.3

func IsVerbose() bool

func MkdirAll

func MkdirAll(dir string)

func PopEchoFilter

func PopEchoFilter()

func PushEchoFilter

func PushEchoFilter(str string)

func Read

func Read(path string) string

func ReadErr

func ReadErr(path string) (string, error)

func ReadFile

func ReadFile(path string) []byte

func Remove

func Remove(dir string)

TODO: if file doesn't exist, don't err

func RemoveAll

func RemoveAll(dir string)

func ScanLine

func ScanLine() string

func ScanLineErr

func ScanLineErr() (string, error)

func SetColorsEnabled added in v0.1.4

func SetColorsEnabled(enabled bool)

func SetErrorHandler

func SetErrorHandler(fnErr func(err error))

SetErrorHandler sets the behavior when an error is encountered while running most commands. The default behavior is to panic.

func SetStderr

func SetStderr(w io.Writer)

SetStderr overrides stderr for Run*/Bash* (defaults to os.Stderr)

func SetStdin

func SetStdin(r io.Reader)

SetStdin overrides stdin for Run*/Bash*/Read* (defaults to os.Stdin)

func SetStdout

func SetStdout(w io.Writer)

SetStdout overrides stdout for Run*/Bash* (defaults to os.Stdout)

func SetVerbose added in v0.1.3

func SetVerbose(b bool)

func SetVerboseEnvVarName added in v0.1.3

func SetVerboseEnvVarName(s string)

SetVerboseEnvVarName allows changing the name of the environment variable that is used to decide if we are in Verbose mode. This function creates the new env var immediately, setting its value to true or false based on the value of the old env var name.

func Stat

func Stat(path string) fs.FileInfo

func Verbose

func Verbose(str string)

func Verbosef

func Verbosef(format string, args ...interface{})

func Warn

func Warn(str string)

func Warnf

func Warnf(format string, args ...interface{})

func Write

func Write(path string, contents string)

func WriteBytes

func WriteBytes(path string, b []byte)

func WriteBytesErr

func WriteBytesErr(path string, b []byte) error

func WriteErr

func WriteErr(path string, contents string) error

func Writef

func Writef(path string, format string, args ...interface{})

Types

type Command

type Command struct {
	// contains filtered or unexported fields
}

func Cmd

func Cmd(command string) *Command

func Cmdf

func Cmdf(format string, args ...interface{}) *Command

func (*Command) Bash

func (c *Command) Bash()

func (*Command) BashErr

func (c *Command) BashErr() error

func (*Command) BashExitStatus

func (c *Command) BashExitStatus() int

func (*Command) BashStr

func (c *Command) BashStr() string

func (*Command) Env

func (c *Command) Env(vars ...string) *Command

Env adds environment variables in the form "KEY=VALUE", to be set on exec.Cmd.Env. Note: these env vars are not seen by ExpandEnv.

func (*Command) Err

func (c *Command) Err(out io.Writer) *Command

func (*Command) ExitStatus

func (c *Command) ExitStatus(n *int) *Command

func (*Command) ExpandEnv

func (c *Command) ExpandEnv() *Command

ExpandEnv calls os.ExpandEnv on the command string before it is parsed and passed to exec.Cmd.

func (*Command) In

func (c *Command) In(in io.Reader) *Command

func (*Command) Out

func (c *Command) Out(out io.Writer) *Command

func (*Command) OutErr

func (c *Command) OutErr(out io.Writer) *Command

func (*Command) Run

func (c *Command) Run()

func (*Command) RunErr

func (c *Command) RunErr() error

func (*Command) RunExitStatus

func (c *Command) RunExitStatus() int

func (*Command) RunStr

func (c *Command) RunStr() string

Jump to

Keyboard shortcuts

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