Merge branch 'master' of github.com:GoogleCloudPlatform/k8s-container-builder into expose-cmd
This commit is contained in:
commit
5578e683a1
|
|
@ -161,6 +161,7 @@
|
|||
"builder/dockerfile/command",
|
||||
"builder/dockerfile/instructions",
|
||||
"builder/dockerfile/parser",
|
||||
"builder/dockerfile/shell",
|
||||
"client",
|
||||
"pkg/homedir",
|
||||
"pkg/idtools",
|
||||
|
|
@ -486,6 +487,6 @@
|
|||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "fd21de0404336debb893db778210835a27a3612fe9b9e5e412dcdc80d288a986"
|
||||
inputs-digest = "eadec1feacc8473e54622d5f3a25fbc9c7fb1f9bd38776475c3e2d283bd80d2a"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
FROM gcr.io/google-appengine/debian9
|
||||
ENV hey hey
|
||||
ENV PATH /usr/local
|
||||
ENV hey hello
|
||||
ENV first=foo second=foo2
|
||||
ENV third $second:/third
|
||||
ENV myName="John Doe" myDog=Rex\ The\ Dog \
|
||||
myCat=fluffy
|
||||
ENV PATH something
|
||||
ENV test=value\ value2
|
||||
ENV test1="a'b'c"
|
||||
ENV test2='a"b"c'
|
||||
ENV test3=a\ b name2=b\ c
|
||||
ENV test4="a\"b"
|
||||
ENV test5='a\"b'
|
||||
ENV test6="a\'b"
|
||||
ENV atomic=one
|
||||
ENV atomic=two newatomic=$atomic
|
||||
ENV newenv=$doesntexist/newenv
|
||||
|
|
@ -9,13 +9,13 @@
|
|||
"Mods": [
|
||||
{
|
||||
"Name": "/var/log/dpkg.log",
|
||||
"Size1": 57425,
|
||||
"Size2": 57425
|
||||
"Size1": 57481,
|
||||
"Size2": 57481
|
||||
},
|
||||
{
|
||||
"Name": "/var/log/apt/term.log",
|
||||
"Size1": 24400,
|
||||
"Size2": 24400
|
||||
"Size1": 24421,
|
||||
"Size2": 24421
|
||||
},
|
||||
{
|
||||
"Name": "/var/cache/ldconfig/aux-cache",
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
},
|
||||
{
|
||||
"Name": "/var/log/apt/history.log",
|
||||
"Size1": 5089,
|
||||
"Size2": 5089
|
||||
"Size1": 5415,
|
||||
"Size2": 5415
|
||||
},
|
||||
{
|
||||
"Name": "/var/log/alternatives.log",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
schemaVersion: '2.0.0'
|
||||
metadataTest:
|
||||
env:
|
||||
- key: hey
|
||||
value: hello
|
||||
- key: PATH
|
||||
value: something
|
||||
- key: first
|
||||
value: foo
|
||||
- key: second
|
||||
value: foo2
|
||||
- key: third
|
||||
value: foo2:/third
|
||||
- key: myName
|
||||
value: John Doe
|
||||
- key: myDog
|
||||
value: Rex The Dog
|
||||
- key: myCat
|
||||
value: fluffy
|
||||
- key: test
|
||||
value: value value2
|
||||
- key: test1
|
||||
value: a'b'c
|
||||
- key: test2
|
||||
value: a"b"c
|
||||
- key: test3
|
||||
value: a b
|
||||
- key: name2
|
||||
value: b c
|
||||
- key: test4
|
||||
value: a"b
|
||||
- key: test5
|
||||
value: a\"b
|
||||
- key: test6
|
||||
value: a\'b
|
||||
- key: atomic
|
||||
value: two
|
||||
- key: newatomic
|
||||
value: one
|
||||
- key: newenv
|
||||
value: /newenv
|
||||
|
|
@ -21,7 +21,7 @@ import (
|
|||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var tests = []struct {
|
||||
var fileTests = []struct {
|
||||
description string
|
||||
dockerfilePath string
|
||||
configPath string
|
||||
|
|
@ -51,6 +51,22 @@ var tests = []struct {
|
|||
},
|
||||
}
|
||||
|
||||
var structureTests = []struct {
|
||||
description string
|
||||
dockerfilePath string
|
||||
structureTestYamlPath string
|
||||
dockerBuildContext string
|
||||
repo string
|
||||
}{
|
||||
{
|
||||
description: "test env",
|
||||
dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_env",
|
||||
repo: "test-env",
|
||||
dockerBuildContext: "/workspace/integration_tests/dockerfiles/",
|
||||
structureTestYamlPath: "/workspace/integration_tests/dockerfiles/test_env.yaml",
|
||||
},
|
||||
}
|
||||
|
||||
type step struct {
|
||||
Name string
|
||||
Args []string
|
||||
|
|
@ -82,15 +98,23 @@ func main() {
|
|||
Name: ubuntuImage,
|
||||
Args: []string{"chmod", "+x", "container-diff-linux-amd64"},
|
||||
}
|
||||
structureTestsStep := step{
|
||||
Name: "gcr.io/cloud-builders/gsutil",
|
||||
Args: []string{"cp", "gs://container-structure-test/latest/container-structure-test", "."},
|
||||
}
|
||||
structureTestPermissions := step{
|
||||
Name: ubuntuImage,
|
||||
Args: []string{"chmod", "+x", "container-structure-test"},
|
||||
}
|
||||
// Build executor image
|
||||
buildExecutorImage := step{
|
||||
Name: dockerImage,
|
||||
Args: []string{"build", "-t", executorImage, "-f", "integration_tests/executor/Dockerfile", "."},
|
||||
}
|
||||
y := testyaml{
|
||||
Steps: []step{containerDiffStep, containerDiffPermissions, buildExecutorImage},
|
||||
Steps: []step{containerDiffStep, containerDiffPermissions, structureTestsStep, structureTestPermissions, buildExecutorImage},
|
||||
}
|
||||
for _, test := range tests {
|
||||
for _, test := range fileTests {
|
||||
// First, build the image with docker
|
||||
dockerImageTag := testRepo + dockerPrefix + test.repo
|
||||
dockerBuild := step{
|
||||
|
|
@ -133,6 +157,43 @@ func main() {
|
|||
y.Steps = append(y.Steps, dockerBuild, kbuild, pullKbuildImage, containerDiff, catContainerDiffOutput, compareOutputs)
|
||||
}
|
||||
|
||||
for _, test := range structureTests {
|
||||
|
||||
// First, build the image with docker
|
||||
dockerImageTag := testRepo + dockerPrefix + test.repo
|
||||
dockerBuild := step{
|
||||
Name: dockerImage,
|
||||
Args: []string{"build", "-t", dockerImageTag, "-f", test.dockerfilePath, test.dockerBuildContext},
|
||||
}
|
||||
|
||||
// Build the image with kbuild
|
||||
kbuildImage := testRepo + kbuildPrefix + test.repo
|
||||
kbuild := step{
|
||||
Name: executorImage,
|
||||
Args: []string{executorCommand, "--destination", kbuildImage, "--dockerfile", test.dockerfilePath},
|
||||
}
|
||||
// Pull the kbuild image
|
||||
pullKbuildImage := step{
|
||||
Name: dockerImage,
|
||||
Args: []string{"pull", kbuildImage},
|
||||
}
|
||||
// Run structure tests on the kbuild and docker image
|
||||
args := "container-structure-test -image " + kbuildImage + " " + test.structureTestYamlPath
|
||||
structureTest := step{
|
||||
Name: ubuntuImage,
|
||||
Args: []string{"sh", "-c", args},
|
||||
Env: []string{"PATH=/workspace:/bin"},
|
||||
}
|
||||
args = "container-structure-test -image " + dockerImageTag + " " + test.structureTestYamlPath
|
||||
dockerStructureTest := step{
|
||||
Name: ubuntuImage,
|
||||
Args: []string{"sh", "-c", args},
|
||||
Env: []string{"PATH=/workspace:/bin"},
|
||||
}
|
||||
|
||||
y.Steps = append(y.Steps, dockerBuild, kbuild, pullKbuildImage, structureTest, dockerStructureTest)
|
||||
}
|
||||
|
||||
d, _ := yaml.Marshal(&y)
|
||||
fmt.Println(string(d))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ func GetCommand(cmd instructions.Command) (DockerCommand, error) {
|
|||
return &RunCommand{cmd: c}, nil
|
||||
case *instructions.ExposeCommand:
|
||||
return &ExposeCommand{cmd: c}, nil
|
||||
case *instructions.EnvCommand:
|
||||
return &EnvCommand{cmd: c}, nil
|
||||
}
|
||||
return nil, errors.Errorf("%s is not a supported command", cmd.Name())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/containers/image/manifest"
|
||||
"github.com/docker/docker/builder/dockerfile/instructions"
|
||||
"github.com/docker/docker/builder/dockerfile/parser"
|
||||
"github.com/docker/docker/builder/dockerfile/shell"
|
||||
"github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EnvCommand struct {
|
||||
cmd *instructions.EnvCommand
|
||||
}
|
||||
|
||||
func (e *EnvCommand) ExecuteCommand(config *manifest.Schema2Config) error {
|
||||
logrus.Info("cmd: ENV")
|
||||
// The dockerfile/shell package handles processing env values
|
||||
// It handles escape characters and supports expansion from the config.Env array
|
||||
// Shlex handles some of the following use cases (these and more are tested in integration tests)
|
||||
// ""a'b'c"" -> "a'b'c"
|
||||
// "Rex\ The\ Dog \" -> "Rex The Dog"
|
||||
// "a\"b" -> "a"b"
|
||||
envString := envToString(e.cmd)
|
||||
p, err := parser.Parse(bytes.NewReader([]byte(envString)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shlex := shell.NewLex(p.EscapeToken)
|
||||
newEnvs := e.cmd.Env
|
||||
for index, pair := range newEnvs {
|
||||
expandedValue, err := shlex.ProcessWord(pair.Value, config.Env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newEnvs[index] = instructions.KeyValuePair{
|
||||
Key: pair.Key,
|
||||
Value: expandedValue,
|
||||
}
|
||||
logrus.Infof("Setting environment variable %s=%s", pair.Key, expandedValue)
|
||||
if err := os.Setenv(pair.Key, expandedValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return updateConfigEnv(newEnvs, config)
|
||||
}
|
||||
|
||||
func updateConfigEnv(newEnvs []instructions.KeyValuePair, config *manifest.Schema2Config) error {
|
||||
// First, convert config.Env array to []instruction.KeyValuePair
|
||||
var kvps []instructions.KeyValuePair
|
||||
for _, env := range config.Env {
|
||||
entry := strings.Split(env, "=")
|
||||
kvps = append(kvps, instructions.KeyValuePair{
|
||||
Key: entry[0],
|
||||
Value: entry[1],
|
||||
})
|
||||
}
|
||||
// Iterate through new environment variables, and replace existing keys
|
||||
// We can't use a map because we need to preserve the order of the environment variables
|
||||
Loop:
|
||||
for _, newEnv := range newEnvs {
|
||||
for index, kvp := range kvps {
|
||||
// If key exists, replace the KeyValuePair...
|
||||
if kvp.Key == newEnv.Key {
|
||||
logrus.Debugf("Replacing environment variable %v with %v in config", kvp, newEnv)
|
||||
kvps[index] = newEnv
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
// ... Else, append it as a new env variable
|
||||
kvps = append(kvps, newEnv)
|
||||
}
|
||||
// Convert back to array and set in config
|
||||
envArray := []string{}
|
||||
for _, kvp := range kvps {
|
||||
entry := kvp.Key + "=" + kvp.Value
|
||||
envArray = append(envArray, entry)
|
||||
}
|
||||
config.Env = envArray
|
||||
return nil
|
||||
}
|
||||
|
||||
func envToString(cmd *instructions.EnvCommand) string {
|
||||
env := []string{"ENV"}
|
||||
for _, kvp := range cmd.Env {
|
||||
env = append(env, kvp.Key+"="+kvp.Value)
|
||||
}
|
||||
return strings.Join(env, " ")
|
||||
}
|
||||
|
||||
// We know that no files have changed, so return an empty array
|
||||
func (e *EnvCommand) FilesToSnapshot() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CreatedBy returns some information about the command for the image config history
|
||||
func (e *EnvCommand) CreatedBy() string {
|
||||
envArray := []string{e.cmd.Name()}
|
||||
for _, pair := range e.cmd.Env {
|
||||
envArray = append(envArray, pair.Key+"="+pair.Value)
|
||||
}
|
||||
return strings.Join(envArray, " ")
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/GoogleCloudPlatform/k8s-container-builder/testutil"
|
||||
"github.com/containers/image/manifest"
|
||||
"github.com/docker/docker/builder/dockerfile/instructions"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdateEnvConfig(t *testing.T) {
|
||||
cfg := &manifest.Schema2Config{
|
||||
Env: []string{
|
||||
"PATH=/path/to/dir",
|
||||
"hey=hey",
|
||||
},
|
||||
}
|
||||
|
||||
newEnvs := []instructions.KeyValuePair{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: "foo2",
|
||||
},
|
||||
{
|
||||
Key: "PATH",
|
||||
Value: "/new/path/",
|
||||
},
|
||||
{
|
||||
Key: "foo",
|
||||
Value: "newfoo",
|
||||
},
|
||||
}
|
||||
|
||||
expectedEnvArray := []string{
|
||||
"PATH=/new/path/",
|
||||
"hey=hey",
|
||||
"foo=newfoo",
|
||||
}
|
||||
updateConfigEnv(newEnvs, cfg)
|
||||
testutil.CheckErrorAndDeepEqual(t, false, nil, expectedEnvArray, cfg.Env)
|
||||
}
|
||||
|
||||
func TestEnvToString(t *testing.T) {
|
||||
envCmd := &instructions.EnvCommand{
|
||||
Env: []instructions.KeyValuePair{
|
||||
{
|
||||
Key: "PATH",
|
||||
Value: "/some/path",
|
||||
},
|
||||
{
|
||||
Key: "HOME",
|
||||
Value: "/root",
|
||||
},
|
||||
},
|
||||
}
|
||||
expectedString := "ENV PATH=/some/path HOME=/root"
|
||||
actualString := envToString(envCmd)
|
||||
testutil.CheckErrorAndDeepEqual(t, false, nil, expectedString, actualString)
|
||||
}
|
||||
9
vendor/github.com/docker/docker/builder/dockerfile/shell/equal_env_unix.go
generated
vendored
Normal file
9
vendor/github.com/docker/docker/builder/dockerfile/shell/equal_env_unix.go
generated
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// +build !windows
|
||||
|
||||
package shell // import "github.com/docker/docker/builder/dockerfile/shell"
|
||||
|
||||
// EqualEnvKeys compare two strings and returns true if they are equal. On
|
||||
// Windows this comparison is case insensitive.
|
||||
func EqualEnvKeys(from, to string) bool {
|
||||
return from == to
|
||||
}
|
||||
9
vendor/github.com/docker/docker/builder/dockerfile/shell/equal_env_windows.go
generated
vendored
Normal file
9
vendor/github.com/docker/docker/builder/dockerfile/shell/equal_env_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package shell // import "github.com/docker/docker/builder/dockerfile/shell"
|
||||
|
||||
import "strings"
|
||||
|
||||
// EqualEnvKeys compare two strings and returns true if they are equal. On
|
||||
// Windows this comparison is case insensitive.
|
||||
func EqualEnvKeys(from, to string) bool {
|
||||
return strings.ToUpper(from) == strings.ToUpper(to)
|
||||
}
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
package shell // import "github.com/docker/docker/builder/dockerfile/shell"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/scanner"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Lex performs shell word splitting and variable expansion.
|
||||
//
|
||||
// Lex takes a string and an array of env variables and
|
||||
// process all quotes (" and ') as well as $xxx and ${xxx} env variable
|
||||
// tokens. Tries to mimic bash shell process.
|
||||
// It doesn't support all flavors of ${xx:...} formats but new ones can
|
||||
// be added by adding code to the "special ${} format processing" section
|
||||
type Lex struct {
|
||||
escapeToken rune
|
||||
}
|
||||
|
||||
// NewLex creates a new Lex which uses escapeToken to escape quotes.
|
||||
func NewLex(escapeToken rune) *Lex {
|
||||
return &Lex{escapeToken: escapeToken}
|
||||
}
|
||||
|
||||
// ProcessWord will use the 'env' list of environment variables,
|
||||
// and replace any env var references in 'word'.
|
||||
func (s *Lex) ProcessWord(word string, env []string) (string, error) {
|
||||
word, _, err := s.process(word, env)
|
||||
return word, err
|
||||
}
|
||||
|
||||
// ProcessWords will use the 'env' list of environment variables,
|
||||
// and replace any env var references in 'word' then it will also
|
||||
// return a slice of strings which represents the 'word'
|
||||
// split up based on spaces - taking into account quotes. Note that
|
||||
// this splitting is done **after** the env var substitutions are done.
|
||||
// Note, each one is trimmed to remove leading and trailing spaces (unless
|
||||
// they are quoted", but ProcessWord retains spaces between words.
|
||||
func (s *Lex) ProcessWords(word string, env []string) ([]string, error) {
|
||||
_, words, err := s.process(word, env)
|
||||
return words, err
|
||||
}
|
||||
|
||||
func (s *Lex) process(word string, env []string) (string, []string, error) {
|
||||
sw := &shellWord{
|
||||
envs: env,
|
||||
escapeToken: s.escapeToken,
|
||||
}
|
||||
sw.scanner.Init(strings.NewReader(word))
|
||||
return sw.process(word)
|
||||
}
|
||||
|
||||
type shellWord struct {
|
||||
scanner scanner.Scanner
|
||||
envs []string
|
||||
escapeToken rune
|
||||
}
|
||||
|
||||
func (sw *shellWord) process(source string) (string, []string, error) {
|
||||
word, words, err := sw.processStopOn(scanner.EOF)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "failed to process %q", source)
|
||||
}
|
||||
return word, words, err
|
||||
}
|
||||
|
||||
type wordsStruct struct {
|
||||
word string
|
||||
words []string
|
||||
inWord bool
|
||||
}
|
||||
|
||||
func (w *wordsStruct) addChar(ch rune) {
|
||||
if unicode.IsSpace(ch) && w.inWord {
|
||||
if len(w.word) != 0 {
|
||||
w.words = append(w.words, w.word)
|
||||
w.word = ""
|
||||
w.inWord = false
|
||||
}
|
||||
} else if !unicode.IsSpace(ch) {
|
||||
w.addRawChar(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wordsStruct) addRawChar(ch rune) {
|
||||
w.word += string(ch)
|
||||
w.inWord = true
|
||||
}
|
||||
|
||||
func (w *wordsStruct) addString(str string) {
|
||||
var scan scanner.Scanner
|
||||
scan.Init(strings.NewReader(str))
|
||||
for scan.Peek() != scanner.EOF {
|
||||
w.addChar(scan.Next())
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wordsStruct) addRawString(str string) {
|
||||
w.word += str
|
||||
w.inWord = true
|
||||
}
|
||||
|
||||
func (w *wordsStruct) getWords() []string {
|
||||
if len(w.word) > 0 {
|
||||
w.words = append(w.words, w.word)
|
||||
|
||||
// Just in case we're called again by mistake
|
||||
w.word = ""
|
||||
w.inWord = false
|
||||
}
|
||||
return w.words
|
||||
}
|
||||
|
||||
// Process the word, starting at 'pos', and stop when we get to the
|
||||
// end of the word or the 'stopChar' character
|
||||
func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) {
|
||||
var result bytes.Buffer
|
||||
var words wordsStruct
|
||||
|
||||
var charFuncMapping = map[rune]func() (string, error){
|
||||
'\'': sw.processSingleQuote,
|
||||
'"': sw.processDoubleQuote,
|
||||
'$': sw.processDollar,
|
||||
}
|
||||
|
||||
for sw.scanner.Peek() != scanner.EOF {
|
||||
ch := sw.scanner.Peek()
|
||||
|
||||
if stopChar != scanner.EOF && ch == stopChar {
|
||||
sw.scanner.Next()
|
||||
break
|
||||
}
|
||||
if fn, ok := charFuncMapping[ch]; ok {
|
||||
// Call special processing func for certain chars
|
||||
tmp, err := fn()
|
||||
if err != nil {
|
||||
return "", []string{}, err
|
||||
}
|
||||
result.WriteString(tmp)
|
||||
|
||||
if ch == rune('$') {
|
||||
words.addString(tmp)
|
||||
} else {
|
||||
words.addRawString(tmp)
|
||||
}
|
||||
} else {
|
||||
// Not special, just add it to the result
|
||||
ch = sw.scanner.Next()
|
||||
|
||||
if ch == sw.escapeToken {
|
||||
// '\' (default escape token, but ` allowed) escapes, except end of line
|
||||
ch = sw.scanner.Next()
|
||||
|
||||
if ch == scanner.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
words.addRawChar(ch)
|
||||
} else {
|
||||
words.addChar(ch)
|
||||
}
|
||||
|
||||
result.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
|
||||
return result.String(), words.getWords(), nil
|
||||
}
|
||||
|
||||
func (sw *shellWord) processSingleQuote() (string, error) {
|
||||
// All chars between single quotes are taken as-is
|
||||
// Note, you can't escape '
|
||||
//
|
||||
// From the "sh" man page:
|
||||
// Single Quotes
|
||||
// Enclosing characters in single quotes preserves the literal meaning of
|
||||
// all the characters (except single quotes, making it impossible to put
|
||||
// single-quotes in a single-quoted string).
|
||||
|
||||
var result bytes.Buffer
|
||||
|
||||
sw.scanner.Next()
|
||||
|
||||
for {
|
||||
ch := sw.scanner.Next()
|
||||
switch ch {
|
||||
case scanner.EOF:
|
||||
return "", errors.New("unexpected end of statement while looking for matching single-quote")
|
||||
case '\'':
|
||||
return result.String(), nil
|
||||
}
|
||||
result.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *shellWord) processDoubleQuote() (string, error) {
|
||||
// All chars up to the next " are taken as-is, even ', except any $ chars
|
||||
// But you can escape " with a \ (or ` if escape token set accordingly)
|
||||
//
|
||||
// From the "sh" man page:
|
||||
// Double Quotes
|
||||
// Enclosing characters within double quotes preserves the literal meaning
|
||||
// of all characters except dollarsign ($), backquote (`), and backslash
|
||||
// (\). The backslash inside double quotes is historically weird, and
|
||||
// serves to quote only the following characters:
|
||||
// $ ` " \ <newline>.
|
||||
// Otherwise it remains literal.
|
||||
|
||||
var result bytes.Buffer
|
||||
|
||||
sw.scanner.Next()
|
||||
|
||||
for {
|
||||
switch sw.scanner.Peek() {
|
||||
case scanner.EOF:
|
||||
return "", errors.New("unexpected end of statement while looking for matching double-quote")
|
||||
case '"':
|
||||
sw.scanner.Next()
|
||||
return result.String(), nil
|
||||
case '$':
|
||||
value, err := sw.processDollar()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result.WriteString(value)
|
||||
default:
|
||||
ch := sw.scanner.Next()
|
||||
if ch == sw.escapeToken {
|
||||
switch sw.scanner.Peek() {
|
||||
case scanner.EOF:
|
||||
// Ignore \ at end of word
|
||||
continue
|
||||
case '"', '$', sw.escapeToken:
|
||||
// These chars can be escaped, all other \'s are left as-is
|
||||
// Note: for now don't do anything special with ` chars.
|
||||
// Not sure what to do with them anyway since we're not going
|
||||
// to execute the text in there (not now anyway).
|
||||
ch = sw.scanner.Next()
|
||||
}
|
||||
}
|
||||
result.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *shellWord) processDollar() (string, error) {
|
||||
sw.scanner.Next()
|
||||
|
||||
// $xxx case
|
||||
if sw.scanner.Peek() != '{' {
|
||||
name := sw.processName()
|
||||
if name == "" {
|
||||
return "$", nil
|
||||
}
|
||||
return sw.getEnv(name), nil
|
||||
}
|
||||
|
||||
sw.scanner.Next()
|
||||
name := sw.processName()
|
||||
ch := sw.scanner.Peek()
|
||||
if ch == '}' {
|
||||
// Normal ${xx} case
|
||||
sw.scanner.Next()
|
||||
return sw.getEnv(name), nil
|
||||
}
|
||||
if ch == ':' {
|
||||
// Special ${xx:...} format processing
|
||||
// Yes it allows for recursive $'s in the ... spot
|
||||
|
||||
sw.scanner.Next() // skip over :
|
||||
modifier := sw.scanner.Next()
|
||||
|
||||
word, _, err := sw.processStopOn('}')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Grab the current value of the variable in question so we
|
||||
// can use to to determine what to do based on the modifier
|
||||
newValue := sw.getEnv(name)
|
||||
|
||||
switch modifier {
|
||||
case '+':
|
||||
if newValue != "" {
|
||||
newValue = word
|
||||
}
|
||||
return newValue, nil
|
||||
|
||||
case '-':
|
||||
if newValue == "" {
|
||||
newValue = word
|
||||
}
|
||||
return newValue, nil
|
||||
|
||||
default:
|
||||
return "", errors.Errorf("unsupported modifier (%c) in substitution", modifier)
|
||||
}
|
||||
}
|
||||
return "", errors.Errorf("missing ':' in substitution")
|
||||
}
|
||||
|
||||
func (sw *shellWord) processName() string {
|
||||
// Read in a name (alphanumeric or _)
|
||||
// If it starts with a numeric then just return $#
|
||||
var name bytes.Buffer
|
||||
|
||||
for sw.scanner.Peek() != scanner.EOF {
|
||||
ch := sw.scanner.Peek()
|
||||
if name.Len() == 0 && unicode.IsDigit(ch) {
|
||||
ch = sw.scanner.Next()
|
||||
return string(ch)
|
||||
}
|
||||
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
|
||||
break
|
||||
}
|
||||
ch = sw.scanner.Next()
|
||||
name.WriteRune(ch)
|
||||
}
|
||||
|
||||
return name.String()
|
||||
}
|
||||
|
||||
func (sw *shellWord) getEnv(name string) string {
|
||||
for _, env := range sw.envs {
|
||||
i := strings.Index(env, "=")
|
||||
if i < 0 {
|
||||
if EqualEnvKeys(name, env) {
|
||||
// Should probably never get here, but just in case treat
|
||||
// it like "var" and "var=" are the same
|
||||
return ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
compareName := env[:i]
|
||||
if !EqualEnvKeys(name, compareName) {
|
||||
continue
|
||||
}
|
||||
return env[i+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Loading…
Reference in New Issue