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