Skip to content

Commit 4ccffe0

Browse files
committed
fix: allow option pairs before command name
1 parent 97bd778 commit 4ccffe0

File tree

2 files changed

+319
-8
lines changed

2 files changed

+319
-8
lines changed

lib/index.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,12 @@ class Sade {
106106
}
107107

108108
parse(arr, opts={}) {
109-
let offset = 2; // argv slicer
109+
let tmp, idx, isVoid, cmd;
110110
let alias = { h:'help', v:'version' };
111-
let argv = mri(arr.slice(offset), { alias });
111+
let argv = mri(arr.slice(2), { alias });
112112
let isSingle = this.single;
113113
let bin = this.bin;
114-
let tmp, name = '';
115-
let isVoid, cmd;
114+
let name = '';
116115

117116
if (isSingle) {
118117
cmd = this.tree[DEF];
@@ -122,11 +121,13 @@ class Sade {
122121
for (; i < len; i++) {
123122
tmp = argv._.slice(0, i).join(' ');
124123
if (this.tree[tmp] !== void 0) {
125-
name=tmp; offset=(i + 2); // argv slicer
124+
name=tmp; idx=arr.indexOf(tmp, 1);
126125
} else {
127126
for (k in this.tree) {
128127
if (this.tree[k].alibi.includes(tmp)) {
129-
name=k; offset=(i + 2);
128+
idx = arr.indexOf(tmp);
129+
arr.splice(idx, 1, ...k.split(' '));
130+
name = k;
130131
break;
131132
}
132133
}
@@ -141,7 +142,6 @@ class Sade {
141142
name = this.default;
142143
cmd = this.tree[name];
143144
arr.unshift(name);
144-
offset++;
145145
} else if (tmp) {
146146
return $.error(bin, `Invalid command: ${tmp}`);
147147
} //=> else: cmd not specified, wait for now...
@@ -161,7 +161,11 @@ class Sade {
161161
opts.alias = Object.assign(all.alias, cmd.alias, opts.alias);
162162
opts.default = Object.assign(all.default, cmd.default, opts.default);
163163

164-
let vals = mri(arr.slice(offset), opts);
164+
tmp = name.split(' ');
165+
idx = arr.indexOf(tmp[0], 2);
166+
if (!!~idx) arr.splice(idx, tmp.length);
167+
168+
let vals = mri(arr.slice(2), opts);
165169
if (!vals || typeof vals === 'string') {
166170
return $.error(bin, vals || 'Parsed unknown option flag(s)!');
167171
}

test/usage.js

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,3 +750,310 @@ test('(usage) alias :: pre-command :: throws', t => {
750750
t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` before defining a command'), '~> threw Error w/ message');
751751
t.end();
752752
});
753+
754+
755+
// ---
756+
// Input Order
757+
// ---
758+
759+
760+
test('(usage) order :: basic', t => {
761+
let pid1 = exec('basic.js', ['--foo', 'bar', 'f']);
762+
t.is(pid1.status, 0, 'exits without error code');
763+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
764+
t.is(pid1.stdout.toString(), '~> ran "foo" action\n', '~> command invoked');
765+
766+
let pid2 = exec('basic.js', ['--foo', 'bar', 'fo']);
767+
t.is(pid2.status, 0, 'exits without error code');
768+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
769+
t.is(pid2.stdout.toString(), '~> ran "foo" action\n', '~> command invoked');
770+
771+
t.end();
772+
});
773+
774+
test('(usage) order :: basic :: error :: invalid command', t => {
775+
let pid = exec('basic.js', ['--foo', 'bar', 'fff']);
776+
t.is(pid.status, 1, 'exits with error code');
777+
t.is(
778+
pid.stderr.toString(),
779+
'\n ERROR\n Invalid command: fff\n\n Run `$ bin --help` for more info.\n\n',
780+
'~> stderr has "Invalid command: fff" error message'
781+
);
782+
t.is(pid.stdout.length, 0, '~> stdout is empty');
783+
t.end();
784+
});
785+
786+
test('(usage) order :: basic :: help', t => {
787+
let pid1 = exec('basic.js', ['--foo', 'bar', '-h']);
788+
t.is(pid1.status, 0, 'exits with error code');
789+
t.true(pid1.stdout.toString().includes('Available Commands\n foo'), '~> shows global help w/ "Available Commands" text');
790+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
791+
792+
let pid2 = exec('basic.js', ['--foo', 'bar', 'f', '-h']);
793+
t.is(pid2.status, 0, 'exits with error code');
794+
t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text');
795+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
796+
797+
t.end();
798+
});
799+
800+
801+
test('(usage) order :: args.required', t => {
802+
let pid = exec('args.js', ['--foo', 'bar', 'f', 'value']);
803+
t.is(pid.status, 0, 'exits without error code');
804+
t.is(pid.stdout.toString(), '~> ran "foo" with "value" arg\n', '~> command invoked');
805+
t.is(pid.stderr.length, 0, '~> stderr is empty');
806+
t.end();
807+
});
808+
809+
test('(usage) order :: args.required :: error :: missing argument', t => {
810+
let pid = exec('args.js', ['--foo', 'bar', 'f']);
811+
t.is(pid.status, 1, 'exits with error code');
812+
t.is(
813+
pid.stderr.toString(),
814+
'\n ERROR\n Insufficient arguments!\n\n Run `$ bin foo --help` for more info.\n\n',
815+
'~> stderr has "Insufficient arguments!" error message'
816+
);
817+
t.is(pid.stdout.length, 0, '~> stdout is empty');
818+
t.end();
819+
});
820+
821+
test('(usage) order :: args.optional', t => {
822+
let pid = exec('args.js', ['--foo', 'bar', 'b']);
823+
t.is(pid.status, 0, 'exits without error code');
824+
t.is(pid.stdout.toString(), '~> ran "bar" with "~default~" arg\n', '~> command invoked');
825+
t.is(pid.stderr.length, 0, '~> stderr is empty');
826+
t.end();
827+
});
828+
829+
test('(usage) order :: args.optional w/ value', t => {
830+
let pid = exec('args.js', ['--foo', 'bar', 'b', 'value']);
831+
t.is(pid.status, 0, 'exits without error code');
832+
t.is(pid.stdout.toString(), '~> ran "bar" with "value" arg\n', '~> command invoked');
833+
t.is(pid.stderr.length, 0, '~> stderr is empty');
834+
t.end();
835+
});
836+
837+
838+
839+
test('(usage) order :: options.long', t => {
840+
let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--long']);
841+
t.is(pid1.status, 0, 'exits without error code');
842+
t.is(pid1.stdout.toString(), '~> ran "long" option\n', '~> command invoked');
843+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
844+
845+
let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-l']);
846+
t.is(pid2.status, 0, 'exits without error code');
847+
t.is(pid2.stdout.toString(), '~> ran "long" option\n', '~> command invoked');
848+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
849+
850+
t.end();
851+
});
852+
853+
test('(usage) order :: options.short', t => {
854+
let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--short']);
855+
t.is(pid1.status, 0, 'exits without error code');
856+
t.is(pid1.stdout.toString(), '~> ran "short" option\n', '~> command invoked');
857+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
858+
859+
let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-s']);
860+
t.is(pid2.status, 0, 'exits without error code');
861+
t.is(pid2.stdout.toString(), '~> ran "short" option\n', '~> command invoked');
862+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
863+
864+
t.end();
865+
});
866+
867+
test('(usage) order :: options.hello', t => {
868+
let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--hello']);
869+
t.is(pid1.status, 0, 'exits without error code');
870+
t.is(pid1.stdout.toString(), '~> ran "hello" option\n', '~> command invoked');
871+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
872+
873+
// shows that '-h' is always reserved
874+
let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-h']);
875+
let stdout = pid2.stdout.toString();
876+
t.is(pid2.status, 0, 'exits without error code');
877+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
878+
879+
t.not(stdout, '~> ran "long" option\n', '~> did NOT run custom "-h" option');
880+
t.true(stdout.includes('-h, --help Displays this message'), '~~> shows `--help` text');
881+
882+
t.end();
883+
});
884+
885+
test('(usage) order :: options.extra', t => {
886+
let pid = exec('options.js', ['--foo', 'bar', 'f', '--extra=opts', '--404']);
887+
t.is(pid.status, 0, 'exits without error code');
888+
t.is(pid.stdout.toString(), '~> default with {"404":true,"_":[],"foo":"bar","extra":"opts"}\n', '~> command invoked');
889+
t.is(pid.stderr.length, 0, '~> stderr is empty');
890+
t.end();
891+
});
892+
893+
test('(usage) order :: options.global', t => {
894+
let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--global']);
895+
t.is(pid1.status, 0, 'exits without error code');
896+
t.is(pid1.stdout.toString(), '~> default with {"_":[],"foo":"bar","global":true,"g":true}\n', '~> command invoked');
897+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
898+
899+
let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-g', 'hello']);
900+
t.is(pid2.status, 0, 'exits without error code');
901+
t.is(pid2.stdout.toString(), '~> default with {"_":[],"foo":"bar","g":"hello","global":"hello"}\n', '~> command invoked');
902+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
903+
904+
t.end();
905+
});
906+
907+
test('(usage) order :: options w/o alias', t => {
908+
let pid1 = exec('options.js', ['--foo', 'bar', 'b', 'hello']);
909+
t.is(pid1.status, 0, 'exits without error code');
910+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
911+
t.is(pid1.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked');
912+
913+
let pid2 = exec('options.js', ['--foo', 'bar', 'b', 'hello', '--only']);
914+
t.is(pid2.status, 0, 'exits without error code');
915+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
916+
t.is(pid2.stdout.toString(), '~> (only) "bar" with "hello" value\n', '~> command invoked');
917+
918+
let pid3 = exec('options.js', ['--foo', 'bar', 'b', 'hello', '-o']);
919+
t.is(pid3.status, 0, 'exits without error code');
920+
t.is(pid3.stderr.length, 0, '~> stderr is empty');
921+
t.is(pid3.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked');
922+
923+
t.end();
924+
});
925+
926+
927+
test('(usage) order :: unknown.custom', t => {
928+
let pid1 = exec('unknown2.js', ['f', '--global', '--local']);
929+
t.is(pid1.status, 0, 'exits without error code');
930+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
931+
t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"local":true,"g":true,"l":true}\n', '~> command invoked');
932+
933+
let pid2 = exec('unknown2.js', ['--foo', 'bar', 'f', '--bar']);
934+
t.is(pid2.status, 1, 'exits with error code');
935+
t.is(pid2.stdout.length, 0, '~> stdout is empty');
936+
t.is(
937+
pid2.stderr.toString(),
938+
'\n ERROR\n Custom error: --foo\n\n Run `$ bin --help` for more info.\n\n', // came first
939+
'~> stderr has "Custom error: --foo" error message' // came first
940+
);
941+
942+
t.end();
943+
});
944+
945+
946+
test('(usage) order :: unknown.plain', t => {
947+
let pid1 = exec('unknown2.js', ['f', '--flag1', '--flag2']);
948+
t.is(pid1.status, 0, 'exits without error code');
949+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
950+
t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"flag1":true,"flag2":true}\n', '~> command invoked');
951+
952+
let pid2 = exec('unknown2.js', ['--foo', 'bar', 'f', '--flag3']);
953+
t.is(pid2.status, 1, 'exits with error code');
954+
t.is(pid2.stdout.length, 0, '~> stdout is empty');
955+
t.is(
956+
pid2.stderr.toString(),
957+
'\n ERROR\n Custom error: --foo\n\n Run `$ bin --help` for more info.\n\n', // came first
958+
'~> stderr has "Custom error: --foo" error message' // came first
959+
);
960+
961+
t.end();
962+
});
963+
964+
965+
966+
test('(usage) order :: subcommands', t => {
967+
let pid1 = exec('subs.js', ['--foo', 'bar', 'r']);
968+
t.is(pid1.status, 0, 'exits without error code');
969+
t.is(pid1.stdout.toString(), '~> ran "remote" action\n', '~> ran parent');
970+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
971+
972+
let pid2 = exec('subs.js', ['--foo', 'bar', 'rr', 'origin', 'foobar']);
973+
t.is(pid2.status, 0, 'exits without error code');
974+
t.is(pid2.stdout.toString(), '~> ran "remote rename" with "origin" and "foobar" args\n', '~> ran "rename" child');
975+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
976+
977+
let pid3 = exec('subs.js', ['--foo', 'bar', 'ra', 'origin', 'foobar']);
978+
t.is(pid3.status, 0, 'exits without error code');
979+
t.is(pid3.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child');
980+
t.is(pid3.stderr.length, 0, '~> stderr is empty');
981+
982+
t.end();
983+
});
984+
985+
test('(usage) order :: subcommands :: help', t => {
986+
let pid1 = exec('subs.js', ['--foo', 'bar', '--help']);
987+
t.is(pid1.status, 0, 'exits without error code');
988+
t.true(pid1.stdout.toString().includes('Available Commands\n remote \n remote add \n remote rename'), '~> shows global help w/ "Available Commands" text');
989+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
990+
991+
let pid2 = exec('subs.js', ['--foo', 'bar', 'r', '--help']);
992+
t.is(pid2.status, 0, 'exits without error code');
993+
t.true(pid2.stdout.toString().includes('Usage\n $ bin remote [options]'), '~> shows "remote" help text');
994+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
995+
996+
let pid3 = exec('subs.js', ['--foo', 'bar', 'rr', '--help']);
997+
t.is(pid3.status, 0, 'exits without error code');
998+
t.true(pid3.stdout.toString().includes('Usage\n $ bin remote rename <old> <new> [options]'), '~> shows "remote rename" help text');
999+
t.is(pid3.stderr.length, 0, '~> stderr is empty');
1000+
1001+
t.end();
1002+
});
1003+
1004+
1005+
test('(usage) order :: default', t => {
1006+
let pid1 = exec('default.js', ['--foo', 'bar']);
1007+
t.is(pid1.status, 0, 'exits without error code');
1008+
t.is(pid1.stdout.toString(), '~> ran "foo" action\n', '~> ran default command');
1009+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
1010+
1011+
let pid2 = exec('default.js', ['--foo', 'bar', 'f']);
1012+
t.is(pid2.status, 0, 'exits without error code');
1013+
t.is(pid2.stdout.toString(), '~> ran "foo" action\n', '~> ran default command (direct)');
1014+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
1015+
1016+
let pid3 = exec('default.js', ['--foo', 'bar', 'b']);
1017+
t.is(pid3.status, 0, 'exits without error code');
1018+
t.is(pid3.stdout.toString(), '~> ran "bar" action\n', '~> ran "bar" command');
1019+
t.is(pid3.stderr.length, 0, '~> stderr is empty');
1020+
1021+
t.end();
1022+
});
1023+
1024+
test('(usage) order :: default :: help', t => {
1025+
let pid1 = exec('default.js', ['--foo', 'bar', '--help']);
1026+
t.is(pid1.status, 0, 'exits without error code');
1027+
t.true(pid1.stdout.toString().includes('Available Commands\n foo \n bar'), '~> shows global help w/ "Available Commands" text');
1028+
t.is(pid1.stderr.length, 0, '~> stderr is empty');
1029+
1030+
let pid2 = exec('default.js', ['--foo', 'bar', 'f', '-h']);
1031+
t.is(pid2.status, 0, 'exits without error code');
1032+
t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text');
1033+
t.is(pid2.stderr.length, 0, '~> stderr is empty');
1034+
1035+
let pid3 = exec('default.js', ['--foo', 'bar', 'b', '-h']);
1036+
t.is(pid3.status, 0, 'exits without error code');
1037+
t.true(pid3.stdout.toString().includes('Usage\n $ bin bar [options]'), '~> shows command help w/ "Usage" text');
1038+
t.is(pid3.stderr.length, 0, '~> stderr is empty');
1039+
1040+
t.end();
1041+
});
1042+
1043+
test('(usage) order :: single :: throws', t => {
1044+
let pid = exec('alias1.js', ['--foo', 'bar']);
1045+
t.is(pid.status, 1, 'exits with error code');
1046+
t.is(pid.stdout.length, 0, '~> stdout is empty');
1047+
// throws an error in the process
1048+
t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` in "single" mode'), '~> threw Error w/ message');
1049+
t.end();
1050+
});
1051+
1052+
test('(usage) order :: pre-command :: throws', t => {
1053+
let pid = exec('alias2.js', ['--foo', 'bar']);
1054+
t.is(pid.status, 1, 'exits with error code');
1055+
t.is(pid.stdout.length, 0, '~> stdout is empty');
1056+
// throws an error in the process
1057+
t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` before defining a command'), '~> threw Error w/ message');
1058+
t.end();
1059+
});

0 commit comments

Comments
 (0)