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  }