basher

package module
v0.0.0-...-2575674 Latest Latest
Warning

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

Go to latest
Published: Jun 20, 2018 License: MIT Imports: 11 Imported by: 0

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.

GoDoc

Note on this fork by StudioEtrange

  • Add autofind bash binary already present on system
  • Extend Application helper to use embedded Bash or not
  • Extend Application helper to launch a specific command (instead of a fixed command)
  • Add a new examples with Application helper with or without embedded bash
  • Add a new examples with cross-compilation
  • Remove useless binary bash files from github repository
  • Create a helper script bash_binaries.sh to download and generate a go package with a specific bash static version
  • Switch repo of go-bindata to https://github.com/puppetlabs/go-bindata
  • Improved README.md
  • Improved go-basher itself dev env

Quicktart by examples

  • Example using go-basher core functions and one embedded scripts see example1

  • Example using go-basher Application helper and one embedded scripts see example2

  • Example using go-basher Application helper, two embedded scripts AND embedded bash binary see example3

  • Example using go-basher Application helper, two embedded scripts, embedded bash binary with cross-compilation see example4

Using go-basher core functions

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

package main

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

	"github.com/studioetrange/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("example.bash", nil)
	status, err := bash.Run("main", os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
	os.Exit(status)
}

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

main() {
	echo "Hello world" | reverse
}
go-basher core functions vs Application helper

Instead of manipulating NewContext, ExportFunc/HandleFuncs and Run/Source functions, you can use Application which is a helper built upon theses functions.

Embedded scripts : How to use

You can bundle your Bash scripts into your Go binary using go-bindata, as a data go package

  • For full example using go-basher core functions and embedded scripts see example1
  • For full example using go-basher Application helper and embedded scripts see example2

Embedded scripts : more explanation

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

go get github.com/puppetlabs/go-bindata/...

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

go-bindata bashfiles

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 previous example program :

  • copy/paste it's import-statements and functions to your application code
  • method A : change bash.Source("bash/example.bash", nil) into bash.Source("bash/example.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/example.bash",
		},
		"main"
		Asset,
		nil,
		false,
	)

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 can be embedded for linux and darwin. Statically compiled bash are released on github: 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, you have to use a function generated by go-bindata as loaderBash parameter so that the built in Bash binary will be extracted into the ~/.basher/ dir.

An help script is provided scripts/bash_binaries.sh to download a specific bash version and generate a go package staticbash

  • For full example using go-basher Application helper, embedded scripts AND embedded bash binary see example3

Working on go-basher code source

If you wish to work on go-basher itself, you'll first need Go installed on your machine (version 1.9+ is required).

see https://golang.org/

Install development env

You need to have Go (version 1.9+ is required). You need to have gnu make.

NOTE : When using Makefile, it will take care of GOPATH variable and tree folders organization by creating correct symbolic link. So you can just clone this project anywhere. And not in any particular folder nor any specific GOPATH folder.

Init steps

  • Clone code
cd $HOME
git clone https://github.com/StudioEtrange/go-basher
  • Workdir
cd go-basher
  • Install minimal base and tree folder
export GOPATH=$(pwd)
export PATH=$PATH:$GOPATH/bin
make tree
  • Install some tools
make tools
  • Install dependencies
make deps

Build & dev steps

  • Launch code compilation
make build
  • Launch tests
make test
  • Install
make install

Test examples

make example-all

Or build example N with :

make exampleN

Then launch example N :

exampleN

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),
	sourcedScripts []string,
	command string,
	loaderSourcedScripts func(string) ([]byte, error),
	loaderBash func(string, string) error,
	copyEnv bool)

Application sets up a common entrypoint for a Bash application that funcs : exported Go functions. sourcedScripts : some bash scripts to sourced command : bash command to execute -- arg passed on the command line will be passed to the command loaderSourcedScripts : loader for sourced bash file - if nil, will use ioutil.ReadFile - Use 'Asset' for embedded scripts loaderBash : loader for binary bash file - if nil, will try to autodetect binary (by using BASH_PATH or 'which bash') - Use 'RestoreAsset' for embedded bash copyEnv : import current shell env into context It uses the DEBUG environment variable to set debug on the Context,

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)

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

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
examples

Jump to

Keyboard shortcuts

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