func makeCommandString(cmd string, args ...string) string {
	quotedArgs := make([]string, len(args))
	for i, arg := range args {
		if strings.ContainsAny(arg, "<#'\" \f\n\r\t\v\\") {
			arg = strconv.Quote(arg)
		}
		quotedArgs[i] = arg
	}
	return fmt.Sprintf("%s %s", cmd, strings.Join(quotedArgs, " "))
}
makeCommandString returns a printable form of the command and arguments with
arguments quoted if necessary.
package build
import (
	"crypto/tls"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"github.com/docker/distribution/reference"
	"github.com/jlhawn/dockramp/build/commands"
	"github.com/jlhawn/dockramp/build/parser"
	"github.com/samalba/dockerclient"
)
type config struct {
	Cmd          []string
	Entrypoint   []string
	Env          []string
	ExposedPorts map[string]struct{}
	Labels       map[string]string
	User         string
	Volumes      map[string]struct{}
	WorkingDir   string
}
func (c *config) toDocker() *dockerclient.ContainerConfig {
	return &dockerclient.ContainerConfig{
		User:         c.User,
		ExposedPorts: c.ExposedPorts,
		Env:          c.Env,
		Cmd:          c.Cmd,
		Volumes:      c.Volumes,
		WorkingDir:   c.WorkingDir,
		Entrypoint:   c.Entrypoint,
		Labels:       c.Labels,
	}
}
type handlerFunc func(args []string, heredoc string) error
type Builder struct {
	daemonURL        string
	tlsConfig        *tls.Config
	client           *dockerclient.DockerClient
	contextDirectory string
	dockerfilePath   string
	ref              reference.Reference
	out io.Writer
	config              *config
	maintainer          string
	imageID             string
	containerID         string
	uncommitted         bool
	uncommittedCommands []string
	cache map[string]string
	handlers map[string]handlerFunc
}
func NewBuilder(daemonURL string, tlsConfig *tls.Config, contextDirectory, dockerfilePath, repoTag string) (*Builder, error) {
	stat, err := os.Stat(contextDirectory)
	if err != nil {
		return nil, fmt.Errorf("unable to access build context directory: %s", err)
	}
	if !stat.IsDir() {
		return nil, fmt.Errorf("context must be a directory")
	}
	if dockerfilePath == "" {
		dockerfilePath = filepath.Join(contextDirectory, "Dockerfile")
	}
	if _, err := os.Stat(dockerfilePath); err != nil {
		return nil, fmt.Errorf("unable to access build file: %s", err)
	}
	ref, err := reference.Parse(repoTag)
	if err != nil {
		if err != reference.ErrNameEmpty {
			return nil, fmt.Errorf("invalid tag: %s", err)
		}
	}
	client, err := dockerclient.NewDockerClient(daemonURL, tlsConfig)
	if err != nil {
		return nil, fmt.Errorf("unable to initialize client: %s", err)
	}
	b := &Builder{
		daemonURL:        daemonURL,
		tlsConfig:        tlsConfig,
		client:           client,
		contextDirectory: contextDirectory,
		dockerfilePath:   dockerfilePath,
		ref:              ref,
		out:              os.Stdout,
		config: &config{
			Labels:       map[string]string{},
			ExposedPorts: map[string]struct{}{},
			Volumes:      map[string]struct{}{},
		},
	}
	b.handlers = map[string]handlerFunc{
		commands.Cmd:        b.handleCmd,
		commands.Copy:       b.handleCopy,
		commands.Entrypoint: b.handleEntrypoint,
		commands.Env:        b.handleEnv,
		commands.Expose:     b.handleExpose,
		commands.Extract:    b.handleExtract,
		commands.From:       b.handleFrom,
		commands.Label:      b.handleLabel,
		commands.Maintainer: b.handleMaintainer,
		commands.Run:        b.handleRun,
		commands.User:       b.handleUser,
		commands.Volume:     b.handleVolume,
		commands.Workdir:    b.handleWorkdir,
		commands.Add:     b.handleAdd,
		commands.Onbuild: b.handleOnbuild,
	}
	if err := b.loadCache(); err != nil {
		return nil, fmt.Errorf("unable to load build cache: %s", err)
	}
	return b, nil
}
func (b *Builder) Run() error {
	dockerfile, err := os.Open(b.dockerfilePath)
	if err != nil {
		return fmt.Errorf("unable to open Dockerfile: %s", err)
	}
	defer dockerfile.Close()
	commands, err := parser.Parse(dockerfile)
	if err != nil {
		return fmt.Errorf("unable to parse Dockerfile: %s", err)
	}
	if len(commands) == 0 {
		return fmt.Errorf("no commands found in Dockerfile")
	}
	for i, command := range commands {
		if err := b.dispatch(i, command); err != nil {
			return err
		}
	}
	if b.uncommitted && !b.probeCache() {
		b.containerID, err = b.createContainer([]string{"/bin/sh", "-c"}, []string{"#(nop)"}, false)
		if err != nil {
			return fmt.Errorf("unable to create container: %s", err)
		}
		if err := b.commit(); err != nil {
			return fmt.Errorf("unable to commit container image: %s", err)
		}
	}
	imageName := b.imageID
	if named, hasName := b.ref.(reference.Named); hasName {
		imageName = b.ref.String()
		var tag string
		if tagged, isTagged := named.(reference.Tagged); isTagged {
			tag = tagged.Tag()
		}
		if err := b.setTag(b.imageID, named.Name(), tag); err != nil {
			return fmt.Errorf("unable to tag built image: %s", err)
		}
	}
	fmt.Fprintf(b.out, "Successfully built %s\n", imageName)
	return nil
}
func (b *Builder) ImageID() string {
	return b.imageID
}
func (b *Builder) dispatch(stepNum int, command *parser.Command) error {
	cmd, args := strings.ToUpper(command.Args[0]), command.Args[1:]
	if (stepNum == 0) != (cmd == commands.From) {
		return fmt.Errorf("FROM must be the first Dockerfile command")
	}
	handler, exists := b.handlers[cmd]
	if !exists {
		return fmt.Errorf("unknown command: %q", cmd)
	}
	if _, ok := commands.ReplaceEnvAllowed[cmd]; ok {
		for i, arg := range args {
			arg, err := processShellWord(arg, b.config.Env)
			if err != nil {
				return err
			}
			args[i] = arg
		}
	}
	commandStr := makeCommandString(cmd, args...)
	fmt.Fprintf(b.out, "Step %d: %s\n", stepNum, commandStr)
	b.uncommitted = true
	b.uncommittedCommands = append(b.uncommittedCommands, commandStr)
	if err := handler(args, command.Heredoc); err != nil {
		return err
	}
	if _, needCommit := commands.FilesystemModifierCommands[cmd]; needCommit && b.uncommitted {
		if err := b.commit(); err != nil {
			return fmt.Errorf("unable to commit container image: %s", err)
		}
	}
	return nil
}