The new file name can contain any number of built-in and custom variables that will be replaced with their corresponding value. Expand for more info.
77 |
78 |
79 | `{{i}}` Index: The index of the file when renaming multiple files to the same name. If you do no include `{{i}}` in your new file name, the index will be appended to the end. Use the `--noindex` option to prevent auto-indexing.
80 |
81 | `{{f}}` File name: The original name of the file.
82 |
83 | `{{ext}}` File extension: The original extension of the file (with the `.`)
84 |
85 | `{{p}}` Parent directory: The name of the parent directory.
86 |
87 | `{{isDirectory}}` Is directory: true/false. Useful for conditionally adding a file extension to files and not directories with `{% if isDirectory %}...`
88 |
89 | `{{os.x}}` Operating System: Information about the OS/user. Replace `x` with `homedir`, `hostname`, `platform`, or `user`
90 |
91 | `{{date.x}}` Dates: Insert a date. Replace `x` with `current` (the current date/time), `create` (the file's created date/time), `access` (the file's last accessed date/time) or `modify` (the file's last modified date/time)
92 |
93 | `{{g}}` GUID: A pseudo-random globally unique identifier.
94 |
95 | `{{exif.x}}` EXIF: Photo EXIF Information. Replace `x` with `iso`, `fnum`, `exposure`, `date`, `width`, or `height`
96 |
97 | `{{id3.x}}` ID3: Gets ID3 tags from MP3 files. Replace `x` with `title`, `artist`, `album`, `track`, `totalTracks`, or `year`
98 |
99 | You can also add your own variables. See the [Customize](#customize) section for more info.
100 |
101 |
102 |
103 |
104 | ## Filters and Examples
105 | You can modify variable values by applying filters. Multiple filters can be chained together. Nunjucks, the underlying variable-replacement engine, has a large number of filters available and Rename-CLI has a few of its own. Expand for more info.
106 |
107 |
108 | String case manipulation
109 | - `{{f|lower}}` - `Something Like This.txt → something like this.txt`
110 | - `{{f|upper}}` - `Something Like This.txt → SOMETHING LIKE THIS.txt`
111 | - `{{f|camel}}` - `Something Like This.txt → somethingLikeThis.txt`
112 | - `{{f|pascal}}` - `Something Like This.txt → SomethingLikeThis.txt`
113 | - `{{f|kebab}}` - `Something Like This.txt → something-like-this.txt`
114 | - `{{f|snake}}` - `Something Like This.txt → something_like_this.txt`
115 |
116 | -----
117 |
118 | `replace('something', 'replacement')` - replace a character or string with something else.
119 |
120 | ```sh
121 | rename "bills file.pdf" "{{ f | replace('bill', 'mary') | pascal }}"
122 |
123 | bills file.pdf → MarysFile.pdf
124 | ```
125 |
126 | -----
127 |
128 | `date` - format a date to a specific format, the default is `yyyyMMdd` if no parameter is passed. To use your own format, simply pass the format as a string parameter to the date filter. Formatting options can be found [here](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table).
129 |
130 | ```sh
131 | rename *.txt "{{ date.current | date }}-{{f}}"
132 |
133 | a.txt → 20200502-a.txt
134 | b.txt → 20200502-b.txt
135 | c.txt → 20200502-c.txt
136 |
137 | rename *.txt "{{ date.current | date('MM-dd-yyyy') }}-{{f}}"
138 |
139 | a.txt → 05-02-2020-a.txt
140 | b.txt → 05-02-2020-b.txt
141 | c.txt → 05-02-2020-c.txt
142 | ```
143 |
144 | -----
145 |
146 | `match(RegExp[, flags, group num/name])` - match substring(s) using a regular expression. The only required parameter is the regular expression (as a string), it also allows for an optional parameter `flags` (a string containing any or all of the flags: g, i, m, s, u, and y, more info [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp#Parameters)), and an optional parameter of the `group` number or name. *Named groups cannot be used with the global flag.*
147 |
148 | ```sh
149 | rename *ExpenseReport* "archive/{{ f | match('^.+(?=Expense)') }}/ExpenseReport.docx" --createdirs
150 |
151 | JanuaryExpenseReport.docx → archive/January/ExpenseReport.docx
152 | MarchExpenseReport.docx → archive/March/ExpenseReport.docx
153 | ```
154 |
155 | -----
156 |
157 | `regexReplace(RegExp[, flags, replacement])` - replace the first regex match with the `replacement` string. To replace all regex matches, pass the `g` flag. `flags` and `replacement` are optional, the default value for replacement is an empty string.
158 |
159 | ```sh
160 | rename test/* "{{ f | regexReplace('(^|e)e', 'g', 'E') }}"
161 |
162 | test/eight.txt → Eight.txt
163 | test/eighteen.txt → EightEn.txt
164 | test/eleven.txt → Eleven.txt
165 | ```
166 |
167 | -----
168 |
169 | `padNumber(length)` - put leading zeroes in front of a number until it is `length` digits long. If `length` is a string, it will use the string's length.
170 |
171 | ```sh
172 | rename Absent\ Sounds/* "{{id3.year}}/{{id3.artist}}/{{id3.album}}/{{ id3.track | padNumber(id3.totalTracks) }} - {{id3.title}}{{ext}}"
173 |
174 | Absent Sounds/Am I Alive.mp3 → 2014/From Indian Lakes/Absent Sounds/05 - Am I Alive.mp3
175 | Absent Sounds/Awful Things.mp3 → 2014/From Indian Lakes/Absent Sounds/07 - Awful Things.mp3
176 | Absent Sounds/Breathe, Desperately.mp3 → 2014/From Indian Lakes/Absent Sounds/03 - Breathe, Desperately.mp3
177 | Absent Sounds/Come In This Light.mp3 → 2014/From Indian Lakes/Absent Sounds/01 - Come In This Light.mp3
178 | Absent Sounds/Fog.mp3 → 2014/From Indian Lakes/Absent Sounds/10 - Fog.mp3
179 | Absent Sounds/Ghost.mp3 → 2014/From Indian Lakes/Absent Sounds/06 - Ghost.mp3
180 | Absent Sounds/Label This Love.mp3 → 2014/From Indian Lakes/Absent Sounds/02 - Label This Love.mp3
181 | Absent Sounds/Runner.mp3 → 2014/From Indian Lakes/Absent Sounds/08 - Runner.mp3
182 | Absent Sounds/Search For More.mp3 → 2014/From Indian Lakes/Absent Sounds/09 - Search For More.mp3
183 | Absent Sounds/Sleeping Limbs.mp3 → 2014/From Indian Lakes/Absent Sounds/04 - Sleeping Limbs.mp3
184 | ```
185 |
186 |
187 |
188 |
189 | ## Customize
190 | You can expand upon and overwrite much of the default functionality by creating your own variables and filters. Expand for more info.
191 |
192 |
193 | ### Variables
194 | The first time you run the rename command a file will be created at `~/.rename/userData.js`, this file can be edited to add new variables that you can access with `{{variableName}}` in your new file name. You can also override the built-in variables by naming your variable the same. The userData.js file contains some examples.
195 |
196 | ```js
197 | // These are some helpful libraries already included in rename-cli
198 | // All the built-in nodejs libraries are also available
199 | // const exif = require('jpeg-exif'); // https://github.com/zhso/jpeg-exif
200 | // const fs = require('fs-extra'); // https://github.com/jprichardson/node-fs-extra
201 | // const Fraction = require('fraction.js'); // https://github.com/infusion/Fraction.js
202 | // const date-fns = require('date-fns'); // https://date-fns.org/
203 |
204 | module.exports = function(fileObj, descriptions) {
205 | let returnData = {};
206 | let returnDescriptions = {};
207 |
208 | // Put your code here to add properties to returnData
209 | // this data will then be available in your output file name
210 | // for example: returnData.myName = 'Your Name Here';
211 | // or: returnData.backupDir = 'D:/backup';
212 |
213 | // Optionally, you can describe a variable and have it show when printing help information
214 | // add the same path as a variable to the returnDescriptions object with a string description
215 | // for example: returnDescriptions.myName = 'My full name';
216 | // or: returnDescriptions.backupDir = 'The path to my backup directory';
217 |
218 | if (!descriptions) return returnData;
219 | else return returnDescriptions;
220 | };
221 | ```
222 |
223 | The `fileObj` that is passed to the function will look something like this:
224 |
225 | ```
226 | {
227 | i: '--FILEINDEXHERE--',
228 | f: 'filename',
229 | fileName: 'filename',
230 | ext: '.txt',
231 | isDirectory: false,
232 | p: 'parent-directory-name',
233 | parent: 'parent-directory-name',
234 | date: {
235 | current: 2020-11-25T17:41:58.303Z,
236 | now: 2020-11-25T17:41:58.303Z,
237 | create: 2020-11-24T23:38:25.455Z,
238 | modify: 2020-11-24T23:38:25.455Z,
239 | access: 2020-11-24T23:38:25.516Z
240 | },
241 | os: {
242 | homedir: '/Users/my-user-name',
243 | platform: 'darwin',
244 | hostname: 'ComputerName.local',
245 | user: 'my-user-name'
246 | },
247 | guid: 'fb274642-0a6f-4fe6-8b07-0bac4db5c87b',
248 | customGuid: [Function: customGuid],
249 | stats: Stats {
250 | dev: 16777225,
251 | mode: 33188,
252 | nlink: 1,
253 | uid: 501,
254 | gid: 20,
255 | rdev: 0,
256 | blksize: 4096,
257 | ino: 48502576,
258 | size: 1455,
259 | blocks: 8,
260 | atimeMs: 1606261105516.3499,
261 | mtimeMs: 1606261105455.4163,
262 | ctimeMs: 1606261105486.9072,
263 | birthtimeMs: 1606261105455.093,
264 | atime: 2020-11-24T23:38:25.516Z,
265 | mtime: 2020-11-24T23:38:25.455Z,
266 | ctime: 2020-11-24T23:38:25.487Z,
267 | birthtime: 2020-11-24T23:38:25.455Z
268 | },
269 | parsedPath: {
270 | root: '/',
271 | dir: '/Users/my-user-name/Projects/node-rename-cli',
272 | base: 'filename.txt',
273 | ext: '.txt',
274 | name: 'filename'
275 | },
276 | exif: { iso: '', fnum: '', exposure: '', date: '', width: '', height: '' },
277 | id3: {
278 | title: '',
279 | artist: '',
280 | album: '',
281 | year: '',
282 | track: '',
283 | totalTracks: ''
284 | }
285 | }
286 | ```
287 |
288 | ### Filters
289 | The first time you run the rename command a file will be created at `~/.rename/userFilters.js`, this file can be edited to add new filters that you can access with `{{someVariable | myNewFilter}}` in your new file name.
290 |
291 | One place custom filters can be really handy is if you have files that you often receive in some weird format and you then convert them to your own desired format. Instead of writing some long, complex new file name, just write your own filter and make the new file name `{{f|myCustomFilterName}}`. You can harness the power of code to do really complex things without having to write a complex command.
292 |
293 | Each filter should accept a parameter that contains the value of the variable passed to the filter (`str` in the example below). You can optionally include more of your own parameters as well. The function should also return a string that will then be inserted into the new file name (or passed to another filter if they are chained). The userFilters.js file contains some examples.
294 |
295 | ```js
296 | // Uncomment the next line to create an alias for any of the default Nunjucks filters https://mozilla.github.io/nunjucks/templating.html#builtin-filters
297 | // const defaultFilters = require('../nunjucks/src/filters');
298 | // These are some helpful libraries already included in rename-cli
299 | // All the built-in nodejs libraries are also available
300 | // const exif = require('jpeg-exif'); // https://github.com/zhso/jpeg-exif
301 | // const fs = require('fs-extra'); // https://github.com/jprichardson/node-fs-extra
302 | // const Fraction = require('fraction.js'); // https://github.com/infusion/Fraction.js
303 | // const { format } = require('date-fns'); // https://date-fns.org/
304 |
305 | module.exports = {
306 | // Create an alias for a built-in filter
307 | // big: defaultFilters.upper,
308 | // Create your own filter
309 | // match: function(str, regexp, flags) {
310 | // if (regexp instanceof RegExp === false) {
311 | // regexp = new RegExp(regexp, flags);
312 | // }
313 | // return str.match(regexp);
314 | // }
315 | };
316 | ```
317 |
318 |
319 |
320 |
--------------------------------------------------------------------------------
/bin.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Handle EPIPE errors when user doesn't put quotes around output file name with parameters
3 | function epipeError(err) {
4 | if (err.code === 'EPIPE' || err.errno === 32) return process.exit;
5 | if (process.stdout.listeners('error').length <= 1) {
6 | process.stdout.removeAllListeners();
7 | process.stdout.emit('error', err);
8 | process.stdout.on('error', epipeError);
9 | }
10 | }
11 | process.stdout.on('error', epipeError);
12 |
13 | const chalk = require('chalk');
14 | const fs = require('fs-extra');
15 | const opn = require('opn');
16 | const os = require('os');
17 | const yargs = require('yargs');
18 |
19 | const database = require('./src/database');
20 | const util = require('./src/util');
21 |
22 | (async () => {
23 | // create ~/.rename/userData.js if not exist
24 | const userDataPath = os.homedir() + '/.rename/userData.js';
25 | await fs.ensureFile(userDataPath).catch(err => { throw err; });
26 | const userDataContents = await fs.readFile(userDataPath, 'utf8').catch(err => { throw err; });
27 | if (userDataContents === '') {
28 | await fs.copyFile(__dirname + '/lib/userData.js', userDataPath);
29 | }
30 | // create ~/.rename/userFilters.js if not exist
31 | const userFiltersPath = os.homedir() + '/.rename/userFilters.js';
32 | await fs.ensureFile(userFiltersPath).catch(err => { throw err; });
33 | const userFiltersContents = await fs.readFile(userFiltersPath, 'utf8').catch(err => { throw err; });
34 | if (userFiltersContents === '') {
35 | await fs.copyFile(__dirname + '/lib/userFilters.js', userFiltersPath);
36 | }
37 | // SQLite database setup
38 | const sequelize = await database.init();
39 |
40 | const { Operation } = require('./src/operation');
41 | const { Options } = require('./src/options');
42 | const { Batch } = require('./src/batch');
43 | const { History } = require('./src/history');
44 | const { Favorites } = require('./src/favorites');
45 |
46 | // Parse command line arguments
47 | const argv = yargs
48 | .usage('Rename-CLI v' + require('./package.json').version + '\n\nUsage:\n\n rename [options] file(s) new-file-name')
49 | .options(require('./lib/yargsOptions'))
50 | .help('help')
51 | .epilogue('Variables:\n\n' + util.getVariableList())// + rename.getReplacementsList())
52 | .wrap(yargs.terminalWidth())
53 | .argv;
54 |
55 | // Turn parsed args into new Options class
56 | const options = new Options(argv);
57 | // Ensure that only input files that exist are considered
58 | await options.validateInputFiles();
59 |
60 | if (options.info) { // view online hlep
61 | opn('https://github.com/jhotmann/node-rename-cli');
62 | if (process.platform !== 'win32') {
63 | process.exit(0);
64 | }
65 | } else if (options.history !== false) { // launch history UI
66 | options.history = options.history || 10;
67 | let history = new History(sequelize, options);
68 | await history.getBatches();
69 | await history.display();
70 | } else if (options.favorites !== false) { // run favorite or launch favorites UI
71 | let favorites = new Favorites(sequelize, options);
72 | await favorites.get();
73 | if (options.favorites) await favorites.run();
74 | } else if (options.undo) { // undo previous rename
75 | options.history = 1;
76 | options.noUndo = true;
77 | let history = new History(sequelize, options);
78 | await history.getBatches();
79 | if (history.batches.length === 0) {
80 | console.log(chalk`{red No batches found that can be undone}`);
81 | process.exit(1);
82 | }
83 | const lastBatch = history.batches[0];
84 | console.log(`Undoing '${util.argvToString(JSON.parse(lastBatch.command))}' (${lastBatch.Ops.length} operation${lastBatch.Ops.length === 1 ? '' : 's'})`);
85 | await history.undoBatch(0);
86 | } else if (options.wizard) { // launch the wizard
87 | await require('./src/wizard')(sequelize);
88 | } else if (options.printdata && options.inputFiles.length === 1) { // print the file's data
89 | let operation = new Operation(options.inputFiles[0], options, sequelize);
90 | operation.printData();
91 | } else if (options.inputFiles.length > 0 && options.outputPattern) { // proceed to do the rename
92 | let batch = new Batch(argv, options, sequelize);
93 | await batch.complete();
94 | } else if (argv._.length === 0 && !options.compiled) { // launch TUI
95 | const ui = require('./src/tui');
96 | await ui(sequelize);
97 | } else { // Invalid command
98 | if (options.invalidInputFiles > 0) {
99 | console.log(chalk`{red ERROR: None of the input files specified exist}`);
100 | } else {
101 | console.log(chalk`{red ERROR: Not enough arguments specified. Type rename -h for help}`);
102 | }
103 | process.exit(1);
104 | }
105 | })();
--------------------------------------------------------------------------------
/bin.test.js:
--------------------------------------------------------------------------------
1 | const async = require('async');
2 | const fs = require('fs-extra');
3 | const os = require('os');
4 | const path = require('path');
5 | const yargs = require('yargs');
6 |
7 | const database = require('./src/database');
8 | const yargsOptions = require('./lib/yargsOptions');
9 |
10 | const { Batch } = require('./src/batch');
11 | const { History } = require('./src/history');
12 | const { Options } = require('./src/options');
13 | const { Favorites } = require('./src/favorites');
14 |
15 | jest.setTimeout(30000);
16 |
17 | let SEQUELIZE;
18 |
19 | beforeAll(async () => {
20 | // remove test directory
21 | await fs.remove('./test');
22 | // create test files/directories
23 | await fs.ensureDir('test');
24 | await fs.ensureDir('test/another-dir');
25 | await async.times(31, async (i) => {
26 | if (i === 0) return;
27 | let num = inWords(i);
28 | let dir = `${i < 20 ? 'test/' : 'test/another-dir/'}`;
29 | let fileName = `${num.trim().replace(' ', '-')}.txt`;
30 | await fs.writeFile(`${dir}${fileName}`, `file ${num.trim()}`, 'utf8');
31 | });
32 | SEQUELIZE = await database.initTest();
33 | });
34 |
35 | describe('Rename a single file: rename test/one.txt test/one-renamed.txt', () => {
36 | const oldFiles = ['test/one.txt'];
37 | const newFiles = ['test/one-renamed.txt'];
38 | let originalContent;
39 | beforeAll(async () => {
40 | originalContent = await fs.readFile('test/one.txt', 'utf8');
41 | await runCommand('rename test/one.txt test/one-renamed.txt');
42 | });
43 | test(`Old files don't exist`, async () => {
44 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
45 | await expect(result).toBe(false);
46 | });
47 | test(`New files do exist`, async () => {
48 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
49 | await expect(result).toBe(true);
50 | });
51 | test(`New file has correct content`, async () => {
52 | const result = await fs.readFile('test/one-renamed.txt', 'utf8');
53 | expect(result).toBe(originalContent);
54 | });
55 | });
56 |
57 | describe('Rename multiple files the same thing with appended index: rename test/f*.txt test/multiple', () => {
58 | const oldFiles = ['test/four.txt', 'test/five.txt', 'test/fourteen.txt', 'test/fifteen.txt'];
59 | const newFiles = ['test/multiple1.txt', 'test/multiple2.txt', 'test/multiple3.txt', 'test/multiple4.txt'];
60 | beforeAll(async () => {
61 | await runCommand('rename test/f*.txt test/multiple');
62 | });
63 | test(`Old files don't exist`, async () => {
64 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
65 | await expect(result).toBe(false);
66 | });
67 | test(`New files do exist`, async () => {
68 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
69 | await expect(result).toBe(true);
70 | });
71 | });
72 |
73 | describe('Rename multiple files the same thing with appended index and file extension specified and sort option: rename test/multiple* test/twelve.txt test/multiple.log', () => {
74 | const oldFiles = ['test/multiple1.txt', 'test/multiple2.txt', 'test/multiple3.txt', 'test/multiple4.txt', 'test/twelve.txt'];
75 | const newFiles = ['test/multiple1.log', 'test/multiple2.log', 'test/multiple3.log', 'test/multiple4.log', 'test/multiple5.log'];
76 | let originalContent;
77 | beforeAll(async () => {
78 | originalContent = await fs.readFile('test/twelve.txt', 'utf8');
79 | await runCommand('rename --sort reverse-alphabet test/multiple* test/twelve.txt test/multiple.log');
80 | });
81 | test(`Old files don't exist`, async () => {
82 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
83 | await expect(result).toBe(false);
84 | });
85 | test(`New files do exist`, async () => {
86 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
87 | await expect(result).toBe(true);
88 | });
89 | test(`New file has correct content`, async () => {
90 | const result = await fs.readFile('test/multiple1.log', 'utf8');
91 | expect(result).toBe(originalContent);
92 | });
93 | });
94 |
95 | describe('Rename with variables and filters: rename test/two.txt "{{p}}/{{f|upper}}.{{\'testing-stuff\'|camel}}"', () => {
96 | const oldFiles = ['test/two.txt'];
97 | const newFiles = ['test/TWO.testingStuff'];
98 | beforeAll(async () => {
99 | await runCommand(`rename test/two.txt "{{p}}/{{f|upper}}.{{'testing-stuff'|camel}}"`);
100 | });
101 | test(`Old files don't exist`, async () => {
102 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
103 | await expect(result).toBe(false);
104 | });
105 | test(`New files do exist`, async () => {
106 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
107 | await expect(result).toBe(true);
108 | });
109 | test(`New files has correct case`, async () => {
110 | const files = await fs.readdir('test');
111 | expect(files.indexOf('TWO.testingStuff')).toBeGreaterThan(-1);
112 | });
113 | });
114 |
115 | describe('Force multiple files to be renamed the same: rename test/th* test/same --noindex -force', () => {
116 | const oldFiles = ['test/three.txt', 'test/thirteen.txt'];
117 | const newFiles = ['test/same.txt'];
118 | beforeAll(async () => {
119 | await runCommand('rename test/th* test/same --noindex -force');
120 | });
121 | test(`Old files don't exist`, async () => {
122 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
123 | await expect(result).toBe(false);
124 | });
125 | test(`New files do exist`, async () => {
126 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
127 | await expect(result).toBe(true);
128 | });
129 | test(`New file has correct content`, async () => {
130 | const result = await fs.readFile('test/same.txt', 'utf8');
131 | expect(result).toMatch(/^file three.*/);
132 | });
133 | });
134 |
135 | describe('Multiple files to be renamed the same but with keep option: rename test/six* test/keep --noindex -k', () => {
136 | const oldFiles = ['test/six.txt', 'test/sixteen.txt'];
137 | const newFiles = ['test/keep.txt', 'test/keep-1.txt'];
138 | beforeAll(async () => {
139 | await runCommand('rename test/six* test/keep --noindex -k');
140 | });
141 | test(`Old files don't exist`, async () => {
142 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
143 | await expect(result).toBe(false);
144 | });
145 | test(`New files do exist`, async () => {
146 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
147 | await expect(result).toBe(true);
148 | });
149 | });
150 |
151 | describe('Move a file to a new directory: rename test/one-renamed.txt "test/another-dir/{{os.platform}}"', () => {
152 | const oldFiles = ['test/one-renamed.txt'];
153 | const newFiles = [`test/another-dir/${os.platform()}.txt`];
154 | beforeAll(async () => {
155 | await runCommand('rename test/one-renamed.txt "test/another-dir/{{os.platform}}"');
156 | });
157 | test(`Old files don't exist`, async () => {
158 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
159 | await expect(result).toBe(false);
160 | });
161 | test(`New files do exist`, async () => {
162 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
163 | await expect(result).toBe(true);
164 | });
165 | });
166 |
167 | describe(`Don't move a file to a new directory: rename test/eight.txt "test/another-dir/{{f}}-notmoved" --nomove`, () => {
168 | const oldFiles = ['test/eight.txt'];
169 | const newFiles = ['test/eight-notmoved.txt'];
170 | beforeAll(async () => {
171 | await runCommand('rename test/eight.txt "test/another-dir/{{f}}-notmoved" --nomove');
172 | });
173 | test(`Old files don't exist`, async () => {
174 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
175 | await expect(result).toBe(false);
176 | });
177 | test(`New files do exist`, async () => {
178 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
179 | await expect(result).toBe(true);
180 | });
181 | });
182 |
183 | describe(`Rename multiple files to the same date and append index: rename --nomove test/seven* "{{ date.current | date('yyyy-MM-dd') }}"`, () => {
184 | const now = new Date();
185 | const nowFormatted = `${now.getFullYear()}-${now.getMonth() < 9 ? '0' : ''}${now.getMonth() + 1}-${now.getDate() < 10 ? '0' : ''}${now.getDate()}`;
186 | const oldFiles = ['test/seven.txt', 'test/seventeen.txt'];
187 | const newFiles = [`test/${nowFormatted}1.txt`, `test/${nowFormatted}2.txt`];
188 | beforeAll(async () => {
189 | await runCommand(`rename --nomove test/seven* "{{ date.current | date('yyyy-MM-dd') }}"`);
190 | });
191 | test(`Old files don't exist`, async () => {
192 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
193 | await expect(result).toBe(false);
194 | });
195 | test(`New files do exist`, async () => {
196 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
197 | await expect(result).toBe(true);
198 | });
199 | });
200 |
201 | describe(`Test --noext option: rename test/ten.txt "test/asdf{{os.user}}" --noext`, () => {
202 | const oldFiles = ['test/ten.txt'];
203 | const newFiles = [`test/asdf${os.userInfo().username}`];
204 | beforeAll(async () => {
205 | await runCommand('rename test/ten.txt "test/asdf{{os.user}}" --noext', true);
206 | });
207 | test(`Old files don't exist`, async () => {
208 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
209 | await expect(result).toBe(false);
210 | });
211 | test(`New files do exist`, async () => {
212 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
213 | await expect(result).toBe(true);
214 | });
215 | });
216 |
217 | describe(`Test undo last rename via History class`, () => {
218 | const newFiles = ['test/ten.txt'];
219 | const oldFiles = [`test/asdf${os.userInfo().username}`];
220 | beforeAll(async () => {
221 | let history = new History(SEQUELIZE, 1, false);
222 | await history.getBatches();
223 | await history.undoBatch(0);
224 | });
225 | test(`Old files don't exist`, async () => {
226 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
227 | await expect(result).toBe(false);
228 | });
229 | test(`New files do exist`, async () => {
230 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
231 | await expect(result).toBe(true);
232 | });
233 | });
234 |
235 | describe(`Rename a mp3 file: rename test/music.mp3 --createdirs "test/{{id3.year}}/{{id3.artist}}/{{id3.track|padNumber(2)}} - {{id3.title}}.{{ext}}"`, () => {
236 | const oldFiles = ['test/music.mp3'];
237 | const newFiles = ['test/2019/Scott Holmes/04 - Upbeat Party.mp3'];
238 | beforeAll(async () => {
239 | //await fs.writeFile('test/music.mp3', await download('https://files.freemusicarchive.org/storage-freemusicarchive-org/music/no_curator/Scott_Holmes/Inspiring__Upbeat_Music/Scott_Holmes_-_04_-_Upbeat_Party.mp3'));
240 | await fs.copyFile('test-files/Scott_Holmes_-_04_-_Upbeat_Party.mp3', 'test/music.mp3');
241 | await runCommand('rename test/music.mp3 --createdirs "test/{{id3.year}}/{{id3.artist}}/{{id3.track|padNumber(2)}} - {{id3.title}}{{ext}}"');
242 | });
243 | test(`Old files don't exist`, async () => {
244 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
245 | await expect(result).toBe(false);
246 | });
247 | test(`New files do exist`, async () => {
248 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
249 | await expect(result).toBe(true);
250 | });
251 | });
252 |
253 | describe(`Run a favorited command`, () => {
254 | const oldFiles = ['test/ten.txt'];
255 | const newFiles = [`test/testten.txt`];
256 | let favorite;
257 | beforeAll(async () => {
258 | let command = 'rename --nomove test/ten.txt {{p}}{{f}}';
259 | favorite = SEQUELIZE.models.Favorites.build({ command: JSON.stringify(command.split(' ')), alias: 'test'});
260 | await favorite.save();
261 | await runFavorite('--favorites test');
262 | });
263 | test(`Old files don't exist`, async () => {
264 | const result = await async.every(oldFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
265 | await expect(result).toBe(false);
266 | });
267 | test(`New files do exist`, async () => {
268 | const result = await async.every(newFiles, async (f) => { return await fs.pathExists(path.resolve(f)); });
269 | await expect(result).toBe(true);
270 | });
271 | afterAll(async () => {
272 | if (favorite) await favorite.destroy();
273 | });
274 | });
275 |
276 | // HELPER FUNCTIONS
277 |
278 | async function runCommand(command, undo) {
279 | undo = undo || false;
280 | let argv = yargs.options(yargsOptions).parse(`${command.replace(/^rename /, '')}${!undo ? ' --noundo' : ''}`);
281 | let batch = new Batch(argv, null, SEQUELIZE);
282 | await batch.complete();
283 | }
284 |
285 | async function runFavorite(command) {
286 | let argv = yargs.options(yargsOptions).parse(command.replace(/^rename /, ''));
287 | const options = new Options(argv);
288 | const favorites = new Favorites(SEQUELIZE, options);
289 | await favorites.get();
290 | if (options.favorites) await favorites.run();
291 | }
292 |
293 | /* eslint-disable eqeqeq */
294 | function inWords (num) {
295 | let a = ['','one ','two ','three ','four ', 'five ','six ','seven ','eight ','nine ','ten ','eleven ','twelve ','thirteen ','fourteen ','fifteen ','sixteen ','seventeen ','eighteen ','nineteen '];
296 | let b = ['', '', 'twenty','thirty','forty','fifty', 'sixty','seventy','eighty','ninety'];
297 | if ((num = num.toString()).length > 9) return 'overflow';
298 | let n = ('000000000' + num).substr(-9).match(/^(\d{2})(\d{2})(\d{2})(\d{1})(\d{2})$/);
299 | if (!n) return;
300 | let str = '';
301 | str += (n[1] != 0) ? (a[Number(n[1])] || b[n[1][0]] + ' ' + a[n[1][1]]) + 'crore ' : '';
302 | str += (n[2] != 0) ? (a[Number(n[2])] || b[n[2][0]] + ' ' + a[n[2][1]]) + 'lakh ' : '';
303 | str += (n[3] != 0) ? (a[Number(n[3])] || b[n[3][0]] + ' ' + a[n[3][1]]) + 'thousand ' : '';
304 | str += (n[4] != 0) ? (a[Number(n[4])] || b[n[4][0]] + ' ' + a[n[4][1]]) + 'hundred ' : '';
305 | str += (n[5] != 0) ? ((str != '') ? 'and ' : '') + (a[Number(n[5])] || b[n[5][0]] + ' ' + a[n[5][1]]) : '';
306 | return str;
307 | }
308 |
--------------------------------------------------------------------------------
/chocolatey/packager.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs-extra');
4 | const nunjucks = require('nunjucks');
5 | const packageJson = require('../package.json');
6 |
7 | let results = nunjucks.render('chocolatey/rename-cli.nuspec.html', packageJson);
8 | fs.writeFileSync('chocolatey/rename-cli/rename-cli.nuspec', results, 'utf8');
9 | fs.copyFileSync('bin/rename-cli.exe', 'chocolatey/rename-cli/tools/rname.exe');
10 | fs.copyFileSync('license', 'chocolatey/rename-cli/tools/LICENSE.txt');
--------------------------------------------------------------------------------
/chocolatey/rename-cli.nuspec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |