如果一个现有的项目,想要开源,免不了要为项目中的文件增加开源协议头信息。虽然很多 IDE 都可以为新创建的文件自动增加头信息,但修改已有的文件还是要麻烦些。好在我们有 addlicense 工具可以使用,一行命令就能搞定。并且 addlicense 是用 Go 语言开发的,本文不仅教你如何使用,还会对其源码进行分析讲解。
The program ensures source code files have copyright license headers by scanning directory patterns recursively.
It modifies all source files in place and avoids adding a license header to any file that already has one.
The pattern argument can be provided multiple times, and may also refer to single files.
Flags:
--check check only mode: verify presence of license headers and exit with non-zero code if missing -h, --help show this help message -c, --holder string copyright holder (default "Google LLC") -l, --license string license type: apache, bsd, mit, mpl (default "apache") -f, --licensef string custom license file (no default) --skip-dirs strings regexps of directories to skip --skip-files strings regexps of files to skip -v, --verbose verbose mode: print the name of the files that are modified -y, --year string copyright year(s) (default "2024")
// Copyright (c) 2024 江湖十年 // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package main
import"fmt"
...
指定自定义 License
我们也可以指定自定义的 License 文件 boilerplate.txt 内容如下:
1 2 3 4
Copyright 2024 jianghushinian <jianghushinian007@outlook.com>. All rights reserved. Use of this source code is governed by a MIT style license that can be found in the LICENSE file. The original repo for this file is https://github.com/jianghushinian/blog-go-example.
# Copyright 2024 jianghushinian <jianghushinian007@outlook.com>. All rights reserved. # Use of this source code is governed by a MIT style # license that can be found in the LICENSE file. The original repo for # this file is https://github.com/jianghushinian/blog-go-example.
// Copyright 2020 Lingfei Kong <colin404@foxmail.com>. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file.
// This program ensures source code files have copyright license headers. // See usage with "addlicense -h". package main
const helpText = `Usage: addlicense [flags] pattern [pattern ...] The program ensures source code files have copyright license headers by scanning directory patterns recursively. It modifies all source files in place and avoids adding a license header to any file that already has one. The pattern argument can be provided multiple times, and may also refer to single files. Flags: `
const tmplApache = `Copyright {{.Year}} {{.Holder}} 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.`
const tmplBSD = `Copyright (c) {{.Year}} {{.Holder}} All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.`
const tmplMIT = `Copyright (c) {{.Year}} {{.Holder}} Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
const tmplMPL = `This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.`
var t *template.Template if *licensef != "" { d, err := ioutil.ReadFile(*licensef) if err != nil { fmt.Printf("license file: %v\n", err) os.Exit(1) } t, err = template.New("").Parse(string(d)) if err != nil { fmt.Printf("license file: %v\n", err) os.Exit(1) } } else { t = licenseTemplate[*license] if t == nil { fmt.Printf("unknown license: %s\n", *license) os.Exit(1) } }
// process at most 1000 files in parallel ch := make(chan *file, 1000) done := make(chanstruct{}) gofunc() { var wg errgroup.Group for f := range ch { f := f // https://golang.org/doc/faq#closures_and_goroutines wg.Go(func()error { // nolint: nestif if *checkonly { // Check if file extension is known lic, err := licenseHeader(f.path, t, data) if err != nil { fmt.Printf("%s: %v\n", f.path, err)
return err } if lic == nil { // Unknown fileExtension returnnil } // Check if file has a license isMissingLicenseHeader, err := fileHasLicense(f.path) if err != nil { fmt.Printf("%s: %v\n", f.path, err)
return err } if isMissingLicenseHeader { fmt.Printf("%s\n", f.path)
// process at most 1000 files in parallel ch := make(chan *file, 1000) done := make(chanstruct{}) gofunc() { var wg errgroup.Group for f := range ch { f := f // https://golang.org/doc/faq#closures_and_goroutines wg.Go(func()error { // nolint: nestif if *checkonly { // Check if file extension is known lic, err := licenseHeader(f.path, t, data) if err != nil { fmt.Printf("%s: %v\n", f.path, err)
return err } if lic == nil { // Unknown fileExtension returnnil } // Check if file has a license isMissingLicenseHeader, err := fileHasLicense(f.path) if err != nil { fmt.Printf("%s: %v\n", f.path, err)
return err } if isMissingLicenseHeader { fmt.Printf("%s\n", f.path)
// process at most 1000 files in parallel ch := make(chan *file, 1000) done := make(chanstruct{}) gofunc() { var wg errgroup.Group for f := range ch { wg.Go(func()error { ... returnnil }) } err := wg.Wait() close(done) if err != nil { os.Exit(1) } }()
for _, d := range pflag.Args() { walk(ch, d) } close(ch) <-done
// Copyright 2024 jianghushinian <jianghushinian007@outlook.com>. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. The original repo for // this file is https://github.com/jianghushinian/blog-go-example.
在 Python 文件中 License 头信息则要长这样:
1 2 3 4
# Copyright 2024 jianghushinian <jianghushinian007@outlook.com>. All rights reserved. # Use of this source code is governed by a MIT style # license that can be found in the LICENSE file. The original repo for # this file is https://github.com/jianghushinian/blog-go-example.
接下来判断如果没拿到结果,说明是不支持的文件扩展名,直接返回不做进一步处理,逻辑如下:
1 2 3
if lic == nil { // Unknown fileExtension returnnil }
var head = []string{ "#!", // shell script "<?xml", // XML declaratioon "<!doctype", // HTML doctype "# encoding:", // Ruby encoding "# frozen_string_literal:", // Ruby interpreter instruction "<?php", // PHP opening tag }
funchashBang(b []byte) []byte { line := make([]byte, 0, len(b)) for _, c := range b { line = append(line, c) if c == '\n' { break } } first := strings.ToLower(string(line)) for _, h := range head { if strings.HasPrefix(first, h) { return line } }
returnnil }
最后这段逻辑就简单了:
1 2 3
if *verbose && modified { fmt.Printf("%s added license\n", f.path) }
该函数返回的错误结果会控制 Walk 是否继续执行。如果函数返回特殊值 filepath.SkipDir,则 Walk 会跳过当前目录(如果 path 是目录跳过当前目录,否则跳过 path 的父目录)但继续遍历其他内容。如果函数返回特殊值 filepath.SkipAll,则 Walk 将跳过所有剩余的文件和目录。否则,如果函数返回非 nil 错误,则 Walk 将完全停止并返回该错误。
使用示例
现在我们准备如下用来测试的目录:
1 2 3 4 5 6 7 8 9 10 11 12 13
$ tree data -a data ├── .git ├── a │ ├── main.go │ └── main_test.go ├── b │ └── c │ └── keep ├── d.go └── d_test.go
5 directories, 5 files
我们来使用 Walk 遍历 data 目录,并且输出每个文件或目录的路径。此外,需要跳过名为 .git 的目录和以 test.go 结尾的 Go 测试文件。
funcmain() { var g errgroup.Group for i := 0; i < 10; i++ { i := i g.Go(func()error { if i == 3 { return errors.New("task 3 failed") } if i == 5 { return errors.New("task 5 failed") }
你可能注意到示例代码中有一句 url := url,这是由于在 Go 1.22 以前,由于 for 循环声明的变量只会被创建一次,并在每次迭代时更新。所以为了避免多个 goroutine 中拿到相同的 url 值,而进行的拷贝操作。
在 Go 1.22 中,循环的每次迭代都会创建新的变量,以避免意外的共享错误。这在 Go 1.22 Release Notes 中有说明。
执行示例代码,得到输出如下:
1 2 3 4
$ go run main.go Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host http://www.google.com/: 200 OK http://www.golang.org/: 200 OK