123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
- package cli
- import (
- "fmt"
- "reflect"
- "regexp"
- "strconv"
- "strings"
- "kitty/tools/cli/markup"
- "kitty/tools/utils"
- )
- var _ = fmt.Print
- func camel_case_dest(x string) string {
- x = strings.ReplaceAll(strings.ReplaceAll(x, "-", "_"), ",", "")
- parts := strings.Split(x, "_")
- for i, p := range parts {
- parts[i] = utils.Capitalize(p)
- }
- return strings.Join(parts, "")
- }
- /*
- Create an [Option] from a string. Syntax is:
- --option-name, --option-alias, -s
- type: string
- dest: destination
- choices: choice1, choice2, choice 3
- depth: 0
- default: something
- Help text on multiple lines. Indented lines are preserved as indented blocks. Blank lines
- are preserved as blank lines. #placeholder_for_formatting# is replaced by the empty string.
- .. code:: blocks are handled specially. Lines in them starting with "$ " have the $ colored
- to indicate a prompt.
- Available types are: string, str, list, int, float, count, bool-set, bool-reset, choices
- The default dest is the first --option-name which must be a long option. The destination is automatically CamelCased from snake_case.
- If choices are specified type is set to choices automatically.
- If depth is negative option is added to all subcommands. If depth is positive option is added to sub-commands upto
- the specified depth.
- Set the help text to "!" to have an option hidden.
- */
- func OptionFromString(entries ...string) (*Option, error) {
- return option_from_string(map[string]string{}, entries...)
- }
- func is_string_slice(f reflect.Value) bool {
- if f.Kind() != reflect.Slice {
- return false
- }
- return f.Type().Elem().Kind() == reflect.String
- }
- func OptionsFromStruct(pointer_to_options_struct any) ([]*Option, error) {
- val := reflect.ValueOf(pointer_to_options_struct).Elem()
- if val.Kind() != reflect.Struct {
- return nil, fmt.Errorf("Need a pointer to a struct to set option values on")
- }
- ans := make([]*Option, 0, val.NumField())
- for i := 0; i < val.NumField(); i++ {
- f := val.Field(i)
- field_name := val.Type().Field(i).Name
- tag := val.Type().Field(i).Tag
- if utils.Capitalize(field_name) != field_name || !f.CanSet() {
- continue
- }
- typ := "str"
- switch f.Kind() {
- case reflect.Slice:
- if !is_string_slice(f) {
- return nil, fmt.Errorf("The field %s is not a slice of strings", field_name)
- }
- typ = "list"
- case reflect.Int:
- typ = "int"
- case reflect.Float64:
- typ = "float"
- case reflect.Bool:
- typ = "bool-set"
- }
- overrides := map[string]string{"dest": field_name, "type": typ}
- opt, err := option_from_string(overrides, string(tag))
- if err != nil {
- return nil, err
- }
- if opt.OptionType == CountOption && f.Kind() != reflect.Int {
- return nil, fmt.Errorf("The field %s is of count type but in the options struct it does not have type int", field_name)
- }
- ans = append(ans, opt)
- }
- return ans, nil
- }
- type multi_scan struct {
- entries []string
- }
- var mpat *regexp.Regexp
- func (self *Option) init_option() {
- self.values_from_cmdline = make([]string, 0, 1)
- self.parsed_values_from_cmdline = make([]any, 0, 1)
- }
- func option_from_spec(spec OptionSpec) (*Option, error) {
- ans := Option{
- Help: spec.Help,
- }
- ans.init_option()
- parts := strings.Split(spec.Name, " ")
- ans.Name = camel_case_dest(parts[0])
- ans.Aliases = make([]Alias, len(parts))
- for i, x := range parts {
- ans.Aliases[i] = Alias{NameWithoutHyphens: strings.TrimLeft(x, "-"), IsShort: !strings.HasPrefix(x, "--")}
- }
- if spec.Dest != "" {
- ans.Name = spec.Dest
- }
- ans.Depth = spec.Depth
- if spec.Choices != "" {
- parts := strings.Split(spec.Choices, ",")
- if len(parts) == 1 {
- parts = strings.Split(spec.Choices, " ")
- } else {
- for i, x := range parts {
- parts[i] = strings.TrimSpace(x)
- }
- }
- ans.Choices = parts
- ans.OptionType = StringOption
- if ans.Default == "" {
- ans.Default = parts[0]
- }
- } else {
- switch spec.Type {
- case "choice", "choices":
- ans.OptionType = StringOption
- case "int":
- ans.OptionType = IntegerOption
- ans.Default = "0"
- case "float":
- ans.OptionType = FloatOption
- ans.Default = "0"
- case "count":
- ans.OptionType = CountOption
- ans.Default = "0"
- case "bool-set":
- ans.OptionType = BoolOption
- ans.Default = "false"
- case "bool-reset":
- ans.OptionType = BoolOption
- ans.Default = "true"
- for _, a := range ans.Aliases {
- a.IsUnset = true
- }
- case "list":
- ans.IsList = true
- fallthrough
- case "str", "string", "":
- ans.OptionType = StringOption
- default:
- return nil, fmt.Errorf("Unknown option type: %s", spec.Type)
- }
- }
- if spec.Default != "" {
- ans.Default = spec.Default
- }
- ans.Help = spec.Help
- ans.Hidden = spec.Help == "!"
- pval, err := ans.parse_value(ans.Default)
- if err != nil {
- return nil, err
- }
- ans.parsed_default = pval
- if ans.IsList {
- ans.parsed_default = []string{}
- }
- ans.Completer = spec.Completer
- if ans.Aliases == nil || len(ans.Aliases) == 0 {
- return nil, fmt.Errorf("No --aliases specified for option")
- }
- if ans.Name == "" {
- return nil, fmt.Errorf("No dest specified for option")
- }
- return &ans, nil
- }
- func indent_of_line(x string) int {
- return len(x) - len(strings.TrimLeft(x, " \n\t\v\f"))
- }
- func escape_text_for_man(raw string) string {
- italic := func(x string) string {
- return "\n.I " + x
- }
- bold := func(x string) string {
- return "\n.B " + x
- }
- text_without_target := func(val string) string {
- text, target := markup.Text_and_target(val)
- no_title := text == target
- if no_title {
- return val
- }
- return text
- }
- ref_hyperlink := func(val, prefix string) string {
- return text_without_target(val)
- }
- raw = markup.ReplaceAllRSTRoles(raw, func(group markup.Rst_format_match) string {
- val := group.Payload
- switch group.Role {
- case "file":
- return italic(val)
- case "env", "envvar":
- return bold(val)
- case "doc":
- return text_without_target(val)
- case "iss":
- return "Issue #" + val
- case "pull":
- return "PR #" + val
- case "disc":
- return "Discussion #" + val
- case "ref":
- return ref_hyperlink(val, "")
- case "ac":
- return ref_hyperlink(val, "action-")
- case "term":
- return ref_hyperlink(val, "term-")
- case "code":
- return markup.Remove_backslash_escapes(val)
- case "link":
- return text_without_target(val)
- case "option":
- idx := strings.LastIndex(val, "--")
- if idx < 0 {
- idx = strings.Index(val, "-")
- }
- if idx > -1 {
- val = strings.TrimSuffix(val[idx:], ">")
- }
- return bold(val)
- case "opt":
- return bold(val)
- case "yellow":
- return val
- case "blue":
- return val
- case "green":
- return val
- case "cyan":
- return val
- case "magenta":
- return val
- case "emph":
- return val
- default:
- return val
- }
- })
- sb := strings.Builder{}
- sb.Grow(2 * len(raw))
- replacements := map[rune]string{
- '"': `\[dq]`, '\'': `\[aq]`, '-': `\-`, '\\': `\e`, '^': `\(ha`, '`': `\(ga`, '~': `\(ti`,
- }
- for _, ch := range raw {
- if rep, found := replacements[ch]; found {
- sb.WriteString(rep)
- } else {
- sb.WriteRune(ch)
- }
- }
- return sb.String()
- }
- func escape_help_for_man(raw string) string {
- help := strings.Builder{}
- help.Grow(len(raw) + 256)
- prev_indent := 0
- in_code_block := false
- lines := utils.Splitlines(raw)
- handle_non_empty_line := func(i int, line string) int {
- if strings.TrimSpace(line) == "#placeholder_for_formatting#" {
- return i + 1
- }
- if strings.HasPrefix(line, ".. code::") {
- in_code_block = true
- return i + 1
- }
- current_indent := indent_of_line(line)
- if current_indent > 1 {
- if prev_indent == 0 {
- help.WriteString("\n")
- } else {
- line = strings.TrimSpace(line)
- }
- }
- prev_indent = current_indent
- if help.Len() > 0 && !strings.HasSuffix(help.String(), "\n") {
- help.WriteString(" ")
- }
- help.WriteString(line)
- return i
- }
- handle_empty_line := func(i int, line string) int {
- prev_indent = 0
- help.WriteString("\n")
- if !strings.HasSuffix(help.String(), "::") {
- help.WriteString("\n")
- }
- return i
- }
- handle_code_block_line := func(i int, line string) int {
- if line == "" {
- help.WriteString("\n")
- return i
- }
- current_indent := indent_of_line(line)
- if current_indent == 0 {
- in_code_block = false
- return handle_non_empty_line(i, line)
- }
- line = line[4:]
- is_prompt := strings.HasPrefix(line, "$ ")
- if is_prompt {
- help.WriteString(":yellow:`$ `")
- line = line[2:]
- }
- help.WriteString(line)
- help.WriteString("\n")
- return i
- }
- for i := 0; i < len(lines); i++ {
- line := lines[i]
- if in_code_block {
- i = handle_code_block_line(i, line)
- continue
- }
- if line != "" {
- i = handle_non_empty_line(i, line)
- } else {
- i = handle_empty_line(i, line)
- }
- }
- return escape_text_for_man(help.String())
- }
- func prepare_help_text_for_display(raw string) string {
- help := strings.Builder{}
- help.Grow(len(raw) + 256)
- prev_indent := 0
- in_code_block := false
- lines := utils.Splitlines(raw)
- handle_non_empty_line := func(i int, line string) int {
- if strings.HasPrefix(line, ".. code::") {
- in_code_block = true
- return i + 1
- }
- current_indent := indent_of_line(line)
- if current_indent > 1 {
- if prev_indent == 0 {
- help.WriteString("\n")
- } else {
- line = strings.TrimSpace(line)
- }
- }
- prev_indent = current_indent
- if help.Len() > 0 && !strings.HasSuffix(help.String(), "\n") {
- help.WriteString(" ")
- }
- help.WriteString(line)
- return i
- }
- handle_empty_line := func(i int, line string) int {
- prev_indent = 0
- help.WriteString("\n")
- if !strings.HasSuffix(help.String(), "::") {
- help.WriteString("\n")
- }
- return i
- }
- handle_code_block_line := func(i int, line string) int {
- if line == "" {
- help.WriteString("\n")
- return i
- }
- current_indent := indent_of_line(line)
- if current_indent == 0 {
- in_code_block = false
- return handle_non_empty_line(i, line)
- }
- line = line[4:]
- is_prompt := strings.HasPrefix(line, "$ ")
- if is_prompt {
- help.WriteString(":yellow:`$ `")
- line = line[2:]
- }
- help.WriteString(line)
- help.WriteString("\n")
- return i
- }
- for i := 0; i < len(lines); i++ {
- line := lines[i]
- if in_code_block {
- i = handle_code_block_line(i, line)
- continue
- }
- if line != "" {
- i = handle_non_empty_line(i, line)
- } else {
- i = handle_empty_line(i, line)
- }
- }
- return help.String()
- }
- func option_from_string(overrides map[string]string, entries ...string) (*Option, error) {
- if mpat == nil {
- mpat = regexp.MustCompile("^([a-z]+)=(.+)")
- }
- spec := OptionSpec{}
- scanner := utils.NewScanLines(entries...)
- in_help := false
- help := strings.Builder{}
- help.Grow(2048)
- if dq, found := overrides["type"]; found {
- spec.Type = dq
- }
- if dq, found := overrides["dest"]; found {
- spec.Dest = dq
- }
- for scanner.Scan() {
- line := scanner.Text()
- if spec.Name == "" {
- if strings.HasPrefix(line, "--") {
- spec.Name = line
- }
- } else if in_help {
- spec.Help += line + "\n"
- } else {
- line = strings.TrimSpace(line)
- matches := mpat.FindStringSubmatch(line)
- if matches == nil {
- continue
- }
- k, v := matches[1], matches[2]
- switch k {
- case "choices":
- spec.Choices = v
- case "default":
- if overrides["default"] == "" {
- spec.Default = v
- }
- case "dest":
- if overrides["dest"] == "" {
- spec.Dest = v
- }
- case "depth":
- depth, err := strconv.ParseInt(v, 0, 0)
- if err != nil {
- return nil, err
- }
- spec.Depth = int(depth)
- case "condition", "completion":
- default:
- return nil, fmt.Errorf("Unknown option metadata key: %s", k)
- case "type":
- spec.Type = v
- }
- }
- }
- return option_from_spec(spec)
- }
|