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:
1 parent
9fe8b06fba
commit
443367f66f
9 files changed
+1629
-98
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user