diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6125df5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.idea/ +target/ +.git/ +.gitignore +.dockerignore +Dockerfile +examples/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ba2c16c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + release: + types: [ created ] + +jobs: + build: + name: Build + runs-on: ubuntu-20.04 + + steps: + - name: Get git tag + id: git_info + if: startsWith(github.ref, 'refs/tags/') + run: echo "::set-output name=tag::${GITHUB_REF##*/}" + - name: Get project info + id: determine + env: + git_tag: ${{ steps.git_info.outputs.tag }} + run: | + repo="${GITHUB_REPOSITORY,,}" # to lower case + # if build triggered by tag, use tag name + tag="${git_tag:-latest}" + + # if tag is a version number prefixed by 'v', remove the 'v' + if [[ "$tag" =~ ^v[0-9].* ]]; then + tag="${tag:1}" + fi + + dock_image=$repo:$tag + echo $dock_image + echo "::set-output name=dock_image::$dock_image" + echo "::set-output name=repo::$repo" + + - uses: actions/checkout@v2 + - uses: docker/setup-buildx-action@v1 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to DockerHub + id: dockerhub_login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + id: docker_build + with: + context: . + file: ./Dockerfile + tags: | + docker.io/${{ steps.determine.outputs.dock_image }} + ghcr.io/${{ steps.determine.outputs.dock_image }} + push: true + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Update DockerHub description + uses: peter-evans/dockerhub-description@v2 + continue-on-error: true # it is not crucial that this works + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + short-description: ${{ steps.pluginmeta.outputs.title }} + readme-filepath: ./README.md + repository: ${{ steps.determine.outputs.repo }} + + - name: Upload to ChRIS Store + if: github.event_name == 'release' + uses: FNNDSC/chrisstore-action@master + with: + descriptor_file: chris_plugin_info.json + auth: ${{ secrets.CHRIS_STORE_USER }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32aa5bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea/ + +### Rust template +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3b4a591 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,311 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bulkrename" +version = "0.1.0" +dependencies = [ + "ansi_term", + "anyhow", + "clap", + "fs_extra", + "lazy_static", + "regex", + "walkdir", +] + +[[package]] +name = "clap" +version = "3.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f63420d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bulkrename" +version = "0.1.0" +edition = "2021" + +readme = "README.md" +description = "Bulk rename ChRIS ds plugin" +repository = "https://github.com/FNNDSC/pl-bulk-rename" +license = "MIT" +categories = ["command-line-utilities"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "3.1.6", features = ["derive"] } +fs_extra = "1.2.0" +regex = "1.5.5" +ansi_term = "0.12" +walkdir = "2.3.2" +anyhow = "1.0.56" +lazy_static = "1.4.0" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..efa8eeb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM rust:1.59-slim-bullseye as builder +WORKDIR /usr/local/src/bulkrename +COPY . . +RUN cargo build --release +RUN strip target/release/bulkrename + +FROM debian:bullseye-slim +COPY --from=builder /usr/local/src/bulkrename/target/release/bulkrename /usr/local/bin/bulkrename +COPY /docker-entrypoint.sh /docker-entrypoint.sh +COPY chris_plugin_info.json /chris_plugin_info.json +CMD ["bulkrename"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..4278d89 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Bulk Rename + +[![Version](https://img.shields.io/docker/v/fnndsc/pl-bulk-rename?sort=semver)](https://hub.docker.com/r/fnndsc/pl-bulk-rename) +[![MIT License](https://img.shields.io/github/license/fnndsc/pl-bulk-rename)](https://github.com/FNNDSC/pl-bulk-rename/blob/main/LICENSE) +[![ci](https://github.com/FNNDSC/pl-bulk-rename/actions/workflows/ci.yml/badge.svg)](https://github.com/FNNDSC/pl-bulk-rename/actions/workflows/ci.yml) + +`pl-bulk-rename` is a [_ChRIS_](https://chrisproject.org/) +_ds_ plugin which copies files from an input directory to an +output directory under different names using regular expressions. + +## Installation + +`pl-bulk-rename` is a _[ChRIS](https://chrisproject.org/) plugin_, meaning it can +run from either within _ChRIS_ or the command-line. + +[![Get it from chrisstore.co](https://ipfs.babymri.org/ipfs/QmaQM9dUAYFjLVn3PpNTrpbKVavvSTxNLE5BocRCW1UoXG/light.png)](https://chrisstore.co/plugin/pl-bulk-rename) + +## Usage + +Regular expression syntax is based on the [regex](https://crates.io/crates/regex) crate. +See https://docs.rs/regex/1.5.5/regex/#grouping-and-flags + +### Local Usage + +To get started with local command-line usage, use [Apptainer](https://apptainer.org/) +(a.k.a. Singularity) to run `pl-bulk-rename` as a container. + +To print its available options, run: + +```shell +singularity exec docker://fnndsc/pl-bulk-rename bulkrename --help +``` + +## Examples + +`bulkrename` copies data from an input directory to an output directory. + +Consider the data in [`examples/input`](examples/input): + +``` +examples/input +├── a +│ ├── food.txt +│ └── log +├── b +│ ├── food.txt +│ └── log +└── c + ├── food.txt + └── log +``` + +To rename every `.txt` file to have the name of their parent directory: + +```shell +bulkrename --filter '.*\.txt' \ + --expression '^(.*?)/(.*?)\.txt$' \ + --replace '$1.txt' \ + examples/input examples/filewise +``` + +To rename the subdirectories `a`, `b`, `c`, of the input directory to have a prefix `pear_`: + +```shell +bulkrename --filter '^[abc]$' \ + --expression '([abc])' \ + --replace 'pear_$1' \ + examples/input examples/dirwise +``` diff --git a/chris_plugin_info.json b/chris_plugin_info.json new file mode 100644 index 0000000..01f53c5 --- /dev/null +++ b/chris_plugin_info.json @@ -0,0 +1,57 @@ +{ + "type": "ds", + "parameters": [ + { + "name": "filter", + "type": "str", + "optional": true, + "flag": "--filter", + "short_flag": "-f", + "action": "store", + "help": "Input path filter. Paths which do not match this regex are excluded.", + "default": ".*", + "ui_exposed": true + }, + { + "name": "expression", + "type": "str", + "optional": true, + "flag": "--expression", + "short_flag": "-e", + "action": "store", + "help": "Regular expression to match paths. See https://docs.rs/regex/1.5.5/regex/#syntax", + "default": "(.*)", + "ui_exposed": true + }, + { + "name": "replacement", + "type": "str", + "optional": true, + "flag": "--replace", + "short_flag": "-r", + "action": "store", + "help": "Replacement string with capture groups. See https://docs.rs/regex/1.5.5/regex/#grouping-and-flags", + "default": "\\1", + "ui_exposed": true + } + ], + "icon": "", + "authors": "FNNDSC ", + "title": "Bulk Rename", + "category": "Utility", + "description": "A ChRIS plugin to rename paths using regex", + "documentation": "https://github.com/FNNDSC/pl-bulk-rename", + "license": "MIT", + "version": "0.1.0", + "selfpath": "/usr/local/bin", + "selfexec": "bulkrename", + "execshell": "/docker-entrypoint.sh", + "min_number_of_workers": 1, + "max_number_of_workers": 1, + "min_memory_limit": "", + "max_memory_limit": "", + "min_cpu_limit": "", + "max_cpu_limit": "", + "min_gpu_limit": 0, + "max_gpu_limit": 0 +} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..d0747ea --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# CUBE **requires** all of selfpath, selfexec, and execshell to be defined, +# which doesn't make sense for binary applications like this one. +# `docker-entrypoint.sh` is a script which transparently executes its +# arguments as a command. + +exec "$@" diff --git a/examples/dirwise/pear_a/food.txt b/examples/dirwise/pear_a/food.txt new file mode 100644 index 0000000..4c479de --- /dev/null +++ b/examples/dirwise/pear_a/food.txt @@ -0,0 +1 @@ +apple diff --git a/examples/dirwise/pear_a/log b/examples/dirwise/pear_a/log new file mode 100644 index 0000000..eb7de92 --- /dev/null +++ b/examples/dirwise/pear_a/log @@ -0,0 +1 @@ +the food is "apple" diff --git a/examples/dirwise/pear_b/food.txt b/examples/dirwise/pear_b/food.txt new file mode 100644 index 0000000..0ebdb91 --- /dev/null +++ b/examples/dirwise/pear_b/food.txt @@ -0,0 +1 @@ +bread diff --git a/examples/dirwise/pear_b/log b/examples/dirwise/pear_b/log new file mode 100644 index 0000000..2311e62 --- /dev/null +++ b/examples/dirwise/pear_b/log @@ -0,0 +1 @@ +the food is "bread" diff --git a/examples/dirwise/pear_c/food.txt b/examples/dirwise/pear_c/food.txt new file mode 100644 index 0000000..871bb87 --- /dev/null +++ b/examples/dirwise/pear_c/food.txt @@ -0,0 +1 @@ +cookie diff --git a/examples/dirwise/pear_c/log b/examples/dirwise/pear_c/log new file mode 100644 index 0000000..89a352a --- /dev/null +++ b/examples/dirwise/pear_c/log @@ -0,0 +1 @@ +the food is "cookie" diff --git a/examples/filewise/a.txt b/examples/filewise/a.txt new file mode 100644 index 0000000..4c479de --- /dev/null +++ b/examples/filewise/a.txt @@ -0,0 +1 @@ +apple diff --git a/examples/filewise/b.txt b/examples/filewise/b.txt new file mode 100644 index 0000000..0ebdb91 --- /dev/null +++ b/examples/filewise/b.txt @@ -0,0 +1 @@ +bread diff --git a/examples/filewise/c.txt b/examples/filewise/c.txt new file mode 100644 index 0000000..871bb87 --- /dev/null +++ b/examples/filewise/c.txt @@ -0,0 +1 @@ +cookie diff --git a/examples/input/a/food.txt b/examples/input/a/food.txt new file mode 100644 index 0000000..4c479de --- /dev/null +++ b/examples/input/a/food.txt @@ -0,0 +1 @@ +apple diff --git a/examples/input/a/log b/examples/input/a/log new file mode 100644 index 0000000..eb7de92 --- /dev/null +++ b/examples/input/a/log @@ -0,0 +1 @@ +the food is "apple" diff --git a/examples/input/b/food.txt b/examples/input/b/food.txt new file mode 100644 index 0000000..0ebdb91 --- /dev/null +++ b/examples/input/b/food.txt @@ -0,0 +1 @@ +bread diff --git a/examples/input/b/log b/examples/input/b/log new file mode 100644 index 0000000..2311e62 --- /dev/null +++ b/examples/input/b/log @@ -0,0 +1 @@ +the food is "bread" diff --git a/examples/input/c/food.txt b/examples/input/c/food.txt new file mode 100644 index 0000000..871bb87 --- /dev/null +++ b/examples/input/c/food.txt @@ -0,0 +1 @@ +cookie diff --git a/examples/input/c/log b/examples/input/c/log new file mode 100644 index 0000000..89a352a --- /dev/null +++ b/examples/input/c/log @@ -0,0 +1 @@ +the food is "cookie" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..44b7af7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,152 @@ +use std::path::{PathBuf}; +use clap::{Parser}; +use regex::{Regex}; +use walkdir::{WalkDir}; +use anyhow::{Context, Result, Ok, bail}; +use ansi_term::{ANSIString}; +use ansi_term::Style; +use ansi_term::Colour::{Cyan, Green}; +use std::fs::create_dir_all; +use fs_extra; +use lazy_static::lazy_static; + +#[derive(Parser)] +#[clap(author, version, +about = "Bulk rename using regular expressions", +long_about = "A fast and simple tool for copying data from an input directory to an output directory under different paths based on regular expressions.\nThe syntax is based on the \"regex\" crate: see https://docs.rs/regex/1.5.5/regex/#grouping-and-flags", +propagate_version = true, +disable_help_subcommand = true +)] +struct Cli { + /// Input path filter. Paths which do not match this regex are excluded. + #[clap(short, long, default_value = ".*")] + filter: String, + + /// Regular expression to match paths + #[clap(short, long, default_value = "(.*)")] + expression: String, + + /// Replacement string with capture groups + #[clap(short, long, default_value = "$0")] + replace: String, + + /// Silence output + #[clap(short, long)] + quiet: bool, + + /// deprecated ChRIS flag. Does nothing. + #[clap(long)] + saveinputmeta: bool, + + /// deprecated ChRIS flag. Does nothing. + #[clap(long)] + saveoutputmeta: bool, + + /// input directory + #[clap()] + input_dir: PathBuf, + + /// output directory + #[clap()] + output_dir: PathBuf, +} + + +fn main() -> Result<()> { + let args: Cli = Cli::parse(); + + if !args.input_dir.is_dir() { + bail!("not a directory: {:?}", args.input_dir); + } + if !args.output_dir.is_dir() { + bail!("not a directory: {:?}", args.output_dir); + } + if !args.output_dir.read_dir()?.next().is_none() { + bail!("not empty: {:?}", args.output_dir); + } + + let filter = Regex::new(&args.filter) + .with_context(|| format!("Invalid option --filter={}", &args.filter))?; + let expression = Regex::new(&args.expression) + .with_context(|| format!("Invalid option --expression={}", &args.expression))?; + + let input_pre = args.input_dir.to_str().unwrap(); + let output_pre = args.output_dir.to_str().unwrap(); + + let mut did_nothing = true; + + for (rel, input_path) in filter_input_dir(&args.input_dir, &filter) { + let renamed = expression.replace(rel.to_str().unwrap(), &args.replace).to_string(); + let output_path = args.output_dir.join(&renamed); + + if output_path.exists() { + bail!( + "{:?} already exists. Hint: to operate on subdirectories, try --filter='^{}$'", + &output_path, + args.filter + ); + } + + cpr(&input_path, &output_path)?; + pretty_print(input_pre, output_pre, &rel, &renamed); + did_nothing = false; + } + + if did_nothing { + bail!("No paths under {:?} matched by --filter={}", &args.input_dir, &args.filter) + } + Ok(()) +} + +/// Pretty much `cp -r $1 $2` +fn cpr(src: &PathBuf, dst: &PathBuf) -> Result<()> { + let parent_dir = dst.parent().unwrap(); + create_dir_all(parent_dir) + .with_context(|| format!("Could not create parent directory {:?}", parent_dir))?; + if src.is_file() { + fs_extra::file::copy(src, dst, &*FILE_COPY_OPTIONS)?; + } + else if src.is_dir() { + fs_extra::dir::copy(src, dst, &*DIR_COPY_OPTIONS)?; + } + else { + bail!("{:?} is not a file nor directory", src); + } + Ok(()) +} + + +/// produce relative subpaths under a directory which match a regex +fn filter_input_dir<'a>( + input_dir: &'a PathBuf, filter: &'a Regex +) -> impl Iterator +'a { + WalkDir::new(input_dir).into_iter() + .map(|e| e.unwrap().into_path()) + .map(move |p| ((p.strip_prefix(input_dir).unwrap()).to_owned(), p)) + .filter(|e| filter.is_match(e.0.to_string_lossy().as_ref())) +} + +fn pretty_print(input_pre: &str, output_pre: &str, src: &PathBuf, dst: &str) { + println!( + "{}/{} {} {}/{}", + input_pre, + Cyan.paint(src.to_str().unwrap()), + *DIM_ARROW, + output_pre, + Green.paint(dst) + ) +} + +lazy_static! { + static ref DIM_ARROW: ANSIString<'static> = Style::new().dimmed().paint("->"); + static ref FILE_COPY_OPTIONS: fs_extra::file::CopyOptions = fs_extra::file::CopyOptions::new(); + // static ref DIR_COPY_OPTIONS: fs_extra::dir::CopyOptions = fs_extra::dir::CopyOptions::new(); + static ref DIR_COPY_OPTIONS: fs_extra::dir::CopyOptions = fs_extra::dir::CopyOptions{ + overwrite: false, + skip_exist: false, + buffer_size: 64000, + copy_inside: true, + content_only: true, + depth: 0 + }; +}