pkg.re/essentialkaos/ek.v11@v12.41.0+incompatible/usage/completion/zsh/zsh.go (about) 1 // Package zsh provides methods for generating zsh completion 2 package zsh 3 4 // ////////////////////////////////////////////////////////////////////////////////// // 5 // // 6 // Copyright (c) 2022 ESSENTIAL KAOS // 7 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 8 // // 9 // ////////////////////////////////////////////////////////////////////////////////// // 10 11 import ( 12 "fmt" 13 "strings" 14 15 "pkg.re/essentialkaos/ek.v12/fmtc" 16 "pkg.re/essentialkaos/ek.v12/options" 17 "pkg.re/essentialkaos/ek.v12/usage" 18 ) 19 20 // ////////////////////////////////////////////////////////////////////////////////// // 21 22 // _ZSH_TEMPLATE is zsh completion template 23 const _ZSH_TEMPLATE = `#compdef {{COMPNAME}} 24 25 # This completion is automatically generated 26 27 typeset -A opt_args 28 29 _{{COMPNAME}}() { 30 _arguments \ 31 {{GLOBAL_ARGS}} 32 33 {{COMMANDS_HANDLERS}} 34 {{FILES_HANDLER}} 35 } 36 37 {{COMMANDS_FUNC}} 38 _{{COMPNAME}} "$@" 39 ` 40 41 // ////////////////////////////////////////////////////////////////////////////////// // 42 43 // Generate generates zsh completion code 44 func Generate(info *usage.Info, opts options.Map, name string, fileGlob ...string) string { 45 result := _ZSH_TEMPLATE 46 47 result = strings.Replace(result, "{{GLOBAL_ARGS}}", genGlobalOptionList(info, opts), -1) 48 result = strings.Replace(result, "{{COMMANDS_HANDLERS}}", genCommandsHandlers(info, opts), -1) 49 result = strings.Replace(result, "{{COMMANDS_FUNC}}", genCommandsFunc(info), -1) 50 result = strings.Replace(result, "{{FILES_HANDLER}}", genFilesHandler(info, fileGlob), -1) 51 result = strings.Replace(result, "{{COMPNAME}}", name, -1) 52 53 nameSafe := strings.Replace(name, "-", "_", -1) 54 55 result = strings.Replace(result, "{{COMPNAME_SAFE}}", nameSafe, -1) 56 57 return result 58 } 59 60 // ////////////////////////////////////////////////////////////////////////////////// // 61 62 // genFilesHandler generates handler for showing files 63 func genFilesHandler(info *usage.Info, fileGlob []string) string { 64 if len(info.Args) == 0 && len(fileGlob) == 0 { 65 return "" 66 } 67 68 if len(fileGlob) != 0 { 69 return " _files -g \"" + fileGlob[0] + "\"" 70 } 71 72 return " _files" 73 } 74 75 // genGlobalOptionList generates list with global options 76 func genGlobalOptionList(info *usage.Info, opts options.Map) string { 77 var result string 78 79 nonGlobalOptions := make(map[string]bool) 80 81 for _, cmd := range info.Commands { 82 for _, opt := range cmd.BoundOptions { 83 nonGlobalOptions[opt] = true 84 } 85 } 86 87 for _, opt := range info.Options { 88 if nonGlobalOptions[opt.Long] { 89 continue 90 } 91 92 result += genOptionDesc(opt, opts, 4) 93 } 94 95 if len(info.Commands) != 0 { 96 result += " '1: :_{{COMPNAME_SAFE}}_cmds' \\\n" 97 } 98 99 result += " '*:: :->cmd_args' && ret=0" 100 101 return result 102 } 103 104 // genOptionDesc generates description for some option 105 func genOptionDesc(opt *usage.Option, opts options.Map, prefixSize int) string { 106 result := strings.Repeat(" ", prefixSize) 107 108 var isBool, isMergeble bool 109 var optV *options.V 110 111 optLong := opt.Long 112 113 if opts != nil { 114 optV = opts[getOptionFullName(opt)] 115 } 116 117 if optV != nil { 118 isBool = optV.Type == options.BOOL 119 isMergeble = optV.Mergeble 120 } 121 122 if !isBool { 123 optLong += "=" 124 } 125 126 if !isMergeble { 127 var exclusion string 128 129 if opt.Short != "" { 130 exclusion = fmt.Sprintf("-%s --%s", opt.Short, optLong) 131 } else { 132 exclusion = fmt.Sprintf("--%s", optLong) 133 } 134 135 if optV != nil && optV.Conflicts != "" { 136 exclusion += genConflictsExclusion(optV.Conflicts) 137 } 138 139 result += "'(" + exclusion + ")'" 140 } 141 142 if opt.Short != "" { 143 result += fmt.Sprintf("{-%s,--%s}", opt.Short, optLong) 144 } else { 145 result += fmt.Sprintf("--%s", opt.Long) 146 } 147 148 result += fmt.Sprintf("'[%s]'", fmtc.Clean(opt.Desc)) 149 result += " \\\n" 150 151 return result 152 } 153 154 // genCommandsHandlers generates handlers for commands 155 func genCommandsHandlers(info *usage.Info, opts options.Map) string { 156 if !isCommandHandlersRequired(info) { 157 return "" 158 } 159 160 result := " case $state in\n" 161 result += " cmd_args)\n" 162 result += " case $words[1] in\n" 163 164 for _, cmd := range info.Commands { 165 if len(cmd.BoundOptions) != 0 { 166 result += genCommandHandler(cmd, info, opts) 167 } 168 } 169 170 result += " esac\n" 171 result += " ;;\n" 172 result += " esac\n" 173 174 return result 175 } 176 177 // genCommandHandler generates handler for given command 178 func genCommandHandler(cmd *usage.Command, info *usage.Info, opts options.Map) string { 179 result := fmt.Sprintf(" %s)\n", cmd.Name) 180 result += " _arguments \\\n" 181 182 for _, optName := range cmd.BoundOptions { 183 opt := info.GetOption(optName) 184 185 if opt == nil { 186 continue 187 } 188 189 result += genOptionDesc(opt, opts, 12) 190 } 191 192 result += " ;;\n" 193 194 return result 195 } 196 197 // genCommandsFunc generates function which generates list with all supported commands 198 func genCommandsFunc(info *usage.Info) string { 199 if len(info.Commands) == 0 { 200 return "" 201 } 202 203 result := "_{{COMPNAME_SAFE}}_cmds() {\n" 204 result += " local -a commands\n" 205 result += " commands=(\n" 206 207 for _, cmd := range info.Commands { 208 result += fmt.Sprintf(" '%s:%s'\n", cmd.Name, fmtc.Clean(cmd.Desc)) 209 } 210 211 result += " )\n" 212 result += " _describe 'command' commands\n" 213 result += "}\n" 214 215 return result 216 } 217 218 // genConflictsExclusion generates list with conflicts exlusions 219 func genConflictsExclusion(opts string) string { 220 var result []string 221 222 for _, opt := range strings.Split(opts, " ") { 223 long, short := options.ParseOptionName(opt) 224 225 if short != "" { 226 result = append(result, "-"+short, "--"+long) 227 } else { 228 result = append(result, "--"+long) 229 } 230 } 231 232 return " " + strings.Join(result, " ") 233 } 234 235 // isCommandHandlersRequired returns true if commands have bound options 236 func isCommandHandlersRequired(info *usage.Info) bool { 237 for _, cmd := range info.Commands { 238 if len(cmd.BoundOptions) != 0 { 239 return true 240 } 241 } 242 243 return false 244 } 245 246 // getOptionFullName generates combined option name 247 func getOptionFullName(opt *usage.Option) string { 248 if opt.Short != "" { 249 return opt.Short + ":" + opt.Long 250 } 251 252 return opt.Long 253 }