Comparing version 0.7.0 to 1.0.0-0
#!/usr/bin/env node | ||
'use strict' | ||
const commandLineArgs = require('command-line-args') | ||
const commandLineUsage = require('command-line-usage') | ||
const cliOptions = require('../lib/cliOptions') | ||
const renamer = require('../') | ||
const ansi = require('ansi-escape-sequences') | ||
const usageSections = [ | ||
{ | ||
header: 'renamer', | ||
content: 'Batch rename files and folders.' | ||
}, | ||
{ | ||
header: 'Synopsis', | ||
content: '$ renamer <options> <files>' | ||
}, | ||
{ | ||
header: 'Options', | ||
optionList: cliOptions, | ||
hide: 'files' | ||
}, | ||
{ | ||
content: 'for more detailed instructions, visit {underline https://github.com/75lb/renamer}' | ||
} | ||
] | ||
let options | ||
try { | ||
options = commandLineArgs(cliOptions) | ||
} catch (err) { | ||
halt(err) | ||
} | ||
if (options.help) { | ||
console.log(commandLineUsage(usageSections)) | ||
} else if (options.files.length) { | ||
const fileStats = renamer.expand(options.files) | ||
options.files = fileStats.filesAndDirs | ||
fileStats.notExisting.forEach(function (file) { | ||
log(options.verbose, { before: file, error: 'does not exist' }) | ||
}) | ||
let results = renamer.replace(options) | ||
results = renamer.replaceIndexToken(results) | ||
if (results.list.length) { | ||
if (options['dry-run']) { | ||
console.log(ansi.format('Dry run', ['bold', 'underline'])) | ||
renamer.dryRun(results).list.forEach(log.bind(null, options.verbose)) | ||
} else { | ||
renamer.rename(results).list.forEach(log.bind(null, options.verbose)) | ||
} | ||
} | ||
} else { | ||
console.error(ansi.format('No input files supplied', 'red')) | ||
console.log(commandLineUsage(usageSections)) | ||
} | ||
function log (verbose, result) { | ||
if (!verbose && !result.renamed) return | ||
const tick = process.platform === 'win32' ? '√' : '✔︎' | ||
const cross = process.platform === 'win32' ? '×': '✖' | ||
const symbol = `{${result.renamed ? 'green' : 'red'} ${result.renamed ? tick : cross}}` | ||
const desc = result.before + (result.after ? ' -> ' + result.after : '') | ||
const errDesc = result.error ? `({red ${result.error}})` : '' | ||
console.log(ansi.format(`${symbol} ${desc} ${errDesc}`)) | ||
} | ||
function halt (err) { | ||
console.error(ansi.format(err.stack, 'red')) | ||
process.exit(1) | ||
} | ||
const CliApp = require('../lib/cli-app') | ||
const cliApp = new CliApp() | ||
module.exports = cliApp.start() |
162
index.js
@@ -1,9 +0,2 @@ | ||
'use strict' | ||
const fileSet = require('file-set') | ||
const path = require('path') | ||
const fs = require('fs') | ||
const Result = require('./lib/Result') | ||
const Results = require('./lib/Results') | ||
const os = require('os') | ||
const t = require('typical') | ||
const EventEmitter = require('events').EventEmitter | ||
@@ -13,129 +6,44 @@ /** | ||
*/ | ||
exports.Result = Result | ||
exports.Results = Results | ||
exports.replace = replace | ||
exports.expand = expand | ||
exports.rename = rename | ||
exports.dryRun = dryRun | ||
exports.replaceIndexToken = replaceIndexToken | ||
/** | ||
* Perform the replace. If no `options.find` is supplied, the entire basename is replaced by `options.replace`. | ||
* | ||
* @param {Object} options - Contains the file list and renaming options | ||
* @returns {Array} An array of ResultObject instances containing `before` and `after` info | ||
* @alias module:renamer | ||
*/ | ||
function replace (options) { | ||
if (!t.isPlainObject(options)) throw new Error('Invalid options supplied') | ||
const findRegex = regExBuilder(options) | ||
const results = new Results() | ||
results.list = options.files.map(replaceSingle.bind(null, findRegex, options.replace)) | ||
return results | ||
} | ||
class Renamer extends EventEmitter { | ||
/** | ||
* @param {object} options - The renamer options | ||
* @param {string[]} [options.files] - One or more glob patterns or names of files to rename. | ||
* @param {sting|RegExp} [options.find] - Find expression. | ||
* @param {string} [options.replace] | ||
* @param {boolean} [options.dryRun] | ||
* @param {boolean} [options.force] | ||
* @param {string[]} [options.plugin] | ||
* @emits module:renamer#rename-start | ||
*/ | ||
rename (options) { | ||
const renameFile = require('./lib/rename-file') | ||
const Replacer = require('./lib/replacer') | ||
const util = require('./lib/util') | ||
function replaceSingle (findRegex, replace, file) { | ||
const result = new Result({ before: path.normalize(file) }) | ||
const dirname = path.dirname(file) | ||
let basename = path.basename(file) | ||
if (findRegex) { | ||
if (basename.search(findRegex) > -1) { | ||
basename = basename.replace(findRegex, replace) | ||
result.after = path.join(dirname, basename) | ||
} else { | ||
/* leave result.after blank, signifying no replace was performed */ | ||
} | ||
} else { | ||
result.after = path.join(dirname, replace) | ||
} | ||
return result | ||
} | ||
function expand (files) { | ||
const fileStats = fileSet(files) | ||
fileStats.filesAndDirs = fileStats.files.concat(fileStats.dirs.reverse()) | ||
return fileStats | ||
} | ||
/** | ||
Takes a Results collection in, sets `renamed` and/or `error` on each with the expected values | ||
@param {Results} results - the Results collection to operate on | ||
@returns {Results} results | ||
*/ | ||
function dryRun (results) { | ||
results.list = results.list.map(function (result, index, resultsSoFar) { | ||
const existing = resultsSoFar.filter(function (prevResult, prevIndex) { | ||
return prevIndex < index && (prevResult.before !== result.before) && (prevResult.after === result.after) | ||
}) | ||
if (result.before === result.after || !result.after) { | ||
result.renamed = false | ||
result.error = 'no change' | ||
} else if (existing.length) { | ||
result.renamed = false | ||
result.error = 'file exists' | ||
} else { | ||
result.renamed = true | ||
} | ||
return result | ||
}) | ||
return results | ||
} | ||
/** | ||
Takes a Results collection in, performs the rename on disk setting `renamer` and `error` as appropriate | ||
@param {Results} results - the Results collection to operate on | ||
@returns {Results} results | ||
*/ | ||
function rename (results) { | ||
results.list = results.list.map(function (result) { | ||
if (!result.after) { | ||
result.renamed = false | ||
result.error = 'no change' | ||
return result | ||
} | ||
try { | ||
if (fs.existsSync(result.after)) { | ||
result.renamed = false | ||
result.error = 'file exists' | ||
} else { | ||
fs.renameSync(result.before, result.after) | ||
result.renamed = true | ||
const files = util.expandGlobPatterns(options.files) | ||
const replacer = new Replacer(options.plugin) | ||
const replaceResults = files | ||
.map((file, index) => replacer.replace(file, options.find, options.replace, options.plugin, index, files)) | ||
.sort((a, b) => util.depthFirstCompare(a.from, b.from)) | ||
for (const replaceResult of replaceResults) { | ||
/** | ||
* Rename start | ||
* @event module:renamer#rename-start | ||
* @type {object} | ||
* @property {string} from | ||
* @property {string} to | ||
*/ | ||
this.emit('rename-start', replaceResult) | ||
if (replaceResult.renamed) { | ||
renameFile(replaceResult.from, replaceResult.to, { force: options.force, dryRun: options.dryRun }) | ||
this.emit('rename-end', replaceResult) | ||
} | ||
} catch (e) { | ||
result.renamed = false | ||
result.error = e.message | ||
} | ||
return result | ||
}) | ||
return results | ||
} | ||
function replaceIndexToken (results) { | ||
results.list = results.list.map(function (result, index) { | ||
if (result.after) { | ||
result.after = result.after.replace(/{{index}}/g, index + 1) | ||
} | ||
return result | ||
}) | ||
return results | ||
} | ||
/** | ||
Search globally by default. If `options.regex` is not set then ensure any special regex characters in `options.find` are escaped. Do nothing if `options.find` is not set. | ||
*/ | ||
function regExBuilder (options) { | ||
if (options.find) { | ||
const re = options.regex ? options.find : escapeRegExp(options.find) | ||
const reOptions = 'g' + (options.insensitive ? 'i' : '') | ||
return new RegExp(re, reOptions) | ||
} | ||
} | ||
function escapeRegExp(string){ | ||
return string | ||
? string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1') | ||
: '' | ||
} | ||
module.exports = Renamer |
{ | ||
"name": "renamer", | ||
"description": "Batch rename files and folders", | ||
"version": "0.7.0", | ||
"description": "Rename files in bulk", | ||
"version": "1.0.0-0", | ||
"author": "Lloyd Brookes <75pound@gmail.com>", | ||
@@ -9,5 +9,10 @@ "bin": "bin/cli.js", | ||
"engines": { | ||
"node": ">=4.0.0" | ||
"node": ">=8.0.0" | ||
}, | ||
"repository": "https://github.com/75lb/renamer", | ||
"files": [ | ||
"index.js", | ||
"lib", | ||
"bin" | ||
], | ||
"keywords": [ | ||
@@ -23,17 +28,21 @@ "rename", | ||
"scripts": { | ||
"test": "test-runner test/cli.js test/renamer.*.js" | ||
"test": "test-runner test/*.js", | ||
"docs": "jsdoc2md index.js > docs/API.md", | ||
"cover": "nyc --reporter=text-lcov test-runner test/*.js | coveralls" | ||
}, | ||
"dependencies": { | ||
"ansi-escape-sequences": "^4.0.0", | ||
"array-back": "^2.0.0", | ||
"chalk": "^2.4.1", | ||
"command-line-args": "^5.0.2", | ||
"command-line-usage": "^5.0.5", | ||
"file-set": "^2.0.0", | ||
"test-value": "^3.0.0", | ||
"typical": "^2.6.1" | ||
"glob": "^7.1.2", | ||
"reduce-flatten": "^2.0.0", | ||
"reduce-unique": "^2.0.1", | ||
"stream-read-all": "^1.0.1" | ||
}, | ||
"devDependencies": { | ||
"more-fs": "~0.5.0", | ||
"coveralls": "^3.0.1", | ||
"mkdirp2": "^1.0.4", | ||
"rimraf": "^2.6.2", | ||
"test-runner": "^0.5.0" | ||
} | ||
} |
299
README.md
[](https://www.npmjs.org/package/renamer) | ||
[](https://www.npmjs.org/package/renamer) | ||
[](https://travis-ci.org/75lb/renamer) | ||
[](https://coveralls.io/github/75lb/renamer?branch=next) | ||
[](https://david-dm.org/75lb/renamer) | ||
[](https://github.com/feross/standard) | ||
renamer | ||
======= | ||
Batch rename files and folders. | ||
***renamer v1.0.0 and this documentation is a WIP*** | ||
Install | ||
------- | ||
Install [node](https://nodejs.org) then: | ||
```sh | ||
$ npm install -g renamer | ||
``` | ||
*Linux/Mac users may need to run the above with `sudo`* | ||
# renamer | ||
Renamer is a command-line utility intended to help rename files and folders in bulk. It is flexible and extensible via plugins. | ||
Usage | ||
----- | ||
``` | ||
renamer | ||
Batch rename files and folders. | ||
## Disclaimer | ||
Usage | ||
$ renamer <options> <files> | ||
Always run this tool with the `--dry-run` option until you are confident the results look correct. | ||
-f, --find <string> The find string, or regular expression when --regex is set. If not set, the whole filename will be replaced. | ||
-r, --replace <string> The replace string. With --regex set, --replace can reference parenthesised substrings from --find with $1, $2, $3 | ||
etc. If omitted, defaults to a blank string. The special token '{{index}}' will insert an incrementing number per | ||
file processed. | ||
-e, --regex When set, --find is interpreted as a regular expression. | ||
-d, --dry-run Used for test runs. Set this to do everything but rename the file. | ||
-i, --insensitive Enable case-insensitive finds. | ||
-v, --verbose Use to print additional information. | ||
-h, --help Print usage instructions. | ||
## Synopsis | ||
for more detailed instructions, visit https://github.com/75lb/renamer | ||
``` | ||
As input, renamer takes a list of filenames or glob patterns plus some options describing how you would like the files to be renamed. If no filesnames/patterns are specified, renamer will look for a newline-separated list of filenames on standard input. | ||
For more information on Regular Expressions, see [this useful guide](https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions). | ||
<pre><code>$ renamer [options] [<u>file</u> <u>...</u>] | ||
</pre></code> | ||
**Don't forget to test your rename procedure first using `--dry-run`!** | ||
Trivial example. It will replace the text `jpeg` with `jpg` in all files or folders in the current directory. | ||
Recursing | ||
--------- | ||
Renamer comes with globbing support built in (provided by [node-glob](https://github.com/isaacs/node-glob), enabling recursive operations. To recurse, use the `**` wildcard where a directory name would appear to apply the meaning "any directory, including this one". | ||
``` | ||
$ renamer --find jpeg --replace jpg * | ||
``` | ||
For example, this command operates on all `js` files in the current directory: | ||
As above but operates on all files and folders recursively. | ||
$ renamer --find this --replace that '*.js' | ||
``` | ||
$ renamer --find jpeg --replace jpg "**" | ||
``` | ||
this command operates on all `js` files, recursively: | ||
Same operation but on a filename list supplied via stdin. This approach is useful for crafting a more specific input list using tools like `find`. This example operates on files modified less than 20 minutes ago. | ||
$ renamer --find this --replace that '**/*.js' | ||
``` | ||
$ find . -mtime -20m | renamer --find jpeg --replace jpg | ||
``` | ||
this command operates on all `js` files from the `lib` directory downward: | ||
Same again but with a hand-rolled input of filenames and glob patterns. Create an input text file, e.g. `files.txt`: | ||
$ renamer --find this --replace that 'lib/**/*.js' | ||
``` | ||
house.jpeg | ||
garden.jpeg | ||
img/* | ||
``` | ||
**Bash users without globstar will need to enclose the glob expression in quotes to prevent native file expansion**, i.e. `'**/*.js'` | ||
Then pipe it into renamer. | ||
Examples | ||
-------- | ||
Some real-world examples. | ||
**Windows users**: the single-quotation marks used in the example commands below are for bash (Mac/Linux) users, please replace these with double-quotation marks on Windows. | ||
### Simple replace | ||
```sh | ||
$ renamer --find '[bad]' --replace '[good]' * | ||
``` | ||
$ cat files.txt | renamer --find jpeg --replace jpg | ||
``` | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── A poem [bad].txt | ||
├── A story [bad].txt</code></pre></td> | ||
<td><pre><code>. | ||
├── A poem [good].txt | ||
├── A story [good].txt</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
Simple example using a [regular expression literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). The case-insensitive pattern `/one/i` matches the input file `ONE.jpg`, renaming it to `two.jpg`. | ||
### Case insenstive finds | ||
```sh | ||
$ renamer --insensitive --find 'mpeg4' --replace 'mp4' * | ||
``` | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── A video.MPEG4 | ||
├── Another video.Mpeg4</code></pre></td> | ||
<td><pre><code>. | ||
├── A video.mp4 | ||
├── Another video.mp4</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
$ renamer --find '/one/i' --replace 'two' ONE.jpg | ||
``` | ||
### Strip out unwanted text | ||
The full set of command-line options. | ||
```sh | ||
$ renamer --find 'Season 1 - ' * | ||
``` | ||
-f, --find string Optional find string (e.g. "one") or regular expression literal (e.g. | ||
"/one/i"). If omitted, the whole filename will be matched and replaced. | ||
-r, --replace string The replace string. If omitted, defaults to a empty string. The special token | ||
'{{index}}' will insert a number, incremented each time a file is replaced. | ||
-d, --dry-run Used for test runs. Set this to do everything but rename the file. | ||
--force If the target path already exists, overwrite it. Use with caution. | ||
-v, --verbose In the output, also include names of files that were not renamed. | ||
-h, --help Print usage instructions. | ||
``` | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── Season 1 - Some crappy episode.mp4 | ||
├── Season 1 - Load of bollocks.mp4</code></pre></td> | ||
<td><pre><code>. | ||
├── Some crappy episode.mp4 | ||
├── Load of bollocks.mp4</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
For more information on Regular Expressions, see [this useful guide](https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions). | ||
### Simple filename cleanup | ||
## Globbing | ||
```sh | ||
$ renamer --regex --find '.*_(\d+)_.*' --replace 'Video $1.mp4' * | ||
``` | ||
Renamer comes with globbing support built in supporting all special characters [described here](https://github.com/isaacs/node-glob#glob-primer). | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── [ag]_Annoying_filename_-_3_[38881CD1].mp4 | ||
├── [ag]_Annoying_filename_-_34_[38881CD1].mp4 | ||
├── [ag]_Annoying_filename_-_53_[38881CD1].mp4</code></pre></td> | ||
<td><pre><code>. | ||
├── Video 3.mp4 | ||
├── Video 34.mp4 | ||
├── Video 53.mp4</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
For example, this command operates on all `js` files in the current directory: | ||
### Give your images a new numbering scheme | ||
```sh | ||
$ renamer --replace 'Image{{index}}.jpg' * | ||
``` | ||
$ renamer --find this --replace that '*.js' | ||
``` | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── IMG_5776.JPG | ||
├── IMG_5777.JPG | ||
├── IMG_5778.JPG</code></pre></td> | ||
<td><pre><code>. | ||
├── Image1.jpg | ||
├── Image2.jpg | ||
├── Image3.jpg</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
This command operates on all `js` files, recursively: | ||
### do something about all those full stops | ||
```sh | ||
$ renamer --regex --find '\.(?!\w+$)' --replace ' ' * | ||
``` | ||
$ renamer --find this --replace that '**/*.js' | ||
``` | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── loads.of.full.stops.every.where.jpeg | ||
├── loads.of.full.stops.every.where.mp4</code></pre></td> | ||
<td><pre><code>. | ||
├── loads of full stops every where.jpeg | ||
├── loads of full stops every where.mp4</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
this command operates on all `js` files from the `lib` directory downward: | ||
### if not already done, add your name to a load of files | ||
```sh | ||
$ renamer --regex --find '(data\d)(\.\w+)' --replace '$1 (checked by Lloyd)$2' * | ||
``` | ||
$ renamer --find this --replace that 'lib/**/*.js' | ||
``` | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── data1.csv | ||
├── data2 (checked by Lloyd).csv | ||
├── data3.xls</code></pre></td> | ||
<td><pre><code>. | ||
├── data1 (checked by Lloyd).csv | ||
├── data2 (checked by Lloyd).csv | ||
├── data3 (checked by Lloyd).xls</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
**Bash users without globstar will need to enclose the glob expression in quotes to prevent native file expansion**, i.e. `'**/*.js'` | ||
## Further reading | ||
### rename files and folders, recursively | ||
Please see [the wiki](https://github.com/75lb/renamer/wiki) for more documentation and examples. | ||
```sh | ||
$ renamer --find 'pic' --replace 'photo' '**' | ||
``` | ||
## Install | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── pic1.jpg | ||
├── pic2.jpg | ||
└── pics | ||
├── pic3.jpg | ||
└── pic4.jpg | ||
</code></pre></td> | ||
<td><pre><code>. | ||
├── photo1.jpg | ||
├── photo2.jpg | ||
└── photos | ||
├── photo3.jpg | ||
└── photo4.jpg</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
### prefix files and folders, recursively | ||
```sh | ||
$ renamer --regex --find '^' --replace 'good-' '**' | ||
``` | ||
$ npm install -g renamer@next | ||
``` | ||
<table> | ||
<thead> | ||
<tr><th>Before</th><th>After</th></tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td><pre><code>. | ||
├── pic1.jpg | ||
├── pic2.jpg | ||
└── pics | ||
├── pic3.jpg | ||
└── pic4.jpg | ||
</code></pre></td> | ||
<td><pre><code>. | ||
├── good-pic1.jpg | ||
├── good-pic2.jpg | ||
└── good-pics | ||
├── good-pic3.jpg | ||
└── good-pic4.jpg</code></pre></td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
* * * | ||
© 2012-18 Lloyd Brookes \<75pound@gmail.com\>. Documented by [jsdoc-to-markdown](https://github.com/75lb/jsdoc-to-markdown). |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
4
1
16981
4
13
349
114
+ Addedchalk@^2.4.1
+ Addedglob@^7.1.2
+ Addedreduce-flatten@^2.0.0
+ Addedreduce-unique@^2.0.1
+ Addedstream-read-all@^1.0.1
+ Addedreduce-flatten@2.0.0(transitive)
+ Addedreduce-unique@2.0.1(transitive)
+ Addedstream-read-all@1.0.1(transitive)
- Removedansi-escape-sequences@^4.0.0
- Removedarray-back@^2.0.0
- Removedfile-set@^2.0.0
- Removedtest-value@^3.0.0
- Removedtypical@^2.6.1
- Removedansi-escape-sequences@4.1.0(transitive)
- Removedfile-set@2.0.1(transitive)
- Removedtest-value@3.0.0(transitive)