Browse code

Add writers capable of adjusting to terminal sizes

Fabiano Franz authored on 2016/09/14 04:31:37
Showing 17 changed files
... ...
@@ -78,10 +78,11 @@ func NewCommandCLI(name, fullName string, in io.Reader, out, errout io.Writer) *
78 78
 		Short: "Command line tools for managing applications",
79 79
 		Long:  cliLong,
80 80
 		Run: func(c *cobra.Command, args []string) {
81
-			c.SetOutput(out)
81
+			explainOut := term.NewResponsiveWriter(out)
82
+			c.SetOutput(explainOut)
82 83
 			cmdutil.RequireNoArguments(c, args)
83
-			fmt.Fprint(out, cliLong)
84
-			fmt.Fprintf(out, cliExplain, fullName)
84
+			fmt.Fprint(explainOut, cliLong)
85
+			fmt.Fprintf(explainOut, cliExplain, fullName)
85 86
 		},
86 87
 		BashCompletionFunction: bashCompletionFunc,
87 88
 	}
... ...
@@ -10,17 +10,17 @@ import (
10 10
 	"net/url"
11 11
 	"os"
12 12
 
13
-	cmdutil "github.com/openshift/origin/pkg/cmd/util"
13
+	"github.com/openshift/origin/pkg/client"
14
+	"github.com/openshift/origin/pkg/user/api"
15
+	clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
16
+
17
+	"github.com/openshift/origin/pkg/cmd/util/term"
14 18
 	kapi "k8s.io/kubernetes/pkg/api"
15 19
 	kerrors "k8s.io/kubernetes/pkg/api/errors"
16 20
 	"k8s.io/kubernetes/pkg/client/restclient"
17
-	clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
18 21
 	kclientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
19 22
 	"k8s.io/kubernetes/pkg/util/sets"
20
-	"k8s.io/kubernetes/pkg/util/term"
21
-
22
-	"github.com/openshift/origin/pkg/client"
23
-	"github.com/openshift/origin/pkg/user/api"
23
+	kterm "k8s.io/kubernetes/pkg/util/term"
24 24
 )
25 25
 
26 26
 // getMatchingClusters examines the kubeconfig for all clusters that point to the same server
... ...
@@ -96,12 +96,12 @@ func promptForInsecureTLS(reader io.Reader, out io.Writer, reason error) bool {
96 96
 		}
97 97
 	}
98 98
 	var input bool
99
-	if term.IsTerminal(reader) {
99
+	if kterm.IsTerminal(reader) {
100 100
 		if len(insecureTLSRequestReason) > 0 {
101 101
 			fmt.Fprintln(out, insecureTLSRequestReason)
102 102
 		}
103 103
 		fmt.Fprintln(out, "You can bypass the certificate check, but any data you send to the server could be intercepted by others.")
104
-		input = cmdutil.PromptForBool(os.Stdin, out, "Use insecure connections? (y/n): ")
104
+		input = term.PromptForBool(os.Stdin, out, "Use insecure connections? (y/n): ")
105 105
 		fmt.Fprintln(out)
106 106
 	}
107 107
 	return input
... ...
@@ -17,7 +17,7 @@ import (
17 17
 	kclientcmd "k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
18 18
 	kclientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
19 19
 	"k8s.io/kubernetes/pkg/util/sets"
20
-	"k8s.io/kubernetes/pkg/util/term"
20
+	kterm "k8s.io/kubernetes/pkg/util/term"
21 21
 
22 22
 	"github.com/openshift/origin/pkg/client"
23 23
 	"github.com/openshift/origin/pkg/cmd/cli/cmd/errors"
... ...
@@ -25,6 +25,7 @@ import (
25 25
 	cmderr "github.com/openshift/origin/pkg/cmd/errors"
26 26
 	cmdutil "github.com/openshift/origin/pkg/cmd/util"
27 27
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
28
+	"github.com/openshift/origin/pkg/cmd/util/term"
28 29
 	"github.com/openshift/origin/pkg/cmd/util/tokencmd"
29 30
 	"github.com/openshift/origin/pkg/user/api"
30 31
 )
... ...
@@ -86,11 +87,11 @@ func (o *LoginOptions) getClientConfig() (*restclient.Config, error) {
86 86
 
87 87
 	if len(o.Server) == 0 {
88 88
 		// we need to have a server to talk to
89
-		if term.IsTerminal(o.Reader) {
89
+		if kterm.IsTerminal(o.Reader) {
90 90
 			for !o.serverProvided() {
91 91
 				defaultServer := defaultClusterURL
92 92
 				promptMsg := fmt.Sprintf("Server [%s]: ", defaultServer)
93
-				o.Server = cmdutil.PromptForStringWithDefault(o.Reader, o.Out, defaultServer, promptMsg)
93
+				o.Server = term.PromptForStringWithDefault(o.Reader, o.Out, defaultServer, promptMsg)
94 94
 			}
95 95
 		}
96 96
 	}
... ...
@@ -12,6 +12,7 @@ import (
12 12
 
13 13
 	"github.com/openshift/origin/pkg/client"
14 14
 	cliconfig "github.com/openshift/origin/pkg/cmd/cli/config"
15
+	"github.com/openshift/origin/pkg/cmd/templates"
15 16
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
16 17
 	projectapi "github.com/openshift/origin/pkg/project/api"
17 18
 )
... ...
@@ -37,20 +38,21 @@ type NewProjectOptions struct {
37 37
 }
38 38
 
39 39
 // RequestProject command description.
40
-const (
41
-	requestProjectLong = `
42
-Create a new project for yourself
40
+var (
41
+	requestProjectLong = templates.LongDesc(`
42
+		Create a new project for yourself
43 43
 
44
-If your administrator allows self-service, this command will create a new project for you and assign you
45
-as the project admin.
44
+		If your administrator allows self-service, this command will create a new project for you and assign you
45
+		as the project admin.
46 46
 
47
-After your project is created it will become the default project in your config.`
47
+		After your project is created it will become the default project in your config.`)
48 48
 
49
-	requestProjectExample = `  # Create a new project with minimal information
50
-  %[1]s %[2]s web-team-dev
49
+	requestProjectExample = templates.Examples(`
50
+		# Create a new project with minimal information
51
+	  %[1]s %[2]s web-team-dev
51 52
 
52
-  # Create a new project with a display name and description
53
-  %[1]s %[2]s web-team-dev --display-name="Web Team Development" --description="Development project for the web team."`
53
+	  # Create a new project with a display name and description
54
+	  %[1]s %[2]s web-team-dev --display-name="Web Team Development" --description="Development project for the web team."`)
54 55
 )
55 56
 
56 57
 // RequestProject next steps.
... ...
@@ -71,7 +73,7 @@ To switch to this project and start adding applications, use:
71 71
 )
72 72
 
73 73
 // NewCmdRequestProject implement the OpenShift cli RequestProject command.
74
-func NewCmdRequestProject(name, baseName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
74
+func NewCmdRequestProject(name, baseName string, f *clientcmd.Factory, out, errout io.Writer) *cobra.Command {
75 75
 	o := &NewProjectOptions{}
76 76
 	o.Out = out
77 77
 	o.Name = baseName
... ...
@@ -12,8 +12,8 @@ import (
12 12
 	"k8s.io/kubernetes/pkg/client/unversioned"
13 13
 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
14 14
 
15
-	"github.com/openshift/origin/pkg/cmd/util"
16 15
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
16
+	"github.com/openshift/origin/pkg/cmd/util/term"
17 17
 	"github.com/openshift/origin/pkg/serviceaccounts"
18 18
 )
19 19
 
... ...
@@ -128,7 +128,7 @@ func (o *GetServiceAccountTokenOptions) Run() error {
128 128
 			}
129 129
 
130 130
 			fmt.Fprintf(o.Out, string(token))
131
-			if util.IsTerminalWriter(o.Out) {
131
+			if term.IsTerminalWriter(o.Out) {
132 132
 				// pretty-print for a TTY
133 133
 				fmt.Fprintf(o.Out, "\n")
134 134
 			}
... ...
@@ -17,8 +17,8 @@ import (
17 17
 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
18 18
 	"k8s.io/kubernetes/pkg/watch"
19 19
 
20
-	"github.com/openshift/origin/pkg/cmd/util"
21 20
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
21
+	"github.com/openshift/origin/pkg/cmd/util/term"
22 22
 	"github.com/openshift/origin/pkg/serviceaccounts"
23 23
 	osautil "github.com/openshift/origin/pkg/serviceaccounts/util"
24 24
 )
... ...
@@ -176,7 +176,7 @@ func (o *NewServiceAccountTokenOptions) Run() error {
176 176
 	}
177 177
 
178 178
 	fmt.Fprintf(o.Out, string(token))
179
-	if util.IsTerminalWriter(o.Out) {
179
+	if term.IsTerminalWriter(o.Out) {
180 180
 		// pretty-print for a TTY
181 181
 		fmt.Fprintf(o.Out, "\n")
182 182
 	}
... ...
@@ -11,9 +11,9 @@ import (
11 11
 	"k8s.io/kubernetes/pkg/api"
12 12
 	client "k8s.io/kubernetes/pkg/client/unversioned"
13 13
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
14
-	"k8s.io/kubernetes/pkg/util/term"
14
+	kterm "k8s.io/kubernetes/pkg/util/term"
15 15
 
16
-	cmdutil "github.com/openshift/origin/pkg/cmd/util"
16
+	"github.com/openshift/origin/pkg/cmd/util/term"
17 17
 )
18 18
 
19 19
 const (
... ...
@@ -166,11 +166,11 @@ func (o *CreateBasicAuthSecretOptions) Complete(f *kcmdutil.Factory, args []stri
166 166
 		if len(o.Password) != 0 {
167 167
 			return errors.New("must provide either --prompt or --password flag")
168 168
 		}
169
-		if !term.IsTerminal(o.Reader) {
169
+		if !kterm.IsTerminal(o.Reader) {
170 170
 			return errors.New("provided reader is not a terminal")
171 171
 		}
172 172
 
173
-		o.Password = cmdutil.PromptForPasswordString(o.Reader, o.Out, "Password: ")
173
+		o.Password = term.PromptForPasswordString(o.Reader, o.Out, "Password: ")
174 174
 		if len(o.Password) == 0 {
175 175
 			return errors.New("password must be provided")
176 176
 		}
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"io/ioutil"
9 9
 	"os"
10 10
 
11
-	"github.com/openshift/origin/pkg/cmd/util"
11
+	"github.com/openshift/origin/pkg/cmd/util/term"
12 12
 	"github.com/spf13/cobra"
13 13
 
14 14
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
... ...
@@ -38,7 +38,7 @@ type DecryptOptions struct {
38 38
 
39 39
 const decryptExample = `	# Decrypt an encrypted file to a cleartext file:
40 40
 	%[1]s --key=secret.key --in=secret.encrypted --out=secret.decrypted
41
-	
41
+
42 42
 	# Decrypt from stdin to stdout:
43 43
 	%[1]s --key=secret.key < secret2.encrypted > secret2.decrypted
44 44
 `
... ...
@@ -79,7 +79,7 @@ func (o *DecryptOptions) Validate(args []string) error {
79 79
 		return errors.New("no arguments are supported")
80 80
 	}
81 81
 
82
-	if len(o.EncryptedFile) == 0 && len(o.EncryptedData) == 0 && (o.EncryptedReader == nil || util.IsTerminalReader(o.EncryptedReader)) {
82
+	if len(o.EncryptedFile) == 0 && len(o.EncryptedData) == 0 && (o.EncryptedReader == nil || term.IsTerminalReader(o.EncryptedReader)) {
83 83
 		return errors.New("no input data specified")
84 84
 	}
85 85
 	if len(o.EncryptedFile) > 0 && len(o.EncryptedData) > 0 {
... ...
@@ -105,7 +105,7 @@ func (o *DecryptOptions) Decrypt() error {
105 105
 		}
106 106
 	case len(o.EncryptedData) > 0:
107 107
 		data = o.EncryptedData
108
-	case o.EncryptedReader != nil && !util.IsTerminalReader(o.EncryptedReader):
108
+	case o.EncryptedReader != nil && !term.IsTerminalReader(o.EncryptedReader):
109 109
 		if d, err := ioutil.ReadAll(o.EncryptedReader); err != nil {
110 110
 			return err
111 111
 		} else {
... ...
@@ -147,7 +147,7 @@ func (o *DecryptOptions) Decrypt() error {
147 147
 		}
148 148
 	case o.DecryptedWriter != nil:
149 149
 		fmt.Fprint(o.DecryptedWriter, string(plaintext))
150
-		if util.IsTerminalWriter(o.DecryptedWriter) {
150
+		if term.IsTerminalWriter(o.DecryptedWriter) {
151 151
 			fmt.Fprintln(o.DecryptedWriter)
152 152
 		}
153 153
 	}
... ...
@@ -12,12 +12,12 @@ import (
12 12
 	"unicode"
13 13
 	"unicode/utf8"
14 14
 
15
+	"github.com/openshift/origin/pkg/cmd/util/term"
15 16
 	"github.com/spf13/cobra"
16 17
 
17 18
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
18 19
 
19 20
 	configapi "github.com/openshift/origin/pkg/cmd/server/api"
20
-	"github.com/openshift/origin/pkg/cmd/util"
21 21
 	pemutil "github.com/openshift/origin/pkg/cmd/util/pem"
22 22
 )
23 23
 
... ...
@@ -47,7 +47,7 @@ type EncryptOptions struct {
47 47
 
48 48
 const encryptExample = `	# Encrypt the content of secret.txt with a generated key:
49 49
 	%[1]s --genkey=secret.key --in=secret.txt --out=secret.encrypted
50
-	
50
+
51 51
 	# Encrypt the content of secret2.txt with an existing key:
52 52
 	%[1]s --key=secret.key < secret2.txt > secret2.encrypted
53 53
 `
... ...
@@ -127,9 +127,9 @@ func (o *EncryptOptions) Encrypt() error {
127 127
 		// Don't warn in cases where we're explicitly being given the data to use
128 128
 		warnWhitespace = false
129 129
 		data = o.CleartextData
130
-	case o.CleartextReader != nil && util.IsTerminalReader(o.CleartextReader) && o.PromptWriter != nil:
130
+	case o.CleartextReader != nil && term.IsTerminalReader(o.CleartextReader) && o.PromptWriter != nil:
131 131
 		// Read a single line from stdin with prompting
132
-		data = []byte(util.PromptForString(o.CleartextReader, o.PromptWriter, "Data to encrypt: "))
132
+		data = []byte(term.PromptForString(o.CleartextReader, o.PromptWriter, "Data to encrypt: "))
133 133
 	case o.CleartextReader != nil:
134 134
 		// Read data from stdin without prompting (allows binary data and piping)
135 135
 		if d, err := ioutil.ReadAll(o.CleartextReader); err != nil {
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"text/template"
8 8
 	"unicode"
9 9
 
10
+	"github.com/openshift/origin/pkg/cmd/util/term"
10 11
 	"github.com/spf13/cobra"
11 12
 	flag "github.com/spf13/pflag"
12 13
 )
... ...
@@ -81,27 +82,30 @@ func ActsAsRootCommand(cmd *cobra.Command, filters []string, groups ...CommandGr
81 81
 	if cmd == nil {
82 82
 		panic("nil root command")
83 83
 	}
84
-	cmd.SetHelpTemplate(MainHelpTemplate())
85 84
 	templater := &templater{
86 85
 		RootCmd:       cmd,
87 86
 		UsageTemplate: MainUsageTemplate(),
87
+		HelpTemplate:  MainHelpTemplate(),
88 88
 		CommandGroups: groups,
89 89
 		Filtered:      filters,
90 90
 	}
91 91
 	cmd.SetUsageFunc(templater.UsageFunc())
92
+	cmd.SetHelpFunc(templater.HelpFunc())
92 93
 	return templater
93 94
 }
94 95
 
95 96
 func UseOptionsTemplates(cmd *cobra.Command) {
96
-	cmd.SetHelpTemplate(OptionsHelpTemplate())
97 97
 	templater := &templater{
98 98
 		UsageTemplate: OptionsUsageTemplate(),
99
+		HelpTemplate:  OptionsHelpTemplate(),
99 100
 	}
100 101
 	cmd.SetUsageFunc(templater.UsageFunc())
102
+	cmd.SetHelpFunc(templater.HelpFunc())
101 103
 }
102 104
 
103 105
 type templater struct {
104 106
 	UsageTemplate string
107
+	HelpTemplate  string
105 108
 	RootCmd       *cobra.Command
106 109
 	CommandGroups
107 110
 	Filtered []string
... ...
@@ -112,39 +116,55 @@ func (templater *templater) ExposeFlags(cmd *cobra.Command, flags ...string) Fla
112 112
 	return templater
113 113
 }
114 114
 
115
+func (templater *templater) HelpFunc() func(*cobra.Command, []string) {
116
+	return func(c *cobra.Command, s []string) {
117
+		t := template.New("help")
118
+		t.Funcs(templater.templateFuncs())
119
+		template.Must(t.Parse(templater.HelpTemplate))
120
+		out := term.NewResponsiveWriter(c.OutOrStdout())
121
+		err := t.Execute(out, c)
122
+		if err != nil {
123
+			c.Println(err)
124
+		}
125
+	}
126
+}
127
+
115 128
 func (templater *templater) UsageFunc(exposedFlags ...string) func(*cobra.Command) error {
116 129
 	return func(c *cobra.Command) error {
117
-		t := template.New("custom")
118
-
119
-		t.Funcs(template.FuncMap{
120
-			"trim":                strings.TrimSpace,
121
-			"trimRight":           func(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) },
122
-			"gt":                  cobra.Gt,
123
-			"eq":                  cobra.Eq,
124
-			"rpad":                rpad,
125
-			"flagsNotIntersected": flagsNotIntersected,
126
-			"visibleFlags":        visibleFlags,
127
-			"flagsUsages":         flagsUsages,
128
-			"cmdGroups":           templater.cmdGroups,
129
-			"rootCmd":             templater.rootCmdName,
130
-			"isRootCmd":           templater.isRootCmd,
131
-			"optionsCmdFor":       templater.optionsCmdFor,
132
-			"usageLine":           templater.usageLine,
133
-			"exposed": func(c *cobra.Command) *flag.FlagSet {
134
-				exposed := flag.NewFlagSet("exposed", flag.ContinueOnError)
135
-				if len(exposedFlags) > 0 {
136
-					for _, name := range exposedFlags {
137
-						if flag := c.Flags().Lookup(name); flag != nil {
138
-							exposed.AddFlag(flag)
139
-						}
130
+		t := template.New("usage")
131
+		t.Funcs(templater.templateFuncs(exposedFlags...))
132
+		template.Must(t.Parse(templater.UsageTemplate))
133
+		out := term.NewResponsiveWriter(c.OutOrStderr())
134
+		return t.Execute(out, c)
135
+	}
136
+}
137
+
138
+func (templater *templater) templateFuncs(exposedFlags ...string) template.FuncMap {
139
+	return template.FuncMap{
140
+		"trim":                strings.TrimSpace,
141
+		"trimRight":           func(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) },
142
+		"gt":                  cobra.Gt,
143
+		"eq":                  cobra.Eq,
144
+		"rpad":                rpad,
145
+		"flagsNotIntersected": flagsNotIntersected,
146
+		"visibleFlags":        visibleFlags,
147
+		"flagsUsages":         flagsUsages,
148
+		"cmdGroups":           templater.cmdGroups,
149
+		"rootCmd":             templater.rootCmdName,
150
+		"isRootCmd":           templater.isRootCmd,
151
+		"optionsCmdFor":       templater.optionsCmdFor,
152
+		"usageLine":           templater.usageLine,
153
+		"exposed": func(c *cobra.Command) *flag.FlagSet {
154
+			exposed := flag.NewFlagSet("exposed", flag.ContinueOnError)
155
+			if len(exposedFlags) > 0 {
156
+				for _, name := range exposedFlags {
157
+					if flag := c.Flags().Lookup(name); flag != nil {
158
+						exposed.AddFlag(flag)
140 159
 					}
141 160
 				}
142
-				return exposed
143
-			},
144
-		})
145
-
146
-		template.Must(t.Parse(templater.UsageTemplate))
147
-		return t.Execute(c.OutOrStderr(), c)
161
+			}
162
+			return exposed
163
+		},
148 164
 	}
149 165
 }
150 166
 
151 167
new file mode 100644
... ...
@@ -0,0 +1,122 @@
0
+package term
1
+
2
+import (
3
+	"bufio"
4
+	"fmt"
5
+	"io"
6
+	"os"
7
+	"strings"
8
+
9
+	"github.com/docker/docker/pkg/term"
10
+	"github.com/golang/glog"
11
+
12
+	kterm "k8s.io/kubernetes/pkg/util/term"
13
+)
14
+
15
+// PromptForString takes an io.Reader and prompts for user input if it's a terminal, returning the result.
16
+func PromptForString(r io.Reader, w io.Writer, format string, a ...interface{}) string {
17
+	if w == nil {
18
+		w = os.Stdout
19
+	}
20
+
21
+	fmt.Fprintf(w, format, a...)
22
+	return readInput(r)
23
+}
24
+
25
+// PromptForPasswordString prompts for user input by disabling echo in terminal, useful for password prompt.
26
+func PromptForPasswordString(r io.Reader, w io.Writer, format string, a ...interface{}) string {
27
+	if w == nil {
28
+		w = os.Stdout
29
+	}
30
+
31
+	if file, ok := r.(*os.File); ok {
32
+		inFd := file.Fd()
33
+
34
+		if term.IsTerminal(inFd) {
35
+			oldState, err := term.SaveState(inFd)
36
+			if err != nil {
37
+				glog.V(3).Infof("Unable to save terminal state")
38
+				return PromptForString(r, w, format, a...)
39
+			}
40
+
41
+			fmt.Fprintf(w, format, a...)
42
+
43
+			term.DisableEcho(inFd, oldState)
44
+
45
+			input := readInput(r)
46
+
47
+			defer term.RestoreTerminal(inFd, oldState)
48
+
49
+			fmt.Fprintf(w, "\n")
50
+
51
+			return input
52
+		}
53
+		glog.V(3).Infof("Stdin is not a terminal")
54
+		return PromptForString(r, w, format, a...)
55
+	}
56
+	return PromptForString(r, w, format, a...)
57
+}
58
+
59
+// PromptForBool prompts for user input of a boolean value. The accepted values are:
60
+//   yes, y, true, 	t, 1 (not case sensitive)
61
+//   no, 	n, false, f, 0 (not case sensitive)
62
+// A valid answer is mandatory so it will keep asking until an answer is provided.
63
+func PromptForBool(r io.Reader, w io.Writer, format string, a ...interface{}) bool {
64
+	if w == nil {
65
+		w = os.Stdout
66
+	}
67
+
68
+	str := PromptForString(r, w, format, a...)
69
+	switch strings.ToLower(str) {
70
+	case "1", "t", "true", "y", "yes":
71
+		return true
72
+	case "0", "f", "false", "n", "no":
73
+		return false
74
+	}
75
+	fmt.Println("Please enter 'yes' or 'no'.")
76
+	return PromptForBool(r, w, format, a...)
77
+}
78
+
79
+// PromptForStringWithDefault prompts for user input but take a default in case nothing is provided.
80
+func PromptForStringWithDefault(r io.Reader, w io.Writer, def string, format string, a ...interface{}) string {
81
+	if w == nil {
82
+		w = os.Stdout
83
+	}
84
+
85
+	s := PromptForString(r, w, format, a...)
86
+	if len(s) == 0 {
87
+		return def
88
+	}
89
+	return s
90
+}
91
+
92
+func readInput(r io.Reader) string {
93
+	if kterm.IsTerminal(r) {
94
+		return readInputFromTerminal(r)
95
+	}
96
+	return readInputFromReader(r)
97
+}
98
+
99
+func readInputFromTerminal(r io.Reader) string {
100
+	reader := bufio.NewReader(r)
101
+	result, _ := reader.ReadString('\n')
102
+	return strings.TrimRight(result, "\r\n")
103
+}
104
+
105
+func readInputFromReader(r io.Reader) string {
106
+	var result string
107
+	fmt.Fscan(r, &result)
108
+	return result
109
+}
110
+
111
+// IsTerminalReader returns whether the passed io.Reader is a terminal or not
112
+func IsTerminalReader(r io.Reader) bool {
113
+	file, ok := r.(*os.File)
114
+	return ok && term.IsTerminal(file.Fd())
115
+}
116
+
117
+// IsTerminalWriter returns whether the passed io.Writer is a terminal or not
118
+func IsTerminalWriter(w io.Writer) bool {
119
+	file, ok := w.(*os.File)
120
+	return ok && term.IsTerminal(file.Fd())
121
+}
0 122
new file mode 100644
... ...
@@ -0,0 +1,33 @@
0
+package term
1
+
2
+import (
3
+	"strings"
4
+	"testing"
5
+)
6
+
7
+func TestReadInputFromTerminal(t *testing.T) {
8
+	testcases := map[string]struct {
9
+		Input  string
10
+		Output string
11
+	}{
12
+		"empty":                             {},
13
+		"empty newline":                     {Input: "\n"},
14
+		"empty windows newline":             {Input: "\r\n"},
15
+		"empty newline with extra":          {Input: "\nextra"},
16
+		"leading space":                     {Input: " data", Output: " data"},
17
+		"leading space newline":             {Input: " data\n", Output: " data"},
18
+		"leading space windows newline":     {Input: " data\r\n", Output: " data"},
19
+		"leading space newline with extra":  {Input: " data\nextra", Output: " data"},
20
+		"trailing space":                    {Input: " data ", Output: " data "},
21
+		"trailing space newline":            {Input: " data \n", Output: " data "},
22
+		"trailing space windows newline":    {Input: " data \r\n", Output: " data "},
23
+		"trailing space newline with extra": {Input: " data \nextra", Output: " data "},
24
+	}
25
+
26
+	for k, tc := range testcases {
27
+		output := readInputFromTerminal(strings.NewReader(tc.Input))
28
+		if output != tc.Output {
29
+			t.Errorf("%s: Expected '%s', got '%s'", k, tc.Output, output)
30
+		}
31
+	}
32
+}
0 33
new file mode 100644
... ...
@@ -0,0 +1,109 @@
0
+package term
1
+
2
+import (
3
+	"io"
4
+	"os"
5
+
6
+	"github.com/docker/docker/pkg/term"
7
+	wordwrap "github.com/mitchellh/go-wordwrap"
8
+	kterm "k8s.io/kubernetes/pkg/util/term"
9
+)
10
+
11
+type wordWrapWriter struct {
12
+	limit  uint
13
+	writer io.Writer
14
+}
15
+
16
+// NewResponsiveWriter creates a Writer that detects the column width of the
17
+// terminal we are in, and adjusts every line width to fit and use recommended
18
+// terminal sizes for better readability. Does proper word wrapping automatically.
19
+//    if terminal width >= 120 columns		use 120 columns
20
+//    if terminal width >= 100 columns		use 100 columns
21
+//    if terminal width >=  80 columns		use  80 columns
22
+// In case we're not in a terminal or if it's smaller than 80 columns width,
23
+// doesn't do any wrapping.
24
+func NewResponsiveWriter(w io.Writer) io.Writer {
25
+	file, ok := w.(*os.File)
26
+	if !ok {
27
+		return w
28
+	}
29
+	fd := file.Fd()
30
+	if !term.IsTerminal(fd) {
31
+		return w
32
+	}
33
+
34
+	terminalSize := kterm.GetSize(fd)
35
+	if terminalSize == nil {
36
+		return w
37
+	}
38
+
39
+	var limit uint
40
+	switch {
41
+	case terminalSize.Width >= 120:
42
+		limit = 120
43
+	case terminalSize.Width >= 100:
44
+		limit = 100
45
+	case terminalSize.Width >= 80:
46
+		limit = 80
47
+	}
48
+
49
+	return NewWordWrapWriter(w, limit)
50
+}
51
+
52
+// NewWordWrapWriter is a Writer that supports a limit of characters on every line
53
+// and does auto word wrapping that respects that limit.
54
+func NewWordWrapWriter(w io.Writer, limit uint) io.Writer {
55
+	return &wordWrapWriter{
56
+		limit:  limit,
57
+		writer: w,
58
+	}
59
+}
60
+
61
+func (w wordWrapWriter) Write(p []byte) (nn int, err error) {
62
+	if w.limit == 0 {
63
+		return w.writer.Write(p)
64
+	}
65
+	original := string(p)
66
+	wrapped := wordwrap.WrapString(original, w.limit)
67
+	return w.writer.Write([]byte(wrapped))
68
+}
69
+
70
+// NewPunchCardWriter is a NewWordWrapWriter that limits the line width to 80 columns.
71
+func NewPunchCardWriter(w io.Writer) io.Writer {
72
+	return NewWordWrapWriter(w, 80)
73
+}
74
+
75
+type maxWidthWriter struct {
76
+	maxWidth     uint
77
+	currentWidth uint
78
+	written      uint
79
+	writer       io.Writer
80
+}
81
+
82
+// NewMaxWidthWriter is a Writer that supports a limit of characters on every
83
+// line, but doesn't do any word wrapping automatically.
84
+func NewMaxWidthWriter(w io.Writer, maxWidth uint) io.Writer {
85
+	return &maxWidthWriter{
86
+		maxWidth: maxWidth,
87
+		writer:   w,
88
+	}
89
+}
90
+
91
+func (m maxWidthWriter) Write(p []byte) (nn int, err error) {
92
+	for _, b := range p {
93
+		if m.currentWidth == m.maxWidth {
94
+			m.writer.Write([]byte{'\n'})
95
+			m.currentWidth = 0
96
+		}
97
+		if b == '\n' {
98
+			m.currentWidth = 0
99
+		}
100
+		_, err := m.writer.Write([]byte{b})
101
+		if err != nil {
102
+			return int(m.written), err
103
+		}
104
+		m.written++
105
+		m.currentWidth++
106
+	}
107
+	return len(p), nil
108
+}
0 109
new file mode 100644
... ...
@@ -0,0 +1,82 @@
0
+package term
1
+
2
+import (
3
+	"bytes"
4
+	"strings"
5
+	"testing"
6
+)
7
+
8
+const test = "Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin Origin"
9
+
10
+func TestWordWrapWriter(t *testing.T) {
11
+	testcases := map[string]struct {
12
+		input    string
13
+		maxWidth uint
14
+	}{
15
+		"max 10":   {input: test, maxWidth: 10},
16
+		"max 80":   {input: test, maxWidth: 80},
17
+		"max 120":  {input: test, maxWidth: 120},
18
+		"max 5000": {input: test, maxWidth: 5000},
19
+	}
20
+	for k, tc := range testcases {
21
+		b := bytes.NewBufferString("")
22
+		w := NewWordWrapWriter(b, tc.maxWidth)
23
+		_, err := w.Write([]byte(tc.input))
24
+		if err != nil {
25
+			t.Errorf("%s: Unexpected error: %v", k, err)
26
+		}
27
+		result := b.String()
28
+		if !strings.Contains(result, "Origin") {
29
+			t.Errorf("%s: Expected to contain \"Origin\"", k)
30
+		}
31
+		if len(result) < len(tc.input) {
32
+			t.Errorf("%s: Unexpectedly short string, got %d wanted at least %d chars: %q", k, len(result), len(tc.input), result)
33
+		}
34
+		for _, line := range strings.Split(result, "\n") {
35
+			if len(line) > int(tc.maxWidth) {
36
+				t.Errorf("%s: Every line must be at most %d chars long, got %d: %q", k, tc.maxWidth, len(line), line)
37
+			}
38
+		}
39
+		for _, word := range strings.Split(result, " ") {
40
+			if !strings.Contains(word, "Origin") {
41
+				t.Errorf("%s: Unexpected broken word: %q", k, word)
42
+			}
43
+		}
44
+	}
45
+}
46
+
47
+func TestMaxWidthWriter(t *testing.T) {
48
+	testcases := map[string]struct {
49
+		input    string
50
+		maxWidth uint
51
+	}{
52
+		"max 10":   {input: test, maxWidth: 10},
53
+		"max 80":   {input: test, maxWidth: 80},
54
+		"max 120":  {input: test, maxWidth: 120},
55
+		"max 5000": {input: test, maxWidth: 5000},
56
+	}
57
+	for k, tc := range testcases {
58
+		b := bytes.NewBufferString("")
59
+		w := NewMaxWidthWriter(b, tc.maxWidth)
60
+		_, err := w.Write([]byte(tc.input))
61
+		if err != nil {
62
+			t.Errorf("%s: Unexpected error: %v", k, err)
63
+		}
64
+		result := b.String()
65
+		if !strings.Contains(result, "Origin") {
66
+			t.Errorf("%s: Expected to contain \"Origin\"", k)
67
+		}
68
+		if len(result) < len(tc.input) {
69
+			t.Errorf("%s: Unexpectedly short string, got %d wanted at least %d chars: %q", k, len(result), len(tc.input), result)
70
+		}
71
+		lines := strings.Split(result, "\n")
72
+		for i, line := range lines {
73
+			if len(line) > int(tc.maxWidth) {
74
+				t.Errorf("%s: Every line must be at most %d chars long, got %d: %q", k, tc.maxWidth, len(line), line)
75
+			}
76
+			if i < len(lines)-1 && len(line) != int(tc.maxWidth) {
77
+				t.Errorf("%s: Lines except the last one are expected to be exactly %d chars long, got %d: %q", k, tc.maxWidth, len(line), line)
78
+			}
79
+		}
80
+	}
81
+}
0 82
deleted file mode 100644
... ...
@@ -1,122 +0,0 @@
1
-package util
2
-
3
-import (
4
-	"bufio"
5
-	"fmt"
6
-	"io"
7
-	"os"
8
-	"strings"
9
-
10
-	"github.com/docker/docker/pkg/term"
11
-	"github.com/golang/glog"
12
-
13
-	kterm "k8s.io/kubernetes/pkg/util/term"
14
-)
15
-
16
-// PromptForString takes an io.Reader and prompts for user input if it's a terminal, returning the result.
17
-func PromptForString(r io.Reader, w io.Writer, format string, a ...interface{}) string {
18
-	if w == nil {
19
-		w = os.Stdout
20
-	}
21
-
22
-	fmt.Fprintf(w, format, a...)
23
-	return readInput(r)
24
-}
25
-
26
-// PromptForPasswordString prompts for user input by disabling echo in terminal, useful for password prompt.
27
-func PromptForPasswordString(r io.Reader, w io.Writer, format string, a ...interface{}) string {
28
-	if w == nil {
29
-		w = os.Stdout
30
-	}
31
-
32
-	if file, ok := r.(*os.File); ok {
33
-		inFd := file.Fd()
34
-
35
-		if term.IsTerminal(inFd) {
36
-			oldState, err := term.SaveState(inFd)
37
-			if err != nil {
38
-				glog.V(3).Infof("Unable to save terminal state")
39
-				return PromptForString(r, w, format, a...)
40
-			}
41
-
42
-			fmt.Fprintf(w, format, a...)
43
-
44
-			term.DisableEcho(inFd, oldState)
45
-
46
-			input := readInput(r)
47
-
48
-			defer term.RestoreTerminal(inFd, oldState)
49
-
50
-			fmt.Fprintf(w, "\n")
51
-
52
-			return input
53
-		}
54
-		glog.V(3).Infof("Stdin is not a terminal")
55
-		return PromptForString(r, w, format, a...)
56
-	}
57
-	return PromptForString(r, w, format, a...)
58
-}
59
-
60
-// PromptForBool prompts for user input of a boolean value. The accepted values are:
61
-//   yes, y, true, 	t, 1 (not case sensitive)
62
-//   no, 	n, false, f, 0 (not case sensitive)
63
-// A valid answer is mandatory so it will keep asking until an answer is provided.
64
-func PromptForBool(r io.Reader, w io.Writer, format string, a ...interface{}) bool {
65
-	if w == nil {
66
-		w = os.Stdout
67
-	}
68
-
69
-	str := PromptForString(r, w, format, a...)
70
-	switch strings.ToLower(str) {
71
-	case "1", "t", "true", "y", "yes":
72
-		return true
73
-	case "0", "f", "false", "n", "no":
74
-		return false
75
-	}
76
-	fmt.Println("Please enter 'yes' or 'no'.")
77
-	return PromptForBool(r, w, format, a...)
78
-}
79
-
80
-// PromptForStringWithDefault prompts for user input but take a default in case nothing is provided.
81
-func PromptForStringWithDefault(r io.Reader, w io.Writer, def string, format string, a ...interface{}) string {
82
-	if w == nil {
83
-		w = os.Stdout
84
-	}
85
-
86
-	s := PromptForString(r, w, format, a...)
87
-	if len(s) == 0 {
88
-		return def
89
-	}
90
-	return s
91
-}
92
-
93
-func readInput(r io.Reader) string {
94
-	if kterm.IsTerminal(r) {
95
-		return readInputFromTerminal(r)
96
-	}
97
-	return readInputFromReader(r)
98
-}
99
-
100
-func readInputFromTerminal(r io.Reader) string {
101
-	reader := bufio.NewReader(r)
102
-	result, _ := reader.ReadString('\n')
103
-	return strings.TrimRight(result, "\r\n")
104
-}
105
-
106
-func readInputFromReader(r io.Reader) string {
107
-	var result string
108
-	fmt.Fscan(r, &result)
109
-	return result
110
-}
111
-
112
-// IsTerminalReader returns whether the passed io.Reader is a terminal or not
113
-func IsTerminalReader(r io.Reader) bool {
114
-	file, ok := r.(*os.File)
115
-	return ok && term.IsTerminal(file.Fd())
116
-}
117
-
118
-// IsTerminalWriter returns whether the passed io.Writer is a terminal or not
119
-func IsTerminalWriter(w io.Writer) bool {
120
-	file, ok := w.(*os.File)
121
-	return ok && term.IsTerminal(file.Fd())
122
-}
123 1
deleted file mode 100644
... ...
@@ -1,33 +0,0 @@
1
-package util
2
-
3
-import (
4
-	"strings"
5
-	"testing"
6
-)
7
-
8
-func TestReadInputFromTerminal(t *testing.T) {
9
-	testcases := map[string]struct {
10
-		Input  string
11
-		Output string
12
-	}{
13
-		"empty":                             {},
14
-		"empty newline":                     {Input: "\n"},
15
-		"empty windows newline":             {Input: "\r\n"},
16
-		"empty newline with extra":          {Input: "\nextra"},
17
-		"leading space":                     {Input: " data", Output: " data"},
18
-		"leading space newline":             {Input: " data\n", Output: " data"},
19
-		"leading space windows newline":     {Input: " data\r\n", Output: " data"},
20
-		"leading space newline with extra":  {Input: " data\nextra", Output: " data"},
21
-		"trailing space":                    {Input: " data ", Output: " data "},
22
-		"trailing space newline":            {Input: " data \n", Output: " data "},
23
-		"trailing space windows newline":    {Input: " data \r\n", Output: " data "},
24
-		"trailing space newline with extra": {Input: " data \nextra", Output: " data "},
25
-	}
26
-
27
-	for k, tc := range testcases {
28
-		output := readInputFromTerminal(strings.NewReader(tc.Input))
29
-		if output != tc.Output {
30
-			t.Errorf("%s: Expected '%s', got '%s'", k, tc.Output, output)
31
-		}
32
-	}
33
-}
... ...
@@ -11,7 +11,7 @@ import (
11 11
 
12 12
 	"github.com/golang/glog"
13 13
 
14
-	"github.com/openshift/origin/pkg/cmd/util"
14
+	"github.com/openshift/origin/pkg/cmd/util/term"
15 15
 )
16 16
 
17 17
 func BasicEnabled() bool {
... ...
@@ -70,12 +70,12 @@ func (c *BasicChallengeHandler) HandleChallenge(requestURL string, headers http.
70 70
 			fmt.Fprintf(w, "Authentication required for %s\n", c.Host)
71 71
 		}
72 72
 		if missingUsername {
73
-			username = util.PromptForString(c.Reader, w, "Username: ")
73
+			username = term.PromptForString(c.Reader, w, "Username: ")
74 74
 		} else {
75 75
 			fmt.Fprintf(w, "Username: %s\n", username)
76 76
 		}
77 77
 		if missingPassword {
78
-			password = util.PromptForPasswordString(c.Reader, w, "Password: ")
78
+			password = term.PromptForPasswordString(c.Reader, w, "Password: ")
79 79
 		}
80 80
 		// remember so we don't re-prompt
81 81
 		c.prompted = true