Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(options): initial support for long options #926

Merged
merged 2 commits into from Jan 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -13,6 +13,7 @@ os:
- osx
script:
- npm run test-with-coverage
- npm run lint
# make sure when the docs are generated nothing changes (a.k.a. the docs have already been generated)
- npm run gendocs
- npm run check-node-support
Expand Down
8 changes: 5 additions & 3 deletions README.md
Expand Up @@ -647,16 +647,18 @@ Available options:
+ `-a`: Change only the access time
+ `-c`: Do not create any files
+ `-m`: Change only the modification time
+ `{'-d': date}`: Use `date` (instance of `Date`) instead of current time
+ `{'-r': file}`: Use `file`'s times instead of current time
+ `{'-d': someDate}`, `{date: someDate}`: Use `someDate` (instance of
`Date`) instead of current time
+ `{'-r': file}`, `{reference: file}`: Use `file`'s times instead of current
time

Examples:

```javascript
touch('source.js');
touch('-c', 'path/to/file.js');
touch({ '-r': 'referenceFile.txt' }, 'path/to/file.js');
touch({ '-d': new Date('December 17, 1995 03:24:00') }, 'path/to/file.js');
touch({ date: new Date('December 17, 1995 03:24:00') }, 'path/to/file.js');
```

Update the access and modification times of each file to the current time.
Expand Down
1 change: 1 addition & 0 deletions appveyor.yml
Expand Up @@ -25,6 +25,7 @@ build: off

test_script:
- npm run test-with-coverage
- npm run lint

on_success:
- npm run codecov -- -f coverage/lcov.info
51 changes: 34 additions & 17 deletions src/common.js
Expand Up @@ -77,6 +77,15 @@ function convertErrorOutput(msg) {
}
exports.convertErrorOutput = convertErrorOutput;

// An exception class to help propagate command errors (e.g., non-zero exit
// status) up to the top-level. {@param value} should be a ShellString.
function CommandError(value) {
this.returnValue = value;
}
CommandError.prototype = Object.create(Error.prototype);
CommandError.prototype.constructor = CommandError;
exports.CommandError = CommandError; // visible for testing

// Shows error message. Throws if config.fatal is true
function error(msg, _code, options) {
// Validate input
Expand Down Expand Up @@ -111,10 +120,7 @@ function error(msg, _code, options) {
if (msg.length > 0 && !options.silent) log(logEntry);

if (!options.continue) {
throw {
msg: 'earlyExit',
retValue: (new ShellString('', state.error, state.errorCode)),
};
throw new CommandError(new ShellString('', state.error, state.errorCode));
}
}
exports.error = error;
Expand Down Expand Up @@ -159,13 +165,15 @@ exports.ShellString = ShellString;
// Throws an error when passed a string that does not start with '-':
// parseOptions('a', {'a':'alice'}); // throws
function parseOptions(opt, map, errorOptions) {
errorOptions = errorOptions || {};
// Validate input
if (typeof opt !== 'string' && !isObject(opt)) {
throw new Error('options must be strings or key-value pairs');
throw new TypeError('options must be strings or key-value pairs');
} else if (!isObject(map)) {
throw new Error('parseOptions() internal error: map must be an object');
} else if (errorOptions && !isObject(errorOptions)) {
throw new Error('parseOptions() internal error: errorOptions must be object');
throw new TypeError('parseOptions() internal error: map must be an object');
} else if (!isObject(errorOptions)) {
throw new TypeError(
'parseOptions() internal error: errorOptions must be object');
}

if (opt === '--') {
Expand Down Expand Up @@ -201,18 +209,27 @@ function parseOptions(opt, map, errorOptions) {
options[optionName] = true;
}
} else {
error('option not recognized: ' + c, errorOptions || {});
error('option not recognized: ' + c, errorOptions);
}
});
} else { // opt is an Object
Object.keys(opt).forEach(function (key) {
// key is a string of the form '-r', '-d', etc.
var c = key[1];
if (c in map) {
var optionName = map[c];
options[optionName] = opt[key]; // assign the given value
if (key[0] === '-') {
// key is a string of the form '-r', '-d', etc.
var c = key[1];
if (c in map) {
var optionName = map[c];
options[optionName] = opt[key]; // assign the given value
} else {
error('option not recognized: ' + c, errorOptions);
}
} else {
error('option not recognized: ' + c, errorOptions || {});
if (key in options) {
// key is a "long option", so it should be the same
options[key] = opt[key];
} else {
error('option not recognized: {' + key + ':...}', errorOptions);
}
}
});
}
Expand Down Expand Up @@ -384,8 +401,8 @@ function wrap(cmd, fn, options) {
retValue = fn.apply(this, args);
} catch (e) {
/* istanbul ignore else */
if (e.msg === 'earlyExit') {
retValue = e.retValue;
if (e instanceof CommandError) {
retValue = e.returnValue;
} else {
throw e; // this is probably a bug that should be thrown up the call stack
}
Expand Down
8 changes: 5 additions & 3 deletions src/touch.js
Expand Up @@ -20,16 +20,18 @@ common.register('touch', _touch, {
//@ + `-a`: Change only the access time
//@ + `-c`: Do not create any files
//@ + `-m`: Change only the modification time
//@ + `{'-d': date}`: Use `date` (instance of `Date`) instead of current time
//@ + `{'-r': file}`: Use `file`'s times instead of current time
//@ + `{'-d': someDate}`, `{date: someDate}`: Use `someDate` (instance of
//@ `Date`) instead of current time
//@ + `{'-r': file}`, `{reference: file}`: Use `file`'s times instead of current
//@ time
//@
//@ Examples:
//@
//@ ```javascript
//@ touch('source.js');
//@ touch('-c', 'path/to/file.js');
//@ touch({ '-r': 'referenceFile.txt' }, 'path/to/file.js');
//@ touch({ '-d': new Date('December 17, 1995 03:24:00') }, 'path/to/file.js');
//@ touch({ date: new Date('December 17, 1995 03:24:00') }, 'path/to/file.js');
//@ ```
//@
//@ Update the access and modification times of each file to the current time.
Expand Down
50 changes: 41 additions & 9 deletions test/common.js
Expand Up @@ -4,13 +4,16 @@ import shell from '..';
import common from '../src/common';
import utils from './utils/utils';

shell.config.silent = true;

test.beforeEach(() => {
shell.config.silent = true;
common.state.error = null;
common.state.errorCode = 0;
});

test.afterEach(() => {
common.config.resetForTesting();
});

//
// Invalids
//
Expand All @@ -34,45 +37,45 @@ test('parseOptions (invalid option in options object)', t => {
f: 'force',
r: 'reverse',
});
});
}, common.CommandError);
});

test('parseOptions (without a hyphen in the string)', t => {
t.throws(() => {
common.parseOptions('f', {
f: 'force',
});
});
}, Error);
});

test('parseOptions (opt is not a string/object)', t => {
t.throws(() => {
common.parseOptions(1, {
f: 'force',
});
});
}, TypeError);
});

test('parseOptions (map is not an object)', t => {
t.throws(() => {
common.parseOptions('-f', 27);
});
}, TypeError);
});

test('parseOptions (errorOptions is not an object)', t => {
t.throws(() => {
common.parseOptions('-f', {
f: 'force',
}, 'not a valid errorOptions');
});
}, TypeError);
});

test('parseOptions (unrecognized string option)', t => {
t.throws(() => {
common.parseOptions('-z', {
f: 'force',
});
});
}, common.CommandError);
});

test('parseOptions (unrecognized option in Object)', t => {
Expand Down Expand Up @@ -200,7 +203,6 @@ test('common.buffer with different config.bufLength', t => {
const buf = common.buffer();
t.truthy(buf instanceof Buffer);
t.is(buf.length, 20);
common.config.reset();
});

test('common.parseOptions (normal case)', t => {
Expand Down Expand Up @@ -264,6 +266,29 @@ test('common.parseOptions throws when passed a string not starting with "-"', t
}, Error, "Options string must start with a '-'");
});

test('common.parseOptions allows long options', t => {
const result = common.parseOptions({ value: true }, {
v: 'value',
});
t.truthy(result.value);
});

test('common.parseOptions allows long options with values', t => {
const someObject = {};
const result = common.parseOptions({ value: someObject }, {
v: 'value',
});
t.is(result.value, someObject);
});

test('common.parseOptions throws for unknown long option', t => {
t.throws(() => {
common.parseOptions({ throws: true }, {
v: 'value',
});
}, common.CommandError);
});

test('common.parseOptions with -- argument', t => {
const result = common.parseOptions('--', {
R: 'recursive',
Expand Down Expand Up @@ -307,3 +332,10 @@ test('Changing shell.config.execPath does not modify process', t => {
shell.config.execPath = 'foo';
t.not(shell.config.execPath, process.execPath);
});

test('CommandError is a subclass of Error', t => {
const e = new common.CommandError(new common.ShellString('some value'));
t.truthy(e instanceof common.CommandError);
t.truthy(e instanceof Error);
t.is(e.constructor, common.CommandError);
});
13 changes: 11 additions & 2 deletions test/touch.js
Expand Up @@ -121,9 +121,18 @@ test('uses a reference file for mtime', t => {

test('accepts -d flag', t => {
const testFile = tmpFile(t);
const oldStat = resetUtimes(testFile);
const date = new Date('December 17, 1995 03:24:00');
const result = shell.touch({'-d': date}, testFile);
const result = shell.touch({ '-d': date }, testFile);
t.is(result.code, 0);
// Compare getTime(), because Date can't be compared with triple-equals.
t.is(common.statFollowLinks(testFile).mtime.getTime(), date.getTime());
t.is(common.statFollowLinks(testFile).atime.getTime(), date.getTime());
});

test('accepts long option (date)', t => {
const testFile = tmpFile(t);
const date = new Date('December 17, 1995 03:24:00');
const result = shell.touch({ date }, testFile);
t.is(result.code, 0);
// Compare getTime(), because Date can't be compared with triple-equals.
t.is(common.statFollowLinks(testFile).mtime.getTime(), date.getTime());
Expand Down