1
0
mirror of https://github.com/go-task/task.git synced 2025-05-31 23:19:42 +02:00

Merge branch 'master' into v3

This commit is contained in:
Andrey Nering 2020-03-28 10:48:49 -03:00
commit acfbbaa549
24 changed files with 132 additions and 536 deletions

26
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: goreleaser
on:
push:
tags:
- '*'
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v1
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@ -5,7 +5,7 @@ jobs:
name: Test
strategy:
matrix:
go-version: [1.12.x, 1.13.x]
go-version: [1.13.x, 1.14.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}}
steps:

View File

@ -1,17 +0,0 @@
language: go
go:
- 1.12.x
- 1.13.x
env:
- GO111MODULE=on
addons:
apt:
packages:
- rpm
script:
- go install -v ./cmd/task
- task ci

View File

@ -1,421 +0,0 @@
// This small web app is used to redirect from the old taskfile.org domain
// to the new taskfile.dev without breaking CIs that uses cURL to download
// "/install.sh" without the -L flag (which follow redirects).
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/install.sh" {
println("Dumping install.sh")
w.Write(installShContent)
return
}
println("Redirecting to https://taskfile.dev" + r.URL.Path)
w.Header().Set("Location", "https://taskfile.dev"+r.URL.Path)
w.WriteHeader(301)
})
println("Listening :8080")
panic(http.ListenAndServe(":8080", nil))
}
var installShContent = []byte(`#!/bin/sh
set -e
# Code generated by godownloader on 2018-04-07T17:47:38Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for go-task/task
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/go-task/task/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktmpdir)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
install -d "${BINDIR}"
for binexe in "task" ; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
}
is_supported_platform() {
platform=$1
found=1
case "$platform" in
windows/386) found=0 ;;
windows/amd64) found=0 ;;
darwin/386) found=0 ;;
darwin/amd64) found=0 ;;
linux/386) found=0 ;;
linux/amd64) found=0 ;;
esac
case "$platform" in
darwin/386) found=1 ;;
esac
return $found
}
check_platform() {
if is_supported_platform "$PLATFORM"; then
# optional logging goes here
true
else
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
fi
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on ARCH
case ${ARCH} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
true
}
adjust_arch() {
# adjust archive name based on ARCH
true
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
log_prefix() {
echo "$0"
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
msys_nt) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="arm5" ;;
armv6*) arch="arm6" ;;
armv7*) arch="arm7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
windows) return 0 ;;
esac
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar -xzf "${tarball}" ;;
*.tar) tar -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
mktmpdir() {
test -z "$TMPDIR" && TMPDIR="$(mktemp -d)"
mkdir -p "${TMPDIR}"
echo "${TMPDIR}"
}
http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
else
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
fi
if [ "$code" != "200" ]; then
log_debug "http_download_curl received HTTP status $code"
return 1
fi
return 0
}
http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
elif is_command wget; then
http_download_wget "$@"
return
fi
log_crit "http_download unable to find wget or curl"
return 1
}
http_copy() {
tmp=$(mktemp)
http_download "${tmp}" "$1" "$2" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_sha256_verify() {
TARGET=$1
checksums=$2
if [ -z "$checksums" ]; then
log_err "hash_sha256_verify checksum file not specified in arg2"
return 1
fi
BASENAME=${TARGET##*/}
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
return 1
fi
got=$(hash_sha256 "$TARGET")
if [ "$want" != "$got" ]; then
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
PROJECT_NAME="task"
OWNER=go-task
REPO="task"
BINARY=task
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
uname_os_check "$OS"
uname_arch_check "$ARCH"
parse_args "$@"
check_platform
tag_to_version
adjust_format
adjust_os
adjust_arch
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
NAME=${BINARY}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=task_checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute
`)

View File

@ -6,7 +6,7 @@ _task_completion()
# Remove colon from word breaks
COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
scripts=$(task -l | sed '1d' | sed 's/^\* //' | awk '{ print $1 }');
scripts=$(task -l | sed '1d' | awk '{ print $2 }' | sed 's/:$//');
curr_arg="${COMP_WORDS[COMP_CWORD]:-"."}"

10
completion/ps/task.ps1 Normal file
View File

@ -0,0 +1,10 @@
$scriptBlock = {
param($commandName, $wordToComplete, $cursorPosition)
$curReg = "task{.exe}? (.*?)$"
$startsWith = $wordToComplete | Select-String $curReg -AllMatches | ForEach-Object { $_.Matches.Groups[1].Value }
$reg = "\* ($startsWith.+?):"
$listOutput = $(task -l)
$listOutput | Select-String $reg -AllMatches | ForEach-Object { $_.Matches.Groups[1].Value + " " }
}
Register-ArgumentCompleter -Native -CommandName task -ScriptBlock $scriptBlock

View File

@ -40,6 +40,19 @@ scoop install task
This installation method is community owned. After a new release of Task, it
may take some time until it's available on Scoop.
## Arch Linux
If you're on Arch Linux you can install Task from
[AUR](https://aur.archlinux.org/packages/taskfile-git) using your favorite
package manager such as `yay`, `pacaur` or `yaourt`:
```cmd
yay -S taskfile-git
```
This installation method is community owned, but since it's `-git` version of
the package, it's always latest available version based on the Git repository.
## Go
Task now uses [Go Modules](https://github.com/golang/go/wiki/Modules), which

2
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/stretchr/testify v1.4.0
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
gopkg.in/yaml.v2 v2.2.2
mvdan.cc/sh/v3 v3.0.0-beta1
mvdan.cc/sh/v3 v3.0.2
)
go 1.13

4
go.sum
View File

@ -53,5 +53,5 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
mvdan.cc/editorconfig v0.1.1-0.20191109213504-890940e3f00e/go.mod h1:Ge4atmRUYqueGppvJ7JNrtqpqokoJEFxYbP0Z+WeKS8=
mvdan.cc/sh/v3 v3.0.0-beta1 h1:UqiwBEXEPzelaGxuvixaOtzc7WzKtrElePJ8HqvW7K8=
mvdan.cc/sh/v3 v3.0.0-beta1/go.mod h1:rBIndNJFYPp8xSppiZcGIk6B5d1g3OEARxEaXjPxwVI=
mvdan.cc/sh/v3 v3.0.2 h1:NU+UpTZHRdIZ9igRo8zi/7+5/NpetYlhlyDhz1/AdMM=
mvdan.cc/sh/v3 v3.0.2/go.mod h1:rBIndNJFYPp8xSppiZcGIk6B5d1g3OEARxEaXjPxwVI=

View File

@ -30,6 +30,10 @@ func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) {
tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
for _, task := range e.Taskfile.Tasks {
if task.Desc != "" {
compiledTask, err := e.CompiledTask(taskfile.Call{Task: task.Task})
if err == nil {
task = compiledTask
}
tasks = append(tasks, task)
}
}

View File

@ -9,12 +9,12 @@ import (
func PrintTasks(l *logger.Logger, t *taskfile.Taskfile, c []taskfile.Call) {
for i, call := range c {
printSpaceBetweenSummaries(l, i)
PrintSpaceBetweenSummaries(l, i)
PrintTask(l, t.Tasks[call.Task])
}
}
func printSpaceBetweenSummaries(l *logger.Logger, i int) {
func PrintSpaceBetweenSummaries(l *logger.Logger, i int) {
spaceRequired := i > 0
if !spaceRequired {
return

View File

@ -70,7 +70,14 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
}
if e.Summary {
summary.PrintTasks(e.Logger, e.Taskfile, calls)
for i, c := range calls {
compiledTask, err := e.CompiledTask(c)
if err != nil {
return nil
}
summary.PrintSpaceBetweenSummaries(e.Logger, i)
summary.PrintTask(e.Logger, compiledTask)
}
return nil
}

View File

@ -28,6 +28,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
new := taskfile.Task{
Task: origTask.Task,
Desc: r.Replace(origTask.Desc),
Summary: r.Replace(origTask.Summary),
Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates),
Dir: r.Replace(origTask.Dir),

2
vendor/modules.txt vendored
View File

@ -37,7 +37,7 @@ golang.org/x/xerrors
golang.org/x/xerrors/internal
# gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2
# mvdan.cc/sh/v3 v3.0.0-beta1
# mvdan.cc/sh/v3 v3.0.2
mvdan.cc/sh/v3/expand
mvdan.cc/sh/v3/interp
mvdan.cc/sh/v3/pattern

View File

@ -24,13 +24,13 @@ func Braces(word *syntax.Word) []*syntax.Word {
continue
}
if br.Sequence {
var from, to int
if br.Chars {
chars := false
from, err1 := strconv.Atoi(br.Elems[0].Lit())
to, err2 := strconv.Atoi(br.Elems[1].Lit())
if err1 != nil || err2 != nil {
chars = true
from = int(br.Elems[0].Lit()[0])
to = int(br.Elems[1].Lit()[0])
} else {
from, _ = strconv.Atoi(br.Elems[0].Lit())
to, _ = strconv.Atoi(br.Elems[1].Lit())
}
upward := from <= to
incr := 1
@ -54,7 +54,7 @@ func Braces(word *syntax.Word) []*syntax.Word {
next := *word
next.Parts = next.Parts[i+1:]
lit := &syntax.Lit{}
if br.Chars {
if chars {
lit.Value = string(n)
} else {
lit.Value = strconv.Itoa(n)

View File

@ -159,6 +159,8 @@ func Document(cfg *Config, word *syntax.Word) (string, error) {
return cfg.fieldJoin(field), nil
}
const patMode = pattern.Filenames | pattern.Braces
// Pattern expands a single shell word as a pattern, using syntax.QuotePattern
// on any non-quoted parts of the input word. The result can be used on
// syntax.TranslatePattern directly.
@ -174,7 +176,7 @@ func Pattern(cfg *Config, word *syntax.Word) (string, error) {
buf := cfg.strBuilder()
for _, part := range field {
if part.quote > quoteNone {
buf.WriteString(pattern.QuoteMeta(part.val))
buf.WriteString(pattern.QuoteMeta(part.val, patMode))
} else {
buf.WriteString(part.val)
}
@ -345,11 +347,11 @@ func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob boo
buf := cfg.strBuilder()
for _, part := range parts {
if part.quote > quoteNone {
buf.WriteString(pattern.QuoteMeta(part.val))
buf.WriteString(pattern.QuoteMeta(part.val, patMode))
continue
}
buf.WriteString(part.val)
if pattern.HasMeta(part.val) {
if pattern.HasMeta(part.val, patMode) {
glob = true
}
}
@ -367,9 +369,10 @@ func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
fields := make([]string, 0, len(words))
dir := cfg.envGet("PWD")
for _, word := range words {
afterBraces := []*syntax.Word{word}
if syntax.SplitBraces(word) {
afterBraces = Braces(word)
word := *word // make a copy, since SplitBraces replaces the Parts slice
afterBraces := []*syntax.Word{&word}
if syntax.SplitBraces(&word) {
afterBraces = Braces(&word)
}
for _, word2 := range afterBraces {
wfields, err := cfg.wordFields(word2.Parts)
@ -540,7 +543,6 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
}
curField = append(curField, fp)
case *syntax.DblQuoted:
allowEmpty = true
if len(x.Parts) == 1 {
pe, _ := x.Parts[0].(*syntax.ParamExp)
if elems := cfg.quotedElems(pe); elems != nil {
@ -556,6 +558,7 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
continue
}
}
allowEmpty = true
wfield, err := cfg.wordField(x.Parts, quoteDouble)
if err != nil {
return nil, err

View File

@ -745,7 +745,6 @@ func (r *Runner) sub() *Runner {
Env: r.Env,
Dir: r.Dir,
Params: r.Params,
Funcs: r.Funcs,
execHandler: r.execHandler,
openHandler: r.openHandler,
stdin: r.stdin,
@ -766,6 +765,10 @@ func (r *Runner) sub() *Runner {
for k, v := range r.cmdVars {
r2.cmdVars[k] = v
}
r2.Funcs = make(map[string]*syntax.Stmt, len(r.Funcs))
for k, v := range r.Funcs {
r2.Funcs[k] = v
}
r2.dirStack = append(r2.dirBootstrap[:0], r.dirStack...)
r2.fillExpandConfig(r.ectx)
r2.didReset = true

View File

@ -16,8 +16,8 @@ import (
"strings"
)
// TODO: support Mode in the other APIs too
// Mode can be used to supply a number of options to the package's functions.
// Not all functions change their behavior with all of the options below.
type Mode uint
const (
@ -254,13 +254,17 @@ func charClass(s string) (string, error) {
// This can be useful to avoid extra work, like TranslatePattern. Note that this
// function cannot be used to avoid QuotePattern, as backslashes are quoted by
// that function but ignored here.
func HasMeta(pat string) bool {
func HasMeta(pat string, mode Mode) bool {
for i := 0; i < len(pat); i++ {
switch pat[i] {
case '\\':
i++
case '*', '?', '[':
return true
case '{':
if mode&Braces != 0 {
return true
}
}
}
return false
@ -270,11 +274,16 @@ func HasMeta(pat string) bool {
// given text. The returned string is a pattern that matches the literal text.
//
// For example, QuoteMeta(`foo*bar?`) returns `foo\*bar\?`.
func QuoteMeta(pat string) string {
func QuoteMeta(pat string, mode Mode) string {
any := false
loop:
for _, r := range pat {
switch r {
case '{':
if mode&Braces == 0 {
continue
}
fallthrough
case '*', '?', '[', '\\':
any = true
break loop
@ -288,6 +297,10 @@ loop:
switch r {
case '*', '?', '[', '\\':
buf.WriteByte('\\')
case '{':
if mode&Braces != 0 {
buf.WriteByte('\\')
}
}
buf.WriteRune(r)
}

View File

@ -1,57 +0,0 @@
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package shell
import (
"context"
"fmt"
"os"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
// SourceFile sources a shell file from disk and returns the variables
// declared in it. It is a convenience function that uses a default shell
// parser, parses a file from disk, and calls SourceNode.
//
// This function should be used with caution, as it can interpret arbitrary
// code. Untrusted shell programs shoudn't be sourced outside of a sandbox
// environment.
func SourceFile(ctx context.Context, path string) (map[string]expand.Variable, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("could not open: %v", err)
}
defer f.Close()
file, err := syntax.NewParser().Parse(f, path)
if err != nil {
return nil, fmt.Errorf("could not parse: %v", err)
}
return SourceNode(ctx, file)
}
// SourceNode sources a shell program from a node and returns the
// variables declared in it. It accepts the same set of node types that
// interp/Runner.Run does.
//
// This function should be used with caution, as it can interpret arbitrary
// code. Untrusted shell programs shoudn't be sourced outside of a sandbox
// environment.
func SourceNode(ctx context.Context, node syntax.Node) (map[string]expand.Variable, error) {
r, _ := interp.New()
if err := r.Run(ctx, node); err != nil {
return nil, fmt.Errorf("could not run: %v", err)
}
// delete the internal shell vars that the user is not
// interested in
delete(r.Vars, "PWD")
delete(r.Vars, "UID")
delete(r.Vars, "HOME")
delete(r.Vars, "PATH")
delete(r.Vars, "IFS")
delete(r.Vars, "OPTIND")
return r.Vars, nil
}

View File

@ -129,7 +129,6 @@ func SplitBraces(word *Word) bool {
broken = true
}
if !broken {
br.Chars = chars[0]
acc.Parts = append(acc.Parts, br)
break
}

View File

@ -256,6 +256,7 @@ skipSpace:
for r != '\n' && r != utf8.RuneSelf {
if r == escNewl {
p.litBs = append(p.litBs, '\\', '\n')
break
}
r = p.rune()
}
@ -316,6 +317,7 @@ skipSpace:
} else {
p.tok = rightParen
p.quote = noState
p.rune() // we are tokenizing manually
}
default: // including '(', '|'
p.advanceLitRe(r)

View File

@ -409,9 +409,9 @@ type Word struct {
func (w *Word) Pos() Pos { return w.Parts[0].Pos() }
func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() }
// Lit returns the word as a literal value, if the word consists of *syntax.Lit
// nodes only. An empty string is returned otherwise. Words with multiple
// literals, which can appear in some edge cases, are handled properly.
// Lit returns the word as a literal value, if the word consists of *Lit nodes
// only. An empty string is returned otherwise. Words with multiple literals,
// which can appear in some edge cases, are handled properly.
//
// For example, the word "foo" will return "foo", but the word "foo${bar}" will
// return "".
@ -859,12 +859,11 @@ type LetClause struct {
func (l *LetClause) Pos() Pos { return l.Let }
func (l *LetClause) End() Pos { return l.Exprs[len(l.Exprs)-1].End() }
// BraceExp represents a Bash brace expression, such as "{x,y}" or "{1..10}".
// BraceExp represents a Bash brace expression, such as "{a,f}" or "{1..10}".
//
// This node will only appear as a result of SplitBraces.
type BraceExp struct {
Sequence bool // {x..y[..incr]} instead of {x,y[,...]}
Chars bool // sequence is of chars, not numbers (TODO: remove)
Elems []*Word
}

View File

@ -14,7 +14,7 @@ import (
// ParserOption is a function which can be passed to NewParser
// to alter its behaviour. To apply option to existing Parser
// call it directly, for example syntax.KeepComments(true)(parser).
// call it directly, for example KeepComments(true)(parser).
type ParserOption func(*Parser)
// KeepComments makes the parser parse comments and attach them to
@ -524,9 +524,13 @@ func (p *Parser) unquotedWordPart(buf *bytes.Buffer, wp WordPart, quotes bool) (
}
func (p *Parser) doHeredocs() {
hdocs := p.heredocs[p.buriedHdocs:]
if len(hdocs) == 0 {
// Nothing do do; don't even issue a read.
return
}
p.rune() // consume '\n', since we know p.tok == _Newl
old := p.quote
hdocs := p.heredocs[p.buriedHdocs:]
p.heredocs = p.heredocs[:p.buriedHdocs]
for i, r := range hdocs {
if p.err != nil {
@ -840,7 +844,7 @@ func (p *Parser) invalidStmtStart() {
}
func (p *Parser) getWord() *Word {
if parts := p.wordParts(); len(parts) > 0 {
if parts := p.wordParts(); len(parts) > 0 && p.err == nil {
return p.word(parts)
}
return nil
@ -1546,13 +1550,15 @@ func (p *Parser) hasValidIdent() bool {
}
if end := p.eqlOffs; end > 0 {
if p.val[end-1] == '+' && p.lang != LangPOSIX {
end--
end-- // a+=x
}
if ValidName(p.val[:end]) {
return true
}
} else if !ValidName(p.val) {
return false // *[i]=x
}
return p.r == '['
return p.r == '[' // a[i]=x
}
func (p *Parser) getAssign(needEqual bool) *Assign {

View File

@ -15,7 +15,7 @@ import (
// PrinterOption is a function which can be passed to NewPrinter
// to alter its behaviour. To apply option to existing Printer
// call it directly, for example syntax.KeepPadding(true)(printer).
// call it directly, for example KeepPadding(true)(printer).
type PrinterOption func(*Printer)
// Indent sets the number of spaces used for indentation. If set to 0,
@ -414,13 +414,13 @@ func (p *Printer) flushHeredocs() {
bufWriter: &extra,
line: r.Hdoc.Pos().Line(),
}
p.tabsPrinter.word(r.Hdoc)
p.tabsPrinter.wordParts(r.Hdoc.Parts, true)
p.indent()
} else {
p.indent()
}
} else if r.Hdoc != nil {
p.word(r.Hdoc)
p.wordParts(r.Hdoc.Parts, true)
}
p.unquotedWord(r.Word)
if r.Hdoc != nil {
@ -478,6 +478,11 @@ func (p *Printer) semiRsrv(s string, pos Pos) {
func (p *Printer) flushComments() {
for i, c := range p.pendingComments {
if i == 0 {
// Flush any pending heredocs first. Otherwise, the
// comments would become part of a heredoc body.
p.flushHeredocs()
}
p.firstLine = false
// We can't call any of the newline methods, as they call this
// function and we'd recurse forever.