Skip to content

Commit f67b17f

Browse files
committed
feat(mvMode): create mv mode
1 parent 48e7222 commit f67b17f

File tree

5 files changed

+163
-29
lines changed

5 files changed

+163
-29
lines changed

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
66
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
77

8-
## Turing-complete JS commandline JSON/text transformer
8+
## Turing-complete JS commandline JSON/text transformer and file renamer
99

1010
Can't remember how `sed` works.
1111
Can't remember how `cut` works.
@@ -75,6 +75,21 @@ Doe, John
7575
]
7676
```
7777

78+
##### Rename .jsx files in the current directory to .js:
79+
```
80+
duh mv *.jsx 'file => file.replace(/\.jsx$/g, ".js")'
81+
```
82+
83+
##### Rename all .jsx files in the current subtree to .js:
84+
```
85+
find . -name *.jsx | duh mv 'file => file.replace(/\.jsx$/g, ".js")'
86+
```
87+
88+
##### Flatten music stored as artist/album/filename into a single directory:
89+
```
90+
duh mv */*/* 'file => { var [artist, album, file] = file.split(/\//g); return `${artist} - ${album} - ${file}` }'
91+
```
92+
7893
## Node version note
7994

8095
The syntax that `duh` accepts for your function depends on the version of Node you're running. If you're running a
@@ -88,6 +103,7 @@ You can use `lambduh` or just `duh`.
88103

89104
You may have noticed above that you can apply a transformation to each line of a plain text file.
90105
This is pretty much the only magic built into the `duh` command:
106+
* If the first argument is `mv` (rather than a function), it runs in **move mode**.
91107
* If the first argument of your function is `lines` (ignoring case), it runs in **lines mode**.
92108
* Otherwise if the first argument of your function starts with `l` or `L`, it runs in **line mode**.
93109
* If the first argument of your function starts with `t` or `T`, it runs in **text mode**.
@@ -117,6 +133,19 @@ in order to `stdout`. Otherwise, it calls `String` on it and writes that string
117133
Calls your function with the entire text from stdin in a single string, and writes what your function returns to
118134
`stdout`.
119135

136+
### Move Mode
137+
138+
Move mode renames each file (or directory) in the arguments by applying your provided function
139+
(which must be the last argument) to each filename. If you don't provide any files in the arguments, or one of your
140+
arguments is a single dash (`-`), it will also read from stdin, treating each line as a filename.
141+
142+
By default it will print the old and new filenames and then ask
143+
for confirmation before actually moving them.
144+
145+
#### Options
146+
* `-y`: move the files without asking you to confirm the new names.
147+
* `--dry-run`: just print out the old and new file names, then exit.
148+
120149
## Customization
121150

122151
`lambduh` requires any `.lambduh.js` files in the working directory or its ancestors (from root to deepest), as well

lib/index.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,43 @@
44

55
require('./loadRcFiles')
66

7-
if (!process.argv[2]) {
8-
console.error('Error: first argument must evaluate to a function') // eslint-disable-line no-console
9-
process.exit(1)
10-
}
7+
function parseFn(code) {
8+
if (!code || !code.trim()) {
9+
console.error('Error: missing function argument') // eslint-disable-line no-console
10+
process.exit(1)
11+
}
1112

12-
var fn = eval(process.argv[2].trim())
13-
if (typeof fn !== 'function') {
14-
console.error('Error: first argument must evaluate to a function') // eslint-disable-line no-console
15-
process.exit(1)
13+
var fn
14+
try {
15+
fn = eval(code)
16+
} catch (error) {
17+
// ignore
18+
}
19+
if (typeof fn !== 'function') {
20+
console.error('Error: invalid function: ' + code) // eslint-disable-line no-console
21+
process.exit(1)
22+
}
23+
return fn
1624
}
1725

18-
switch (require('./pickInputMode')(process.argv[2].trim())) {
26+
switch (require('./pickInputMode')(process.argv[2])) {
1927
case 'lines':
20-
require('./linesMode')(fn)
28+
require('./linesMode')(parseFn(process.argv[2]))
2129
break
2230
case 'line':
23-
require('./lineMode')(fn)
31+
require('./lineMode')(parseFn(process.argv[2]))
2432
break
2533
case 'text':
26-
require('./textMode')(fn)
34+
require('./textMode')(parseFn(process.argv[2]))
2735
break
2836
case 'json':
29-
require('./jsonMode')(fn)
37+
require('./jsonMode')(parseFn(process.argv[2]))
38+
break
39+
case 'mv':
40+
require('./mvMode')(parseFn(process.argv[Math.max(3, process.argv.length - 1)]))
3041
break
3142
default:
32-
process.stderr.write("Error: Can't determine input mode!")
43+
console.error('See ' + require('../package.json').repository.url + ' for usage information.') // eslint-disable-line no-console
3344
process.exit(1)
3445
}
3546

lib/mvMode.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict'
2+
3+
module.exports = function mvMode(fn) {
4+
var files = process.argv.slice(3, process.argv.length - 1).filter(function (file) {
5+
return !file.startsWith('-')
6+
})
7+
8+
if (process.argv.indexOf('-') > 0 || !files.length) {
9+
require('line-reader').eachLine(
10+
process.stdin,
11+
function (file) {
12+
files.push(file)
13+
},
14+
function done(error) {
15+
if (error) {
16+
console.error(error.stack) // eslint-disable-line no-console
17+
process.exit(1)
18+
}
19+
ready()
20+
}
21+
)
22+
} else ready()
23+
24+
function ready() {
25+
run(files, fn, {
26+
dryRun: process.argv.indexOf('--dry-run') >= 0,
27+
noConfirm: process.argv.indexOf('-y') >= 0,
28+
})
29+
}
30+
31+
function run(files, fn, options) {
32+
var dryRun = options.dryRun
33+
var noConfirm = options.noConfirm
34+
var silent = options.silent
35+
36+
var fs = require('fs')
37+
38+
files.forEach(function (oldPath) {
39+
try {
40+
var newPath = fn(oldPath)
41+
} catch (error) {
42+
console.error(error.stack) // eslint-disable-line no-console
43+
process.exit(1)
44+
}
45+
if (newPath === oldPath) return
46+
if (!silent) console[dryRun ? 'log' : 'error'](oldPath + ' -> ' + newPath) // eslint-disable-line no-console
47+
if (!dryRun && noConfirm) fs.renameSync(oldPath, newPath)
48+
})
49+
50+
if (!dryRun && !noConfirm) {
51+
var rl = require('readline').createInterface({
52+
input: process.stdin,
53+
output: process.stdout,
54+
})
55+
56+
rl.question('Apply renaming? (y/N)', function (answer) {
57+
rl.close()
58+
if (answer && answer[0].toLowerCase() === 'y') {
59+
run(files, fn, {noConfirm: true, silent: true})
60+
}
61+
})
62+
}
63+
}
64+
}
65+

lib/pickInputMode.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use strict'
22

33
module.exports = function pickMode(code) {
4+
if (!code) return null
5+
if (code === 'mv') return 'mv'
6+
47
var match = /^\s*(function(\s+\w+)?\s*\(\s*|\(\s*)?(\w+)/.exec(code.toLowerCase())
58
var firstArg = match && match[3]
69

test/index.js

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ var spawn = require('child_process').spawn
22
var expect = require('chai').expect
33
var pickInputMode = require('../lib/pickInputMode')
44

5-
function testCase(input, fn, output, done) {
6-
var child = spawn(process.argv[0], [
7-
require.resolve('../lib'),
8-
fn,
9-
])
5+
function testCase(input, args, output, done) {
6+
var child = spawn(process.argv[0], [require.resolve('../lib')].concat(args))
107
var outchunks = []
118
var errchunks = []
129
child.stdout.on('data', function (chunk) { outchunks.push(chunk) })
@@ -33,15 +30,7 @@ describe('lambduh', function () {
3330
testCase(
3431
'input',
3532
'2881234',
36-
new Error("first argument must evaluate to a function"),
37-
done
38-
)
39-
})
40-
it("errors if it can't determine input mode", function (done) {
41-
testCase(
42-
'input',
43-
'() => {}',
44-
new Error("Can't determine input mode!"),
33+
new Error("invalid function"),
4534
done
4635
)
4736
})
@@ -68,6 +57,9 @@ describe('lambduh', function () {
6857
expect(pickInputMode('function aksnd923ka_( t ) { return t.toUpperCase() }')).to.equal('text')
6958
expect(pickInputMode('function blaAARrgh__(j){ return j.toUpperCase() }')).to.equal('json')
7059
})
60+
it('works for mv', function () {
61+
expect(pickInputMode('mv')).to.equal('mv')
62+
})
7163
it("returns null if it can't find first argument", function () {
7264
expect(pickInputMode('(')).to.equal(null)
7365
})
@@ -160,5 +152,39 @@ describe('lambduh', function () {
160152
)
161153
})
162154
})
155+
describe('mvMode', function () {
156+
it('works for valid input', function (done) {
157+
testCase(
158+
'foo\nbar\nBAZ',
159+
['mv', '--dry-run', 'qux', '-', 'blah blah', 'file => file.toUpperCase()'],
160+
'qux -> QUX\nblah blah -> BLAH BLAH\nfoo -> FOO\nbar -> BAR\n',
161+
done
162+
)
163+
})
164+
it('reads from STDIN when no files are given in arguments', function (done) {
165+
testCase(
166+
'foo\nbar\nBAZ',
167+
['mv', '--dry-run', 'file => file.toUpperCase()'],
168+
'foo -> FOO\nbar -> BAR\n',
169+
done
170+
)
171+
})
172+
it("doesn't read from STDIN when file arguments are given", function (done) {
173+
testCase(
174+
'qux',
175+
['mv', '--dry-run', 'foo', 'bar', 'file => file.toUpperCase()'],
176+
'foo -> FOO\nbar -> BAR\n',
177+
done
178+
)
179+
})
180+
it('handles errors in function', function (done) {
181+
testCase(
182+
'foo\nbar\nBAZ',
183+
['mv', '--dry-run', '-', 'file => { throw new Error("TEST") }'],
184+
new Error("TEST"),
185+
done
186+
)
187+
})
188+
})
163189
})
164190

0 commit comments

Comments
 (0)