github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/cmd/cli.go (about)

     1  package cmd
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/fatih/color"
    13  	"github.com/hashicorp/logutils"
    14  	flags "github.com/jessevdk/go-flags"
    15  
    16  	"github.com/terraform-linters/tflint/formatter"
    17  	"github.com/terraform-linters/tflint/tflint"
    18  )
    19  
    20  // Exit codes are int values that represent an exit code for a particular error.
    21  const (
    22  	ExitCodeOK    int = 0
    23  	ExitCodeError int = 1 + iota
    24  	ExitCodeIssuesFound
    25  )
    26  
    27  // CLI is the command line object
    28  type CLI struct {
    29  	// outStream and errStream are the stdout and stderr
    30  	// to write message from the CLI.
    31  	outStream, errStream io.Writer
    32  	loader               tflint.AbstractLoader
    33  	formatter            *formatter.Formatter
    34  	testMode             bool
    35  }
    36  
    37  // NewCLI returns new CLI initialized by input streams
    38  func NewCLI(outStream io.Writer, errStream io.Writer) *CLI {
    39  	return &CLI{
    40  		outStream: outStream,
    41  		errStream: errStream,
    42  	}
    43  }
    44  
    45  // Run invokes the CLI with the given arguments.
    46  func (cli *CLI) Run(args []string) int {
    47  	var opts Options
    48  	parser := flags.NewParser(&opts, flags.HelpFlag)
    49  	parser.Usage = "[OPTIONS] [FILE or DIR...]"
    50  	parser.UnknownOptionHandler = unknownOptionHandler
    51  	// Parse commandline flag
    52  	args, err := parser.ParseArgs(args)
    53  	// Set up output formatter
    54  	cli.formatter = &formatter.Formatter{
    55  		Stdout: cli.outStream,
    56  		Stderr: cli.errStream,
    57  		Format: opts.Format,
    58  	}
    59  	if opts.NoColor {
    60  		color.NoColor = true
    61  		cli.formatter.NoColor = true
    62  	}
    63  	level := os.Getenv("TFLINT_LOG")
    64  	if opts.LogLevel != "" {
    65  		level = opts.LogLevel
    66  	}
    67  	log.SetOutput(&logutils.LevelFilter{
    68  		Levels:   []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"},
    69  		MinLevel: logutils.LogLevel(strings.ToUpper(level)),
    70  		Writer:   os.Stderr,
    71  	})
    72  	log.SetFlags(log.Ltime | log.Lshortfile)
    73  
    74  	if err != nil {
    75  		if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
    76  			fmt.Fprintln(cli.outStream, err)
    77  			return ExitCodeOK
    78  		}
    79  		cli.formatter.Print(tflint.Issues{}, tflint.NewContextError("Failed to parse CLI options", err), map[string][]byte{})
    80  		return ExitCodeError
    81  	}
    82  	dir, filterFiles, err := processArgs(args[1:])
    83  	if err != nil {
    84  		cli.formatter.Print(tflint.Issues{}, tflint.NewContextError("Failed to parse CLI arguments", err), map[string][]byte{})
    85  		return ExitCodeError
    86  	}
    87  
    88  	switch {
    89  	case opts.Version:
    90  		return cli.printVersion(opts)
    91  	case opts.Langserver:
    92  		return cli.startLanguageServer(opts.Config, opts.toConfig())
    93  	default:
    94  		return cli.inspect(opts, dir, filterFiles)
    95  	}
    96  }
    97  
    98  func processArgs(args []string) (string, []string, error) {
    99  	if len(args) == 0 {
   100  		return ".", []string{}, nil
   101  	}
   102  
   103  	var dir string
   104  	filterFiles := []string{}
   105  
   106  	for _, file := range args {
   107  		fileInfo, err := os.Stat(file)
   108  		if err != nil {
   109  			if os.IsNotExist(err) {
   110  				return dir, filterFiles, fmt.Errorf("Failed to load `%s`: File not found", file)
   111  			}
   112  			return dir, filterFiles, fmt.Errorf("Failed to load `%s`: %s", file, err)
   113  		}
   114  
   115  		if fileInfo.IsDir() {
   116  			dir = file
   117  			if len(args) != 1 {
   118  				return dir, filterFiles, fmt.Errorf("Failed to load `%s`: Multiple arguments are not allowed when passing a directory", file)
   119  			}
   120  			return dir, filterFiles, nil
   121  		}
   122  
   123  		if !strings.HasSuffix(file, ".tf") && !strings.HasSuffix(file, ".tf.json") {
   124  			return dir, filterFiles, fmt.Errorf("Failed to load `%s`: File is not a target of Terraform", file)
   125  		}
   126  
   127  		fileDir := filepath.Dir(file)
   128  		if dir == "" {
   129  			dir = fileDir
   130  			filterFiles = append(filterFiles, file)
   131  		} else if fileDir == dir {
   132  			filterFiles = append(filterFiles, file)
   133  		} else {
   134  			return dir, filterFiles, fmt.Errorf("Failed to load `%s`: Multiple files in different directories are not allowed", file)
   135  		}
   136  	}
   137  
   138  	return dir, filterFiles, nil
   139  }
   140  
   141  func unknownOptionHandler(option string, arg flags.SplitArgument, args []string) ([]string, error) {
   142  	if option == "debug" {
   143  		return []string{}, errors.New("`debug` option was removed in v0.8.0. Please set `TFLINT_LOG` environment variables instead")
   144  	}
   145  	if option == "fast" {
   146  		return []string{}, errors.New("`fast` option was removed in v0.9.0. The `aws_instance_invalid_ami` rule is already fast enough")
   147  	}
   148  	if option == "error-with-issues" {
   149  		return []string{}, errors.New("`error-with-issues` option was removed in v0.9.0. The behavior is now default")
   150  	}
   151  	if option == "quiet" || option == "q" {
   152  		return []string{}, errors.New("`quiet` option was removed in v0.11.0. The behavior is now default")
   153  	}
   154  	if option == "ignore-rule" {
   155  		return []string{}, errors.New("`ignore-rule` option was removed in v0.12.0. Please use `--disable-rule` instead")
   156  	}
   157  	return []string{}, fmt.Errorf("`%s` is unknown option. Please run `tflint --help`", option)
   158  }