github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/prog/prog.go (about)

     1  // Package prog provides the entry point to Elvish. Its subpackages correspond
     2  // to subprograms of Elvish.
     3  package prog
     4  
     5  // This package sets up the basic environment and calls the appropriate
     6  // "subprogram", one of the daemon, the terminal interface, or the web
     7  // interface.
     8  
     9  import (
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"runtime/pprof"
    15  
    16  	"src.elv.sh/pkg/logutil"
    17  )
    18  
    19  // Default port on which the web interface runs. The number is chosen because it
    20  // resembles "elvi".
    21  const defaultWebPort = 3171
    22  
    23  // DeprecationLevel is a global flag that controls which deprecations to show.
    24  // If its value is X, Elvish shows deprecations that should be shown for version
    25  // 0.X.
    26  var DeprecationLevel = 15
    27  
    28  // SetDeprecationLevel sets ShowDeprecations to the given value, and returns a
    29  // function to restore the old value.
    30  func SetDeprecationLevel(level int) func() {
    31  	save := DeprecationLevel
    32  	DeprecationLevel = level
    33  	return func() { DeprecationLevel = save }
    34  }
    35  
    36  // Flags keeps command-line flags.
    37  type Flags struct {
    38  	Log, CPUProfile string
    39  
    40  	Help, Version, BuildInfo, JSON bool
    41  
    42  	CodeInArg, CompileOnly, NoRc bool
    43  
    44  	Web  bool
    45  	Port int
    46  
    47  	Daemon bool
    48  	Forked int
    49  
    50  	Bin, DB, Sock string
    51  }
    52  
    53  func newFlagSet(stderr io.Writer, f *Flags) *flag.FlagSet {
    54  	fs := flag.NewFlagSet("elvish", flag.ContinueOnError)
    55  	fs.SetOutput(stderr)
    56  	fs.Usage = func() { usage(stderr, fs) }
    57  
    58  	fs.StringVar(&f.Log, "log", "", "a file to write debug log to except for the daemon")
    59  	fs.StringVar(&f.CPUProfile, "cpuprofile", "", "write cpu profile to file")
    60  
    61  	fs.BoolVar(&f.Help, "help", false, "show usage help and quit")
    62  	fs.BoolVar(&f.Version, "version", false, "show version and quit")
    63  	fs.BoolVar(&f.BuildInfo, "buildinfo", false, "show build info and quit")
    64  	fs.BoolVar(&f.JSON, "json", false, "show output in JSON. Useful with -buildinfo.")
    65  
    66  	// The `-i` option is for compatibility with POSIX shells so that programs, such as the `script`
    67  	// command, will work when asked to launch an interactive Elvish shell.
    68  	fs.Bool("i", false, "force interactive mode; currently ignored")
    69  	fs.BoolVar(&f.CodeInArg, "c", false, "take first argument as code to execute")
    70  	fs.BoolVar(&f.CompileOnly, "compileonly", false, "Parse/Compile but do not execute")
    71  	fs.BoolVar(&f.NoRc, "norc", false, "run elvish without invoking rc.elv")
    72  
    73  	fs.BoolVar(&f.Web, "web", false, "run backend of web interface")
    74  	fs.IntVar(&f.Port, "port", defaultWebPort, "the port of the web backend")
    75  
    76  	fs.BoolVar(&f.Daemon, "daemon", false, "run daemon instead of shell")
    77  
    78  	fs.StringVar(&f.Bin, "bin", "", "path to the elvish binary")
    79  	fs.StringVar(&f.DB, "db", "", "path to the database")
    80  	fs.StringVar(&f.Sock, "sock", "", "path to the daemon socket")
    81  
    82  	fs.IntVar(&DeprecationLevel, "deprecation-level", DeprecationLevel, "show warnings for all features deprecated as of version 0.X")
    83  
    84  	return fs
    85  }
    86  
    87  func usage(out io.Writer, f *flag.FlagSet) {
    88  	fmt.Fprintln(out, "Usage: elvish [flags] [script]")
    89  	fmt.Fprintln(out, "Supported flags:")
    90  	f.PrintDefaults()
    91  }
    92  
    93  // Run parses command-line flags and runs the first applicable subprogram. It
    94  // returns the exit status of the program.
    95  func Run(fds [3]*os.File, args []string, programs ...Program) int {
    96  	f := &Flags{}
    97  	fs := newFlagSet(fds[2], f)
    98  	err := fs.Parse(args[1:])
    99  	if err != nil {
   100  		// Error and usage messages are already shown.
   101  		return 2
   102  	}
   103  
   104  	// Handle flags common to all subprograms.
   105  	if f.CPUProfile != "" {
   106  		f, err := os.Create(f.CPUProfile)
   107  		if err != nil {
   108  			fmt.Fprintln(fds[2], "Warning: cannot create CPU profile:", err)
   109  			fmt.Fprintln(fds[2], "Continuing without CPU profiling.")
   110  		} else {
   111  			pprof.StartCPUProfile(f)
   112  			defer pprof.StopCPUProfile()
   113  		}
   114  	}
   115  
   116  	if f.Daemon {
   117  		// We expect our stdout file handle is open on a unique log file for the daemon to write its
   118  		// log messages. See daemon.Spawn() in pkg/daemon.
   119  		logutil.SetOutput(fds[1])
   120  	} else if f.Log != "" {
   121  		err = logutil.SetOutputFile(f.Log)
   122  		if err != nil {
   123  			fmt.Fprintln(fds[2], err)
   124  		}
   125  	}
   126  
   127  	if f.Help {
   128  		fs.SetOutput(fds[1])
   129  		usage(fds[1], fs)
   130  		return 0
   131  	}
   132  
   133  	p := findProgram(f, programs)
   134  	if p == nil {
   135  		fmt.Fprintln(fds[2], "program bug: no suitable subprogram")
   136  		return 2
   137  	}
   138  
   139  	err = p.Run(fds, f, fs.Args())
   140  	if err == nil {
   141  		return 0
   142  	}
   143  	if msg := err.Error(); msg != "" {
   144  		fmt.Fprintln(fds[2], msg)
   145  	}
   146  	switch err := err.(type) {
   147  	case badUsageError:
   148  		usage(fds[2], fs)
   149  	case exitError:
   150  		return err.exit
   151  	}
   152  	return 2
   153  }
   154  
   155  func findProgram(f *Flags, programs []Program) Program {
   156  	for _, program := range programs {
   157  		if program.ShouldRun(f) {
   158  			return program
   159  		}
   160  	}
   161  	return nil
   162  }
   163  
   164  // BadUsage returns an error that may be returned by Program.Main, which
   165  // requests the main program to print out a message, the usage information and
   166  // exit with 2.
   167  func BadUsage(msg string) error { return badUsageError{msg} }
   168  
   169  type badUsageError struct{ msg string }
   170  
   171  func (e badUsageError) Error() string { return e.msg }
   172  
   173  // Exit returns an error that may be returned by Program.Main, which requests the
   174  // main program to exit with the given code. If the exit code is 0, it returns nil.
   175  func Exit(exit int) error {
   176  	if exit == 0 {
   177  		return nil
   178  	}
   179  	return exitError{exit}
   180  }
   181  
   182  type exitError struct{ exit int }
   183  
   184  func (e exitError) Error() string { return "" }
   185  
   186  // Program represents a subprogram.
   187  type Program interface {
   188  	// ShouldRun returns whether the subprogram should run.
   189  	ShouldRun(f *Flags) bool
   190  	// Run runs the subprogram.
   191  	Run(fds [3]*os.File, f *Flags, args []string) error
   192  }