Skip to content

Commit

Permalink
interp: add initial mapfile or readarray builtin
Browse files Browse the repository at this point in the history
Add an initial implementation of mapfile or readarray, this version only
supports the basic options of `-d`, `-t`, and a custom array variable
name. At present it only support splitting on ASCII delimiters.
  • Loading branch information
lollipopman committed May 18, 2022
1 parent be03afe commit 0fa8249
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 1 deletion.
85 changes: 84 additions & 1 deletion interp/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package interp

import (
"bufio"
"bytes"
"context"
"errors"
Expand All @@ -25,7 +26,7 @@ func isBuiltin(name string) bool {
"wait", "builtin", "trap", "type", "source", ".", "command",
"dirs", "pushd", "popd", "umask", "alias", "unalias",
"fg", "bg", "getopts", "eval", "test", "[", "exec",
"return", "read", "shopt":
"return", "read", "mapfile", "readarray", "shopt":
return true
}
return false
Expand Down Expand Up @@ -800,13 +801,95 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
return 2
}
}

case "readarray", "mapfile":
dropDelim := false
delim := "\n"
arrayName := "MAPFILE"
fp := flagParser{remaining: args}
for fp.more() {
switch flag := fp.flag(); flag {
case "-t":
// Remove the delim from each line read
dropDelim = true
case "-d":
if len(fp.remaining) == 0 {
r.errf("%s: -d: option requires an argument\n", name)
return 2
}
delim = fp.value()
if delim == "" {
// Bash sets the delim to an ASCII NUL if provided with an empty
// string.
delim = "\x00"
}
default:
r.errf("%s: invalid option %q\n", name, flag)
return 2
}
}

args := fp.args()

if len(args) > 1 {
r.errf("%s: Only one array name may be specified, %v\n", name, args)
return 2
}

if len(args) == 1 {
if !syntax.ValidName(args[0]) {
r.errf("%s: invalid identifier %q\n", name, args[0])
return 2
}
arrayName = args[0]
}

var vr expand.Variable
vr.Kind = expand.Indexed
scanner := bufio.NewScanner(r.stdin)
scanner.Split(mapfileSplit(delim[0], dropDelim))
for scanner.Scan() {
vr.List = append(vr.List, scanner.Text())
}
if err := scanner.Err(); err != nil {
r.errf("%s: unable to read, %v", name, err)
return 2
}
r.setVarInternal(arrayName, vr)

return 0

default:
// "umask", "fg", "bg",
panic(fmt.Sprintf("unhandled builtin: %s", name))
}
return 0
}

// Returns a suitable Split function for a bufio.Scanner, the code is mostly
// stolen from bufio.ScanLines
func mapfileSplit(delim byte, dropDelim bool) func(data []byte, atEOF bool) (advance int, token []byte, err error) {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, delim); i >= 0 {
// We have a full newline-terminated line.
if dropDelim {
return i + 1, data[0:i], nil
} else {
return i + 1, data[0 : i+1], nil
}
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
}

func (r *Runner) printOptLine(name string, enabled bool) {
status := "off"
if enabled {
Expand Down
22 changes: 22 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2832,6 +2832,23 @@ set +o pipefail
"a() { while getopts abc: opt; do echo $opt $OPTARG; done }; a -a -b -c arg",
"a\nb\nc arg\n",
},
// mapfile
{
"mapfile <<EOF\na\nb\nc\nEOF\n" + `for x in "${MAPFILE[@]}"; do echo "$x"; done`,
"a\n\nb\n\nc\n\n",
},
{
"mapfile -t <<EOF\na\nb\nc\nEOF\n" + `for x in "${MAPFILE[@]}"; do echo "$x"; done`,
"a\nb\nc\n",
},
{
"mapfile -t -d b <<EOF\nabc\nEOF\n" + `for x in "${MAPFILE[@]}"; do echo "$x"; done`,
"a\nc\n\n",
},
{
"mapfile -t butter <<EOF\na\nb\nc\nEOF\n" + `for x in "${butter[@]}"; do echo "$x"; done`,
"a\nb\nc\n",
},
}

var runTestsUnix = []runTest{
Expand Down Expand Up @@ -3186,6 +3203,11 @@ hello, world
`+ () { echo "$@"; }; + foo_interp_missing; @ () { echo "$@"; }; @ lala; ? () { echo "$@"; }; ? bar_interp_missing`,
"foo_interp_missing\nlala\nbar_interp_missing\n",
},
// mapfile, no process substitution yet on Windows
{
`mapfile -t -d "" < <(printf "a\0b\n"); for x in "${MAPFILE[@]}"; do echo "$x"; done`,
"a\nb\n\n",
},
}

var runTestsWindows = []runTest{
Expand Down

0 comments on commit 0fa8249

Please sign in to comment.