bsh

package module
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: Apr 11, 2023 License: MIT Imports: 13 Imported by: 2

README

bsh

Overview

bsh is a library to provide a more conscise and legible way of writing shell scripts in Go.

The original intention was for use in a Magefile, but there's nothing preventing this from working in other contexts.

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 via io.Reader/Writers.
  • Run commands bash-style pipes by actually invoking bash (works on Windows if you have bash in your path, eg from installing Git for Windows).
  • 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 Bsh commands to not require exlicit error handling.
    • This mimics bash's set -e, where any error results in a panic.
    • The default "panic" behavior can be swapped out dynamically via 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/bsh"
  "github.com/magefile/mage/mg"
)

var sh bsh.Bsh

// BUILD COMMANDS

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

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

  sh.Echof("Building %s...", target)
  sh.MkdirAll("local/")
  sh.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() {
  sh.Echo("Deleting local...")
  sh.RemoveAll("local")
}

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

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

  sh.Chdir("local")
  sh.Echo("Running...")
  sh.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" {
      sh.Warnf(`Unrecognized command "%s"`, cmd)
    }
    sh.Echo("pg start   - Starts the postgres docker container. If the container didn't previously exist, it is created.")
    sh.Echo("pg stop    - Stops the postgres docker container.")
    sh.Echo("pg destroy - Destroys the postgres docker container (including data).")
    sh.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 := sh.Cmd(`docker ps --filter "name=` + PG_CONTAINER_NAME + `" -q -a`).RunStr()
  if len(existingContainer) > 0 {
    sh.Cmd("docker start " + PG_CONTAINER_NAME).OutErr(nil).Run()
  } else {
    if !sh.Exists(PG_PASS_FILE) {
      sh.Warnf(`Please create a file named "%s" that contains the database password.`, PG_PASS_FILE)
      return
    }
    pgpass := pgGetPass()
    sh.PushEchoFilter(pgpass)
    defer sh.PopEchoFilter()
    sh.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()
  }

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

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

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

func pgPsql() {
  pgpass := pgGetPass()
  sh.PushEchoFilter(pgpass)
  defer sh.PopEchoFilter()
  sh.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()
  sh.PushEchoFilter(password)
  defer sh.PopEchoFilter()
  sh.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(sh.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 ExeName

func ExeName(path string) string

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

Types

type Bsh

type Bsh struct {
	Stdin        io.Reader
	Stdout       io.Writer
	Stderr       io.Writer
	DisableColor bool
	// contains filtered or unexported fields
}

func (*Bsh) Append

func (b *Bsh) Append(path string, contents string)

func (*Bsh) AppendBytes

func (b *Bsh) AppendBytes(path string, data []byte)

func (*Bsh) AppendBytesErr

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

func (*Bsh) AppendErr

func (b *Bsh) AppendErr(path string, contents string) error

func (*Bsh) Appendf

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

func (*Bsh) Ask

func (b *Bsh) Ask(msg string) string

func (*Bsh) Askf

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

func (*Bsh) Chdir

func (b *Bsh) Chdir(dir string)

Chdir is os.Chdir, but with errors handled by this instance of Bsh

func (*Bsh) Cmd

func (b *Bsh) Cmd(command string) *Command

func (*Bsh) Cmdf

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

func (*Bsh) Copy added in v0.1.4

func (b *Bsh) Copy(src, dst string) bool

Copy attempts to open file at src and create/overwrite new file at dst, then copy the contents. If src does not exist, Copy returns false, otherwise it returns true. Other errors will panic.

func (*Bsh) Echo

func (b *Bsh) Echo(str string)

func (*Bsh) Echof

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

func (*Bsh) ExeName

func (b *Bsh) ExeName(path string) string

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

func (*Bsh) Exists

func (b *Bsh) Exists(path string) bool

Exists checks if this path already exists on disc (as a file or folder or whatever)

func (*Bsh) Getwd

func (b *Bsh) Getwd() string

Getwd is os.Getwd, but with errors handled by this instance of Bsh

func (*Bsh) InDir added in v0.1.1

func (b *Bsh) InDir(path string, fn func())

InDir saves the cwd, creates the given path (if needed), cds into the given path, executes the given func, then restores the previous cwd.

func (*Bsh) IsDir

func (b *Bsh) IsDir(path string) bool

IsDir checks if this path is a folder (returns false if path doesn't exist, or exists but is a file)

func (*Bsh) IsFile

func (b *Bsh) IsFile(path string) bool

IsFile checks if this path is a file (returns false if path doesn't exist, or exists but is a folder)

func (*Bsh) IsVerbose

func (b *Bsh) IsVerbose() bool

func (*Bsh) MkdirAll

func (b *Bsh) MkdirAll(dir string)

MkdirAll is os.MkdirAll, but with errors handled by this instance of Bsh

func (*Bsh) Must added in v0.1.3

func (b *Bsh) Must(err error)

Must can be used to wrap errors that you want bsh to handle.

func (*Bsh) MustCopy added in v0.1.4

func (b *Bsh) MustCopy(src, dst string)

MustCopy attempts to open file at src and create/overwrite new file at dst, then copy the contents. Any error in this process will panic.

func (*Bsh) Panic

func (b *Bsh) Panic(err error)

Panic is called internally any time there's an unhandled error. It will in turn call any error handler set by SetErrorHandler, or panic() if no error handler was set.

func (*Bsh) PopEchoFilter

func (b *Bsh) PopEchoFilter()

func (*Bsh) PushEchoFilter

func (b *Bsh) PushEchoFilter(str string)

func (*Bsh) Read

func (b *Bsh) Read(path string) string

func (*Bsh) ReadErr

func (b *Bsh) ReadErr(path string) (string, error)

func (*Bsh) ReadFile

func (b *Bsh) ReadFile(path string) []byte

func (*Bsh) Remove

func (b *Bsh) Remove(dir string)

Remove is os.Remove, but with errors handled by this instance of Bsh

func (*Bsh) RemoveAll

func (b *Bsh) RemoveAll(dir string)

RemoveAll is os.RemoveAll, but with errors handled by this instance of Bsh

func (*Bsh) ScanLine

func (b *Bsh) ScanLine() string

func (*Bsh) ScanLineErr

func (b *Bsh) ScanLineErr() (string, error)

func (*Bsh) SetErrorHandler

func (b *Bsh) SetErrorHandler(fnErr func(error))

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

func (*Bsh) SetVerbose

func (b *Bsh) SetVerbose(v bool)

func (*Bsh) SetVerboseEnvVarName

func (b *Bsh) 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 (*Bsh) Stat

func (b *Bsh) Stat(path string) fs.FileInfo

Stat is os.Stat, but with errors handled by this instance of Bsh

func (*Bsh) Verbose

func (b *Bsh) Verbose(str string)

func (*Bsh) Verbosef

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

func (*Bsh) Warn

func (b *Bsh) Warn(str string)

func (*Bsh) Warnf

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

func (*Bsh) Write

func (b *Bsh) Write(path string, contents string)

func (*Bsh) WriteBytes

func (b *Bsh) WriteBytes(path string, data []byte)

func (*Bsh) WriteBytesErr

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

func (*Bsh) WriteErr

func (b *Bsh) WriteErr(path string, contents string) error

func (*Bsh) Writef

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

type Command

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

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) Dir added in v0.1.5

func (c *Command) Dir(dir string) *Command

Dir sets the working directory

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(w 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(r io.Reader) *Command

func (*Command) Out

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

func (*Command) OutErr

func (c *Command) OutErr(w 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

func (*Command) StdErr added in v0.1.2

func (c *Command) StdErr() io.Writer

func (*Command) StdIn added in v0.1.2

func (c *Command) StdIn() io.Reader

func (*Command) StdOut added in v0.1.2

func (c *Command) StdOut() io.Writer

Jump to

Keyboard shortcuts

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