go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/cmd/copyright/main.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package main 9 10 import ( 11 "bufio" 12 "errors" 13 "fmt" 14 "os" 15 "strings" 16 "time" 17 18 "github.com/urfave/cli/v2" 19 "go.charczuk.com/sdk/copyright" 20 ) 21 22 func main() { 23 app := &cli.App{ 24 Name: "copyright", 25 UsageText: `code copyright management cli 26 27 > copyright [inject|verify|remove] [ROOT(s)...] 28 29 Verify, inject or remove copyright notices from files in a given tree roots. 30 31 By default without other flags provided, this tool verifies that copyright notices are present. 32 33 To verify headers: 34 35 > copyright 36 - OR - 37 > copyright verify 38 - OR - 39 > copyright verify ./critical 40 - OR - 41 > copyright verify ./critical/foo.py 42 43 To inject headers: 44 45 > copyright inject 46 - OR - 47 > copyright inject ./critical/foo.py 48 49 - NOTE: you can run "--inject" multiple times; it will only add the header if it is not present. 50 - NOTE: Headers are treated exactly; do not edit the headers once they've been injected 51 otherwise the next 'inject' pass will duplicate the headers. 52 53 To remove headers: 54 55 > copyright remove 56 57 If you have an old version of the header in your files, and you want to migrate to an updated version: 58 59 1. Save the existing header to a file, "notice.txt", including any newlines between the notice and code 60 2. Remove existing notices: 61 > copyright remove --ext=py=notice.txt 62 3. Then inject the new notice: 63 > copyright inject --use-include-defaults=false --include="*.py" 64 4. You should now have the new notice in your files, and "copyright inject" will honor it`, 65 66 Flags: persistentFlags, 67 Commands: []*cli.Command{ 68 verify, 69 inject, 70 remove, 71 }, 72 } 73 74 if err := app.Run(os.Args); err != nil { 75 if !errors.Is(err, copyright.ErrFailure) { 76 fmt.Fprintf(os.Stderr, "%+v\n", err) 77 } 78 os.Exit(1) 79 } 80 } 81 82 var persistentFlags = []cli.Flag{ 83 &cli.BoolFlag{Name: "quiet", Usage: "If all output should be suppressed"}, 84 &cli.BoolFlag{Name: "verbose", Usage: "If verbose output should be shown"}, 85 &cli.BoolFlag{Name: "debug", Usage: "If debug output should be shown"}, 86 87 &cli.StringFlag{Name: "notice-path", Usage: "The {{ .Notice }} template value read from a given path"}, 88 &cli.StringFlag{Name: "notice", Value: copyright.DefaultNoticeTemplate, Usage: "The notice template value as a literal string"}, 89 90 &cli.StringFlag{Name: "restrictions-path", Usage: "The {{ .Restrictions }} template variable value read from a given path"}, 91 &cli.StringFlag{Name: "restrictions", Value: "open-source", Usage: "The {{ .Restrictions }} template variable value as a literal (use `internal` for the default internal template, and `open-source` for the default open source template)"}, 92 93 &cli.StringFlag{Name: "holder", Value: copyright.DefaultCopyrightHolder, Usage: "The {{ .CopyrightHolder }} template variable value, or the entity that holds the copyright"}, 94 &cli.IntFlag{Name: "year", Value: time.Now().UTC().Year(), Usage: "The {{ .Year }} template variable value"}, 95 &cli.StringFlag{Name: "license", Value: copyright.DefaultOpenSourceLicense, Usage: "The {{ .License }} template variable value, or the license type"}, 96 97 &cli.StringSliceFlag{Name: "ext", Usage: "Injection template paths for extensions; should be in the form -ext=js=js_template.txt, you can provide be multiple"}, 98 99 &cli.BoolFlag{Name: "use-exclude-defaults", Value: true, Usage: "If we should consider the default exclude glob list"}, 100 &cli.StringSliceFlag{Name: "exclude", Usage: "Path globs to exclude from processing"}, 101 &cli.StringFlag{Name: "excludes-path", Usage: "Path globs to exclude from processing read from a given path"}, 102 &cli.BoolFlag{Name: "use-include-defaults", Value: true, Usage: "If we should consider the default include glob list"}, 103 &cli.StringSliceFlag{Name: "include", Usage: "Path globs to include in processing"}, 104 &cli.StringFlag{Name: "includes-path", Usage: "Path globs to include in processing read a given path"}, 105 } 106 107 var verify = &cli.Command{ 108 Name: "verify", 109 Usage: "Verify that files contain proper copyright headers", 110 Flags: append(persistentFlags, 111 &cli.BoolFlag{Name: "show-diff", Usage: "If we should show the difference between the template and the header found in files"}, 112 &cli.BoolFlag{Name: "exit-first", Usage: "If we should exit after the first failure"}, 113 ), 114 Action: wrapCommand(func(ctx *cli.Context, engine *copyright.Copyright, roots ...string) (err error) { 115 engine.ShowDiff = ctx.Bool("show-diff") 116 engine.ExitFirst = ctx.Bool("exit-first") 117 for _, root := range roots { 118 if err = engine.Verify(ctx.Context, root); err != nil { 119 return 120 } 121 } 122 return 123 }), 124 } 125 126 var inject = &cli.Command{ 127 Name: "inject", 128 Usage: "Inject copyright headers into files", 129 Flags: append(persistentFlags, 130 &cli.BoolFlag{Name: "dry-run", Usage: "If we should print the actions taken but not modify files"}, 131 ), 132 Action: wrapCommand(func(ctx *cli.Context, engine *copyright.Copyright, roots ...string) (err error) { 133 engine.DryRun = ctx.Bool("dry-run") 134 for _, root := range roots { 135 if err = engine.Inject(ctx.Context, root); err != nil { 136 return 137 } 138 } 139 return 140 }), 141 } 142 var remove = &cli.Command{ 143 Name: "remove", 144 Usage: "Remove copyright headers from files", 145 Flags: append(persistentFlags, 146 &cli.BoolFlag{Name: "dry-run", Value: false, Usage: "If we should print the actions taken but not modify files"}, 147 ), 148 Action: wrapCommand(func(ctx *cli.Context, engine *copyright.Copyright, roots ...string) (err error) { 149 engine.DryRun = ctx.Bool("dry-run") 150 for _, root := range roots { 151 if err = engine.Remove(ctx.Context, root); err != nil { 152 return 153 } 154 } 155 return 156 }), 157 } 158 159 func wrapCommand(commandAction func(*cli.Context, *copyright.Copyright, ...string) error) func(*cli.Context) error { 160 return func(ctx *cli.Context) error { 161 flagNoticeTemplate := strings.TrimSpace(readPathOrLiteral(ctx.String("notice-path"), ctx.String("notice"))) 162 if flagNoticeTemplate == "" { 163 return fmt.Errorf("--notice provided is an empty string; cannot continue") 164 } 165 166 var roots []string 167 if args := ctx.Args().Slice(); len(args) > 0 { 168 roots = args[:] 169 } else { 170 roots = []string{"."} 171 } 172 173 flagExcludes := ctx.StringSlice("exclude") 174 if ctx.Bool("use-exclude-defaults") { 175 flagExcludes = append(flagExcludes, flagStrings(copyright.DefaultExcludes)...) 176 } 177 if excludesPath := ctx.String("excludes-path"); excludesPath != "" { 178 excludes, err := readGlobFile(excludesPath) 179 if err != nil { 180 return err 181 } 182 flagExcludes = append(flagExcludes, excludes...) 183 } 184 185 flagIncludes := ctx.StringSlice("include") 186 if ctx.Bool("use-include-defaults") { 187 flagIncludes = append(flagIncludes, flagStrings(copyright.DefaultIncludes)...) 188 } 189 if includesPath := ctx.String("includes-path"); includesPath != "" { 190 includes, err := readGlobFile(includesPath) 191 if err != nil { 192 return err 193 } 194 flagIncludes = append(flagIncludes, includes...) 195 } 196 197 var restrictions string 198 var err error 199 if restrictionsPath := ctx.String("restrictions-path"); restrictionsPath != "" { 200 restrictions, err = tryReadFile(restrictionsPath) 201 if err != nil { 202 return fmt.Errorf("cannot read restrictions by path; %w", err) 203 } 204 } else { 205 restrictions = ctx.String("restrictions") 206 switch restrictions { 207 case "open-source": 208 restrictions = copyright.DefaultRestrictionsTemplate 209 case "internal": 210 restrictions = copyright.DefaultRestrictionsTemplateInternal 211 default: 212 } 213 } 214 215 extensionInjectionTemplates := copyright.DefaultExtensionInjectionTemplates 216 for _, extValue := range ctx.StringSlice("ext") { 217 ext, noticeTemplate, err := parseExtensionNoticeBodyTemplate(extValue) 218 if err != nil { 219 return err 220 } 221 extensionInjectionTemplates[ext] = noticeTemplate 222 } 223 224 engine := ©right.Copyright{ 225 Config: copyright.Config{ 226 NoticeTemplate: flagNoticeTemplate, 227 CopyrightHolder: ctx.String("holder"), 228 RestrictionsTemplate: restrictions, 229 Year: ctx.Int("year"), 230 License: ctx.String("license"), 231 ExtensionInjectionTemplates: extensionInjectionTemplates, 232 Excludes: flagExcludes, 233 Includes: flagIncludes, 234 Quiet: ctx.Bool("quiet"), 235 Verbose: ctx.Bool("verbose"), 236 Debug: ctx.Bool("debug"), 237 }, 238 } 239 return commandAction(ctx, engine, roots...) 240 } 241 } 242 243 type flagStrings []string 244 245 func (fs flagStrings) String() string { 246 return strings.Join(fs, ", ") 247 } 248 249 func (fs *flagStrings) Set(flagValue string) error { 250 if flagValue == "" { 251 return fmt.Errorf("invalid flag value; is empty") 252 } 253 *fs = append(*fs, flagValue) 254 return nil 255 } 256 257 func tryReadFile(path string) (string, error) { 258 contents, err := os.ReadFile(path) 259 return string(contents), err 260 } 261 262 func readPathOrLiteral(path, literal string) string { 263 if path != "" { 264 contents, _ := os.ReadFile(path) 265 return string(contents) 266 } 267 return literal 268 } 269 270 func readGlobFile(path string) ([]string, error) { 271 f, err := os.Open(path) 272 if err != nil { 273 return nil, err 274 } 275 defer f.Close() 276 var output []string 277 scanner := bufio.NewScanner(f) 278 for scanner.Scan() { 279 output = append(output, strings.TrimSpace(scanner.Text())) 280 } 281 return output, nil 282 } 283 284 func parseExtensionNoticeBodyTemplate(extensionNoticeBodyTemplate string) (extension copyright.Extension, noticeBodyTemplate string, err error) { 285 parts := strings.SplitN(extensionNoticeBodyTemplate, "=", 2) 286 if len(parts) < 2 { 287 err = fmt.Errorf("invalid `-ext` value; %s", extensionNoticeBodyTemplate) 288 return 289 } 290 extension = copyright.Extension(parts[0]) 291 if !strings.HasPrefix(string(extension), ".") { 292 extension = "." + extension 293 } 294 noticeBodyTemplate = readPathOrLiteral(parts[1], parts[1]) 295 return 296 }