add functions, positional parameters, case and read

- functions: name() { ...; }, function name { ...; }, return, local,
  unset -f; in pipes, & and $(...) they run in a subshell that gets all
  function definitions and the positional parameters
- positional parameters $0, $1 ... ${10}, $#, "$@", "$*", $$; set by
  gosh script args, gosh -c command name args, function calls,
  source file args and set --; shift; for x without "in"
- case word in pattern|pattern) ...;; esac and { list; } groups
- read [-r] [-p prompt] [name ...] with $IFS splitting and $REPLY

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian WaltherandClaude Opus 5.5 committed 2026-10-04 17:20:59 +02:00
1 parent 9fe8b06fba
commit 443367f66f
9 files changed
+1629 -98

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
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)
}
line, literal, eof, err := readInputLine(stdin, raw, interactiveShell && terminal)
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:
}
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
}