github.com/mithrandie/csvq@v1.18.1/lib/cli/app.go (about) 1 package cli 2 3 import ( 4 "context" 5 "log" 6 "os" 7 "os/signal" 8 "sort" 9 10 "github.com/mithrandie/csvq/lib/action" 11 "github.com/mithrandie/csvq/lib/file" 12 "github.com/mithrandie/csvq/lib/option" 13 "github.com/mithrandie/csvq/lib/parser" 14 "github.com/mithrandie/csvq/lib/query" 15 16 "github.com/urfave/cli/v2" 17 ) 18 19 func Run() { 20 action.CurrentVersion, _ = action.ParseVersion(query.Version) 21 if !action.CurrentVersion.IsEmpty() { 22 query.Version = action.CurrentVersion.String() 23 } 24 25 cli.AppHelpTemplate = appHHelpTemplate 26 cli.CommandHelpTemplate = commandHelpTemplate 27 28 app := cli.NewApp() 29 30 app.Name = "csvq" 31 app.Usage = "SQL-like query language for csv" 32 app.ArgsUsage = "[query|argument]" 33 app.Version = query.Version 34 app.Authors = []*cli.Author{{Name: "Yuki et al."}} 35 app.OnUsageError = onUsageError 36 app.Flags = []cli.Flag{ 37 &cli.StringFlag{ 38 Name: "repository", 39 Aliases: []string{"r"}, 40 Usage: "directory `PATH` where files are located", 41 }, 42 &cli.StringFlag{ 43 Name: "timezone", 44 Aliases: []string{"z"}, 45 Value: "Local", 46 Usage: "default timezone", 47 }, 48 &cli.StringFlag{ 49 Name: "datetime-format", 50 Aliases: []string{"t"}, 51 Usage: "datetime format to parse strings", 52 }, 53 &cli.BoolFlag{ 54 Name: "ansi-quotes", 55 Aliases: []string{"k"}, 56 Usage: "use double quotation mark as identifier enclosure", 57 }, 58 &cli.BoolFlag{ 59 Name: "strict-equal", 60 Aliases: []string{"g"}, 61 Usage: "compare strictly equal or not in DISTINCT, GROUP BY and ORDER BY", 62 }, 63 &cli.Float64Flag{ 64 Name: "wait-timeout", 65 Aliases: []string{"w"}, 66 Value: 10, 67 Usage: "maximum time in seconds to wait for locked files to be released", 68 }, 69 &cli.StringFlag{ 70 Name: "source", 71 Aliases: []string{"s"}, 72 Usage: "load query or statements from `FILE`", 73 }, 74 &cli.StringFlag{ 75 Name: "import-format", 76 Aliases: []string{"i"}, 77 Value: "CSV", 78 Usage: "default format to load files", 79 }, 80 &cli.StringFlag{ 81 Name: "delimiter", 82 Aliases: []string{"d"}, 83 Value: ",", 84 Usage: "field delimiter for CSV", 85 }, 86 &cli.BoolFlag{ 87 Name: "allow-uneven-fields", 88 Usage: "allow loading CSV files with uneven field length", 89 }, 90 &cli.StringFlag{ 91 Name: "delimiter-positions", 92 Aliases: []string{"m"}, 93 Usage: "delimiter positions for FIXED", 94 }, 95 &cli.StringFlag{ 96 Name: "json-query", 97 Aliases: []string{"j"}, 98 Usage: "`QUERY` for JSON", 99 }, 100 &cli.StringFlag{ 101 Name: "encoding", 102 Aliases: []string{"e"}, 103 Value: "AUTO", 104 Usage: "file encoding", 105 }, 106 &cli.BoolFlag{ 107 Name: "no-header", 108 Aliases: []string{"n"}, 109 Usage: "import the first line as a record", 110 }, 111 &cli.BoolFlag{ 112 Name: "without-null", 113 Aliases: []string{"a"}, 114 Usage: "parse empty fields as empty strings", 115 }, 116 &cli.StringFlag{ 117 Name: "out", 118 Aliases: []string{"o"}, 119 Usage: "export result sets of select queries to `FILE`", 120 }, 121 &cli.BoolFlag{ 122 Name: "strip-ending-line-break", 123 Aliases: []string{"T"}, 124 Usage: "strip line break from the end of files and query results", 125 }, 126 &cli.StringFlag{ 127 Name: "format", 128 Aliases: []string{"f"}, 129 Usage: "format of query results. (default: \"CSV\" for output to pipe, \"TEXT\" otherwise)", 130 }, 131 &cli.StringFlag{ 132 Name: "write-encoding", 133 Aliases: []string{"E"}, 134 Value: "UTF8", 135 Usage: "character encoding of query results", 136 }, 137 &cli.StringFlag{ 138 Name: "write-delimiter", 139 Aliases: []string{"D"}, 140 Value: ",", 141 Usage: "field delimiter for CSV in query results", 142 }, 143 &cli.StringFlag{ 144 Name: "write-delimiter-positions", 145 Aliases: []string{"M"}, 146 Usage: "delimiter positions for FIXED in query results", 147 }, 148 &cli.BoolFlag{ 149 Name: "without-header", 150 Aliases: []string{"N"}, 151 Usage: "export result sets of select queries without the header line", 152 }, 153 &cli.StringFlag{ 154 Name: "line-break", 155 Aliases: []string{"l"}, 156 Value: "LF", 157 Usage: "line break in query results", 158 }, 159 &cli.BoolFlag{ 160 Name: "enclose-all", 161 Aliases: []string{"Q"}, 162 Usage: "enclose all string values in CSV and TSV", 163 }, 164 &cli.StringFlag{ 165 Name: "json-escape", 166 Aliases: []string{"J"}, 167 Value: "BACKSLASH", 168 Usage: "JSON escape type", 169 }, 170 &cli.BoolFlag{ 171 Name: "pretty-print", 172 Aliases: []string{"P"}, 173 Usage: "make JSON output easier to read in query results", 174 }, 175 &cli.BoolFlag{ 176 Name: "scientific-notation", 177 Aliases: []string{"SN"}, 178 Usage: "use scientific notation for large exponents in output", 179 }, 180 &cli.BoolFlag{ 181 Name: "east-asian-encoding", 182 Aliases: []string{"W"}, 183 Usage: "count ambiguous characters as fullwidth", 184 }, 185 &cli.BoolFlag{ 186 Name: "count-diacritical-sign", 187 Aliases: []string{"S"}, 188 Usage: "count diacritical signs as halfwidth", 189 }, 190 &cli.BoolFlag{ 191 Name: "count-format-code", 192 Aliases: []string{"A"}, 193 Usage: "count format characters and zero-width spaces as halfwidth", 194 }, 195 &cli.BoolFlag{ 196 Name: "color", 197 Aliases: []string{"c"}, 198 Usage: "use ANSI color escape sequences", 199 }, 200 &cli.BoolFlag{ 201 Name: "quiet", 202 Aliases: []string{"q"}, 203 Usage: "suppress operation log output", 204 }, 205 &cli.IntFlag{ 206 Name: "limit-recursion", 207 Value: 1000, 208 Usage: "maximum number of iterations for recursive queries", 209 }, 210 &cli.IntFlag{ 211 Name: "cpu", 212 Aliases: []string{"p"}, 213 Value: option.GetDefaultNumberOfCPU(), 214 Usage: "hint for the number of cpu cores to be used", 215 }, 216 &cli.BoolFlag{ 217 Name: "stats", 218 Aliases: []string{"x"}, 219 Usage: "show execution time and memory statistics", 220 }, 221 } 222 223 app.Commands = []*cli.Command{ 224 { 225 Name: "fields", 226 Usage: "Show fields in a file", 227 ArgsUsage: "DATA_FILE_PATH", 228 Action: commandAction(func(ctx context.Context, c *cli.Context, proc *query.Processor) error { 229 if 1 != c.NArg() { 230 return query.NewIncorrectCommandUsageError("fields subcommand takes exactly 1 argument") 231 } 232 table := c.Args().First() 233 return action.ShowFields(ctx, proc, table) 234 }), 235 }, 236 { 237 Name: "calc", 238 Usage: "Calculate a value from stdin", 239 ArgsUsage: "expression", 240 Action: commandAction(func(ctx context.Context, c *cli.Context, proc *query.Processor) error { 241 if !proc.Tx.Session.CanReadStdin { 242 return query.NewIncorrectCommandUsageError(query.ErrMsgStdinEmpty) 243 } 244 if 1 != c.NArg() { 245 return query.NewIncorrectCommandUsageError("calc subcommand takes exactly 1 argument") 246 } 247 expr := c.Args().First() 248 return action.Calc(ctx, proc, expr) 249 }), 250 }, 251 { 252 Name: "syntax", 253 Usage: "Print syntax", 254 ArgsUsage: "[search_word ...]", 255 Action: commandAction(func(ctx context.Context, c *cli.Context, proc *query.Processor) error { 256 words := append([]string{c.Args().First()}, c.Args().Tail()...) 257 return action.Syntax(ctx, proc, words) 258 }), 259 }, 260 { 261 Name: "check-update", 262 Usage: "Check for updates", 263 ArgsUsage: " ", 264 Flags: []cli.Flag{ 265 &cli.BoolFlag{ 266 Name: "include-pre-release", 267 Usage: "check including pre-release version", 268 }, 269 }, 270 Action: commandAction(func(ctx context.Context, c *cli.Context, proc *query.Processor) error { 271 if 0 < c.NArg() { 272 return query.NewIncorrectCommandUsageError("check-update subcommand takes no argument") 273 } 274 275 includePreRelease := false 276 if c.IsSet("include-pre-release") { 277 includePreRelease = c.Bool("include-pre-release") 278 } 279 return action.CheckUpdate(includePreRelease) 280 }), 281 }, 282 } 283 284 for i := range app.Commands { 285 app.Commands[i].OnUsageError = onUsageError 286 } 287 288 app.Action = commandAction(func(ctx context.Context, c *cli.Context, proc *query.Processor) error { 289 queryString, path, err := readQuery(ctx, c, proc.Tx) 290 if err != nil { 291 return err 292 } 293 294 if len(queryString) < 1 { 295 err = action.LaunchInteractiveShell(ctx, proc) 296 } else { 297 err = action.Run(ctx, proc, queryString, path, c.String("out")) 298 } 299 300 return err 301 }) 302 303 sort.Sort(cli.FlagsByName(app.Flags)) 304 sort.Sort(cli.CommandsByName(app.Commands)) 305 306 if err := app.Run(os.Args); err != nil { 307 log.Fatalln(err.Error()) 308 } 309 } 310 311 func onUsageError(c *cli.Context, err error, isSubcommand bool) error { 312 if isSubcommand { 313 if e := cli.ShowCommandHelp(c, c.Command.Name); e != nil { 314 println(e.Error()) 315 } 316 } 317 if _, ok := err.(*query.IncorrectCommandUsageError); !ok { 318 err = query.NewIncorrectCommandUsageError(err.Error()) 319 } 320 return Exit(err, nil) 321 } 322 323 func commandAction(fn func(ctx context.Context, c *cli.Context, proc *query.Processor) error) func(c *cli.Context) error { 324 return func(c *cli.Context) (err error) { 325 ctx, cancel := context.WithCancel(context.Background()) 326 defer cancel() 327 328 session := query.NewSession() 329 tx, e := query.NewTransaction(ctx, file.DefaultWaitTimeout, file.DefaultRetryDelay, session) 330 if e != nil { 331 return Exit(e, nil) 332 } 333 334 proc := query.NewProcessor(tx) 335 defer func() { 336 if e := proc.AutoRollback(); e != nil { 337 proc.LogError(e.Error()) 338 } 339 if e := proc.ReleaseResourcesWithErrors(); e != nil { 340 proc.LogError(e.Error()) 341 } 342 343 if err != nil { 344 if _, ok := err.(*query.IncorrectCommandUsageError); ok { 345 err = onUsageError(c, err, 0 < len(c.Command.Name)) 346 } else { 347 err = Exit(err, proc.Tx) 348 } 349 } 350 }() 351 352 // Handle signals 353 ch := make(chan os.Signal, 1) 354 signal.Notify(ch, action.Signals...) 355 var signalReceived error 356 357 go func() { 358 sig := <-ch 359 signalReceived = query.NewSignalReceived(sig) 360 cancel() 361 }() 362 363 // Run preload commands 364 if err = runPreloadCommands(ctx, proc); err != nil { 365 return 366 } 367 368 // Overwrite Flags with Command Options 369 if err = overwriteFlags(c, proc.Tx); err != nil { 370 return 371 } 372 373 err = fn(ctx, c, proc) 374 if signalReceived != nil { 375 err = signalReceived 376 } 377 return 378 } 379 } 380 381 func overwriteFlags(c *cli.Context, tx *query.Transaction) error { 382 if c.IsSet("repository") { 383 if err := tx.SetFlag(option.RepositoryFlag, c.String("repository")); err != nil { 384 return query.NewIncorrectCommandUsageError(err.Error()) 385 } 386 } 387 if c.IsSet("timezone") { 388 if err := tx.SetFlag(option.TimezoneFlag, c.String("timezone")); err != nil { 389 return query.NewIncorrectCommandUsageError(err.Error()) 390 } 391 } 392 if c.IsSet("datetime-format") { 393 _ = tx.SetFlag(option.DatetimeFormatFlag, c.String("datetime-format")) 394 } 395 if c.IsSet("ansi-quotes") { 396 _ = tx.SetFlag(option.AnsiQuotesFlag, c.Bool("ansi-quotes")) 397 } 398 if c.IsSet("strict-equal") { 399 _ = tx.SetFlag(option.StrictEqualFlag, c.Bool("strict-equal")) 400 } 401 402 if c.IsSet("wait-timeout") { 403 _ = tx.SetFlag(option.WaitTimeoutFlag, c.Float64("wait-timeout")) 404 } 405 if c.IsSet("color") { 406 _ = tx.SetFlag(option.ColorFlag, c.Bool("color")) 407 } 408 409 if c.IsSet("import-format") { 410 if err := tx.SetFlag(option.ImportFormatFlag, c.String("import-format")); err != nil { 411 return query.NewIncorrectCommandUsageError(err.Error()) 412 } 413 } 414 if c.IsSet("delimiter") { 415 if err := tx.SetFlag(option.DelimiterFlag, c.String("delimiter")); err != nil { 416 return query.NewIncorrectCommandUsageError(err.Error()) 417 } 418 } 419 if c.IsSet("allow-uneven-fields") { 420 _ = tx.SetFlag(option.AllowUnevenFieldsFlag, c.Bool("allow-uneven-fields")) 421 } 422 if c.IsSet("delimiter-positions") { 423 if err := tx.SetFlag(option.DelimiterPositionsFlag, c.String("delimiter-positions")); err != nil { 424 return query.NewIncorrectCommandUsageError(err.Error()) 425 } 426 } 427 if c.IsSet("json-query") { 428 _ = tx.SetFlag(option.JsonQueryFlag, c.String("json-query")) 429 } 430 if c.IsSet("encoding") { 431 if err := tx.SetFlag(option.EncodingFlag, c.String("encoding")); err != nil { 432 return query.NewIncorrectCommandUsageError(err.Error()) 433 } 434 } 435 if c.IsSet("no-header") { 436 _ = tx.SetFlag(option.NoHeaderFlag, c.Bool("no-header")) 437 } 438 if c.IsSet("without-null") { 439 _ = tx.SetFlag(option.WithoutNullFlag, c.Bool("without-null")) 440 } 441 442 if c.IsSet("strip-ending-line-break") { 443 _ = tx.SetFlag(option.StripEndingLineBreakFlag, c.Bool("strip-ending-line-break")) 444 } 445 446 if err := tx.SetFormatFlag(c.String("format"), c.String("out")); err != nil { 447 return query.NewIncorrectCommandUsageError(err.Error()) 448 } 449 450 if c.IsSet("write-encoding") { 451 if err := tx.SetFlag(option.ExportEncodingFlag, c.String("write-encoding")); err != nil { 452 return query.NewIncorrectCommandUsageError(err.Error()) 453 } 454 } 455 if c.IsSet("write-delimiter") { 456 if err := tx.SetFlag(option.ExportDelimiterFlag, c.String("write-delimiter")); err != nil { 457 return query.NewIncorrectCommandUsageError(err.Error()) 458 } 459 } 460 if c.IsSet("write-delimiter-positions") { 461 if err := tx.SetFlag(option.ExportDelimiterPositionsFlag, c.String("write-delimiter-positions")); err != nil { 462 return query.NewIncorrectCommandUsageError(err.Error()) 463 } 464 } 465 if c.IsSet("without-header") { 466 _ = tx.SetFlag(option.WithoutHeaderFlag, c.Bool("without-header")) 467 } 468 if c.IsSet("line-break") { 469 if err := tx.SetFlag(option.LineBreakFlag, c.String("line-break")); err != nil { 470 return query.NewIncorrectCommandUsageError(err.Error()) 471 } 472 } 473 if c.IsSet("enclose-all") { 474 _ = tx.SetFlag(option.EncloseAllFlag, c.Bool("enclose-all")) 475 } 476 if c.IsSet("json-escape") { 477 if err := tx.SetFlag(option.JsonEscapeFlag, c.String("json-escape")); err != nil { 478 return query.NewIncorrectCommandUsageError(err.Error()) 479 } 480 } 481 if c.IsSet("pretty-print") { 482 _ = tx.SetFlag(option.PrettyPrintFlag, c.Bool("pretty-print")) 483 } 484 if c.IsSet("scientific-notation") { 485 _ = tx.SetFlag(option.ScientificNotationFlag, c.Bool("scientific-notation")) 486 } 487 488 if c.IsSet("east-asian-encoding") { 489 _ = tx.SetFlag(option.EastAsianEncodingFlag, c.Bool("east-asian-encoding")) 490 } 491 if c.IsSet("count-diacritical-sign") { 492 _ = tx.SetFlag(option.CountDiacriticalSignFlag, c.Bool("count-diacritical-sign")) 493 } 494 if c.IsSet("count-format-code") { 495 _ = tx.SetFlag(option.CountFormatCodeFlag, c.Bool("count-format-code")) 496 } 497 498 if c.IsSet("quiet") { 499 _ = tx.SetFlag(option.QuietFlag, c.Bool("quiet")) 500 } 501 if c.IsSet("limit-recursion") { 502 _ = tx.SetFlag(option.LimitRecursion, c.Int64("limit-recursion")) 503 } 504 if c.IsSet("cpu") { 505 _ = tx.SetFlag(option.CPUFlag, c.Int64("cpu")) 506 } 507 if c.IsSet("stats") { 508 _ = tx.SetFlag(option.StatsFlag, c.Bool("stats")) 509 } 510 511 return nil 512 } 513 514 func runPreloadCommands(ctx context.Context, proc *query.Processor) (err error) { 515 files := option.GetSpecialFilePath(option.PreloadCommandFileName) 516 for _, fpath := range files { 517 if !file.Exists(fpath) { 518 continue 519 } 520 521 statements, err := query.LoadStatementsFromFile(ctx, proc.Tx, parser.Identifier{Literal: fpath}) 522 if err != nil { 523 return err 524 } 525 526 if _, err := proc.Execute(ctx, statements); err != nil { 527 return err 528 } 529 } 530 return nil 531 } 532 533 func readQuery(ctx context.Context, c *cli.Context, tx *query.Transaction) (queryString string, path string, err error) { 534 if c.IsSet("source") && 0 < len(c.String("source")) { 535 if 0 < c.NArg() { 536 err = query.NewIncorrectCommandUsageError("no argument can be passed when \"--source\" option is specified") 537 } else { 538 path = c.String("source") 539 queryString, err = query.LoadContentsFromFile(ctx, tx, parser.Identifier{Literal: path}) 540 } 541 } else { 542 switch c.NArg() { 543 case 0: 544 // Launch interactive shell 545 case 1: 546 queryString = c.Args().First() 547 default: 548 err = query.NewIncorrectCommandUsageError("csvq command takes exactly 1 argument") 549 } 550 } 551 return 552 } 553 554 func Exit(err error, tx *query.Transaction) error { 555 if err == nil { 556 return nil 557 } 558 if exit, ok := err.(*query.ForcedExit); ok && exit.Code() == 0 { 559 return nil 560 } 561 562 code := query.ReturnCodeApplicationError 563 message := err.Error() 564 if tx != nil { 565 message = tx.Error(message) 566 } 567 568 if apperr, ok := err.(query.Error); ok { 569 code = apperr.Code() 570 } 571 572 return cli.Exit(message, code) 573 }