copyright-header 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/AUTHORS +2 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/README.md +69 -0
- data/Rakefile +1 -0
- data/bin/copyright-header +9 -0
- data/contrib/syntax.yml +61 -0
- data/copyright-header.gemspec +21 -0
- data/lib/copyright_header.rb +7 -0
- data/lib/copyright_header/command_line.rb +112 -0
- data/lib/copyright_header/parser.rb +227 -0
- data/lib/copyright_header/version.rb +3 -0
- data/licenses/BSD-2-CLAUSE.erb +9 -0
- data/licenses/BSD-3-CLAUSE.erb +10 -0
- data/licenses/BSD-4-CLAUSE.erb +11 -0
- data/licenses/GPL3.erb +17 -0
- data/licenses/MIT.erb +7 -0
- metadata +83 -0
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
CopyrightHeader
|
2
|
+
===============
|
3
|
+
|
4
|
+
|
5
|
+
Features
|
6
|
+
------------
|
7
|
+
|
8
|
+
* Add/remove a copyright headers recursively on source files
|
9
|
+
* Customize the syntax configuration for how to write out comments
|
10
|
+
* Built-in support for GPL3 and MIT licenses
|
11
|
+
* Supports custom licenes with --license-file argument
|
12
|
+
* ERB template support
|
13
|
+
|
14
|
+
Caveats
|
15
|
+
-------
|
16
|
+
* Will only remove headers to files that have exactly the same header as the one we added
|
17
|
+
* Will only add headers to files which do not contain the pattern /[Cc]opyright|[Lc]icense/ in the first N lines
|
18
|
+
|
19
|
+
|
20
|
+
Requirements
|
21
|
+
------------
|
22
|
+
|
23
|
+
* Ruby 1.9.2 (supported version)
|
24
|
+
|
25
|
+
Installation & Usage
|
26
|
+
--------------------
|
27
|
+
|
28
|
+
# gem install copyright-header
|
29
|
+
# copyright-header --help
|
30
|
+
|
31
|
+
Add a GPL3 License header to a file:
|
32
|
+
|
33
|
+
copyright-header --add-path /tmp/test.rb --license GPL3 --dry-run
|
34
|
+
|
35
|
+
Remove the header created in the previous step (without --dry-run argument):
|
36
|
+
|
37
|
+
copyright-header --remove-path /tmp/test.rb --license GPL3 --dry-run
|
38
|
+
|
39
|
+
Paths can be either files or directories. It will recursively traverse the directory tree ignoring all dot files.
|
40
|
+
|
41
|
+
You can specify an alternative syntax configuration file using the --syntax argument.
|
42
|
+
|
43
|
+
|
44
|
+
Full list of supported arguments:
|
45
|
+
|
46
|
+
Usage: copyright-header options [file]
|
47
|
+
-n, --dry-run Output the parsed files to STDOUT
|
48
|
+
-o, --output-dir DIR Use DIR as output directory
|
49
|
+
--license-file FILE Use FILE as header
|
50
|
+
--license [GPL3|MIT] Use LICENSE as header
|
51
|
+
--copyright-software NAME The common name for this piece of software (e.g. "Copyright Header")
|
52
|
+
--copyright-software-description DESC
|
53
|
+
The common name for this piece of software (e.g. "A utility to manipulate copyright headers on source code files")
|
54
|
+
--copyright-holder NAME The common name for this piece of software (e.g. "Erik Osterman <e@osterman.com>"). Repeat argument for multiple names.
|
55
|
+
--copyright-year YEAR The common name for this piece of software (e.g. "2012"). Repeat argument for multiple years.
|
56
|
+
-w, --word-wrap LEN Maximum number of characters per line for license (default: 80)
|
57
|
+
-a, --add-path PATH Recursively insert header in all files found in path
|
58
|
+
-r, --remove-path PATH Recursively remove header in all files found in path
|
59
|
+
-c, --syntax FILE Syntax configuration file
|
60
|
+
-V, --version Display version information
|
61
|
+
-h, --help Display this screen
|
62
|
+
|
63
|
+
|
64
|
+
Contact Information
|
65
|
+
-------------------
|
66
|
+
|
67
|
+
Erik Osterman <e@osterman.com>
|
68
|
+
http://www.osterman.com/
|
69
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift(File.expand_path('.')) # Ruby 1.9 doesn't have . in the load path...
|
3
|
+
$:.push(File.expand_path('lib/'))
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'copyright_header'
|
7
|
+
|
8
|
+
command_line = CopyrightHeader::CommandLine.new
|
9
|
+
command_line.execute
|
data/contrib/syntax.yml
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
ruby:
|
2
|
+
ext: ['.rb']
|
3
|
+
after: ['^#!']
|
4
|
+
comment:
|
5
|
+
open: '#\n'
|
6
|
+
close: '#\n'
|
7
|
+
prefix: '# '
|
8
|
+
|
9
|
+
html:
|
10
|
+
ext: ['.html', '.htm', '.xhtml', '.xml']
|
11
|
+
comment:
|
12
|
+
open: '<!--\n'
|
13
|
+
close: '-->\n'
|
14
|
+
prefix: ' '
|
15
|
+
|
16
|
+
php:
|
17
|
+
ext: ['.php']
|
18
|
+
after: [ '^#!' ]
|
19
|
+
comment:
|
20
|
+
open: '<?php \n/*\n'
|
21
|
+
close: ' */ ?>\n'
|
22
|
+
prefix: ' * '
|
23
|
+
|
24
|
+
|
25
|
+
javacript:
|
26
|
+
ext: ['.js']
|
27
|
+
comment:
|
28
|
+
open: '/*\n'
|
29
|
+
close: ' */\n\n'
|
30
|
+
prefix: ' * '
|
31
|
+
|
32
|
+
css:
|
33
|
+
ext: ['.css']
|
34
|
+
comment:
|
35
|
+
open: '/*\n'
|
36
|
+
close: ' */\n\n'
|
37
|
+
prefix: ' * '
|
38
|
+
|
39
|
+
|
40
|
+
c:
|
41
|
+
ext: ['.java', '.c', '.cpp', '.cc']
|
42
|
+
comment:
|
43
|
+
open: '/*'
|
44
|
+
close: ' */\n\n'
|
45
|
+
prefix: ' * '
|
46
|
+
|
47
|
+
cpp:
|
48
|
+
ext: ['.cpp', '.cc', '.hh', '.hpp']
|
49
|
+
comment:
|
50
|
+
open: '//\n'
|
51
|
+
close: '//\n\n'
|
52
|
+
prefix: '// '
|
53
|
+
|
54
|
+
java:
|
55
|
+
ext: ['.java']
|
56
|
+
comment:
|
57
|
+
open: '/*\n'
|
58
|
+
close: ' */\n\n'
|
59
|
+
prefix: ' * '
|
60
|
+
|
61
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$:.push File.expand_path("../lib", __FILE__)
|
4
|
+
require "copyright_header/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "copyright-header"
|
8
|
+
s.version = CopyrightHeader::VERSION
|
9
|
+
s.authors = ["Erik Osterman"]
|
10
|
+
s.email = ["e@osterman.com"]
|
11
|
+
s.homepage = "https://github.com/osterman/copyright-header"
|
12
|
+
s.summary = %q{A utility to insert copyright headers into various types of source code files}
|
13
|
+
s.description = %q{A utility which is able to recursively insert and remove copyright headers from source code files based on file extensions.}
|
14
|
+
s.rubyforge_project = "copyright-header"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.extra_rdoc_files = ['README.md', 'LICENSE', 'AUTHORS', 'contrib/syntax.yml' ]
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module CopyrightHeader
|
4
|
+
class MissingArgumentException < Exception; end
|
5
|
+
|
6
|
+
class CommandLine
|
7
|
+
attr_accessor :options, :parser, :optparse
|
8
|
+
def initialize(options = {})
|
9
|
+
begin
|
10
|
+
@options = options
|
11
|
+
@options[:base_path] = File.expand_path File.dirname(__FILE__) + "/../../"
|
12
|
+
@optparse = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: #{$0} options [file]"
|
14
|
+
|
15
|
+
@options[:dry_run] = false
|
16
|
+
opts.on( '-n', '--dry-run', 'Output the parsed files to STDOUT' ) do
|
17
|
+
@options[:dry_run] = true
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on( '-o', '--output-dir DIR', 'Use DIR as output directory') do |dir|
|
21
|
+
@options[:output_dir] = dir + '/'
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on( '--license-file FILE', 'Use FILE as header' ) do|file|
|
25
|
+
@options[:license_file] = file
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on( '--license [' + Dir.glob(@options[:base_path] + '/licenses/*').map { |f| File.basename(f, '.erb') }.join('|') + ']', 'Use LICENSE as header' ) do|license|
|
29
|
+
@options[:license] = license
|
30
|
+
@options[:license_file] = @options[:base_path] + '/licenses/' + license + '.erb'
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on( '--copyright-software NAME', 'The common name for this piece of software (e.g. "Copyright Header")' ) do|name|
|
34
|
+
@options[:copyright_software] = name
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on( '--copyright-software-description DESC', 'The common name for this piece of software (e.g. "A utility to manipulate copyright headers on source code files")' ) do|desc|
|
38
|
+
@options[:copyright_software_description] = desc
|
39
|
+
end
|
40
|
+
|
41
|
+
@options[:copyright_holders] = []
|
42
|
+
opts.on( '--copyright-holder NAME', 'The common name for this piece of software (e.g. "Erik Osterman <e@osterman.com>"). Repeat argument for multiple names.' ) do|name|
|
43
|
+
@options[:copyright_holders] << name
|
44
|
+
end
|
45
|
+
|
46
|
+
@options[:copyright_years] = []
|
47
|
+
opts.on( '--copyright-year YEAR', 'The common name for this piece of software (e.g. "2012"). Repeat argument for multiple years.' ) do|year|
|
48
|
+
@options[:copyright_years] << year
|
49
|
+
end
|
50
|
+
|
51
|
+
@options[:word_wrap] = 80
|
52
|
+
opts.on( '-w', '--word-wrap LEN', 'Maximum number of characters per line for license (default: 80)' ) do |len|
|
53
|
+
@options[:word_wrap] = len.to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on( '-a', '--add-path PATH', 'Recursively insert header in all files found in path' ) do |path|
|
57
|
+
@options[:add_path] = path
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on( '-r', '--remove-path PATH', 'Recursively remove header in all files found in path' ) do |path|
|
61
|
+
@options[:remove_path] = path
|
62
|
+
end
|
63
|
+
|
64
|
+
@options[:syntax] = @options[:base_path] + '/contrib/syntax.yml'
|
65
|
+
opts.on( '-c', '--syntax FILE', 'Syntax configuration file' ) do |path|
|
66
|
+
@options[:syntax] = path
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on( '-V', '--version', 'Display version information' ) do
|
70
|
+
puts "CopyrightHeader #{CopyrightHeader::VERSION}"
|
71
|
+
puts "Copyright (C) 2012 Erik Osterman <e@osterman.com>"
|
72
|
+
puts "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>"
|
73
|
+
puts "This is free software: you are free to change and redistribute it."
|
74
|
+
puts "There is NO WARRANTY, to the extent permitted by law."
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
79
|
+
puts opts
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
@optparse.parse!
|
85
|
+
unless @options.has_key?(:license_file)
|
86
|
+
raise MissingArgumentException.new("Missing --license or --license-file argument")
|
87
|
+
end
|
88
|
+
|
89
|
+
unless File.file?(@options[:license_file])
|
90
|
+
raise MissingArgumentException.new("Invalid --license or --license-file argument. Cannot open #{@options[:license_file]}")
|
91
|
+
end
|
92
|
+
|
93
|
+
if @options[:license]
|
94
|
+
raise MissingArgumentException.new("Missing --copyright-software argument") if @options[:copyright_software].nil?
|
95
|
+
raise MissingArgumentException.new("Missing --copyright-software-description argument") if @options[:copyright_software_description].nil?
|
96
|
+
raise MissingArgumentException.new("Missing --copyright-holder argument") unless @options[:copyright_holders].length > 0
|
97
|
+
raise MissingArgumentException.new("Missing --copyright-year argument") unless @options[:copyright_years].length > 0
|
98
|
+
end
|
99
|
+
rescue MissingArgumentException => e
|
100
|
+
puts e.message
|
101
|
+
puts @optparse
|
102
|
+
exit (1)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def execute
|
107
|
+
@parser = CopyrightHeader::Parser.new(@options)
|
108
|
+
@parser.execute
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
module CopyrightHeader
|
7
|
+
class FileNotFoundException < Exception; end
|
8
|
+
|
9
|
+
class License
|
10
|
+
@lines = []
|
11
|
+
def initialize(options)
|
12
|
+
@options = options
|
13
|
+
@lines = load_template.split(/(\n)/)
|
14
|
+
end
|
15
|
+
|
16
|
+
def word_wrap(text, max_width = nil)
|
17
|
+
max_width ||= @options[:word_wrap]
|
18
|
+
text.gsub(/(.{1,#{max_width}})(\s|\Z)/, "\\1\n")
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_template
|
22
|
+
if File.exists?(@options[:license_file])
|
23
|
+
template = ::ERB.new File.new(@options[:license_file]).read, 0, '%<'
|
24
|
+
license = template.result(OpenStruct.new(@options).instance_eval { binding })
|
25
|
+
license += "\n" if license !~ /\n$/s
|
26
|
+
word_wrap(license)
|
27
|
+
else
|
28
|
+
raise FileNotFoundException.new("Unable to open #{file}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def format(comment_open = nil, comment_close = nil, comment_prefix = nil)
|
33
|
+
comment_open ||= ''
|
34
|
+
comment_close ||= ''
|
35
|
+
comment_prefix ||= ''
|
36
|
+
license = comment_open + @lines.map { |line| comment_prefix + line }.join() + comment_close
|
37
|
+
license.gsub!(/\\n/, "\n")
|
38
|
+
license
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Header
|
43
|
+
@file = nil
|
44
|
+
@contents = nil
|
45
|
+
@config = nil
|
46
|
+
|
47
|
+
def initialize(file, config)
|
48
|
+
@file = file
|
49
|
+
@contents = File.read(@file)
|
50
|
+
@config = config
|
51
|
+
end
|
52
|
+
|
53
|
+
def format(license)
|
54
|
+
license.format(@config[:comment]['open'], @config[:comment]['close'], @config[:comment]['prefix'])
|
55
|
+
end
|
56
|
+
|
57
|
+
def add(license)
|
58
|
+
if has_copyright?
|
59
|
+
puts "SKIP #{@file}; detected exisiting license"
|
60
|
+
return nil
|
61
|
+
end
|
62
|
+
|
63
|
+
copyright = self.format(license)
|
64
|
+
if copyright.nil?
|
65
|
+
puts "Copyright is nil"
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
|
69
|
+
text = ""
|
70
|
+
if @config.has_key?(:after) && @config[:after].instance_of?(Array)
|
71
|
+
copyright_written = false
|
72
|
+
lines = @contents.split(/\n/)
|
73
|
+
head = lines.shift(10)
|
74
|
+
while(head.size > 0)
|
75
|
+
line = head.shift
|
76
|
+
text += line + "\n"
|
77
|
+
@config[:after].each do |regex|
|
78
|
+
pattern = Regexp.new(regex)
|
79
|
+
if pattern.match(line)
|
80
|
+
text += copyright
|
81
|
+
copyright_written = true
|
82
|
+
break
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
if copyright_written
|
87
|
+
text += lines.join("\n")
|
88
|
+
else
|
89
|
+
text = copyright + text + lines.join("\n")
|
90
|
+
end
|
91
|
+
else
|
92
|
+
# Simply prepend text
|
93
|
+
text = copyright + @contents
|
94
|
+
end
|
95
|
+
return text
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove(license)
|
99
|
+
if has_copyright?
|
100
|
+
text = self.format(license)
|
101
|
+
@contents.gsub!(/#{Regexp.escape(text)}/, '')
|
102
|
+
@contents
|
103
|
+
else
|
104
|
+
puts "SKIP #{@file}; copyright not detected"
|
105
|
+
return nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def has_copyright?(lines = 10)
|
110
|
+
@contents.split(/\n/)[0..lines].select { |line| line =~ /[Cc]opyright|[Ll]icense/ }.length > 0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Syntax
|
115
|
+
def initialize(config)
|
116
|
+
@config = {}
|
117
|
+
syntax = YAML.load_file(config)
|
118
|
+
syntax.each_value do |format|
|
119
|
+
format['ext'].each do |ext|
|
120
|
+
@config[ext] = {
|
121
|
+
:before => format['before'],
|
122
|
+
:after => format['after'],
|
123
|
+
:comment => format['comment']
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def ext(file)
|
130
|
+
File.extname(file)
|
131
|
+
end
|
132
|
+
|
133
|
+
def supported?(file)
|
134
|
+
@config.has_key? ext(file)
|
135
|
+
end
|
136
|
+
|
137
|
+
def header(file)
|
138
|
+
Header.new(file, @config[ext(file)])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class Parser
|
143
|
+
attr_accessor :options
|
144
|
+
@syntax = nil
|
145
|
+
@license = nil
|
146
|
+
def initialize(options = {})
|
147
|
+
@options = options
|
148
|
+
@exclude = [ /^LICENSE(|\.txt)$/i, /^holders(|\.txt)$/i, /^README/, /^\./]
|
149
|
+
@license = License.new(:license_file => @options[:license_file],
|
150
|
+
:copyright_software => @options[:copyright_software],
|
151
|
+
:copyright_software_description => @options[:copyright_software_description],
|
152
|
+
:copyright_years => @options[:copyright_years],
|
153
|
+
:copyright_holders => @options[:copyright_holders],
|
154
|
+
:word_wrap => @options[:word_wrap])
|
155
|
+
@syntax = Syntax.new(@options[:syntax])
|
156
|
+
end
|
157
|
+
|
158
|
+
def execute
|
159
|
+
if @options.has_key?(:add_path)
|
160
|
+
add(@options[:add_path])
|
161
|
+
end
|
162
|
+
|
163
|
+
if @options.has_key?(:remove_path)
|
164
|
+
remove(@options[:remove_path])
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def transform(method, path)
|
169
|
+
paths = []
|
170
|
+
if File.file?(path)
|
171
|
+
paths << path
|
172
|
+
else
|
173
|
+
paths << Dir.glob("#{path}/**/*")
|
174
|
+
end
|
175
|
+
|
176
|
+
puts paths.inspect
|
177
|
+
|
178
|
+
paths.flatten!
|
179
|
+
|
180
|
+
paths.each do |path|
|
181
|
+
if File.file?(path)
|
182
|
+
if @exclude.include? File.basename(path)
|
183
|
+
puts "SKIP #{path}; excluded"
|
184
|
+
next
|
185
|
+
end
|
186
|
+
|
187
|
+
if @syntax.supported?(path)
|
188
|
+
header = @syntax.header(path)
|
189
|
+
contents = header.send(method, @license)
|
190
|
+
if contents.nil?
|
191
|
+
puts "SKIP #{path}; failed to generate license"
|
192
|
+
else
|
193
|
+
write(path, contents)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
else
|
197
|
+
puts "SKIP #{path}; unsupported"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Add copyright header recursively
|
203
|
+
def add(dir)
|
204
|
+
transform(:add, dir)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Remove copyright header recursively
|
208
|
+
def remove(dir)
|
209
|
+
transform(:remove, dir)
|
210
|
+
end
|
211
|
+
|
212
|
+
def write(file, contents)
|
213
|
+
puts "UPDATE #{file}"
|
214
|
+
if @options[:dry_run] || @options[:output_dir].nil?
|
215
|
+
puts contents
|
216
|
+
else
|
217
|
+
dir = "#{@options[:output_dir]}/#{File.dirname(file)}"
|
218
|
+
FileUtils.mkpath dir unless File.directory?(dir)
|
219
|
+
output_path = @options[:output_dir] + file
|
220
|
+
f =File.new(output_path, 'w')
|
221
|
+
f.write(contents)
|
222
|
+
f.close
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|