├── README.md ├── all2pdf.sh ├── ceval.pl ├── contributors.py ├── cve2bibtex.py ├── dismas.ml ├── github-contributor-stats.py ├── gost94sums.sh ├── installdate.sh ├── irclogmailer.sh ├── list-required-firmware.py ├── maya_timestamp.py ├── motto_clock.sh ├── nproc.py ├── ocacc.py ├── otpgen.pl ├── phabricator-query.py ├── randomfile ├── sign-release.sh ├── sshforget ├── usg-config-export.py └── vyos-release-notes.py /README.md: -------------------------------------------------------------------------------- 1 | scripts 2 | ======= 3 | 4 | Miscellaneous scripts written at various times for various purposes. 5 | 6 | Some may not work in certain (many?) environments. 7 | If you want to use any of them and they don't work for you, please let me know, 8 | though I can't promise that I will actually fix them. 9 | 10 | ## File list 11 | 12 | * nproc.py 13 | 14 | Like `nproc` from GNU coreutils, but returns the number of physical cores (while `nproc` does not adjust for Hyper-Threading). 15 | 16 | * usg-config-export.py 17 | 18 | Exports specified sections of a Ubiquiti Unified Service Gateway config into JSON. 19 | 20 | * installdate.sh 21 | 22 | Attempts to determine installation date 23 | of a Linux, FreeBSD, Mac OS X, or Solaris system. 24 | 25 | * motto_clock.sh 26 | 27 | Shows the current time plus a time-themed motto in the style of medieval tower clocks. 28 | 29 | * ceval.pl 30 | 31 | Compiles and executes C one-liners with gcc. Usage example: `./ceval.pl 'int a = 5 % 2 == 0 ? 1 : 0; printf("%d\n", a);'` 32 | 33 | * maya_timestamp.py 34 | 35 | Converts UNIX timestamp to the Maya calendar date. Usage example: `./maya_timestamp.py $(date +%s)`. 36 | 37 | * gost94sums.sh 38 | 39 | Creates GOST94 sums file in format analogous to the usual md5sums or sha1sums (may not work with new OpenSSL versions). 40 | 41 | * otpgen.pl 42 | 43 | Generates a PDF or plain text one time pad. 44 | 45 | * irclogmailer.sh 46 | 47 | Emails yesterday irssi logs to watchers. 48 | 49 | * randomfile 50 | 51 | Picks a random file from given directory. 52 | 53 | * sshforget 54 | 55 | Removes a line from SSH known_hosts. 56 | 57 | * all2pdf.sh 58 | 59 | gunzips and converts all DVI and PS files in a directory to PDF. 60 | 61 | * phabricator-query.py 62 | 63 | Automatically retrieves task data from a Phabricator query 64 | and displays it in HTML or plain text. 65 | 66 | * ocacc.py 67 | 68 | Obsessive-compulsive acronym capitalization checker. 69 | Checks abbreviation/acronym capitalization style against 70 | a file that lists correctly capitalized versions one per line. 71 | 72 | * vyos-release-notes.py 73 | 74 | Generates release notes from Phabricator. It uses custom fields from the VyOS phabricator instance, 75 | so it's only really useful for VyOS. You can adapt it to your own project, but you are on your own there. ;) 76 | 77 | * cve2bibtex.py 78 | 79 | Converts CVE 5.0 JSON format to a BibLaTeX @online macro for citing. 80 | -------------------------------------------------------------------------------- /all2pdf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # In the perfect world, all academic papers 4 | # are in standards-compliant PDF with proper metadata. 5 | # In practice they come in all sizes and shapes: 6 | # PostScript, DVI, or gzipped versions of those. 7 | # This script homogenizes a directory with papers 8 | # by converting them all to PDFs 9 | # 10 | # This file is public domain. 11 | 12 | convert_all() 13 | { 14 | SUFFIX=$1 15 | CONVERTOR=$2 16 | 17 | PATTERN="s/\.$SUFFIX$/\.pdf/" 18 | 19 | find . -type f -name "*.$SUFFIX" -print0 | while read -d $'\0' FILENAME; do 20 | PDFNAME=$(echo $FILENAME | sed -e $PATTERN) 21 | echo "$FILENAME -> $PDFNAME" 22 | $CONVERTOR $FILENAME $PDFNAME 23 | done 24 | 25 | find . -type f -name "*.$SUFFIX" -delete 26 | } 27 | 28 | find . -type f -name "*.gz" | xargs gunzip 29 | 30 | convert_all ps ps2pdf 31 | convert_all dvi dvipdf 32 | -------------------------------------------------------------------------------- /ceval.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | my $CC = "gcc -x c -lm"; 7 | my $TMP_DIR = "/tmp/"; 8 | 9 | my $template = < 11 | #include 12 | #include 13 | #include 14 | 15 | int main(void) 16 | { 17 | __PLACEHOLDER__; 18 | return(0); 19 | } 20 | 21 | EOL 22 | 23 | if( $#ARGV < 0 ) 24 | { 25 | print "I see no point in evaluating empty string.\n"; 26 | exit(1); 27 | } 28 | elsif( $#ARGV > 0) 29 | { 30 | print "Too many arguments\n"; 31 | exit(1); 32 | } 33 | 34 | my $input = $ARGV[0]; 35 | 36 | # Join timestamp and a random number in hope 37 | # it gives a unique file name 38 | my $file_name = $TMP_DIR . time() . int(rand(1000)); 39 | my $src_file_name = $file_name . ".c"; 40 | 41 | my $code = $template; 42 | $code =~ s/__PLACEHOLDER__/$input/; 43 | 44 | open( HANDLE, ">$src_file_name" ); 45 | print HANDLE $code; 46 | close( HANDLE ); 47 | 48 | my $cc_output = `$CC -o $file_name $src_file_name`; 49 | unlink($src_file_name); 50 | print "$cc_output"; 51 | 52 | if( $? == 0 ) 53 | { 54 | my $output = `$file_name`; 55 | print "$output\n"; 56 | unlink($file_name); 57 | exit(0); 58 | } 59 | else 60 | { 61 | exit(1); 62 | } 63 | 64 | -------------------------------------------------------------------------------- /contributors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2019 Daniil Baturin 3 | # 4 | # Permission is hereby granted, free of charge, 5 | # to any person obtaining a copy of this software 6 | # and associated documentation files (the "Software"), 7 | # to deal in the Software without restriction, 8 | # including without limitation the rights to use, 9 | # copy, modify, merge, publish, distribute, sublicense, 10 | # and/or sell copies of the Software, and to permit persons 11 | # to whom the Software is furnished to do so, subject 12 | # to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice 15 | # shall be included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | # 25 | # Synopsis: like "git shortlog", but works across multiple repos 26 | # For counting contributors to a project that has more than one repo 27 | # 28 | # Clone all the repos into one dir, then run 29 | # contributors.py --since $DATE /path/to/repos/dir 30 | 31 | import re 32 | import os 33 | import sys 34 | import operator 35 | import argparse 36 | import subprocess 37 | 38 | GIT_LOG_CMD = "git shortlog --summary --numbered --email" 39 | 40 | parser = argparse.ArgumentParser() 41 | parser.add_argument('--pull', action='store_true', help='Force git pull') 42 | parser.add_argument('--since', type=str, help='Earliest date to count from') 43 | parser.add_argument('source_path', type=str, help='Path to the directory with git repos') 44 | 45 | args = parser.parse_args() 46 | 47 | if args.since: 48 | GIT_LOG_CMD += " --since {0}".format(args.since) 49 | 50 | repos = filter(lambda x: os.path.isdir(os.path.join(args.source_path, x)), os.listdir(args.source_path)) 51 | 52 | contributors = {} 53 | 54 | def update_contributors(contributors): 55 | cmd = GIT_LOG_CMD 56 | 57 | p = subprocess.Popen([cmd], stdout=subprocess.PIPE, shell=True) 58 | out = p.stdout.readlines() 59 | 60 | for l in out: 61 | commits, name, email = re.match(r'^\s*(\d+)\s+(.*)\s+<(.*)>\s*$', l.decode()).groups() 62 | if email in contributors: 63 | contributors[email]['commits'] += int(commits) 64 | else: 65 | contributors[email] = {} 66 | contributors[email]['commits'] = int(commits) 67 | contributors[email]['name'] = name 68 | 69 | for r in repos: 70 | repo_path = os.path.join(args.source_path, r) 71 | print("Processing {0}".format(repo_path), file=sys.stderr) 72 | os.chdir(repo_path) 73 | if args.pull: 74 | os.system("git pull") 75 | update_contributors(contributors) 76 | 77 | contributors = sorted(contributors.items(), key=lambda x: operator.itemgetter(1)(x)['commits'], reverse=True) 78 | 79 | for k in contributors: 80 | email, data = k 81 | print("{0} {1} {2}".format(data['commits'], data['name'], email)) 82 | -------------------------------------------------------------------------------- /cve2bibtex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This script converts CVE 5.0 JSON format to BibTeX 4 | # to make it easy to cite CVEs in LaTeX documents. 5 | # 6 | # It uses the BibLaTeX "@online" macro, 7 | # although it's trivial to adjust to the classic @MISC of BibTeX, of course. 8 | # 9 | # Typical usage: curl https://cveawg.mitre.org/api/cve/CVE-2023-38408 | cve2bibtex 10 | # 11 | # Copyright (c) 2023 Daniil Baturin 12 | # 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy 14 | # of this software and associated documentation files (the "Software"), to deal 15 | # in the Software without restriction, including without limitation the rights 16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 17 | # of the Software, and to permit persons to whom the Software is furnished to do so, 18 | # subject to the following conditions: 19 | # 20 | # The above copyright notice and this permission notice shall be included in all copies 21 | # or substantial portions of the Software. 22 | # 23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 24 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 25 | # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 26 | # FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 27 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | 30 | import re 31 | import sys 32 | import json 33 | import dateutil.parser 34 | 35 | data = json.load(sys.stdin) 36 | 37 | cve_id = data["cveMetadata"]["cveId"] 38 | 39 | cve_date = dateutil.parser.isoparse(data["cveMetadata"]["datePublished"]) 40 | cve_year = cve_date.year 41 | cve_month = cve_date.month 42 | 43 | cve_url = f"https://www.cve.org/CVERecord?id={cve_id}" 44 | 45 | # Find any English description 46 | # The descriptions field is an array of {"lang": $code, "value": $desc} objects. 47 | # 48 | # In practice, all CVEs have one with {"lang": "en"}, 49 | # but the schema demands support for regional variants: 50 | # https://github.com/CVEProject/cve-schema/blob/8994b0ac23f89d7b2c8d750500bc88400e4336d7/schema/v5.0/CVE_JSON_5.0_schema.json#L1027-L1031 51 | for d in data["containers"]["cna"]["descriptions"]: 52 | if re.match(r'^en([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))?$', d["lang"]): 53 | cve_desc = d["value"] 54 | 55 | bibtex_tmpl = f""" 56 | @online{{{cve_id}, 57 | author = {{MITRE}}, 58 | title = {{{cve_id}: {cve_desc}}}, 59 | month = {cve_month} 60 | year = {cve_year}, 61 | url = {{{cve_url}}}, 62 | }} 63 | """ 64 | 65 | print(bibtex_tmpl) 66 | -------------------------------------------------------------------------------- /dismas.ml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ocaml 2 | 3 | (* 4 | * Copyright (C) 2020 Daniil Baturin 5 | * 6 | * Permission is hereby granted, free of charge, 7 | * to any person obtaining a copy of this software 8 | * and associated documentation files (the "Software"), 9 | * to deal in the Software without restriction, 10 | * including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, 12 | * and/or sell copies of the Software, and to permit persons 13 | * to whom the Software is furnished to do so, subject 14 | * to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice 17 | * shall be included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 23 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 25 | * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | * 27 | * Synopsis: automatically makes cross-versions of OCaml packages 28 | *) 29 | 30 | 31 | #use "topfind";; 32 | #require "opam-file-format";; 33 | 34 | open OpamParserTypes 35 | 36 | 37 | (* Some constraints, notably {with-test} and {with-doc} make no sense for cross-versions of packages 38 | since running tests of cross-compiled libraries is generally impossible, 39 | and docs are better left to native versions. *) 40 | 41 | let useless_targets = ["with-doc"; "with-test"] 42 | 43 | let in_list x xs = 44 | let res = List.find_opt ((=) x) xs in 45 | match res with 46 | | None -> false 47 | | _ -> true 48 | 49 | let rec is_useless_target opt = 50 | match opt with 51 | | Ident (_, i) -> in_list i useless_targets 52 | | Logop (_, _, l, r) -> (is_useless_target l) || (is_useless_target r) 53 | | _ -> false 54 | 55 | let has_useless_target opts = 56 | let res = List.find_opt is_useless_target opts in 57 | match res with 58 | | None -> false 59 | | _ -> true 60 | 61 | (* One thing we need to fix is build commands. 62 | 63 | The first thing to fix is the command used to build the package. 64 | That involves adding a cross-toolchain option and replacing metavariables. 65 | *) 66 | 67 | (* If a list starts with "dune", it must be a dune command. *) 68 | let is_dune_command opts = 69 | match opts with 70 | | String (_, "dune") :: _ -> true 71 | | _ -> false 72 | 73 | (* Fixing the dune build command. 74 | 75 | For cross-versions, ["dune", "-p", name] doesn't work because the opam package name is 76 | $package-$target (e.g. frobnicate-windows), while dune package name stays the same. 77 | We have to hardcode the package name instead. 78 | 79 | Positions get messed up, but it shouldn't matter. 80 | *) 81 | let rec fix_dune_package_name package opts = 82 | match opts with 83 | | [] -> [] 84 | | String (pos1, "-p") :: Ident (pos2, "name") :: ds' -> 85 | String (pos1, "-p") :: String (pos2, package) :: ds' 86 | | opt :: opts' -> opt :: (fix_dune_package_name package opts') 87 | 88 | (* Fix the build section. 89 | 90 | We need to 91 | 1. Adjust the build commands to take the cross-toolchain into account 92 | 2. Remove useless targets like {with-test} and {with-doc} that make no sense for cross-versions 93 | *) 94 | let rec fix_build_section target package bopts = 95 | match bopts with 96 | | [] -> [] 97 | | (List (pos, opts)) :: bopts' -> 98 | if (is_dune_command opts) then 99 | (* Dune supports explicit toolchain option (-x $target), so we need to fix the package name 100 | and add that option. 101 | 102 | Positions for the new options are completely fake, but since the formatter ignores them, 103 | it shouldn't be a problem. 104 | *) 105 | (List (pos, (List.append (fix_dune_package_name package opts) [String (pos, "-x"); String (pos, target)])) :: 106 | (fix_build_section target package bopts')) 107 | else (List (pos, opts)) :: (fix_build_section target package bopts') 108 | | (Option (pos, opts, constraints) as bopt) :: bopts' -> 109 | if (has_useless_target constraints) then (fix_build_section target package bopts') 110 | else bopt :: (fix_build_section target package bopts') 111 | | bopt :: bopts' -> bopt :: (fix_build_section target package bopts') 112 | 113 | 114 | (* Next step is fixing dependencies. 115 | 116 | Most of the time, a cross-version of a package requires cross-versions of all its dependencies, 117 | though in some cases both native and cross-version are needed. 118 | 119 | We also remove dependencies that are only needed for build targets like {with-doc} and {with-test}. 120 | *) 121 | 122 | (* Build tools are always native and do not have cross-versions. 123 | Thus build tool dependencies should never be rewritten. *) 124 | let no_cross_deps = ["dune"; "dune-configurator"; "ocamlbuild"; "oasis"; "ocamlfind"] 125 | 126 | let make_cross_dep target dep = 127 | if not (in_list dep no_cross_deps) then Printf.sprintf "%s-%s" dep target 128 | else dep 129 | 130 | let rec fix_dependencies target deps = 131 | match deps with 132 | | [] -> [] 133 | | (String (pos, dep)) :: deps -> 134 | (String (pos, make_cross_dep target dep)) :: (fix_dependencies target deps) 135 | | (Option (pos1, (String (pos2, dep)), constraints)) :: deps -> 136 | if has_useless_target constraints then fix_dependencies target deps 137 | else (Option (pos1, (String (pos2, make_cross_dep target dep)), constraints)) :: (fix_dependencies target deps) 138 | | dep :: deps' -> dep :: fix_dependencies target deps 139 | 140 | let rec apply_fixes target package opts = 141 | match opts with 142 | | [] -> [] 143 | | Variable (pos1, "build", (List (pos2, bs))) :: opts' -> 144 | (Variable (pos1, "build", (List (pos2, fix_build_section target package bs)))) :: apply_fixes target package opts' 145 | | Variable (pos1, "depends", (List (pos2, ds))) :: opts' -> 146 | (Variable (pos1, "depends", (List (pos2, fix_dependencies target ds)))) :: apply_fixes target package opts' 147 | | opt :: opts' -> opt :: apply_fixes target package opts' 148 | 149 | let fix_opam_file target package opam_file = 150 | let new_contents = apply_fixes target package opam_file.file_contents in 151 | OpamPrinter.opamfile {opam_file with file_contents=new_contents} 152 | 153 | let () = 154 | let target = Sys.argv.(1) in 155 | let package = Sys.argv.(2) in 156 | let file = Sys.argv.(3) in 157 | let opam_file = OpamParser.file file in 158 | let new_file = fix_opam_file target package opam_file in 159 | print_endline new_file 160 | -------------------------------------------------------------------------------- /github-contributor-stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2021 Daniil Baturin 3 | # 4 | # Permission is hereby granted, free of charge, 5 | # to any person obtaining a copy of this software 6 | # and associated documentation files (the "Software"), 7 | # to deal in the Software without restriction, 8 | # including without limitation the rights to use, 9 | # copy, modify, merge, publish, distribute, sublicense, 10 | # and/or sell copies of the Software, and to permit persons 11 | # to whom the Software is furnished to do so, subject 12 | # to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice 15 | # shall be included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | # 25 | # Synopsis: expors repository contributor data from GitHub over its REST API 26 | # 27 | # Usage: GITHUB_TOKEN=... github-contributors.py user/repo 28 | 29 | import os 30 | import sys 31 | import json 32 | import urllib.request 33 | 34 | def make_request(query_url): 35 | req = urllib.request.Request(query_url) 36 | token = os.getenv("GITHUB_TOKEN") 37 | if token: 38 | req.add_header("Authorization", "token {0}".format(token)) 39 | 40 | with urllib.request.urlopen(req) as f: 41 | data = json.load(f) 42 | 43 | return data 44 | 45 | def fetch_contributors(repo): 46 | contributors = [] 47 | page = 0 48 | while True: 49 | query_url = "https://api.github.com/repos/{0}/contributors?page={1}".format(repo, page) 50 | data = make_request(query_url) 51 | if data: 52 | contributors += data 53 | page += 1 54 | else: 55 | break 56 | 57 | return contributors 58 | 59 | if __name__ == '__main__': 60 | contributors = fetch_contributors(sys.argv[1]) 61 | for c in contributors: 62 | # Get user's stats 63 | query_url = "https://api.github.com/users/{0}".format(c["login"]) 64 | user_data = make_request(query_url) 65 | 66 | # Screw datetime parsing. ;) 67 | user_registration_year = user_data['created_at'][:4] 68 | 69 | print("{0},{1},{2}".format(c['login'], c['contributions'], user_registration_year)) 70 | 71 | -------------------------------------------------------------------------------- /gost94sums.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # In Soviet Russia source code adds license headers to you!! 3 | # Uhm, I mean, this file is public domain. 4 | 5 | files=`find . -maxdepth 1 -name "*.*" -type f` 6 | 7 | for i in $files; do 8 | file=`echo $i | awk -F '/' '{print $2}'` 9 | echo `openssl dgst -md_gost94 $file | awk -F ' ' '{print $2}'` $file >> gost94sums 10 | done 11 | -------------------------------------------------------------------------------- /installdate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Try to find out system installation date 4 | 5 | # WARNING: it isn't a forensic tool or something, 6 | # as there aren't really immutable entities in 7 | # any system, so any attempt to determine 8 | # installation date is no more than a guess. 9 | # 10 | # It was written for fun and research purposes 11 | # and the only valid use for it is to check 12 | # how old a system running on a dusty server 13 | # in a closet is before shutting it down forever. 14 | 15 | # Copyright (C) 2012 Daniil Baturin 16 | # 17 | # Permission is hereby granted, free of charge, 18 | # to any person obtaining a copy of this software 19 | # and associated documentation files (the "Software"), 20 | # to deal in the Software without restriction, 21 | # including without limitation the rights to use, 22 | # copy, modify, merge, publish, distribute, sublicense, 23 | # and/or sell copies of the Software, and to permit persons 24 | # to whom the Software is furnished to do so, subject 25 | # to the following conditions: 26 | # 27 | # The above copyright notice and this permission notice 28 | # shall be included in all copies or substantial portions of the Software. 29 | # 30 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 31 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 32 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 33 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 34 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 35 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 36 | # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 37 | 38 | if [ -n "$1" ]; then 39 | DATE_FORMAT="$1" 40 | else 41 | DATE_FORMAT="%F" 42 | fi 43 | 44 | if [ `id -u` != 0 ]; then 45 | echo "Some checks require root privileges!" 46 | exit 1 47 | fi 48 | 49 | # Defaults 50 | INSTALL_DATE=Unknown 51 | SOURCE=None 52 | 53 | OS=`uname` 54 | 55 | result() { 56 | echo "Installation date: $INSTALL_DATE" 57 | echo "Information source: $SOURCE" 58 | exit 59 | } 60 | 61 | case "$OS" in 62 | Linux) 63 | ## First try "good", distribution 64 | ## specific methods (installer logs mostly) 65 | 66 | # Debian-based 67 | if [ -d "/var/log/installer" ]; then 68 | RAW_DATE=`stat --format "%y" /var/log/installer/` 69 | INSTALL_DATE=`date -d "$RAW_DATE" +"$DATE_FORMAT"` 70 | SOURCE="Installer log" 71 | result 72 | fi 73 | 74 | # RedHat-based 75 | if [ -f "/root/install.log" ]; then 76 | RAW_DATE=`stat --format "%y" /root/install.log` 77 | INSTALL_DATE=`date -d "$RAW_DATE" +"$DATE_FORMAT"` 78 | SOURCE="Installer log" 79 | result 80 | fi 81 | 82 | ## No luck with good methods. 83 | ## Try to find out root filesystem creation date, 84 | ## most likely it's the same to installation date. 85 | 86 | ROOT_DEV=`mount | grep " / " | awk -F' ' '{print $1}'` 87 | ROOT_OPT=`mount | grep $ROOT_DEV` 88 | ROOT_FS=`echo $ROOT_OPT | awk -F' ' '{print $5}'` 89 | 90 | if [ ! -z `echo $ROOT_FS | grep "ext"` ]; then 91 | # ext2/ext3/ext4 92 | RAW_DATE=`tune2fs -l $ROOT_DEV | grep "Filesystem created" | sed -e 's/Filesystem created:\s\+//'` 93 | INSTALL_DATE=`date -d "$RAW_DATE" +"$DATE_FORMAT"` 94 | SOURCE="Filesystem creation date" 95 | result 96 | elif [ ! -z `echo $ROOT_FS | grep "jfs"` ]; then 97 | # JFS 98 | RAW_DATE=`jfs_tune -l $ROOT_DEV | grep "Filesystem creation" | sed -e 's/Filesystem creation:\s\+//'` 99 | INSTALL_DATE=`date -d "$RAW_DATE" +"$DATE_FORMAT"` 100 | SOURCE="Filesystem creation date" 101 | result 102 | fi 103 | ;; 104 | 105 | FreeBSD) 106 | # Apparently /home -> /usr/home symlink is 107 | # never changed under normal circumstances, 108 | # so should be as old as the system itself 109 | 110 | RAW_DATE=`stat -f%m /home` 111 | INSTALL_DATE=`date -j -f "%s" $RAW_DATE +"%F"` 112 | SOURCE="/home symlink" 113 | result 114 | ;; 115 | 116 | Darwin) 117 | # Mac OS X, actually 118 | 119 | if [ -r "/private/var/db/.AppleSetupDone" ]; then 120 | RAW_DATE=`stat -f%m /private/var/db/.AppleSetupDone` 121 | INSTALL_DATE=`date -j -f "%s" "$RAW_DATE" +"$DATE_FORMAT"` 122 | SOURCE=".AppleSetupDone" 123 | result 124 | fi 125 | ;; 126 | 127 | SunOS) 128 | # SUNWsolnm contains /etc/release and apparently 129 | # is never updated, so its installation date 130 | # should represent system installation date too. 131 | 132 | INSTALL_DATE=`pkginfo -l SUNWsolnm | grep INSTDATE | sed -e 's/\s*INSTDATE:\s*//'` 133 | SOURCE="SUNWsolnm package" 134 | result 135 | ;; 136 | 137 | *) 138 | echo "Sorry, operating system $OS is not supported" 139 | exit 1 140 | esac 141 | -------------------------------------------------------------------------------- /irclogmailer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Mail yesterday irssi logs to watchers 4 | # 5 | # Copyright (c) 2012 Daniil Baturin 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | 25 | ## Configuration, please edit 26 | 27 | # irssi log dir (autolog_path = ) 28 | LOG_DIR="/home/jrandomhacker/.irssi/logs/netname" 29 | 30 | # Log files, space separated (typically $channel.log) 31 | LOG_FILES="#ircjerks.log" 32 | 33 | # Temporary files dir 34 | TMP_DIR=/tmp 35 | 36 | # Watchers' email addresses, space separated 37 | WATCHERS="jrandomhacker@example.org jcertainuser@example.net" 38 | 39 | # Date format should match that in log_close_string 40 | DATE_FORMAT="%b %d %Y" # Jun 4 2012 41 | 42 | 43 | ## The rest, do not edit if not sure 44 | 45 | START=$(date -d yesterday +"$DATE_FORMAT") 46 | END=$(date +"$DATE_FORMAT") 47 | 48 | # Check if mutt is installed 49 | if [ ! -x $(which mutt) ]; then 50 | echo "Error: mutt mail client is not installed!" 51 | exit 1 52 | fi 53 | 54 | for file in "$LOG_FILES"; do 55 | THIS=$LOG_DIR/$file 56 | START_LINE=$(cat -n "$THIS" | grep -e "$START" | awk -F ' ' '{print $1}') 57 | STOP_LINE=$(cat -n "$THIS" | grep -e "$END" | awk -F ' ' '{print $1}') 58 | LINES=$(wc -l "$THIS"|awk -F ' ' '{print $1}') 59 | TMP_FILE=$TMP_DIR/$file-$(date +%d%m) 60 | 61 | # Grab everything between yesterday and today day change line, 62 | # remove service messages (they start with timestamp and "-!-" 63 | tail -n $(($LINES-$START_LINE+1)) "$THIS" | head -n $(($STOP_LINE-$START_LINE+1)) \ 64 | | grep -vPe '^[0-9]{2}:[0-9]{2}-\!-' > $TMP_FILE 65 | 66 | # Remove extension 67 | LOG_NAME=$(echo $file | sed -e s/\\..*//) 68 | 69 | for address in "$WATCHERS"; do 70 | mutt -s "$LOG_NAME IRC log $(date +"%b %d %Y")" $address < $TMP_FILE 71 | done 72 | 73 | rm -f $TMP_FILE 74 | done 75 | 76 | -------------------------------------------------------------------------------- /list-required-firmware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (C) 2020 Daniil Baturin 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License version 2 or later as 7 | # published by the Free Software Foundation. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import re 19 | import os 20 | import sys 21 | import glob 22 | import argparse 23 | import subprocess 24 | 25 | # Loads the kernel config -- only options set to y or m 26 | def load_config(path): 27 | with open(path, 'r') as f: 28 | config = f.read() 29 | targets = re.findall(r'(.*)=(?:y|m)', config) 30 | return targets 31 | 32 | # Finds subdir targets from the Makefile 33 | # that are enabled by the kernel build config 34 | def find_enabled_subdirs(config, makefile_path): 35 | try: 36 | with open(makefile_path, 'r') as f: 37 | makefile = f.read() 38 | except OSError: 39 | # Shouldn't happen due to the way collect_source_files() 40 | # calls this function. 41 | return [] 42 | 43 | dir_stmts = re.findall(r'obj-\$\((.*)\)\s+\+=\s+(.*)/(?:\n|$)', makefile) 44 | subdirs = [] 45 | 46 | for ds in dir_stmts: 47 | config_key, src_dir = ds 48 | 49 | if args.debug: 50 | print("Processing make targets from {0} ({1})".format(ds[1], ds[0]), file=sys.stderr) 51 | if config_key in config: 52 | subdirs.append(src_dir) 53 | elif args.debug: 54 | print("{0} is disabled in the config, ignoring {1}".format(ds[0], ds[1]), file=sys.stderr) 55 | 56 | return subdirs 57 | 58 | # For filtering 59 | def file_loads_firmware(file): 60 | with open(file, 'r') as f: 61 | source = f.read() 62 | if re.search(r'MODULE_FIRMWARE\((.*)\)', source): 63 | return True 64 | 65 | # Find all source files that reference firmware 66 | def collect_source_files(config, path): 67 | files = [] 68 | 69 | makefile = os.path.join(path, "Makefile") 70 | 71 | # Find and process all C files in this directory 72 | # This is a compromise: sometimes there are single-file modules, 73 | # that in fact may be disabled in the config, 74 | # so this approach can create occasional false positives. 75 | c_files = glob.glob("{0}/*.c".format(path)) 76 | files = list(filter(file_loads_firmware, c_files)) 77 | 78 | # Now walk the subdirectories 79 | enabled_subdirs = find_enabled_subdirs(config, makefile) 80 | subdirs = glob.glob("{0}/*/".format(path)) 81 | for d in subdirs: 82 | dir_name = d.rstrip("/") 83 | 84 | if os.path.exists(os.path.join(d, "Makefile")): 85 | # If there's a makefile, it's an independent module 86 | # or a high level dir 87 | if os.path.basename(dir_name) in enabled_subdirs: 88 | files = files + collect_source_files(config, d) 89 | else: 90 | # It's simply a subdirectory of the current module 91 | # Some modules, like iwlwifi, keep their firmware-loading files 92 | # in subdirs, so we have to handle this case 93 | c_files = glob.iglob("{0}/**/*.c".format(d), recursive=True) 94 | files += list(filter(file_loads_firmware, c_files)) 95 | 96 | return files 97 | 98 | if __name__ == '__main__': 99 | parser = argparse.ArgumentParser() 100 | parser.add_argument("-s", "--source-dir", action="append", help="Kernel source directory to process", required=True) 101 | parser.add_argument("-c", "--kernel-config", action="store", help="Kernel configuration") 102 | parser.add_argument("-d", "--debug", action="store_true", help="Enable Debug output") 103 | parser.add_argument("-f", "--list-source-files", action="store_true", help="List source files that reference firmware and exit") 104 | args = parser.parse_args() 105 | 106 | if not args.kernel_config: 107 | args.kernel_config = ".config" 108 | 109 | config = load_config(args.kernel_config) 110 | 111 | # Collect source files that reference firmware 112 | for directory in args.source_dir: 113 | source_files = collect_source_files(config, directory) 114 | 115 | if args.list_source_files: 116 | for sf in source_files: 117 | print(sf) 118 | else: 119 | fw_files = [] 120 | for sf in source_files: 121 | i_file = re.sub(r'\.c', r'.i', sf) 122 | res = subprocess.run(["make {0} 2>&1".format(i_file)], shell=True, capture_output=True) 123 | if res.returncode != 0: 124 | print("Failed to preprocess file {0}".format(sf), file=sys.stderr) 125 | print(res.stdout.decode(), file=sys.stderr) 126 | else: 127 | with open(i_file, 'r') as f: 128 | source = f.read() 129 | fw_statements = re.findall(r'__UNIQUE_ID_firmware.*"firmware"\s+"="\s+(.*);', source) 130 | fw_files += list(map(lambda s: re.sub(r'(\s|")', r'', s), fw_statements)) 131 | 132 | for fw in fw_files: 133 | print(fw) 134 | -------------------------------------------------------------------------------- /maya_timestamp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2013 Daniil Baturin 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import sys 24 | 25 | class MayaDate(object): 26 | """ Converts number of days since UNIX epoch 27 | to the Maya calendar date. 28 | 29 | Ancient Maya people used three independent calendars for 30 | different purposes. 31 | 32 | Long count calendar is for recording historical events. 33 | It and represents the number of days passed 34 | since some date in the past the Maya believed is the day 35 | our world was created. 36 | 37 | Tzolkin calendar is for religious purposes, it has 38 | two independent cycles of 13 and 20 days, where 13 day 39 | cycle days are numbered, and 20 day cycle days are named. 40 | 41 | Haab calendar is for agriculture and daily life, it's a 42 | 365 day calendar with 18 months 20 days each, and 5 43 | nameless days. 44 | 45 | The smallest unit of long count calendar is one day (kin) 46 | 47 | """ 48 | 49 | """ The long count calendar uses five different base 18 or base 20 50 | cycles. Long-count date is writtin in dot separated format 51 | from longest to shortest cycle, 52 | .... 53 | for example, "13.0.0.9.2". 54 | 55 | Classic version actually used by the ancient Maya wraps around 56 | every 13th baktun, but modern researchers often use longer cycles 57 | such as piktun = 20 baktun. 58 | 59 | """ 60 | kin = 1 61 | winal = 20 # 20 kin 62 | tun = 360 # 18 winal 63 | katun = 7200 # 20 tun 64 | baktun = 144000 # 20 katun 65 | 66 | """ Tzolk'in date is composed of two independent cycles. 67 | Dates repeat every 260 days, 13 Ajaw is considered the end 68 | of tzolk'in. 69 | 70 | Every day of the 20 day cycle has unique name, we number 71 | them from zero so it's easier to map remainder to day: 72 | """ 73 | tzolkin_days = { 0: "Imix'", 74 | 1: "Ik'", 75 | 2: "Ak'b'al", 76 | 3: "K'an", 77 | 4: "Chikchan", 78 | 5: "Kimi", 79 | 6: "Manik'", 80 | 7: "Lamat", 81 | 8: "Muluk", 82 | 9: "Ok", 83 | 10: "Chuwen", 84 | 11: "Eb'", 85 | 12: "B'en", 86 | 13: "Ix", 87 | 14: "Men", 88 | 15: "Kib'", 89 | 16: "Kab'an", 90 | 17: "Etz'nab'", 91 | 18: "Kawak", 92 | 19: "Ajaw" } 93 | 94 | """ As said above, haab (year) has 19 months. Only 18 are 95 | true months of 20 days each, the remaining 5 days called "wayeb" 96 | do not really belong to any month, but we think of them as a pseudo-month 97 | for convenience. 98 | 99 | Also, note that days of the month are actually numbered from 0, not from 1, 100 | it's not for technical reasons. 101 | """ 102 | haab_months = { 0: "Pop", 103 | 1: "Wo'", 104 | 2: "Sip", 105 | 3: "Sotz'", 106 | 4: "Sek", 107 | 5: "Xul", 108 | 6: "Yaxk'in'", 109 | 7: "Mol", 110 | 8: "Ch'en", 111 | 9: "Yax", 112 | 10: "Sak'", 113 | 11: "Keh", 114 | 12: "Mak", 115 | 13: "K'ank'in", 116 | 14: "Muwan'", 117 | 15: "Pax", 118 | 16: "K'ayab", 119 | 17: "Kumk'u", 120 | 18: "Wayeb'" } 121 | 122 | """ Now we need to map the beginning of UNIX epoch 123 | (Jan 1 1970 00:00 UTC) to the beginning of the long count 124 | calendar (0.0.0.0.0, 4 Ajaw, 8 Kumk'u). 125 | 126 | The problem with mapping the long count calendar to 127 | any other is that its start date is not known exactly. 128 | 129 | The most widely accepted hypothesis suggests it was 130 | August 11, 3114 BC gregorian date. In this case UNIX epoch 131 | starts on 12.17.16.7.5, 13 Chikchan, 3 K'ank'in 132 | 133 | It's known as Goodman-Martinez-Thompson (GMT) correlation 134 | constant. 135 | """ 136 | start_days = 1856305 137 | 138 | """ Seconds in day, for conversion from timestamp """ 139 | seconds_in_day = 60 * 60 * 24 140 | 141 | def __init__(self, timestamp): 142 | if timestamp is None: 143 | self.days = self.start_days 144 | else: 145 | self.days = self.start_days + (int(timestamp) // self.seconds_in_day) 146 | 147 | def long_count_date(self): 148 | """ Returns long count date string """ 149 | days = self.days 150 | 151 | cur_baktun = days // self.baktun 152 | days = days % self.baktun 153 | 154 | cur_katun = days // self.katun 155 | days = days % self.katun 156 | 157 | cur_tun = days // self.tun 158 | days = days % self.tun 159 | 160 | cur_winal = days // self.winal 161 | days = days % self.winal 162 | 163 | cur_kin = days 164 | 165 | longcount_string = "{0}.{1}.{2}.{3}.{4}".format( cur_baktun, 166 | cur_katun, 167 | cur_tun, 168 | cur_winal, 169 | cur_kin ) 170 | return(longcount_string) 171 | 172 | def tzolkin_date(self): 173 | """ Returns tzolkin date string """ 174 | days = self.days 175 | 176 | """ The start date is not the beginning of both cycles, 177 | it's 4 Ajaw. So we need to add 4 to the 13 days cycle day, 178 | and substract 1 from the 20 day cycle to get correct result. 179 | """ 180 | tzolkin_13 = (days + 4) % 13 181 | tzolkin_20 = (days - 1) % 20 182 | 183 | tzolkin_string = "{0} {1}".format(tzolkin_13, self.tzolkin_days[tzolkin_20]) 184 | 185 | return(tzolkin_string) 186 | 187 | def haab_date(self): 188 | """ Returns haab date string. 189 | 190 | The time start on 8 Kumk'u rather than 0 Pop, which is 191 | 17 days before the new haab, so we need to substract 17 192 | from the current date to get correct result. 193 | """ 194 | days = self.days 195 | 196 | haab_day = (days - 17) % 365 197 | haab_month = haab_day // 20 198 | haab_day_of_month = haab_day % 20 199 | 200 | haab_string = "{0} {1}".format(haab_day_of_month, self.haab_months[haab_month]) 201 | 202 | return(haab_string) 203 | 204 | def date(self): 205 | return("{0}, {1}, {2}".format( self.long_count_date(), self.tzolkin_date(), self.haab_date() )) 206 | 207 | try: 208 | timestamp = sys.argv[1] 209 | except: 210 | print("Please specify timestamp in the argument") 211 | sys.exit(1) 212 | 213 | maya_date = MayaDate(timestamp) 214 | print(maya_date.date()) 215 | 216 | -------------------------------------------------------------------------------- /motto_clock.sh: -------------------------------------------------------------------------------- 1 | # Medieval wall/tower clocks were often decorated 2 | # with latin mottos that encourage to value the time 3 | # and remember one won't live forever 4 | # 5 | # Let's implement it in a modern environment 6 | function clock() 7 | { 8 | 9 | mottos=( 10 | "Vulnerant omnes, ultima necat" # "Each [hour] hurts, the last one kills" 11 | "Ultima forsan" # "Perhaps the last [hour]" 12 | "Memento mori" # "Remember you are mortal" 13 | "Tempus fugit" # "Time flies" 14 | "Vita brevis" # "Life is short" 15 | ) 16 | 17 | date "${@}" 18 | echo ${mottos[$(($RANDOM%${#mottos[@]}))]} 19 | } 20 | 21 | clock 22 | -------------------------------------------------------------------------------- /nproc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Retrieves (or at least attempts to retrieve) the total number of real CPU cores 4 | # installed in a Linux system. 5 | # 6 | # The issue of core count is complicated by existence of SMT, e.g. Intel's Hyper Threading. 7 | # GNU nproc returns the number of LOGICAL cores, 8 | # which is 2x of the real cores if SMT is enabled. 9 | # 10 | # The idea is to find all physical CPUs and add up their core counts. 11 | # It has special cases for x86_64 and MAY work correctly on other architectures, 12 | # but nothing is certain. 13 | # 14 | # Copyright (c) 2022 Daniil Baturin 15 | # 16 | # Permission is hereby granted, free of charge, to any person obtaining a copy 17 | # of this software and associated documentation files (the "Software"), to deal 18 | # in the Software without restriction, including without limitation the rights 19 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | # copies of the Software, and to permit persons to whom the Software is 21 | # furnished to do so, subject to the following conditions: 22 | # 23 | # The above copyright notice and this permission notice shall be included in 24 | # all copies or substantial portions of the Software. 25 | # 26 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 32 | # THE SOFTWARE. 33 | 34 | 35 | import re 36 | 37 | 38 | def read_cpuinfo(): 39 | with open('/proc/cpuinfo', 'r') as f: 40 | return f.readlines() 41 | 42 | def split_line(l): 43 | l = l.strip() 44 | parts = re.split(r'\s*:\s*', l) 45 | return (parts[0], ":".join(parts[1:])) 46 | 47 | def find_cpus(cpuinfo_lines): 48 | cpus = {} 49 | 50 | cpu_number = 0 51 | 52 | for l in cpuinfo_lines: 53 | key, value = split_line(l) 54 | if key == 'processor': 55 | cpu_number = value 56 | cpus[cpu_number] = {} 57 | else: 58 | cpus[cpu_number][key] = value 59 | 60 | return cpus 61 | 62 | def find_physical_cpus(cpus): 63 | phys_cpus = {} 64 | 65 | for num in cpus: 66 | if 'physical id' in cpus[num]: 67 | # On at least some architectures, CPUs in different sockets 68 | # have different 'physical id' field, e.g. on x86_64. 69 | phys_id = cpus[num]['physical id'] 70 | if phys_id not in phys_cpus: 71 | phys_cpus[phys_id] = cpus[num] 72 | else: 73 | # On other architectures, e.g. on ARM, there's no such field. 74 | # We just assume they are different CPUs, 75 | # whether single core ones or cores of physical CPUs. 76 | phys_cpus[num] = cpu[num] 77 | 78 | return phys_cpus 79 | 80 | 81 | if __name__ == '__main__': 82 | physical_cpus = find_physical_cpus(find_cpus(read_cpuinfo())) 83 | 84 | core_count = 0 85 | 86 | for num in physical_cpus: 87 | # Some architectures, e.g. x86_64, include a field for core count. 88 | # Since we found unique physical CPU entries, we can sum their core counts. 89 | if 'cpu cores' in physical_cpus[num]: 90 | core_count += int(physical_cpus[num]['cpu cores']) 91 | else: 92 | core_count += 1 93 | 94 | print(core_count) 95 | -------------------------------------------------------------------------------- /ocacc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | 6 | if len(sys.argv) < 3: 7 | print("Usage: {0}