├── test ├── expected │ ├── foo 2.txt │ ├── foo copy 2.txt │ ├── foo copy 3.txt │ ├── foo copy 4.txt │ ├── foo copy 5.txt │ ├── foo copy 6.txt │ ├── foo copy.txt │ └── foo.txt ├── fixtures │ ├── bar.txt │ ├── foo 2.txt │ ├── one.txt │ ├── qux 2.txt │ ├── qux.txt │ ├── foo (2).txt │ ├── foo 2 2.txt │ ├── foo 22.txt │ ├── foo 3 copy.txt │ ├── foo copy 2.txt │ ├── foo copy 3.txt │ ├── foo copy 4.txt │ ├── foo copy 5.txt │ ├── foo copy 6.txt │ ├── foo copy.txt │ ├── one (copy).txt │ ├── qux (2).txt │ ├── abc (2) - Copy.txt │ ├── abc.txt │ ├── foo.txt │ ├── sub │ │ ├── bar.txt │ │ ├── foo.txt │ │ └── nested │ │ │ ├── bar.txt │ │ │ ├── foo.txt │ │ │ ├── qux.txt │ │ │ ├── foo copy.txt │ │ │ ├── qux (2).txt │ │ │ └── qux 2.txt │ └── abc (2) - Copy - Copy.txt ├── support │ └── index.js ├── ordinals.js ├── windows.js ├── darwin.js └── linux.js ├── .npmrc ├── .gitattributes ├── .travis.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── examples.js ├── package.json ├── .eslintrc.json ├── .verb.md ├── index.js ├── .github └── contributing.md └── README.md /test/expected/foo 2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/bar.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo 2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/one.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/qux 2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/qux.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/expected/foo copy 2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/expected/foo copy 3.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/expected/foo copy 4.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/expected/foo copy 5.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/expected/foo copy 6.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/expected/foo copy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo (2).txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo 2 2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo 22.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo 3 copy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo copy 2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo copy 3.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo copy 4.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo copy 5.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo copy 6.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo copy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/one (copy).txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/qux (2).txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/expected/foo.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/abc (2) - Copy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/abc.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/foo.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/sub/bar.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/sub/foo.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/abc (2) - Copy - Copy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/sub/nested/bar.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/sub/nested/foo.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/sub/nested/qux.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/sub/nested/foo copy.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/sub/nested/qux (2).txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /test/fixtures/sub/nested/qux 2.txt: -------------------------------------------------------------------------------- 1 | content... -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text eol=lf 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | os: 3 | - linux 4 | - osx 5 | - windows 6 | language: node_js 7 | node_js: 8 | - node 9 | - '11' 10 | - '10' 11 | - '9' 12 | - '8' 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | *.DS_Store 3 | .idea 4 | *.sublime-* 5 | 6 | # test related, or directories generated by tests 7 | test/actual 8 | actual 9 | coverage 10 | .nyc* 11 | 12 | # npm 13 | node_modules 14 | npm-debug.log 15 | 16 | # yarn 17 | yarn.lock 18 | yarn-error.log 19 | 20 | # misc 21 | _gh_pages 22 | _draft 23 | _drafts 24 | bower_components 25 | vendor 26 | temp 27 | tmp 28 | TODO.md 29 | package-lock.json -------------------------------------------------------------------------------- /test/support/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const increment = require('../..'); 6 | 7 | const generate = (dir, filenames, options = {}) => { 8 | let { start = 0, number = 1 } = options; 9 | let files = []; 10 | 11 | for (let filename of [].concat(filenames)) { 12 | let basepath = path.join(dir, filename); 13 | 14 | for (let i = start; i < number; i++) { 15 | let file = increment.path(basepath, { ...options, start: i }); 16 | files.push(file); 17 | } 18 | } 19 | 20 | return files; 21 | }; 22 | 23 | console.log(generate(__dirname, 'foo.txt', { start: 1, number: 20, platform: 'win32' })) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present, Jon Schlinkert. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples.js: -------------------------------------------------------------------------------- 1 | const increment = require('.'); 2 | 3 | console.log(increment('foo/bar.txt', { platform: 'darwin' })); //=> foo/bar copy.txt 4 | console.log(increment('foo/bar.txt', { platform: 'linux' })); //=> foo/bar (copy).txt 5 | console.log(increment('foo/bar.txt', { platform: 'win32' })); //=> foo/bar (2).txt 6 | 7 | console.log(increment.path('foo/bar.txt', { platform: 'darwin' })); //=> foo/bar copy.txt 8 | console.log(increment.path('foo/bar.txt', { platform: 'linux' })); //=> foo/bar (copy).txt 9 | console.log(increment.path('foo/bar.txt', { platform: 'win32' })); //=> foo/bar (2).txt 10 | 11 | 12 | console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'darwin' })); 13 | //=> { path: 'foo/bar copy.txt', base: 'bar copy.txt' } 14 | console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'linux' })); 15 | //=> { path: 'foo/bar (copy).txt', base: 'bar (copy).txt' } 16 | console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'win32' })); 17 | //=> { path: 'foo/bar (2).txt', base: 'bar (2).txt' } 18 | 19 | console.log(increment.ordinal(1)); //=> 'st' 20 | console.log(increment.ordinal(2)); //=> 'nd' 21 | console.log(increment.ordinal(3)); //=> 'rd' 22 | console.log(increment.ordinal(110)); //=> 'th' 23 | 24 | console.log(increment.toOrdinal(1)); //=> '1st' 25 | console.log(increment.toOrdinal(2)); //=> '2nd' 26 | console.log(increment.toOrdinal(3)); //=> '3rd' 27 | console.log(increment.toOrdinal(110)); //=> '110th' 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add-filename-increment", 3 | "description": "When copying or moving files, it's common for operating systems to automatically add an increment or 'copy' to duplicate file names. This does that for Node.js applications, with automatic platform detection and support for Linux, MacOs, and Windows conventions.", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/jonschlinkert/add-filename-increment", 6 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)", 7 | "repository": "jonschlinkert/add-filename-increment", 8 | "bugs": { 9 | "url": "https://github.com/jonschlinkert/add-filename-increment/issues" 10 | }, 11 | "license": "MIT", 12 | "files": [ 13 | "index.js" 14 | ], 15 | "main": "index.js", 16 | "engines": { 17 | "node": ">=8" 18 | }, 19 | "scripts": { 20 | "test": "mocha" 21 | }, 22 | "dependencies": { 23 | "strip-filename-increment": "^2.0.1" 24 | }, 25 | "devDependencies": { 26 | "gulp-format-md": "^2.0.0", 27 | "mocha": "^6.2.0" 28 | }, 29 | "keywords": [ 30 | "filename", 31 | "increment" 32 | ], 33 | "verb": { 34 | "toc": false, 35 | "layout": "default", 36 | "tasks": [ 37 | "readme" 38 | ], 39 | "plugins": [ 40 | "gulp-format-md" 41 | ], 42 | "related": { 43 | "list": [ 44 | "micromatch", 45 | "write", 46 | "strip-filename-increment" 47 | ] 48 | }, 49 | "lint": { 50 | "reflinks": true 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /test/ordinals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const assert = require('assert').strict; 5 | const { ordinal, toOrdinal } = require('..'); 6 | 7 | describe('ordinals', () => { 8 | it('should return the ordinal suffix only', () => { 9 | assert.equal(ordinal(0), 'th'); 10 | assert.equal(ordinal(1), 'st'); 11 | assert.equal(ordinal(2), 'nd'); 12 | assert.equal(ordinal(3), 'rd'); 13 | }); 14 | 15 | it('should append zero with "th"', () => { 16 | assert.equal(toOrdinal(0), '0th'); 17 | assert.equal(toOrdinal(-0), '0th'); 18 | }); 19 | 20 | it('should append "st" to numbers ending with 1, accept for 11', () => { 21 | assert.equal(toOrdinal(1), '1st'); 22 | assert.equal(toOrdinal(11), '11th'); 23 | assert.equal(toOrdinal(21), '21st'); 24 | assert.equal(toOrdinal(31), '31st'); 25 | assert.equal(toOrdinal(41), '41st'); 26 | assert.equal(toOrdinal(51), '51st'); 27 | assert.equal(toOrdinal(61), '61st'); 28 | assert.equal(toOrdinal(71), '71st'); 29 | assert.equal(toOrdinal(81), '81st'); 30 | assert.equal(toOrdinal(91), '91st'); 31 | assert.equal(toOrdinal(111), '111th'); 32 | assert.equal(toOrdinal(121), '121st'); 33 | assert.equal(toOrdinal(211), '211th'); 34 | assert.equal(toOrdinal(311), '311th'); 35 | assert.equal(toOrdinal(321), '321st'); 36 | assert.equal(toOrdinal(1111), '1111th'); 37 | assert.equal(toOrdinal(10011), '10011th'); 38 | assert.equal(toOrdinal(10111), '10111th'); 39 | }); 40 | 41 | it('should append "nd" to numbers ending in 2, accept for 12', () => { 42 | assert.equal(toOrdinal(2), '2nd'); 43 | assert.equal(toOrdinal(12), '12th'); 44 | assert.equal(toOrdinal(22), '22nd'); 45 | assert.equal(toOrdinal(32), '32nd'); 46 | assert.equal(toOrdinal(42), '42nd'); 47 | assert.equal(toOrdinal(52), '52nd'); 48 | assert.equal(toOrdinal(62), '62nd'); 49 | assert.equal(toOrdinal(72), '72nd'); 50 | assert.equal(toOrdinal(82), '82nd'); 51 | assert.equal(toOrdinal(92), '92nd'); 52 | assert.equal(toOrdinal(112), '112th'); 53 | assert.equal(toOrdinal(212), '212th'); 54 | assert.equal(toOrdinal(1012), '1012th'); 55 | assert.equal(toOrdinal(10012), '10012th'); 56 | }); 57 | 58 | it('should append "rd" to numbers ending with 3, accept for 13', () => { 59 | assert.equal(toOrdinal('03'), '3rd'); 60 | assert.equal(toOrdinal(3), '3rd'); 61 | assert.equal(toOrdinal(13), '13th'); 62 | assert.equal(toOrdinal(23), '23rd'); 63 | assert.equal(toOrdinal(33), '33rd'); 64 | assert.equal(toOrdinal(43), '43rd'); 65 | assert.equal(toOrdinal(53), '53rd'); 66 | assert.equal(toOrdinal(63), '63rd'); 67 | assert.equal(toOrdinal(73), '73rd'); 68 | assert.equal(toOrdinal(83), '83rd'); 69 | assert.equal(toOrdinal(93), '93rd'); 70 | assert.equal(toOrdinal(103), '103rd'); 71 | assert.equal(toOrdinal(113), '113th'); 72 | assert.equal(toOrdinal(123), '123rd'); 73 | assert.equal(toOrdinal(213), '213th'); 74 | assert.equal(toOrdinal(1013), '1013th'); 75 | assert.equal(toOrdinal(10013), '10013th'); 76 | }); 77 | 78 | it('should work with negative numbers', () => { 79 | assert.equal(toOrdinal(-0), '0th'); 80 | assert.equal(toOrdinal(-1), '-1st'); 81 | assert.equal(toOrdinal(-2), '-2nd'); 82 | assert.equal(toOrdinal(-3), '-3rd'); 83 | }); 84 | 85 | it('should throw a TypeError when NaN', () => { 86 | assert.throws(() => toOrdinal(NaN), TypeError); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/windows.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const path = require('path'); 5 | const assert = require('assert').strict; 6 | const increment = require('..'); 7 | const posix = str => str.replace(/\\/g, '/'); 8 | 9 | const fixtures = (...args) => { 10 | return posix(path.join(__dirname, 'fixtures', ...args)); 11 | }; 12 | const inc = (fp, opts) => { 13 | return posix(increment(fixtures(fp), { ...opts, fs: true, platform: 'windows' })); 14 | }; 15 | 16 | describe('darwin', () => { 17 | it('should not increment the filename when it does not exist', () => { 18 | assert.equal(inc('baz.txt'), fixtures('baz.txt')); 19 | }); 20 | 21 | it('should increment the filename when it exists already', () => { 22 | assert.equal(inc('bar.txt'), fixtures('bar (2).txt')); 23 | assert.equal(inc('sub/foo.txt'), fixtures('sub/foo (2).txt')); 24 | assert.equal(inc('sub/nested/foo.txt'), fixtures('sub/nested/foo (2).txt')); 25 | }); 26 | 27 | it('should strip existing raw numbers and increments before updating increment', () => { 28 | let opts = { strip: true, removeRawNumbers: true }; 29 | assert.equal(inc('foo.txt', opts), fixtures('foo (3).txt')); 30 | assert.equal(inc('foo 2.txt', opts), fixtures('foo (3).txt')); 31 | assert.equal(inc('foo copy.txt', opts), fixtures('foo (3).txt')); 32 | assert.equal(inc('qux 2.txt', opts), fixtures('qux (3).txt')); 33 | assert.equal(inc('abc (2) - Copy.txt', opts), fixtures('abc (2).txt')); 34 | assert.equal(inc('abc (2) - Copy Copy.txt', opts), fixtures('abc (2).txt')); 35 | assert.equal(inc('sub/nested/foo copy.txt', opts), fixtures('sub/nested/foo (2).txt')); 36 | assert.equal(inc('sub/nested/foo copy 2.txt', opts), fixtures('sub/nested/foo (2).txt')); 37 | }); 38 | 39 | it('should strip existing increments before updating increment', () => { 40 | let opts = { strip: true }; 41 | assert.equal(inc('foo.txt', opts), fixtures('foo (3).txt')); 42 | assert.equal(inc('foo 2.txt', opts), fixtures('foo 2 (2).txt')); 43 | assert.equal(inc('foo copy.txt', opts), fixtures('foo (3).txt')); 44 | assert.equal(inc('qux 2.txt', opts), fixtures('qux 2 (2).txt')); 45 | assert.equal(inc('abc (2) - Copy.txt', opts), fixtures('abc (2).txt')); 46 | assert.equal(inc('abc (2) - Copy Copy.txt', opts), fixtures('abc (2).txt')); 47 | assert.equal(inc('sub/nested/foo copy.txt', opts), fixtures('sub/nested/foo (2).txt')); 48 | assert.equal(inc('sub/nested/foo copy 2.txt', opts), fixtures('sub/nested/foo (2).txt')); 49 | }); 50 | 51 | it('should start at the given number, or the next number that does not exist', () => { 52 | assert.equal(inc('foo.txt', { start: 1 }), fixtures('foo (3).txt')); 53 | assert.equal(inc('foo.txt', { start: 2 }), fixtures('foo (3).txt')); 54 | assert.equal(inc('foo.txt', { start: 3 }), fixtures('foo (3).txt')); 55 | assert.equal(inc('foo.txt', { start: 4 }), fixtures('foo (4).txt')); 56 | assert.equal(inc('foo.txt', { start: 5 }), fixtures('foo (5).txt')); 57 | assert.equal(inc('foo.txt', { start: 6 }), fixtures('foo (6).txt')); 58 | assert.equal(inc('foo.txt', { start: 7 }), fixtures('foo (7).txt')); 59 | assert.equal(inc('foo.txt', { start: 101 }), fixtures('foo (101).txt')); 60 | assert.equal(inc('foo.txt', { start: 102 }), fixtures('foo (102).txt')); 61 | }); 62 | 63 | it('should not strip increments when disabled', () => { 64 | let opts = { stripIncrement: false }; 65 | assert.equal(inc('foo.txt', opts), fixtures('foo (3).txt')); 66 | assert.equal(inc('foo 2.txt', opts), fixtures('foo 2 (2).txt')); 67 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy (2).txt')); 68 | }); 69 | 70 | it('should use a custom function to increment the file name', () => { 71 | let opts = { 72 | increment(stem, n) { 73 | return stem.replace(/\s\d+$/, '') + ' copy ' + (n + 1); 74 | } 75 | }; 76 | 77 | assert.equal(inc('foo.txt', opts), fixtures('foo copy 7.txt')); 78 | assert.equal(inc('foo 2.txt', opts), fixtures('foo copy 7.txt')); 79 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy copy 2.txt')); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/darwin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const path = require('path'); 5 | const assert = require('assert').strict; 6 | const increment = require('..'); 7 | const posix = str => str.replace(/\\/g, '/'); 8 | 9 | const fixtures = (...args) => { 10 | return posix(path.join(__dirname, 'fixtures', ...args)); 11 | }; 12 | const inc = (fp, opts) => { 13 | return posix(increment(fixtures(fp), { ...opts, fs: true, platform: 'darwin' })); 14 | }; 15 | 16 | describe('darwin', () => { 17 | it('should not increment the filename when it does not exist', () => { 18 | assert.equal(inc('baz.txt'), fixtures('baz.txt')); 19 | }); 20 | 21 | it('should increment the filename when it exists already', () => { 22 | assert.equal(inc('bar.txt'), fixtures('bar copy.txt')); 23 | assert.equal(inc('sub/foo.txt'), fixtures('sub/foo copy.txt')); 24 | assert.equal(inc('sub/nested/foo.txt'), fixtures('sub/nested/foo copy 2.txt')); 25 | }); 26 | 27 | it('should strip existing increments and raw numbers before updating increment', () => { 28 | const opts = { strip: true, removeRawNumbers: true }; 29 | assert.equal(inc('foo.txt', opts), fixtures('foo copy 7.txt')); 30 | assert.equal(inc('foo 2.txt', opts), fixtures('foo copy 7.txt')); 31 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy 7.txt')); 32 | assert.equal(inc('qux 2.txt', opts), fixtures('qux copy.txt')); 33 | assert.equal(inc('abc (2) - Copy.txt', opts), fixtures('abc copy.txt')); 34 | assert.equal(inc('abc (2) - Copy Copy.txt', opts), fixtures('abc copy.txt')); 35 | assert.equal(inc('sub/nested/foo copy.txt', opts), fixtures('sub/nested/foo copy 2.txt')); 36 | assert.equal(inc('sub/nested/foo copy 2.txt', opts), fixtures('sub/nested/foo copy 2.txt')); 37 | }); 38 | 39 | it('should strip existing increments before updating increment', () => { 40 | const opts = { strip: true }; 41 | assert.equal(inc('foo.txt', opts), fixtures('foo copy 7.txt')); 42 | assert.equal(inc('foo 2.txt', opts), fixtures('foo 2 copy.txt')); 43 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy 7.txt')); 44 | assert.equal(inc('qux 2.txt', opts), fixtures('qux 2 copy.txt')); 45 | assert.equal(inc('abc (2) - Copy.txt', opts), fixtures('abc copy.txt')); 46 | assert.equal(inc('abc (2) - Copy Copy.txt', opts), fixtures('abc copy.txt')); 47 | assert.equal(inc('sub/nested/foo copy.txt', opts), fixtures('sub/nested/foo copy 2.txt')); 48 | assert.equal(inc('sub/nested/foo copy 2.txt', opts), fixtures('sub/nested/foo copy 2.txt')); 49 | }); 50 | 51 | it('should start at the given number', () => { 52 | // fixtures exist up to "6", so "7" is the lowest number we should see. 53 | assert.equal(inc('foo.txt', { start: 1 }), fixtures('foo copy 7.txt')); 54 | assert.equal(inc('foo.txt', { start: 2 }), fixtures('foo copy 7.txt')); 55 | assert.equal(inc('foo.txt', { start: 3 }), fixtures('foo copy 7.txt')); 56 | assert.equal(inc('foo.txt', { start: 4 }), fixtures('foo copy 7.txt')); 57 | assert.equal(inc('foo.txt', { start: 5 }), fixtures('foo copy 7.txt')); 58 | assert.equal(inc('foo.txt', { start: 6 }), fixtures('foo copy 7.txt')); 59 | assert.equal(inc('foo.txt', { start: 7 }), fixtures('foo copy 7.txt')); 60 | assert.equal(inc('foo.txt', { start: 8 }), fixtures('foo copy 8.txt')); 61 | assert.equal(inc('foo.txt', { start: 101 }), fixtures('foo copy 101.txt')); 62 | }); 63 | 64 | it('should not strip increments when disabled', () => { 65 | let opts = { strip: false }; 66 | assert.equal(inc('foo.txt', opts), fixtures('foo copy 7.txt')); 67 | assert.equal(inc('foo 2.txt', opts), fixtures('foo 2 copy.txt')); 68 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy copy.txt')); 69 | }); 70 | 71 | it('should use a custom function to increment the file name', () => { 72 | let opts = { 73 | increment(stem, n) { 74 | return stem.replace(/(\s+copy\s*|\s\d+)$/, '') + ' copy ' + (n + 1); 75 | } 76 | }; 77 | 78 | assert.equal(inc('foo.txt', opts), fixtures('foo copy 7.txt')); 79 | assert.equal(inc('foo 2.txt', opts), fixtures('foo copy 7.txt')); 80 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy 7.txt')); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "browser": false, 8 | "es6": true, 9 | "node": true, 10 | "mocha": true 11 | }, 12 | 13 | "parserOptions":{ 14 | "ecmaVersion": 9, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "modules": true, 18 | "experimentalObjectRestSpread": true 19 | } 20 | }, 21 | 22 | "globals": { 23 | "document": false, 24 | "navigator": false, 25 | "window": false 26 | }, 27 | 28 | "rules": { 29 | "accessor-pairs": 2, 30 | "arrow-spacing": [2, { "before": true, "after": true }], 31 | "block-spacing": [2, "always"], 32 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 33 | "comma-dangle": [2, "never"], 34 | "comma-spacing": [2, { "before": false, "after": true }], 35 | "comma-style": [2, "last"], 36 | "constructor-super": 2, 37 | "curly": [2, "multi-line"], 38 | "dot-location": [2, "property"], 39 | "eol-last": 2, 40 | "eqeqeq": [2, "allow-null"], 41 | "generator-star-spacing": [2, { "before": true, "after": true }], 42 | "handle-callback-err": [2, "^(err|error)$" ], 43 | "indent": [2, 2, { "SwitchCase": 1 }], 44 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 45 | "keyword-spacing": [2, { "before": true, "after": true }], 46 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 47 | "new-parens": 2, 48 | "no-array-constructor": 2, 49 | "no-caller": 2, 50 | "no-class-assign": 2, 51 | "no-cond-assign": 2, 52 | "no-const-assign": 2, 53 | "no-control-regex": 2, 54 | "no-debugger": 2, 55 | "no-delete-var": 2, 56 | "no-dupe-args": 2, 57 | "no-dupe-class-members": 2, 58 | "no-dupe-keys": 2, 59 | "no-duplicate-case": 2, 60 | "no-empty-character-class": 2, 61 | "no-eval": 2, 62 | "no-ex-assign": 2, 63 | "no-extend-native": 2, 64 | "no-extra-bind": 2, 65 | "no-extra-boolean-cast": 2, 66 | "no-extra-parens": [2, "functions"], 67 | "no-fallthrough": 2, 68 | "no-floating-decimal": 2, 69 | "no-func-assign": 2, 70 | "no-implied-eval": 2, 71 | "no-inner-declarations": [2, "functions"], 72 | "no-invalid-regexp": 2, 73 | "no-irregular-whitespace": 2, 74 | "no-iterator": 2, 75 | "no-label-var": 2, 76 | "no-labels": 2, 77 | "no-lone-blocks": 2, 78 | "no-mixed-spaces-and-tabs": 2, 79 | "no-multi-spaces": 2, 80 | "no-multi-str": 2, 81 | "no-multiple-empty-lines": [2, { "max": 1 }], 82 | "no-native-reassign": 0, 83 | "no-negated-in-lhs": 2, 84 | "no-new": 2, 85 | "no-new-func": 2, 86 | "no-new-object": 2, 87 | "no-new-require": 2, 88 | "no-new-wrappers": 2, 89 | "no-obj-calls": 2, 90 | "no-octal": 2, 91 | "no-octal-escape": 2, 92 | "no-proto": 0, 93 | "no-redeclare": 2, 94 | "no-regex-spaces": 2, 95 | "no-return-assign": 2, 96 | "no-self-compare": 2, 97 | "no-sequences": 2, 98 | "no-shadow-restricted-names": 2, 99 | "no-spaced-func": 2, 100 | "no-sparse-arrays": 2, 101 | "no-this-before-super": 2, 102 | "no-throw-literal": 2, 103 | "no-trailing-spaces": 0, 104 | "no-undef": 2, 105 | "no-undef-init": 2, 106 | "no-unexpected-multiline": 2, 107 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 108 | "no-unreachable": 2, 109 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 110 | "no-useless-call": 0, 111 | "no-with": 2, 112 | "one-var": [0, { "initialized": "never" }], 113 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 114 | "padded-blocks": [0, "never"], 115 | "quotes": [2, "single", "avoid-escape"], 116 | "radix": 2, 117 | "semi": [2, "always"], 118 | "semi-spacing": [2, { "before": false, "after": true }], 119 | "space-before-blocks": [2, "always"], 120 | "space-before-function-paren": [2, "never"], 121 | "space-in-parens": [2, "never"], 122 | "space-infix-ops": 2, 123 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 124 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 125 | "use-isnan": 2, 126 | "valid-typeof": 2, 127 | "wrap-iife": [2, "any"], 128 | "yoda": [2, "never"] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | ## What does this do? 2 | 3 | When copying files, it's common for operating systems to append a numerical increment or the word 'copy' to a file name to prevent the existing file from being overwritten. 4 | 5 | This library allows you to do the same thing in your Node.js application, using the correct conventions for the most commonly used [operating systems](#operating-systems). 6 | 7 | ## Usage 8 | 9 | All methods automatically detect the platform to use, unless `platform` is defined on the [options](#options). 10 | 11 | ```js 12 | const increment = require('{%= name %}'); 13 | ``` 14 | 15 | ## API 16 | {%= apidocs("index.js") %} 17 | 18 | 19 | ## Options 20 | 21 | ### options.fs 22 | 23 | **Description**: Check the file system, and automatically increment the file based on existing files. Thus, if the file name is `foo.txt`, and `foo (2).txt` already exists, the file will automatically be renamed to `foo (3).txt`. 24 | 25 | Also uses the correct conventions for Linux, Windows (win32), and MacOS (darwin). 26 | 27 | **Type**: `boolean` 28 | 29 | **Default**: `undefined` 30 | 31 | 32 | ### options.increment 33 | 34 | **Description**: Custom function to handling incrementing a file name. This is mostly useful when `options.fs` is also defined, since this function will only be called if a file name needs to be incremented, allowing you to control how incrementing is done. 35 | 36 | **Type**: `function` 37 | 38 | **Default**: `undefined` 39 | 40 | 41 | ### options.platform 42 | 43 | **Description**: Specify the platform conventions to use. 44 | 45 | **Type**: `string` 46 | 47 | **Default**: Uses `process.platform`. Valid values are `linux`, `win32`, and `darwin`. 48 | 49 | 50 | ## Operating Systems 51 | 52 | - [Linux](#linux) 53 | - [MacOS](#macos) 54 | - [Windows](#windows) 55 | 56 | **Supported Operating Systems** 57 | 58 | Currently Windows, Darwin (MacOS), and Linux are supported. This library attempts to automatically use the correct conventions for each operating system. Please [create an issue](../../issues/new) if you ecounter a bug. 59 | 60 | If you use an operating system with different conventions, and you would like for this library to add support, please [create an issue](../../issues/new) with a detailed description of those conventions, or feel free to do a [pull request](.github/contributing.md). 61 | 62 | ### Linux 63 | 64 | When a file is copied or moved, and the destination file path already exists, Linux uses the following conventions for incrementing the file name. 65 | 66 | | **Source path** | **Destination path** | **Type** | **Directory1** | 67 | | --- | --- | --- | --- | 68 | | `foo.txt` | `foo (copy).txt`, `foo (another copy).txt`, `foo (3rd copy).txt`, ... | file | Same directory as source | 69 | | `foo` | `foo (copy)`, `foo (another copy)`, `foo (3rd copy)`, ... | directory | Same directory as source | 70 | 71 | 1 _On Linux, when a file or folder is copied or moved to a different directory and another file or folder with the same name exists in that directory, you are prompted to choose a new name for the file or folder, or to cancel or skip the operation._ 72 | 73 | ### MacOS 74 | 75 | When a file is copied or moved, and the destination file path already exists, MacOS uses the following conventions for incrementing the file name. 76 | 77 | | **Source path** | **Destination path** | **Type** | **Directory1** | 78 | | --- | --- | --- | --- | 79 | | `foo.txt` | `foo copy.txt`, `foo copy 2.txt`, ... | file | Same directory as source | 80 | | `foo.txt` | `foo 2.txt`, `foo 3.txt`, ... | file | Different directory than source | 81 | | `foo` | `foo copy`, `foo copy 2`, ... | directory | Same directory as source | 82 | 83 | 1 _MacOS uses different conventions for incrementing file names when the source file is copied, moved or renamed to a different directory, versus when the file is copied into the same directory._ 84 | 85 | ### Windows 86 | 87 | When a file is copied or moved, and the destination file path already exists, Windows uses the following conventions for incrementing the file name. 88 | 89 | | **Source path** | **Destination path** | **Type** | **Directory1** | 90 | | --- | --- | --- | --- | 91 | | `foo.txt` | `foo - Copy.txt` | file | Same directory as source | 92 | | `foo.txt` | `foo (2).txt` | file | Different directory than source | 93 | | `foo (2).txt` | `foo (3).txt` | file | Different directory than source | 94 | | `foo` | `foo - Copy` | directory | Same directory as source | 95 | | `foo - Copy` | `foo - Copy (2)` | directory | Same directory as source | 96 | 97 | 1 _Windows uses different conventions for incrementing file names when the source file is copied, moved or renamed to a different directory, versus when the file is copied into the same directory. Also, when a folder is copied to a new directory, and the new directory already has a folder with the same name, Windows just merges the folders automatically._ 98 | -------------------------------------------------------------------------------- /test/linux.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | const path = require('path'); 5 | const assert = require('assert').strict; 6 | const increment = require('..'); 7 | const posix = str => str.replace(/\\/g, '/'); 8 | 9 | const fixtures = (...args) => { 10 | return posix(path.join(__dirname, 'fixtures', ...args)); 11 | }; 12 | const inc = (fp, opts) => { 13 | return posix(increment(fixtures(fp), { ...opts, fs: true, platform: 'linux' })); 14 | }; 15 | 16 | describe('linux', () => { 17 | it('should not increment the filename when it does not exist', () => { 18 | assert.equal(inc('baz.txt'), fixtures('baz.txt')); 19 | }); 20 | 21 | it('should increment the filename when it exists already', () => { 22 | assert.equal(inc('bar.txt'), fixtures('bar (copy).txt')); 23 | assert.equal(inc('sub/foo.txt'), fixtures('sub/foo (copy).txt')); 24 | assert.equal(inc('sub/nested/foo.txt'), fixtures('sub/nested/foo (copy).txt')); 25 | }); 26 | 27 | it('should strip existing increments and raw numbers before updating increment', () => { 28 | let opts = { strip: true, removeRawNumbers: true }; 29 | assert.equal(inc('foo.txt', opts), fixtures('foo (copy).txt')); 30 | assert.equal(inc('foo 2.txt', opts), fixtures('foo (copy).txt')); 31 | assert.equal(inc('foo copy.txt', opts), fixtures('foo (copy).txt')); 32 | assert.equal(inc('one (copy).txt', opts), fixtures('one (another copy).txt')); 33 | assert.equal(inc('qux 2.txt', opts), fixtures('qux (copy).txt')); 34 | assert.equal(inc('abc (2) - Copy.txt', opts), fixtures('abc (copy).txt')); 35 | assert.equal(inc('abc (2) - Copy Copy.txt', opts), fixtures('abc (copy).txt')); 36 | assert.equal(inc('sub/nested/foo copy.txt', opts), fixtures('sub/nested/foo (copy).txt')); 37 | assert.equal(inc('sub/nested/foo copy 2.txt', opts), fixtures('sub/nested/foo (copy).txt')); 38 | }); 39 | 40 | it('should strip existing increments before updating increment', () => { 41 | let opts = { strip: true }; 42 | assert.equal(inc('foo.txt', opts), fixtures('foo (copy).txt')); 43 | assert.equal(inc('foo 2.txt', opts), fixtures('foo 2 (copy).txt')); 44 | assert.equal(inc('foo copy.txt', opts), fixtures('foo (copy).txt')); 45 | assert.equal(inc('one (copy).txt', opts), fixtures('one (another copy).txt')); 46 | assert.equal(inc('qux 2.txt', opts), fixtures('qux 2 (copy).txt')); 47 | assert.equal(inc('abc (2) - Copy.txt', opts), fixtures('abc (copy).txt')); 48 | assert.equal(inc('abc (2) - Copy Copy.txt', opts), fixtures('abc (copy).txt')); 49 | assert.equal(inc('sub/nested/foo copy.txt', opts), fixtures('sub/nested/foo (copy).txt')); 50 | assert.equal(inc('sub/nested/foo copy 2.txt', opts), fixtures('sub/nested/foo (copy).txt')); 51 | }); 52 | 53 | it('should start at the given number, or the next number that does not exist', () => { 54 | assert.equal(inc('baz.txt', { start: 0 }), fixtures('baz.txt'), 'baz does not exist'); 55 | assert.equal(inc('bar.txt', { start: 0 }), fixtures('bar (copy).txt'), 'bar is in fixtures'); 56 | assert.equal(inc('foo.txt', { start: 2 }), fixtures('foo (another copy).txt'), 'foo is in fixtures'); 57 | assert.equal(inc('foo.txt', { start: 3 }), fixtures('foo (3rd copy).txt')); 58 | assert.equal(inc('foo.txt', { start: 4 }), fixtures('foo (4th copy).txt')); 59 | assert.equal(inc('foo.txt', { start: 5 }), fixtures('foo (5th copy).txt')); 60 | assert.equal(inc('foo.txt', { start: 6 }), fixtures('foo (6th copy).txt')); 61 | assert.equal(inc('foo.txt', { start: 7 }), fixtures('foo (7th copy).txt')); 62 | assert.equal(inc('foo.txt', { start: 8 }), fixtures('foo (8th copy).txt')); 63 | assert.equal(inc('foo.txt', { start: 9 }), fixtures('foo (9th copy).txt')); 64 | assert.equal(inc('foo.txt', { start: 10 }), fixtures('foo (10th copy).txt')); 65 | assert.equal(inc('foo.txt', { start: 11 }), fixtures('foo (11th copy).txt')); 66 | assert.equal(inc('foo.txt', { start: 12 }), fixtures('foo (12th copy).txt')); 67 | assert.equal(inc('foo.txt', { start: 13 }), fixtures('foo (13th copy).txt')); 68 | assert.equal(inc('foo.txt', { start: 14 }), fixtures('foo (14th copy).txt')); 69 | assert.equal(inc('foo.txt', { start: 112 }), fixtures('foo (112th copy).txt')); 70 | assert.equal(inc('foo.txt', { start: 1112 }), fixtures('foo (1112th copy).txt')); 71 | assert.equal(inc('foo.txt', { start: 22 }), fixtures('foo (22nd copy).txt')); 72 | assert.equal(inc('foo.txt', { start: 122 }), fixtures('foo (122nd copy).txt')); 73 | assert.equal(inc('foo.txt', { start: 1122 }), fixtures('foo (1122nd copy).txt')); 74 | assert.equal(inc('foo.txt', { start: 102 }), fixtures('foo (102nd copy).txt')); 75 | assert.equal(inc('foo.txt', { start: 103 }), fixtures('foo (103rd copy).txt')); 76 | }); 77 | 78 | it('should not strip increments when disabled', () => { 79 | let opts = { stripIncrement: false }; 80 | assert.equal(inc('foo.txt', opts), fixtures('foo (copy).txt')); 81 | assert.equal(inc('foo 2.txt', opts), fixtures('foo 2 (copy).txt')); 82 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy (copy).txt')); 83 | }); 84 | 85 | it('should use a custom function to increment the file name', () => { 86 | let opts = { 87 | increment(stem, n) { 88 | return stem.replace(/\s\d+$/, '') + ' [copy ' + (n + 1) + ']'; 89 | } 90 | }; 91 | 92 | assert.equal(inc('foo.txt', opts), fixtures('foo [copy 2].txt')); 93 | assert.equal(inc('foo 2.txt', opts), fixtures('foo [copy 2].txt')); 94 | assert.equal(inc('foo copy.txt', opts), fixtures('foo copy [copy 2].txt')); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const strip = require('strip-filename-increment'); 6 | const ordinals = ['th', 'st', 'nd', 'rd']; 7 | 8 | const ordinal = n => { 9 | if (isNaN(n)) { 10 | throw new TypeError('expected a number'); 11 | } 12 | return ordinals[((n % 100) - 20) % 10] || ordinals[n % 100] || ordinals[0]; 13 | }; 14 | 15 | const toOrdinal = number => { 16 | return `${Number(number)}${ordinal(Math.abs(number))}`; 17 | }; 18 | 19 | const format = { 20 | darwin(stem, n) { 21 | if (n === 1) return `${stem} copy`; 22 | if (n > 1) return `${stem} copy ${n}`; 23 | return stem; 24 | }, 25 | default: (stem, n) => n > 1 ? `${stem} (${n})` : stem, 26 | win32: (stem, n) => n > 1 ? `${stem} (${n})` : stem, 27 | windows: (stem, n) => format.win32(stem, n), 28 | linux(stem, n) { 29 | if (n === 0) return stem; 30 | if (n === 1) return `${stem} (copy)`; 31 | if (n === 2) return `${stem} (another copy)`; 32 | return `${stem} (${toOrdinal(n)} copy)`; 33 | } 34 | }; 35 | 36 | /** 37 | * The main export is a function that adds a trailing increment to 38 | * the `stem` (basename without extension) of the given file path or object. 39 | * ```js 40 | * console.log(increment('foo/bar.txt', { platform: 'darwin' })); 41 | * //=> foo/bar copy.txt 42 | * console.log(increment('foo/bar.txt', { platform: 'linux' })); 43 | * //=> foo/bar (copy).txt 44 | * console.log(increment('foo/bar.txt', { platform: 'win32' })); 45 | * //=> foo/bar (2).txt 46 | * ``` 47 | * @name increment 48 | * @param {String|Object} `file` If the file is an object, it must have a `path` property. 49 | * @param {Object} `options` See [available options](#options). 50 | * @return {String|Object} Returns a file of the same type that was given, with an increment added to the file name. 51 | * @api public 52 | */ 53 | 54 | const increment = (...args) => { 55 | return typeof args[0] === 'string' ? increment.path(...args) : increment.file(...args); 56 | }; 57 | 58 | /** 59 | * Add a trailing increment to the given `filepath`. 60 | * 61 | * ```js 62 | * console.log(increment.path('foo/bar.txt', { platform: 'darwin' })); 63 | * //=> foo/bar copy.txt 64 | * console.log(increment.path('foo/bar.txt', { platform: 'linux' })); 65 | * //=> foo/bar (copy).txt 66 | * console.log(increment.path('foo/bar.txt', { platform: 'win32' })); 67 | * //=> foo/bar (2).txt 68 | * ``` 69 | * @name .path 70 | * @param {String} `filepath` 71 | * @param {Object} `options` See [available options](#options). 72 | * @return {String} 73 | * @api public 74 | */ 75 | 76 | increment.path = (filepath, options = {}) => { 77 | return path.format(increment.file(path.parse(filepath), options)); 78 | }; 79 | 80 | /** 81 | * Add a trailing increment to the `file.base` of the given file object. 82 | * 83 | * ```js 84 | * console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'darwin' })); 85 | * //=> { path: 'foo/bar copy.txt', base: 'bar copy.txt' } 86 | * console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'linux' })); 87 | * //=> { path: 'foo/bar (copy).txt', base: 'bar (copy).txt' } 88 | * console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'win32' })); 89 | * //=> { path: 'foo/bar (2).txt', base: 'bar (2).txt' } 90 | * ``` 91 | * @name .file 92 | * @param {String|Object} `file` If passed as a string, the path will be parsed to create an object using `path.parse()`. 93 | * @param {Object} `options` See [available options](#options). 94 | * @return {Object} Returns an object. 95 | * @api public 96 | */ 97 | 98 | increment.file = (file, options = {}) => { 99 | if (typeof file === 'string') { 100 | let temp = file; 101 | file = path.parse(file); 102 | file.path = temp; 103 | } 104 | 105 | file = { ...file }; 106 | 107 | if (file.path && Object.keys(file).length === 1) { 108 | let temp = file.path; 109 | file = path.parse(file.path); 110 | file.path = temp; 111 | } 112 | 113 | if (file.dirname && !file.dir) file.dir = file.dirname; 114 | if (file.basename && !file.base) file.base = file.basename; 115 | if (file.extname && !file.ext) file.ext = file.extname; 116 | if (file.stem && !file.name) file.name = file.stem; 117 | 118 | let { start = 1, platform = process.platform } = options; 119 | let fn = options.increment || format[platform] || format.default; 120 | 121 | if (start === 1 && (platform === 'win32' || platform === 'windows')) { 122 | if (!options.increment) { 123 | start++; 124 | } 125 | } 126 | 127 | if (options.strip === true) { 128 | file.name = strip.increment(file.name, options); 129 | file.dir = strip.increment(file.dir, options); 130 | file.base = file.name + file.ext; 131 | } 132 | 133 | if (options.fs === true) { 134 | let name = file.name; 135 | let dest = path.format(file); 136 | 137 | while (fs.existsSync(dest)) { 138 | file.base = fn(name, start++) + file.ext; 139 | dest = path.format(file); 140 | } 141 | } else { 142 | file.base = fn(file.name, start) + file.ext; 143 | } 144 | 145 | file.path = path.join(file.dir, file.base); 146 | return file; 147 | }; 148 | 149 | /** 150 | * Returns an ordinal-suffix for the given number. This is used 151 | * when creating increments for files on Linux. 152 | * 153 | * ```js 154 | * const { ordinal } = require('add-filename-increment'); 155 | * console.log(ordinal(1)); //=> 'st' 156 | * console.log(ordinal(2)); //=> 'nd' 157 | * console.log(ordinal(3)); //=> 'rd' 158 | * console.log(ordinal(110)); //=> 'th' 159 | * ``` 160 | * @name .ordinal 161 | * @param {Number} `num` 162 | * @return {String} 163 | * @api public 164 | */ 165 | 166 | increment.ordinal = ordinal; 167 | 168 | /** 169 | * Returns an ordinal for the given number. 170 | * 171 | * ```js 172 | * const { toOrdinal } = require('add-filename-increment'); 173 | * console.log(toOrdinal(1)); //=> '1st' 174 | * console.log(toOrdinal(2)); //=> '2nd' 175 | * console.log(toOrdinal(3)); //=> '3rd' 176 | * console.log(toOrdinal(110)); //=> '110th' 177 | * ``` 178 | * @name .toOrdinal 179 | * @param {Number} `num` 180 | * @return {String} 181 | * @api public 182 | */ 183 | 184 | increment.toOrdinal = toOrdinal; 185 | module.exports = increment; 186 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to increment-filename 2 | 3 | First and foremost, thank you! We appreciate that you want to contribute to `increment-filename`, your time is valuable, and your contributions mean a lot to us! 4 | 5 | 6 | 7 | Please keep in mind that increment-filename is Free and Open-Source Software, build by people like you who spend their free time creating and maintaing tools for the rest of the community to use. 8 | 9 | 10 | ## Important! 11 | 12 | By contributing to this project, you agree that: 13 | 14 | 1. _You have authored 100% of the content_ 15 | 1. _You have the necessary rights to the content_ 16 | 1. _You have received the necessary permissions from your employer to make the contributions (if applicable)_ 17 | 1. _The content you contribute may be provided under the Project license(s)_ 18 | 19 | 20 | ## Why you should contribute 21 | 22 | Please consider contributing to this project if any of the following is true: 23 | 24 | - You have (or want to gain) expertise in community development, communication, or education 25 | - You want open source communities to be more collaborative and inclusive 26 | - You want to help lower the burden to first time contributors 27 | - You know how to improve the quality of this project or its documentation 28 | 29 | 30 | ## Getting started 31 | 32 | **What does "contributing" mean?** 33 | 34 | Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following: 35 | 36 | - Adding, updating or correcting documentation 37 | - Significantly improving code performance, quality or readability 38 | - Making feature requests on the [issue tracker](../../issues) 39 | - Submiting a [bug report](../../issues) 40 | - Star the project on Github 41 | - Tweet a simple "thank you" to the project's author (`twitter.com/@jonschlinkert`) 42 | 43 | **Showing support for increment-filename** 44 | 45 | Don't have time to contribute? No worries! There are other ways to show your :heart: and support (and motivate the current maintainers to keep up the great work!): 46 | 47 | - star the [project](https://github.com/jonschlinkert/increment-filename) 48 | - follow the [project's author on GitHub](https://github.com/jonschlinkert) 49 | - follow the [project's author on twitter](https://twitter.com/jonschlinkert) 50 | - connect with the [project's author on LinkedIn](https://twitter.com/jonschlinkert) 51 | - tweet your support! `thanks for creating https://github.com/jonschlinkert/increment-filename @jonschlinkert! #nodejs #github #javascript` 52 | 53 | If you'd like to learn more about contributing in general, please see the [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing). 54 | 55 | ## Issues 56 | 57 | ### Before creating an issue 58 | 59 | **TLDR;** 60 | 61 | - Make sure you're using the latest version of `increment-filename` 62 | - Make sure the issue is in this library, and not a dependency 63 | - Search google, [GitHub]() and [stackoverflow.com]() for solutions 64 | - Search existing issues, open and closed, for a solution before creating a new issue 65 | - Use [stackoverflow.com][so] if you need implementation help or have general questions 66 | 67 | **Make sure you are using the latest version** 68 | 69 | It's common for users to create issues before checking the version they're using, only to find that their problem has been solved in a recently published version. 70 | 71 | **Create the issue in the appropriate repository** 72 | 73 | Please try to determine if the issue is caused by an underlying library, and if so, create the issue there. Sometimes this is difficult to know. We only ask that you give a reasonable attempt to find out first. 74 | 75 | **Investigate the issue** 76 | 77 | Check to see if any existing issues (open or closed) have been created about the same thing, or something similar. 78 | 79 | If you find an open issue about the same or similar problem, and you can shed light on the problem, please add a comment to the existing issue (pull requests are also encouraged and very much appreciated). 80 | 81 | **Avoid creating issues for implementation help** 82 | 83 | Help us keep the issue tracker focused on bugs and feature requests by asking implementation-related questions on [stackoverflow.com][so]. StackOverflow was created for this purpose, and both you and the person that answers your question will get reputation points for answering the question. 84 | 85 | **Avoid creating issues to bump dependency versions** 86 | 87 | This is a time-wasting 88 | 89 | 90 | - **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue. 91 | 92 | Please check the readme to see if there is additional advice about creating issues. 93 | 94 | ### Creating an issue 95 | 96 | Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue: 97 | 98 | - **operating system**: what OS are you using? 99 | - **version**: what version of `increment-filename` are you using? 100 | - **node version**: what version of `node.js` are you using? 101 | - **extensions (plugins, helpers, middleware etc)**: if applicable, please list any extensions you're using. 102 | - **error messages**: please paste any error messages, with as much detail as possible, into the issue itself or a [gist](https://gist.github.com/) if you prefer. 103 | 104 | 105 | ### Closing issues 106 | 107 | The original poster or the maintainer's of `increment-filename` may close an issue at any time. Typically, but not exclusively, issues are closed when: 108 | 109 | - The issue is resolved 110 | - The project's maintainers have determined the issue is out of scope 111 | - An issue is clearly a duplicate of another issue, in which case the duplicate issue will be linked. 112 | - A discussion has clearly run its course 113 | 114 | 115 | ## Next steps 116 | 117 | **Tips for creating idiomatic issues** 118 | 119 | Spending just a little extra time to review best practices and brush up on your contributing skills will, at minimum, make your issue easier to read, easier to resolve, and more likely to be found by others who have the same or similar issue in the future. At best, it will open up doors and potential career opportunities by helping you be at your best. 120 | 121 | The following resources were hand-picked to help you be the most effective contributor you can be: 122 | 123 | - The [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) is a great place for newcomers to start, but there is also information for experienced contributors there. 124 | - Take some time to learn basic markdown. We can't stress this enough. Don't start pasting code into GitHub issues before you've taken a moment to review this [markdown cheatsheet](https://gist.github.com/jonschlinkert/5854601) 125 | - The GitHub guide to [basic markdown](https://help.github.com/articles/markdown-basics/) is another great markdown resource. 126 | - Learn about [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). And if you want to really go above and beyond, read [mastering markdown](https://guides.github.com/features/mastering-markdown/). 127 | 128 | At the very least, please try to: 129 | 130 | - Use backticks to wrap code. This ensures that it retains its formatting and isn't modified when it's rendered by GitHub, and makes the code more readable to others 131 | - When applicable, use syntax highlighting by adding the correct language name after the first "code fence" 132 | 133 | 134 | [so]: http://stackoverflow.com/questions/tagged/increment-filename 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # add-filename-increment [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=W8YFZ425KND68) [![NPM version](https://img.shields.io/npm/v/add-filename-increment.svg?style=flat)](https://www.npmjs.com/package/add-filename-increment) [![NPM monthly downloads](https://img.shields.io/npm/dm/add-filename-increment.svg?style=flat)](https://npmjs.org/package/add-filename-increment) [![NPM total downloads](https://img.shields.io/npm/dt/add-filename-increment.svg?style=flat)](https://npmjs.org/package/add-filename-increment) [![Build Status](https://travis-ci.org/jonschlinkert/add-filename-increment.svg?branch=master)](https://travis-ci.org/jonschlinkert/add-filename-increment) 2 | 3 | > When copying or moving files, it's common for operating systems to automatically add an increment or 'copy' to duplicate file names. This does that for Node.js applications, with automatic platform detection and support for Linux, MacOs, and Windows conventions. 4 | 5 | Please consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support. 6 | 7 | ## Install 8 | 9 | Install with [npm](https://www.npmjs.com/) (requires [Node.js](https://nodejs.org/en/) >=8): 10 | 11 | ```sh 12 | $ npm install --save add-filename-increment 13 | ``` 14 | 15 | ## What does this do? 16 | 17 | When copying files, it's common for operating systems to append a numerical increment or the word 'copy' to a file name to prevent the existing file from being overwritten. 18 | 19 | This library allows you to do the same thing in your Node.js application, using the correct conventions for the most commonly used [operating systems](#operating-systems). 20 | 21 | ## Usage 22 | 23 | All methods automatically detect the platform to use, unless `platform` is defined on the [options](#options). 24 | 25 | ```js 26 | const increment = require('add-filename-increment'); 27 | ``` 28 | 29 | ## API 30 | 31 | ### [increment](index.js#L54) 32 | 33 | The main export is a function that adds a trailing increment to 34 | the `stem` (basename without extension) of the given file path or object. 35 | 36 | **Params** 37 | 38 | * `file` **{String|Object}**: If the file is an object, it must have a `path` property. 39 | * `options` **{Object}**: See [available options](#options). 40 | * `returns` **{String|Object}**: Returns a file of the same type that was given, with an increment added to the file name. 41 | 42 | **Example** 43 | 44 | ```js 45 | console.log(increment('foo/bar.txt', { platform: 'darwin' })); 46 | //=> foo/bar copy.txt 47 | console.log(increment('foo/bar.txt', { platform: 'linux' })); 48 | //=> foo/bar (copy).txt 49 | console.log(increment('foo/bar.txt', { platform: 'win32' })); 50 | //=> foo/bar (2).txt 51 | ``` 52 | 53 | ### [.path](index.js#L76) 54 | 55 | Add a trailing increment to the given `filepath`. 56 | 57 | **Params** 58 | 59 | * `filepath` **{String}** 60 | * `options` **{Object}**: See [available options](#options). 61 | * `returns` **{String}** 62 | 63 | **Example** 64 | 65 | ```js 66 | console.log(increment.path('foo/bar.txt', { platform: 'darwin' })); 67 | //=> foo/bar copy.txt 68 | console.log(increment.path('foo/bar.txt', { platform: 'linux' })); 69 | //=> foo/bar (copy).txt 70 | console.log(increment.path('foo/bar.txt', { platform: 'win32' })); 71 | //=> foo/bar (2).txt 72 | ``` 73 | 74 | ### [.file](index.js#L98) 75 | 76 | Add a trailing increment to the `file.base` of the given file object. 77 | 78 | **Params** 79 | 80 | * `file` **{String|Object}**: If passed as a string, the path will be parsed to create an object using `path.parse()`. 81 | * `options` **{Object}**: See [available options](#options). 82 | * `returns` **{Object}**: Returns an object. 83 | 84 | **Example** 85 | 86 | ```js 87 | console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'darwin' })); 88 | //=> { path: 'foo/bar copy.txt', base: 'bar copy.txt' } 89 | console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'linux' })); 90 | //=> { path: 'foo/bar (copy).txt', base: 'bar (copy).txt' } 91 | console.log(increment.file({ path: 'foo/bar.txt' }, { platform: 'win32' })); 92 | //=> { path: 'foo/bar (2).txt', base: 'bar (2).txt' } 93 | ``` 94 | 95 | ### [.ordinal](index.js#L166) 96 | 97 | Returns an ordinal-suffix for the given number. This is used when creating increments for files on Linux. 98 | 99 | **Params** 100 | 101 | * `num` **{Number}** 102 | * `returns` **{String}** 103 | 104 | **Example** 105 | 106 | ```js 107 | const { ordinal } = require('add-filename-increment'); 108 | console.log(ordinal(1)); //=> 'st' 109 | console.log(ordinal(2)); //=> 'nd' 110 | console.log(ordinal(3)); //=> 'rd' 111 | console.log(ordinal(110)); //=> 'th' 112 | ``` 113 | 114 | ### [.toOrdinal](index.js#L184) 115 | 116 | Returns an ordinal for the given number. 117 | 118 | **Params** 119 | 120 | * `num` **{Number}** 121 | * `returns` **{String}** 122 | 123 | **Example** 124 | 125 | ```js 126 | const { toOrdinal } = require('add-filename-increment'); 127 | console.log(toOrdinal(1)); //=> '1st' 128 | console.log(toOrdinal(2)); //=> '2nd' 129 | console.log(toOrdinal(3)); //=> '3rd' 130 | console.log(toOrdinal(110)); //=> '110th' 131 | ``` 132 | 133 | ## Options 134 | 135 | ### options.fs 136 | 137 | **Description**: Check the file system, and automatically increment the file based on existing files. Thus, if the file name is `foo.txt`, and `foo (2).txt` already exists, the file will automatically be renamed to `foo (3).txt`. 138 | 139 | Also uses the correct conventions for Linux, Windows (win32), and MacOS (darwin). 140 | 141 | **Type**: `boolean` 142 | 143 | **Default**: `undefined` 144 | 145 | ### options.increment 146 | 147 | **Description**: Custom function to handling incrementing a file name. This is mostly useful when `options.fs` is also defined, since this function will only be called if a file name needs to be incremented, allowing you to control how incrementing is done. 148 | 149 | **Type**: `function` 150 | 151 | **Default**: `undefined` 152 | 153 | ### options.platform 154 | 155 | **Description**: Specify the platform conventions to use. 156 | 157 | **Type**: `string` 158 | 159 | **Default**: Uses `process.platform`. Valid values are `linux`, `win32`, and `darwin`. 160 | 161 | ## Operating Systems 162 | 163 | * [Linux](#linux) 164 | * [MacOS](#macos) 165 | * [Windows](#windows) 166 | 167 | **Supported Operating Systems** 168 | 169 | Currently Windows, Darwin (MacOS), and Linux are supported. This library attempts to automatically use the correct conventions for each operating system. Please [create an issue](../../issues/new) if you ecounter a bug. 170 | 171 | If you use an operating system with different conventions, and you would like for this library to add support, please [create an issue](../../issues/new) with a detailed description of those conventions, or feel free to do a [pull request](.github/contributing.md). 172 | 173 | ### Linux 174 | 175 | When a file is copied or moved, and the destination file path already exists, Linux uses the following conventions for incrementing the file name. 176 | 177 | | **Source path** | **Destination path** | **Type** | **Directory1** | 178 | | --- | --- | --- | --- | 179 | | `foo.txt` | `foo (copy).txt`, `foo (another copy).txt`, `foo (3rd copy).txt`, ... | file | Same directory as source | 180 | | `foo` | `foo (copy)`, `foo (another copy)`, `foo (3rd copy)`, ... | directory | Same directory as source | 181 | 182 | 1 _On Linux, when a file or folder is copied or moved to a different directory and another file or folder with the same name exists in that directory, you are prompted to choose a new name for the file or folder, or to cancel or skip the operation._ 183 | 184 | ### MacOS 185 | 186 | When a file is copied or moved, and the destination file path already exists, MacOS uses the following conventions for incrementing the file name. 187 | 188 | | **Source path** | **Destination path** | **Type** | **Directory1** | 189 | | --- | --- | --- | --- | 190 | | `foo.txt` | `foo copy.txt`, `foo copy 2.txt`, ... | file | Same directory as source | 191 | | `foo.txt` | `foo 2.txt`, `foo 3.txt`, ... | file | Different directory than source | 192 | | `foo` | `foo copy`, `foo copy 2`, ... | directory | Same directory as source | 193 | 194 | 1 _MacOS uses different conventions for incrementing file names when the source file is copied, moved or renamed to a different directory, versus when the file is copied into the same directory._ 195 | 196 | ### Windows 197 | 198 | When a file is copied or moved, and the destination file path already exists, Windows uses the following conventions for incrementing the file name. 199 | 200 | | **Source path** | **Destination path** | **Type** | **Directory1** | 201 | | --- | --- | --- | --- | 202 | | `foo.txt` | `foo - Copy.txt` | file | Same directory as source | 203 | | `foo.txt` | `foo (2).txt` | file | Different directory than source | 204 | | `foo (2).txt` | `foo (3).txt` | file | Different directory than source | 205 | | `foo` | `foo - Copy` | directory | Same directory as source | 206 | | `foo - Copy` | `foo - Copy (2)` | directory | Same directory as source | 207 | 208 | 1 _Windows uses different conventions for incrementing file names when the source file is copied, moved or renamed to a different directory, versus when the file is copied into the same directory. Also, when a folder is copied to a new directory, and the new directory already has a folder with the same name, Windows just merges the folders automatically._ 209 | 210 | ## About 211 | 212 |
213 | Contributing 214 | 215 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 216 | 217 | Please read the [contributing guide](.github/contributing.md) for advice on opening issues, pull requests, and coding standards. 218 | 219 |
220 | 221 |
222 | Running Tests 223 | 224 | Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: 225 | 226 | ```sh 227 | $ npm install && npm test 228 | ``` 229 | 230 |
231 | 232 |
233 | Building docs 234 | 235 | _(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_ 236 | 237 | To generate the readme, run the following command: 238 | 239 | ```sh 240 | $ npm install -g verbose/verb#dev verb-generate-readme && verb 241 | ``` 242 | 243 |
244 | 245 | ### Related projects 246 | 247 | You might also be interested in these projects: 248 | 249 | * [micromatch](https://www.npmjs.com/package/micromatch): Glob matching for javascript/node.js. A replacement and faster alternative to minimatch and multimatch. | [homepage](https://github.com/micromatch/micromatch "Glob matching for javascript/node.js. A replacement and faster alternative to minimatch and multimatch.") 250 | * [strip-filename-increment](https://www.npmjs.com/package/strip-filename-increment): Operating systems commonly add a trailing increment, or the word 'copy', or something similar to… [more](https://github.com/jonschlinkert/strip-filename-increment) | [homepage](https://github.com/jonschlinkert/strip-filename-increment "Operating systems commonly add a trailing increment, or the word 'copy', or something similar to duplicate files. This strips those increments. Tested on Windows, MacOS, and Linux.") 251 | * [write](https://www.npmjs.com/package/write): Write data to a file, replacing the file if it already exists and creating any… [more](https://github.com/jonschlinkert/write) | [homepage](https://github.com/jonschlinkert/write "Write data to a file, replacing the file if it already exists and creating any intermediate directories if they don't already exist. Thin wrapper around node's native fs methods.") 252 | 253 | ### Author 254 | 255 | **Jon Schlinkert** 256 | 257 | * [GitHub Profile](https://github.com/jonschlinkert) 258 | * [Twitter Profile](https://twitter.com/jonschlinkert) 259 | * [LinkedIn Profile](https://linkedin.com/in/jonschlinkert) 260 | 261 | ### License 262 | 263 | Copyright © 2019, [Jon Schlinkert](https://github.com/jonschlinkert). 264 | Released under the [MIT License](LICENSE). 265 | 266 | *** 267 | 268 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on September 04, 2019._ --------------------------------------------------------------------------------