git » summer » commit dbf6a0f

Skip periodic printing if output is not a terminal

author Alberto Bertogli
2023-08-04 22:44:43 UTC
committer Alberto Bertogli
2023-08-05 00:07:16 UTC
parent 514357d400600af4d66bec2fbed24f9103c1c3a8

Skip periodic printing if output is not a terminal

This allows summer to be used non-interactively, like on cron jobs.

go.mod +2 -1
go.sum +4 -1
summer.go +10 -3
test/access.t +0 -4
test/basic.t +0 -10
test/dryrun_sqlite.t +0 -7
test/dryrun_xattr.t +0 -7
test/help.t +6 -0
test/singlefile.t +0 -1
test/sqlite.t +0 -4
test/tty.t +15 -0
ui.go +16 -10

diff --git a/go.mod b/go.mod
index b097087..84d4fc4 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.19
 require (
 	github.com/mattn/go-sqlite3 v1.14.16
 	github.com/pkg/xattr v0.4.9
+	golang.org/x/term v0.11.0
 )
 
-require golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
+require golang.org/x/sys v0.11.0 // indirect
diff --git a/go.sum b/go.sum
index 2e4ed8c..9bdf4a4 100644
--- a/go.sum
+++ b/go.sum
@@ -2,5 +2,8 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
 github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
-golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
 golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
diff --git a/summer.go b/summer.go
index 4df19ef..a55696f 100644
--- a/summer.go
+++ b/summer.go
@@ -9,6 +9,8 @@ import (
 	"os"
 	"path/filepath"
 	"syscall"
+
+	"golang.org/x/term"
 )
 
 const usage = `# summer 🌞 🏖
@@ -40,6 +42,7 @@ Flags:
 var (
 	dbPath        = flag.String("db", "", "database to read from/write to")
 	oneFilesystem = flag.Bool("x", false, "don't cross filesystem boundaries")
+	forceTTY      = flag.Bool("forcetty", false, "force TTY output")
 )
 
 var options = struct {
@@ -48,6 +51,9 @@ var options = struct {
 
 	// Do not cross filesystem boundaries.
 	oneFilesystem bool
+
+	// Whether output is a TTY.
+	isTTY bool
 }{}
 
 func Usage() {
@@ -62,6 +68,7 @@ func main() {
 	flag.Parse()
 
 	options.oneFilesystem = *oneFilesystem
+	options.isTTY = *forceTTY || term.IsTerminal(int(os.Stdout.Fd()))
 
 	op := flag.Arg(0)
 	root := flag.Arg(1)
@@ -150,7 +157,7 @@ func getDeviceForPath(path string) uint64 {
 
 func generate(root string) error {
 	rootDev := getDeviceForPath(root)
-	p := NewProgress()
+	p := NewProgress(options.isTTY)
 	defer p.Stop()
 
 	fn := func(path string, d fs.DirEntry, err error) error {
@@ -195,7 +202,7 @@ func generate(root string) error {
 
 func verify(root string) error {
 	rootDev := getDeviceForPath(root)
-	p := NewProgress()
+	p := NewProgress(options.isTTY)
 	defer p.Stop()
 
 	fn := func(path string, d fs.DirEntry, err error) error {
@@ -251,7 +258,7 @@ func verify(root string) error {
 
 func update(root string) error {
 	rootDev := getDeviceForPath(root)
-	p := NewProgress()
+	p := NewProgress(options.isTTY)
 	defer p.Stop()
 
 	fn := func(path string, d fs.DirEntry, err error) error {
diff --git a/test/access.t b/test/access.t
index 426ac8f..f72a125 100644
--- a/test/access.t
+++ b/test/access.t
@@ -9,15 +9,12 @@ interfere.
   $ echo marola > root/hola
 
   $ summer -db=db.sqlite3 generate root/
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 2 new, 0 corrupted
 
   $ summer -db=db.sqlite3 verify root/
-  \r (no-eol) (esc)
   0s: 2 matched, 0 modified, 0 new, 0 corrupted
   $ chmod 0000 root/empty
   $ summer -db=db.sqlite3 verify root/
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 0 new, 0 corrupted
   open root/empty: permission denied
   [1]
@@ -26,7 +23,6 @@ Test behaviour when the root does not exist. This exercises some different
 code paths, because the root is special.
 
   $ summer verify doesnotexist
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 0 new, 0 corrupted
   lstat doesnotexist: no such file or directory
   [1]
diff --git a/test/basic.t b/test/basic.t
index dd65c48..0adf197 100644
--- a/test/basic.t
+++ b/test/basic.t
@@ -10,11 +10,9 @@ Generate test data.
 Generate and verify.
 
   $ summer generate .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 2 new, 0 corrupted
 
   $ summer verify .
-  \r (no-eol) (esc)
   0s: 2 matched, 0 modified, 0 new, 0 corrupted
 
 Check handling of new and updated files.
@@ -22,13 +20,10 @@ Check handling of new and updated files.
   $ echo trova > nueva
   $ touch empty
   $ summer verify .
-  \r (no-eol) (esc)
   0s: 1 matched, 1 modified, 1 new, 0 corrupted
   $ summer update .
-  \r (no-eol) (esc)
   0s: 1 matched, 1 modified, 1 new, 0 corrupted
   $ summer verify .
-  \r (no-eol) (esc)
   0s: 3 matched, 0 modified, 0 new, 0 corrupted
 
 Corrupt a file by changing its contents without changing the mtime.
@@ -36,13 +31,11 @@ Corrupt a file by changing its contents without changing the mtime.
   $ OLD_MTIME=`stat -c "%y" hola`
   $ echo sospechoso >> hola
   $ summer verify .
-  \r (no-eol) (esc)
   0s: 2 matched, 1 modified, 0 new, 0 corrupted
   $ touch --date="$OLD_MTIME" hola
 
   $ summer verify .
   "hola": FILE CORRUPTED - expected:239059f6, got:916db13f
-  \r (no-eol) (esc)
   0s: 2 matched, 0 modified, 0 new, 1 corrupted
   detected 1 corrupted files
   [1]
@@ -52,7 +45,6 @@ it.
 
   $ summer update .
   "hola": FILE CORRUPTED - expected:239059f6, got:916db13f
-  \r (no-eol) (esc)
   0s: 2 matched, 0 modified, 0 new, 1 corrupted
   detected 1 corrupted files
   [1]
@@ -61,10 +53,8 @@ Editing the file makes us ignore the previous checksum.
 
   $ touch hola
   $ summer update .
-  \r (no-eol) (esc)
   0s: 2 matched, 1 modified, 0 new, 0 corrupted
   $ summer verify .
-  \r (no-eol) (esc)
   0s: 3 matched, 0 modified, 0 new, 0 corrupted
 
 Check verbose and quiet.
diff --git a/test/dryrun_sqlite.t b/test/dryrun_sqlite.t
index 63dfbbe..c563f9c 100644
--- a/test/dryrun_sqlite.t
+++ b/test/dryrun_sqlite.t
@@ -10,21 +10,17 @@ Generate test data.
 Generate and verify.
 
   $ summer -n -db=db.sqlite3 generate .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 3 new, 0 corrupted
 
   $ summer -n -db=db.sqlite3 verify .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 3 new, 0 corrupted
 
   $ summer -n -db=db.sqlite3 verify .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 3 new, 0 corrupted
 
 Now write data for real, so we can test modification.
 
   $ summer -db=db.sqlite3 generate .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 3 new, 0 corrupted
 
 Check handling of new and updated files.
@@ -32,11 +28,8 @@ Check handling of new and updated files.
   $ echo trova > nueva
   $ touch empty
   $ summer -n -db=db.sqlite3 verify .
-  \r (no-eol) (esc)
   0s: 1 matched, 2 modified, 1 new, 0 corrupted
   $ summer -n -db=db.sqlite3 update .
-  \r (no-eol) (esc)
   0s: 1 matched, 2 modified, 1 new, 0 corrupted
   $ summer -n -db=db.sqlite3 verify .
-  \r (no-eol) (esc)
   0s: 1 matched, 2 modified, 1 new, 0 corrupted
diff --git a/test/dryrun_xattr.t b/test/dryrun_xattr.t
index 30a42ed..99195ef 100644
--- a/test/dryrun_xattr.t
+++ b/test/dryrun_xattr.t
@@ -10,21 +10,17 @@ Generate test data.
 Generate and verify.
 
   $ summer -n generate .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 2 new, 0 corrupted
 
   $ summer -n verify .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 2 new, 0 corrupted
 
   $ summer -n verify .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 2 new, 0 corrupted
 
 Now write data for real, so we can test modification.
 
   $ summer generate .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 2 new, 0 corrupted
 
 Check handling of new and updated files.
@@ -32,11 +28,8 @@ Check handling of new and updated files.
   $ echo trova > nueva
   $ touch empty
   $ summer -n verify .
-  \r (no-eol) (esc)
   0s: 1 matched, 1 modified, 1 new, 0 corrupted
   $ summer -n update .
-  \r (no-eol) (esc)
   0s: 1 matched, 1 modified, 1 new, 0 corrupted
   $ summer -n verify .
-  \r (no-eol) (esc)
   0s: 1 matched, 1 modified, 1 new, 0 corrupted
diff --git a/test/help.t b/test/help.t
index a14fefa..d3b4b5a 100644
--- a/test/help.t
+++ b/test/help.t
@@ -31,6 +31,8 @@ No arguments.
   Flags:
     -db string
       \tdatabase to read from/write to (esc)
+    -forcetty
+      \tforce TTY output (esc)
     -n\tdry-run mode (do not write anything) (esc)
     -q\tquiet mode (esc)
     -v\tverbose mode (list each file) (esc)
@@ -67,6 +69,8 @@ Too few arguments.
   Flags:
     -db string
       \tdatabase to read from/write to (esc)
+    -forcetty
+      \tforce TTY output (esc)
     -n\tdry-run mode (do not write anything) (esc)
     -q\tquiet mode (esc)
     -v\tverbose mode (list each file) (esc)
@@ -103,6 +107,8 @@ No valid path (the argument is given, but it is empty).
   Flags:
     -db string
       \tdatabase to read from/write to (esc)
+    -forcetty
+      \tforce TTY output (esc)
     -n\tdry-run mode (do not write anything) (esc)
     -q\tquiet mode (esc)
     -v\tverbose mode (list each file) (esc)
diff --git a/test/singlefile.t b/test/singlefile.t
index 62d15fd..018b2b3 100644
--- a/test/singlefile.t
+++ b/test/singlefile.t
@@ -14,7 +14,6 @@ Test that summer works fine when given a file instead of a directory.
   0s: 1 matched, 0 modified, 1 new, 0 corrupted
 
   $ summer update ./hola
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 1 new, 0 corrupted
 
   $ summer -v verify .
diff --git a/test/sqlite.t b/test/sqlite.t
index 056f317..1bf30b1 100644
--- a/test/sqlite.t
+++ b/test/sqlite.t
@@ -7,13 +7,10 @@ This is enough to exercise the backend.
   $ echo marola > hola
 
   $ summer -db=db.sqlite3 generate .
-  \r (no-eol) (esc)
   0s: 0 matched, 0 modified, 3 new, 0 corrupted
   $ summer -db=db.sqlite3 verify .
-  \r (no-eol) (esc)
   0s: 2 matched, 1 modified, 0 new, 0 corrupted
   $ summer -db=db.sqlite3 update .
-  \r (no-eol) (esc)
   0s: 2 matched, 1 modified, 0 new, 0 corrupted
 
 Check that the root path doesn't confuse us.
@@ -27,7 +24,6 @@ Check that the root path doesn't confuse us.
 Force a write error to check it is appropriately handled.
 
   $ summer "-db=file:db.sqlite3?mode=ro" update .
-  \r (no-eol) (esc)
   0s: 0 matched, 1 modified, 0 new, 0 corrupted
   attempt to write a readonly database
   [1]
diff --git a/test/tty.t b/test/tty.t
new file mode 100644
index 0000000..8803e3d
--- /dev/null
+++ b/test/tty.t
@@ -0,0 +1,15 @@
+Test output to a TTY.
+
+summer will auto-detect if stdout is a tty or not, and change some of the
+output accordingly. In this test framework, stdout is not a TTY, so all other
+tests use that codepath.
+
+In this test we force tty output, and check the output is as expected.
+
+  $ alias summer="$TESTDIR/../summer"
+
+  $ touch file1 file2 file3
+
+  $ summer -forcetty -n generate .
+  \r (no-eol) (esc)
+  0s: 0 matched, 0 modified, 3 new, 0 corrupted
diff --git a/ui.go b/ui.go
index e31eabb..1b37192 100644
--- a/ui.go
+++ b/ui.go
@@ -48,6 +48,7 @@ func PrintVersion() {
 
 type Progress struct {
 	start time.Time
+	isTTY bool
 
 	wg sync.WaitGroup
 	mu sync.Mutex
@@ -57,10 +58,11 @@ type Progress struct {
 	done chan bool
 }
 
-func NewProgress() *Progress {
+func NewProgress(isTTY bool) *Progress {
 	p := &Progress{
 		start: time.Now(),
 		done:  make(chan bool),
+		isTTY: isTTY,
 	}
 	p.wg.Add(1)
 	go p.periodicPrint()
@@ -85,26 +87,30 @@ func (p *Progress) periodicPrint() {
 	for {
 		select {
 		case <-p.done:
-			p.print()
-			if !*verbose {
-				fmt.Printf("\n")
-			}
+			p.print(true)
 			return
 		case <-ticker.C:
-			p.print()
+			if p.isTTY {
+				p.print(false)
+			}
 		}
 	}
 }
 
-func (p *Progress) print() {
+func (p *Progress) print(last bool) {
 	p.mu.Lock()
 	defer p.mu.Unlock()
 
-	// Usually we just overwrite the previous line.
-	// But when verbose, just print them.
 	prefix := "\r"
 	suffix := ""
-	if *verbose {
+	if last {
+		suffix = "\n"
+	}
+
+	// Usually we just overwrite the previous line.
+	// But when verbose, just print one after the other.
+	// For non-TTY, never overwrite.
+	if *verbose || !p.isTTY {
 		prefix = ""
 		suffix = "\n"
 	}