Add go template shell completion for --format

The --format flags accepts go template strings. I use this often but I
consistently forget the field names. This commit adds a way to provide
shell completion for the --format flag. It works by automatically
receiving the field names with the reflect package from the given
struct. This requires almost no maintenance since this ensures that we
always use the correct field names. This also works for nested structs.

```
$ podman ps --format "{{.P"
{{.Pid}}      {{.PIDNS}}    {{.Pod}}      {{.PodName}}  {{.Ports}}
```

NOTE: This only works when you use quotes otherwise the shell does not
provide completions. Also this does not work for fish at the moment.

Signed-off-by: Paul Holzinger <paul.holzinger@web.de>
This commit is contained in:
Paul Holzinger 2021-04-21 11:32:03 +02:00
parent 979f047d73
commit d81021ed26
27 changed files with 258 additions and 28 deletions

View File

@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"os"
"reflect"
"strings"
"github.com/containers/common/pkg/config"
@ -891,11 +892,86 @@ func AutocompleteNetworkFlag(cmd *cobra.Command, args []string, toComplete strin
return append(networks, suggestions...), dir
}
// AutocompleteJSONFormat - Autocomplete format flag option.
// -> "json"
func AutocompleteJSONFormat(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// AutocompleteFormat - Autocomplete json or a given struct to use for a go template.
// The input can be nil, In this case only json will be autocompleted.
// This function will only work for structs other types are not supported.
// When "{{." is typed the field and method names of the given struct will be completed.
// This also works recursive for nested structs.
func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// this function provides shell completion for go templates
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// autocomplete json when nothing or json is typed
if strings.HasPrefix("json", toComplete) {
return []string{"json"}, cobra.ShellCompDirectiveNoFileComp
}
// no input struct we cannot provide completion return nothing
if o == nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
// toComplete could look like this: {{ .Config }} {{ .Field.F
// 1. split the template variable delimiter
vars := strings.Split(toComplete, "{{")
if len(vars) == 1 {
// no variables return no completion
return nil, cobra.ShellCompDirectiveNoFileComp
}
// clean the spaces from the last var
field := strings.Split(vars[len(vars)-1], " ")
// split this into it struct field names
fields := strings.Split(field[len(field)-1], ".")
f := reflect.ValueOf(o)
for i := 1; i < len(fields); i++ {
if f.Kind() == reflect.Ptr {
f = f.Elem()
}
// // the only supported type is struct
if f.Kind() != reflect.Struct {
return nil, cobra.ShellCompDirectiveNoFileComp
}
// last field get all names to suggest
if i == len(fields)-1 {
suggestions := []string{}
for j := 0; j < f.NumField(); j++ {
fname := f.Type().Field(j).Name
suffix := "}}"
kind := f.Type().Field(j).Type.Kind()
if kind == reflect.Ptr {
// make sure to read the actual type when it is a pointer
kind = f.Type().Field(j).Type.Elem().Kind()
}
// when we have a nested struct do not append braces instead append a dot
if kind == reflect.Struct {
suffix = "."
}
if strings.HasPrefix(fname, fields[i]) {
// add field name with closing braces
suggestions = append(suggestions, fname+suffix)
}
}
for j := 0; j < f.NumMethod(); j++ {
fname := f.Type().Method(j).Name
if strings.HasPrefix(fname, fields[i]) {
// add method name with closing braces
suggestions = append(suggestions, fname+"}}")
}
}
// add the current toComplete value in front so that the shell can complete this correctly
toCompArr := strings.Split(toComplete, ".")
toCompArr[len(toCompArr)-1] = ""
toComplete = strings.Join(toCompArr, ".")
return prefixSlice(toComplete, suggestions), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
// set the next struct field
f = f.FieldByName(fields[i])
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// AutocompleteEventFilter - Autocomplete event filter flag options.
// -> "container=", "event=", "image=", "pod=", "volume=", "type="

View File

@ -0,0 +1,142 @@
package common_test
import (
"testing"
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
type Car struct {
Brand string
Stats struct {
HP *int
Displacement int
}
Extras map[string]string
}
func (c Car) Type() string {
return ""
}
func (c Car) Color() string {
return ""
}
func TestAutocompleteFormat(t *testing.T) {
testStruct := struct {
Name string
Age int
Car *Car
}{}
testStruct.Car = &Car{}
testStruct.Car.Extras = map[string]string{"test": "1"}
tests := []struct {
name string
toComplete string
expected []string
}{
{
"empty completion",
"",
[]string{"json"},
},
{
"json completion",
"json",
[]string{"json"},
},
{
"invalid completion",
"blahblah",
nil,
},
{
"invalid completion",
"{{",
nil,
},
{
"invalid completion",
"{{ ",
nil,
},
{
"invalid completion",
"{{ ..",
nil,
},
{
"fist level struct field name",
"{{.",
[]string{"{{.Name}}", "{{.Age}}", "{{.Car."},
},
{
"fist level struct field name",
"{{ .",
[]string{"{{ .Name}}", "{{ .Age}}", "{{ .Car."},
},
{
"fist level struct field name",
"{{ .N",
[]string{"{{ .Name}}"},
},
{
"second level struct field name",
"{{ .Car.",
[]string{"{{ .Car.Brand}}", "{{ .Car.Stats.", "{{ .Car.Extras}}", "{{ .Car.Color}}", "{{ .Car.Type}}"},
},
{
"second level struct field name",
"{{ .Car.B",
[]string{"{{ .Car.Brand}}"},
},
{
"three level struct field name",
"{{ .Car.Stats.",
[]string{"{{ .Car.Stats.HP}}", "{{ .Car.Stats.Displacement}}"},
},
{
"three level struct field name",
"{{ .Car.Stats.D",
[]string{"{{ .Car.Stats.Displacement}}"},
},
{
"second level struct field name",
"{{ .Car.B",
[]string{"{{ .Car.Brand}}"},
},
{
"invalid field name",
"{{ .Ca.B",
nil,
},
{
"map key names don't work",
"{{ .Car.Extras.",
nil,
},
{
"two variables struct field name",
"{{ .Car.Brand }} {{ .Car.",
[]string{"{{ .Car.Brand }} {{ .Car.Brand}}", "{{ .Car.Brand }} {{ .Car.Stats.", "{{ .Car.Brand }} {{ .Car.Extras}}",
"{{ .Car.Brand }} {{ .Car.Color}}", "{{ .Car.Brand }} {{ .Car.Type}}"},
},
{
"only dot without variable",
".",
nil,
},
}
for _, test := range tests {
completion, directive := common.AutocompleteFormat(testStruct)(nil, nil, test.toComplete)
// directive should always be greater than ShellCompDirectiveNoFileComp
assert.GreaterOrEqual(t, directive, cobra.ShellCompDirectiveNoFileComp, "unexpected ShellCompDirective")
assert.Equal(t, test.expected, completion, test.name)
}
}

View File

@ -39,7 +39,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format")
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
validate.AddLatestFlag(diffCmd, &diffOpts.Latest)
}

View File

@ -5,6 +5,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/inspect"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/spf13/cobra"
)
@ -35,7 +36,12 @@ func init() {
formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.InspectContainerData{
State: &define.InspectContainerState{},
NetworkSettings: &define.InspectNetworkSettings{},
Config: &define.InspectContainerConfig{},
HostConfig: &define.InspectContainerHostConfig{},
}))
validate.AddLatestFlag(inspectCmd, &inspectOpts.Latest)
}

View File

@ -61,7 +61,7 @@ func mountFlags(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVar(&mountOpts.Format, formatFlagName, "", "Print the mounted containers in specified format (json)")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
flags.BoolVar(&mountOpts.NoTruncate, "notruncate", false, "Do not truncate output")
}

View File

@ -87,7 +87,7 @@ func listFlagSet(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVar(&listOpts.Format, formatFlagName, "", "Pretty-print containers to JSON or using a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.ListContainer{}))
lastFlagName := "last"
flags.IntVarP(&listOpts.Last, lastFlagName, "n", -1, "Print the n last created containers (all states)")

View File

@ -71,7 +71,7 @@ func statFlags(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVar(&statsOptions.Format, formatFlagName, "", "Pretty-print container statistics to JSON or using a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.ContainerStats{}))
flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen between intervals")
flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false")

View File

@ -43,7 +43,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format")
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
validate.AddLatestFlag(diffCmd, &diffOpts.Latest)
}

View File

@ -72,7 +72,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&format, formatFlagName, "", "Print the created units in specified format (json)")
_ = systemdCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = systemdCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
flags.SetNormalizeFunc(utils.AliasFlags)
}

View File

@ -41,7 +41,7 @@ func diffFlags(flags *pflag.FlagSet) {
formatFlagName := "format"
flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format")
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
}
func diff(cmd *cobra.Command, args []string) error {

View File

@ -74,7 +74,7 @@ func historyFlags(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVar(&opts.format, formatFlagName, "", "Change the output to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.ImageHistoryLayer{}))
flags.BoolVarP(&opts.human, "human", "H", true, "Display sizes and dates in human readable format")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output")

View File

@ -5,6 +5,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/inspect"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/pkg/domain/entities"
inspectTypes "github.com/containers/podman/v3/pkg/inspect"
"github.com/spf13/cobra"
)
@ -34,7 +35,7 @@ func init() {
formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(inspectTypes.ImageData{}))
}
func inspectExec(cmd *cobra.Command, args []string) error {

View File

@ -83,7 +83,7 @@ func imageListFlagSet(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, "", "Change the output format to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.ImageSummary{}))
flags.BoolVar(&listFlag.digests, "digests", false, "Show digests")
flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings")

View File

@ -51,7 +51,7 @@ func mountFlags(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVar(&mountOpts.Format, formatFlagName, "", "Print the mounted images in specified format (json)")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
}
func init() {

View File

@ -33,7 +33,7 @@ func init() {
formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "", "Pretty-print network to JSON or using a Go template")
_ = networkinspectCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = networkinspectCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
}
func networkInspect(_ *cobra.Command, args []string) error {

View File

@ -41,7 +41,7 @@ var (
func networkListFlags(flags *pflag.FlagSet) {
formatFlagName := "format"
flags.StringVar(&networkListOptions.Format, formatFlagName, "", "Pretty-print networks to JSON or using a Go template")
_ = networklistCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = networklistCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(ListPrintReports{}))
flags.BoolVarP(&networkListOptions.Quiet, "quiet", "q", false, "display only names")
flags.BoolVar(&noTrunc, "no-trunc", false, "Do not truncate the network ID")

View File

@ -10,6 +10,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -44,7 +45,7 @@ func init() {
formatFlagName := "format"
flags.StringVarP(&inspectOptions.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.InspectPodData{}))
validate.AddLatestFlag(inspectCmd, &inspectOptions.Latest)
}

View File

@ -61,7 +61,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&psInput.Format, formatFlagName, "", "Pretty-print pods to JSON or using a Go template")
_ = psCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = psCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(ListPodReporter{}))
flags.Bool("noheading", false, "Do not print headers")
flags.BoolVar(&psInput.Namespace, "namespace", false, "Display namespace information of the pod")

View File

@ -58,7 +58,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&statsOptions.Format, formatFlagName, "", "Pretty-print container statistics to JSON or using a Go template")
_ = statsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = statsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.PodStatsReport{}))
flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen when streaming")
flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result")

View File

@ -40,7 +40,7 @@ func init() {
flags := inspectCmd.Flags()
formatFlagName := "format"
flags.StringVar(&format, formatFlagName, "", "Format volume output using Go template")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.SecretInfoReport{}))
}
func inspect(cmd *cobra.Command, args []string) error {

View File

@ -47,7 +47,7 @@ func init() {
flags := lsCmd.Flags()
formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, "{{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}\t\n", "Format volume output using Go template")
_ = lsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = lsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.SecretInfoReport{}))
flags.BoolVar(&listFlag.noHeading, "noheading", false, "Do not print headers")
}

View File

@ -52,7 +52,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&eventFormat, formatFlagName, "", "format the output using a Go template")
_ = eventsCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = eventsCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(events.Event{}))
flags.BoolVar(&eventOptions.Stream, "stream", true, "stream new events; for testing only")

View File

@ -10,6 +10,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/ghodss/yaml"
"github.com/spf13/cobra"
@ -68,7 +69,7 @@ func infoFlags(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVarP(&inFormat, formatFlagName, "f", "", "Change the output format to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.Info{Host: &define.HostInfo{}, Store: &define.StoreInfo{}}))
}
func info(cmd *cobra.Command, args []string) error {

View File

@ -38,7 +38,7 @@ func init() {
formatFlagName := "format"
flags.StringVarP(&versionFormat, formatFlagName, "f", "", "Change the output format to JSON or a Go template")
_ = versionCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = versionCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.SystemVersionReport{}))
}
func version(cmd *cobra.Command, args []string) error {

View File

@ -4,6 +4,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/inspect"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -41,7 +42,7 @@ func init() {
formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format volume output using Go template")
_ = inspectCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = inspectCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.InspectVolumeData{}))
}
func volumeInspect(cmd *cobra.Command, args []string) error {

View File

@ -14,6 +14,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/parse"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -60,7 +61,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&cliOpts.Format, formatFlagName, "{{.Driver}}\t{{.Name}}\n", "Format volume output using Go template")
_ = lsCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = lsCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.InspectVolumeData{}))
flags.Bool("noheading", false, "Do not print headers")
flags.BoolVarP(&cliOpts.Quiet, "quiet", "q", false, "Print volume output in quiet mode")

1
go.sum
View File

@ -213,6 +213,7 @@ github.com/containers/storage v1.23.5/go.mod h1:ha26Q6ngehFNhf3AWoXldvAvwI4jFe3E
github.com/containers/storage v1.24.8/go.mod h1:YC+2pY8SkfEAcZkwycxYbpK8EiRbx5soPPwz9dxe4IQ=
github.com/containers/storage v1.28.0/go.mod h1:ixAwO7Bj31cigqPEG7aCz+PYmxkDxbIFdUFioYdxbzI=
github.com/containers/storage v1.28.1/go.mod h1:5bwiMh2LkrN3AWIfDFMH7A/xbVNLcve+oeXYvHvW8cc=
github.com/containers/storage v1.29.0/go.mod h1:u84RU4CCufGeJBNTRNwMB+FoE+AiFeFw4SsMoqAOeCM=
github.com/containers/storage v1.30.0 h1:KS6zmoPyy0Qcx1HCCiseQ0ysSckRvtiuoVpIGh9iwQA=
github.com/containers/storage v1.30.0/go.mod h1:M/xn0pg6ReYFrLtWl5YELI/a4Xjq+Z3e5GJxQrJCcDI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=