add non-interactive mode: -c, script files and commands from stdin
- gosh -c 'command' runs the command and exits, so gosh works as login shell for ssh host command, scp and rsync - gosh script runs a script file, lines starting with # are skipped - without a terminal, commands are read from stdin (byte by byte, so started commands can read the rest of stdin) - the exit status is the one of the last command (127 not found, 126 not executable, 128+n killed by signal n) - -i forces interactive mode, -l/--login is accepted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f51680f83c
commit
b602e17622
3 files changed
+252
-7
No files matched your search
@@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -611,15 +614,151 @@ func completeFilePath(line, prefix string) []string {
|
||||
return matches
|
||||
}
|
||||
|
||||
const usage = `usage: gosh [-l] [-i] [script]
|
||||
gosh [-l] -c command
|
||||
gosh -version`
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "-version", "--version", "-V":
|
||||
os.Exit(run(os.Args[1:]))
|
||||
}
|
||||
|
||||
// run parses the command line and runs gosh in the matching mode. It returns
|
||||
// the exit status of the shell.
|
||||
func run(args []string) int {
|
||||
// -l/--login is accepted for compatibility (e.g. "su -l", login(1)), but
|
||||
// gosh does not read a startup file yet. -i forces interactive mode even
|
||||
// if stdin is not a terminal.
|
||||
forceInteractive := false
|
||||
flags:
|
||||
for len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "-l", "--login":
|
||||
case "-i":
|
||||
forceInteractive = true
|
||||
default:
|
||||
break flags
|
||||
}
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
switch arg := args[0]; {
|
||||
case arg == "-version" || arg == "--version" || arg == "-V":
|
||||
fmt.Println("gosh", versionInfo())
|
||||
return
|
||||
return 0
|
||||
case arg == "-h" || arg == "--help":
|
||||
fmt.Println(usage)
|
||||
return 0
|
||||
case arg == "-c":
|
||||
// Like sh, further arguments are allowed (they would become $0,
|
||||
// $1, ...), but gosh has no positional parameters yet.
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "gosh: -c: option requires an argument")
|
||||
fmt.Fprintln(os.Stderr, usage)
|
||||
return 2
|
||||
}
|
||||
return runScript(strings.NewReader(args[1]))
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
fmt.Fprintf(os.Stderr, "gosh: %s: invalid option\n", arg)
|
||||
fmt.Fprintln(os.Stderr, usage)
|
||||
return 2
|
||||
case arg == "-":
|
||||
return runScript(os.Stdin)
|
||||
default:
|
||||
f, err := os.Open(arg)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gosh:", err)
|
||||
return 127
|
||||
}
|
||||
defer f.Close()
|
||||
return runScript(bufio.NewReader(f))
|
||||
}
|
||||
}
|
||||
|
||||
// Without a terminal (e.g. "echo ls | gosh") read the commands from stdin.
|
||||
if !forceInteractive && !readline.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return runScript(os.Stdin)
|
||||
}
|
||||
|
||||
interactive()
|
||||
return 0
|
||||
}
|
||||
|
||||
// runScript executes the commands from r line by line. Like sh, it does not
|
||||
// stop at a failing command and returns the exit status of the last one.
|
||||
// Empty lines and lines starting with # (e.g. a #!/usr/bin/gosh line) are
|
||||
// skipped.
|
||||
func runScript(r io.Reader) int {
|
||||
status := 0
|
||||
for {
|
||||
line, err := readLine(r)
|
||||
if trimmed := strings.TrimSpace(line); trimmed != "" && !strings.HasPrefix(trimmed, "#") {
|
||||
cmdErr := execInput(line)
|
||||
status = exitStatus(cmdErr)
|
||||
// A command that ran and failed reports its own errors.
|
||||
var exitErr *exec.ExitError
|
||||
if cmdErr != nil && !errors.As(cmdErr, &exitErr) {
|
||||
fmt.Fprintln(os.Stderr, "gosh:", cmdErr)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
fmt.Fprintln(os.Stderr, "gosh:", err)
|
||||
return 1
|
||||
}
|
||||
return status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readLine reads a single line from r without the trailing newline. It reads
|
||||
// byte by byte so that nothing after the line is consumed: when the script
|
||||
// comes from stdin, the commands it starts read the rest of stdin (like in
|
||||
// sh, "printf 'cat\nhello\n' | gosh" prints hello). Pass a bufio.Reader if
|
||||
// that does not matter.
|
||||
func readLine(r io.Reader) (string, error) {
|
||||
var b strings.Builder
|
||||
buf := make([]byte, 1)
|
||||
for {
|
||||
n, err := r.Read(buf)
|
||||
if n > 0 {
|
||||
if buf[0] == '\n' {
|
||||
return b.String(), nil
|
||||
}
|
||||
b.WriteByte(buf[0])
|
||||
}
|
||||
if err != nil {
|
||||
return b.String(), err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exitStatus converts the result of execInput into a shell exit status: the
|
||||
// exit code of the command, 128+n if it was killed by signal n, 127 if it was
|
||||
// not found, 126 if it could not be executed and 1 for other errors.
|
||||
func exitStatus(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok && ws.Signaled() {
|
||||
return 128 + int(ws.Signal())
|
||||
}
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
if errors.Is(err, exec.ErrNotFound) || errors.Is(err, fs.ErrNotExist) {
|
||||
return 127
|
||||
}
|
||||
if errors.Is(err, fs.ErrPermission) {
|
||||
return 126
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// interactive runs the interactive shell with line editing, history and
|
||||
// completion until the user exits.
|
||||
func interactive() {
|
||||
// Ctrl+C sends SIGINT to the whole foreground process group. Catch it so
|
||||
// that it only stops the running command and not the shell. (signal.Ignore
|
||||
// would be inherited by the child processes.)
|
||||
@@ -800,7 +939,9 @@ Features:
|
||||
(keys user, root, path; values are ANSI SGR codes, empty = no color);
|
||||
disabled with $NO_COLOR, TERM=dumb or when output is not a terminal
|
||||
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
||||
using an unset variable is an error, use ${VAR:-} for an empty default`)
|
||||
using an unset variable is an error, use ${VAR:-} for an empty default
|
||||
- Non-interactive use: gosh -c 'command', gosh script, command | gosh;
|
||||
the exit status is the one of the last command`)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user