From 4379c3279422aff3a5ed97c0b5611c5949105083 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 17 Feb 2021 08:13:06 +0800 Subject: [PATCH 1/3] add copy function, more control flags --- README.md | 31 +++-- reforg | 310 +++++++++++++++++++++++++++++++++++++++++ remap-demail-classifer | 115 --------------- 3 files changed, 329 insertions(+), 127 deletions(-) create mode 100755 reforg delete mode 100755 remap-demail-classifer diff --git a/README.md b/README.md index d0e2358..9fbda85 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -# REMAP DEMAIL File Classifier +# reforg - Organize Files Based on Regular Expressions -`remap-demail-classifer` is a command line application written in -Python3. It classifies fetched DEMAIL attachment files using a regex -specification. +`reforg` is a command line application written in Python(3). It reorganizes +files under given directories based on a set of regex-powered rules. There are no specific requirements for the application to run other than `>= Python3.6`. @@ -10,14 +9,22 @@ than `>= Python3.6`. CLI arguments are as follows: ``` -./remap-demail-classifer -``` +$ ./reforg --help +usage: reforg [-h] --spec SPEC [--ignore IGNORE] [--prefix PREFIX] --root ROOT + [--dry-run] [--metadata] [--force] + DIRS [DIRS ...] -Example: +positional arguments: + DIRS -``` -./remap-demail-classifer spec.csv \ - '^(?P[0-9]{4}\-[0-9]{2}\-[0-9]{2})T[0-9]{2}:[0-9]{2}:[0-9]{2}Z_[A-Z0-9]{32}_[A-Z0-9]{32}_' \ - tmp/ignore.dat \ - /data/remap/tenants/deployment/demail/downloaded +optional arguments: + -h, --help show this help message and exit + --spec SPEC Specification file + --ignore IGNORE Blacklist file (names of files to ignore) + --prefix PREFIX Regular expression prefix for all patterns in the + specification file + --root ROOT Root directory to copy renamed files to + --dry-run Dry run + --metadata Preserve metadata + --force Force copy even if the target exists ``` diff --git a/reforg b/reforg new file mode 100755 index 0000000..53da7bd --- /dev/null +++ b/reforg @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import re +import shutil +import sys +from enum import Enum, auto +from pathlib import Path +from typing import Iterable, List, NamedTuple, Optional, Pattern, Set, TextIO + +#: Application version. +__version__ = "0.0.1.dev0" + + +###################### +## DATA DEFINITIONS ## +###################### + + +class Config(NamedTuple): + """ + Program configuration. + """ + + #: Specifications. + specs: List["Spec"] + + #: Ignored files. + ignores: Set[str] + + #: Root directory. + root: Path + + #: Preserve metadata? + metadata: bool + + #: Force overwrite? + force: bool + + #: Dry-run: + dryrun: bool + + #: Directories to traverse: + dirs: List[Path] + + #: Indicates if we are on a terminal: + terminal: bool + + +class Spec(NamedTuple): + """ + Pattern-Template specification. + """ + + #: Original filename pattern. + pattern: Pattern + + #: Target filename template. + template: str + + #: Sub directory for the target file. + subdir: Path + + +class TColor: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +class Status(Enum): + """ + Indicates the copy status. + """ + + DRYRUN = auto() + EXISTS = auto() + COPIED = auto() + FORCED = auto() + + +################################## +## CLI ARGUMENTS PARSER HELPERS ## +################################## + + +def existingdir(x: str) -> Path: + """ + Ensures an existing directory. + """ + path = Path(x) + + if not (path.exists() and path.is_dir()): + raise ValueError(f"{x} is not an existing directory.") + + return path + + +def compile_specs(cfile: TextIO, prefix: str) -> List[Spec]: + """ + Reads given file and compiles specs. + """ + return [ + Spec(re.compile(prefix + r["pattern"]), r["template"], Path(r["directory"])) + for r in csv.DictReader(cfile) + ] + + +def parse_cli_arguments() -> Config: + """ + Parses and return arguments. + """ + ## Define the parser: + parser = argparse.ArgumentParser(prog="reforg") + + ## Add specification file: + parser.add_argument( + "--spec", required=True, type=argparse.FileType("r"), help="Specification file" + ) + + ## Add blacklist file: + parser.add_argument( + "--ignore", + required=False, + type=argparse.FileType("r"), + help="Blacklist file (names of files to ignore)", + ) + + ## Add regex prefix: + parser.add_argument( + "--prefix", + required=False, + help="Regular expression prefix for all patterns in the specification file", + ) + + ## Add root firectory to clone files to: + parser.add_argument( + "--root", + required=True, + type=existingdir, + help="Root directory to copy renamed files to", + ) + + ## Add dry-run: + parser.add_argument("--dry-run", action="store_true", help="Dry run") + + ## Add flag to preserve metadata: + parser.add_argument("--metadata", action="store_true", help="Preserve metadata") + + ## Add flag to force copy: + parser.add_argument("--force", action="store_true", help="Force copy even if the target exists") + + ## Add directories to traverse + parser.add_argument("dirs", metavar="DIRS", type=existingdir, nargs="+") + + ## Parse arguments: + args = parser.parse_args() + + ## Compile specifications: + specs = compile_specs(args.spec, args.prefix or "") + + ## Compile ignores set: + ignores = args.ignore and set(i.strip() for i in args.ignore.readlines()) or set([]) + + ## Build config and return: + return Config( + specs=specs, + ignores=ignores, + root=args.root, + force=args.force, + metadata=args.metadata, + dryrun=args.dry_run, + dirs=args.dirs, + terminal=sys.stdout.isatty(), + ) + + +############### +## WORKHORSE ## +############### + + +def classify(parent: Path, specs: List[Spec], path: Path) -> Optional[Path]: + """ + Attempts to classify the given path as per given patterns. + """ + ## Get and trim filename: + filename = path.name.strip() + + ## Iterate over specs: + for spec in specs: + ## Attempt to match: + match = spec.pattern.match(filename) + + ## If we have a match, get groups, skip otherwise: + if match: + ## Yep, we have a match: + groups = match.groupdict() + + ## Attempt to compile the target filename: + try: + newfilename = spec.template.format(**groups) + except KeyError as err: + print(err) + print(path) + + ## Build and return the target path: + return parent / spec.subdir / newfilename + + ## We do not have any match, return None: + return None + + +def traverse(config: Config) -> Iterable[Path]: + """ + Traverses directories and yields paths of regular files. + """ + for pdir in config.dirs: + for path in (i for i in pdir.iterdir() if i.is_file()): + if path.name in config.ignores: + continue + yield path + + +def print_term(orig: Path, copy: Path, status: Status) -> None: + """ + Print operation on a terminal. + """ + print(f"{TColor.WARNING}{status.name} {TColor.OKBLUE}{orig}{TColor.ENDC} -> {TColor.OKGREEN}{copy}{TColor.ENDC}") + + +def print_nonterm(orig: Path, copy: Path, status: Status) -> None: + """ + Print operation on a non-terminal. + """ + print(f"{status.name} {orig} -> {copy}") + + +def copier(config: Config, orig: Path, copy: Path) -> None: + """ + Attempts to copy the file. + """ + if config.dryrun: + status = Status.DRYRUN + else: + ## Get the copier function: + copier = shutil.copy2 if config.metadata else shutil.copy + + ## Get the target parent directory: + parent = copy.parent + + ## Create parent directory if it does not exist: + parent.mkdir(parents=True, exist_ok=True) + + ## Check whether target exists, and copy accordingly: + if copy.exists() and config.force: + copier(str(orig), str(copy)) + status = Status.FORCED + elif copy.exists(): + status = Status.EXISTS + else: + copier(str(orig), str(copy)) + status = Status.COPIED + + ## Log it: + if config.terminal: + print_term(orig, copy, status) + else: + print_nonterm(orig, copy, status) + + +################ +## ENTRYPOINT ## +################ + + +def main() -> None: + """ + Entrypoint. + """ + ## Parse config: + config = parse_cli_arguments() + + ## Print function: + printer = print_term if config.terminal else print_nonterm + + ## Copy function: + + ## Attempt to classify each path: + for path in traverse(config): + ## Attempt: + attempt = classify(config.root, config.specs, path) + + ## Success? + if attempt is None: + raise Exception(f"Path could not be classified. Filename: {path.name}") + + ## Copy: + copier(config, path, attempt) + + +if __name__ == "__main__": + main() diff --git a/remap-demail-classifer b/remap-demail-classifer deleted file mode 100755 index 8802a8e..0000000 --- a/remap-demail-classifer +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 - -import csv -import re -import sys -from pathlib import Path -from typing import Iterable, List, NamedTuple, Optional, Pattern - - -#: Application version. -__version__ = "0.0.1.dev0" - - -class Spec(NamedTuple): - """ - Pattern-Template specification. - """ - - #: Original filename pattern. - pattern: Pattern - - #: Target filename template. - template: str - - #: Sub directory for the target file. - subdir: Path - - -def classify(parent: Path, specs: List[Spec], path: Path) -> Optional[Path]: - """ - Attempts to classify the given path as per given patterns. - """ - ## Get and trim filename: - filename = path.name.strip() - - ## Iterate over specs: - for spec in specs: - ## Attempt to match: - match = spec.pattern.match(filename) - - ## If we have a match, get groups, skip otherwise: - if match: - ## Yep, we have a match: - groups = match.groupdict() - - ## Attempt to compile the target filename: - try: - newfilename = spec.template.format(**groups) - except KeyError as err: - print(err) - print(path) - - ## Build and return the target path: - return parent / spec.subdir / newfilename - - ## We do not have any match, return None: - return None - - -def compile_specs(path: Path, prefix: str) -> List[Spec]: - """ - Reads given file and compiles specs. - """ - ## Open the file: - with path.open() as cfile: - ## Read the CSV, compile patterns and build specs: - specs = [ - Spec(re.compile(prefix + r["pattern"]), r["template"], Path(r["directory"])) - for r in csv.DictReader(cfile) - ] - - ## Return specs: - return specs - - -def main( - specfile: Path, prefix: str, ignorepath: Path, parent: Path, paths: Iterable[Path] -) -> None: - """ - Entrypoint. - """ - ## Read in the specifications: - specs = compile_specs(specfile, prefix) - - ## Read in ignored files: - ignores = set( - i for i in (i.strip() for i in ignorepath.read_text().split("\n")) if i - ) - - ## Attempt to classify each path: - for path in paths: - ## Check if the path is ignored: - if path.name in ignores: - print(f"IGNORE: {path}") - continue - - ## Attempt: - attempt = classify(parent, specs, path) - - ## Success? - if attempt is None: - raise Exception(f"Path could not be classified. Filename: {path.name}") - - ## Print: - print(f"{path} -> {attempt}") - - -if __name__ == "__main__": - main( - Path(sys.argv[1]), - sys.argv[2], - Path(sys.argv[3]), - Path("/files/"), - (i for i in Path(sys.argv[4]).iterdir() if i.is_file()), - ) From a5d5cd76e5c267b7a0e63797f39d8cbef18d3052 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 17 Feb 2021 09:28:42 +0800 Subject: [PATCH 2/3] feat: use JSON as specification, add example, refactor --- README.md | 60 +++++++++--- example/source/2020-12-31_A001.txt | 0 example/source/2020-12-31_A002.txt | 0 example/source/2020-12-31_A012.txt | 0 example/source/2021-01-04_B003.dat | 0 example/source/2021-01-05_C004.dat | 0 example/source/zartzurt_ignore.txt | 0 example/spec.json | 20 ++++ example/spec.yaml | 12 +++ example/target/.gitignore | 2 + reforg | 145 ++++++++++++++++------------- 11 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 example/source/2020-12-31_A001.txt create mode 100644 example/source/2020-12-31_A002.txt create mode 100644 example/source/2020-12-31_A012.txt create mode 100644 example/source/2021-01-04_B003.dat create mode 100644 example/source/2021-01-05_C004.dat create mode 100644 example/source/zartzurt_ignore.txt create mode 100644 example/spec.json create mode 100644 example/spec.yaml create mode 100644 example/target/.gitignore diff --git a/README.md b/README.md index 9fbda85..f7153b0 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,55 @@ CLI arguments are as follows: ``` $ ./reforg --help -usage: reforg [-h] --spec SPEC [--ignore IGNORE] [--prefix PREFIX] --root ROOT - [--dry-run] [--metadata] [--force] - DIRS [DIRS ...] +usage: reforg [-h] --spec SPEC-FILE --root ROOT-DIR [--dry-run] [--metadata] + [--force] + DIR [DIR ...] + +Organize files based on regular expressions positional arguments: - DIRS + DIR Directory to list files of to process. Note that special + entries in the directory and sub-directories are not + listed and therefore not processed. optional arguments: - -h, --help show this help message and exit - --spec SPEC Specification file - --ignore IGNORE Blacklist file (names of files to ignore) - --prefix PREFIX Regular expression prefix for all patterns in the - specification file - --root ROOT Root directory to copy renamed files to - --dry-run Dry run - --metadata Preserve metadata - --force Force copy even if the target exists + -h, --help show this help message and exit + --spec SPEC-FILE Specification file + --root ROOT-DIR Root directory to copy processed files to + --dry-run Dry run (do not copy anything) + --metadata Preserve metadata while copying + --force Force copy if target exists (overwrite existing files) + +reforg -- v0.0.1.dev0 +``` + +Example: + +``` +./reforg --spec example/spec.json --root example/target/ --metadata --force --dry-run example/source/ +``` + +## Specification Format + +See [./example/spec.json](./example/spec.json) for an example. + +Note that we are using JSON as specification file format. A much better file +format would be YAML (or maybe even TOML). However, we want to stick to the idea +of external *no-dependencies* for easier deployment. We may wish to change that +in the future. + +For convenience, you may wish to write the specification in YAML (as in +[./example/spec.yaml](./example/spec.yaml)) and then convert to JSON: + ``` +cd example/ +yq . < spec.yaml > spec.json +``` + +... or pipe converted JSON directly to the command (note the `--spec -` +argument): + +``` +yq . < example/spec.yaml | ./reforg --spec - --root example/target/ --metadata --force --dry-run example/source/ +``` + diff --git a/example/source/2020-12-31_A001.txt b/example/source/2020-12-31_A001.txt new file mode 100644 index 0000000..e69de29 diff --git a/example/source/2020-12-31_A002.txt b/example/source/2020-12-31_A002.txt new file mode 100644 index 0000000..e69de29 diff --git a/example/source/2020-12-31_A012.txt b/example/source/2020-12-31_A012.txt new file mode 100644 index 0000000..e69de29 diff --git a/example/source/2021-01-04_B003.dat b/example/source/2021-01-04_B003.dat new file mode 100644 index 0000000..e69de29 diff --git a/example/source/2021-01-05_C004.dat b/example/source/2021-01-05_C004.dat new file mode 100644 index 0000000..e69de29 diff --git a/example/source/zartzurt_ignore.txt b/example/source/zartzurt_ignore.txt new file mode 100644 index 0000000..e69de29 diff --git a/example/spec.json b/example/spec.json new file mode 100644 index 0000000..a1db314 --- /dev/null +++ b/example/spec.json @@ -0,0 +1,20 @@ +{ + "prefix": "^(?P[0-9]{4}\\-[0-9]{2}\\-[0-9]{2})_", + "rules": [ + { + "regex": "A(?P[0-9]+).txt$", + "filename": "{recdate}_{id}.txt", + "directory": "./txt/A" + }, + { + "regex": "(?P[B-Z])(?P[0-9]+).dat$", + "filename": "{recdate}_{id}.dat", + "directory": "./dat/{code}" + } + ], + "ignore": [ + "^.*ignore.*$", + "A012.txt", + "Z013.dat" + ] +} diff --git a/example/spec.yaml b/example/spec.yaml new file mode 100644 index 0000000..01e82bb --- /dev/null +++ b/example/spec.yaml @@ -0,0 +1,12 @@ +prefix: ^(?P[0-9]{4}\-[0-9]{2}\-[0-9]{2})_ +rules: + - regex: A(?P[0-9]+).txt$ + filename: "{recdate}_{id}.txt" + directory: ./txt/A + - regex: (?P[B-Z])(?P[0-9]+).dat$ + filename: "{recdate}_{id}.dat" + directory: ./dat/{code} +ignore: + - ^.*ignore.*$ + - A012.txt + - Z013.dat diff --git a/example/target/.gitignore b/example/target/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/example/target/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/reforg b/reforg index 53da7bd..ecbafd6 100755 --- a/reforg +++ b/reforg @@ -1,13 +1,13 @@ #!/usr/bin/env python3 import argparse -import csv +import json import re import shutil import sys from enum import Enum, auto from pathlib import Path -from typing import Iterable, List, NamedTuple, Optional, Pattern, Set, TextIO +from typing import Dict, Iterable, List, NamedTuple, Optional, Pattern #: Application version. __version__ = "0.0.1.dev0" @@ -23,13 +23,13 @@ class Config(NamedTuple): Program configuration. """ - #: Specifications. - specs: List["Spec"] + #: Copy rules. + rules: List["Rule"] - #: Ignored files. - ignores: Set[str] + #: Ignore patterns. + ignores: List[Pattern[str]] - #: Root directory. + #: Root directory to copy files to. root: Path #: Preserve metadata? @@ -38,32 +38,36 @@ class Config(NamedTuple): #: Force overwrite? force: bool - #: Dry-run: + #: Dry run? dryrun: bool + #: Are we on a terminal? + terminal: bool + #: Directories to traverse: dirs: List[Path] - #: Indicates if we are on a terminal: - terminal: bool - -class Spec(NamedTuple): +class Rule(NamedTuple): """ - Pattern-Template specification. + Copy rule. """ #: Original filename pattern. - pattern: Pattern + regex: Pattern[str] #: Target filename template. template: str #: Sub directory for the target file. - subdir: Path + directory: str class TColor: + """ + Terminal colors. + """ + HEADER = "\033[95m" OKBLUE = "\033[94m" OKCYAN = "\033[96m" @@ -77,7 +81,7 @@ class TColor: class Status(Enum): """ - Indicates the copy status. + Copy status indication. """ DRYRUN = auto() @@ -103,13 +107,13 @@ def existingdir(x: str) -> Path: return path -def compile_specs(cfile: TextIO, prefix: str) -> List[Spec]: +def compile_rules(rules: List[Dict[str, str]], prefix: str) -> List[Rule]: """ - Reads given file and compiles specs. + Compiles copy rules. """ return [ - Spec(re.compile(prefix + r["pattern"]), r["template"], Path(r["directory"])) - for r in csv.DictReader(cfile) + Rule(re.compile(prefix + r["regex"]), r["filename"], r["directory"]) + for r in rules ] @@ -118,61 +122,70 @@ def parse_cli_arguments() -> Config: Parses and return arguments. """ ## Define the parser: - parser = argparse.ArgumentParser(prog="reforg") - - ## Add specification file: - parser.add_argument( - "--spec", required=True, type=argparse.FileType("r"), help="Specification file" + parser = argparse.ArgumentParser( + prog=f"reforg", + description="Organize files based on regular expressions", + epilog=f"reforg -- v{__version__}", ) - ## Add blacklist file: + ## Add specification file: parser.add_argument( - "--ignore", - required=False, + "--spec", + metavar="SPEC-FILE", + required=True, type=argparse.FileType("r"), - help="Blacklist file (names of files to ignore)", - ) - - ## Add regex prefix: - parser.add_argument( - "--prefix", - required=False, - help="Regular expression prefix for all patterns in the specification file", + help="Specification file", ) ## Add root firectory to clone files to: parser.add_argument( "--root", + metavar="ROOT-DIR", required=True, type=existingdir, - help="Root directory to copy renamed files to", + help="Root directory to copy processed files to", ) ## Add dry-run: - parser.add_argument("--dry-run", action="store_true", help="Dry run") + parser.add_argument( + "--dry-run", action="store_true", help="Dry run (do not copy anything)" + ) ## Add flag to preserve metadata: - parser.add_argument("--metadata", action="store_true", help="Preserve metadata") + parser.add_argument( + "--metadata", action="store_true", help="Preserve metadata while copying" + ) ## Add flag to force copy: - parser.add_argument("--force", action="store_true", help="Force copy even if the target exists") + parser.add_argument( + "--force", + action="store_true", + help="Force copy if target exists (overwrite existing files)", + ) ## Add directories to traverse - parser.add_argument("dirs", metavar="DIRS", type=existingdir, nargs="+") + parser.add_argument( + "dirs", + metavar="DIR", + type=existingdir, + nargs="+", + help=( + "Directory to list files of to process. " + "Note that special entries in the directory and sub-directories " + "are not listed and therefore not processed." + ), + ) ## Parse arguments: args = parser.parse_args() - ## Compile specifications: - specs = compile_specs(args.spec, args.prefix or "") - - ## Compile ignores set: - ignores = args.ignore and set(i.strip() for i in args.ignore.readlines()) or set([]) + ## Load the spec file: + spec = json.load(args.spec) ## Build config and return: return Config( - specs=specs, - ignores=ignores, + rules=compile_rules(spec.get("rules", []), spec.get("prefix", "")), + ignores=[re.compile(i) for i in spec.get("ignore", [])], root=args.root, force=args.force, metadata=args.metadata, @@ -187,17 +200,17 @@ def parse_cli_arguments() -> Config: ############### -def classify(parent: Path, specs: List[Spec], path: Path) -> Optional[Path]: +def classify(config: Config, path: Path) -> Optional[Path]: """ Attempts to classify the given path as per given patterns. """ ## Get and trim filename: - filename = path.name.strip() + filename = path.name - ## Iterate over specs: - for spec in specs: + ## Iterate over rules: + for rule in config.rules: ## Attempt to match: - match = spec.pattern.match(filename) + match = rule.regex.match(filename) ## If we have a match, get groups, skip otherwise: if match: @@ -206,25 +219,34 @@ def classify(parent: Path, specs: List[Spec], path: Path) -> Optional[Path]: ## Attempt to compile the target filename: try: - newfilename = spec.template.format(**groups) + newfilename = rule.template.format(**groups) + directory = Path(rule.directory.format(**groups)) except KeyError as err: print(err) print(path) + raise err ## Build and return the target path: - return parent / spec.subdir / newfilename + return config.root / directory / newfilename ## We do not have any match, return None: return None +def is_ignored(ignores: List[Pattern[str]], filename: str) -> bool: + for ignore in ignores: + if ignore.match(filename): + return True + return False + + def traverse(config: Config) -> Iterable[Path]: """ Traverses directories and yields paths of regular files. """ for pdir in config.dirs: for path in (i for i in pdir.iterdir() if i.is_file()): - if path.name in config.ignores: + if is_ignored(config.ignores, path.name): continue yield path @@ -233,7 +255,9 @@ def print_term(orig: Path, copy: Path, status: Status) -> None: """ Print operation on a terminal. """ - print(f"{TColor.WARNING}{status.name} {TColor.OKBLUE}{orig}{TColor.ENDC} -> {TColor.OKGREEN}{copy}{TColor.ENDC}") + print( + f"{TColor.WARNING}{status.name} {TColor.OKBLUE}{orig}{TColor.ENDC} -> {TColor.OKGREEN}{copy}{TColor.ENDC}" + ) def print_nonterm(orig: Path, copy: Path, status: Status) -> None: @@ -288,15 +312,10 @@ def main() -> None: ## Parse config: config = parse_cli_arguments() - ## Print function: - printer = print_term if config.terminal else print_nonterm - - ## Copy function: - ## Attempt to classify each path: for path in traverse(config): ## Attempt: - attempt = classify(config.root, config.specs, path) + attempt = classify(config, path) ## Success? if attempt is None: From a59935fefda259b04860ce51d619679298216fc7 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 17 Feb 2021 10:47:35 +0800 Subject: [PATCH 3/3] chore: add installation script, update README --- README.md | 9 ++++++++- install.sh | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100755 install.sh diff --git a/README.md b/README.md index f7153b0..2999ca9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ files under given directories based on a set of regex-powered rules. There are no specific requirements for the application to run other than `>= Python3.6`. +## Installation + +``` +curl -o - https://raw.githubusercontent.com/telostat/reforg/main/reforg/install.sh | sudo sh -x +``` + +## Usage + CLI arguments are as follows: ``` @@ -61,4 +69,3 @@ argument): ``` yq . < example/spec.yaml | ./reforg --spec - --root example/target/ --metadata --force --dry-run example/source/ ``` - diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..967d297 --- /dev/null +++ b/install.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh + +set -e + +## Target installation directory: +REFORG_INSTALL_DIR="${REFORG_INSTALL_DIR:-"/usr/local/bin"}" + +## Download the script: +curl -s -o - https://raw.githubusercontent.com/telostat/reforg/main/reforg > "${REFORG_INSTALL_DIR}/reforg" + +## Change file permissions: +chmod +x "${REFORG_INSTALL_DIR}/reforg" + +## Run: +reforg --help