diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a02f949..0c5cb3d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ IMPROVEMENTS: * core: User variables can now be used for integer, boolean, etc. values. [GH-418] * builder/amazon/all: Interrupts work while waiting for AMI to be ready. +* provisioner/shell: Script line-endings are automatically converted to + Unix-style line-endings. Can be disabled by setting "binary" to "true". + [GH-277] BUG FIXES: diff --git a/provisioner/shell/provisioner.go b/provisioner/shell/provisioner.go index e8a5f61a5..3cb75187d 100644 --- a/provisioner/shell/provisioner.go +++ b/provisioner/shell/provisioner.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/packer" + "io" "io/ioutil" "log" "os" @@ -20,6 +21,10 @@ const DefaultRemotePath = "/tmp/script.sh" type config struct { common.PackerConfig `mapstructure:",squash"` + // If true, the script contains binary and line endings will not be + // converted from Windows to Unix-style. + Binary bool + // An inline script to execute. Multiple strings are all executed // in the context of a single shell. Inline []string @@ -259,6 +264,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } + var r io.Reader = f + if !p.config.Binary { + r = &UnixReader{Reader: r} + } + if err := comm.Upload(p.config.RemotePath, f); err != nil { return fmt.Errorf("Error uploading script: %s", err) } diff --git a/provisioner/shell/unix_reader.go b/provisioner/shell/unix_reader.go new file mode 100644 index 000000000..5745dd291 --- /dev/null +++ b/provisioner/shell/unix_reader.go @@ -0,0 +1,88 @@ +package shell + +import ( + "bufio" + "bytes" + "io" + "sync" +) + +// UnixReader is a Reader implementation that automatically converts +// Windows line endings to Unix line endings. +type UnixReader struct { + Reader io.Reader + + buf []byte + once sync.Once + scanner *bufio.Scanner +} + +func (r *UnixReader) Read(p []byte) (n int, err error) { + // Create the buffered reader once + r.once.Do(func() { + r.scanner = bufio.NewScanner(r.Reader) + r.scanner.Split(scanUnixLine) + }) + + // If we have no data in our buffer, scan to the next token + if len(r.buf) == 0 { + if !r.scanner.Scan() { + err = r.scanner.Err() + if err == nil { + err = io.EOF + } + + return 0, err + } + + r.buf = r.scanner.Bytes() + } + + // Write out as much data as we can to the buffer, storing the rest + // for the next read. + n = len(p) + if n > len(r.buf) { + n = len(r.buf) + } + copy(p, r.buf) + r.buf = r.buf[n:] + + return +} + +// scanUnixLine is a bufio.Scanner SplitFunc. It tokenizes on lines, but +// only returns unix-style lines. So even if the line is "one\r\n", the +// token returned will be "one\n". +func scanUnixLine(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + if i := bytes.IndexByte(data, '\n'); i >= 0 { + // We have a new-line terminated line. Return the line with the newline + return i + 1, dropCR(data[0 : i+1]), nil + } + + if atEOF { + // We have a final, non-terminated line + return len(data), dropCR(data), nil + } + + if data[len(data)-1] != '\r' { + // We have a normal line, just let it tokenize + return len(data), data, nil + } + + // We need more data + return 0, nil, nil +} + +func dropCR(data []byte) []byte { + if len(data) > 0 && data[len(data)-2] == '\r' { + // Trim off the last byte and replace it with a '\n' + data = data[0 : len(data)-1] + data[len(data)-1] = '\n' + } + + return data +} diff --git a/provisioner/shell/unix_reader_test.go b/provisioner/shell/unix_reader_test.go new file mode 100644 index 000000000..8dedf6300 --- /dev/null +++ b/provisioner/shell/unix_reader_test.go @@ -0,0 +1,33 @@ +package shell + +import ( + "bytes" + "io" + "testing" +) + +func TestUnixReader_impl(t *testing.T) { + var raw interface{} + raw = new(UnixReader) + if _, ok := raw.(io.Reader); !ok { + t.Fatal("should be reader") + } +} + +func TestUnixReader(t *testing.T) { + input := "one\r\ntwo\nthree\r\n" + expected := "one\ntwo\nthree\n" + + r := &UnixReader{ + Reader: bytes.NewReader([]byte(input)), + } + + result := new(bytes.Buffer) + if _, err := io.Copy(result, r); err != nil { + t.Fatalf("err: %s", err) + } + + if result.String() != expected { + t.Fatalf("bad: %#v", result.String()) + } +} diff --git a/website/source/docs/provisioners/shell.html.markdown b/website/source/docs/provisioners/shell.html.markdown index d3a895b3a..4c5a19992 100644 --- a/website/source/docs/provisioners/shell.html.markdown +++ b/website/source/docs/provisioners/shell.html.markdown @@ -47,6 +47,10 @@ Exactly _one_ of the following is required: Optional parameters: +* `binary` (boolean) - If true, specifies that the script(s) are binary + files, and Packer should therefore not convert Windows line endings to + Unix line endings (if there are any). By default this is false. + * `environment_vars` (array of strings) - An array of key/value pairs to inject prior to the execute_command. The format should be `key=value`. Packer injects some environmental variables by default