working commit

This commit is contained in:
2026-03-13 19:02:42 +02:00
parent bebbf79c7a
commit 5c1da77f4c
1329 changed files with 314708 additions and 39 deletions
+214
View File
@@ -0,0 +1,214 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package i18n
import (
"archive/zip"
"bytes"
"embed"
"errors"
"fmt"
"os"
"strings"
"sync"
"github.com/chai2010/gettext-go"
"k8s.io/klog/v2"
)
//go:embed translations
var translations embed.FS
var knownTranslations = map[string][]string{
"kubectl": {
"default",
"en_US",
"fr_FR",
"zh_CN",
"ja_JP",
"zh_TW",
"it_IT",
"de_DE",
"ko_KR",
"pt_BR",
},
// only used for unit tests.
"test": {
"default",
"en_US",
},
}
var (
lazyLoadTranslationsOnce sync.Once
LoadTranslationsFunc = func() error {
return LoadTranslations("kubectl", nil)
}
translationsLoaded bool
)
// SetLoadTranslationsFunc sets the function called to lazy load translations.
// It must be called in an init() func that occurs BEFORE any i18n.T() calls are made by any package. You can
// accomplish this by creating a separate package containing your init() func, and then importing that package BEFORE
// any other packages that call i18n.T().
//
// Example Usage:
//
// package myi18n
//
// import "k8s.io/kubectl/pkg/util/i18n"
//
// func init() {
// if err := i18n.SetLoadTranslationsFunc(loadCustomTranslations); err != nil {
// panic(err)
// }
// }
//
// func loadCustomTranslations() error {
// // Load your custom translations here...
// }
//
// And then in your main or root command package, import your custom package like this:
//
// import (
// // Other imports that don't need i18n...
// _ "example.com/myapp/myi18n"
// // Other imports that do need i18n...
// )
func SetLoadTranslationsFunc(f func() error) error {
if translationsLoaded {
return errors.New("translations have already been loaded")
}
LoadTranslationsFunc = func() error {
if err := f(); err != nil {
return err
}
translationsLoaded = true
return nil
}
return nil
}
func loadSystemLanguage() string {
// Implements the following locale priority order: LC_ALL, LC_MESSAGES, LANG
// Similarly to: https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
langStr := os.Getenv("LC_ALL")
if langStr == "" {
langStr = os.Getenv("LC_MESSAGES")
}
if langStr == "" {
langStr = os.Getenv("LANG")
}
if langStr == "" {
klog.V(3).Infof("Couldn't find the LC_ALL, LC_MESSAGES or LANG environment variables, defaulting to en_US")
return "default"
}
pieces := strings.Split(langStr, ".")
if len(pieces) != 2 {
klog.V(3).Infof("Unexpected system language (%s), defaulting to en_US", langStr)
return "default"
}
return pieces[0]
}
func findLanguage(root string, getLanguageFn func() string) string {
langStr := getLanguageFn()
translations := knownTranslations[root]
for ix := range translations {
if translations[ix] == langStr {
return langStr
}
}
klog.V(3).Infof("Couldn't find translations for %s, using default", langStr)
return "default"
}
// LoadTranslations loads translation files. getLanguageFn should return a language
// string (e.g. 'en-US'). If getLanguageFn is nil, then the loadSystemLanguage function
// is used, which uses the 'LANG' environment variable.
func LoadTranslations(root string, getLanguageFn func() string) error {
if getLanguageFn == nil {
getLanguageFn = loadSystemLanguage
}
langStr := findLanguage(root, getLanguageFn)
translationFiles := []string{
fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.po", root, langStr),
fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.mo", root, langStr),
}
// TODO: list the directory and load all files.
buf := new(bytes.Buffer)
w := zip.NewWriter(buf)
// Make sure to check the error on Close.
for _, file := range translationFiles {
filename := "translations/" + file
f, err := w.Create(file)
if err != nil {
return err
}
data, err := translations.ReadFile(filename)
if err != nil {
return err
}
if _, err := f.Write(data); err != nil {
return nil
}
}
if err := w.Close(); err != nil {
return err
}
gettext.BindLocale(gettext.New("k8s", root+".zip", buf.Bytes()))
gettext.SetDomain("k8s")
gettext.SetLanguage(langStr)
translationsLoaded = true
return nil
}
func lazyLoadTranslations() {
lazyLoadTranslationsOnce.Do(func() {
if translationsLoaded {
return
}
if err := LoadTranslationsFunc(); err != nil {
klog.Warning("Failed to load translations")
}
})
}
// T translates a string, possibly substituting arguments into it along
// the way. If len(args) is > 0, args1 is assumed to be the plural value
// and plural translation is used.
func T(defaultValue string, args ...int) string {
lazyLoadTranslations()
if len(args) == 0 {
return gettext.PGettext("", defaultValue)
}
return fmt.Sprintf(gettext.PNGettext("", defaultValue, defaultValue+".plural", args[0]),
args[0])
}
// Errorf produces an error with a translated error string.
// Substitution is performed via the `T` function above, following
// the same rules.
func Errorf(defaultValue string, args ...int) error {
return errors.New(T(defaultValue, args...))
}
+7
View File
@@ -0,0 +1,7 @@
# See the OWNERS docs at https://go.k8s.io/owners
reviewers: []
approvers:
- sig-cli-maintainers
emeritus_approvers:
- brendandburns
+82
View File
@@ -0,0 +1,82 @@
# Translations README
This is a basic sketch of the workflow needed to add translations:
# Adding/Updating Translations
## New languages
Create `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/kubectl/<language>/LC_MESSAGES/k8s.po`. There's
no need to update `translations/test/...` which is only used for unit tests.
There is an example [PR here](https://github.com/kubernetes/kubernetes/pull/40645) which adds support for French.
Once you've added a new language, you'll need to register it in
`staging/src/k8s.io/kubectl/pkg/util/i18n/i18n.go` by adding it to the `knownTranslations` map.
## Wrapping strings
There is a simple script in `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/extract.py` that performs
simple regular expression based wrapping of strings. It can always
use improvements to understand additional strings.
## Extracting strings
Once the strings are wrapped, you can extract strings from go files using
the `go-xgettext` command which can be installed with:
```console
go get github.com/gosexy/gettext/go-xgettext
```
Once that's installed you can run `./hack/update-translations.sh`, which
will extract and sort any new strings.
## Adding new translations
Edit the appropriate `k8s.po` file, `poedit` is a popular open source tool
for translations. You can load the `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/kubectl/template.pot` file
to find messages that might be missing.
Once you are done with your `k8s.po` file, generate the corresponding `k8s.mo`
file. `poedit` does this automatically on save, but you can also run
`./hack/update-translations.sh` to perform the `po` to `mo` translation.
We use the English translation as the `msgid`.
## Regenerating the bindata file
> Note: Regeneration of bindata is no more necessary for Kubernetes 1.22+ as
> the translations are now embedded into the binary at compile time.
> See: https://github.com/kubernetes/kubernetes/pull/99829
With the `mo` files up to date, you can now convert the generated files
into code using `go-bindata` command which can be installed with:
```console
go get github.com/go-bindata/go-bindata/...
```
Run `./hack/generate-bindata.sh`, this will turn the translation files
into generated code which will in turn be packaged into the Kubernetes
binaries.
## Extracting strings
There is a script in `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/extract.py` that knows how to do some
simple extraction. It needs a lot of work.
# Using translations
To use translations, you simply need to add:
```go
import pkg/i18n
...
// Get a translated string
translated := i18n.T("Your message in english here")
// Get a translated plural string
translated := i18n.T("You had %d items", items)
// Translated error
return i18n.Error("Something bad happened")
// Translated plural error
return i18n.Error("%d bad things happened")
```
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Extract strings from command files and externalize into translation files.
Expects to be run from the root directory of the repository.
Usage:
extract.py pkg/kubectl/cmd/apply.go
"""
import fileinput
import sys
import re
class MatchHandler(object):
""" Simple holder for a regular expression and a function
to run if that regular expression matches a line.
The function should expect (re.match, file, linenumber) as parameters
"""
def __init__(self, regex, replace_fn):
self.regex = re.compile(regex)
self.replace_fn = replace_fn
def short_replace(match, file, line_number):
"""Replace a Short: ... cobra command description with an internationalization
"""
sys.stdout.write('{}i18n.T({}),\n'.format(match.group(1), match.group(2)))
SHORT_MATCH = MatchHandler(r'(\s+Short:\s+)("[^"]+"),', short_replace)
def import_replace(match, file, line_number):
"""Add an extra import for the i18n library.
Doesn't try to be smart and detect if it's already present, assumes a
gofmt round wil fix things.
"""
sys.stdout.write('{}\n"k8s.io/kubectl/pkg/util/i18n"\n'.format(match.group(1)))
IMPORT_MATCH = MatchHandler('(.*"k8s.io/kubectl/pkg/cmd/util")', import_replace)
def string_flag_replace(match, file, line_number):
"""Replace a cmd.Flags().String("...", "", "...") with an internationalization
"""
sys.stdout.write('{}i18n.T("{})"))\n'.format(match.group(1), match.group(2)))
STRING_FLAG_MATCH = MatchHandler('(\s+cmd\.Flags\(\).String\("[^"]*", "[^"]*", )"([^"]*)"\)', string_flag_replace)
def long_string_replace(match, file, line_number):
return '{}i18n.T({}){}'.format(match.group(1), match.group(2), match.group(3))
LONG_DESC_MATCH = MatchHandler('(LongDesc\()(`[^`]+`)([^\n]\n)', long_string_replace)
EXAMPLE_MATCH = MatchHandler('(Examples\()(`[^`]+`)([^\n]\n)', long_string_replace)
def replace(filename, matchers, multiline_matchers):
"""Given a file and a set of matchers, run those matchers
across the file and replace it with the results.
"""
# Run all the matchers
line_number = 0
for line in fileinput.input(filename, inplace=True):
line_number += 1
matched = False
for matcher in matchers:
match = matcher.regex.match(line)
if match:
matcher.replace_fn(match, filename, line_number)
matched = True
break
if not matched:
sys.stdout.write(line)
sys.stdout.flush()
with open(filename, 'r') as datafile:
content = datafile.read()
for matcher in multiline_matchers:
match = matcher.regex.search(content)
while match:
rep = matcher.replace_fn(match, filename, 0)
# Escape back references in the replacement string
# (And escape for Python)
# (And escape for regex)
rep = re.sub('\\\\(\\d)', '\\\\\\\\\\1', rep)
content = matcher.regex.sub(rep, content, 1)
match = matcher.regex.search(content)
sys.stdout.write(content)
# gofmt the file again
from subprocess import call
call(["goimports", "-w", filename])
replace(sys.argv[1], [SHORT_MATCH, IMPORT_MATCH, STRING_FLAG_MATCH], [LONG_DESC_MATCH, EXAMPLE_MATCH])
+6
View File
@@ -0,0 +1,6 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- sig-cli-maintainers
reviewers:
- sig-cli-reviewers
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,103 @@
# Test translations for unit tests.
# Copyright (C) 2016
# This file is distributed under the same license as the Kubernetes package.
# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
#
msgid ""
msgstr ""
"Project-Id-Version: gettext-go-examples-hello\n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2021-07-07 20:15+0200\n"
"PO-Revision-Date: 2017-01-29 22:54-0800\n"
"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.6.10\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_cluster.go#L38
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go:42
msgid "Delete the specified cluster from the kubeconfig"
msgstr "Supprimer le cluster spécifié du kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_context.go#L38
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go:42
msgid "Delete the specified context from the kubeconfig"
msgstr "Supprimer le contexte spécifié du kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_contexts.go#L62
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go:72
msgid "Describe one or many contexts"
msgstr "Décrire un ou plusieurs contextes"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_clusters.go#L40
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go:41
msgid "Display clusters defined in the kubeconfig"
msgstr "Afficher les cluster définis dans kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/view.go#L64
#: staging/src/k8s.io/kubectl/pkg/cmd/config/view.go:81
msgid "Display merged kubeconfig settings or a specified kubeconfig file"
msgstr ""
"Afficher les paramètres fusionnés de kubeconfig ou d'un fichier kubeconfig "
"spécifié"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/config.go#L39
#: staging/src/k8s.io/kubectl/pkg/cmd/config/config.go:42
msgid "Modify kubeconfig files"
msgstr "Modifier des fichiers kubeconfig"
#: staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go:135
msgid "Update the annotations on a resource"
msgstr "Mettre à jour les annotations d'une ressource"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/apply.go#L98
#~ msgid "Apply a configuration to a resource by filename or stdin"
#~ msgstr ""
#~ "Appliquer une configuration à une ressource par nom de fichier ou depuis "
#~ "stdin"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/current_context.go#L48
#~ msgid "Displays the current-context"
#~ msgstr "Affiche le contexte actuel"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_cluster.go#L67
#~ msgid "Sets a cluster entry in kubeconfig"
#~ msgstr "Définit un cluster dans kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_context.go#L57
#~ msgid "Sets a context entry in kubeconfig"
#~ msgstr "Définit un contexte dans kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_authinfo.go#L103
#~ msgid "Sets a user entry in kubeconfig"
#~ msgstr "Définit un utilisateur dans kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/set.go#L59
#~ msgid "Sets an individual value in a kubeconfig file"
#~ msgstr "Définit une valeur individuelle dans un fichier kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/use_context.go#L48
#~ msgid "Sets the current-context in a kubeconfig file"
#~ msgstr "Définit le contexte courant dans un fichier kubeconfig"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/unset.go#L47
#~ msgid "Unsets an individual value in a kubeconfig file"
#~ msgstr "Supprime une valeur individuelle dans un fichier kubeconfig"
#~ msgid ""
#~ "watch is only supported on individual resources and resource collections "
#~ "- %d resources were found"
#~ msgid_plural ""
#~ "watch is only supported on individual resources and resource collections "
#~ "- %d resources were found"
#~ msgstr[0] ""
#~ "watch n'est compatible qu'avec les ressources individuelles et les "
#~ "collections de ressources. - %d ressource a été trouvée. "
#~ msgstr[1] ""
#~ "watch n'est compatible qu'avec les ressources individuelles et les "
#~ "collections de ressources. - %d ressources ont été trouvées. "
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,96 @@
# Test translations for unit tests.
# Copyright (C) 2017
# This file is distributed under the same license as the Kubernetes package.
# FIRST AUTHOR ianyrchoi@gmail.com, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: gettext-go-examples-hello\n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2021-07-07 20:15+0200\n"
"PO-Revision-Date: 2018-04-03 06:05+0900\n"
"Last-Translator: Ian Y. Choi <ianyrchoi@gmail.com>\n"
"Language-Team: \n"
"Language: ko_KR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Plural-Forms: nplurals=1; plural=0;\n"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_cluster.go#L38
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go:42
msgid "Delete the specified cluster from the kubeconfig"
msgstr "kubeconfig에서 지정된 클러스터를 삭제합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_context.go#L38
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go:42
msgid "Delete the specified context from the kubeconfig"
msgstr "kubeconfig에서 지정된 컨텍스트를 삭제합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_contexts.go#L62
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go:72
msgid "Describe one or many contexts"
msgstr "하나 또는 여러 컨텍스트를 설명합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_clusters.go#L40
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go:41
msgid "Display clusters defined in the kubeconfig"
msgstr "kubeconfig에 정의된 클러스터를 표시합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/view.go#L64
#: staging/src/k8s.io/kubectl/pkg/cmd/config/view.go:81
msgid "Display merged kubeconfig settings or a specified kubeconfig file"
msgstr "병합된 kubeconfig 설정 또는 지정된 kubeconfig 파일을 표시합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/config.go#L39
#: staging/src/k8s.io/kubectl/pkg/cmd/config/config.go:42
msgid "Modify kubeconfig files"
msgstr "kubeconfig 파일을 수정합니다"
#: staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go:135
msgid "Update the annotations on a resource"
msgstr "자원에 대한 주석을 업데이트합니다"
# https://github.com/kubernetes/kubernetes/blob/masterpkg/kubectl/cmd/apply.go#L98
#~ msgid "Apply a configuration to a resource by filename or stdin"
#~ msgstr "구성을 파일 이름 또는 stdin에 의한 자원에 적용합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/current_context.go#L48
#~ msgid "Displays the current-context"
#~ msgstr "현재-컨텍스트를 표시합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_cluster.go#L67
#~ msgid "Sets a cluster entry in kubeconfig"
#~ msgstr "kubeconfig에서 클러스터 항목을 설정합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_context.go#L57
#~ msgid "Sets a context entry in kubeconfig"
#~ msgstr "kubeconfig에서 컨텍스트 항목을 설정합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_authinfo.go#L103
#~ msgid "Sets a user entry in kubeconfig"
#~ msgstr "kubeconfig에서 사용자 항목을 설정합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/set.go#L59
#~ msgid "Sets an individual value in a kubeconfig file"
#~ msgstr "kubeconfig 파일에서 단일값을 설정합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/use_context.go#L48
#~ msgid "Sets the current-context in a kubeconfig file"
#~ msgstr "kubeconfig 파일에서 현재-컨텍스트를 설정합니다"
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/unset.go#L47
#~ msgid "Unsets an individual value in a kubeconfig file"
#~ msgstr "kubeconfig 파일에서 단일값 설정을 해제합니다"
#~ msgid ""
#~ "watch is only supported on individual resources and resource collections "
#~ "- %d resources were found"
#~ msgid_plural ""
#~ "watch is only supported on individual resources and resource collections "
#~ "- %d resources were found"
#~ msgstr[0] ""
#~ "watch는 단일 리소스와 리소스 모음만을 지원합니다 - %d 개 자원을 발견하였습"
#~ "니다"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,81 @@
# Test translations for unit tests.
# Copyright (C) 2017
# This file is distributed under the same license as the Kubernetes package.
# FIRST AUTHOR warmchang@outlook.com, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: hello-world\n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2021-07-07 20:15+0200\n"
"PO-Revision-Date: 2017-06-02 09:13+0800\n"
"Last-Translator: William Chang <warmchang@outlook.com>\n"
"Language-Team: \n"
"Language: zh\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.2\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go:42
msgid "Delete the specified cluster from the kubeconfig"
msgstr "刪除 kubeconfig 檔案中指定的叢集(cluster)"
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go:42
msgid "Delete the specified context from the kubeconfig"
msgstr "刪除 kubeconfig 檔案中指定的 context"
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go:72
msgid "Describe one or many contexts"
msgstr "描述一個或多個 context"
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go:41
msgid "Display clusters defined in the kubeconfig"
msgstr "顯示 kubeconfig 檔案中定義的叢集(cluster)"
#: staging/src/k8s.io/kubectl/pkg/cmd/config/view.go:81
msgid "Display merged kubeconfig settings or a specified kubeconfig file"
msgstr "顯示合併的 kubeconfig 配置或一個指定的 kubeconfig 檔案"
#: staging/src/k8s.io/kubectl/pkg/cmd/config/config.go:42
msgid "Modify kubeconfig files"
msgstr "修改 kubeconfig 檔案"
#: staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go:135
msgid "Update the annotations on a resource"
msgstr "更新一個資源的注解(annotations)"
#~ msgid "Apply a configuration to a resource by filename or stdin"
#~ msgstr "通過檔案名或標準輸入流(stdin)對資源進行配置"
#~ msgid "Displays the current-context"
#~ msgstr "顯示目前的 context"
#~ msgid "Sets a cluster entry in kubeconfig"
#~ msgstr "設置 kubeconfig 檔案中的一個叢集(cluster)條目"
#~ msgid "Sets a context entry in kubeconfig"
#~ msgstr "設置 kubeconfig 檔案中的一個 context 條目"
#~ msgid "Sets a user entry in kubeconfig"
#~ msgstr "設置 kubeconfig 檔案中的一個使用者條目"
#~ msgid "Sets an individual value in a kubeconfig file"
#~ msgstr "設置 kubeconfig 檔案中的一個值"
#~ msgid "Sets the current-context in a kubeconfig file"
#~ msgstr "設置 kubeconfig 檔案中的目前 context"
#~ msgid "Unsets an individual value in a kubeconfig file"
#~ msgstr "取消設置 kubeconfig 檔案中的一個值"
#~ msgid ""
#~ "watch is only supported on individual resources and resource collections "
#~ "- %d resources were found"
#~ msgid_plural ""
#~ "watch is only supported on individual resources and resource collections "
#~ "- %d resources were found"
#~ msgstr[0] "一次只能 watch 一個資源或資料集合 - 找到了 %d 個資源"
#~ msgstr[1] "一次只能 watch 一個資源或資料集合 - 找到了 %d 個資源"
Binary file not shown.
@@ -0,0 +1,28 @@
# Test translations for unit tests.
# Copyright (C) 2016
# This file is distributed under the same license as the Kubernetes package.
# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
#
msgid ""
msgstr ""
"Project-Id-Version: gettext-go-examples-hello\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-12-12 20:03+0000\n"
"PO-Revision-Date: 2016-12-13 21:35-0800\n"
"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.6.10\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: en\n"
msgid "test_plural"
msgid_plural "test_plural"
msgstr[0] "there was %d item"
msgstr[1] "there were %d items"
msgid "test_string"
msgstr "foo"
Binary file not shown.
@@ -0,0 +1,28 @@
# Test translations for unit tests.
# Copyright (C) 2016
# This file is distributed under the same license as the Kubernetes package.
# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
#
msgid ""
msgstr ""
"Project-Id-Version: gettext-go-examples-hello\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-12-12 20:03+0000\n"
"PO-Revision-Date: 2016-12-13 22:12-0800\n"
"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.6.10\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: en\n"
msgid "test_plural"
msgid_plural "test_plural"
msgstr[0] "there was %d item"
msgstr[1] "there were %d items"
msgid "test_string"
msgstr "baz"
+104
View File
@@ -0,0 +1,104 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package interrupt
import (
"os"
"os/signal"
"sync"
"syscall"
)
// terminationSignals are signals that cause the program to exit in the
// supported platforms (linux, darwin, windows).
var terminationSignals = []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT}
// Handler guarantees execution of notifications after a critical section (the function passed
// to a Run method), even in the presence of process termination. It guarantees exactly once
// invocation of the provided notify functions.
type Handler struct {
notify []func()
final func(os.Signal)
once sync.Once
}
// Chain creates a new handler that invokes all notify functions when the critical section exits
// and then invokes the optional handler's notifications. This allows critical sections to be
// nested without losing exactly once invocations. Notify functions can invoke any cleanup needed
// but should not exit (which is the responsibility of the parent handler).
func Chain(handler *Handler, notify ...func()) *Handler {
if handler == nil {
return New(nil, notify...)
}
return New(handler.Signal, append(notify, handler.Close)...)
}
// New creates a new handler that guarantees all notify functions are run after the critical
// section exits (or is interrupted by the OS), then invokes the final handler. If no final
// handler is specified, the default final is `os.Exit(1)`. A handler can only be used for
// one critical section.
func New(final func(os.Signal), notify ...func()) *Handler {
return &Handler{
final: final,
notify: notify,
}
}
// Close executes all the notification handlers if they have not yet been executed.
func (h *Handler) Close() {
h.once.Do(func() {
for _, fn := range h.notify {
fn()
}
})
}
// Signal is called when an os.Signal is received, and guarantees that all notifications
// are executed, then the final handler is executed. This function should only be called once
// per Handler instance.
func (h *Handler) Signal(s os.Signal) {
h.once.Do(func() {
for _, fn := range h.notify {
fn()
}
if h.final == nil {
os.Exit(1)
}
h.final(s)
})
}
// Run ensures that any notifications are invoked after the provided fn exits (even if the
// process is interrupted by an OS termination signal). Notifications are only invoked once
// per Handler instance, so calling Run more than once will not behave as the user expects.
func (h *Handler) Run(fn func() error) error {
ch := make(chan os.Signal, 1)
signal.Notify(ch, terminationSignals...)
defer func() {
signal.Stop(ch)
close(ch)
}()
go func() {
sig, ok := <-ch
if !ok {
return
}
h.Signal(sig)
}()
defer h.Close()
return fn()
}
+6
View File
@@ -0,0 +1,6 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- apelisse
reviewers:
- apelisse
+21
View File
@@ -0,0 +1,21 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package openapi is a collection of libraries for fetching the openapi spec
// from a Kubernetes server and then indexing the type definitions.
// The openapi spec contains the object model definitions and extensions metadata
// such as the patchStrategy and patchMergeKey for creating patches.
package openapi
+177
View File
@@ -0,0 +1,177 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
openapi_v2 "github.com/google/gnostic-models/openapiv2"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/util/proto"
"sigs.k8s.io/yaml"
)
// OpenAPIResourcesGetter represents a function to return
// OpenAPI V2 resource specifications. Used for lazy-loading
// these resource specifications.
type OpenAPIResourcesGetter interface {
OpenAPISchema() (Resources, error)
}
// Resources interface describe a resources provider, that can give you
// resource based on group-version-kind.
type Resources interface {
LookupResource(gvk schema.GroupVersionKind) proto.Schema
GetConsumes(gvk schema.GroupVersionKind, operation string) []string
}
// groupVersionKindExtensionKey is the key used to lookup the
// GroupVersionKind value for an object definition from the
// definition's "extensions" map.
const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
// document is an implementation of `Resources`. It looks for
// resources in an openapi Schema.
type document struct {
// Maps gvk to model name
resources map[schema.GroupVersionKind]string
models proto.Models
doc *openapi_v2.Document
}
var _ Resources = &document{}
// NewOpenAPIData creates a new `Resources` out of the openapi document
func NewOpenAPIData(doc *openapi_v2.Document) (Resources, error) {
models, err := proto.NewOpenAPIData(doc)
if err != nil {
return nil, err
}
resources := map[schema.GroupVersionKind]string{}
for _, modelName := range models.ListModels() {
model := models.LookupModel(modelName)
if model == nil {
panic("ListModels returns a model that can't be looked-up.")
}
gvkList := parseGroupVersionKind(model)
for _, gvk := range gvkList {
if len(gvk.Kind) > 0 {
resources[gvk] = modelName
}
}
}
return &document{
resources: resources,
models: models,
doc: doc,
}, nil
}
func (d *document) LookupResource(gvk schema.GroupVersionKind) proto.Schema {
modelName, found := d.resources[gvk]
if !found {
return nil
}
return d.models.LookupModel(modelName)
}
func (d *document) GetConsumes(gvk schema.GroupVersionKind, operation string) []string {
for _, path := range d.doc.GetPaths().GetPath() {
for _, ex := range path.GetValue().GetPatch().GetVendorExtension() {
if ex.GetValue().GetYaml() == "" ||
ex.GetName() != "x-kubernetes-group-version-kind" {
continue
}
var value map[string]string
err := yaml.Unmarshal([]byte(ex.GetValue().GetYaml()), &value)
if err != nil {
continue
}
if value["group"] == gvk.Group && value["kind"] == gvk.Kind && value["version"] == gvk.Version {
switch operation {
case "GET":
return path.GetValue().GetGet().GetConsumes()
case "PATCH":
return path.GetValue().GetPatch().GetConsumes()
case "HEAD":
return path.GetValue().GetHead().GetConsumes()
case "PUT":
return path.GetValue().GetPut().GetConsumes()
case "POST":
return path.GetValue().GetPost().GetConsumes()
case "OPTIONS":
return path.GetValue().GetOptions().GetConsumes()
case "DELETE":
return path.GetValue().GetDelete().GetConsumes()
}
}
}
}
return nil
}
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
func parseGroupVersionKind(s proto.Schema) []schema.GroupVersionKind {
extensions := s.GetExtensions()
gvkListResult := []schema.GroupVersionKind{}
// Get the extensions
gvkExtension, ok := extensions[groupVersionKindExtensionKey]
if !ok {
return []schema.GroupVersionKind{}
}
// gvk extension must be a list of at least 1 element.
gvkList, ok := gvkExtension.([]interface{})
if !ok {
return []schema.GroupVersionKind{}
}
for _, gvk := range gvkList {
// gvk extension list must be a map with group, version, and
// kind fields
gvkMap, ok := gvk.(map[interface{}]interface{})
if !ok {
continue
}
group, ok := gvkMap["group"].(string)
if !ok {
continue
}
version, ok := gvkMap["version"].(string)
if !ok {
continue
}
kind, ok := gvkMap["kind"].(string)
if !ok {
continue
}
gvkListResult = append(gvkListResult, schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
})
}
return gvkListResult
}
+82
View File
@@ -0,0 +1,82 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"sync"
openapi_v2 "github.com/google/gnostic-models/openapiv2"
"k8s.io/client-go/discovery"
)
// CachedOpenAPIGetter fetches the openapi schema once and then caches it in memory
type CachedOpenAPIGetter struct {
openAPIClient discovery.OpenAPISchemaInterface
// Cached results
sync.Once
openAPISchema *openapi_v2.Document
err error
}
var _ discovery.OpenAPISchemaInterface = &CachedOpenAPIGetter{}
// NewOpenAPIGetter returns an object to return OpenAPIDatas which reads
// from a server, and then stores in memory for subsequent invocations
func NewOpenAPIGetter(openAPIClient discovery.OpenAPISchemaInterface) *CachedOpenAPIGetter {
return &CachedOpenAPIGetter{
openAPIClient: openAPIClient,
}
}
// OpenAPISchema implements OpenAPISchemaInterface.
func (g *CachedOpenAPIGetter) OpenAPISchema() (*openapi_v2.Document, error) {
g.Do(func() {
g.openAPISchema, g.err = g.openAPIClient.OpenAPISchema()
})
// Return the saved result.
return g.openAPISchema, g.err
}
type CachedOpenAPIParser struct {
openAPIClient discovery.OpenAPISchemaInterface
// Cached results
sync.Once
openAPIResources Resources
err error
}
func NewOpenAPIParser(openAPIClient discovery.OpenAPISchemaInterface) *CachedOpenAPIParser {
return &CachedOpenAPIParser{
openAPIClient: openAPIClient,
}
}
func (p *CachedOpenAPIParser) Parse() (Resources, error) {
p.Do(func() {
oapi, err := p.openAPIClient.OpenAPISchema()
if err != nil {
p.err = err
return
}
p.openAPIResources, p.err = NewOpenAPIData(oapi)
})
return p.openAPIResources, p.err
}
+59
View File
@@ -0,0 +1,59 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package templates
import (
"github.com/spf13/cobra"
)
type CommandGroup struct {
Message string
Commands []*cobra.Command
}
type CommandGroups []CommandGroup
func (g CommandGroups) Add(c *cobra.Command) {
for _, group := range g {
c.AddCommand(group.Commands...)
}
}
func (g CommandGroups) Has(c *cobra.Command) bool {
for _, group := range g {
for _, command := range group.Commands {
if command == c {
return true
}
}
}
return false
}
func AddAdditionalCommands(g CommandGroups, message string, cmds []*cobra.Command) CommandGroups {
group := CommandGroup{Message: message}
for _, c := range cmds {
// Don't show commands that have no short description
if !g.Has(c) && len(c.Short) != 0 {
group.Commands = append(group.Commands, c)
}
}
if len(group.Commands) == 0 {
return g
}
return append(g, group)
}
+76
View File
@@ -0,0 +1,76 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package templates
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/mitchellh/go-wordwrap"
flag "github.com/spf13/pflag"
)
const offset = 10
// HelpFlagPrinter is a printer that
// processes the help flag and print
// it to i/o writer
type HelpFlagPrinter struct {
wrapLimit uint
out io.Writer
}
// NewHelpFlagPrinter will initialize a HelpFlagPrinter given the
// i/o writer
func NewHelpFlagPrinter(out io.Writer, wrapLimit uint) *HelpFlagPrinter {
return &HelpFlagPrinter{
wrapLimit: wrapLimit,
out: out,
}
}
// PrintHelpFlag will beautify the help flags and print it out to p.out
func (p *HelpFlagPrinter) PrintHelpFlag(flag *flag.Flag) {
formatBuf := new(bytes.Buffer)
writeFlag(formatBuf, flag)
wrappedStr := formatBuf.String()
flagAndUsage := strings.Split(formatBuf.String(), "\n")
flagStr := flagAndUsage[0]
// if the flag usage is longer than one line, wrap it again
if len(flagAndUsage) > 1 {
nextLines := strings.Join(flagAndUsage[1:], " ")
wrappedUsages := wordwrap.WrapString(nextLines, p.wrapLimit-offset)
wrappedStr = flagStr + "\n" + wrappedUsages
}
appendTabStr := strings.ReplaceAll(wrappedStr, "\n", "\n\t")
fmt.Fprint(p.out, appendTabStr+"\n\n")
}
// writeFlag will output the help flag based
// on the format provided by getFlagFormat to i/o writer
func writeFlag(out io.Writer, f *flag.Flag) {
deprecated := ""
if f.Deprecated != "" {
deprecated = fmt.Sprintf(" (DEPRECATED: %s)", f.Deprecated)
}
fmt.Fprintf(out, getFlagFormat(f), f.Shorthand, f.Name, f.DefValue, f.Usage, deprecated)
}
+116
View File
@@ -0,0 +1,116 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package templates
import (
"fmt"
"io"
"strings"
"github.com/russross/blackfriday/v2"
)
const linebreak = "\n"
// ASCIIRenderer implements blackfriday.Renderer
var _ blackfriday.Renderer = &ASCIIRenderer{}
// ASCIIRenderer is a blackfriday.Renderer intended for rendering markdown
// documents as plain text, well suited for human reading on terminals.
type ASCIIRenderer struct {
Indentation string
listItemCount uint
listLevel uint
}
// render markdown to text
func (r *ASCIIRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
switch node.Type {
case blackfriday.Text:
raw := string(node.Literal)
lines := strings.Split(raw, linebreak)
for _, line := range lines {
trimmed := strings.Trim(line, " \n\t")
if len(trimmed) > 0 && trimmed[0] != '_' {
w.Write([]byte(" "))
}
w.Write([]byte(trimmed))
}
case blackfriday.HorizontalRule, blackfriday.Hardbreak:
w.Write([]byte(linebreak + "----------" + linebreak))
case blackfriday.Code, blackfriday.CodeBlock:
w.Write([]byte(linebreak))
lines := []string{}
for _, line := range strings.Split(string(node.Literal), linebreak) {
trimmed := strings.Trim(line, " \t")
// Adding 4 times of indentation will let blackfriday to accept
// this literal as Code or CodeBlock again in next invocation
indented := strings.Repeat(r.Indentation, 4) + trimmed
lines = append(lines, indented)
}
w.Write([]byte(strings.Join(lines, linebreak)))
case blackfriday.Image:
w.Write(node.LinkData.Destination)
case blackfriday.Link:
w.Write([]byte(" "))
w.Write(node.LinkData.Destination)
case blackfriday.Paragraph:
if r.listLevel == 0 {
w.Write([]byte(linebreak))
}
case blackfriday.List:
if entering {
w.Write([]byte(linebreak))
r.listLevel++
} else {
r.listLevel--
r.listItemCount = 0
}
case blackfriday.Item:
if entering {
r.listItemCount++
for i := 0; uint(i) < r.listLevel; i++ {
w.Write([]byte(r.Indentation))
}
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
w.Write([]byte(fmt.Sprintf("%d. ", r.listItemCount)))
} else {
w.Write([]byte("* "))
}
} else {
w.Write([]byte(linebreak))
}
default:
normalText(w, node.Literal)
}
return blackfriday.GoToNext
}
func normalText(w io.Writer, text []byte) {
w.Write([]byte(strings.Trim(string(text), " \n\t")))
}
// RenderHeader writes document preamble and TOC if requested.
func (r *ASCIIRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
}
// RenderFooter writes document footer.
func (r *ASCIIRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
io.WriteString(w, "\n")
}
+97
View File
@@ -0,0 +1,97 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package templates
import (
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/russross/blackfriday/v2"
"github.com/spf13/cobra"
)
const Indentation = ` `
// LongDesc normalizes a command's long description to follow the conventions.
func LongDesc(s string) string {
if len(s) == 0 {
return s
}
return normalizer{s}.heredoc().markdown().trim().string
}
// Examples normalizes a command's examples to follow the conventions.
func Examples(s string) string {
if len(s) == 0 {
return s
}
return normalizer{s}.trim().indent().string
}
// Normalize perform all required normalizations on a given command.
func Normalize(cmd *cobra.Command) *cobra.Command {
if len(cmd.Long) > 0 {
cmd.Long = LongDesc(cmd.Long)
}
if len(cmd.Example) > 0 {
cmd.Example = Examples(cmd.Example)
}
return cmd
}
// NormalizeAll perform all required normalizations in the entire command tree.
func NormalizeAll(cmd *cobra.Command) *cobra.Command {
if cmd.HasSubCommands() {
for _, subCmd := range cmd.Commands() {
NormalizeAll(subCmd)
}
}
Normalize(cmd)
return cmd
}
type normalizer struct {
string
}
func (s normalizer) markdown() normalizer {
bytes := []byte(s.string)
formatted := blackfriday.Run(bytes, blackfriday.WithExtensions(blackfriday.NoIntraEmphasis), blackfriday.WithRenderer(&ASCIIRenderer{Indentation: Indentation}))
s.string = string(formatted)
return s
}
func (s normalizer) heredoc() normalizer {
s.string = heredoc.Doc(s.string)
return s
}
func (s normalizer) trim() normalizer {
s.string = strings.TrimSpace(s.string)
return s
}
func (s normalizer) indent() normalizer {
indentedLines := []string{}
for _, line := range strings.Split(s.string, "\n") {
trimmed := strings.TrimSpace(line)
indented := Indentation + trimmed
indentedLines = append(indentedLines, indented)
}
s.string = strings.Join(indentedLines, "\n")
return s
}
+319
View File
@@ -0,0 +1,319 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package templates
import (
"bytes"
"fmt"
"strings"
"text/template"
"unicode"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"k8s.io/kubectl/pkg/util/term"
)
type FlagExposer interface {
ExposeFlags(cmd *cobra.Command, flags ...string) FlagExposer
}
func ActsAsRootCommand(cmd *cobra.Command, filters []string, groups ...CommandGroup) FlagExposer {
if cmd == nil {
panic("nil root command")
}
templater := &templater{
RootCmd: cmd,
UsageTemplate: MainUsageTemplate(),
HelpTemplate: MainHelpTemplate(),
CommandGroups: groups,
Filtered: filters,
}
cmd.SetFlagErrorFunc(templater.FlagErrorFunc())
cmd.SilenceUsage = true
cmd.SetUsageFunc(templater.UsageFunc())
cmd.SetHelpFunc(templater.HelpFunc())
return templater
}
func UseOptionsTemplates(cmd *cobra.Command) {
templater := &templater{
UsageTemplate: OptionsUsageTemplate(),
HelpTemplate: OptionsHelpTemplate(),
}
cmd.SetUsageFunc(templater.UsageFunc())
cmd.SetHelpFunc(templater.HelpFunc())
}
type templater struct {
UsageTemplate string
HelpTemplate string
RootCmd *cobra.Command
CommandGroups
Filtered []string
}
func (templater *templater) FlagErrorFunc(exposedFlags ...string) func(*cobra.Command, error) error {
return func(c *cobra.Command, err error) error {
c.SilenceUsage = true
switch c.CalledAs() {
case "options":
return fmt.Errorf("%s\nRun '%s' without flags.", err, c.CommandPath())
default:
return fmt.Errorf("%s\nSee '%s --help' for usage.", err, c.CommandPath())
}
}
}
func (templater *templater) ExposeFlags(cmd *cobra.Command, flags ...string) FlagExposer {
cmd.SetUsageFunc(templater.UsageFunc(flags...))
return templater
}
func (templater *templater) HelpFunc() func(*cobra.Command, []string) {
return func(c *cobra.Command, s []string) {
t := template.New("help")
t.Funcs(templater.templateFuncs())
template.Must(t.Parse(templater.HelpTemplate))
out := term.NewResponsiveWriter(c.OutOrStdout())
err := t.Execute(out, c)
if err != nil {
c.Println(err)
}
}
}
func (templater *templater) UsageFunc(exposedFlags ...string) func(*cobra.Command) error {
return func(c *cobra.Command) error {
t := template.New("usage")
t.Funcs(templater.templateFuncs(exposedFlags...))
template.Must(t.Parse(templater.UsageTemplate))
out := term.NewResponsiveWriter(c.OutOrStderr())
return t.Execute(out, c)
}
}
func (templater *templater) templateFuncs(exposedFlags ...string) template.FuncMap {
return template.FuncMap{
"trim": strings.TrimSpace,
"trimRight": func(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) },
"trimLeft": func(s string) string { return strings.TrimLeftFunc(s, unicode.IsSpace) },
"gt": cobra.Gt,
"eq": cobra.Eq,
"rpad": rpad,
"appendIfNotPresent": appendIfNotPresent,
"flagsNotIntersected": flagsNotIntersected,
"visibleFlags": visibleFlags,
"flagsUsages": flagsUsages,
"cmdGroups": templater.cmdGroups,
"cmdGroupsString": templater.cmdGroupsString,
"rootCmd": templater.rootCmdName,
"isRootCmd": templater.isRootCmd,
"optionsCmdFor": templater.optionsCmdFor,
"usageLine": templater.usageLine,
"reverseParentsNames": templater.reverseParentsNames,
"exposed": func(c *cobra.Command) *flag.FlagSet {
exposed := flag.NewFlagSet("exposed", flag.ContinueOnError)
if len(exposedFlags) > 0 {
for _, name := range exposedFlags {
if flag := c.Flags().Lookup(name); flag != nil {
exposed.AddFlag(flag)
}
}
}
return exposed
},
}
}
func (templater *templater) cmdGroups(c *cobra.Command, all []*cobra.Command) []CommandGroup {
if len(templater.CommandGroups) > 0 && c == templater.RootCmd {
all = filter(all, templater.Filtered...)
return AddAdditionalCommands(templater.CommandGroups, "Other Commands:", all)
}
all = filter(all, "options")
return []CommandGroup{
{
Message: "Available Commands:",
Commands: all,
},
}
}
func (t *templater) cmdGroupsString(c *cobra.Command) string {
groups := []string{}
for _, cmdGroup := range t.cmdGroups(c, c.Commands()) {
cmds := []string{cmdGroup.Message}
for _, cmd := range cmdGroup.Commands {
if cmd.IsAvailableCommand() {
cmds = append(cmds, " "+rpad(cmd.Name(), cmd.NamePadding())+" "+cmd.Short)
}
}
groups = append(groups, strings.Join(cmds, "\n"))
}
return strings.Join(groups, "\n\n")
}
func (t *templater) rootCmdName(c *cobra.Command) string {
return t.rootCmd(c).CommandPath()
}
func (t *templater) reverseParentsNames(c *cobra.Command) []string {
reverseParentsNames := []string{}
parents := t.parents(c)
for i := len(parents) - 1; i >= 0; i-- {
reverseParentsNames = append(reverseParentsNames, parents[i].Name())
}
return reverseParentsNames
}
func (t *templater) isRootCmd(c *cobra.Command) bool {
return t.rootCmd(c) == c
}
func (t *templater) parents(c *cobra.Command) []*cobra.Command {
parents := []*cobra.Command{c}
for current := c; !t.isRootCmd(current) && current.HasParent(); {
current = current.Parent()
parents = append(parents, current)
}
return parents
}
func (t *templater) rootCmd(c *cobra.Command) *cobra.Command {
if c != nil && !c.HasParent() {
return c
}
if t.RootCmd == nil {
panic("nil root cmd")
}
return t.RootCmd
}
func (t *templater) optionsCmdFor(c *cobra.Command) string {
if !c.Runnable() {
return ""
}
rootCmdStructure := t.parents(c)
for i := len(rootCmdStructure) - 1; i >= 0; i-- {
cmd := rootCmdStructure[i]
if _, _, err := cmd.Find([]string{"options"}); err == nil {
return cmd.CommandPath() + " options"
}
}
return ""
}
func (t *templater) usageLine(c *cobra.Command) string {
usage := c.UseLine()
suffix := "[options]"
if c.HasFlags() && !strings.Contains(usage, suffix) {
usage += " " + suffix
}
return usage
}
// flagsUsages will print out the kubectl help flags
func flagsUsages(f *flag.FlagSet) (string, error) {
flagBuf := new(bytes.Buffer)
wrapLimit, err := term.GetWordWrapperLimit()
if err != nil {
wrapLimit = 0
}
printer := NewHelpFlagPrinter(flagBuf, wrapLimit)
f.VisitAll(func(flag *flag.Flag) {
if flag.Hidden {
return
}
printer.PrintHelpFlag(flag)
})
return flagBuf.String(), nil
}
// getFlagFormat will output the flag format
func getFlagFormat(f *flag.Flag) string {
var format string
format = "--%s=%s:\n%s%s"
if f.Value.Type() == "string" {
format = "--%s='%s':\n%s%s"
}
if len(f.Shorthand) > 0 {
format = " -%s, " + format
} else {
format = " %s" + format
}
return format
}
func rpad(s string, padding int) string {
template := fmt.Sprintf("%%-%ds", padding)
return fmt.Sprintf(template, s)
}
func appendIfNotPresent(s, stringToAppend string) string {
if strings.Contains(s, stringToAppend) {
return s
}
return s + " " + stringToAppend
}
func flagsNotIntersected(l *flag.FlagSet, r *flag.FlagSet) *flag.FlagSet {
f := flag.NewFlagSet("notIntersected", flag.ContinueOnError)
l.VisitAll(func(flag *flag.Flag) {
if r.Lookup(flag.Name) == nil {
f.AddFlag(flag)
}
})
return f
}
func visibleFlags(l *flag.FlagSet) *flag.FlagSet {
hidden := "help"
f := flag.NewFlagSet("visible", flag.ContinueOnError)
l.VisitAll(func(flag *flag.Flag) {
if flag.Name != hidden {
f.AddFlag(flag)
}
})
return f
}
func filter(cmds []*cobra.Command, names ...string) []*cobra.Command {
out := []*cobra.Command{}
for _, c := range cmds {
if c.Hidden {
continue
}
skip := false
for _, name := range names {
if name == c.Name() {
skip = true
break
}
}
if skip {
continue
}
out = append(out, c)
}
return out
}
+104
View File
@@ -0,0 +1,104 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package templates
import (
"strings"
"unicode"
)
const (
// SectionVars is the help template section that declares variables to be used in the template.
SectionVars = `{{$isRootCmd := isRootCmd .}}` +
`{{$rootCmd := rootCmd .}}` +
`{{$visibleFlags := visibleFlags (flagsNotIntersected .LocalFlags .PersistentFlags)}}` +
`{{$explicitlyExposedFlags := exposed .}}` +
`{{$optionsCmdFor := optionsCmdFor .}}` +
`{{$usageLine := usageLine .}}` +
`{{$reverseParentsNames := reverseParentsNames .}}`
// SectionAliases is the help template section that displays command aliases.
SectionAliases = `{{if gt .Aliases 0}}Aliases:
{{.NameAndAliases}}
{{end}}`
// SectionExamples is the help template section that displays command examples.
SectionExamples = `{{if .HasExample}}Examples:
{{trimRight .Example}}
{{end}}`
// SectionSubcommands is the help template section that displays the command's subcommands.
SectionSubcommands = `{{if .HasAvailableSubCommands}}{{cmdGroupsString .}}
{{end}}`
// SectionFlags is the help template section that displays the command's flags.
SectionFlags = `{{ if or $visibleFlags.HasFlags $explicitlyExposedFlags.HasFlags}}Options:
{{ if $visibleFlags.HasFlags}}{{trimRight (flagsUsages $visibleFlags)}}{{end}}{{ if $explicitlyExposedFlags.HasFlags}}{{ if $visibleFlags.HasFlags}}
{{end}}{{trimRight (flagsUsages $explicitlyExposedFlags)}}{{end}}
{{end}}`
// SectionUsage is the help template section that displays the command's usage.
SectionUsage = `{{if and .Runnable (ne .UseLine "") (ne .UseLine $rootCmd)}}Usage:
{{$usageLine}}
{{end}}`
// SectionTipsHelp is the help template section that displays the '--help' hint.
SectionTipsHelp = `{{if .HasSubCommands}}Use "{{range $reverseParentsNames}}{{.}} {{end}}<command> --help" for more information about a given command.
{{end}}`
// SectionTipsGlobalOptions is the help template section that displays the 'options' hint for displaying global flags.
SectionTipsGlobalOptions = `{{if $optionsCmdFor}}Use "{{$optionsCmdFor}}" for a list of global command-line options (applies to all commands).
{{end}}`
)
// MainHelpTemplate if the template for 'help' used by most commands.
func MainHelpTemplate() string {
return `{{with or .Long .Short }}{{. | trim}}{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
}
// MainUsageTemplate if the template for 'usage' used by most commands.
func MainUsageTemplate() string {
sections := []string{
"\n\n",
SectionVars,
SectionAliases,
SectionExamples,
SectionSubcommands,
SectionFlags,
SectionUsage,
SectionTipsHelp,
SectionTipsGlobalOptions,
}
return strings.TrimRightFunc(strings.Join(sections, ""), unicode.IsSpace)
}
// OptionsHelpTemplate if the template for 'help' used by the 'options' command.
func OptionsHelpTemplate() string {
return ""
}
// OptionsUsageTemplate if the template for 'usage' used by the 'options' command.
func OptionsUsageTemplate() string {
return `{{ if .HasInheritedFlags}}The following options can be passed to any command:
{{flagsUsages .InheritedFlags}}{{end}}`
}
+148
View File
@@ -0,0 +1,148 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"fmt"
"github.com/moby/term"
"k8s.io/apimachinery/pkg/util/runtime"
)
// TerminalSize represents the width and height of a terminal.
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSize.
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
type TerminalSize struct {
Width uint16
Height uint16
}
// TerminalSizeQueue is capable of returning terminal resize events as they occur.
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSizeQueue.
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
type TerminalSizeQueue interface {
// Next returns the new terminal size after the terminal has been resized. It returns nil when
// monitoring has been stopped.
Next() *TerminalSize
}
// GetSize returns the current size of the user's terminal. If it isn't a terminal,
// nil is returned.
func (t TTY) GetSize() *TerminalSize {
outFd, isTerminal := term.GetFdInfo(t.Out)
if !isTerminal {
return nil
}
return GetSize(outFd)
}
// GetSize returns the current size of the terminal associated with fd.
func GetSize(fd uintptr) *TerminalSize {
winsize, err := term.GetWinsize(fd)
if err != nil {
runtime.HandleError(fmt.Errorf("unable to get terminal size: %v", err))
return nil
}
return &TerminalSize{Width: winsize.Width, Height: winsize.Height}
}
// MonitorSize monitors the terminal's size. It returns a TerminalSizeQueue primed with
// initialSizes, or nil if there's no TTY present.
func (t *TTY) MonitorSize(initialSizes ...*TerminalSize) TerminalSizeQueue {
outFd, isTerminal := term.GetFdInfo(t.Out)
if !isTerminal {
return nil
}
t.sizeQueue = &sizeQueue{
t: *t,
// make it buffered so we can send the initial terminal sizes without blocking, prior to starting
// the streaming below
resizeChan: make(chan TerminalSize, len(initialSizes)),
stopResizing: make(chan struct{}),
}
t.sizeQueue.monitorSize(outFd, initialSizes...)
return t.sizeQueue
}
// sizeQueue implements remotecommand.TerminalSizeQueue
type sizeQueue struct {
t TTY
// resizeChan receives a Size each time the user's terminal is resized.
resizeChan chan TerminalSize
stopResizing chan struct{}
}
// make sure sizeQueue implements the TerminalSizeQueue interface
var _ TerminalSizeQueue = &sizeQueue{}
// monitorSize primes resizeChan with initialSizes and then monitors for resize events. With each
// new event, it sends the current terminal size to resizeChan.
func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*TerminalSize) {
// send the initial sizes
for i := range initialSizes {
if initialSizes[i] != nil {
s.resizeChan <- *initialSizes[i]
}
}
resizeEvents := make(chan TerminalSize, 1)
monitorResizeEvents(outFd, resizeEvents, s.stopResizing)
// listen for resize events in the background
go func() {
defer runtime.HandleCrash()
for {
select {
case size, ok := <-resizeEvents:
if !ok {
return
}
select {
// try to send the size to resizeChan, but don't block
case s.resizeChan <- size:
// send successful
default:
// unable to send / no-op
}
case <-s.stopResizing:
return
}
}
}()
}
// Next returns the new terminal size after the terminal has been resized. It returns nil when
// monitoring has been stopped.
func (s *sizeQueue) Next() *TerminalSize {
size, ok := <-s.resizeChan
if !ok {
return nil
}
return &size
}
// stop stops the background goroutine that is monitoring for terminal resizes.
func (s *sizeQueue) stop() {
close(s.stopResizing)
}
+61
View File
@@ -0,0 +1,61 @@
//go:build !windows
// +build !windows
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"os"
"os/signal"
"golang.org/x/sys/unix"
"k8s.io/apimachinery/pkg/util/runtime"
)
// monitorResizeEvents spawns a goroutine that waits for SIGWINCH signals (these indicate the
// terminal has resized). After receiving a SIGWINCH, this gets the terminal size and tries to send
// it to the resizeEvents channel. The goroutine stops when the stop channel is closed.
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
go func() {
defer runtime.HandleCrash()
winch := make(chan os.Signal, 1)
signal.Notify(winch, unix.SIGWINCH)
defer signal.Stop(winch)
for {
select {
case <-winch:
size := GetSize(fd)
if size == nil {
return
}
// try to send size
select {
case resizeEvents <- *size:
// success
default:
// not sent
}
case <-stop:
return
}
}
}()
}
+61
View File
@@ -0,0 +1,61 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"time"
"k8s.io/apimachinery/pkg/util/runtime"
)
// monitorResizeEvents spawns a goroutine that periodically gets the terminal size and tries to send
// it to the resizeEvents channel if the size has changed. The goroutine stops when the stop channel
// is closed.
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
go func() {
defer runtime.HandleCrash()
size := GetSize(fd)
if size == nil {
return
}
lastSize := *size
for {
// see if we need to stop running
select {
case <-stop:
return
default:
}
size := GetSize(fd)
if size == nil {
return
}
if size.Height != lastSize.Height || size.Width != lastSize.Width {
lastSize.Height = size.Height
lastSize.Width = size.Width
resizeEvents <- *size
}
// sleep to avoid hot looping
time.Sleep(250 * time.Millisecond)
}
}()
}
+115
View File
@@ -0,0 +1,115 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"io"
"os"
"k8s.io/cli-runtime/pkg/printers"
"github.com/moby/term"
"k8s.io/kubectl/pkg/util/interrupt"
)
// SafeFunc is a function to be invoked by TTY.
type SafeFunc func() error
// TTY helps invoke a function and preserve the state of the terminal, even if the process is
// terminated during execution. It also provides support for terminal resizing for remote command
// execution/attachment.
type TTY struct {
// In is a reader representing stdin. It is a required field.
In io.Reader
// Out is a writer representing stdout. It must be set to support terminal resizing. It is an
// optional field.
Out io.Writer
// Raw is true if the terminal should be set raw.
Raw bool
// TryDev indicates the TTY should try to open /dev/tty if the provided input
// is not a file descriptor.
TryDev bool
// Parent is an optional interrupt handler provided to this function - if provided
// it will be invoked after the terminal state is restored. If it is not provided,
// a signal received during the TTY will result in os.Exit(0) being invoked.
Parent *interrupt.Handler
// sizeQueue is set after a call to MonitorSize() and is used to monitor SIGWINCH signals when the
// user's terminal resizes.
sizeQueue *sizeQueue
}
// IsTerminalIn returns true if t.In is a terminal. Does not check /dev/tty
// even if TryDev is set.
func (t TTY) IsTerminalIn() bool {
return printers.IsTerminal(t.In)
}
// IsTerminalOut returns true if t.Out is a terminal. Does not check /dev/tty
// even if TryDev is set.
func (t TTY) IsTerminalOut() bool {
return printers.IsTerminal(t.Out)
}
// IsTerminal returns whether the passed object is a terminal or not.
// Deprecated: use printers.IsTerminal instead.
var IsTerminal = printers.IsTerminal
// AllowsColorOutput returns true if the specified writer is a terminal and
// the process environment indicates color output is supported and desired.
// Deprecated: use printers.AllowsColorOutput instead.
var AllowsColorOutput = printers.AllowsColorOutput
// Safe invokes the provided function and will attempt to ensure that when the
// function returns (or a termination signal is sent) that the terminal state
// is reset to the condition it was in prior to the function being invoked. If
// t.Raw is true the terminal will be put into raw mode prior to calling the function.
// If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file
// will be opened (if available).
func (t TTY) Safe(fn SafeFunc) error {
inFd, isTerminal := term.GetFdInfo(t.In)
if !isTerminal && t.TryDev {
if f, err := os.Open("/dev/tty"); err == nil {
defer f.Close()
inFd = f.Fd()
isTerminal = term.IsTerminal(inFd)
}
}
if !isTerminal {
return fn()
}
var state *term.State
var err error
if t.Raw {
state, err = term.MakeRaw(inFd)
} else {
state, err = term.SaveState(inFd)
}
if err != nil {
return err
}
return interrupt.Chain(t.Parent, func() {
if t.sizeQueue != nil {
t.sizeQueue.stop()
}
term.RestoreTerminal(inFd, state)
}).Run(fn)
}
+144
View File
@@ -0,0 +1,144 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"errors"
"io"
"os"
wordwrap "github.com/mitchellh/go-wordwrap"
"github.com/moby/term"
)
type wordWrapWriter struct {
limit uint
writer io.Writer
}
// NewResponsiveWriter creates a Writer that detects the column width of the
// terminal we are in, and adjusts every line width to fit and use recommended
// terminal sizes for better readability. Does proper word wrapping automatically.
//
// if terminal width >= 120 columns use 120 columns
// if terminal width >= 100 columns use 100 columns
// if terminal width >= 80 columns use 80 columns
//
// In case we're not in a terminal or if it's smaller than 80 columns width,
// doesn't do any wrapping.
func NewResponsiveWriter(w io.Writer) io.Writer {
file, ok := w.(*os.File)
if !ok {
return w
}
fd := file.Fd()
if !term.IsTerminal(fd) {
return w
}
terminalSize := GetSize(fd)
if terminalSize == nil {
return w
}
limit := getTerminalLimitWidth(terminalSize)
return NewWordWrapWriter(w, limit)
}
// NewWordWrapWriter is a Writer that supports a limit of characters on every line
// and does auto word wrapping that respects that limit.
func NewWordWrapWriter(w io.Writer, limit uint) io.Writer {
return &wordWrapWriter{
limit: limit,
writer: w,
}
}
func getTerminalLimitWidth(terminalSize *TerminalSize) uint {
var limit uint
switch {
case terminalSize.Width >= 120:
limit = 120
case terminalSize.Width >= 100:
limit = 100
case terminalSize.Width >= 80:
limit = 80
}
return limit
}
func GetWordWrapperLimit() (uint, error) {
stdout := os.Stdout
fd := stdout.Fd()
if !term.IsTerminal(fd) {
return 0, errors.New("file descriptor is not a terminal")
}
terminalSize := GetSize(fd)
if terminalSize == nil {
return 0, errors.New("terminal size is nil")
}
return getTerminalLimitWidth(terminalSize), nil
}
func (w wordWrapWriter) Write(p []byte) (nn int, err error) {
if w.limit == 0 {
return w.writer.Write(p)
}
original := string(p)
wrapped := wordwrap.WrapString(original, w.limit)
return w.writer.Write([]byte(wrapped))
}
// NewPunchCardWriter is a NewWordWrapWriter that limits the line width to 80 columns.
func NewPunchCardWriter(w io.Writer) io.Writer {
return NewWordWrapWriter(w, 80)
}
type maxWidthWriter struct {
maxWidth uint
currentWidth uint
written uint
writer io.Writer
}
// NewMaxWidthWriter is a Writer that supports a limit of characters on every
// line, but doesn't do any word wrapping automatically.
func NewMaxWidthWriter(w io.Writer, maxWidth uint) io.Writer {
return &maxWidthWriter{
maxWidth: maxWidth,
writer: w,
}
}
func (m maxWidthWriter) Write(p []byte) (nn int, err error) {
for _, b := range p {
if m.currentWidth == m.maxWidth {
m.writer.Write([]byte{'\n'})
m.currentWidth = 0
}
if b == '\n' {
m.currentWidth = 0
}
_, err := m.writer.Write([]byte{b})
if err != nil {
return int(m.written), err
}
m.written++
m.currentWidth++
}
return len(p), nil
}