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 }