1/* vi: set sw=4 ts=4: */
2/*
3 * test implementation for busybox
4 *
5 * Copyright (c) by a whole pile of folks:
6 *
7 *     test(1); version 7-like  --  author Erik Baalbergen
8 *     modified by Eric Gisin to be used as built-in.
9 *     modified by Arnold Robbins to add SVR3 compatibility
10 *     (-x -c -b -p -u -g -k) plus Korn's -L -nt -ot -ef and new -S (socket).
11 *     modified by J.T. Conklin for NetBSD.
12 *     modified by Herbert Xu to be used as built-in in ash.
13 *     modified by Erik Andersen <andersen@codepoet.org> to be used
14 *     in busybox.
15 *     modified by Bernhard Reutner-Fischer to be useable (i.e. a bit less bloaty).
16 *
17 * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
18 *
19 * Original copyright notice states:
20 *     "This program is in the Public Domain."
21 */
22
23//kbuild:lib-$(CONFIG_TEST)      += test.o test_ptr_hack.o
24//kbuild:lib-$(CONFIG_ASH)       += test.o test_ptr_hack.o
25//kbuild:lib-$(CONFIG_HUSH)      += test.o test_ptr_hack.o
26
27//config:config TEST
28//config:	bool "test"
29//config:	default y
30//config:	help
31//config:	  test is used to check file types and compare values,
32//config:	  returning an appropriate exit code. The bash shell
33//config:	  has test built in, ash can build it in optionally.
34//config:
35//config:config FEATURE_TEST_64
36//config:	bool "Extend test to 64 bit"
37//config:	default y
38//config:	depends on TEST || ASH_BUILTIN_TEST || HUSH
39//config:	help
40//config:	  Enable 64-bit support in test.
41
42#include "libbb.h"
43#include <setjmp.h>
44
45/* This is a NOFORK applet. Be very careful! */
46
47/* test_main() is called from shells, and we need to be extra careful here.
48 * This is true regardless of PREFER_APPLETS and STANDALONE_SHELL
49 * state. */
50
51/* test(1) accepts the following grammar:
52	oexpr	::= aexpr | aexpr "-o" oexpr ;
53	aexpr	::= nexpr | nexpr "-a" aexpr ;
54	nexpr	::= primary | "!" primary
55	primary ::= unary-operator operand
56		| operand binary-operator operand
57		| operand
58		| "(" oexpr ")"
59		;
60	unary-operator ::= "-r"|"-w"|"-x"|"-f"|"-d"|"-c"|"-b"|"-p"|
61		"-u"|"-g"|"-k"|"-s"|"-t"|"-z"|"-n"|"-o"|"-O"|"-G"|"-L"|"-S";
62
63	binary-operator ::= "="|"=="|"!="|"-eq"|"-ne"|"-ge"|"-gt"|"-le"|"-lt"|
64			"-nt"|"-ot"|"-ef";
65	operand ::= <any legal UNIX file name>
66*/
67
68/* TODO: handle [[ expr ]] bashism bash-compatibly.
69 * [[ ]] is meant to be a "better [ ]", with less weird syntax
70 * and without the risk of variables and quoted strings misinterpreted
71 * as operators.
72 * This will require support from shells - we need to know quote status
73 * of each parameter (see below).
74 *
75 * Word splitting and pathname expansion should NOT be performed:
76 *      # a="a b"; [[ $a = "a b" ]] && echo YES
77 *      YES
78 *      # [[ /bin/m* ]] && echo YES
79 *      YES
80 *
81 * =~ should do regexp match
82 * = and == should do pattern match against right side:
83 *      # [[ *a* == bab ]] && echo YES
84 *      # [[ bab == *a* ]] && echo YES
85 *      YES
86 * != does the negated == (i.e., also with pattern matching).
87 * Pattern matching is quotation-sensitive:
88 *      # [[ bab == "b"a* ]] && echo YES
89 *      YES
90 *      # [[ bab == b"a*" ]] && echo YES
91 *
92 * Conditional operators such as -f must be unquoted literals to be recognized:
93 *      # [[ -e /bin ]] && echo YES
94 *      YES
95 *      # [[ '-e' /bin ]] && echo YES
96 *      bash: conditional binary operator expected...
97 *      # A='-e'; [[ $A /bin ]] && echo YES
98 *      bash: conditional binary operator expected...
99 *
100 * || and && should work as -o and -a work in [ ]
101 * -a and -o aren't recognized (&& and || are to be used instead)
102 * ( and ) do not need to be quoted unlike in [ ]:
103 *      # [[ ( abc ) && '' ]] && echo YES
104 *      # [[ ( abc ) || '' ]] && echo YES
105 *      YES
106 *      # [[ ( abc ) -o '' ]] && echo YES
107 *      bash: syntax error in conditional expression...
108 *
109 * Apart from the above, [[ expr ]] should work as [ expr ]
110 */
111
112#define TEST_DEBUG 0
113
114enum token {
115	EOI,
116
117	FILRD, /* file access */
118	FILWR,
119	FILEX,
120
121	FILEXIST,
122
123	FILREG, /* file type */
124	FILDIR,
125	FILCDEV,
126	FILBDEV,
127	FILFIFO,
128	FILSOCK,
129
130	FILSYM,
131	FILGZ,
132	FILTT,
133
134	FILSUID, /* file bit */
135	FILSGID,
136	FILSTCK,
137
138	FILNT, /* file ops */
139	FILOT,
140	FILEQ,
141
142	FILUID,
143	FILGID,
144
145	STREZ, /* str ops */
146	STRNZ,
147	STREQ,
148	STRNE,
149	STRLT,
150	STRGT,
151
152	INTEQ, /* int ops */
153	INTNE,
154	INTGE,
155	INTGT,
156	INTLE,
157	INTLT,
158
159	UNOT,
160	BAND,
161	BOR,
162	LPAREN,
163	RPAREN,
164	OPERAND
165};
166#define is_int_op(a)      (((unsigned char)((a) - INTEQ)) <= 5)
167#define is_str_op(a)      (((unsigned char)((a) - STREZ)) <= 5)
168#define is_file_op(a)     (((unsigned char)((a) - FILNT)) <= 2)
169#define is_file_access(a) (((unsigned char)((a) - FILRD)) <= 2)
170#define is_file_type(a)   (((unsigned char)((a) - FILREG)) <= 5)
171#define is_file_bit(a)    (((unsigned char)((a) - FILSUID)) <= 2)
172
173#if TEST_DEBUG
174int depth;
175#define nest_msg(...) do { \
176	depth++; \
177	fprintf(stderr, "%*s", depth*2, ""); \
178	fprintf(stderr, __VA_ARGS__); \
179} while (0)
180#define unnest_msg(...) do { \
181	fprintf(stderr, "%*s", depth*2, ""); \
182	fprintf(stderr, __VA_ARGS__); \
183	depth--; \
184} while (0)
185#define dbg_msg(...) do { \
186	fprintf(stderr, "%*s", depth*2, ""); \
187	fprintf(stderr, __VA_ARGS__); \
188} while (0)
189#define unnest_msg_and_return(expr, ...) do { \
190	number_t __res = (expr); \
191	fprintf(stderr, "%*s", depth*2, ""); \
192	fprintf(stderr, __VA_ARGS__, res); \
193	depth--; \
194	return __res; \
195} while (0)
196static const char *const TOKSTR[] = {
197	"EOI",
198	"FILRD",
199	"FILWR",
200	"FILEX",
201	"FILEXIST",
202	"FILREG",
203	"FILDIR",
204	"FILCDEV",
205	"FILBDEV",
206	"FILFIFO",
207	"FILSOCK",
208	"FILSYM",
209	"FILGZ",
210	"FILTT",
211	"FILSUID",
212	"FILSGID",
213	"FILSTCK",
214	"FILNT",
215	"FILOT",
216	"FILEQ",
217	"FILUID",
218	"FILGID",
219	"STREZ",
220	"STRNZ",
221	"STREQ",
222	"STRNE",
223	"STRLT",
224	"STRGT",
225	"INTEQ",
226	"INTNE",
227	"INTGE",
228	"INTGT",
229	"INTLE",
230	"INTLT",
231	"UNOT",
232	"BAND",
233	"BOR",
234	"LPAREN",
235	"RPAREN",
236	"OPERAND"
237};
238#else
239#define nest_msg(...)   ((void)0)
240#define unnest_msg(...) ((void)0)
241#define dbg_msg(...)    ((void)0)
242#define unnest_msg_and_return(expr, ...) return expr
243#endif
244
245enum {
246	UNOP,
247	BINOP,
248	BUNOP,
249	BBINOP,
250	PAREN
251};
252
253struct operator_t {
254	unsigned char op_num, op_type;
255};
256
257static const struct operator_t ops_table[] = {
258	{ /* "-r" */ FILRD   , UNOP   },
259	{ /* "-w" */ FILWR   , UNOP   },
260	{ /* "-x" */ FILEX   , UNOP   },
261	{ /* "-e" */ FILEXIST, UNOP   },
262	{ /* "-f" */ FILREG  , UNOP   },
263	{ /* "-d" */ FILDIR  , UNOP   },
264	{ /* "-c" */ FILCDEV , UNOP   },
265	{ /* "-b" */ FILBDEV , UNOP   },
266	{ /* "-p" */ FILFIFO , UNOP   },
267	{ /* "-u" */ FILSUID , UNOP   },
268	{ /* "-g" */ FILSGID , UNOP   },
269	{ /* "-k" */ FILSTCK , UNOP   },
270	{ /* "-s" */ FILGZ   , UNOP   },
271	{ /* "-t" */ FILTT   , UNOP   },
272	{ /* "-z" */ STREZ   , UNOP   },
273	{ /* "-n" */ STRNZ   , UNOP   },
274	{ /* "-h" */ FILSYM  , UNOP   },    /* for backwards compat */
275
276	{ /* "-O" */ FILUID  , UNOP   },
277	{ /* "-G" */ FILGID  , UNOP   },
278	{ /* "-L" */ FILSYM  , UNOP   },
279	{ /* "-S" */ FILSOCK , UNOP   },
280	{ /* "="  */ STREQ   , BINOP  },
281	{ /* "==" */ STREQ   , BINOP  },
282	{ /* "!=" */ STRNE   , BINOP  },
283	{ /* "<"  */ STRLT   , BINOP  },
284	{ /* ">"  */ STRGT   , BINOP  },
285	{ /* "-eq"*/ INTEQ   , BINOP  },
286	{ /* "-ne"*/ INTNE   , BINOP  },
287	{ /* "-ge"*/ INTGE   , BINOP  },
288	{ /* "-gt"*/ INTGT   , BINOP  },
289	{ /* "-le"*/ INTLE   , BINOP  },
290	{ /* "-lt"*/ INTLT   , BINOP  },
291	{ /* "-nt"*/ FILNT   , BINOP  },
292	{ /* "-ot"*/ FILOT   , BINOP  },
293	{ /* "-ef"*/ FILEQ   , BINOP  },
294	{ /* "!"  */ UNOT    , BUNOP  },
295	{ /* "-a" */ BAND    , BBINOP },
296	{ /* "-o" */ BOR     , BBINOP },
297	{ /* "("  */ LPAREN  , PAREN  },
298	{ /* ")"  */ RPAREN  , PAREN  },
299};
300/* Please keep these two tables in sync */
301static const char ops_texts[] ALIGN1 =
302	"-r"  "\0"
303	"-w"  "\0"
304	"-x"  "\0"
305	"-e"  "\0"
306	"-f"  "\0"
307	"-d"  "\0"
308	"-c"  "\0"
309	"-b"  "\0"
310	"-p"  "\0"
311	"-u"  "\0"
312	"-g"  "\0"
313	"-k"  "\0"
314	"-s"  "\0"
315	"-t"  "\0"
316	"-z"  "\0"
317	"-n"  "\0"
318	"-h"  "\0"
319
320	"-O"  "\0"
321	"-G"  "\0"
322	"-L"  "\0"
323	"-S"  "\0"
324	"="   "\0"
325	"=="  "\0"
326	"!="  "\0"
327	"<"   "\0"
328	">"   "\0"
329	"-eq" "\0"
330	"-ne" "\0"
331	"-ge" "\0"
332	"-gt" "\0"
333	"-le" "\0"
334	"-lt" "\0"
335	"-nt" "\0"
336	"-ot" "\0"
337	"-ef" "\0"
338	"!"   "\0"
339	"-a"  "\0"
340	"-o"  "\0"
341	"("   "\0"
342	")"   "\0"
343;
344
345
346#if ENABLE_FEATURE_TEST_64
347typedef int64_t number_t;
348#else
349typedef int number_t;
350#endif
351
352
353/* We try to minimize both static and stack usage. */
354struct test_statics {
355	char **args;
356	/* set only by check_operator(), either to bogus struct
357	 * or points to matching operator_t struct. Never NULL. */
358	const struct operator_t *last_operator;
359	gid_t *group_array;
360	int ngroups;
361	jmp_buf leaving;
362};
363
364/* See test_ptr_hack.c */
365extern struct test_statics *const test_ptr_to_statics;
366
367#define S (*test_ptr_to_statics)
368#define args            (S.args         )
369#define last_operator   (S.last_operator)
370#define group_array     (S.group_array  )
371#define ngroups         (S.ngroups      )
372#define leaving         (S.leaving      )
373
374#define INIT_S() do { \
375	(*(struct test_statics**)&test_ptr_to_statics) = xzalloc(sizeof(S)); \
376	barrier(); \
377} while (0)
378#define DEINIT_S() do { \
379	free(test_ptr_to_statics); \
380} while (0)
381
382static number_t primary(enum token n);
383
384static void syntax(const char *op, const char *msg) NORETURN;
385static void syntax(const char *op, const char *msg)
386{
387	if (op && *op) {
388		bb_error_msg("%s: %s", op, msg);
389	} else {
390		bb_error_msg("%s: %s"+4, msg);
391	}
392	longjmp(leaving, 2);
393}
394
395/* atoi with error detection */
396//XXX: FIXME: duplicate of existing libbb function?
397static number_t getn(const char *s)
398{
399	char *p;
400#if ENABLE_FEATURE_TEST_64
401	long long r;
402#else
403	long r;
404#endif
405
406	errno = 0;
407#if ENABLE_FEATURE_TEST_64
408	r = strtoll(s, &p, 10);
409#else
410	r = strtol(s, &p, 10);
411#endif
412
413	if (errno != 0)
414		syntax(s, "out of range");
415
416	if (p == s || *(skip_whitespace(p)) != '\0')
417		syntax(s, "bad number");
418
419	return r;
420}
421
422/* UNUSED
423static int newerf(const char *f1, const char *f2)
424{
425	struct stat b1, b2;
426
427	return (stat(f1, &b1) == 0 &&
428			stat(f2, &b2) == 0 && b1.st_mtime > b2.st_mtime);
429}
430
431static int olderf(const char *f1, const char *f2)
432{
433	struct stat b1, b2;
434
435	return (stat(f1, &b1) == 0 &&
436			stat(f2, &b2) == 0 && b1.st_mtime < b2.st_mtime);
437}
438
439static int equalf(const char *f1, const char *f2)
440{
441	struct stat b1, b2;
442
443	return (stat(f1, &b1) == 0 &&
444			stat(f2, &b2) == 0 &&
445			b1.st_dev == b2.st_dev && b1.st_ino == b2.st_ino);
446}
447*/
448
449
450static enum token check_operator(const char *s)
451{
452	static const struct operator_t no_op = {
453		.op_num = -1,
454		.op_type = -1
455	};
456	int n;
457
458	last_operator = &no_op;
459	if (s == NULL)
460		return EOI;
461	n = index_in_strings(ops_texts, s);
462	if (n < 0)
463		return OPERAND;
464	last_operator = &ops_table[n];
465	return ops_table[n].op_num;
466}
467
468
469static int binop(void)
470{
471	const char *opnd1, *opnd2;
472	const struct operator_t *op;
473	number_t val1, val2;
474
475	opnd1 = *args;
476	check_operator(*++args);
477	op = last_operator;
478
479	opnd2 = *++args;
480	if (opnd2 == NULL)
481		syntax(args[-1], "argument expected");
482
483	if (is_int_op(op->op_num)) {
484		val1 = getn(opnd1);
485		val2 = getn(opnd2);
486		if (op->op_num == INTEQ)
487			return val1 == val2;
488		if (op->op_num == INTNE)
489			return val1 != val2;
490		if (op->op_num == INTGE)
491			return val1 >= val2;
492		if (op->op_num == INTGT)
493			return val1 >  val2;
494		if (op->op_num == INTLE)
495			return val1 <= val2;
496		/*if (op->op_num == INTLT)*/
497		return val1 <  val2;
498	}
499	if (is_str_op(op->op_num)) {
500		val1 = strcmp(opnd1, opnd2);
501		if (op->op_num == STREQ)
502			return val1 == 0;
503		if (op->op_num == STRNE)
504			return val1 != 0;
505		if (op->op_num == STRLT)
506			return val1 < 0;
507		/*if (op->op_num == STRGT)*/
508		return val1 > 0;
509	}
510	/* We are sure that these three are by now the only binops we didn't check
511	 * yet, so we do not check if the class is correct:
512	 */
513/*	if (is_file_op(op->op_num)) */
514	{
515		struct stat b1, b2;
516
517		if (stat(opnd1, &b1) || stat(opnd2, &b2))
518			return 0; /* false, since at least one stat failed */
519		if (op->op_num == FILNT)
520			return b1.st_mtime > b2.st_mtime;
521		if (op->op_num == FILOT)
522			return b1.st_mtime < b2.st_mtime;
523		/*if (op->op_num == FILEQ)*/
524		return b1.st_dev == b2.st_dev && b1.st_ino == b2.st_ino;
525	}
526	/*return 1; - NOTREACHED */
527}
528
529
530static void initialize_group_array(void)
531{
532	int n;
533
534	/* getgroups may be expensive, try to use it only once */
535	ngroups = 32;
536	do {
537		/* FIXME: ash tries so hard to not die on OOM,
538		 * and we spoil it with just one xrealloc here */
539		/* We realloc, because test_main can be entered repeatedly by shell.
540		 * Testcase (ash): 'while true; do test -x some_file; done'
541		 * and watch top. (some_file must have owner != you) */
542		n = ngroups;
543		group_array = xrealloc(group_array, n * sizeof(gid_t));
544		ngroups = getgroups(n, group_array);
545	} while (ngroups > n);
546}
547
548
549/* Return non-zero if GID is one that we have in our groups list. */
550//XXX: FIXME: duplicate of existing libbb function?
551// see toplevel TODO file:
552// possible code duplication ingroup() and is_a_group_member()
553static int is_a_group_member(gid_t gid)
554{
555	int i;
556
557	/* Short-circuit if possible, maybe saving a call to getgroups(). */
558	if (gid == getgid() || gid == getegid())
559		return 1;
560
561	if (ngroups == 0)
562		initialize_group_array();
563
564	/* Search through the list looking for GID. */
565	for (i = 0; i < ngroups; i++)
566		if (gid == group_array[i])
567			return 1;
568
569	return 0;
570}
571
572
573/* Do the same thing access(2) does, but use the effective uid and gid,
574   and don't make the mistake of telling root that any file is
575   executable. */
576static int test_eaccess(char *path, int mode)
577{
578	struct stat st;
579	unsigned int euid = geteuid();
580
581	if (stat(path, &st) < 0)
582		return -1;
583
584	if (euid == 0) {
585		/* Root can read or write any file. */
586		if (mode != X_OK)
587			return 0;
588
589		/* Root can execute any file that has any one of the execute
590		   bits set. */
591		if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
592			return 0;
593	}
594
595	if (st.st_uid == euid)  /* owner */
596		mode <<= 6;
597	else if (is_a_group_member(st.st_gid))
598		mode <<= 3;
599
600	if (st.st_mode & mode)
601		return 0;
602
603	return -1;
604}
605
606
607static int filstat(char *nm, enum token mode)
608{
609	struct stat s;
610	unsigned i = i; /* gcc 3.x thinks it can be used uninitialized */
611
612	if (mode == FILSYM) {
613#ifdef S_IFLNK
614		if (lstat(nm, &s) == 0) {
615			i = S_IFLNK;
616			goto filetype;
617		}
618#endif
619		return 0;
620	}
621
622	if (stat(nm, &s) != 0)
623		return 0;
624	if (mode == FILEXIST)
625		return 1;
626	if (is_file_access(mode)) {
627		if (mode == FILRD)
628			i = R_OK;
629		if (mode == FILWR)
630			i = W_OK;
631		if (mode == FILEX)
632			i = X_OK;
633		return test_eaccess(nm, i) == 0;
634	}
635	if (is_file_type(mode)) {
636		if (mode == FILREG)
637			i = S_IFREG;
638		if (mode == FILDIR)
639			i = S_IFDIR;
640		if (mode == FILCDEV)
641			i = S_IFCHR;
642		if (mode == FILBDEV)
643			i = S_IFBLK;
644		if (mode == FILFIFO) {
645#ifdef S_IFIFO
646			i = S_IFIFO;
647#else
648			return 0;
649#endif
650		}
651		if (mode == FILSOCK) {
652#ifdef S_IFSOCK
653			i = S_IFSOCK;
654#else
655			return 0;
656#endif
657		}
658 filetype:
659		return ((s.st_mode & S_IFMT) == i);
660	}
661	if (is_file_bit(mode)) {
662		if (mode == FILSUID)
663			i = S_ISUID;
664		if (mode == FILSGID)
665			i = S_ISGID;
666		if (mode == FILSTCK)
667			i = S_ISVTX;
668		return ((s.st_mode & i) != 0);
669	}
670	if (mode == FILGZ)
671		return s.st_size > 0L;
672	if (mode == FILUID)
673		return s.st_uid == geteuid();
674	if (mode == FILGID)
675		return s.st_gid == getegid();
676	return 1; /* NOTREACHED */
677}
678
679
680static number_t nexpr(enum token n)
681{
682	number_t res;
683
684	nest_msg(">nexpr(%s)\n", TOKSTR[n]);
685	if (n == UNOT) {
686		n = check_operator(*++args);
687		if (n == EOI) {
688			/* special case: [ ! ], [ a -a ! ] are valid */
689			/* IOW, "! ARG" may miss ARG */
690			unnest_msg("<nexpr:1 (!EOI)\n");
691			return 1;
692		}
693		res = !nexpr(n);
694		unnest_msg("<nexpr:%lld\n", res);
695		return res;
696	}
697	res = primary(n);
698	unnest_msg("<nexpr:%lld\n", res);
699	return res;
700}
701
702
703static number_t aexpr(enum token n)
704{
705	number_t res;
706
707	nest_msg(">aexpr(%s)\n", TOKSTR[n]);
708	res = nexpr(n);
709	dbg_msg("aexpr: nexpr:%lld, next args:%s\n", res, args[1]);
710	if (check_operator(*++args) == BAND) {
711		dbg_msg("aexpr: arg is AND, next args:%s\n", args[1]);
712		res = aexpr(check_operator(*++args)) && res;
713		unnest_msg("<aexpr:%lld\n", res);
714		return res;
715	}
716	args--;
717	unnest_msg("<aexpr:%lld, args:%s\n", res, args[0]);
718	return res;
719}
720
721
722static number_t oexpr(enum token n)
723{
724	number_t res;
725
726	nest_msg(">oexpr(%s)\n", TOKSTR[n]);
727	res = aexpr(n);
728	dbg_msg("oexpr: aexpr:%lld, next args:%s\n", res, args[1]);
729	if (check_operator(*++args) == BOR) {
730		dbg_msg("oexpr: next arg is OR, next args:%s\n", args[1]);
731		res = oexpr(check_operator(*++args)) || res;
732		unnest_msg("<oexpr:%lld\n", res);
733		return res;
734	}
735	args--;
736	unnest_msg("<oexpr:%lld, args:%s\n", res, args[0]);
737	return res;
738}
739
740
741static number_t primary(enum token n)
742{
743#if TEST_DEBUG
744	number_t res = res; /* for compiler */
745#else
746	number_t res;
747#endif
748	const struct operator_t *args0_op;
749
750	nest_msg(">primary(%s)\n", TOKSTR[n]);
751	if (n == EOI) {
752		syntax(NULL, "argument expected");
753	}
754	if (n == LPAREN) {
755		res = oexpr(check_operator(*++args));
756		if (check_operator(*++args) != RPAREN)
757			syntax(NULL, "closing paren expected");
758		unnest_msg("<primary:%lld\n", res);
759		return res;
760	}
761
762	/* coreutils 6.9 checks "is args[1] binop and args[2] exist?" first,
763	 * do the same */
764	args0_op = last_operator;
765	/* last_operator = operator at args[1] */
766	if (check_operator(args[1]) != EOI) { /* if args[1] != NULL */
767		if (args[2]) {
768			// coreutils also does this:
769			// if (args[3] && args[0]="-l" && args[2] is BINOP)
770			//	return binop(1 /* prepended by -l */);
771			if (last_operator->op_type == BINOP)
772				unnest_msg_and_return(binop(), "<primary: binop:%lld\n");
773		}
774	}
775	/* check "is args[0] unop?" second */
776	if (args0_op->op_type == UNOP) {
777		/* unary expression */
778		if (args[1] == NULL)
779//			syntax(args0_op->op_text, "argument expected");
780			goto check_emptiness;
781		args++;
782		if (n == STREZ)
783			unnest_msg_and_return(args[0][0] == '\0', "<primary:%lld\n");
784		if (n == STRNZ)
785			unnest_msg_and_return(args[0][0] != '\0', "<primary:%lld\n");
786		if (n == FILTT)
787			unnest_msg_and_return(isatty(getn(*args)), "<primary: isatty(%s)%lld\n", *args);
788		unnest_msg_and_return(filstat(*args, n), "<primary: filstat(%s):%lld\n", *args);
789	}
790
791	/*check_operator(args[1]); - already done */
792	if (last_operator->op_type == BINOP) {
793		/* args[2] is known to be NULL, isn't it bound to fail? */
794		unnest_msg_and_return(binop(), "<primary:%lld\n");
795	}
796 check_emptiness:
797	unnest_msg_and_return(args[0][0] != '\0', "<primary:%lld\n");
798}
799
800
801int test_main(int argc, char **argv)
802{
803	int res;
804	const char *arg0;
805//	bool negate = 0;
806
807	arg0 = bb_basename(argv[0]);
808	if (arg0[0] == '[') {
809		--argc;
810		if (!arg0[1]) { /* "[" ? */
811			if (NOT_LONE_CHAR(argv[argc], ']')) {
812				bb_error_msg("missing ]");
813				return 2;
814			}
815		} else { /* assuming "[[" */
816			if (strcmp(argv[argc], "]]") != 0) {
817				bb_error_msg("missing ]]");
818				return 2;
819			}
820		}
821		argv[argc] = NULL;
822	}
823
824	/* We must do DEINIT_S() prior to returning */
825	INIT_S();
826
827	res = setjmp(leaving);
828	if (res)
829		goto ret;
830
831	/* resetting ngroups is probably unnecessary.  it will
832	 * force a new call to getgroups(), which prevents using
833	 * group data fetched during a previous call.  but the
834	 * only way the group data could be stale is if there's
835	 * been an intervening call to setgroups(), and this
836	 * isn't likely in the case of a shell.  paranoia
837	 * prevails...
838	 */
839	/*ngroups = 0; - done by INIT_S() */
840
841	//argc--;
842	argv++;
843
844	/* Implement special cases from POSIX.2, section 4.62.4 */
845	if (!argv[0]) { /* "test" */
846		res = 1;
847		goto ret;
848	}
849#if 0
850// Now it's fixed in the parser and should not be needed
851	if (LONE_CHAR(argv[0], '!') && argv[1]) {
852		negate = 1;
853		//argc--;
854		argv++;
855	}
856	if (!argv[1]) { /* "test [!] arg" */
857		res = (*argv[0] == '\0');
858		goto ret;
859	}
860	if (argv[2] && !argv[3]) {
861		check_operator(argv[1]);
862		if (last_operator->op_type == BINOP) {
863			/* "test [!] arg1 <binary_op> arg2" */
864			args = argv;
865			res = (binop() == 0);
866			goto ret;
867		}
868	}
869
870	/* Some complex expression. Undo '!' removal */
871	if (negate) {
872		negate = 0;
873		//argc++;
874		argv--;
875	}
876#endif
877	args = argv;
878	res = !oexpr(check_operator(*args));
879
880	if (*args != NULL && *++args != NULL) {
881		/* TODO: example when this happens? */
882		bb_error_msg("%s: unknown operand", *args);
883		res = 2;
884	}
885 ret:
886	DEINIT_S();
887//	return negate ? !res : res;
888	return res;
889}
890