basher

package module
Version: v5.1.2+incompatible Latest Latest
Warning

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

Go to latest
Published: Jan 22, 2021 License: MIT Imports: 16 Imported by: 29

README

go-basher

A Go library for creating Bash environments, exporting Go functions in them as Bash functions, and running commands in that Bash environment. Combined with a tool like go-bindata, you can write programs that are part written in Go and part written in Bash that can be distributed as standalone binaries.

Circle CI GoDoc

Using go-basher

Here we have a simple Go program that defines a reverse function, creates a Bash environment sourcing main.bash and then runs main in that environment.

package main

import (
	"os"
	"io/ioutil"
	"log"
	"strings"

	"github.com/progrium/go-basher"
)

func reverse(args []string) {
	bytes, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		log.Fatal(err)
	}
	runes := []rune(strings.Trim(string(bytes), "\n"))
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	println(string(runes))
}

func main() {
	bash, _ := basher.NewContext("/bin/bash", false)
	bash.ExportFunc("reverse", reverse)
	if bash.HandleFuncs(os.Args) {
		os.Exit(0)
	}

	bash.Source("main.bash", nil)
	status, err := bash.Run("main", os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
	os.Exit(status)
}

Here is our main.bash file, the actual heart of the program:

main() {
	echo "Hello world" | reverse
}

Using go-basher with go-bindata

You can bundle your Bash scripts into your Go binary using go-bindata. First install go-bindata:

$ go get github.com/jteeuwen/go-bindata/...

Now put all your Bash scripts in a directory called bash. The above example program would mean you'd have a bash/main.bash file. Run go-bindata on the directory:

$ go-bindata bash

This will produce a bindata.go file that includes all of your Bash scripts.

bindata.go includes a function called Asset that behaves like ioutil.ReadFile for files in your bindata.go.

Here's how you embed it into the above example program:

  • copy/paste it's import-statements and functions to your application code
  • method A: change bash.Source("bash/main.bash", nil) into bash.Source("bash/main.bash, Asset)
  • method B: replace all code in the main()-function with the Application()-helper function (see below)
	basher.Application(
		map[string]func([]string){
			"reverse":      reverse,
		}, []string{
			"bash/main.bash",
		},
		Asset,
		true,
	)

Batteries included, but replaceable

Did you already hear that term? Sometimes Bash binary is missing, for example when using alpine linux or busybox. Or sometimes its not the correct version. Like OSX ships with Bash 3.x which misses a lot of usefull features. Or you want to make sure to avoid shellshock attack.

For those reasons static versions of Bash binaries are included for linux and darwin. Statically linked bash binaries are released at: https://github.com/robxu9/bash-static. These are then turned into go code, with go-bindata: bindata_linux.go and bindata_darwin.go.

When you use the basher.Application() function, the built in Bash binary will be extracted into the ~/.basher/ dir.

When you use the basher.ApplicationWithPath() function, you will need to specify a bash path with the same setup guarantees as basher.Application().

When you use the basher.NewContext() function, you have to specify the path to Bash and will have complete freedom to modify the context at will.

Motivation

Go is a great compiled systems language, but it can still be faster to write and glue existing commands together in Bash. However, there are operations you wouldn't want to do in Bash that are straightforward in Go, for example, writing and reading structured data formats. By allowing them to work together, you can use each where they are strongest.

Take a common task like making an HTTP request for JSON data. Parsing JSON is easy in Go, but without depending on a tool like jq it is not even worth trying in Bash. And some formats like YAML don't even have a good jq equivalent. Whereas making an HTTP request in Go in the simplest case is going to be 6+ lines, as opposed to Bash where you can use curl in one line. If we write our JSON parser in Go and fetch the HTTP doc with curl, we can express printing a field from a remote JSON object in one line:

curl -s https://api.github.com/users/progrium | parse-user-field email

In this case, the command parse-user-field is an app specific function defined in your Go program.

Why would this ever be worth it? I can think of several basic cases:

  1. you're writing a program in Bash that involves some complex functionality that should be in Go
  2. you're writing a CLI tool in Go but, to start, prototyping would be quicker in Bash
  3. you're writing a program in Bash and want it to be easier to distribute, like a Go binary

License

BSD

Documentation

Overview

Package basher provides an API for running and integrating with Bash from Go

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Application

func Application(
	funcs map[string]func([]string),
	scripts []string,
	loader func(string) ([]byte, error),
	copyEnv bool)

Application sets up a common entrypoint for a Bash application that uses exported Go functions. It uses the DEBUG environment variable to set debug on the Context, and SHELL for the Bash binary if it includes the string "bash". You can pass a loader function to use for the sourced files, and a boolean for whether or not the environment should be copied into the Context process.

func ApplicationWithPath

func ApplicationWithPath(
	funcs map[string]func([]string),
	scripts []string,
	loader func(string) ([]byte, error),
	copyEnv bool,
	bashPath string)

ApplicationWithPath functions as Application does while also allowing the developer to modify the specified bashPath.

func Asset

func Asset(name string) ([]byte, error)

Asset loads and returns the asset for the given name. It returns an error if the asset could not be found or could not be loaded.

func AssetDir

func AssetDir(name string) ([]string, error)

AssetDir returns the file names below a certain directory embedded in the file by go-bindata. For example if you run go-bindata on data/... and data contains the following hierarchy:

data/
  foo.txt
  img/
    a.png
    b.png

then AssetDir("data") would return []string{"foo.txt", "img"} AssetDir("data/img") would return []string{"a.png", "b.png"} AssetDir("foo.txt") and AssetDir("notexist") would return an error AssetDir("") will return []string{"data"}.

func AssetInfo

func AssetInfo(name string) (os.FileInfo, error)

AssetInfo loads and returns the asset info for the given name. It returns an error if the asset could not be found or could not be loaded.

func AssetNames

func AssetNames() []string

AssetNames returns the names of the assets.

func MustAsset

func MustAsset(name string) []byte

MustAsset is like Asset but panics when Asset would return an error. It simplifies safe initialization of global variables.

func RestoreAsset

func RestoreAsset(dir, name string) error

RestoreAsset restores an asset under the given directory

func RestoreAssets

func RestoreAssets(dir, name string) error

RestoreAssets restores an asset under the given directory recursively

Types

type Context

type Context struct {
	sync.Mutex

	// Debug simply leaves the generated BASH_ENV file produced
	// from each Run call of this Context for debugging.
	Debug bool

	// BashPath is the path to the Bash executable to be used by Run
	BashPath string

	// SelfPath is set by NewContext to be the current executable path.
	// It's used to call back into the calling Go process to run exported
	// functions.
	SelfPath string

	// The io.Reader given to Bash for STDIN
	Stdin io.Reader

	// The io.Writer given to Bash for STDOUT
	Stdout io.Writer

	// The io.Writer given to Bash for STDERR
	Stderr io.Writer
	// contains filtered or unexported fields
}

A Context is an instance of a Bash interpreter and environment, including sourced scripts, environment variables, and embedded Go functions

func NewContext

func NewContext(bashpath string, debug bool) (*Context, error)

Creates and initializes a new Context that will use the given Bash executable. The debug mode will leave the produced temporary BASH_ENV file for inspection.

func (*Context) CopyEnv

func (c *Context) CopyEnv()

Copies the current environment variables into the Context

func (*Context) Export

func (c *Context) Export(name string, value string)

Export adds an environment variable to the Context

func (*Context) ExportFunc

func (c *Context) ExportFunc(name string, fn func([]string))

Registers a function with the Context that will produce a Bash function in the environment that calls back into your executable triggering the function defined as fn.

func (*Context) HandleFuncs

func (c *Context) HandleFuncs(args []string) bool

Expects your os.Args to parse and handle any callbacks to Go functions registered with ExportFunc. You normally call this at the beginning of your program. If a registered function is found and handled, HandleFuncs will exit with the appropriate exit code for you.

func (*Context) Run

func (c *Context) Run(command string, args []string) (int, error)

Runs a command in Bash from this Context. With each call, a temporary file is generated used as BASH_ENV when calling Bash that includes all variables, sourced scripts, and exported functions from the Context. Standard I/O by default is attached to the calling process I/O. You can change this by setting the Stdout, Stderr, Stdin variables of the Context.

func (*Context) Source

func (c *Context) Source(filepath string, loader func(string) ([]byte, error)) error

Source adds a shell script to the Context environment. The loader argument can be nil which means it will use ioutil.Readfile and load from disk, but it exists so you can use the Asset function produced by go-bindata when including script files in your Go binary. Calls to Source adds files to the environment in order.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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