src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/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  
    15  	"src.elv.sh/pkg/logutil"
    16  )
    17  
    18  // DeprecationLevel is a global flag that controls which deprecations to show.
    19  // If its value is X, Elvish shows deprecations that should be shown for version
    20  // 0.X.
    21  var DeprecationLevel = 20
    22  
    23  // Program represents a subprogram.
    24  type Program interface {
    25  	RegisterFlags(fs *FlagSet)
    26  	// Run runs the subprogram.
    27  	Run(fds [3]*os.File, args []string) error
    28  }
    29  
    30  func usage(out io.Writer, fs *flag.FlagSet) {
    31  	fmt.Fprintln(out, "Usage: elvish [flags] [script] [args]")
    32  	fmt.Fprintln(out, "Supported flags:")
    33  	fs.SetOutput(out)
    34  	fs.PrintDefaults()
    35  }
    36  
    37  // Run parses command-line flags and runs the first applicable subprogram. It
    38  // returns the exit status of the program.
    39  func Run(fds [3]*os.File, args []string, p Program) int {
    40  	fs := flag.NewFlagSet("elvish", flag.ContinueOnError)
    41  	// Error and usage will be printed explicitly.
    42  	fs.SetOutput(io.Discard)
    43  
    44  	var log string
    45  	var help bool
    46  	fs.StringVar(&log, "log", "",
    47  		"Path to a file to write debug logs")
    48  	fs.BoolVar(&help, "help", false,
    49  		"Show usage help and quit")
    50  	fs.IntVar(&DeprecationLevel, "deprecation-level", DeprecationLevel,
    51  		"Show warnings for all features deprecated as of version 0.X")
    52  
    53  	p.RegisterFlags(&FlagSet{FlagSet: fs})
    54  
    55  	err := fs.Parse(args[1:])
    56  	if err != nil {
    57  		if err == flag.ErrHelp {
    58  			// (*flag.FlagSet).Parse returns ErrHelp when -h or -help was
    59  			// requested but *not* defined. Elvish defines -help, but not -h; so
    60  			// this means that -h has been requested. Handle this by printing
    61  			// the same message as an undefined flag.
    62  			fmt.Fprintln(fds[2], "flag provided but not defined: -h")
    63  		} else {
    64  			fmt.Fprintln(fds[2], err)
    65  		}
    66  		usage(fds[2], fs)
    67  		return 2
    68  	}
    69  
    70  	if log != "" {
    71  		err = logutil.SetOutputFile(log)
    72  		if err == nil {
    73  			defer logutil.SetOutput(io.Discard)
    74  		} else {
    75  			fmt.Fprintln(fds[2], err)
    76  		}
    77  	}
    78  
    79  	if help {
    80  		usage(fds[1], fs)
    81  		return 0
    82  	}
    83  
    84  	err = p.Run(fds, fs.Args())
    85  	if err == nil {
    86  		return 0
    87  	}
    88  	if msg := err.Error(); msg != "" {
    89  		fmt.Fprintln(fds[2], msg)
    90  	}
    91  	switch err := err.(type) {
    92  	case badUsageError:
    93  		usage(fds[2], fs)
    94  	case exitError:
    95  		return err.exit
    96  	}
    97  	return 2
    98  }
    99  
   100  // Composite returns a Program that tries each of the given programs,
   101  // terminating at the first one that doesn't return NotSuitable().
   102  func Composite(programs ...Program) Program {
   103  	return composite(programs)
   104  }
   105  
   106  type composite []Program
   107  
   108  func (cp composite) RegisterFlags(f *FlagSet) {
   109  	for _, p := range cp {
   110  		p.RegisterFlags(f)
   111  	}
   112  }
   113  
   114  func (cp composite) Run(fds [3]*os.File, args []string) error {
   115  	var cleanups []func([3]*os.File)
   116  	for _, p := range cp {
   117  		err := p.Run(fds, args)
   118  		if np, ok := err.(nextProgramError); ok {
   119  			cleanups = append(cleanups, np.cleanups...)
   120  		} else {
   121  			for i := len(cleanups) - 1; i >= 0; i-- {
   122  				cleanups[i](fds)
   123  			}
   124  			return err
   125  		}
   126  	}
   127  	// If we have reached here, all subprograms have returned ErrNextProgram
   128  	return NextProgram(cleanups...)
   129  }
   130  
   131  // NextProgram returns a special error that may be returned by [Program.Run]
   132  // that is part of a [Composite] program, indicating that the next program
   133  // should be tried. It can carry a list of cleanup functions that should be run
   134  // in reverse order before the [Composite] program finishes.
   135  func NextProgram(cleanups ...func([3]*os.File)) error { return nextProgramError{cleanups} }
   136  
   137  type nextProgramError struct{ cleanups []func([3]*os.File) }
   138  
   139  // If this error ever gets printed, it has been bubbled to [Run] when all
   140  // programs have returned this error type.
   141  func (e nextProgramError) Error() string {
   142  	return "internal error: no suitable subprogram"
   143  }
   144  
   145  // BadUsage returns a special error that may be returned by Program.Run. It
   146  // causes the main function to print out a message, the usage information and
   147  // exit with 2.
   148  func BadUsage(msg string) error { return badUsageError{msg} }
   149  
   150  type badUsageError struct{ msg string }
   151  
   152  func (e badUsageError) Error() string { return e.msg }
   153  
   154  // Exit returns a special error that may be returned by Program.Run. It causes
   155  // the main function to exit with the given code without printing any error
   156  // messages. Exit(0) returns nil.
   157  func Exit(exit int) error {
   158  	if exit == 0 {
   159  		return nil
   160  	}
   161  	return exitError{exit}
   162  }
   163  
   164  type exitError struct{ exit int }
   165  
   166  func (e exitError) Error() string { return "" }