1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
|
// Package shell is the entry point for the terminal interface of Elvish.
package shell
import (
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strconv"
"time"
"src.elv.sh/pkg/cli/term"
"src.elv.sh/pkg/daemon/daemondefs"
"src.elv.sh/pkg/env"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/logutil"
"src.elv.sh/pkg/mods"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/prog"
"src.elv.sh/pkg/sys"
"src.elv.sh/pkg/ui"
)
var logger = logutil.GetLogger("[shell] ")
// Program is the shell subprogram. It has two slightly different modes of
// operation:
//
// - When the command line argument contains a filename or "-c some-code", the
// shell is non-interactive. In this mode, it just evaluates the given file or
// code.
//
// - Otherwise, the shell is interactive, and launches a terminal [REPL]. This
// mode also initializes the storage backend, which in turn activates the
// storage daemon.
//
// To enable building a daemon-less version, the subprogram doesn't depend
// on pkg/daemon, and the caller should supply pkg/daemon.Activate in the
// ActivateDaemon field to enable functionalities. If it is nil, daemon
// functionalities are disabled.
//
// [REPL]: https://en.wikipedia.org/wiki/Read–eval–print_loop
type Program struct {
ActivateDaemon daemondefs.ActivateFunc
codeInArg bool
compileOnly bool
noRC bool
rc string
json *bool
daemonPaths *prog.DaemonPaths
}
func (p *Program) RegisterFlags(fs *prog.FlagSet) {
// script(1) (and possibly other programs) assume shells support -i
fs.Bool("i", false,
"A no-op flag, introduced for compatibility")
// termux (and possibly other programs) assume shells support -l
fs.Bool("l", false,
"A no-op flag, introduced for compatibility")
fs.BoolVar(&p.codeInArg, "c", false,
"Treat the first argument as code to execute")
fs.BoolVar(&p.compileOnly, "compileonly", false,
"Parse and compile Elvish code without executing it")
fs.BoolVar(&p.noRC, "norc", false,
"Don't read the RC file when running interactively")
fs.StringVar(&p.rc, "rc", "",
"Path to the RC file when running interactively")
p.json = fs.JSON()
if p.ActivateDaemon != nil {
p.daemonPaths = fs.DaemonPaths()
}
}
func (p *Program) Run(fds [3]*os.File, args []string) error {
cleanup1 := incSHLVL()
defer cleanup1()
cleanup2 := initSignal(fds)
defer cleanup2()
// https://no-color.org
ui.NoColor = os.Getenv(env.NO_COLOR) != ""
interactive := len(args) == 0
ev := p.makeEvaler(fds[2], interactive)
defer ev.PreExit()
if !interactive {
exit := script(
ev, fds, args, &scriptCfg{
Cmd: p.codeInArg, CompileOnly: p.compileOnly, JSON: *p.json})
return prog.Exit(exit)
}
var spawnCfg *daemondefs.SpawnConfig
if p.ActivateDaemon != nil {
var err error
spawnCfg, err = daemonPaths(p.daemonPaths)
if err != nil {
fmt.Fprintln(fds[2], "Warning:", err)
fmt.Fprintln(fds[2], "Storage daemon may not function.")
}
}
interact(ev, fds, &interactCfg{
RC: ev.EffectiveRcPath,
ActivateDaemon: p.ActivateDaemon, SpawnConfig: spawnCfg})
return nil
}
// Creates an Evaler, sets the module search directories and installs all the
// standard builtin modules.
//
// It writes a warning message to the supplied Writer if it could not initialize
// module search directories.
func (p *Program) makeEvaler(stderr io.Writer, interactive bool) *eval.Evaler {
ev := eval.NewEvaler()
var errRc error
ev.RcPath, errRc = rcPath()
switch {
case !interactive || p.noRC:
// Leave ev.ActualRcPath empty
case p.rc != "":
// Use explicit -rc flag value
var err error
ev.EffectiveRcPath, err = filepath.Abs(p.rc)
if err != nil {
fmt.Fprintln(stderr, "Warning:", err)
}
default:
if errRc == nil {
// Use default path stored in ev.RcPath
ev.EffectiveRcPath = ev.RcPath
} else {
fmt.Fprintln(stderr, "Warning:", errRc)
}
}
libs, err := libPaths()
if err != nil {
fmt.Fprintln(stderr, "Warning: resolving lib paths:", err)
} else {
ev.LibDirs = libs
}
mods.AddTo(ev)
return ev
}
// Increments the SHLVL environment variable. It returns a function to restore
// the original value of SHLVL.
func incSHLVL() func() {
oldValue, hadValue := os.LookupEnv(env.SHLVL)
i, err := strconv.Atoi(oldValue)
if err != nil {
i = 0
}
os.Setenv(env.SHLVL, strconv.Itoa(i+1))
if hadValue {
return func() { os.Setenv(env.SHLVL, oldValue) }
} else {
return func() { os.Unsetenv(env.SHLVL) }
}
}
func initSignal(fds [3]*os.File) func() {
sigCh := sys.NotifySignals()
go func() {
for sig := range sigCh {
logger.Println("signal", sig)
handleSignal(sig, fds[2])
}
}()
return func() {
signal.Stop(sigCh)
close(sigCh)
}
}
func evalInTTY(fds [3]*os.File, ev *eval.Evaler, ed editor, src parse.Source) error {
start := time.Now()
ports, cleanup := eval.PortsFromFiles(fds, ev.ValuePrefix())
defer cleanup()
restore := term.SetupForEval(fds[0], fds[1])
defer restore()
ctx, done := eval.ListenInterrupts()
err := ev.Eval(src, eval.EvalCfg{
Ports: ports, Interrupts: ctx, PutInFg: true})
done()
if ed != nil {
ed.RunAfterCommandHooks(src, time.Since(start).Seconds(), err)
}
return err
}
|