option-from-string.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package cli
  3. import (
  4. "fmt"
  5. "reflect"
  6. "regexp"
  7. "strconv"
  8. "strings"
  9. "kitty/tools/cli/markup"
  10. "kitty/tools/utils"
  11. )
  12. var _ = fmt.Print
  13. func camel_case_dest(x string) string {
  14. x = strings.ReplaceAll(strings.ReplaceAll(x, "-", "_"), ",", "")
  15. parts := strings.Split(x, "_")
  16. for i, p := range parts {
  17. parts[i] = utils.Capitalize(p)
  18. }
  19. return strings.Join(parts, "")
  20. }
  21. /*
  22. Create an [Option] from a string. Syntax is:
  23. --option-name, --option-alias, -s
  24. type: string
  25. dest: destination
  26. choices: choice1, choice2, choice 3
  27. depth: 0
  28. default: something
  29. Help text on multiple lines. Indented lines are preserved as indented blocks. Blank lines
  30. are preserved as blank lines. #placeholder_for_formatting# is replaced by the empty string.
  31. .. code:: blocks are handled specially. Lines in them starting with "$ " have the $ colored
  32. to indicate a prompt.
  33. Available types are: string, str, list, int, float, count, bool-set, bool-reset, choices
  34. The default dest is the first --option-name which must be a long option. The destination is automatically CamelCased from snake_case.
  35. If choices are specified type is set to choices automatically.
  36. If depth is negative option is added to all subcommands. If depth is positive option is added to sub-commands upto
  37. the specified depth.
  38. Set the help text to "!" to have an option hidden.
  39. */
  40. func OptionFromString(entries ...string) (*Option, error) {
  41. return option_from_string(map[string]string{}, entries...)
  42. }
  43. func is_string_slice(f reflect.Value) bool {
  44. if f.Kind() != reflect.Slice {
  45. return false
  46. }
  47. return f.Type().Elem().Kind() == reflect.String
  48. }
  49. func OptionsFromStruct(pointer_to_options_struct any) ([]*Option, error) {
  50. val := reflect.ValueOf(pointer_to_options_struct).Elem()
  51. if val.Kind() != reflect.Struct {
  52. return nil, fmt.Errorf("Need a pointer to a struct to set option values on")
  53. }
  54. ans := make([]*Option, 0, val.NumField())
  55. for i := 0; i < val.NumField(); i++ {
  56. f := val.Field(i)
  57. field_name := val.Type().Field(i).Name
  58. tag := val.Type().Field(i).Tag
  59. if utils.Capitalize(field_name) != field_name || !f.CanSet() {
  60. continue
  61. }
  62. typ := "str"
  63. switch f.Kind() {
  64. case reflect.Slice:
  65. if !is_string_slice(f) {
  66. return nil, fmt.Errorf("The field %s is not a slice of strings", field_name)
  67. }
  68. typ = "list"
  69. case reflect.Int:
  70. typ = "int"
  71. case reflect.Float64:
  72. typ = "float"
  73. case reflect.Bool:
  74. typ = "bool-set"
  75. }
  76. overrides := map[string]string{"dest": field_name, "type": typ}
  77. opt, err := option_from_string(overrides, string(tag))
  78. if err != nil {
  79. return nil, err
  80. }
  81. if opt.OptionType == CountOption && f.Kind() != reflect.Int {
  82. return nil, fmt.Errorf("The field %s is of count type but in the options struct it does not have type int", field_name)
  83. }
  84. ans = append(ans, opt)
  85. }
  86. return ans, nil
  87. }
  88. type multi_scan struct {
  89. entries []string
  90. }
  91. var mpat *regexp.Regexp
  92. func (self *Option) init_option() {
  93. self.values_from_cmdline = make([]string, 0, 1)
  94. self.parsed_values_from_cmdline = make([]any, 0, 1)
  95. }
  96. func option_from_spec(spec OptionSpec) (*Option, error) {
  97. ans := Option{
  98. Help: spec.Help,
  99. }
  100. ans.init_option()
  101. parts := strings.Split(spec.Name, " ")
  102. ans.Name = camel_case_dest(parts[0])
  103. ans.Aliases = make([]Alias, len(parts))
  104. for i, x := range parts {
  105. ans.Aliases[i] = Alias{NameWithoutHyphens: strings.TrimLeft(x, "-"), IsShort: !strings.HasPrefix(x, "--")}
  106. }
  107. if spec.Dest != "" {
  108. ans.Name = spec.Dest
  109. }
  110. ans.Depth = spec.Depth
  111. if spec.Choices != "" {
  112. parts := strings.Split(spec.Choices, ",")
  113. if len(parts) == 1 {
  114. parts = strings.Split(spec.Choices, " ")
  115. } else {
  116. for i, x := range parts {
  117. parts[i] = strings.TrimSpace(x)
  118. }
  119. }
  120. ans.Choices = parts
  121. ans.OptionType = StringOption
  122. if ans.Default == "" {
  123. ans.Default = parts[0]
  124. }
  125. } else {
  126. switch spec.Type {
  127. case "choice", "choices":
  128. ans.OptionType = StringOption
  129. case "int":
  130. ans.OptionType = IntegerOption
  131. ans.Default = "0"
  132. case "float":
  133. ans.OptionType = FloatOption
  134. ans.Default = "0"
  135. case "count":
  136. ans.OptionType = CountOption
  137. ans.Default = "0"
  138. case "bool-set":
  139. ans.OptionType = BoolOption
  140. ans.Default = "false"
  141. case "bool-reset":
  142. ans.OptionType = BoolOption
  143. ans.Default = "true"
  144. for _, a := range ans.Aliases {
  145. a.IsUnset = true
  146. }
  147. case "list":
  148. ans.IsList = true
  149. fallthrough
  150. case "str", "string", "":
  151. ans.OptionType = StringOption
  152. default:
  153. return nil, fmt.Errorf("Unknown option type: %s", spec.Type)
  154. }
  155. }
  156. if spec.Default != "" {
  157. ans.Default = spec.Default
  158. }
  159. ans.Help = spec.Help
  160. ans.Hidden = spec.Help == "!"
  161. pval, err := ans.parse_value(ans.Default)
  162. if err != nil {
  163. return nil, err
  164. }
  165. ans.parsed_default = pval
  166. if ans.IsList {
  167. ans.parsed_default = []string{}
  168. }
  169. ans.Completer = spec.Completer
  170. if ans.Aliases == nil || len(ans.Aliases) == 0 {
  171. return nil, fmt.Errorf("No --aliases specified for option")
  172. }
  173. if ans.Name == "" {
  174. return nil, fmt.Errorf("No dest specified for option")
  175. }
  176. return &ans, nil
  177. }
  178. func indent_of_line(x string) int {
  179. return len(x) - len(strings.TrimLeft(x, " \n\t\v\f"))
  180. }
  181. func escape_text_for_man(raw string) string {
  182. italic := func(x string) string {
  183. return "\n.I " + x
  184. }
  185. bold := func(x string) string {
  186. return "\n.B " + x
  187. }
  188. text_without_target := func(val string) string {
  189. text, target := markup.Text_and_target(val)
  190. no_title := text == target
  191. if no_title {
  192. return val
  193. }
  194. return text
  195. }
  196. ref_hyperlink := func(val, prefix string) string {
  197. return text_without_target(val)
  198. }
  199. raw = markup.ReplaceAllRSTRoles(raw, func(group markup.Rst_format_match) string {
  200. val := group.Payload
  201. switch group.Role {
  202. case "file":
  203. return italic(val)
  204. case "env", "envvar":
  205. return bold(val)
  206. case "doc":
  207. return text_without_target(val)
  208. case "iss":
  209. return "Issue #" + val
  210. case "pull":
  211. return "PR #" + val
  212. case "disc":
  213. return "Discussion #" + val
  214. case "ref":
  215. return ref_hyperlink(val, "")
  216. case "ac":
  217. return ref_hyperlink(val, "action-")
  218. case "term":
  219. return ref_hyperlink(val, "term-")
  220. case "code":
  221. return markup.Remove_backslash_escapes(val)
  222. case "link":
  223. return text_without_target(val)
  224. case "option":
  225. idx := strings.LastIndex(val, "--")
  226. if idx < 0 {
  227. idx = strings.Index(val, "-")
  228. }
  229. if idx > -1 {
  230. val = strings.TrimSuffix(val[idx:], ">")
  231. }
  232. return bold(val)
  233. case "opt":
  234. return bold(val)
  235. case "yellow":
  236. return val
  237. case "blue":
  238. return val
  239. case "green":
  240. return val
  241. case "cyan":
  242. return val
  243. case "magenta":
  244. return val
  245. case "emph":
  246. return val
  247. default:
  248. return val
  249. }
  250. })
  251. sb := strings.Builder{}
  252. sb.Grow(2 * len(raw))
  253. replacements := map[rune]string{
  254. '"': `\[dq]`, '\'': `\[aq]`, '-': `\-`, '\\': `\e`, '^': `\(ha`, '`': `\(ga`, '~': `\(ti`,
  255. }
  256. for _, ch := range raw {
  257. if rep, found := replacements[ch]; found {
  258. sb.WriteString(rep)
  259. } else {
  260. sb.WriteRune(ch)
  261. }
  262. }
  263. return sb.String()
  264. }
  265. func escape_help_for_man(raw string) string {
  266. help := strings.Builder{}
  267. help.Grow(len(raw) + 256)
  268. prev_indent := 0
  269. in_code_block := false
  270. lines := utils.Splitlines(raw)
  271. handle_non_empty_line := func(i int, line string) int {
  272. if strings.TrimSpace(line) == "#placeholder_for_formatting#" {
  273. return i + 1
  274. }
  275. if strings.HasPrefix(line, ".. code::") {
  276. in_code_block = true
  277. return i + 1
  278. }
  279. current_indent := indent_of_line(line)
  280. if current_indent > 1 {
  281. if prev_indent == 0 {
  282. help.WriteString("\n")
  283. } else {
  284. line = strings.TrimSpace(line)
  285. }
  286. }
  287. prev_indent = current_indent
  288. if help.Len() > 0 && !strings.HasSuffix(help.String(), "\n") {
  289. help.WriteString(" ")
  290. }
  291. help.WriteString(line)
  292. return i
  293. }
  294. handle_empty_line := func(i int, line string) int {
  295. prev_indent = 0
  296. help.WriteString("\n")
  297. if !strings.HasSuffix(help.String(), "::") {
  298. help.WriteString("\n")
  299. }
  300. return i
  301. }
  302. handle_code_block_line := func(i int, line string) int {
  303. if line == "" {
  304. help.WriteString("\n")
  305. return i
  306. }
  307. current_indent := indent_of_line(line)
  308. if current_indent == 0 {
  309. in_code_block = false
  310. return handle_non_empty_line(i, line)
  311. }
  312. line = line[4:]
  313. is_prompt := strings.HasPrefix(line, "$ ")
  314. if is_prompt {
  315. help.WriteString(":yellow:`$ `")
  316. line = line[2:]
  317. }
  318. help.WriteString(line)
  319. help.WriteString("\n")
  320. return i
  321. }
  322. for i := 0; i < len(lines); i++ {
  323. line := lines[i]
  324. if in_code_block {
  325. i = handle_code_block_line(i, line)
  326. continue
  327. }
  328. if line != "" {
  329. i = handle_non_empty_line(i, line)
  330. } else {
  331. i = handle_empty_line(i, line)
  332. }
  333. }
  334. return escape_text_for_man(help.String())
  335. }
  336. func prepare_help_text_for_display(raw string) string {
  337. help := strings.Builder{}
  338. help.Grow(len(raw) + 256)
  339. prev_indent := 0
  340. in_code_block := false
  341. lines := utils.Splitlines(raw)
  342. handle_non_empty_line := func(i int, line string) int {
  343. if strings.HasPrefix(line, ".. code::") {
  344. in_code_block = true
  345. return i + 1
  346. }
  347. current_indent := indent_of_line(line)
  348. if current_indent > 1 {
  349. if prev_indent == 0 {
  350. help.WriteString("\n")
  351. } else {
  352. line = strings.TrimSpace(line)
  353. }
  354. }
  355. prev_indent = current_indent
  356. if help.Len() > 0 && !strings.HasSuffix(help.String(), "\n") {
  357. help.WriteString(" ")
  358. }
  359. help.WriteString(line)
  360. return i
  361. }
  362. handle_empty_line := func(i int, line string) int {
  363. prev_indent = 0
  364. help.WriteString("\n")
  365. if !strings.HasSuffix(help.String(), "::") {
  366. help.WriteString("\n")
  367. }
  368. return i
  369. }
  370. handle_code_block_line := func(i int, line string) int {
  371. if line == "" {
  372. help.WriteString("\n")
  373. return i
  374. }
  375. current_indent := indent_of_line(line)
  376. if current_indent == 0 {
  377. in_code_block = false
  378. return handle_non_empty_line(i, line)
  379. }
  380. line = line[4:]
  381. is_prompt := strings.HasPrefix(line, "$ ")
  382. if is_prompt {
  383. help.WriteString(":yellow:`$ `")
  384. line = line[2:]
  385. }
  386. help.WriteString(line)
  387. help.WriteString("\n")
  388. return i
  389. }
  390. for i := 0; i < len(lines); i++ {
  391. line := lines[i]
  392. if in_code_block {
  393. i = handle_code_block_line(i, line)
  394. continue
  395. }
  396. if line != "" {
  397. i = handle_non_empty_line(i, line)
  398. } else {
  399. i = handle_empty_line(i, line)
  400. }
  401. }
  402. return help.String()
  403. }
  404. func option_from_string(overrides map[string]string, entries ...string) (*Option, error) {
  405. if mpat == nil {
  406. mpat = regexp.MustCompile("^([a-z]+)=(.+)")
  407. }
  408. spec := OptionSpec{}
  409. scanner := utils.NewScanLines(entries...)
  410. in_help := false
  411. help := strings.Builder{}
  412. help.Grow(2048)
  413. if dq, found := overrides["type"]; found {
  414. spec.Type = dq
  415. }
  416. if dq, found := overrides["dest"]; found {
  417. spec.Dest = dq
  418. }
  419. for scanner.Scan() {
  420. line := scanner.Text()
  421. if spec.Name == "" {
  422. if strings.HasPrefix(line, "--") {
  423. spec.Name = line
  424. }
  425. } else if in_help {
  426. spec.Help += line + "\n"
  427. } else {
  428. line = strings.TrimSpace(line)
  429. matches := mpat.FindStringSubmatch(line)
  430. if matches == nil {
  431. continue
  432. }
  433. k, v := matches[1], matches[2]
  434. switch k {
  435. case "choices":
  436. spec.Choices = v
  437. case "default":
  438. if overrides["default"] == "" {
  439. spec.Default = v
  440. }
  441. case "dest":
  442. if overrides["dest"] == "" {
  443. spec.Dest = v
  444. }
  445. case "depth":
  446. depth, err := strconv.ParseInt(v, 0, 0)
  447. if err != nil {
  448. return nil, err
  449. }
  450. spec.Depth = int(depth)
  451. case "condition", "completion":
  452. default:
  453. return nil, fmt.Errorf("Unknown option metadata key: %s", k)
  454. case "type":
  455. spec.Type = v
  456. }
  457. }
  458. }
  459. return option_from_spec(spec)
  460. }