github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/file/commands.go (about)

     1  package file
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"strings"
     9  
    10  	"github.com/spf13/cobra"
    11  )
    12  
    13  // CommandsFile is data structure representing the text file with commands
    14  // (flags and arguments) on each line. Lines trigger independent invocations
    15  // of given chifra subcommand.
    16  type CommandsFile struct {
    17  	Lines []CommandFileLine
    18  }
    19  
    20  // CommandFileLine stores line number for future reference, flag (usually a string
    21  // that starts with "-" or "--") and args (any other string) information
    22  type CommandFileLine struct {
    23  	LineNumber uint
    24  	Flags      []string
    25  	Args       []string
    26  }
    27  
    28  // ParseCommandsFile parses a text file into `CommandsFile` struct. While parsing, the function validates flags
    29  // present on the current line.
    30  func ParseCommandsFile(cmd *cobra.Command, filePath string) (cf CommandsFile, err error) {
    31  	// TODO: parallelize
    32  	inputFile, err := os.OpenFile(filePath, os.O_RDONLY, 0)
    33  	if err != nil {
    34  		return
    35  	}
    36  
    37  	// scanner to get file lines
    38  	scanner := bufio.NewScanner(inputFile)
    39  	// we will use a simple counter to keep track of line numbers for improved error reporting
    40  	lineNumber := uint(0)
    41  	// this helper function reduce repeated code
    42  	reportErrWithLineNumber := func(err error, lineNumber uint) error {
    43  		return fmt.Errorf("on line %d: %s", lineNumber, err)
    44  	}
    45  	for scanner.Scan() {
    46  		trimmed := strings.TrimSpace(scanner.Text())
    47  		lineNumber++
    48  
    49  		// ignore comments and empty lines
    50  		if len(trimmed) == 0 || trimmed[0] == '#' || trimmed[0] == ';' {
    51  			continue
    52  		}
    53  
    54  		// --file inside file is forbidden
    55  		if strings.Contains(trimmed, "--file") {
    56  			err = reportErrWithLineNumber(errors.New("file uses --file flag recursively"), lineNumber)
    57  			return
    58  		}
    59  
    60  		// remove unwanted whitespace including duplicate spaces, etc.
    61  		trimmed = strings.Join(strings.Fields(trimmed), " ")
    62  
    63  		// both cobra and pflags packages expect their parameters to be slices of strings
    64  		tokens := strings.Split(trimmed, " ")
    65  		// validate flags (we assume that `Flags()` returns `FlagSet` with both local and
    66  		// global flags defined)
    67  		err = cmd.ParseFlags(tokens)
    68  		if err != nil {
    69  			err = reportErrWithLineNumber(err, lineNumber)
    70  			return
    71  		}
    72  		// line has been parsed successfully
    73  		cf.Lines = append(cf.Lines, CommandFileLine{
    74  			LineNumber: uint(lineNumber),
    75  			Flags:      tokens,
    76  			Args:       cmd.Flags().Args(),
    77  		})
    78  	}
    79  	if err = scanner.Err(); err != nil {
    80  		err = reportErrWithLineNumber(err, lineNumber)
    81  		return
    82  	}
    83  
    84  	return
    85  }
    86  
    87  // RunWithFileSupport returns a function to run Cobra command. The command runs in the usual
    88  // way unless `--file` is specified. If it is specified, this function will parse the file
    89  // and then run the command in series of independent calls (just like calling `chifra`
    90  // N times on the command line, but without wasting time and resources for the startup)
    91  func RunWithFileSupport(
    92  	mode string,
    93  	run func(cmd *cobra.Command, args []string) error,
    94  	resetOptions func(testMode bool),
    95  ) func(cmd *cobra.Command, args []string) error {
    96  
    97  	return func(cmd *cobra.Command, args []string) error {
    98  		// try to open the file
    99  		filePath, err := cmd.Flags().GetString("file")
   100  		if err != nil {
   101  			return err
   102  		}
   103  
   104  		// TODO: see issue #2444 - probably better ways to do this
   105  		forced := map[string]bool{
   106  			"monitors": true,
   107  		}
   108  		if filePath == "" || forced[mode] {
   109  			// `--file` has not been provided, run the command as usual
   110  			return run(cmd, args)
   111  		}
   112  
   113  		// TODO: see issue #2444 - probably better ways to do this
   114  		disallowed := map[string]bool{
   115  			"init": true,
   116  		}
   117  		if disallowed[mode] {
   118  			msg := fmt.Sprintf("The --file option is not allowed in %s mode.", mode)
   119  			return errors.New(msg)
   120  		}
   121  
   122  		// parse commands file
   123  		commandsFile, err := ParseCommandsFile(cmd, filePath)
   124  		if err != nil {
   125  			return err
   126  		}
   127  
   128  		testMode := IsTestMode()
   129  		for _, line := range commandsFile.Lines {
   130  			resetOptions(testMode)
   131  			// first, parse flags from the command line
   132  			_ = cmd.ParseFlags(os.Args[1:])
   133  			// next, parse flags from the file
   134  			err = cmd.ParseFlags(line.Flags)
   135  			if err != nil {
   136  				return err
   137  			}
   138  			// build arguments using both ones from command line and the file
   139  			var callArgs []string
   140  			callArgs = append(callArgs, args...)
   141  			callArgs = append(callArgs, line.Args...)
   142  			err = run(cmd, callArgs)
   143  			if err != nil {
   144  				return err
   145  			}
   146  		}
   147  		return nil
   148  	}
   149  }
   150  
   151  func IsTestMode() bool {
   152  	return os.Getenv("TEST_MODE") == "true"
   153  }