Skip to content

Commit b662de8

Browse files
authored
cmd: Print a message when reading from stdin implicitly (#78)
One thing that can be confusing about the UX of the CLI is that when you call it without any arguments, it's reading from stdin but you may not know that. You may assume it's just taking forever to start up. This changes it to print a message to stderr if it's reading from stdin because no arguments were given and no input is read for 200 milliseconds.
1 parent 3327cbc commit b662de8

File tree

3 files changed

+127
-10
lines changed

3 files changed

+127
-10
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
### Changed
10+
- cmd/errtrace:
11+
Print a message when reading from stdin because no arguments were given.
12+
Use '-' as the file name to read from stdin without a warning.
13+
814
## v0.2.0 - 2023-11-30
915

1016
This release contains minor improvements to the errtrace code transformer

cmd/errtrace/main.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ package main
2525

2626
import (
2727
"bytes"
28+
"errors"
2829
"flag"
2930
"fmt"
3031
"go/ast"
@@ -38,6 +39,7 @@ import (
3839
"sort"
3940
"strconv"
4041
"strings"
42+
"time"
4143
)
4244

4345
func main() {
@@ -54,6 +56,8 @@ type mainParams struct {
5456
List bool // -l
5557
Format format // -format
5658
Files []string // list of files to process
59+
60+
ImplicitStdin bool // whether stdin was picked because there were no args
5761
}
5862

5963
func (p *mainParams) shouldFormat() bool {
@@ -93,6 +97,7 @@ func (p *mainParams) Parse(w io.Writer, args []string) error {
9397
if len(p.Files) == 0 {
9498
// Read file from stdin when there's no args, similar to gofmt.
9599
p.Files = []string{"-"}
100+
p.ImplicitStdin = true
96101
}
97102

98103
return nil
@@ -175,10 +180,11 @@ func (cmd *mainCmd) Run(args []string) (exitCode int) {
175180

176181
for _, file := range p.Files {
177182
req := fileRequest{
178-
Format: p.shouldFormat(),
179-
Write: p.Write,
180-
List: p.List,
181-
Filename: file,
183+
Format: p.shouldFormat(),
184+
Write: p.Write,
185+
List: p.List,
186+
Filename: file,
187+
ImplicitStdin: p.ImplicitStdin,
182188
}
183189
if err := cmd.processFile(req); err != nil {
184190
cmd.log.Printf("%s:%s", file, err)
@@ -194,6 +200,8 @@ type fileRequest struct {
194200
Write bool
195201
List bool
196202
Filename string
203+
204+
ImplicitStdin bool
197205
}
198206

199207
// processFile processes a single file.
@@ -421,15 +429,50 @@ func (cmd *mainCmd) processFile(r fileRequest) error {
421429
return err
422430
}
423431

432+
var _stdinWait = 200 * time.Millisecond
433+
424434
func (cmd *mainCmd) readFile(r fileRequest) ([]byte, error) {
425-
if r.Filename == "-" {
426-
if r.Write {
427-
return nil, fmt.Errorf("can't use -w with stdin")
428-
}
435+
if r.Filename != "-" {
436+
return os.ReadFile(r.Filename)
437+
}
438+
439+
if r.Write {
440+
return nil, fmt.Errorf("can't use -w with stdin")
441+
}
442+
443+
if !r.ImplicitStdin {
429444
return io.ReadAll(cmd.Stdin)
430445
}
431446

432-
return os.ReadFile(r.Filename)
447+
// If we're reading from stdin because there were no other arguments,
448+
// wait a short time for the first read.
449+
// If there's nothing, print a warning and continue waiting.
450+
firstRead := make(chan struct{})
451+
go func(firstRead <-chan struct{}) {
452+
select {
453+
case <-firstRead:
454+
case <-time.After(_stdinWait):
455+
cmd.log.Println("reading from stdin; use '-h' for help")
456+
}
457+
}(firstRead)
458+
459+
var buff bytes.Buffer
460+
bs := make([]byte, 1024)
461+
for {
462+
n, err := cmd.Stdin.Read(bs)
463+
buff.Write(bs[:n])
464+
if err != nil {
465+
if errors.Is(err, io.EOF) {
466+
err = nil
467+
}
468+
return buff.Bytes(), err
469+
}
470+
471+
if firstRead != nil {
472+
close(firstRead)
473+
firstRead = nil
474+
}
475+
}
433476
}
434477

435478
type walker struct {

cmd/errtrace/main_test.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strconv"
1515
"strings"
1616
"testing"
17+
"time"
1718

1819
"braces.dev/errtrace/internal/diff"
1920
)
@@ -359,7 +360,7 @@ func TestFormatAuto(t *testing.T) {
359360
Stdin: strings.NewReader("unused"),
360361
Stdout: &out,
361362
Stderr: &err,
362-
}).Run([]string{"-w"})
363+
}).Run([]string{"-w", "-"})
363364
if want := 1; exitCode != want {
364365
t.Errorf("exit code = %d, want %d", exitCode, want)
365366
}
@@ -440,6 +441,73 @@ func _() {
440441
}
441442
}
442443

444+
func TestStdinNoInputMessage(t *testing.T) {
445+
// Verify that if there's no input on implicit stdin,
446+
// we print a message to stderr to help the user.
447+
defer func(old time.Duration) { _stdinWait = old }(_stdinWait)
448+
_stdinWait = 10 * time.Millisecond // make the test run faster
449+
450+
tests := []struct {
451+
name string
452+
delay time.Duration // before writing
453+
args []string
454+
wantStderr string
455+
}{
456+
{
457+
name: "no delay",
458+
delay: 0,
459+
},
460+
{
461+
name: "delay",
462+
delay: 20 * time.Millisecond,
463+
wantStderr: "reading from stdin; use '-h' for help\n",
464+
},
465+
{
466+
name: "explicit stdin with delay",
467+
args: []string{"-"},
468+
delay: 20 * time.Millisecond,
469+
},
470+
}
471+
472+
for _, tt := range tests {
473+
t.Run(tt.name, func(t *testing.T) {
474+
// mainCmd.Run is blocking so this has to be in a goroutine.
475+
stdin, stdinw := io.Pipe()
476+
done := make(chan struct{})
477+
go func() {
478+
defer close(done)
479+
480+
if tt.delay > 0 {
481+
time.Sleep(20 * time.Millisecond)
482+
}
483+
if _, err := io.WriteString(stdinw, "package foo\n"); err != nil {
484+
t.Error(err)
485+
}
486+
487+
if err := stdinw.Close(); err != nil {
488+
t.Error(err)
489+
}
490+
}()
491+
defer func() { <-done }()
492+
493+
var stderr bytes.Buffer
494+
exitCode := (&mainCmd{
495+
Stdin: stdin,
496+
Stdout: io.Discard,
497+
Stderr: &stderr,
498+
}).Run(tt.args)
499+
500+
if want := 0; exitCode != want {
501+
t.Errorf("exit code = %d, want %d", exitCode, want)
502+
}
503+
504+
if want, got := tt.wantStderr, stderr.String(); got != want {
505+
t.Errorf("stderr = %q, want %q", got, want)
506+
}
507+
})
508+
}
509+
}
510+
443511
func indent(s string) string {
444512
return "\t" + strings.ReplaceAll(s, "\n", "\n\t")
445513
}

0 commit comments

Comments
 (0)