1
0
mirror of https://github.com/go-task/task.git synced 2025-06-15 00:15:10 +02:00

Merge branch 'master' into master

This commit is contained in:
Tobias Salzmann
2018-08-01 10:47:25 +02:00
committed by GitHub
56 changed files with 886 additions and 299 deletions

View File

@ -1,9 +1,6 @@
* Bug reports and feature requests are welcome in [the issues][issues] * Bug reports and feature requests are welcome in [the issues][issues]
* For questions and discussion there's the [Slack room][slack] ([invititation here][slackinvite])
* Pull Requests are welcome. For more complex changes and features it's * Pull Requests are welcome. For more complex changes and features it's
recommended to open an issue with the feature request first recommended to open an issue with the feature request first
* Documentation contributions are as important as code contributions * Documentation contributions are as important as code contributions
[issues]: https://github.com/go-task/task/issues [issues]: https://github.com/go-task/task/issues
[slack]: https://gophers.slack.com/messages/task
[slackinvite]: https://invite.slack.golangbridge.org/

View File

@ -1,7 +1,4 @@
<!-- <!--
For questions and general talk there's the Slack room: https://gophers.slack.com/messages/task
Invite to the Slack is available in this link: https://invite.slack.golangbridge.org/
If relevant, include the following information: If relevant, include the following information:
- Task version - Task version
- OS - OS

83
Gopkg.lock generated
View File

@ -3,129 +3,186 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:f3960e064201714a3507bf96183be246b3d941568af01dc5cff2a388ac4c7515"
name = "github.com/Masterminds/semver" name = "github.com/Masterminds/semver"
packages = ["."] packages = ["."]
revision = "3c560837130448941620d7694991d3ec440aefc0" pruneopts = "NUT"
revision = "4ca3c04fd4fe2a472df0d7121b1b9462f2214c43"
[[projects]] [[projects]]
branch = "master"
digest = "1:43f9a530dfe36fb355b05fbc7a5712f8149a7f6bdfd131bc0ccc634e25c4dd1e"
name = "github.com/Masterminds/sprig" name = "github.com/Masterminds/sprig"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c" revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c"
version = "v2.15.0"
[[projects]] [[projects]]
digest = "1:975108e8d4f5dab096fc991326e96a5716ee8d02e5e7386bb4796171afc4ab9a"
name = "github.com/aokoli/goutils" name = "github.com/aokoli/goutils"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "3391d3790d23d03408670993e957e8f408993c34" revision = "3391d3790d23d03408670993e957e8f408993c34"
version = "v1.0.1" version = "v1.0.1"
[[projects]] [[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew" name = "github.com/davecgh/go-spew"
packages = ["spew"] packages = ["spew"]
pruneopts = "NUT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76" revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0" version = "v1.1.0"
[[projects]] [[projects]]
digest = "1:1bb197a3b5db4e06e00b7560f8e89836c486627f2a0338332ed37daa003d259e"
name = "github.com/google/uuid" name = "github.com/google/uuid"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "064e2069ce9c359c118179501254f67d7d37ba24" revision = "064e2069ce9c359c118179501254f67d7d37ba24"
version = "0.2" version = "0.2"
[[projects]] [[projects]]
digest = "1:f5db19d350bd0b542d17f7e7cf4e7068bac416c08adb6a129b3c6d1db8211051"
name = "github.com/huandu/xstrings" name = "github.com/huandu/xstrings"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1" revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1"
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:65300ccc4bcb38b107b868155c303312978981e56bca707c81efec57575b5e06"
name = "github.com/imdario/mergo" name = "github.com/imdario/mergo"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "9316a62528ac99aaecb4e47eadd6dc8aa6533d58" revision = "9316a62528ac99aaecb4e47eadd6dc8aa6533d58"
version = "v0.3.5" version = "v0.3.5"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:fdf499904ff0a8b5e05a32d98a1e65ddaff6b358640dbd8696ce8bb4e8d2d246"
name = "github.com/mattn/go-zglob" name = "github.com/mattn/go-zglob"
packages = [ packages = [
".", ".",
"fastwalk" "fastwalk",
] ]
revision = "49693fbb3fe3c3a75fc4e4d6fb1d7cedcbdeb385" pruneopts = "NUT"
revision = "c436403c742d0b6d8fc37e69eadf33e024fe74fa"
[[projects]] [[projects]]
branch = "master"
digest = "1:9db29b604bd78452d167abed82386ddd2f93973df3841896fb6ab8aff936f1d6"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "NUT"
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib" name = "github.com/pmezard/go-difflib"
packages = ["difflib"] packages = ["difflib"]
pruneopts = "NUT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2" revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:5089085e1b27a57be4fb7acf32bfa71fb6236dc0e8371165651c9e15285a9ce0"
name = "github.com/radovskyb/watcher" name = "github.com/radovskyb/watcher"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "0d9d32686dbf6395752c9b209398a59e302a7f1e" revision = "0d9d32686dbf6395752c9b209398a59e302a7f1e"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:8c05fbdaac9713aed2a89d171a8b4e98a563f6c84e48352d0bcb10b1a5122488"
name = "github.com/spf13/pflag" name = "github.com/spf13/pflag"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "3ebe029320b2676d667ae88da602a5f854788a8a" revision = "3ebe029320b2676d667ae88da602a5f854788a8a"
[[projects]] [[projects]]
digest = "1:bacb8b590716ab7c33f2277240972c9582d389593ee8d66fc10074e0508b8126"
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
packages = ["assert"] packages = ["assert"]
pruneopts = "NUT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2" version = "v1.2.2"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:17ff32de9f3bf39f76d339ddffcd380140721b112bea96837a86d4dc26c2c633"
name = "golang.org/x/crypto" name = "golang.org/x/crypto"
packages = [ packages = [
"pbkdf2", "pbkdf2",
"scrypt", "scrypt",
"ssh/terminal" "ssh/terminal",
] ]
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602" pruneopts = "NUT"
revision = "a2144134853fc9a27a7b1e3eb4f19f1a76df13c9"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = ["context"] packages = ["context"]
revision = "afe8f62b1d6bbd81f31868121a50b06d8188e1f9" pruneopts = "NUT"
revision = "a680a1efc54dd51c040b3b5ce4939ea3cf2ea0d1"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
name = "golang.org/x/sync" name = "golang.org/x/sync"
packages = ["errgroup"] packages = ["errgroup"]
pruneopts = "NUT"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:64107e3c8f52f341891a565d117bf263ed396fe0c16bad19634b25be25debfaa"
name = "golang.org/x/sys" name = "golang.org/x/sys"
packages = [ packages = [
"unix", "unix",
"windows" "windows",
] ]
revision = "63fc586f45fe72d95d5240a5d5eb95e6503907d3" pruneopts = "NUT"
revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4"
[[projects]] [[projects]]
branch = "v2"
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
name = "gopkg.in/yaml.v2" name = "gopkg.in/yaml.v2"
packages = ["."] packages = ["."]
pruneopts = "NUT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:01be9a02fb8bcae03383a7422d0bd48813fd49834a50be5ef933e430604b3423"
name = "mvdan.cc/sh" name = "mvdan.cc/sh"
packages = [ packages = [
"interp", "interp",
"shell", "shell",
"syntax" "syntax",
] ]
revision = "ca7561fd34910fd8575a3830d3cded291c0ce8b2" pruneopts = "NUT"
revision = "54e5852f101469e5ff9b03902ce0d4ff2ef09809"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "600bd482208fdedec60141bfaffe55eb403df077944bfdf5c007a33132c8ab5a" input-imports = [
"github.com/Masterminds/semver",
"github.com/Masterminds/sprig",
"github.com/mattn/go-zglob",
"github.com/mitchellh/go-homedir",
"github.com/radovskyb/watcher",
"github.com/spf13/pflag",
"github.com/stretchr/testify/assert",
"golang.org/x/sync/errgroup",
"gopkg.in/yaml.v2",
"mvdan.cc/sh/interp",
"mvdan.cc/sh/shell",
"mvdan.cc/sh/syntax",
]
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -7,10 +7,6 @@
branch = "master" branch = "master"
name = "github.com/Masterminds/sprig" name = "github.com/Masterminds/sprig"
[[constraint]]
branch = "master"
name = "github.com/imdario/mergo"
[[constraint]] [[constraint]]
branch = "master" branch = "master"
name = "github.com/mattn/go-zglob" name = "github.com/mattn/go-zglob"

View File

@ -1,4 +1,3 @@
[![Join Slack room](https://img.shields.io/badge/%23task-chat%20room-blue.svg)](https://gophers.slack.com/messages/task)
[![Build Status](https://travis-ci.org/go-task/task.svg?branch=master)](https://travis-ci.org/go-task/task) [![Build Status](https://travis-ci.org/go-task/task.svg?branch=master)](https://travis-ci.org/go-task/task)
# Task - A task runner / simpler Make alternative written in Go # Task - A task runner / simpler Make alternative written in Go
@ -127,7 +126,7 @@ tasks:
### OS specific task ### OS specific task
If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your taskfile If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your Taskfile
based on the operating system. based on the operating system.
Example: Example:
@ -146,13 +145,18 @@ tasks:
Taskfile_linux.yml: Taskfile_linux.yml:
```yml ```yml
version: '2'
tasks: tasks:
build: build:
cmds: cmds:
- echo "linux" - echo "linux"
``` ```
Will print out `linux` and not default. Will print out `linux` and not `default`.
Keep in mind that the version of the files should match. Also, when redefining
a task the whole task is replaced, properties of the task are not merged.
It's also possible to have an OS specific `Taskvars.yml` file, like It's also possible to have an OS specific `Taskvars.yml` file, like
`Taskvars_windows.yml`, `Taskfile_linux.yml`, or `Taskvars_darwin.yml`. See the `Taskvars_windows.yml`, `Taskfile_linux.yml`, or `Taskvars_darwin.yml`. See the
@ -325,7 +329,6 @@ If you prefer this check to be made by the content of the files, instead of
its timestamp, just set the `method` property to `checksum`. its timestamp, just set the `method` property to `checksum`.
You will probably want to ignore the `.task` folder in your `.gitignore` file You will probably want to ignore the `.task` folder in your `.gitignore` file
(It's there that Task stores the last checksum). (It's there that Task stores the last checksum).
This feature is still experimental and can change until it's stable.
```yml ```yml
version: '2' version: '2'

View File

@ -5,24 +5,13 @@ vars:
sh: git log -n 1 --format=%h sh: git log -n 1 --format=%h
GO_PACKAGES: GO_PACKAGES:
. sh: go list ./...
./cmd/task
./internal/args
./internal/compiler
./internal/compiler/v1
./internal/compiler/v2
./internal/execext
./internal/logger
./internal/osext
./internal/output
./internal/status
./internal/taskfile
./internal/taskfile/version
./internal/templater
tasks: tasks:
# compiles current source code and make "task" executable available on default:
# $GOPATH/bin/task{.exe} cmds:
- task: test
install: install:
desc: Installs Task desc: Installs Task
cmds: cmds:
@ -35,8 +24,6 @@ tasks:
cmds: cmds:
- task: go-get - task: go-get
vars: {REPO: github.com/golang/lint/golint} vars: {REPO: github.com/golang/lint/golint}
- task: go-get
vars: {REPO: github.com/asticode/go-astitodo/astitodo}
- task: go-get - task: go-get
vars: {REPO: github.com/golang/dep/cmd/dep} vars: {REPO: github.com/golang/dep/cmd/dep}
- task: go-get - task: go-get
@ -58,14 +45,14 @@ tasks:
lint: lint:
desc: Runs golint desc: Runs golint
cmds: cmds:
- golint {{.GO_PACKAGES}} - golint {{catLines .GO_PACKAGES}}
silent: true silent: true
test: test:
desc: Runs test suite desc: Runs test suite
deps: [install] deps: [install]
cmds: cmds:
- go test {{.GO_PACKAGES}} - go test {{catLines .GO_PACKAGES}}
test-release: test-release:
desc: Tests release process without publishing desc: Tests release process without publishing
@ -77,12 +64,6 @@ tasks:
cmds: cmds:
- godownloader --repo go-task/task -o install-task.sh - godownloader --repo go-task/task -o install-task.sh
todo:
desc: Prints TODO comments present in the code
cmds:
- astitodo {{.GO_PACKAGES}}
silent: true
ci: ci:
cmds: cmds:
- task: go-get - task: go-get
@ -93,3 +74,8 @@ tasks:
go-get: go-get:
cmds: cmds:
- go get -u {{.REPO}} - go get -u {{.REPO}}
packages:
cmds:
- echo '{{.GO_PACKAGES}}'
silent: true

View File

@ -1,22 +0,0 @@
package osext
import (
"os"
"github.com/mitchellh/go-homedir"
)
// Expand is an improved version of os.ExpandEnv,
// that not only expand enrionment variable ($GOPATH/src/github.com/...)
// but also expands "~" as the home directory.
func Expand(s string) (string, error) {
s = os.ExpandEnv(s)
var err error
s, err = homedir.Expand(s)
if err != nil {
return "", err
}
return s, nil
}

View File

@ -4,9 +4,8 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"github.com/go-task/task/internal/osext"
"github.com/mattn/go-zglob" "github.com/mattn/go-zglob"
"mvdan.cc/sh/shell"
) )
func glob(dir string, globs []string) (files []string, err error) { func glob(dir string, globs []string) (files []string, err error) {
@ -14,7 +13,7 @@ func glob(dir string, globs []string) (files []string, err error) {
if !filepath.IsAbs(g) { if !filepath.IsAbs(g) {
g = filepath.Join(dir, g) g = filepath.Join(dir, g)
} }
g, err = osext.Expand(g) g, err = shell.Expand(g, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,27 @@
package taskfile
import (
"fmt"
)
// Merge merges the second Taskfile into the first
func Merge(t1, t2 *Taskfile) error {
if t1.Version != t2.Version {
return fmt.Errorf(`Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
}
if t2.Expansions != 0 && t2.Expansions != 2 {
t1.Expansions = t2.Expansions
}
if t2.Output != "" {
t1.Output = t2.Output
}
for k, v := range t2.Vars {
t1.Vars[k] = v
}
for k, v := range t2.Tasks {
t1.Tasks[k] = v
}
return nil
}

View File

@ -0,0 +1,47 @@
package read
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/go-task/task/internal/taskfile"
"gopkg.in/yaml.v2"
)
// Taskfile reads a Taskfile for a given directory
func Taskfile(dir string) (*taskfile.Taskfile, error) {
path := filepath.Join(dir, "Taskfile.yml")
t, err := readTaskfile(path)
if err != nil {
return nil, err
}
path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
if _, err = os.Stat(path); err == nil {
osTaskfile, err := readTaskfile(path)
if err != nil {
return nil, err
}
if err = taskfile.Merge(t, osTaskfile); err != nil {
return nil, err
}
}
for name, task := range t.Tasks {
task.Task = name
}
return t, nil
}
func readTaskfile(file string) (*taskfile.Taskfile, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
var t taskfile.Taskfile
return &t, yaml.NewDecoder(f).Decode(&t)
}

View File

@ -0,0 +1,52 @@
package read
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/go-task/task/internal/taskfile"
"gopkg.in/yaml.v2"
)
// Taskvars reads a Taskvars for a given directory
func Taskvars(dir string) (taskfile.Vars, error) {
vars := make(taskfile.Vars)
path := filepath.Join(dir, "Taskvars.yml")
if _, err := os.Stat(path); err == nil {
vars, err = readTaskvars(path)
if err != nil {
return nil, err
}
}
path = filepath.Join(dir, fmt.Sprintf("Taskvars_%s.yml", runtime.GOOS))
if _, err := os.Stat(path); err == nil {
osVars, err := readTaskvars(path)
if err != nil {
return nil, err
}
if vars == nil {
vars = osVars
} else {
for k, v := range osVars {
vars[k] = v
}
}
}
return vars, nil
}
func readTaskvars(file string) (taskfile.Vars, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
var vars taskfile.Vars
return vars, yaml.NewDecoder(f).Decode(&vars)
}

11
task.go
View File

@ -14,6 +14,7 @@ import (
"github.com/go-task/task/internal/logger" "github.com/go-task/task/internal/logger"
"github.com/go-task/task/internal/output" "github.com/go-task/task/internal/output"
"github.com/go-task/task/internal/taskfile" "github.com/go-task/task/internal/taskfile"
"github.com/go-task/task/internal/taskfile/read"
"github.com/go-task/task/internal/taskfile/version" "github.com/go-task/task/internal/taskfile/version"
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
@ -21,8 +22,6 @@ import (
) )
const ( const (
// TaskFilePath is the default Taskfile
TaskFilePath = "Taskfile"
// MaximumTaskCall is the max number of times a task can be called. // MaximumTaskCall is the max number of times a task can be called.
// This exists to prevent infinite loops on cyclic dependencies // This exists to prevent infinite loops on cyclic dependencies
MaximumTaskCall = 100 MaximumTaskCall = 100
@ -77,7 +76,13 @@ func (e *Executor) Run(calls ...taskfile.Call) error {
// Setup setups Executor's internal state // Setup setups Executor's internal state
func (e *Executor) Setup() error { func (e *Executor) Setup() error {
if err := e.readTaskfile(); err != nil { var err error
e.Taskfile, err = read.Taskfile(e.Dir)
if err != nil {
return err
}
e.taskvars, err = read.Taskvars(e.Dir)
if err != nil {
return err return err
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/go-task/task" "github.com/go-task/task"
"github.com/go-task/task/internal/taskfile" "github.com/go-task/task/internal/taskfile"
"github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -435,3 +436,22 @@ func TestTaskIgnoreErrors(t *testing.T) {
assert.Error(t, e.Run(taskfile.Call{Task: "cmd-should-fail"})) assert.Error(t, e.Run(taskfile.Call{Task: "cmd-should-fail"}))
}) })
} }
func TestExpand(t *testing.T) {
const dir = "testdata/expand"
home, err := homedir.Dir()
if err != nil {
t.Errorf("Couldn't get $HOME: %v", err)
}
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(taskfile.Call{Task: "pwd"}))
assert.Equal(t, home, strings.TrimSpace(buff.String()))
}

View File

@ -1,74 +0,0 @@
package task
import (
"fmt"
"io/ioutil"
"path/filepath"
"runtime"
"github.com/go-task/task/internal/taskfile"
"github.com/imdario/mergo"
"gopkg.in/yaml.v2"
)
// readTaskfile parses Taskfile from the disk
func (e *Executor) readTaskfile() error {
path := filepath.Join(e.Dir, TaskFilePath)
var err error
e.Taskfile, err = e.readTaskfileData(path)
if err != nil {
return err
}
osTasks, err := e.readTaskfileData(fmt.Sprintf("%s_%s", path, runtime.GOOS))
if err != nil {
switch err.(type) {
case taskFileNotFound:
default:
return err
}
} else {
if err := mergo.MapWithOverwrite(&e.Taskfile.Tasks, osTasks.Tasks); err != nil {
return err
}
}
for name, task := range e.Taskfile.Tasks {
task.Task = name
}
return e.readTaskvars()
}
func (e *Executor) readTaskfileData(path string) (*taskfile.Taskfile, error) {
if b, err := ioutil.ReadFile(path + ".yml"); err == nil {
var taskfile taskfile.Taskfile
return &taskfile, yaml.Unmarshal(b, &taskfile)
}
return nil, taskFileNotFound{path}
}
func (e *Executor) readTaskvars() error {
var (
file = filepath.Join(e.Dir, TaskvarsFilePath)
osSpecificFile = fmt.Sprintf("%s_%s", file, runtime.GOOS)
)
if b, err := ioutil.ReadFile(file + ".yml"); err == nil {
if err := yaml.Unmarshal(b, &e.taskvars); err != nil {
return err
}
}
if b, err := ioutil.ReadFile(osSpecificFile + ".yml"); err == nil {
osTaskvars := make(taskfile.Vars, 10)
if err := yaml.Unmarshal(b, &osTaskvars); err != nil {
return err
}
for k, v := range osTaskvars {
e.taskvars[k] = v
}
}
return nil
}

8
testdata/expand/Taskfile.yml vendored Normal file
View File

@ -0,0 +1,8 @@
version: '2'
tasks:
pwd:
cmds:
- pwd
dir: '~'
silent: true

View File

@ -3,14 +3,10 @@ package task
import ( import (
"path/filepath" "path/filepath"
"github.com/go-task/task/internal/osext"
"github.com/go-task/task/internal/taskfile" "github.com/go-task/task/internal/taskfile"
"github.com/go-task/task/internal/templater" "github.com/go-task/task/internal/templater"
)
var ( "mvdan.cc/sh/shell"
// TaskvarsFilePath file containing additional variables.
TaskvarsFilePath = "Taskvars"
) )
// CompiledTask returns a copy of a task, but replacing variables in almost all // CompiledTask returns a copy of a task, but replacing variables in almost all
@ -40,7 +36,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
Method: r.Replace(origTask.Method), Method: r.Replace(origTask.Method),
Prefix: r.Replace(origTask.Prefix), Prefix: r.Replace(origTask.Prefix),
} }
new.Dir, err = osext.Expand(new.Dir) new.Dir, err = shell.Expand(new.Dir, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -47,7 +47,7 @@ parts of the package.
// Handle constraint not being parseable. // Handle constraint not being parseable.
} }
v, _ := semver.NewVersion("1.3") v, err := semver.NewVersion("1.3")
if err != nil { if err != nil {
// Handle version not being parseable. // Handle version not being parseable.
} }

View File

@ -106,7 +106,7 @@ func MustParse(v string) *Version {
// Note, if the original version contained a leading v this version will not. // Note, if the original version contained a leading v this version will not.
// See the Original() method to retrieve the original value. Semantic Versions // See the Original() method to retrieve the original value. Semantic Versions
// don't contain a leading v per the spec. Instead it's optional on // don't contain a leading v per the spec. Instead it's optional on
// impelementation. // implementation.
func (v *Version) String() string { func (v *Version) String() string {
var buf bytes.Buffer var buf bytes.Buffer

View File

@ -19,6 +19,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync"
) )
// TraverseLink is a sentinel error for fastWalk, similar to filepath.SkipDir. // TraverseLink is a sentinel error for fastWalk, similar to filepath.SkipDir.
@ -67,11 +68,18 @@ func FastWalk(root string, walkFn func(path string, typ os.FileMode) error) erro
// buffered for correctness & not leaking goroutines: // buffered for correctness & not leaking goroutines:
resc: make(chan error, numWorkers), resc: make(chan error, numWorkers),
} }
defer close(w.donec)
// TODO(bradfitz): start the workers as needed? maybe not worth it. // TODO(bradfitz): start the workers as needed? maybe not worth it.
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ { for i := 0; i < numWorkers; i++ {
go w.doWork() wg.Add(1)
go w.doWork(&wg)
} }
// Ensure we wait for goroutines we started to finish before we return.
defer wg.Wait()
defer close(w.donec)
todo := []walkItem{{dir: root}} todo := []walkItem{{dir: root}}
out := 0 out := 0
for { for {
@ -113,10 +121,11 @@ func FastWalk(root string, walkFn func(path string, typ os.FileMode) error) erro
// doWork reads directories as instructed (via workc) and runs the // doWork reads directories as instructed (via workc) and runs the
// user's callback function. // user's callback function.
func (w *walker) doWork() { func (w *walker) doWork(wg *sync.WaitGroup) {
for { for {
select { select {
case <-w.donec: case <-w.donec:
wg.Done()
return return
case it := <-w.workc: case it := <-w.workc:
w.resc <- w.walk(it.dir, !it.callbackDone) w.resc <- w.walk(it.dir, !it.callbackDone)

21
vendor/github.com/mitchellh/go-homedir/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

155
vendor/github.com/mitchellh/go-homedir/homedir.go generated vendored Normal file
View File

@ -0,0 +1,155 @@
package homedir
import (
"bytes"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
)
// DisableCache will disable caching of the home directory. Caching is enabled
// by default.
var DisableCache bool
var homedirCache string
var cacheLock sync.RWMutex
// Dir returns the home directory for the executing user.
//
// This uses an OS-specific method for discovering the home directory.
// An error is returned if a home directory cannot be detected.
func Dir() (string, error) {
if !DisableCache {
cacheLock.RLock()
cached := homedirCache
cacheLock.RUnlock()
if cached != "" {
return cached, nil
}
}
cacheLock.Lock()
defer cacheLock.Unlock()
var result string
var err error
if runtime.GOOS == "windows" {
result, err = dirWindows()
} else {
// Unix-like system, so just assume Unix
result, err = dirUnix()
}
if err != nil {
return "", err
}
homedirCache = result
return result, nil
}
// Expand expands the path to include the home directory if the path
// is prefixed with `~`. If it isn't prefixed with `~`, the path is
// returned as-is.
func Expand(path string) (string, error) {
if len(path) == 0 {
return path, nil
}
if path[0] != '~' {
return path, nil
}
if len(path) > 1 && path[1] != '/' && path[1] != '\\' {
return "", errors.New("cannot expand user-specific home dir")
}
dir, err := Dir()
if err != nil {
return "", err
}
return filepath.Join(dir, path[1:]), nil
}
func dirUnix() (string, error) {
homeEnv := "HOME"
if runtime.GOOS == "plan9" {
// On plan9, env vars are lowercase.
homeEnv = "home"
}
// First prefer the HOME environmental variable
if home := os.Getenv(homeEnv); home != "" {
return home, nil
}
var stdout bytes.Buffer
// If that fails, try OS specific commands
if runtime.GOOS == "darwin" {
cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`)
cmd.Stdout = &stdout
if err := cmd.Run(); err == nil {
result := strings.TrimSpace(stdout.String())
if result != "" {
return result, nil
}
}
} else {
cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid()))
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
// If the error is ErrNotFound, we ignore it. Otherwise, return it.
if err != exec.ErrNotFound {
return "", err
}
} else {
if passwd := strings.TrimSpace(stdout.String()); passwd != "" {
// username:password:uid:gid:gecos:home:shell
passwdParts := strings.SplitN(passwd, ":", 7)
if len(passwdParts) > 5 {
return passwdParts[5], nil
}
}
}
}
// If all else fails, try the shell
stdout.Reset()
cmd := exec.Command("sh", "-c", "cd && pwd")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
result := strings.TrimSpace(stdout.String())
if result == "" {
return "", errors.New("blank output when reading home directory")
}
return result, nil
}
func dirWindows() (string, error) {
// First prefer the HOME environmental variable
if home := os.Getenv("HOME"); home != "" {
return home, nil
}
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home := drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
if home == "" {
return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank")
}
return home, nil
}

View File

@ -14,7 +14,11 @@ var fcntl64Syscall uintptr = SYS_FCNTL
// FcntlInt performs a fcntl syscall on fd with the provided command and argument. // FcntlInt performs a fcntl syscall on fd with the provided command and argument.
func FcntlInt(fd uintptr, cmd, arg int) (int, error) { func FcntlInt(fd uintptr, cmd, arg int) (int, error) {
valptr, _, err := Syscall(fcntl64Syscall, fd, uintptr(cmd), uintptr(arg)) valptr, _, errno := Syscall(fcntl64Syscall, fd, uintptr(cmd), uintptr(arg))
var err error
if errno != 0 {
err = errno
}
return int(valptr), err return int(valptr), err
} }

View File

@ -206,7 +206,7 @@ func (sa *SockaddrDatalink) sockaddr() (unsafe.Pointer, _Socklen, error) {
return unsafe.Pointer(&sa.raw), SizeofSockaddrDatalink, nil return unsafe.Pointer(&sa.raw), SizeofSockaddrDatalink, nil
} }
func anyToSockaddr(rsa *RawSockaddrAny) (Sockaddr, error) { func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) {
switch rsa.Addr.Family { switch rsa.Addr.Family {
case AF_LINK: case AF_LINK:
pp := (*RawSockaddrDatalink)(unsafe.Pointer(rsa)) pp := (*RawSockaddrDatalink)(unsafe.Pointer(rsa))
@ -286,7 +286,7 @@ func Accept(fd int) (nfd int, sa Sockaddr, err error) {
Close(nfd) Close(nfd)
return 0, nil, ECONNABORTED return 0, nil, ECONNABORTED
} }
sa, err = anyToSockaddr(&rsa) sa, err = anyToSockaddr(fd, &rsa)
if err != nil { if err != nil {
Close(nfd) Close(nfd)
nfd = 0 nfd = 0
@ -306,7 +306,7 @@ func Getsockname(fd int) (sa Sockaddr, err error) {
rsa.Addr.Family = AF_UNIX rsa.Addr.Family = AF_UNIX
rsa.Addr.Len = SizeofSockaddrUnix rsa.Addr.Len = SizeofSockaddrUnix
} }
return anyToSockaddr(&rsa) return anyToSockaddr(fd, &rsa)
} }
//sysnb socketpair(domain int, typ int, proto int, fd *[2]int32) (err error) //sysnb socketpair(domain int, typ int, proto int, fd *[2]int32) (err error)
@ -356,7 +356,7 @@ func Recvmsg(fd int, p, oob []byte, flags int) (n, oobn int, recvflags int, from
recvflags = int(msg.Flags) recvflags = int(msg.Flags)
// source address is only specified if the socket is unconnected // source address is only specified if the socket is unconnected
if rsa.Addr.Family != AF_UNSPEC { if rsa.Addr.Family != AF_UNSPEC {
from, err = anyToSockaddr(&rsa) from, err = anyToSockaddr(fd, &rsa)
} }
return return
} }

View File

@ -87,7 +87,7 @@ func Accept4(fd, flags int) (nfd int, sa Sockaddr, err error) {
if len > SizeofSockaddrAny { if len > SizeofSockaddrAny {
panic("RawSockaddrAny too small") panic("RawSockaddrAny too small")
} }
sa, err = anyToSockaddr(&rsa) sa, err = anyToSockaddr(fd, &rsa)
if err != nil { if err != nil {
Close(nfd) Close(nfd)
nfd = 0 nfd = 0

View File

@ -89,7 +89,7 @@ func Accept4(fd, flags int) (nfd int, sa Sockaddr, err error) {
if len > SizeofSockaddrAny { if len > SizeofSockaddrAny {
panic("RawSockaddrAny too small") panic("RawSockaddrAny too small")
} }
sa, err = anyToSockaddr(&rsa) sa, err = anyToSockaddr(fd, &rsa)
if err != nil { if err != nil {
Close(nfd) Close(nfd)
nfd = 0 nfd = 0

View File

@ -489,6 +489,47 @@ func (sa *SockaddrL2) sockaddr() (unsafe.Pointer, _Socklen, error) {
return unsafe.Pointer(&sa.raw), SizeofSockaddrL2, nil return unsafe.Pointer(&sa.raw), SizeofSockaddrL2, nil
} }
// SockaddrRFCOMM implements the Sockaddr interface for AF_BLUETOOTH type sockets
// using the RFCOMM protocol.
//
// Server example:
//
// fd, _ := Socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM)
// _ = unix.Bind(fd, &unix.SockaddrRFCOMM{
// Channel: 1,
// Addr: [6]uint8{0, 0, 0, 0, 0, 0}, // BDADDR_ANY or 00:00:00:00:00:00
// })
// _ = Listen(fd, 1)
// nfd, sa, _ := Accept(fd)
// fmt.Printf("conn addr=%v fd=%d", sa.(*unix.SockaddrRFCOMM).Addr, nfd)
// Read(nfd, buf)
//
// Client example:
//
// fd, _ := Socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM)
// _ = Connect(fd, &SockaddrRFCOMM{
// Channel: 1,
// Addr: [6]byte{0x11, 0x22, 0x33, 0xaa, 0xbb, 0xcc}, // CC:BB:AA:33:22:11
// })
// Write(fd, []byte(`hello`))
type SockaddrRFCOMM struct {
// Addr represents a bluetooth address, byte ordering is little-endian.
Addr [6]uint8
// Channel is a designated bluetooth channel, only 1-30 are available for use.
// Since Linux 2.6.7 and further zero value is the first available channel.
Channel uint8
raw RawSockaddrRFCOMM
}
func (sa *SockaddrRFCOMM) sockaddr() (unsafe.Pointer, _Socklen, error) {
sa.raw.Family = AF_BLUETOOTH
sa.raw.Channel = sa.Channel
sa.raw.Bdaddr = sa.Addr
return unsafe.Pointer(&sa.raw), SizeofSockaddrRFCOMM, nil
}
// SockaddrCAN implements the Sockaddr interface for AF_CAN type sockets. // SockaddrCAN implements the Sockaddr interface for AF_CAN type sockets.
// The RxID and TxID fields are used for transport protocol addressing in // The RxID and TxID fields are used for transport protocol addressing in
// (CAN_TP16, CAN_TP20, CAN_MCNET, and CAN_ISOTP), they can be left with // (CAN_TP16, CAN_TP20, CAN_MCNET, and CAN_ISOTP), they can be left with
@ -651,7 +692,7 @@ func (sa *SockaddrVM) sockaddr() (unsafe.Pointer, _Socklen, error) {
return unsafe.Pointer(&sa.raw), SizeofSockaddrVM, nil return unsafe.Pointer(&sa.raw), SizeofSockaddrVM, nil
} }
func anyToSockaddr(rsa *RawSockaddrAny) (Sockaddr, error) { func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) {
switch rsa.Addr.Family { switch rsa.Addr.Family {
case AF_NETLINK: case AF_NETLINK:
pp := (*RawSockaddrNetlink)(unsafe.Pointer(rsa)) pp := (*RawSockaddrNetlink)(unsafe.Pointer(rsa))
@ -728,6 +769,30 @@ func anyToSockaddr(rsa *RawSockaddrAny) (Sockaddr, error) {
Port: pp.Port, Port: pp.Port,
} }
return sa, nil return sa, nil
case AF_BLUETOOTH:
proto, err := GetsockoptInt(fd, SOL_SOCKET, SO_PROTOCOL)
if err != nil {
return nil, err
}
// only BTPROTO_L2CAP and BTPROTO_RFCOMM can accept connections
switch proto {
case BTPROTO_L2CAP:
pp := (*RawSockaddrL2)(unsafe.Pointer(rsa))
sa := &SockaddrL2{
PSM: pp.Psm,
CID: pp.Cid,
Addr: pp.Bdaddr,
AddrType: pp.Bdaddr_type,
}
return sa, nil
case BTPROTO_RFCOMM:
pp := (*RawSockaddrRFCOMM)(unsafe.Pointer(rsa))
sa := &SockaddrRFCOMM{
Channel: pp.Channel,
Addr: pp.Bdaddr,
}
return sa, nil
}
} }
return nil, EAFNOSUPPORT return nil, EAFNOSUPPORT
} }
@ -739,7 +804,7 @@ func Accept(fd int) (nfd int, sa Sockaddr, err error) {
if err != nil { if err != nil {
return return
} }
sa, err = anyToSockaddr(&rsa) sa, err = anyToSockaddr(fd, &rsa)
if err != nil { if err != nil {
Close(nfd) Close(nfd)
nfd = 0 nfd = 0
@ -757,7 +822,7 @@ func Accept4(fd int, flags int) (nfd int, sa Sockaddr, err error) {
if len > SizeofSockaddrAny { if len > SizeofSockaddrAny {
panic("RawSockaddrAny too small") panic("RawSockaddrAny too small")
} }
sa, err = anyToSockaddr(&rsa) sa, err = anyToSockaddr(fd, &rsa)
if err != nil { if err != nil {
Close(nfd) Close(nfd)
nfd = 0 nfd = 0
@ -771,7 +836,7 @@ func Getsockname(fd int) (sa Sockaddr, err error) {
if err = getsockname(fd, &rsa, &len); err != nil { if err = getsockname(fd, &rsa, &len); err != nil {
return return
} }
return anyToSockaddr(&rsa) return anyToSockaddr(fd, &rsa)
} }
func GetsockoptIPMreqn(fd, level, opt int) (*IPMreqn, error) { func GetsockoptIPMreqn(fd, level, opt int) (*IPMreqn, error) {
@ -960,7 +1025,7 @@ func Recvmsg(fd int, p, oob []byte, flags int) (n, oobn int, recvflags int, from
recvflags = int(msg.Flags) recvflags = int(msg.Flags)
// source address is only specified if the socket is unconnected // source address is only specified if the socket is unconnected
if rsa.Addr.Family != AF_UNSPEC { if rsa.Addr.Family != AF_UNSPEC {
from, err = anyToSockaddr(&rsa) from, err = anyToSockaddr(fd, &rsa)
} }
return return
} }

View File

@ -112,7 +112,7 @@ func Getsockname(fd int) (sa Sockaddr, err error) {
if err = getsockname(fd, &rsa, &len); err != nil { if err = getsockname(fd, &rsa, &len); err != nil {
return return
} }
return anyToSockaddr(&rsa) return anyToSockaddr(fd, &rsa)
} }
// GetsockoptString returns the string value of the socket option opt for the // GetsockoptString returns the string value of the socket option opt for the
@ -314,7 +314,11 @@ func UtimesNanoAt(dirfd int, path string, ts []Timespec, flags int) error {
// FcntlInt performs a fcntl syscall on fd with the provided command and argument. // FcntlInt performs a fcntl syscall on fd with the provided command and argument.
func FcntlInt(fd uintptr, cmd, arg int) (int, error) { func FcntlInt(fd uintptr, cmd, arg int) (int, error) {
valptr, _, err := sysvicall6(uintptr(unsafe.Pointer(&procfcntl)), 3, uintptr(fd), uintptr(cmd), uintptr(arg), 0, 0, 0) valptr, _, errno := sysvicall6(uintptr(unsafe.Pointer(&procfcntl)), 3, uintptr(fd), uintptr(cmd), uintptr(arg), 0, 0, 0)
var err error
if errno != 0 {
err = errno
}
return int(valptr), err return int(valptr), err
} }
@ -356,7 +360,7 @@ func Futimes(fd int, tv []Timeval) error {
return futimesat(fd, nil, (*[2]Timeval)(unsafe.Pointer(&tv[0]))) return futimesat(fd, nil, (*[2]Timeval)(unsafe.Pointer(&tv[0])))
} }
func anyToSockaddr(rsa *RawSockaddrAny) (Sockaddr, error) { func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) {
switch rsa.Addr.Family { switch rsa.Addr.Family {
case AF_UNIX: case AF_UNIX:
pp := (*RawSockaddrUnix)(unsafe.Pointer(rsa)) pp := (*RawSockaddrUnix)(unsafe.Pointer(rsa))
@ -407,7 +411,7 @@ func Accept(fd int) (nfd int, sa Sockaddr, err error) {
if nfd == -1 { if nfd == -1 {
return return
} }
sa, err = anyToSockaddr(&rsa) sa, err = anyToSockaddr(fd, &rsa)
if err != nil { if err != nil {
Close(nfd) Close(nfd)
nfd = 0 nfd = 0
@ -444,7 +448,7 @@ func Recvmsg(fd int, p, oob []byte, flags int) (n, oobn int, recvflags int, from
oobn = int(msg.Accrightslen) oobn = int(msg.Accrightslen)
// source address is only specified if the socket is unconnected // source address is only specified if the socket is unconnected
if rsa.Addr.Family != AF_UNSPEC { if rsa.Addr.Family != AF_UNSPEC {
from, err = anyToSockaddr(&rsa) from, err = anyToSockaddr(fd, &rsa)
} }
return return
} }

View File

@ -219,7 +219,7 @@ func Getpeername(fd int) (sa Sockaddr, err error) {
if err = getpeername(fd, &rsa, &len); err != nil { if err = getpeername(fd, &rsa, &len); err != nil {
return return
} }
return anyToSockaddr(&rsa) return anyToSockaddr(fd, &rsa)
} }
func GetsockoptByte(fd, level, opt int) (value byte, err error) { func GetsockoptByte(fd, level, opt int) (value byte, err error) {
@ -291,7 +291,7 @@ func Recvfrom(fd int, p []byte, flags int) (n int, from Sockaddr, err error) {
return return
} }
if rsa.Addr.Family != AF_UNSPEC { if rsa.Addr.Family != AF_UNSPEC {
from, err = anyToSockaddr(&rsa) from, err = anyToSockaddr(fd, &rsa)
} }
return return
} }

View File

@ -1474,6 +1474,21 @@ func Munlockall() (err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func faccessat(dirfd int, path string, mode uint32) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
_, _, e1 := Syscall(SYS_FACCESSAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(mode))
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Dup2(oldfd int, newfd int) (err error) { func Dup2(oldfd int, newfd int) (err error) {
_, _, e1 := Syscall(SYS_DUP2, uintptr(oldfd), uintptr(newfd), 0) _, _, e1 := Syscall(SYS_DUP2, uintptr(oldfd), uintptr(newfd), 0)
if e1 != 0 { if e1 != 0 {
@ -1495,21 +1510,6 @@ func EpollCreate(size int) (fd int, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func faccessat(dirfd int, path string, mode uint32) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
_, _, e1 := Syscall(SYS_FACCESSAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(mode))
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) { func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer var _p0 unsafe.Pointer
if len(events) > 0 { if len(events) > 0 {

View File

@ -1474,6 +1474,21 @@ func Munlockall() (err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func faccessat(dirfd int, path string, mode uint32) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
_, _, e1 := Syscall(SYS_FACCESSAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(mode))
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Dup2(oldfd int, newfd int) (err error) { func Dup2(oldfd int, newfd int) (err error) {
_, _, e1 := Syscall(SYS_DUP2, uintptr(oldfd), uintptr(newfd), 0) _, _, e1 := Syscall(SYS_DUP2, uintptr(oldfd), uintptr(newfd), 0)
if e1 != 0 { if e1 != 0 {
@ -1495,21 +1510,6 @@ func EpollCreate(size int) (fd int, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func faccessat(dirfd int, path string, mode uint32) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
_, _, e1 := Syscall(SYS_FACCESSAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(mode))
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) { func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer var _p0 unsafe.Pointer
if len(events) > 0 { if len(events) > 0 {

View File

@ -248,6 +248,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -401,6 +408,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -250,6 +250,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -405,6 +412,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -251,6 +251,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -404,6 +411,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -251,6 +251,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -406,6 +413,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -249,6 +249,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -402,6 +409,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -251,6 +251,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -406,6 +413,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -251,6 +251,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -406,6 +413,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -249,6 +249,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -402,6 +409,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -252,6 +252,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -407,6 +414,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -252,6 +252,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -407,6 +414,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -250,6 +250,13 @@ type RawSockaddrL2 struct {
_ [1]byte _ [1]byte
} }
type RawSockaddrRFCOMM struct {
Family uint16
Bdaddr [6]uint8
Channel uint8
_ [1]byte
}
type RawSockaddrCAN struct { type RawSockaddrCAN struct {
Family uint16 Family uint16
_ [2]byte _ [2]byte
@ -405,6 +412,7 @@ const (
SizeofSockaddrNetlink = 0xc SizeofSockaddrNetlink = 0xc
SizeofSockaddrHCI = 0x6 SizeofSockaddrHCI = 0x6
SizeofSockaddrL2 = 0xe SizeofSockaddrL2 = 0xe
SizeofSockaddrRFCOMM = 0xa
SizeofSockaddrCAN = 0x10 SizeofSockaddrCAN = 0x10
SizeofSockaddrALG = 0x58 SizeofSockaddrALG = 0x58
SizeofSockaddrVM = 0x10 SizeofSockaddrVM = 0x10

View File

@ -43,6 +43,11 @@ const (
SC_STATUS_PROCESS_INFO = 0 SC_STATUS_PROCESS_INFO = 0
SC_ACTION_NONE = 0
SC_ACTION_RESTART = 1
SC_ACTION_REBOOT = 2
SC_ACTION_RUN_COMMAND = 3
SERVICE_STOPPED = 1 SERVICE_STOPPED = 1
SERVICE_START_PENDING = 2 SERVICE_START_PENDING = 2
SERVICE_STOP_PENDING = 3 SERVICE_STOP_PENDING = 3
@ -148,6 +153,19 @@ type ENUM_SERVICE_STATUS_PROCESS struct {
ServiceStatusProcess SERVICE_STATUS_PROCESS ServiceStatusProcess SERVICE_STATUS_PROCESS
} }
type SERVICE_FAILURE_ACTIONS struct {
ResetPeriod uint32
RebootMsg *uint16
Command *uint16
ActionsCount uint32
Actions *SC_ACTION
}
type SC_ACTION struct {
Type uint32
Delay uint32
}
//sys CloseServiceHandle(handle Handle) (err error) = advapi32.CloseServiceHandle //sys CloseServiceHandle(handle Handle) (err error) = advapi32.CloseServiceHandle
//sys CreateService(mgr Handle, serviceName *uint16, displayName *uint16, access uint32, srvType uint32, startType uint32, errCtl uint32, pathName *uint16, loadOrderGroup *uint16, tagId *uint32, dependencies *uint16, serviceStartName *uint16, password *uint16) (handle Handle, err error) [failretval==0] = advapi32.CreateServiceW //sys CreateService(mgr Handle, serviceName *uint16, displayName *uint16, access uint32, srvType uint32, startType uint32, errCtl uint32, pathName *uint16, loadOrderGroup *uint16, tagId *uint32, dependencies *uint16, serviceStartName *uint16, password *uint16) (handle Handle, err error) [failretval==0] = advapi32.CreateServiceW
//sys OpenService(mgr Handle, serviceName *uint16, access uint32) (handle Handle, err error) [failretval==0] = advapi32.OpenServiceW //sys OpenService(mgr Handle, serviceName *uint16, access uint32) (handle Handle, err error) [failretval==0] = advapi32.OpenServiceW

View File

@ -94,16 +94,29 @@ const (
FILE_APPEND_DATA = 0x00000004 FILE_APPEND_DATA = 0x00000004
FILE_WRITE_ATTRIBUTES = 0x00000100 FILE_WRITE_ATTRIBUTES = 0x00000100
FILE_SHARE_READ = 0x00000001 FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002 FILE_SHARE_WRITE = 0x00000002
FILE_SHARE_DELETE = 0x00000004 FILE_SHARE_DELETE = 0x00000004
FILE_ATTRIBUTE_READONLY = 0x00000001
FILE_ATTRIBUTE_HIDDEN = 0x00000002 FILE_ATTRIBUTE_READONLY = 0x00000001
FILE_ATTRIBUTE_SYSTEM = 0x00000004 FILE_ATTRIBUTE_HIDDEN = 0x00000002
FILE_ATTRIBUTE_DIRECTORY = 0x00000010 FILE_ATTRIBUTE_SYSTEM = 0x00000004
FILE_ATTRIBUTE_ARCHIVE = 0x00000020 FILE_ATTRIBUTE_DIRECTORY = 0x00000010
FILE_ATTRIBUTE_NORMAL = 0x00000080 FILE_ATTRIBUTE_ARCHIVE = 0x00000020
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400 FILE_ATTRIBUTE_DEVICE = 0x00000040
FILE_ATTRIBUTE_NORMAL = 0x00000080
FILE_ATTRIBUTE_TEMPORARY = 0x00000100
FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
FILE_ATTRIBUTE_COMPRESSED = 0x00000800
FILE_ATTRIBUTE_OFFLINE = 0x00001000
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000
FILE_ATTRIBUTE_ENCRYPTED = 0x00004000
FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000
FILE_ATTRIBUTE_VIRTUAL = 0x00010000
FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000
FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x00040000
FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 0x00400000
INVALID_FILE_ATTRIBUTES = 0xffffffff INVALID_FILE_ATTRIBUTES = 0xffffffff

View File

@ -290,7 +290,7 @@ func (r *Runner) builtinCode(pos syntax.Pos, name string, args []string) int {
if parseErr { if parseErr {
return 2 return 2
} }
return oneIf(r.bashTest(expr) == "") return oneIf(r.bashTest(expr, true) == "")
case "exec": case "exec":
// TODO: Consider syscall.Exec, i.e. actually replacing // TODO: Consider syscall.Exec, i.e. actually replacing
// the process. It's in theory what a shell should do, // the process. It's in theory what a shell should do,

View File

@ -7,4 +7,4 @@
// //
// This package is a work in progress and EXPERIMENTAL; its API is not // This package is a work in progress and EXPERIMENTAL; its API is not
// subject to the 1.x backwards compatibility guarantee. // subject to the 1.x backwards compatibility guarantee.
package interp // import "mvdan.cc/sh/interp" package interp

View File

@ -247,7 +247,7 @@ func (r *Runner) wordField(wps []syntax.WordPart, ql quoteLevel) []fieldPart {
case '\n': // remove \\\n case '\n': // remove \\\n
i++ i++
continue continue
case '\\', '$', '`': // special chars case '"', '\\', '$', '`': // special chars
continue continue
} }
} }

View File

@ -602,7 +602,7 @@ func (r *Runner) cmd(cm syntax.Command) {
} }
case *syntax.TestClause: case *syntax.TestClause:
r.exit = 0 r.exit = 0
if r.bashTest(x.X) == "" && r.exit == 0 { if r.bashTest(x.X, false) == "" && r.exit == 0 {
// to preserve exit code 2 for regex // to preserve exit code 2 for regex
// errors, etc // errors, etc
r.exit = 1 r.exit = 1

View File

@ -15,29 +15,36 @@ import (
) )
// non-empty string is true, empty string is false // non-empty string is true, empty string is false
func (r *Runner) bashTest(expr syntax.TestExpr) string { func (r *Runner) bashTest(expr syntax.TestExpr, classic bool) string {
switch x := expr.(type) { switch x := expr.(type) {
case *syntax.Word: case *syntax.Word:
return r.loneWord(x) return r.loneWord(x)
case *syntax.ParenTest: case *syntax.ParenTest:
return r.bashTest(x.X) return r.bashTest(x.X, classic)
case *syntax.BinaryTest: case *syntax.BinaryTest:
switch x.Op { switch x.Op {
case syntax.TsMatch, syntax.TsNoMatch: case syntax.TsMatch, syntax.TsNoMatch:
str := r.loneWord(x.X.(*syntax.Word)) str := r.loneWord(x.X.(*syntax.Word))
yw := x.Y.(*syntax.Word) yw := x.Y.(*syntax.Word)
pat := r.lonePattern(yw) if classic { // test, [
if match(pat, str) == (x.Op == syntax.TsMatch) { lit := r.loneWord(yw)
return "1" if (str == lit) == (x.Op == syntax.TsMatch) {
return "1"
}
} else { // [[
pat := r.lonePattern(yw)
if match(pat, str) == (x.Op == syntax.TsMatch) {
return "1"
}
} }
return "" return ""
} }
if r.binTest(x.Op, r.bashTest(x.X), r.bashTest(x.Y)) { if r.binTest(x.Op, r.bashTest(x.X, classic), r.bashTest(x.Y, classic)) {
return "1" return "1"
} }
return "" return ""
case *syntax.UnaryTest: case *syntax.UnaryTest:
if r.unTest(x.Op, r.bashTest(x.X)) { if r.unTest(x.Op, r.bashTest(x.X, classic)) {
return "1" return "1"
} }
return "" return ""

View File

@ -6,4 +6,4 @@
// //
// This package is a work in progress and EXPERIMENTAL; its API is not // This package is a work in progress and EXPERIMENTAL; its API is not
// subject to the 1.x backwards compatibility guarantee. // subject to the 1.x backwards compatibility guarantee.
package shell // import "mvdan.cc/sh/shell" package shell

View File

@ -4,6 +4,7 @@
package shell package shell
import ( import (
"context"
"strings" "strings"
"mvdan.cc/sh/interp" "mvdan.cc/sh/interp"
@ -19,23 +20,28 @@ import (
// //
// Any side effects or modifications to the system are forbidden when // Any side effects or modifications to the system are forbidden when
// interpreting the program. This is enforced via whitelists when // interpreting the program. This is enforced via whitelists when
// executing programs and opening paths. // executing programs and opening paths. The interpreter also has a timeout of
// two seconds.
func Expand(s string, env func(string) string) (string, error) { func Expand(s string, env func(string) string) (string, error) {
p := syntax.NewParser() p := syntax.NewParser()
src := "<<EOF\n" + s + "\nEOF" src := "<<EXPAND_EOF\n" + s + "\nEXPAND_EOF"
f, err := p.Parse(strings.NewReader(src), "") f, err := p.Parse(strings.NewReader(src), "")
if err != nil { if err != nil {
return "", err return "", err
} }
word := f.Stmts[0].Redirs[0].Hdoc word := f.Stmts[0].Redirs[0].Hdoc
last := word.Parts[len(word.Parts)-1].(*syntax.Lit)
// since the heredoc implies a trailing newline
last.Value = strings.TrimSuffix(last.Value, "\n")
r := pureRunner() r := pureRunner()
if env != nil { if env != nil {
r.Env = interp.FuncEnviron(env) r.Env = interp.FuncEnviron(env)
} }
r.Reset() r.Reset()
ctx, cancel := context.WithTimeout(context.Background(), pureRunnerTimeout)
defer cancel()
r.Context = ctx
fields := r.Fields(word) fields := r.Fields(word)
// TODO: runner error // TODO: runner error
join := strings.Join(fields, "") return strings.Join(fields, ""), nil
// since the heredoc implies a trailing newline
return strings.TrimSuffix(join, "\n"), nil
} }

View File

@ -4,9 +4,11 @@
package shell package shell
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"os" "os"
"time"
"mvdan.cc/sh/interp" "mvdan.cc/sh/interp"
"mvdan.cc/sh/syntax" "mvdan.cc/sh/syntax"
@ -44,6 +46,8 @@ var purePrograms = []string{
"env", "sleep", "uniq", "sort", "env", "sleep", "uniq", "sort",
} }
var pureRunnerTimeout = 2 * time.Second
func pureRunner() *interp.Runner { func pureRunner() *interp.Runner {
r := &interp.Runner{} r := &interp.Runner{}
// forbid executing programs that might cause trouble // forbid executing programs that might cause trouble
@ -67,10 +71,14 @@ func pureRunner() *interp.Runner {
// //
// Any side effects or modifications to the system are forbidden when // Any side effects or modifications to the system are forbidden when
// interpreting the program. This is enforced via whitelists when // interpreting the program. This is enforced via whitelists when
// executing programs and opening paths. // executing programs and opening paths. The interpreter also has a timeout of
// two seconds.
func SourceNode(node syntax.Node) (map[string]interp.Variable, error) { func SourceNode(node syntax.Node) (map[string]interp.Variable, error) {
r := pureRunner() r := pureRunner()
r.Reset() r.Reset()
ctx, cancel := context.WithTimeout(context.Background(), pureRunnerTimeout)
defer cancel()
r.Context = ctx
if err := r.Run(node); err != nil { if err := r.Run(node); err != nil {
return nil, fmt.Errorf("could not run: %v", err) return nil, fmt.Errorf("could not run: %v", err)
} }

View File

@ -3,4 +3,4 @@
// Package syntax implements parsing and formatting of shell programs. // Package syntax implements parsing and formatting of shell programs.
// It supports both POSIX Shell and Bash. // It supports both POSIX Shell and Bash.
package syntax // import "mvdan.cc/sh/syntax" package syntax

View File

@ -107,6 +107,7 @@ retry:
} else if p.fill(); p.bs == nil { } else if p.fill(); p.bs == nil {
p.bsp++ p.bsp++
p.r = utf8.RuneSelf p.r = utf8.RuneSelf
p.w = 1
} else { } else {
goto retry goto retry
} }
@ -232,6 +233,7 @@ skipSpace:
w := utf8.RuneLen(r) w := utf8.RuneLen(r)
if bytes.HasPrefix(p.bs[p.bsp-w:], p.stopAt) { if bytes.HasPrefix(p.bs[p.bsp-w:], p.stopAt) {
p.r = utf8.RuneSelf p.r = utf8.RuneSelf
p.w = 1
p.tok = _EOF p.tok = _EOF
return return
} }
@ -748,10 +750,13 @@ func (p *Parser) endLit() (s string) {
return return
} }
func (p *Parser) numLit() bool { func (p *Parser) isLitRedir() bool {
for _, b := range p.litBs { lit := p.litBs[:len(p.litBs)-1]
if lit[0] == '{' && lit[len(lit)-1] == '}' {
return ValidName(string(lit[1 : len(lit)-1]))
}
for _, b := range lit {
switch b { switch b {
case '>', '<':
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default: default:
return false return false
@ -800,7 +805,7 @@ loop:
break loop break loop
} }
case '/': case '/':
if p.quote&allParamExp != 0 && p.quote != paramExpExp { if p.quote != paramExpExp {
break loop break loop
} }
case ':', '=', '%', '^', ',', '?', '!', '*': case ':', '=', '%', '^', ',', '?', '!', '*':
@ -838,7 +843,7 @@ loop:
p.discardLit(2) p.discardLit(2)
} }
case '>', '<': case '>', '<':
if p.peekByte('(') || !p.numLit() { if p.peekByte('(') || !p.isLitRedir() {
tok = _Lit tok = _Lit
} else { } else {
tok = _LitRedir tok = _LitRedir

View File

@ -232,7 +232,7 @@ func (a *Assign) End() Pos {
type Redirect struct { type Redirect struct {
OpPos Pos OpPos Pos
Op RedirOperator Op RedirOperator
N *Lit // N>, must be a number N *Lit // fd>, or {varname}> in Bash
Word *Word // >word Word *Word // >word
Hdoc *Word // here-document body Hdoc *Word // here-document body
} }

View File

@ -30,6 +30,18 @@ func Variant(l LangVariant) func(*Parser) {
return func(p *Parser) { p.lang = l } return func(p *Parser) { p.lang = l }
} }
func (l LangVariant) String() string {
switch l {
case LangBash:
return "bash"
case LangPOSIX:
return "posix"
case LangMirBSDKorn:
return "mksh"
}
return "unknown shell language variant"
}
// StopAt configures the lexer to stop at an arbitrary word, treating it // StopAt configures the lexer to stop at an arbitrary word, treating it
// as if it were the end of the input. It can contain any characters // as if it were the end of the input. It can contain any characters
// except whitespace, and cannot be over four bytes in size. // except whitespace, and cannot be over four bytes in size.
@ -480,26 +492,60 @@ func (p *Parser) errPass(err error) {
p.err = err p.err = err
p.bsp = len(p.bs) + 1 p.bsp = len(p.bs) + 1
p.r = utf8.RuneSelf p.r = utf8.RuneSelf
p.w = 1
p.tok = _EOF p.tok = _EOF
} }
} }
// ParseError represents an error found when parsing a source file. // ParseError represents an error found when parsing a source file, from which
// the parser cannot recover.
type ParseError struct { type ParseError struct {
Filename string Filename string
Pos Pos
Text string Text string
} }
func (e *ParseError) Error() string { func (e ParseError) Error() string {
if e.Filename == "" { if e.Filename == "" {
return fmt.Sprintf("%s: %s", e.Pos.String(), e.Text) return fmt.Sprintf("%s: %s", e.Pos.String(), e.Text)
} }
return fmt.Sprintf("%s:%s: %s", e.Filename, e.Pos.String(), e.Text) return fmt.Sprintf("%s:%s: %s", e.Filename, e.Pos.String(), e.Text)
} }
// LangError is returned when the parser encounters code that is only valid in
// other shell language variants. The error includes what feature is not present
// in the current language variant, and what languages support it.
type LangError struct {
Filename string
Pos
Feature string
Langs []LangVariant
}
func (e LangError) Error() string {
var buf bytes.Buffer
if e.Filename != "" {
buf.WriteString(e.Filename + ":")
}
buf.WriteString(e.Pos.String() + ": ")
buf.WriteString(e.Feature)
if strings.HasSuffix(e.Feature, "s") {
buf.WriteString(" are a ")
} else {
buf.WriteString(" is a ")
}
for i, lang := range e.Langs {
if i > 0 {
buf.WriteString("/")
}
buf.WriteString(lang.String())
}
buf.WriteString(" feature")
return buf.String()
}
func (p *Parser) posErr(pos Pos, format string, a ...interface{}) { func (p *Parser) posErr(pos Pos, format string, a ...interface{}) {
p.errPass(&ParseError{ p.errPass(ParseError{
Filename: p.f.Name, Filename: p.f.Name,
Pos: pos, Pos: pos,
Text: fmt.Sprintf(format, a...), Text: fmt.Sprintf(format, a...),
@ -510,6 +556,15 @@ func (p *Parser) curErr(format string, a ...interface{}) {
p.posErr(p.pos, format, a...) p.posErr(p.pos, format, a...)
} }
func (p *Parser) langErr(pos Pos, feature string, langs ...LangVariant) {
p.errPass(LangError{
Filename: p.f.Name,
Pos: pos,
Feature: feature,
Langs: langs,
})
}
func (p *Parser) stmts(fn func(*Stmt) bool, stops ...string) { func (p *Parser) stmts(fn func(*Stmt) bool, stops ...string) {
gotEnd := true gotEnd := true
loop: loop:
@ -668,7 +723,7 @@ func (p *Parser) wordPart() WordPart {
p.next() p.next()
if p.got(hash) { if p.got(hash) {
if p.lang != LangMirBSDKorn { if p.lang != LangMirBSDKorn {
p.posErr(ar.Pos(), "unsigned expressions are a mksh feature") p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn)
} }
ar.Unsigned = true ar.Unsigned = true
} }
@ -712,9 +767,16 @@ func (p *Parser) wordPart() WordPart {
p.pos = posAddCol(p.pos, 1) p.pos = posAddCol(p.pos, 1)
pe.Param = p.getLit() pe.Param = p.getLit()
if pe.Param != nil && pe.Param.Value == "" { if pe.Param != nil && pe.Param.Value == "" {
// e.g. "$\\\n", which we can't detect above
l := p.lit(pe.Dollar, "$") l := p.lit(pe.Dollar, "$")
p.next() if p.val == "" {
// e.g. "$\\\n" followed by a closing double
// quote, so we need the next token.
p.next()
} else {
// e.g. "$\\\"" within double quotes, so we must
// keep the rest of the literal characters.
l.ValueEnd = posAddCol(l.ValuePos, 1)
}
return l return l
} }
return pe return pe
@ -777,7 +839,7 @@ func (p *Parser) wordPart() WordPart {
return cs return cs
case globQuest, globStar, globPlus, globAt, globExcl: case globQuest, globStar, globPlus, globAt, globExcl:
if p.lang == LangPOSIX { if p.lang == LangPOSIX {
p.curErr("extended globs are a bash feature") p.langErr(p.pos, "extended globs", LangBash, LangMirBSDKorn)
} }
eg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos} eg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos}
lparens := 1 lparens := 1
@ -1058,7 +1120,7 @@ func (p *Parser) paramExp() *ParamExp {
case exclMark: case exclMark:
if paramNameOp(p.r) { if paramNameOp(p.r) {
if p.lang == LangPOSIX { if p.lang == LangPOSIX {
p.curErr("${!foo} is a bash feature") p.langErr(p.pos, "${!foo}", LangBash, LangMirBSDKorn)
} }
pe.Excl = true pe.Excl = true
p.next() p.next()
@ -1067,6 +1129,9 @@ func (p *Parser) paramExp() *ParamExp {
op := p.tok op := p.tok
switch p.tok { switch p.tok {
case _Lit, _LitWord: case _Lit, _LitWord:
if !numberLiteral(p.val) && !ValidName(p.val) {
p.curErr("invalid parameter name")
}
pe.Param = p.lit(p.pos, p.val) pe.Param = p.lit(p.pos, p.val)
p.next() p.next()
case quest, minus: case quest, minus:
@ -1093,7 +1158,7 @@ func (p *Parser) paramExp() *ParamExp {
return pe return pe
case leftBrack: case leftBrack:
if p.lang == LangPOSIX { if p.lang == LangPOSIX {
p.curErr("arrays are a bash feature") p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn)
} }
if !ValidName(pe.Param.Value) { if !ValidName(pe.Param.Value) {
p.curErr("cannot index a special parameter name") p.curErr("cannot index a special parameter name")
@ -1113,7 +1178,7 @@ func (p *Parser) paramExp() *ParamExp {
case slash, dblSlash: case slash, dblSlash:
// pattern search and replace // pattern search and replace
if p.lang == LangPOSIX { if p.lang == LangPOSIX {
p.curErr("search and replace is a bash feature") p.langErr(p.pos, "search and replace", LangBash, LangMirBSDKorn)
} }
pe.Repl = &Replace{All: p.tok == dblSlash} pe.Repl = &Replace{All: p.tok == dblSlash}
p.quote = paramExpRepl p.quote = paramExpRepl
@ -1126,7 +1191,7 @@ func (p *Parser) paramExp() *ParamExp {
case colon: case colon:
// slicing // slicing
if p.lang == LangPOSIX { if p.lang == LangPOSIX {
p.curErr("slicing is a bash feature") p.langErr(p.pos, "slicing", LangBash, LangMirBSDKorn)
} }
pe.Slice = &Slice{} pe.Slice = &Slice{}
colonPos := p.pos colonPos := p.pos
@ -1141,13 +1206,13 @@ func (p *Parser) paramExp() *ParamExp {
case caret, dblCaret, comma, dblComma: case caret, dblCaret, comma, dblComma:
// upper/lower case // upper/lower case
if p.lang != LangBash { if p.lang != LangBash {
p.curErr("this expansion operator is a bash feature") p.langErr(p.pos, "this expansion operator", LangBash)
} }
pe.Exp = p.paramExpExp() pe.Exp = p.paramExpExp()
case at, star: case at, star:
switch { switch {
case p.tok == at && p.lang == LangPOSIX: case p.tok == at && p.lang == LangPOSIX:
p.curErr("this expansion operator is a bash feature") p.langErr(p.pos, "this expansion operator", LangBash, LangMirBSDKorn)
case p.tok == star && !pe.Excl: case p.tok == star && !pe.Excl:
p.curErr("not a valid parameter expansion operator: %v", p.tok) p.curErr("not a valid parameter expansion operator: %v", p.tok)
case pe.Excl: case pe.Excl:
@ -1252,6 +1317,15 @@ func ValidName(val string) bool {
return true return true
} }
func numberLiteral(val string) bool {
for _, r := range val {
if '0' > r || r > '9' {
return false
}
}
return true
}
func (p *Parser) hasValidIdent() bool { func (p *Parser) hasValidIdent() bool {
if p.tok != _Lit && p.tok != _LitWord { if p.tok != _Lit && p.tok != _LitWord {
return false return false
@ -1319,7 +1393,7 @@ func (p *Parser) getAssign(needEqual bool) *Assign {
} }
if as.Value == nil && p.tok == leftParen { if as.Value == nil && p.tok == leftParen {
if p.lang == LangPOSIX { if p.lang == LangPOSIX {
p.curErr("arrays are a bash feature") p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn)
} }
if as.Index != nil { if as.Index != nil {
p.curErr("arrays cannot be nested") p.curErr("arrays cannot be nested")
@ -1394,6 +1468,9 @@ func (p *Parser) doRedirect(s *Stmt) {
s.Redirs = append(s.Redirs, r) s.Redirs = append(s.Redirs, r)
} }
r.N = p.getLit() r.N = p.getLit()
if p.lang != LangBash && r.N != nil && r.N.Value[0] == '{' {
p.langErr(r.N.Pos(), "{varname} redirects", LangBash)
}
r.Op, r.OpPos = RedirOperator(p.tok), p.pos r.Op, r.OpPos = RedirOperator(p.tok), p.pos
p.next() p.next()
switch r.Op { switch r.Op {
@ -1643,7 +1720,7 @@ func (p *Parser) arithmExpCmd(s *Stmt) {
p.next() p.next()
if p.got(hash) { if p.got(hash) {
if p.lang != LangMirBSDKorn { if p.lang != LangMirBSDKorn {
p.posErr(ar.Pos(), "unsigned expressions are a mksh feature") p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn)
} }
ar.Unsigned = true ar.Unsigned = true
} }
@ -1725,7 +1802,7 @@ func (p *Parser) loop(fpos Pos) Loop {
if p.lang != LangBash { if p.lang != LangBash {
switch p.tok { switch p.tok {
case leftParen, dblLeftParen: case leftParen, dblLeftParen:
p.curErr("c-style fors are a bash feature") p.langErr(p.pos, "c-style fors", LangBash)
} }
} }
if p.tok == dblLeftParen { if p.tok == dblLeftParen {
@ -1909,7 +1986,7 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr {
} }
case TsReMatch: case TsReMatch:
if p.lang != LangBash { if p.lang != LangBash {
p.curErr("regex tests are a bash feature") p.langErr(p.pos, "regex tests", LangBash)
} }
oldReOpenParens := p.reOpenParens oldReOpenParens := p.reOpenParens
old := p.preNested(testRegexp) old := p.preNested(testRegexp)

View File

@ -68,17 +68,12 @@ func NewPrinter(options ...func(*Printer)) *Printer {
func (p *Printer) Print(w io.Writer, node Node) error { func (p *Printer) Print(w io.Writer, node Node) error {
p.reset() p.reset()
p.bufWriter.Reset(w) p.bufWriter.Reset(w)
p.line = node.Pos().Line()
switch x := node.(type) { switch x := node.(type) {
case *File: case *File:
p.stmts(x.StmtList) p.stmtList(x.StmtList)
p.newline(x.End()) p.newline(x.End())
case *Stmt: case *Stmt:
sl := StmtList{Stmts: []*Stmt{x}} p.stmtList(StmtList{Stmts: []*Stmt{x}})
// StmtList.pos is better than Stmt.Pos, since it also takes
// comments into account.
p.line = sl.pos().Line()
p.stmts(sl)
case *Word: case *Word:
p.word(x) p.word(x)
case Command: case Command:
@ -152,6 +147,8 @@ type Printer struct {
// comment in the same line, breaking programs. // comment in the same line, breaking programs.
pendingComments []Comment pendingComments []Comment
// firstLine means we are still writing the first line
firstLine bool
// line is the current line number // line is the current line number
line uint line uint
@ -180,7 +177,11 @@ func (p *Printer) reset() {
p.wantSpace, p.wantNewline = false, false p.wantSpace, p.wantNewline = false, false
p.commentPadding = 0 p.commentPadding = 0
p.pendingComments = p.pendingComments[:0] p.pendingComments = p.pendingComments[:0]
// minification uses its own newline logic
p.firstLine = !p.minify
p.line = 0 p.line = 0
p.lastLevel, p.level = 0, 0 p.lastLevel, p.level = 0, 0
p.levelIncs = p.levelIncs[:0] p.levelIncs = p.levelIncs[:0]
p.nestedBinary = false p.nestedBinary = false
@ -354,7 +355,8 @@ func (p *Printer) flushHeredocs() {
} }
func (p *Printer) newlines(pos Pos) { func (p *Printer) newlines(pos Pos) {
if !p.wantNewline && p.line == 0 && len(p.pendingComments) == 0 { if p.firstLine && len(p.pendingComments) == 0 {
p.firstLine = false
return // no empty lines at the top return // no empty lines at the top
} }
if !p.wantNewline && pos.Line() <= p.line { if !p.wantNewline && pos.Line() <= p.line {
@ -379,11 +381,11 @@ func (p *Printer) rightParen(pos Pos) {
p.wantSpace = true p.wantSpace = true
} }
func (p *Printer) semiRsrv(s string, pos Pos, fallback bool) { func (p *Printer) semiRsrv(s string, pos Pos) {
if p.wantNewline || pos.Line() > p.line { if p.wantNewline || pos.Line() > p.line {
p.newlines(pos) p.newlines(pos)
} else { } else {
if fallback && !p.wroteSemi { if !p.wroteSemi {
p.WriteByte(';') p.WriteByte(';')
} }
if !p.minify { if !p.minify {
@ -403,6 +405,9 @@ func (p *Printer) comment(c Comment) {
func (p *Printer) flushComments() { func (p *Printer) flushComments() {
for i, c := range p.pendingComments { for i, c := range p.pendingComments {
p.firstLine = false
// We can't call any of the newline methods, as they call this
// function and we'd recurse forever.
cline := c.Hash.Line() cline := c.Hash.Line()
switch { switch {
case i > 0, cline > p.line && p.line > 0: case i > 0, cline > p.line && p.line > 0:
@ -430,6 +435,9 @@ func (p *Printer) flushComments() {
} }
func (p *Printer) comments(cs []Comment) { func (p *Printer) comments(cs []Comment) {
if p.minify {
return
}
p.pendingComments = append(p.pendingComments, cs...) p.pendingComments = append(p.pendingComments, cs...)
} }
@ -465,12 +473,12 @@ func (p *Printer) wordPart(wp, next WordPart) {
p.wantSpace = true p.wantSpace = true
p.nestedStmts(x.StmtList, x.Right) p.nestedStmts(x.StmtList, x.Right)
p.wantSpace = false p.wantSpace = false
p.semiRsrv("}", x.Right, true) p.semiRsrv("}", x.Right)
case x.ReplyVar: case x.ReplyVar:
p.WriteString("${|") p.WriteString("${|")
p.nestedStmts(x.StmtList, x.Right) p.nestedStmts(x.StmtList, x.Right)
p.wantSpace = false p.wantSpace = false
p.semiRsrv("}", x.Right, true) p.semiRsrv("}", x.Right)
default: default:
p.WriteString("$(") p.WriteString("$(")
p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0])
@ -478,14 +486,19 @@ func (p *Printer) wordPart(wp, next WordPart) {
p.rightParen(x.Right) p.rightParen(x.Right)
} }
case *ParamExp: case *ParamExp:
nextLit, ok := next.(*Lit)
litCont := ";" litCont := ";"
if ok { if nextLit, ok := next.(*Lit); ok {
litCont = nextLit.Value[:1] litCont = nextLit.Value[:1]
} }
if p.minify && !x.Excl && !x.Length && !x.Width && name := x.Param.Value
x.Index == nil && x.Slice == nil && x.Repl == nil && switch {
x.Exp == nil && !ValidName(x.Param.Value+litCont) { case !p.minify:
case x.Excl, x.Length, x.Width:
case x.Index != nil, x.Slice != nil:
case x.Repl != nil, x.Exp != nil:
case len(name) > 1 && !ValidName(name): // ${10}
case ValidName(name + litCont): // ${var}cont
default:
x2 := *x x2 := *x
x2.Short = true x2.Short = true
p.paramExp(&x2) p.paramExp(&x2)
@ -510,7 +523,7 @@ func (p *Printer) wordPart(wp, next WordPart) {
} }
p.WriteString(x.Op.String()) p.WriteString(x.Op.String())
p.nestedStmts(x.StmtList, x.Rparen) p.nestedStmts(x.StmtList, x.Rparen)
p.WriteByte(')') p.rightParen(x.Rparen)
} }
} }
@ -775,6 +788,7 @@ func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {
} }
func (p *Printer) stmt(s *Stmt) { func (p *Printer) stmt(s *Stmt) {
p.wroteSemi = false
if s.Negated { if s.Negated {
p.spacedString("!", s.Pos()) p.spacedString("!", s.Pos())
} }
@ -787,8 +801,7 @@ func (p *Printer) stmt(s *Stmt) {
if r.OpPos.Line() > p.line { if r.OpPos.Line() > p.line {
p.bslashNewl() p.bslashNewl()
} }
if p.minify && r.N == nil { if p.wantSpace {
} else if p.wantSpace {
p.spacePad(r.Pos()) p.spacePad(r.Pos())
} }
if r.N != nil { if r.N != nil {
@ -805,7 +818,6 @@ func (p *Printer) stmt(s *Stmt) {
p.pendingHdocs = append(p.pendingHdocs, r) p.pendingHdocs = append(p.pendingHdocs, r)
} }
} }
p.wroteSemi = false
switch { switch {
case s.Semicolon.IsValid() && s.Semicolon.Line() > p.line: case s.Semicolon.IsValid() && s.Semicolon.Line() > p.line:
p.bslashNewl() p.bslashNewl()
@ -839,8 +851,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
if r.Pos().After(x.Args[1].Pos()) || r.Op == Hdoc || r.Op == DashHdoc { if r.Pos().After(x.Args[1].Pos()) || r.Op == Hdoc || r.Op == DashHdoc {
break break
} }
if p.minify && r.N == nil { if p.wantSpace {
} else if p.wantSpace {
p.spacePad(r.Pos()) p.spacePad(r.Pos())
} }
if r.N != nil { if r.N != nil {
@ -860,7 +871,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
p.WriteByte('{') p.WriteByte('{')
p.wantSpace = true p.wantSpace = true
p.nestedStmts(x.StmtList, x.Rbrace) p.nestedStmts(x.StmtList, x.Rbrace)
p.semiRsrv("}", x.Rbrace, true) p.semiRsrv("}", x.Rbrace)
case *IfClause: case *IfClause:
p.ifClause(x, false) p.ifClause(x, false)
case *Subshell: case *Subshell:
@ -880,7 +891,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
p.nestedStmts(x.Cond, Pos{}) p.nestedStmts(x.Cond, Pos{})
p.semiOrNewl("do", x.DoPos) p.semiOrNewl("do", x.DoPos)
p.nestedStmts(x.Do, x.DonePos) p.nestedStmts(x.Do, x.DonePos)
p.semiRsrv("done", x.DonePos, true) p.semiRsrv("done", x.DonePos)
case *ForClause: case *ForClause:
if x.Select { if x.Select {
p.WriteString("select ") p.WriteString("select ")
@ -890,7 +901,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
p.loop(x.Loop) p.loop(x.Loop)
p.semiOrNewl("do", x.DoPos) p.semiOrNewl("do", x.DoPos)
p.nestedStmts(x.Do, x.DonePos) p.nestedStmts(x.Do, x.DonePos)
p.semiRsrv("done", x.DonePos, true) p.semiRsrv("done", x.DonePos)
case *BinaryCmd: case *BinaryCmd:
p.stmt(x.X) p.stmt(x.X)
if p.minify || x.Y.Pos().Line() <= p.line { if p.minify || x.Y.Pos().Line() <= p.line {
@ -973,6 +984,8 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
p.wantNewline = true p.wantNewline = true
} }
p.spacedToken(ci.Op.String(), ci.OpPos) p.spacedToken(ci.Op.String(), ci.OpPos)
// avoid ; directly after tokens like ;;
p.wroteSemi = true
if inlineCom != nil { if inlineCom != nil {
p.comment(*inlineCom) p.comment(*inlineCom)
} }
@ -984,7 +997,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
p.flushComments() p.flushComments()
p.decLevel() p.decLevel()
} }
p.semiRsrv("esac", x.Esac, len(x.Items) == 0) p.semiRsrv("esac", x.Esac)
case *ArithmCmd: case *ArithmCmd:
p.WriteString("((") p.WriteString("((")
if x.Unsigned { if x.Unsigned {
@ -1037,17 +1050,17 @@ func (p *Printer) ifClause(ic *IfClause, elif bool) {
p.semiOrNewl("then", ic.ThenPos) p.semiOrNewl("then", ic.ThenPos)
p.nestedStmts(ic.Then, ic.bodyEndPos()) p.nestedStmts(ic.Then, ic.bodyEndPos())
if ic.FollowedByElif() { if ic.FollowedByElif() {
p.semiRsrv("elif", ic.ElsePos, true) p.semiRsrv("elif", ic.ElsePos)
p.ifClause(ic.Else.Stmts[0].Cmd.(*IfClause), true) p.ifClause(ic.Else.Stmts[0].Cmd.(*IfClause), true)
return return
} }
if !ic.Else.empty() { if !ic.Else.empty() {
p.semiRsrv("else", ic.ElsePos, true) p.semiRsrv("else", ic.ElsePos)
p.nestedStmts(ic.Else, ic.FiPos) p.nestedStmts(ic.Else, ic.FiPos)
} else if ic.ElsePos.IsValid() { } else if ic.ElsePos.IsValid() {
p.line = ic.ElsePos.Line() p.line = ic.ElsePos.Line()
} }
p.semiRsrv("fi", ic.FiPos, true) p.semiRsrv("fi", ic.FiPos)
} }
func startsWithLparen(s *Stmt) bool { func startsWithLparen(s *Stmt) bool {
@ -1069,7 +1082,7 @@ func (p *Printer) hasInline(s *Stmt) bool {
return false return false
} }
func (p *Printer) stmts(sl StmtList) { func (p *Printer) stmtList(sl StmtList) {
sep := p.wantNewline || sep := p.wantNewline ||
(len(sl.Stmts) > 0 && sl.Stmts[0].Pos().Line() > p.line) (len(sl.Stmts) > 0 && sl.Stmts[0].Pos().Line() > p.line)
inlineIndent := 0 inlineIndent := 0
@ -1216,7 +1229,7 @@ func (p *Printer) nestedStmts(sl StmtList, closing Pos) {
// do foo; done // do foo; done
p.wantNewline = true p.wantNewline = true
} }
p.stmts(sl) p.stmtList(sl)
if closing.IsValid() { if closing.IsValid() {
p.flushComments() p.flushComments()
} }