stdio.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. package main
  2. import (
  3. "fmt"
  4. "io"
  5. "os"
  6. )
  7. // stdioPath is the special path value that selects stdin (for inputs) or
  8. // stdout (for outputs) instead of a file on disk.
  9. const stdioPath = "-"
  10. // stdioHelpText is rendered just under the Usage line of each subcommand
  11. // help so the - convention is documented once instead of on every flag.
  12. const stdioHelpText = " Pass \"-\" to any path flag to read from stdin or write to stdout.\n"
  13. // stdinReader is the source used when an input flag is set to "-".
  14. // It is a package level var so tests can swap in a deterministic reader.
  15. // Tests that mutate stdinReader cannot run with t.Parallel().
  16. var stdinReader io.Reader = os.Stdin
  17. // ioClaims tracks which flags have claimed stdin and stdout during a single
  18. // command invocation so we can refuse a second flag asking for the same
  19. // stream.
  20. type ioClaims struct {
  21. in string
  22. out string
  23. }
  24. func (c *ioClaims) claimIn(flagName string) error {
  25. if c.in != "" && c.in != flagName {
  26. return fmt.Errorf("-%s and -%s both set to %q, only one input may read from stdin", c.in, flagName, stdioPath)
  27. }
  28. c.in = flagName
  29. return nil
  30. }
  31. func (c *ioClaims) claimOut(flagName string) error {
  32. if c.out != "" && c.out != flagName {
  33. return fmt.Errorf("-%s and -%s both set to %q, only one output may write to stdout", c.out, flagName, stdioPath)
  34. }
  35. c.out = flagName
  36. return nil
  37. }
  38. // reserveInputs walks alternating (flagName, path) pairs and claims stdin
  39. // for any path equal to stdioPath. It must be called before any input is
  40. // read so a conflict can be reported immediately instead of blocking on
  41. // io.ReadAll while waiting for input that will never arrive.
  42. func reserveInputs(claims *ioClaims, pairs ...string) error {
  43. return reserveStdio(claims, "reserveInputs", (*ioClaims).claimIn, pairs)
  44. }
  45. // reserveOutputs walks alternating (flagName, path) pairs and claims stdout
  46. // for any path equal to stdioPath. It must be called before any output is
  47. // written so a conflict cannot leave one stream half written before the
  48. // second flag fails.
  49. func reserveOutputs(claims *ioClaims, pairs ...string) error {
  50. return reserveStdio(claims, "reserveOutputs", (*ioClaims).claimOut, pairs)
  51. }
  52. func reserveStdio(claims *ioClaims, who string, claim func(*ioClaims, string) error, pairs []string) error {
  53. if len(pairs)%2 != 0 {
  54. panic(who + " requires alternating name, path pairs")
  55. }
  56. for i := 0; i < len(pairs); i += 2 {
  57. name, path := pairs[i], pairs[i+1]
  58. if path != stdioPath {
  59. continue
  60. }
  61. if err := claim(claims, name); err != nil {
  62. return err
  63. }
  64. }
  65. return nil
  66. }
  67. // readInput returns the bytes referenced by path, reading from stdin when
  68. // path is stdioPath.
  69. func readInput(flagName, path string, claims *ioClaims) ([]byte, error) {
  70. if path == stdioPath {
  71. if err := claims.claimIn(flagName); err != nil {
  72. return nil, err
  73. }
  74. return io.ReadAll(stdinReader)
  75. }
  76. return os.ReadFile(path)
  77. }
  78. // openInput returns a reader for path. When path is stdioPath the returned
  79. // reader wraps stdin and Close is a no-op.
  80. func openInput(flagName, path string, claims *ioClaims) (io.ReadCloser, error) {
  81. if path == stdioPath {
  82. if err := claims.claimIn(flagName); err != nil {
  83. return nil, err
  84. }
  85. return io.NopCloser(stdinReader), nil
  86. }
  87. return os.Open(path)
  88. }
  89. // writeOutput writes data to path, or to stdout when path is stdioPath. perm
  90. // is only used for file output. The caller must have already claimed stdout
  91. // via reserveOutputs before invoking with stdioPath.
  92. func writeOutput(path string, data []byte, perm os.FileMode, stdout io.Writer) error {
  93. if path == stdioPath {
  94. _, err := stdout.Write(data)
  95. return err
  96. }
  97. return os.WriteFile(path, data, perm)
  98. }
  99. // isStdio reports whether path is the stdio sentinel and so should skip
  100. // existence checks like "refuse to overwrite".
  101. func isStdio(path string) bool {
  102. return path == stdioPath
  103. }