Logo

index : cp-license

---

  • summary
  • about
  • tree
  • log
  • branches
<< path: root/public/cp-license.git/html/cp-license.py blob: e398356f2025ed3aba3dc578ea2acb67fffbaac4 [raw] [clear marker]

        
0#!/bin/python3
1# MIT License
2#
3# Copyright (c) 2025 dev@ptrace.dev
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to deal
7# in the Software without restriction, including without limitation the rights
8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9# copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
22#
23from sys import exit, argv
24from shutil import copy, copy2
25from pathlib import Path
26from dataclasses import dataclass
27from datetime import datetime
28
29
30"""
31# How to add a new license
32
33Create a new license inside the `license/` directory.
34If the license has a shorter variant for adding it into source files,
35create this file inside `license/short/` with the same name.
36
37Append a new entry to the `LICENSES` dictionary. The structure for a new entry is:
38
39```python
40"short-name": {
41 "name": "License_File_Name_",
42 "url": "https://link-to-license",
43 "author": "author-placeholder-inside-license",
44 "date": "date-placeholder-inside-license",
45 "short": License_Short_Variants.DIFFERENT
46},
47```
48
49Note on [Optional] -> You can omit this field entirely.
50 Don't use booleans or `None` here if you don't need those fields.
51
52- short-name: [Mandatory] Some short alias for the argument parser
53- name: [Mandatory] The license file name
54- url: [Mandatory] Url to the license, or leave an empty string
55- author: [Optional] Some licenses have placeholders for the author name,
56 insert the placeholder here
57- date: [Optional] The same goes for the date - insert the placeholder here
58- short: [Optional] Some licenes have a short variant, so you're able to
59 prepend it in your source file.
60 This field expects the type `License_Short_Variants`.
61 For more information, please inspect this class.
62
63That's it.
64"""
65
66
67
68@dataclass
69class License_Short_Variants:
70 """
71 Some licenses provide a shorter variant,
72 which is meant to be prepended to your source code.
73
74 But other licenses are short enough already.
75 So we're just accounting for both variants here.
76 """
77 SAME = "short-same" # The same license file can be used
78 DIFFERENT = "short-different" # A different license file has to be used,
79 # stored inside license/short/
80
81
82LICENSES = {
83 "agpl3": {
84 "name": "GNU_AGPL_v3",
85 "url": "https://choosealicense.com/licenses/agpl-3.0/",
86 "author": "<name of author>",
87 "date": "<year>",
88 "short": License_Short_Variants.DIFFERENT,
89 },
90 "gpl3": {
91 "name": "GNU_GPL_v3",
92 "url": "https://choosealicense.com/licenses/gpl-3.0/",
93 "author": "<name of author>",
94 "date": "<year>",
95 "short": License_Short_Variants.DIFFERENT,
96 },
97 "lgpl3": {
98 "name": "GNU_LGPL_v3",
99 "url": "https://choosealicense.com/licenses/lgpl-3.0/",
100 },
101 "apache2": {
102 "name": "Apache_License_v2_0",
103 "url": "https://choosealicense.com/licenses/apache-2.0/",
104 "author": "[name of copyright owner]",
105 "date": "[yyyy]",
106 "short": License_Short_Variants.DIFFERENT,
107 },
108 "boost1": {
109 "name": "Boost_Software_License_v_1_0",
110 "url": "https://choosealicense.com/licenses/bsl-1.0/",
111 "short": License_Short_Variants.SAME,
112 },
113 "mit": {
114 "name": "MIT_License",
115 "url": "https://choosealicense.com/licenses/mit/",
116 "author": "[fullname]",
117 "date": "[year]",
118 "short": License_Short_Variants.SAME,
119 },
120 "unlicense": {
121 "name": "The_Unlicense",
122 "url": "https://choosealicense.com/licenses/unlicense/",
123 "short": License_Short_Variants.SAME,
124 },
125 "cc0": {
126 "name": "Public_Domain_CC0_1_0",
127 "url": "https://creativecommons.org/publicdomain/zero/1.0/",
128 "short": License_Short_Variants.SAME,
129 },
130 "bsd3clause": {
131 "name": "BSD_3_CLAUSE",
132 "url": "https://opensource.org/license/bsd-3-clause",
133 "author": "[fullname]",
134 "date": "[year]",
135 },
136}
137
138
139
140class Language_Comment_Tokens:
141 """
142 # How to add a new language comment token
143 Inside the constructor, add a new variable that contains the token,
144 that is used by the language as comment or docstring (whatever the
145 convention is).
146
147 If it's a inline comment, just add a single string.
148
149 If it's a multiline comment, add a tuple where:
150 - position 0 -> the start of a comment
151 - position 1 -> some padding
152 - position 2 -> the end of a comment
153 """
154 def __init__(self):
155 self.bash = '#'
156 self.c = ("/*", " *", " */")
157 self.cpp = ("/*", " *", " */")
158 self.cs = "//"
159 self.go = "//"
160 self.jai = ("/*", " *", " */")
161 self.java = ("/*", " *", " */")
162 self.js = ("/*", " *", " */")
163 self.lua = "--"
164 self.python = '#'
165 self.rust = "//!"
166
167 def tokens_get(self, language):
168 for k, v in self.__dict__.items():
169 if language.lower() == k:
170 return v
171 log(3, "Couldn't find a supported language.")
172 print(self)
173 exit(1)
174
175 def __repr__(self):
176 buf = []
177 print("Supported languages:")
178 for name, _ in self.__dict__.items():
179 buf.append(f" - {name.capitalize()}")
180 return "\n".join(buf)
181
182
183
184class Args:
185 def __init__(self):
186 self.help = (["h", "help"], "Prints this help menu")
187 self.url = (["url"], "Prints a single url of a license: `cp-license url mit`")
188 self.urls = (["urls"], "Prints the url of all licenses")
189 self.licenses = (["l", "list"], "Lists all licenses")
190 self.languages = (["lang", "languages"], "Lists all supported languages")
191 self.prepend = (
192 ["pre", "prepend"],
193 "Prepends the short license variant to a file. " \
194 "Use `cp-license h pre` for more information"
195 )
196
197 def max_width(self):
198 max = 0
199 for _, value in self.__dict__.items():
200 current = len(', '.join(value[0]))
201 max = current if current > max else max
202 return max
203
204 def __repr__(self):
205 width = self.max_width()
206 buf = []
207 print("Available commands:")
208 for _, value in self.__dict__.items():
209 out = util_print_by_columns(', '.join(value[0]), value[1], width+6)
210 buf.append(f" - {out}")
211 return "\n".join(buf)
212
213
214
215EXEC_FILE_LOCATION = Path(__file__).resolve(strict=True).parent
216LICENSES_DIR = EXEC_FILE_LOCATION / Path("licenses")
217
218
219
220# Procs: comment_*
221def comment_prepend_short_license(license_short_name, author, date, language, source_code_fp: Path):
222 if date == "0": date = None
223
224 license_ = license_validate_and_return(license_short_name)
225 license_variant_type = license_.get("short")
226 license_author = license_.get("author")
227
228 if not license_variant_type:
229 log(3, "No short license variant specified or available for this license")
230 exit(1)
231
232 if not source_code_fp.exists():
233 log(3, f"The source code file you supplied does not exist: `{source_code_fp}`")
234 exit(1)
235
236 if not author and license_author:
237 log(
238 2,
239 "This licence has a placeholder for the author. "
240 "But you did not supplied an author name. You might adapt it manually."
241 )
242
243 author = license_author
244 date = None
245
246 sc_fp = source_code_fp.resolve()
247 short_variant = True if license_variant_type == License_Short_Variants.DIFFERENT else False
248
249 content = license_add_author_date(
250 license_short_name,
251 author,
252 date=date,
253 short_variant=short_variant
254 )
255 content = content.splitlines()
256
257 tokens = Language_Comment_Tokens().tokens_get(language)
258
259 if isinstance(tokens, str):
260 new_content = comment_line(tokens, content)
261 elif isinstance(tokens, tuple):
262 new_content = comment_wrapped(tokens, content)
263 else:
264 raise RuntimeError("Unexpected type")
265
266 comment_backup_source_code_file(sc_fp)
267 comment_save_to_file(new_content, sc_fp)
268
269
270def comment_line(token, content):
271 buf = []
272 for line in content:
273 space = "" if not line else " "
274 buf.append(f"{token}{space}{line}")
275 return buf
276
277
278def comment_wrapped(tokens, content):
279 assert len(tokens) == 3, "Expected three tokens"
280 buf = [tokens[0]]
281 line: str
282 for line in content:
283 space = "" if not line else " "
284 buf.append(f"{tokens[1]}{space}{line}")
285 buf.append(tokens[2])
286 return buf
287
288
289def comment_check_shebang_and_editor_modes(content):
290 token_shebang = "#!"
291 token_editor_mode = "-*-"
292 current_line_no = -1
293 head = 5
294
295 # only checking the first few lines. I don't expect someone
296 # with an insane amount of editor modes ...
297
298 for idx, line in enumerate(content[:head]):
299 if line.startswith(token_shebang):
300 current_line_no = idx
301 elif token_editor_mode in line:
302 current_line_no = idx
303
304 return current_line_no
305
306
307def comment_concat_license_and_source_file(license_text, source_file):
308 source_file_lines = source_file.splitlines()
309 license_text = "\n".join(license_text)
310 license_text = license_text + "\n" if not license_text.endswith("\n") else license_text
311
312 split_idx = comment_check_shebang_and_editor_modes(source_file_lines)
313
314 if split_idx == -1:
315 return license_text + "\n" + source_file
316
317 head = "\n".join(source_file_lines[:split_idx+1])
318 tail = "\n".join(source_file_lines[split_idx+1:])
319
320 return head + "\n" + license_text + tail
321
322
323def comment_backup_source_code_file(source_code_fp: Path):
324 backup_fn = source_code_fp.name + ".bak"
325 backup_fp = source_code_fp.with_name(backup_fn)
326
327 if backup_fp.exists():
328 log(1, f"backup already exists: '{backup_fp}'")
329 return
330
331 try:
332 copy2(source_code_fp, backup_fp)
333 except Exception as e:
334 print(e)
335 exit(1)
336
337 log(1, f"created a backup of '{source_code_fp}'")
338
339
340def comment_save_to_file(license_text, source_code_fp):
341 file = util_file_read(source_code_fp)
342 buf = comment_concat_license_and_source_file(license_text, file)
343 util_file_write(source_code_fp, buf)
344
345
346
347# Procs: print_*
348def print_help():
349 help_text = """Creates a license of your choice inside a target directory.
350
351Usage: cp-license [license short name] [destination directory] [author] [date]
352
353[license short name] (Mandatory) -> pass `l` or `list` to print all license aliases.
354[destination directory] (Mandatory) -> path where the license should be stored.
355[author] (Optional) -> this will be placed inside the license
356[date] (Optional) -> this will be placed inside the license.
357 If omitted, it will automatically use the
358 current year.
359
360Examples:
361 cp-license mit myprojectdir <- creates a MIT License
362 cp-license gpl3 myprojectdir Peter 2019 <- creates a GPL License with the
363 author & date replaced
364"""
365 print(help_text)
366 print(Args())
367
368
369def print_prepend():
370 help_text = """Prepends a short variant of a license in your source file.
371
372Usage: cp-license pre [license short name] [author] [date] [language] [source file]
373
374[license short name] (Mandatory)
375[author] (Optional) -> if you omit this, omit [date] too
376[date] (Optional) -> if you pass `0` it will use the current year
377[language] (Mandatory) -> which comment type should be used.
378 Use `cp-license languages` for a list.
379[source file] (Mandatory) -> which file should be modified.
380 Note: It creates a backup of your original file.
381
382Examples:
383 cp-license pre mit Peter 0 c src/main.c <- prepends the MIT license to your main.c file
384 cp-license pre cc0 java src/main.java <- the same with the CC0 license
385
386Note:
387 - If you have a license that has a field for author/date, but you did not
388 supply those arguments, it will prepend the license anyway and display
389 a warning.
390 - If you have shebang or editor modes on top of your file, it will respect them
391 and place the licence after those.
392"""
393 print(help_text)
394
395
396def print_licenses():
397 width = utils_max_width_of('name') + 6
398
399 print("Available licenses (<Name of license> -> <cli alias>):")
400 for k, v in LICENSES.items():
401 out = util_print_by_columns(f" - {v['name']}", f"-> {k}", width)
402 print(out)
403
404
405def print_urls():
406 width = utils_max_width_of('name') + 6
407 for _, v in LICENSES.items():
408 out = util_print_by_columns(f" - {v['name']}", f"-> {v['url']}", width)
409 print(out)
410
411
412def print_url(license_short_name):
413 license_ = LICENSES.get(license_short_name)
414 if not license_:
415 log(3, f"Could not find your license: '{license_short_name}'")
416 print_licenses()
417 exit(1)
418 print(f"{license_['name']} -> {license_['url']}")
419
420
421
422# Procs: license_*
423def license_validate_and_return(license_short_name):
424 license_ = LICENSES.get(license_short_name.lower())
425 if not license_:
426 log(3, f"Could not find your license: '{license_short_name}'")
427 print_licenses()
428 exit(1)
429 return license_
430
431
432def license_path(license_short_name, short_variant=False):
433 license_ = license_validate_and_return(license_short_name)
434
435 if short_variant:
436 license_fp = LICENSES_DIR / Path("short") / Path(license_['name'])
437 else:
438 license_fp = LICENSES_DIR / Path(license_['name'])
439
440 if not license_fp.exists():
441 log(3, f"Could not find license file in '{license_fp}'")
442 exit(1)
443
444 return license_fp
445
446
447def license_add_author_date(license_short_name, author, date=None, short_variant=False):
448 license_fp = license_path(license_short_name, short_variant)
449 date = date if date else datetime.now().year
450
451 license_file = util_file_read(license_fp)
452
453 # The type of this variable gets already checked by `license_path()`
454 license_ = LICENSES.get(license_short_name.lower())
455
456 indicator_author = license_.get("author") # type: ignore[reportOptionalMemberAccess]
457 indicator_date = license_.get("date") # type: ignore[reportOptionalMemberAccess]
458
459 license_content = license_file
460
461 if indicator_author:
462 license_content = license_content.replace(indicator_author, author)
463 if indicator_date:
464 license_content = license_content.replace(indicator_date, str(date))
465
466 return license_content
467
468
469def license_create_with_author_date_info(license_short_name, destination_dir, author, date=None):
470 dest = destination_dir_validate_and_resolve(destination_dir) / Path("LICENSE")
471 dest = destination_file_rename_if_exists(dest)
472
473 license_content = license_add_author_date(license_short_name, author, date)
474 util_file_write(dest, license_content)
475
476
477def license_copy_to_destination(license_short_name, destination_dir: Path):
478 license_file = license_path(license_short_name)
479
480 dest = destination_dir_validate_and_resolve(destination_dir) / Path("LICENSE")
481 dest = destination_file_rename_if_exists(dest)
482
483 copy(license_file, dest)
484 log(1, f"Copied {LICENSES[license_short_name]['name']} to '{dest}'")
485
486
487# Procs: destination_*
488def destination_dir_validate_and_resolve(dest: Path):
489 if not dest.exists():
490 log(3, f"Destination directory does not exists: '{dest}'")
491 exit(1)
492 return dest.resolve()
493
494
495def destination_file_rename_if_exists(dest: Path):
496 if dest.exists():
497 new_fp = dest.name + ".new_license"
498 log(2,
499 f"a file with the same name already exists. " \
500 f"Thew created license is now stored with a new name: '{new_fp}'"
501 )
502 return dest.with_name(new_fp)
503 return dest
504
505
506
507# Procs: util_*
508def util_print_by_columns(str1, str2, width):
509 a = str1.ljust(width)
510 return a + str2
511
512
513def utils_max_width_of(key):
514 max = 0
515 for _, v in LICENSES.items():
516 current = len(v[key])
517 max = current if current > max else max
518 return max
519
520
521def util_file_read(fp: Path):
522 try:
523 with open(fp, "r", encoding="utf8") as rf:
524 return rf.read()
525 except Exception as e:
526 log(3, e)
527 exit(1)
528
529
530def util_file_write(fp: Path, content):
531 try:
532 with open(fp, "w+", encoding="utf8") as wf:
533 wf.write(content)
534 except Exception as e:
535 log(3, e)
536 exit(1)
537
538
539
540# Procs: log_*
541def log(code, msg):
542 levels = [
543 "DEBUG", "INFO", "WARNING", "ERROR"
544 ]
545 print(f"[ {levels[code]} ]: {msg}")
546
547
548
549# Procs: parse_*
550def parse_args(args):
551
552 def validate(arg, available_args):
553 if arg in available_args:
554 return True
555 return False
556
557 def idk():
558 log(3, "Unknown arguments")
559 exit(1)
560
561 arguments = Args()
562
563 if len(args) < 2:
564 print_help()
565 exit(1)
566
567 elif len(args) == 2:
568 if validate(args[1], arguments.help[0]):
569 print_help()
570 exit(0)
571
572 if validate(args[1], arguments.urls[0]):
573 print_urls()
574 exit(0)
575
576 if validate(args[1], arguments.licenses[0]):
577 print_licenses()
578 exit(0)
579
580 if validate(args[1], arguments.languages[0]):
581 print(Language_Comment_Tokens())
582 exit(0)
583
584 idk()
585
586 elif len(args) == 3:
587 if validate(args[1], arguments.url[0]):
588 print_url(args[2])
589 exit(0)
590
591 if validate(args[1], arguments.help[0]) and validate(args[2], arguments.prepend[0]):
592 print_prepend()
593 exit(0)
594
595 license_copy_to_destination(
596 license_short_name=args[1],
597 destination_dir=Path(args[2])
598 )
599 exit(0)
600
601 elif len(args) == 4:
602 license_create_with_author_date_info(
603 license_short_name=args[1],
604 destination_dir=Path(args[2]),
605 author=args[3]
606 )
607 exit(0)
608
609 elif len(args) == 5:
610 if validate(args[1], arguments.prepend[0]):
611 comment_prepend_short_license(
612 license_short_name=args[2],
613 author=None,
614 date=None,
615 language=args[3],
616 source_code_fp=Path(args[4])
617 )
618 exit(0)
619
620 license_create_with_author_date_info(
621 license_short_name=args[1],
622 destination_dir=Path(args[2]),
623 author=args[3],
624 date=args[4]
625 )
626 exit(0)
627
628 elif len(args) == 7:
629 if validate(args[1], arguments.prepend[0]):
630 comment_prepend_short_license(
631 license_short_name=args[2],
632 author=args[3],
633 date=args[4],
634 language=args[5],
635 source_code_fp=Path(args[6])
636 )
637 exit(0)
638 else:
639 log(3, "Cannot parse this combination of arguments. Use `h` for help.")
640 exit(1)
641
642
643if __name__ == "__main__":
644 parse_args(argv)
645
646
Copyright 2026  E766CB298A6D1E64 | Git-Thing heavily inspired by cgit