<commit_before>// Copyright 2017-2020 The Usacloud 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 core

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"sync"

	"github.com/fatih/color"
	"github.com/sacloud/libsacloud/v2/sacloud/accessor"
	"github.com/sacloud/libsacloud/v2/sacloud/types"
	"github.com/sacloud/usacloud/pkg/cli"
	"github.com/sacloud/usacloud/pkg/cmd/cflag"
	"github.com/sacloud/usacloud/pkg/cmd/root"
	"github.com/sacloud/usacloud/pkg/cmd/services"
	"github.com/sacloud/usacloud/pkg/output"
	"github.com/sacloud/usacloud/pkg/printer"
	"github.com/sacloud/usacloud/pkg/term"
	"github.com/sacloud/usacloud/pkg/util"
	"github.com/sacloud/usacloud/pkg/validate"
	"github.com/spf13/cobra"
)

type SelectorType int

const (
	SelectorTypeNone          = iota // セレクタなし(List系/自前実装系など)
	SelectorTypeRequireSingle        // ID or Name or Tagsを受け取る(複数ヒットNG)
	SelectorTypeRequireMulti         // ID or Name or Tagsを受け取る(複数ヒットOK)
)

type ValidateFunc func(ctx cli.Context, parameter interface{}) error

// Command コマンド定義、実行時のコンテキスト(設定保持)にもなる
type Command struct {
	Name      string   // コマンド名、ケバブケース(ハイフン区切り)で指定すること
	Aliases   []string // エイリアス
	Usage     string
	ArgsUsage string // Argumentsの説明、省略した場合はSelectorTypeの値に応じた内容が設定される // TODO 未実装

	Category string // カテゴリーのキー
	Order    int    // コマンドが属するリソース内での並び順

	// コマンド動作関連
	SelectorType   SelectorType
	NoProgress     bool // コマンド実行時のプログレス表示の有無
	ConfirmMessage string

	// パラメータ関連
	ParameterCategories  []Category
	ParameterInitializer func() interface{}
	ServiceFuncAltName   string // デフォルトのlibsacloud service呼び出しコード生成用、空の場合はNameをCamelizeしたものが利用される // TODO libsacloud側で対応すべき

	// テーブル形式での出力対象列。省略した場合はIDとNameが出力される
	ColumnDefs []output.ColumnDef

	// TODO そのうちDeprecatedWarningとかも対応する
	ExperimentWarning string

	// 操作対象リソースの一覧取得用。通常はResourceに紐づけられたfuncを利用する
	ListAllFunc func(ctx cli.Context, parameter interface{}) ([]interface{}, error)

	// 特殊な引数補完をしたい場合に設定する
	CustomCompletionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)

	// カスタムバリデーション用。空の場合usacloud/pkg/validate.Execが実行される
	ValidateFunc ValidateFunc

	// コマンドの実処理。設定してない場合はデフォルトのlibsacloud service呼び出しが行われる
	Func func(ctx cli.Context, parameter interface{}) ([]interface{}, error)

	resource         *Resource
	currentParameter interface{}
}

func (c *Command) ValidateSchema() error {
	if c.resource == nil {
		return fmt.Errorf(`command "%s" has invalid schema: resource required`, c.Name)
	}
	if c.ParameterInitializer == nil {
		return fmt.Errorf(`command "%s %s" has invalid schema: ParameterInitializer required`, c.resource.Name, c.Name)
	}
	return nil
}

func (c *Command) CLICommand() *cobra.Command {
	c.currentParameter = c.ParameterInitializer()
	cmd := &cobra.Command{
		Use:          c.Name,
		Aliases:      c.Aliases,
		Short:        c.Usage,
		Long:         c.Usage,
		SilenceUsage: true,
		Run: func(cmd *cobra.Command, args []string) {
			// コンテキスト構築
			var err error
			ctx, cancel, needContinue, err := c.initCommandContext(cmd, args)
			if err != nil {
				// この段階ではctx.IO()が参照できないため標準エラーに出力する
				fmt.Fprintln(os.Stderr, err) // nolint
				return
			}
			defer cancel()

			if !needContinue {
				return
			}

			doneCh := make(chan error)
			defer close(doneCh)

			go func() {
				doneCh <- c.Run(ctx, cmd, args)
			}()

			select {
			case <-ctx.Done():
				err = fmt.Errorf("command[%s/%s] timed out: %s", c.resource.Name, c.Name, ctx.Err())
				break
			case e := <-doneCh:
				err = e
				break
			}
			if err != nil {
				out := ctx.IO().Err()
				fmt.Fprintln(out, "") // nolint // エラーの前後は常に改行させる
				(&printer.Printer{NoColor: ctx.Option().NoColor}).Fprint(out, color.New(color.FgHiRed), err)
				fmt.Fprintln(out, "") // nolint // エラーの前後は常に改行させる
				os.Exit(1)
			}
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			// カスタムFuncが登録されていたらそちらを優先
			if c.CustomCompletionFunc != nil {
				return c.CustomCompletionFunc(cmd, args, toComplete)
			}

			if c.SelectorType == SelectorTypeNone {
				return nil, cobra.ShellCompDirectiveNoFileComp
			}

			// コンテキスト構築
			ctx, cancel, needContinue, err := c.initCommandContext(cmd, args)
			if !needContinue || err != nil {
				return nil, cobra.ShellCompDirectiveError
			}
			defer cancel()

			return c.CompleteArgs(ctx, cmd, args, toComplete)
		},
	}

	if c, ok := c.currentParameter.(FlagInitializer); ok {
		c.SetupCobraCommandFlags(cmd)
	}

	cmd.InheritedFlags().SortFlags = false

	return cmd
}

func (c *Command) CompleteArgs(ctx cli.Context, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	// 各リソースのListAllFuncをコールし、toCompleteと前方一致するリソースを抽出
	zoned, err := c.collectResources(ctx)
	if err != nil {
		return nil, cobra.ShellCompDirectiveError
	}

	// ゾーンごとに分かれているため1つにまとめる
	var resources []interface{}
	for _, z := range zoned {
		resources = append(resources, z.resources...)<commit_msg>Set command usage<commit_after>// Copyright 2017-2020 The Usacloud 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 core

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"sync"

	"github.com/fatih/color"
	"github.com/sacloud/libsacloud/v2/sacloud/accessor"
	"github.com/sacloud/libsacloud/v2/sacloud/types"
	"github.com/sacloud/usacloud/pkg/cli"
	"github.com/sacloud/usacloud/pkg/cmd/cflag"
	"github.com/sacloud/usacloud/pkg/cmd/root"
	"github.com/sacloud/usacloud/pkg/cmd/services"
	"github.com/sacloud/usacloud/pkg/output"
	"github.com/sacloud/usacloud/pkg/printer"
	"github.com/sacloud/usacloud/pkg/term"
	"github.com/sacloud/usacloud/pkg/util"
	"github.com/sacloud/usacloud/pkg/validate"
	"github.com/spf13/cobra"
)

type SelectorType int

const (
	SelectorTypeNone          = iota // セレクタなし(List系/自前実装系など)
	SelectorTypeRequireSingle        // ID or Name or Tagsを受け取る(複数ヒットNG)
	SelectorTypeRequireMulti         // ID or Name or Tagsを受け取る(複数ヒットOK)
)

type ValidateFunc func(ctx cli.Context, parameter interface{}) error

// Command コマンド定義、実行時のコンテキスト(設定保持)にもなる
type Command struct {
	Name      string   // コマンド名、ケバブケース(ハイフン区切り)で指定すること
	Aliases   []string // エイリアス
	Usage     string
	ArgsUsage string // Argumentsの説明、省略した場合はSelectorTypeの値に応じた内容が設定される

	Category string // カテゴリーのキー
	Order    int    // コマンドが属するリソース内での並び順

	// コマンド動作関連
	SelectorType   SelectorType
	NoProgress     bool // コマンド実行時のプログレス表示の有無
	ConfirmMessage string

	// パラメータ関連
	ParameterCategories  []Category
	ParameterInitializer func() interface{}
	ServiceFuncAltName   string // デフォルトのlibsacloud service呼び出しコード生成用、空の場合はNameをCamelizeしたものが利用される // TODO libsacloud側で対応すべき

	// テーブル形式での出力対象列。省略した場合はIDとNameが出力される
	ColumnDefs []output.ColumnDef

	// TODO そのうちDeprecatedWarningとかも対応する
	ExperimentWarning string

	// 操作対象リソースの一覧取得用。通常はResourceに紐づけられたfuncを利用する
	ListAllFunc func(ctx cli.Context, parameter interface{}) ([]interface{}, error)

	// 特殊な引数補完をしたい場合に設定する
	CustomCompletionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)

	// カスタムバリデーション用。空の場合usacloud/pkg/validate.Execが実行される
	ValidateFunc ValidateFunc

	// コマンドの実処理。設定してない場合はデフォルトのlibsacloud service呼び出しが行われる
	Func func(ctx cli.Context, parameter interface{}) ([]interface{}, error)

	resource         *Resource
	currentParameter interface{}
}

func (c *Command) ValidateSchema() error {
	if c.resource == nil {
		return fmt.Errorf(`command "%s" has invalid schema: resource required`, c.Name)
	}
	if c.ParameterInitializer == nil {
		return fmt.Errorf(`command "%s %s" has invalid schema: ParameterInitializer required`, c.resource.Name, c.Name)
	}
	return nil
}

func (c *Command) argsUsage() string {
	// NOTE: cobra.Command#Useには"コマンド名 usage"のように最初のスペース以前がコマンド名となる値を設定しないと
	//       cobra.Command#Name()が正しい値を返さない仕様となっている。
	//       ref: https://github.com/spf13/cobra/blob/86f8bfd7fef868a174e1b606783bd7f5c82ddf8f/command.go#L1286-L1294
	if c.ArgsUsage != "" {
		usage := c.ArgsUsage
		if !strings.HasPrefix(usage, " ") {
			usage = " " + usage
		}
		return usage
	}

	if c.SelectorType == SelectorTypeNone {
		return ""
	}

	suffix := ""
	if c.SelectorType == SelectorTypeRequireMulti {
		suffix = "..."
	}
	return " { ID | NAME | TAG }" + suffix
}

func (c *Command) CLICommand() *cobra.Command {
	c.currentParameter = c.ParameterInitializer()
	cmd := &cobra.Command{
		Use:          c.Name + c.argsUsage(),
		Aliases:      c.Aliases,
		Short:        c.Usage,
		Long:         c.Usage,
		SilenceUsage: true,
		Run: func(cmd *cobra.Command, args []string) {
			// コンテキスト構築
			var err error
			ctx, cancel, needContinue, err := c.initCommandContext(cmd, args)
			if err != nil {
				// この段階ではctx.IO()が参照できないため標準エラーに出力する
				fmt.Fprintln(os.Stderr, err) // nolint
				return
			}
			defer cancel()

			if !needContinue {
				return
			}

			doneCh := make(chan error)
			defer close(doneCh)

			go func() {
				doneCh <- c.Run(ctx, cmd, args)
			}()

			select {
			case <-ctx.Done():
				err = fmt.Errorf("command[%s/%s] timed out: %s", c.resource.Name, c.Name, ctx.Err())
				break
			case e := <-doneCh:
				err = e
				break
			}
			if err != nil {
				out := ctx.IO().Err()
				fmt.Fprintln(out, "") // nolint // エラーの前後は常に改行させる
				(&printer.Printer{NoColor: ctx.Option().NoColor}).Fprint(out, color.New(color.FgHiRed), err)
				fmt.Fprintln(out, "") // nolint // エラーの前後は常に改行させる
				os.Exit(1)
			}
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			// カスタムFuncが登録されていたらそちらを優先
			if c.CustomCompletionFunc != nil {
				return c.CustomCompletionFunc(cmd, args, toComplete)
			}

			if c.SelectorType == SelectorTypeNone {
				return nil, cobra.ShellCompDirectiveNoFileComp
			}

			// コンテキスト構築
			ctx, cancel, needContinue, err := c.initCommandContext(cmd, args)
			if !needContinue || err != nil {
				return nil, cobra.ShellCompDirectiveError
			}
			defer cancel()

			return c.CompleteArgs(ctx, cmd, args, toComplete)
		},
	}

	if c, ok := c.currentParameter.(FlagInitializer); ok {
		c.SetupCobraCommandFlags(cmd)
	}

	cmd.InheritedFlags().SortFlags = false
	return cmd
}

func (c *Command) CompleteArgs(ctx cli.Context, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	// 各リソースのListAllFuncをコールし、toCompleteと前方一致するリソースを抽出
	zoned, err := c.collectResources(ctx)
	if err != nil {
		return nil, cobra.ShellCompDirectiveError
	}

	// ゾーンごとに分かれているため1つにまとめる
	var resources []interface{}
	for _, z := range zoned {
		resources = append(resources, z.resources...)
{'fraction_non_alphanumeric': 0.12061517193710039, 'fraction_numerical': 0.005875237601520649, 'mean_word_length': 3.703372612759041, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 3, 'https://': 1, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5074658', 'n_tokens_mistral': 5014, 'n_tokens_neox': 4670, 'n_words': 893}