From 65d9d416b4300e85304fd158d9df2f6272590849 Mon Sep 17 00:00:00 2001
From: Junegunn Choi <junegunn.c@gmail.com>
Date: Tue, 15 Sep 2015 13:21:51 +0900
Subject: [PATCH] Change exit status (0: OK, 1: No match, 2: Error/Interrupted)

A la grep. Close #345
---
 src/constants.go     |  6 ++++++
 src/core.go          | 15 ++++++++++++---
 src/curses/curses.go |  4 ++--
 src/options.go       |  8 ++++----
 src/terminal.go      | 17 ++++++++++------
 test/test_go.rb      | 46 +++++++++++++++++++++++++++++++++++++++-----
 6 files changed, 76 insertions(+), 20 deletions(-)

diff --git a/src/constants.go b/src/constants.go
index b2225f14..9a4fc29f 100644
--- a/src/constants.go
+++ b/src/constants.go
@@ -47,3 +47,9 @@ const (
 	EvtHeader
 	EvtClose
 )
+
+const (
+	exitOk      = 0
+	exitNoMatch = 1
+	exitError   = 2
+)
diff --git a/src/core.go b/src/core.go
index 96bfdd4c..04b6eab7 100644
--- a/src/core.go
+++ b/src/core.go
@@ -56,7 +56,7 @@ func Run(opts *Options) {
 
 	if opts.Version {
 		fmt.Println(version)
-		os.Exit(0)
+		os.Exit(exitOk)
 	}
 
 	// Event channel
@@ -156,12 +156,14 @@ func Run(opts *Options) {
 
 		pattern := patternBuilder([]rune(*opts.Filter))
 
+		found := false
 		if streamingFilter {
 			reader := Reader{
 				func(runes []byte) bool {
 					item := chunkList.trans(runes, 0)
 					if item != nil && pattern.MatchItem(item) {
 						fmt.Println(string(item.text))
+						found = true
 					}
 					return false
 				}, eventBox, opts.ReadZero}
@@ -176,9 +178,13 @@ func Run(opts *Options) {
 				pattern: pattern})
 			for i := 0; i < merger.Length(); i++ {
 				fmt.Println(merger.Get(i).AsString(opts.Ansi))
+				found = true
 			}
 		}
-		os.Exit(0)
+		if found {
+			os.Exit(exitOk)
+		}
+		os.Exit(exitNoMatch)
 	}
 
 	// Synchronous search
@@ -253,7 +259,10 @@ func Run(opts *Options) {
 									for i := 0; i < count; i++ {
 										fmt.Println(val.Get(i).AsString(opts.Ansi))
 									}
-									os.Exit(0)
+									if count > 0 {
+										os.Exit(exitOk)
+									}
+									os.Exit(exitNoMatch)
 								}
 								deferred = false
 								terminal.startChan <- true
diff --git a/src/curses/curses.go b/src/curses/curses.go
index 3de8e982..59cea3b0 100644
--- a/src/curses/curses.go
+++ b/src/curses/curses.go
@@ -261,7 +261,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
 	_screen = C.newterm(nil, C.stderr, C.stdin)
 	if _screen == nil {
 		fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
-		os.Exit(1)
+		os.Exit(2)
 	}
 	C.set_term(_screen)
 	if mouse {
@@ -275,7 +275,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
 	go func() {
 		<-intChan
 		Close()
-		os.Exit(1)
+		os.Exit(2)
 	}()
 
 	if theme != nil {
diff --git a/src/options.go b/src/options.go
index 70900660..47d8bb11 100644
--- a/src/options.go
+++ b/src/options.go
@@ -180,14 +180,14 @@ func defaultOptions() *Options {
 		Version:     false}
 }
 
-func help(ok int) {
+func help(code int) {
 	os.Stderr.WriteString(usage)
-	os.Exit(ok)
+	os.Exit(code)
 }
 
 func errorExit(msg string) {
 	os.Stderr.WriteString(msg + "\n")
-	os.Exit(1)
+	os.Exit(exitError)
 }
 
 func optString(arg string, prefixes ...string) (bool, string) {
@@ -682,7 +682,7 @@ func parseOptions(opts *Options, allArgs []string) {
 		arg := allArgs[i]
 		switch arg {
 		case "-h", "--help":
-			help(0)
+			help(exitOk)
 		case "-x", "--extended":
 			opts.Mode = ModeExtended
 		case "-e", "--extended-exact":
diff --git a/src/terminal.go b/src/terminal.go
index 053ed783..c3fb966b 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -280,17 +280,19 @@ func (t *Terminal) UpdateList(merger *Merger) {
 	t.reqBox.Set(reqList, nil)
 }
 
-func (t *Terminal) output() {
+func (t *Terminal) output() bool {
 	if t.printQuery {
 		fmt.Println(string(t.input))
 	}
 	if len(t.expect) > 0 {
 		fmt.Println(t.pressed)
 	}
-	if len(t.selected) == 0 {
+	found := len(t.selected) > 0
+	if !found {
 		cnt := t.merger.Length()
 		if cnt > 0 && cnt > t.cy {
 			fmt.Println(t.merger.Get(t.cy).AsString(t.ansi))
+			found = true
 		}
 	} else {
 		sels := make([]selectedItem, 0, len(t.selected))
@@ -302,6 +304,7 @@ func (t *Terminal) output() {
 			fmt.Println(*sel.text)
 		}
 	}
+	return found
 }
 
 func runeWidth(r rune, prefixWidth int) int {
@@ -743,7 +746,7 @@ func (t *Terminal) Loop() {
 	}
 
 	exit := func(code int) {
-		if code == 0 && t.history != nil {
+		if code <= exitNoMatch && t.history != nil {
 			t.history.append(string(t.input))
 		}
 		os.Exit(code)
@@ -776,11 +779,13 @@ func (t *Terminal) Loop() {
 						t.printAll()
 					case reqClose:
 						C.Close()
-						t.output()
-						exit(0)
+						if t.output() {
+							exit(exitOk)
+						}
+						exit(exitNoMatch)
 					case reqQuit:
 						C.Close()
-						exit(1)
+						exit(exitError)
 					}
 				}
 				t.placeCursor()
diff --git a/test/test_go.rb b/test/test_go.rb
index d1f45dc1..5b352647 100644
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -780,11 +780,6 @@ class TestGoFZF < TestBase
     tmux.send_keys :Enter
   end
 
-  def test_invalid_term
-    tmux.send_keys "TERM=xxx fzf", :Enter
-    tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } }
-  end
-
   def test_with_nth
     writelines tempname, ['hello world ', 'byebye']
     assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp
@@ -801,6 +796,47 @@ class TestGoFZF < TestBase
     assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp
   end
 
+  def test_exit_0_exit_code
+    `echo foo | #{FZF} -q bar -0`
+    assert_equal 1, $?.exitstatus
+  end
+
+  def test_invalid_term
+    lines = `TERM=xxx #{FZF}`
+    assert_equal 2, $?.exitstatus
+    assert lines.include?('Invalid $TERM: xxx')
+  end
+
+  def test_invalid_option
+    lines = `#{FZF} --foobar 2>&1`
+    assert_equal 2, $?.exitstatus
+    assert lines.include?('unknown option: --foobar'), lines
+  end
+
+  def test_filter_exitstatus
+    # filter / streaming filter
+    ["", "--no-sort"].each do |opts|
+      assert `echo foo | #{FZF} -f foo #{opts}`.include?('foo')
+      assert_equal 0, $?.exitstatus
+
+      assert `echo foo | #{FZF} -f bar #{opts}`.empty?
+      assert_equal 1, $?.exitstatus
+    end
+  end
+
+  def test_exitstatus_empty
+    { '99' => '0', '999' => '1' }.each do |query, status|
+      tmux.send_keys "seq 100 | #{FZF} -q #{query}", :Enter
+      tmux.until { |lines| lines[-2] =~ %r{ [10]/100} }
+      tmux.send_keys :Enter
+
+      tmux.send_keys 'echo --\$?--'
+      tmux.until { |lines| lines.last.include? "echo --$?--" }
+      tmux.send_keys :Enter
+      tmux.until { |lines| lines.last.include? "--#{status}--" }
+    end
+  end
+
 private
   def writelines path, lines
     File.unlink path while File.exists? path
-- 
GitLab