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 }