- ${#v}, ${v#p} ${v##p} ${v%p} ${v%%p}, ${v/p/r} ${v//p/r} ${v/#p/r}
${v/%p/r}, ${v:off:len}, ${v-w} ${v:=w} ${v:+w} ${v:?msg}; nested ${}
- arithmetic expansion $((...)) with the C operators, ** and assignments
- set -e, set -x, set -o pipefail (also as gosh options, passed on to
subshells); set shows variables, set -o the options
- trap for EXIT and signals; scripts with an EXIT trap run it on
SIGINT/SIGTERM/SIGHUP too
- eval, exec (also exec > file without a command), $!, wait pid
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
5.2 KiB
Go
220 lines
5.2 KiB
Go
package main
|
|
|
|
// The read builtin: read [-r] [-p prompt] [name ...] reads a line and splits
|
|
// it into fields at the characters of $IFS (default: blank, tab, newline).
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/chzyer/readline"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// builtinRead implements read. Each name gets one field, the last one the
|
|
// rest of the line; without names the whole line is stored in $REPLY. The
|
|
// status is 1 at the end of the input.
|
|
func builtinRead(args []string, stdin *os.File) error {
|
|
raw := false
|
|
prompt := ""
|
|
args = args[1:]
|
|
for len(args) > 0 && strings.HasPrefix(args[0], "-") && args[0] != "-" {
|
|
opt := args[0]
|
|
args = args[1:]
|
|
if opt == "--" {
|
|
break
|
|
}
|
|
for k := 1; k < len(opt); k++ {
|
|
switch opt[k] {
|
|
case 'r':
|
|
raw = true
|
|
case 'p':
|
|
switch {
|
|
case k+1 < len(opt):
|
|
prompt = opt[k+1:]
|
|
case len(args) > 0:
|
|
prompt, args = args[0], args[1:]
|
|
default:
|
|
return errors.New("read: -p: option requires an argument")
|
|
}
|
|
k = len(opt)
|
|
default:
|
|
return fmt.Errorf("read: -%c: invalid option\nusage: read [-r] [-p prompt] [name ...]", opt[k])
|
|
}
|
|
}
|
|
}
|
|
for _, name := range args {
|
|
if !isVarName(name) {
|
|
return fmt.Errorf("read: `%s': not a valid identifier", sanitize(name))
|
|
}
|
|
}
|
|
|
|
terminal := readline.IsTerminal(int(stdin.Fd()))
|
|
if prompt != "" && terminal {
|
|
fmt.Fprint(stdio(2), prompt)
|
|
}
|
|
// Ctrl+C or a trapped signal ends a read from the terminal
|
|
line, literal, eof, err := readInputLine(stdin, raw, terminal && (interactiveShell || len(trapChans) > 0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
os.Setenv("REPLY", line)
|
|
} else {
|
|
ifs, ok := os.LookupEnv("IFS")
|
|
if !ok {
|
|
ifs = " \t\n"
|
|
}
|
|
fields := splitFields(line, literal, ifs, len(args))
|
|
for i, name := range args {
|
|
value := ""
|
|
if i < len(fields) {
|
|
value = fields[i]
|
|
}
|
|
os.Setenv(name, value)
|
|
}
|
|
}
|
|
if eof {
|
|
return statusError(1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readInputLine reads a line from f, byte by byte so that nothing after the
|
|
// line is consumed. Unless raw is set, a backslash escapes the next
|
|
// character (literal marks the escaped characters, which never split
|
|
// fields) and backslash-newline continues the line. eof reports that the
|
|
// input ended before a newline. If interruptible is set, Ctrl+C aborts the
|
|
// read with status 130.
|
|
func readInputLine(f *os.File, raw, interruptible bool) (line string, literal []bool, eof bool, err error) {
|
|
readByte := plainByteReader(f)
|
|
if interruptible {
|
|
intr := make(chan os.Signal, 1)
|
|
signal.Notify(intr, os.Interrupt)
|
|
defer signal.Stop(intr)
|
|
readByte = interruptibleByteReader(f, intr)
|
|
}
|
|
|
|
var b []byte
|
|
for {
|
|
c, err := readByte()
|
|
if err == io.EOF {
|
|
return string(b), literal, true, nil
|
|
}
|
|
if err != nil {
|
|
return "", nil, false, err
|
|
}
|
|
if c == '\n' {
|
|
return string(b), literal, false, nil
|
|
}
|
|
if c == '\\' && !raw {
|
|
c, err = readByte()
|
|
if err == io.EOF {
|
|
return string(b), literal, true, nil
|
|
}
|
|
if err != nil {
|
|
return "", nil, false, err
|
|
}
|
|
if c == '\n' {
|
|
continue // the line goes on
|
|
}
|
|
b = append(b, c)
|
|
literal = append(literal, true)
|
|
continue
|
|
}
|
|
b = append(b, c)
|
|
literal = append(literal, false)
|
|
}
|
|
}
|
|
|
|
func plainByteReader(f *os.File) func() (byte, error) {
|
|
buf := make([]byte, 1)
|
|
return func() (byte, error) {
|
|
for {
|
|
n, err := f.Read(buf)
|
|
if n == 1 {
|
|
return buf[0], nil
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// interruptibleByteReader reads from the terminal f only when input is
|
|
// available, so that a SIGINT on intr can end the read in between.
|
|
func interruptibleByteReader(f *os.File, intr chan os.Signal) func() (byte, error) {
|
|
next := plainByteReader(f)
|
|
fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}}
|
|
return func() (byte, error) {
|
|
for {
|
|
select {
|
|
case <-intr:
|
|
fmt.Fprintln(stdio(2))
|
|
return 0, statusError(128 + int(syscall.SIGINT))
|
|
default:
|
|
}
|
|
if hasPendingSignal() {
|
|
return 0, statusError(128 + int(syscall.SIGINT))
|
|
}
|
|
n, err := unix.Poll(fds, 100)
|
|
if err != nil && !errors.Is(err, unix.EINTR) {
|
|
return 0, err
|
|
}
|
|
if n > 0 {
|
|
return next()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// splitFields splits line into at most n fields like read does: IFS
|
|
// whitespace (blank, tab, newline in ifs) around fields is removed, other IFS
|
|
// characters separate fields (also empty ones). The last field is the rest of
|
|
// the line. Characters marked in literal are never separators.
|
|
func splitFields(line string, literal []bool, ifs string, n int) []string {
|
|
isSep := func(i int) bool { return !literal[i] && strings.IndexByte(ifs, line[i]) >= 0 }
|
|
isSpace := func(i int) bool { return isSep(i) && strings.IndexByte(" \t\n", line[i]) >= 0 }
|
|
|
|
start, end := 0, len(line)
|
|
for start < end && isSpace(start) {
|
|
start++
|
|
}
|
|
for end > start && isSpace(end-1) {
|
|
end--
|
|
}
|
|
|
|
var fields []string
|
|
i := start
|
|
for i < end && len(fields) < n-1 {
|
|
j := i
|
|
for j < end && !isSep(j) {
|
|
j++
|
|
}
|
|
fields = append(fields, line[i:j])
|
|
// The separator: whitespace, at most one other IFS character,
|
|
// whitespace
|
|
for j < end && isSpace(j) {
|
|
j++
|
|
}
|
|
if j < end && isSep(j) && !isSpace(j) {
|
|
j++
|
|
for j < end && isSpace(j) {
|
|
j++
|
|
}
|
|
}
|
|
i = j
|
|
}
|
|
if i < end {
|
|
fields = append(fields, line[i:end])
|
|
}
|
|
return fields
|
|
}
|