Compare commits
38 Commits
9ed70b317d
...
master
Author | SHA1 | Date | |
---|---|---|---|
eacb730961 | |||
844cf4b2de | |||
434f969556 | |||
d98801dd66 | |||
83639a10e2 | |||
34e5b107ff | |||
70af81ed84 | |||
b745915e49 | |||
c6d79c9eb1 | |||
682503a24a | |||
4bf334c9d5 | |||
90a1db4f0c | |||
a3bb168c14 | |||
f4fe30ce9f | |||
c84e0d8c4c | |||
5c3431428f | |||
028c93eb80 | |||
352af7da14 | |||
f4d9c37687 | |||
7bab8a9436 | |||
f31b1b2705 | |||
7b7f6438d4 | |||
4bea0eb15e | |||
1f1f2567ae | |||
2e38fe83f1 | |||
83c384e55c | |||
26cbac64e8 | |||
2f5b1c7be6 | |||
03fa2b3d8b | |||
286ca0b5a5 | |||
64204b561d | |||
4f2eeb6a54 | |||
8662972fe5 | |||
a566813c56 | |||
7bb56ac14d | |||
e95f5b5ac9 | |||
efe4855297 | |||
1eebfd9717 |
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: Module",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "renamebycsv.cli",
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/tests/resources/files",
|
||||||
|
"group\\d+-\\w-(\\d+)\\.txt",
|
||||||
|
"${workspaceFolder}/tests/resources/groups.csv",
|
||||||
|
"target",
|
||||||
|
"replaced",
|
||||||
|
"-d",
|
||||||
|
"-e",
|
||||||
|
"abc",
|
||||||
|
"-k"
|
||||||
|
],
|
||||||
|
"justMyCode": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
26
Jenkinsfile
vendored
26
Jenkinsfile
vendored
@@ -3,23 +3,35 @@ pipeline {
|
|||||||
stages {
|
stages {
|
||||||
stage("install") {
|
stage("install") {
|
||||||
steps {
|
steps {
|
||||||
sh 'conda env update --file environment.yml'
|
sh 'mamba env update --file environment.yml --prefix ./env || mamba env create --force --file environment.yml --prefix ./env'
|
||||||
sh 'echo "conda activate renamebycsv" >> ~/.bashrc'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage("build") {
|
stage("build") {
|
||||||
steps {
|
steps {
|
||||||
|
sh 'rm -rf ./dist/*'
|
||||||
sh "python -m build"
|
sh "python -m build"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage("publish") {
|
stage("test") {
|
||||||
|
steps {
|
||||||
|
sh "pip install dist/*.whl --force-reinstall"
|
||||||
|
sh "renamebycsv -h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage("archive") {
|
||||||
|
steps {
|
||||||
|
archiveArtifacts artifacts: 'dist/*.tar.gz, dist/*.whl', fingerprint: true, followSymlinks: false, onlyIfSuccessful: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage("publish package") {
|
||||||
|
environment {
|
||||||
|
CREDS = credentials('rs-git-package-registry-ydeng')
|
||||||
|
}
|
||||||
when {
|
when {
|
||||||
branch '**/master'
|
branch '**/master'
|
||||||
}
|
}
|
||||||
steps {
|
steps {
|
||||||
withCredentials([usernamePassword(credentialsId: 'rs-git-package-registry-ydeng', passwordVariable: 'PASS', usernameVariable: 'USER')]) {
|
sh returnStatus: true, script: 'python -m twine upload --repository-url https://git.reslate.systems/api/packages/${CREDS_USR}/pypi -u ${CREDS_USR} -p ${CREDS_PSW} --non-interactive --disable-progress-bar --verbose dist/*'
|
||||||
sh "python -m twine upload --repository-url https://git.reslate.systems/api/packages/${USER}/pypi -u ${USER} -p ${PASS} --non-interactive --disable-progress-bar --verbose dist/*"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
README.md
35
README.md
@@ -5,5 +5,38 @@ A simple program that renames files by using a spreadsheet in CSV format as a di
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Rename files recursively within a directory to a desired string
|
- Rename files recursively within a directory to a desired string
|
||||||
|
- Replace only the REGEX match portion
|
||||||
- Desired string is set by a CSV where one column is the original string, and another column is the string to replace the original string with
|
- Desired string is set by a CSV where one column is the original string, and another column is the string to replace the original string with
|
||||||
- Uses a REGEX capture group to select file and the portion of the filename to rename
|
- Uses a REGEX capture group to select file and the portion of the filename to rename
|
||||||
|
- Ability to define file extension
|
||||||
|
|
||||||
|
## Installing using `pip`
|
||||||
|
|
||||||
|
1. Run `pip install --index-url https://git.reslate.systems/api/packages/ydeng/pypi/simple renamebycsv` in any `python3` enabled terminal.
|
||||||
|
|
||||||
|
2. Run `renamebycsv -h` to see the help and confirm installation was successful.
|
||||||
|
|
||||||
|
## Advanced Usage: What is REGEX?
|
||||||
|
|
||||||
|
This program makes heavy use of REGEX, also known as Regular Expression to give users the flexibility to choose which portion of any given filename should be the portion used by the program to look up in the CSV. It is therefore critical for users of this script to understand how REGEX works. Here are some key pointers to get you started:
|
||||||
|
|
||||||
|
- REGEX works by matching one string to another, this is just like if you used any in-text search function.
|
||||||
|
- Where it differs is the ability to use one REGEX string to match many strings.
|
||||||
|
- i.e, the REGEX "`abc\d+`" will match with "`abc1`", "`abc2`", "`abc12`", but not "`ac12`" or "`abc`".
|
||||||
|
- Many characters can be used as normal and will match a string literally (character for character), but some will be treated as special characters (such as the previously used `\`, which indicates that the letter afterwards should be treated specially, such as a token)
|
||||||
|
- Common tokens to be aware of: `.` for any character, `\d` for single digits, `\w` for word characters, `\s` for space characters (tabs, spaces, linebreaks, etc.). Tokens can be repeated by using `+`, indicating "one or more", `*` indicating "none or more". If you want to match something that is read as a token by default, such as `.`, or `+`, using the `\` in front of it will cause it to match `.` literally, i.e, `1\.2` matches `1.3`, but not `123`, `1a3`, etc.
|
||||||
|
- A capture group is a way of "selecting" a part of a text and is formed by using `(` and `)` around the REGEX that should be selected.
|
||||||
|
|
||||||
|
Now for a few examples:
|
||||||
|
|
||||||
|
Let's say we have files `run325-a-1.vcf`, `run326-b-2.vcf`, and `run327-b-3.vcf`. If we know that all that matters is the `1` after the `run[numbers]-[character]-`, we can write `run\d+-\w-(\d)\.vcf` which will match with all 3 of the above examples, and select the last digit. The program can then use a given CSV to look up the selected digits and replace the name with what is given by the CSV.
|
||||||
|
|
||||||
|
For learning and testing your own REGEX, checkout [regex101.com](https://regex101.com/), which allows you to write the strings that you're trying to match, and the REGEX. It will show you live which parts of the strings match to what, if any parts match.
|
||||||
|
|
||||||
|
## Not Working?
|
||||||
|
|
||||||
|
If the program is not working the way you would like it, try running the program in `-v DEBUG` mode which increases verbosity. Typically, files not being renamed can be attributed to one of two problems:
|
||||||
|
|
||||||
|
1. It's looking in the wrong directory. The solution would be to double check that the directory it's looking in (printed by the program each run) is correct. If not, try adding quotes around the path in the command line.
|
||||||
|
|
||||||
|
2. The provided REGEX pattern isn't matching to any of the files. In this case, test one or two of the files at [regex101.com](https://regex101.com/) with your pattern.
|
@@ -3,9 +3,10 @@ channels:
|
|||||||
- anaconda
|
- anaconda
|
||||||
- conda-forge
|
- conda-forge
|
||||||
dependencies:
|
dependencies:
|
||||||
- build=0.7.0
|
- python-build=0.7
|
||||||
- pytest=7.2.2
|
- pytest=7.2
|
||||||
- python=3.11.3
|
- python=3.11
|
||||||
- setuptools=67.6.1
|
- setuptools=67.6
|
||||||
- twine=4.0.2
|
- twine=4.0
|
||||||
- cryptography=38.0.4
|
- cryptography=38.0.4
|
||||||
|
prefix: ./env
|
103
renamebycsv/cli.py
Executable file
103
renamebycsv/cli.py
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from renamebycsv.renamer import find_all_candidates, rename_by_csv
|
||||||
|
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
candidates = find_all_candidates(args.input_dir, args.pattern, args.recursive)
|
||||||
|
if len(candidates):
|
||||||
|
rename_by_csv(
|
||||||
|
args.csv,
|
||||||
|
candidates,
|
||||||
|
args.current,
|
||||||
|
args.become,
|
||||||
|
args.dry,
|
||||||
|
args.extension,
|
||||||
|
args.keep_extension,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
program_name = "renamebycsv"
|
||||||
|
argparser = argparse.ArgumentParser(
|
||||||
|
program_name, description="Rename all files by using a CSV as a dictionary."
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"input_dir",
|
||||||
|
help="The directory containing the items that is to be renamed.",
|
||||||
|
metavar="I",
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"pattern",
|
||||||
|
help="The regex to apply to each file name. The first capture group is used to "
|
||||||
|
"perform the replacement.",
|
||||||
|
metavar="R",
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"csv",
|
||||||
|
help="The CSV to use as the dictionary for the substitutions in file name.",
|
||||||
|
metavar="C",
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"current",
|
||||||
|
help="The column header to look for the text matched by the regex.",
|
||||||
|
metavar="F",
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"become", help="The column header to replace the regex match.", metavar="T"
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--recursive",
|
||||||
|
help="Perform renaming action recursively.",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
help="Overwrite files if file already exists.",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"-d", "--dry", help="Do not perform any renames", action="store_true"
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"-V",
|
||||||
|
"--verbosity",
|
||||||
|
help="Set the logging verbosity.",
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
default="INFO",
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"-e",
|
||||||
|
"--extension",
|
||||||
|
help='Sets the new file extension after the renaming. Use empty string ("") '
|
||||||
|
"to not add extension. Will use empty string by default.",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"-k",
|
||||||
|
"--keep-extension",
|
||||||
|
help="Keeps the OS recognized extension from the original filename. Will "
|
||||||
|
'append to end of argument given by "-e" or "--extension".',
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = argparser.parse_args()
|
||||||
|
logging.basicConfig(
|
||||||
|
format="[%(filename)s %(asctime)s - %(levelname)s] %(message)s",
|
||||||
|
level=args.verbosity.upper(),
|
||||||
|
)
|
||||||
|
run(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
116
renamebycsv/renamebycsv.py → renamebycsv/renamer.py
Executable file → Normal file
116
renamebycsv/renamebycsv.py → renamebycsv/renamer.py
Executable file → Normal file
@@ -1,14 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import csv
|
import csv
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
def find_all_candidates(input_dir: str, regex: str, recursive: bool):
|
def find_all_candidates(input_dir: str, regex: str, recursive: bool):
|
||||||
|
logging.info(
|
||||||
|
'Searching "%s" for files that match "%s" %s',
|
||||||
|
input_dir,
|
||||||
|
regex,
|
||||||
|
"recursively" if recursive else "non-recursively",
|
||||||
|
)
|
||||||
results = []
|
results = []
|
||||||
for subitem in os.listdir(input_dir):
|
for subitem in os.listdir(input_dir):
|
||||||
subitem_path = os.path.join(input_dir, subitem)
|
subitem_path = os.path.join(input_dir, subitem)
|
||||||
@@ -22,15 +25,25 @@ def find_all_candidates(input_dir: str, regex: str, recursive: bool):
|
|||||||
continue
|
continue
|
||||||
results.append((subitem_path, subitem, match))
|
results.append((subitem_path, subitem, match))
|
||||||
logging.debug(f'Collecting "{subitem}"...')
|
logging.debug(f'Collecting "{subitem}"...')
|
||||||
|
if len(results) < 1:
|
||||||
|
logging.info(
|
||||||
|
'No results found matching "%s" in "%s". Please double check your REGEX '
|
||||||
|
"pattern and directory being searched.",
|
||||||
|
regex,
|
||||||
|
input_dir,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.info("Collected %d files to rename.", len(results))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def rename(
|
def rename_by_csv(
|
||||||
csv_path: str,
|
csv_path: str,
|
||||||
candidates: Iterable[tuple[str, str, re.Match]],
|
candidates: Iterable[tuple[str, str, re.Match]],
|
||||||
current: str,
|
current: str,
|
||||||
become: str,
|
become: str,
|
||||||
dry: bool,
|
dry: bool,
|
||||||
|
extension: str,
|
||||||
keep_extension: bool,
|
keep_extension: bool,
|
||||||
):
|
):
|
||||||
replacement_dict = {}
|
replacement_dict = {}
|
||||||
@@ -40,6 +53,10 @@ def rename(
|
|||||||
become_col_ind = None
|
become_col_ind = None
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if current_col_ind is None and become_col_ind is None:
|
if current_col_ind is None and become_col_ind is None:
|
||||||
|
if current not in row:
|
||||||
|
logging.error("\"%s\" not in header %s.", current, list(row))
|
||||||
|
if become not in row:
|
||||||
|
logging.error("\"%s\" not in header %s.", become, list(row))
|
||||||
current_col_ind = row.index(current)
|
current_col_ind = row.index(current)
|
||||||
become_col_ind = row.index(become)
|
become_col_ind = row.index(become)
|
||||||
continue
|
continue
|
||||||
@@ -47,20 +64,30 @@ def rename(
|
|||||||
row[current_col_ind] in replacement_dict
|
row[current_col_ind] in replacement_dict
|
||||||
and replacement_dict[row[current_col_ind]] != row[become_col_ind]
|
and replacement_dict[row[current_col_ind]] != row[become_col_ind]
|
||||||
):
|
):
|
||||||
|
# Check if there's a duplicate key for different values.
|
||||||
raise Exception("Duplicate current key.")
|
raise Exception("Duplicate current key.")
|
||||||
replacement_dict[row[current_col_ind]] = row[become_col_ind]
|
replacement_dict[row[current_col_ind]] = row[become_col_ind]
|
||||||
for subitem_path, subitem, match in candidates:
|
for subitem_path, subitem, match in candidates:
|
||||||
|
if match.group(1) not in replacement_dict:
|
||||||
|
logging.warning(
|
||||||
|
'Group "%s" was not matched to any row in the provided CSV. '
|
||||||
|
"Skipping...",
|
||||||
|
match.group(1),
|
||||||
|
)
|
||||||
|
continue
|
||||||
original = subitem_path
|
original = subitem_path
|
||||||
objective = os.path.join(
|
objective = os.path.join(
|
||||||
os.path.dirname(subitem_path),
|
os.path.dirname(subitem_path),
|
||||||
re.sub(match.re, replacement_dict[match.group(1)], subitem),
|
re.sub(match.re, replacement_dict[match.group(1)], subitem.strip()),
|
||||||
)
|
)
|
||||||
|
if extension:
|
||||||
|
objective += ("." if not extension.startswith(".") else "") + extension
|
||||||
if keep_extension:
|
if keep_extension:
|
||||||
objective += os.path.splitext(subitem_path)[1]
|
objective += os.path.splitext(subitem_path)[1]
|
||||||
logging.info(f'Will rename "{original}" to "{os.path.basename(objective)}"')
|
logging.info(f'Will rename "{original}" to "{os.path.basename(objective)}"')
|
||||||
if os.path.exists(objective):
|
if os.path.exists(objective):
|
||||||
logging.error(
|
logging.error(
|
||||||
f'Path at "{objective}" exists, not continuing. '
|
f'Path at "{objective}" already exists, not continuing. '
|
||||||
"Use -f to overwrite instead of stopping."
|
"Use -f to overwrite instead of stopping."
|
||||||
)
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
@@ -68,78 +95,3 @@ def rename(
|
|||||||
os.rename(original, objective)
|
os.rename(original, objective)
|
||||||
if dry:
|
if dry:
|
||||||
logging.info("No file names were modified.")
|
logging.info("No file names were modified.")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
program_name = "renamebycsv"
|
|
||||||
argparser = argparse.ArgumentParser(
|
|
||||||
program_name, "Rename all files by using a CSV as a dictionary."
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"input_dir",
|
|
||||||
help="The directory containing the items that is to be renamed.",
|
|
||||||
metavar="I",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"regex",
|
|
||||||
help="The regex to apply to each file name. The first capture group is used to "
|
|
||||||
"perform the replacement.",
|
|
||||||
metavar="R",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"csv",
|
|
||||||
help="The CSV to use as the dictionary for the substitutions in file name.",
|
|
||||||
metavar="C",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"current",
|
|
||||||
help="The column header to look for the text matched by the regex.",
|
|
||||||
metavar="F",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"become", help="The column header to replace the regex match.", metavar="T"
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"-r",
|
|
||||||
"--recursive",
|
|
||||||
help="Perform renaming action recursively",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"-f",
|
|
||||||
"--force",
|
|
||||||
help="Overwrite files if file already exists",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"-d", "--dry", help="Do not perform any renames", action="store_true"
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"-V",
|
|
||||||
"--verbosity",
|
|
||||||
help="Set the logging verbosity",
|
|
||||||
required=False,
|
|
||||||
type=str,
|
|
||||||
default="INFO",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"-k",
|
|
||||||
"--keep-extension",
|
|
||||||
help="Keeps the original file's extension by appending it to the end of the "
|
|
||||||
"name defined by the CSV.",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = argparser.parse_args()
|
|
||||||
logging.basicConfig(
|
|
||||||
format="[%(filename)s %(asctime)s - %(levelname)s] %(message)s",
|
|
||||||
level=args.verbosity.upper(),
|
|
||||||
)
|
|
||||||
candidates = find_all_candidates(args.input_dir, args.regex, args.recursive)
|
|
||||||
rename(
|
|
||||||
args.csv, candidates, args.current, args.become, args.dry, args.keep_extension
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@@ -1,10 +1,10 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = renamebycsv
|
name = renamebycsv
|
||||||
version = 0.0.1
|
version = 0.0.8
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
packages = renamebycsv
|
packages = renamebycsv
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
renamebycsv = renamebycsv.renamebycsv:main
|
renamebycsv = renamebycsv.cli:main
|
1
tests/resources/files/foo.txt
Normal file
1
tests/resources/files/foo.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Text
|
2
tests/resources/files/group1-a-12.txt
Normal file
2
tests/resources/files/group1-a-12.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
Text
|
1
tests/resources/files/group1-a-13.txt
Normal file
1
tests/resources/files/group1-a-13.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Text
|
1
tests/resources/files/group1-b-10.txt
Normal file
1
tests/resources/files/group1-b-10.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Text
|
1
tests/resources/files/group1-b-11.txt
Normal file
1
tests/resources/files/group1-b-11.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Text
|
1
tests/resources/files/group1-b-14.txt
Normal file
1
tests/resources/files/group1-b-14.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Text
|
1
tests/resources/files/group1-b-9.txt
Normal file
1
tests/resources/files/group1-b-9.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Text
|
6
tests/resources/groups.csv
Normal file
6
tests/resources/groups.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
target,replaced
|
||||||
|
9,a
|
||||||
|
10,b
|
||||||
|
11,c
|
||||||
|
12,d
|
||||||
|
13,e
|
|
Reference in New Issue
Block a user