1use std::collections::HashMap;
18use std::fs::{self, Metadata};
19use std::os::unix::fs::MetadataExt;
20use std::path::Path;
21
22use crate::glob::pattern_match;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CondType {
27 Not, And, Or, StrEq, StrDeq, StrNeq, StrLt, StrGt, Nt, Ot, Ef, Eq, Ne, Lt, Gt, Le, Ge, Regex, FileTest(char),
57
58 Mod,
60 Modi,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum CondResult {
66 True, False, Error, OptionNotExist, }
71
72impl CondResult {
73 pub fn to_exit_code(self) -> i32 {
74 match self {
75 CondResult::True => 0,
76 CondResult::False => 1,
77 CondResult::Error => 2,
78 CondResult::OptionNotExist => 3,
79 }
80 }
81
82 pub fn from_bool(b: bool) -> Self {
83 if b {
84 CondResult::True
85 } else {
86 CondResult::False
87 }
88 }
89
90 pub fn negate(self) -> Self {
91 match self {
92 CondResult::True => CondResult::False,
93 CondResult::False => CondResult::True,
94 other => other,
95 }
96 }
97}
98
99pub struct CondEval<'a> {
101 options: &'a HashMap<String, bool>,
103 variables: &'a HashMap<String, String>,
105 posix_mode: bool,
107 tracing: bool,
109}
110
111impl<'a> CondEval<'a> {
112 pub fn new(options: &'a HashMap<String, bool>, variables: &'a HashMap<String, String>) -> Self {
113 CondEval {
114 options,
115 variables,
116 posix_mode: false,
117 tracing: false,
118 }
119 }
120
121 pub fn with_posix_mode(mut self, posix: bool) -> Self {
122 self.posix_mode = posix;
123 self
124 }
125
126 pub fn with_tracing(mut self, tracing: bool) -> Self {
127 self.tracing = tracing;
128 self
129 }
130
131 pub fn eval(&self, expr: &CondExpr) -> CondResult {
133 match expr {
134 CondExpr::Not(inner) => {
135 let result = self.eval(inner);
136 result.negate()
137 }
138
139 CondExpr::And(left, right) => {
140 let left_result = self.eval(left);
141 if left_result != CondResult::True {
142 return left_result;
143 }
144 self.eval(right)
145 }
146
147 CondExpr::Or(left, right) => {
148 let left_result = self.eval(left);
149 if left_result == CondResult::True {
150 return CondResult::True;
151 }
152 if left_result == CondResult::Error {
153 return CondResult::Error;
154 }
155 self.eval(right)
156 }
157
158 CondExpr::Unary(op, arg) => self.eval_unary(*op, arg),
159
160 CondExpr::Binary(op, left, right) => self.eval_binary(*op, left, right),
161
162 CondExpr::Ternary(_, _, _, _) => CondResult::Error, }
164 }
165
166 fn eval_unary(&self, op: char, arg: &str) -> CondResult {
167 match op {
168 'a' | 'e' => CondResult::from_bool(self.file_exists(arg)),
170 'b' => CondResult::from_bool(self.is_block_device(arg)),
171 'c' => CondResult::from_bool(self.is_char_device(arg)),
172 'd' => CondResult::from_bool(self.is_directory(arg)),
173 'f' => CondResult::from_bool(self.is_regular_file(arg)),
174 'g' => CondResult::from_bool(self.has_setgid(arg)),
175 'h' | 'L' => CondResult::from_bool(self.is_symlink(arg)),
176 'k' => CondResult::from_bool(self.has_sticky(arg)),
177 'p' => CondResult::from_bool(self.is_fifo(arg)),
178 'r' => CondResult::from_bool(self.is_readable(arg)),
179 's' => CondResult::from_bool(self.has_size(arg)),
180 'S' => CondResult::from_bool(self.is_socket(arg)),
181 'u' => CondResult::from_bool(self.has_setuid(arg)),
182 'w' => CondResult::from_bool(self.is_writable(arg)),
183 'x' => CondResult::from_bool(self.is_executable(arg)),
184 'O' => CondResult::from_bool(self.is_owned_by_euid(arg)),
185 'G' => CondResult::from_bool(self.is_owned_by_egid(arg)),
186 'N' => CondResult::from_bool(self.is_modified_since_read(arg)),
187
188 'n' => CondResult::from_bool(!arg.is_empty()),
190 'z' => CondResult::from_bool(arg.is_empty()),
191
192 'o' => self.test_option(arg),
194
195 'v' => CondResult::from_bool(self.variables.contains_key(arg)),
197
198 't' => {
200 if let Ok(fd) = arg.parse::<i32>() {
201 CondResult::from_bool(unsafe { libc::isatty(fd) } != 0)
202 } else {
203 CondResult::Error
204 }
205 }
206
207 _ => CondResult::Error,
208 }
209 }
210
211 fn eval_binary(&self, op: CondType, left: &str, right: &str) -> CondResult {
212 match op {
213 CondType::StrEq | CondType::StrDeq => {
215 if !self.posix_mode {
217 CondResult::from_bool(pattern_match(right, left, true, true))
218 } else {
219 CondResult::from_bool(left == right)
220 }
221 }
222 CondType::StrNeq => {
223 if !self.posix_mode {
224 CondResult::from_bool(!pattern_match(right, left, true, true))
225 } else {
226 CondResult::from_bool(left != right)
227 }
228 }
229 CondType::StrLt => CondResult::from_bool(left < right),
230 CondType::StrGt => CondResult::from_bool(left > right),
231
232 CondType::Eq => self.numeric_compare(left, right, |a, b| a == b),
234 CondType::Ne => self.numeric_compare(left, right, |a, b| a != b),
235 CondType::Lt => self.numeric_compare(left, right, |a, b| a < b),
236 CondType::Gt => self.numeric_compare(left, right, |a, b| a > b),
237 CondType::Le => self.numeric_compare(left, right, |a, b| a <= b),
238 CondType::Ge => self.numeric_compare(left, right, |a, b| a >= b),
239
240 CondType::Nt => self.file_newer_than(left, right),
242 CondType::Ot => self.file_older_than(left, right),
243 CondType::Ef => self.same_file(left, right),
244
245 CondType::Regex => self.regex_match(left, right),
247
248 _ => CondResult::Error,
249 }
250 }
251
252 fn get_metadata(&self, path: &str) -> Option<Metadata> {
255 if let Some(fd_str) = path.strip_prefix("/dev/fd/") {
257 if let Ok(fd) = fd_str.parse::<i32>() {
258 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
260 if unsafe { libc::fstat(fd, &mut stat) } == 0 {
261 return fs::metadata(path).ok();
264 }
265 }
266 }
267 fs::metadata(path).ok()
268 }
269
270 fn get_symlink_metadata(&self, path: &str) -> Option<Metadata> {
271 fs::symlink_metadata(path).ok()
272 }
273
274 fn file_exists(&self, path: &str) -> bool {
275 Path::new(path).exists()
276 }
277
278 fn is_block_device(&self, path: &str) -> bool {
279 self.get_metadata(path)
280 .map(|m| m.mode() & libc::S_IFMT as u32 == libc::S_IFBLK as u32)
281 .unwrap_or(false)
282 }
283
284 fn is_char_device(&self, path: &str) -> bool {
285 self.get_metadata(path)
286 .map(|m| m.mode() & libc::S_IFMT as u32 == libc::S_IFCHR as u32)
287 .unwrap_or(false)
288 }
289
290 fn is_directory(&self, path: &str) -> bool {
291 Path::new(path).is_dir()
292 }
293
294 fn is_regular_file(&self, path: &str) -> bool {
295 Path::new(path).is_file()
296 }
297
298 fn is_symlink(&self, path: &str) -> bool {
299 self.get_symlink_metadata(path)
300 .map(|m| m.file_type().is_symlink())
301 .unwrap_or(false)
302 }
303
304 fn is_fifo(&self, path: &str) -> bool {
305 self.get_metadata(path)
306 .map(|m| m.mode() & libc::S_IFMT as u32 == libc::S_IFIFO as u32)
307 .unwrap_or(false)
308 }
309
310 fn is_socket(&self, path: &str) -> bool {
311 self.get_metadata(path)
312 .map(|m| m.mode() & libc::S_IFMT as u32 == libc::S_IFSOCK as u32)
313 .unwrap_or(false)
314 }
315
316 fn has_setuid(&self, path: &str) -> bool {
317 self.get_metadata(path)
318 .map(|m| m.mode() & libc::S_ISUID as u32 != 0)
319 .unwrap_or(false)
320 }
321
322 fn has_setgid(&self, path: &str) -> bool {
323 self.get_metadata(path)
324 .map(|m| m.mode() & libc::S_ISGID as u32 != 0)
325 .unwrap_or(false)
326 }
327
328 fn has_sticky(&self, path: &str) -> bool {
329 self.get_metadata(path)
330 .map(|m| m.mode() & libc::S_ISVTX as u32 != 0)
331 .unwrap_or(false)
332 }
333
334 fn is_readable(&self, path: &str) -> bool {
335 use std::ffi::CString;
336 if let Ok(c_path) = CString::new(path) {
337 unsafe { libc::access(c_path.as_ptr(), libc::R_OK) == 0 }
338 } else {
339 fs::metadata(path).is_ok()
340 }
341 }
342
343 fn is_writable(&self, path: &str) -> bool {
344 use std::ffi::CString;
345 if let Ok(c_path) = CString::new(path) {
346 unsafe { libc::access(c_path.as_ptr(), libc::W_OK) == 0 }
347 } else {
348 self.get_metadata(path)
349 .map(|m| m.mode() & 0o200 != 0)
350 .unwrap_or(false)
351 }
352 }
353
354 fn is_executable(&self, path: &str) -> bool {
355 self.get_metadata(path)
356 .map(|m| {
357 let mode = m.mode();
358 (mode & 0o111 != 0) || (mode & libc::S_IFMT as u32 == libc::S_IFDIR as u32)
360 })
361 .unwrap_or(false)
362 }
363
364 fn has_size(&self, path: &str) -> bool {
365 self.get_metadata(path)
366 .map(|m| m.len() > 0)
367 .unwrap_or(false)
368 }
369
370 fn is_owned_by_euid(&self, path: &str) -> bool {
371 self.get_metadata(path)
372 .map(|m| m.uid() == unsafe { libc::geteuid() })
373 .unwrap_or(false)
374 }
375
376 fn is_owned_by_egid(&self, path: &str) -> bool {
377 self.get_metadata(path)
378 .map(|m| m.gid() == unsafe { libc::getegid() })
379 .unwrap_or(false)
380 }
381
382 fn is_modified_since_read(&self, path: &str) -> bool {
383 self.get_metadata(path)
384 .map(|m| m.mtime() >= m.atime())
385 .unwrap_or(false)
386 }
387
388 fn numeric_compare<F>(&self, left: &str, right: &str, cmp: F) -> CondResult
391 where
392 F: Fn(f64, f64) -> bool,
393 {
394 let left_val = self.parse_number(left);
395 let right_val = self.parse_number(right);
396
397 match (left_val, right_val) {
398 (Some(l), Some(r)) => CondResult::from_bool(cmp(l, r)),
399 _ => CondResult::Error,
400 }
401 }
402
403 fn parse_number(&self, s: &str) -> Option<f64> {
404 if self.posix_mode {
406 s.trim().parse::<i64>().ok().map(|i| i as f64)
407 } else {
408 if let Ok(i) = s.trim().parse::<i64>() {
410 Some(i as f64)
411 } else {
412 s.trim().parse::<f64>().ok()
413 }
414 }
415 }
416
417 fn file_newer_than(&self, left: &str, right: &str) -> CondResult {
420 let left_meta = match self.get_metadata(left) {
421 Some(m) => m,
422 None => return CondResult::False,
423 };
424 let right_meta = match self.get_metadata(right) {
425 Some(m) => m,
426 None => return CondResult::False,
427 };
428
429 CondResult::from_bool(left_meta.mtime() > right_meta.mtime())
430 }
431
432 fn file_older_than(&self, left: &str, right: &str) -> CondResult {
433 let left_meta = match self.get_metadata(left) {
434 Some(m) => m,
435 None => return CondResult::False,
436 };
437 let right_meta = match self.get_metadata(right) {
438 Some(m) => m,
439 None => return CondResult::False,
440 };
441
442 CondResult::from_bool(left_meta.mtime() < right_meta.mtime())
443 }
444
445 fn same_file(&self, left: &str, right: &str) -> CondResult {
446 let left_meta = match self.get_metadata(left) {
447 Some(m) => m,
448 None => return CondResult::False,
449 };
450 let right_meta = match self.get_metadata(right) {
451 Some(m) => m,
452 None => return CondResult::False,
453 };
454
455 CondResult::from_bool(
456 left_meta.dev() == right_meta.dev() && left_meta.ino() == right_meta.ino(),
457 )
458 }
459
460 fn test_option(&self, name: &str) -> CondResult {
463 if name.len() == 1 {
465 let ch = name.chars().next().unwrap();
466 if let Some(opt_name) = short_option_name(ch) {
467 if let Some(&val) = self.options.get(opt_name) {
468 return CondResult::from_bool(val);
469 }
470 }
471 }
472
473 if let Some(&val) = self.options.get(name) {
475 CondResult::from_bool(val)
476 } else {
477 CondResult::OptionNotExist
478 }
479 }
480
481 fn regex_match(&self, text: &str, pattern: &str) -> CondResult {
484 #[cfg(feature = "regex")]
485 {
486 match regex::Regex::new(pattern) {
487 Ok(re) => CondResult::from_bool(re.is_match(text)),
488 Err(_) => CondResult::Error,
489 }
490 }
491 #[cfg(not(feature = "regex"))]
492 {
493 CondResult::from_bool(pattern_match(pattern, text, true, true))
495 }
496 }
497}
498
499fn short_option_name(c: char) -> Option<&'static str> {
501 Some(match c {
502 'a' => "allexport",
503 'B' => "braceccl",
504 'C' => "noclobber",
505 'e' => "errexit",
506 'f' => "noglob",
507 'g' => "histignorespace",
508 'h' => "hashcmds",
509 'H' => "histexpand",
510 'i' => "interactive",
511 'I' => "ignoreeof",
512 'j' => "monitor",
513 'k' => "keywordargs",
514 'l' => "login",
515 'm' => "monitor",
516 'n' => "noexec",
517 'p' => "privileged",
518 'P' => "physical",
519 'r' => "restricted",
520 's' => "stdin",
521 't' => "singlecommand",
522 'u' => "nounset",
523 'v' => "verbose",
524 'w' => "chaselinks",
525 'x' => "xtrace",
526 'X' => "listtypes",
527 'Y' => "menucomplete",
528 'Z' => "zle",
529 '0' => "correct",
530 '1' => "printexitvalue",
531 '2' => "autolist",
532 '3' => "autocontinue",
533 '4' => "autoparamslash",
534 '5' => "autopushd",
535 '6' => "autoremoveslash",
536 '7' => "bsdecho",
537 '8' => "nocaseglob",
538 '9' => "cdablevars",
539 _ => return None,
540 })
541}
542
543#[derive(Debug, Clone)]
545pub enum CondExpr {
546 Not(Box<CondExpr>),
547 And(Box<CondExpr>, Box<CondExpr>),
548 Or(Box<CondExpr>, Box<CondExpr>),
549 Unary(char, String),
550 Binary(CondType, String, String),
551 Ternary(CondType, String, String, String),
552}
553
554pub struct CondParser<'a> {
556 tokens: Vec<&'a str>,
557 pos: usize,
558 posix_mode: bool,
559}
560
561impl<'a> CondParser<'a> {
562 pub fn new(tokens: Vec<&'a str>, posix_mode: bool) -> Self {
563 CondParser {
564 tokens,
565 pos: 0,
566 posix_mode,
567 }
568 }
569
570 pub fn parse(&mut self) -> Result<CondExpr, String> {
571 self.parse_or()
572 }
573
574 fn parse_or(&mut self) -> Result<CondExpr, String> {
575 let mut left = self.parse_and()?;
576
577 while self.match_token("||") || self.match_token("-o") {
578 let right = self.parse_and()?;
579 left = CondExpr::Or(Box::new(left), Box::new(right));
580 }
581
582 Ok(left)
583 }
584
585 fn parse_and(&mut self) -> Result<CondExpr, String> {
586 let mut left = self.parse_not()?;
587
588 while self.match_token("&&") || self.match_token("-a") {
589 let right = self.parse_not()?;
590 left = CondExpr::And(Box::new(left), Box::new(right));
591 }
592
593 Ok(left)
594 }
595
596 fn parse_not(&mut self) -> Result<CondExpr, String> {
597 if self.match_token("!") {
598 let inner = self.parse_not()?;
599 Ok(CondExpr::Not(Box::new(inner)))
600 } else {
601 self.parse_primary()
602 }
603 }
604
605 fn parse_primary(&mut self) -> Result<CondExpr, String> {
606 if self.match_token("(") {
608 let expr = self.parse_or()?;
609 if !self.match_token(")") {
610 return Err("missing )".to_string());
611 }
612 return Ok(expr);
613 }
614
615 if let Some(tok) = self.peek() {
617 if tok.starts_with('-') && tok.len() == 2 {
618 let op = tok.chars().nth(1).unwrap();
619 if is_unary_op(op) {
621 self.advance();
622 let arg = self.expect_arg()?;
623 return Ok(CondExpr::Unary(op, arg.to_string()));
624 }
625 }
626 }
627
628 let left = self.expect_arg()?;
630
631 if let Some(op) = self.peek() {
632 if let Some(cond_type) = parse_binary_op(op) {
633 self.advance();
634 let right = self.expect_arg()?;
635 return Ok(CondExpr::Binary(
636 cond_type,
637 left.to_string(),
638 right.to_string(),
639 ));
640 }
641 }
642
643 Ok(CondExpr::Unary('n', left.to_string()))
645 }
646
647 fn peek(&self) -> Option<&'a str> {
648 self.tokens.get(self.pos).copied()
649 }
650
651 fn advance(&mut self) -> Option<&'a str> {
652 let tok = self.tokens.get(self.pos).copied();
653 self.pos += 1;
654 tok
655 }
656
657 fn match_token(&mut self, expected: &str) -> bool {
658 if self.peek() == Some(expected) {
659 self.advance();
660 true
661 } else {
662 false
663 }
664 }
665
666 fn expect_arg(&mut self) -> Result<&'a str, String> {
667 self.advance()
668 .ok_or_else(|| "expected argument".to_string())
669 }
670}
671
672fn is_unary_op(c: char) -> bool {
673 matches!(
674 c,
675 'a' | 'b'
676 | 'c'
677 | 'd'
678 | 'e'
679 | 'f'
680 | 'g'
681 | 'h'
682 | 'k'
683 | 'L'
684 | 'n'
685 | 'o'
686 | 'p'
687 | 'r'
688 | 's'
689 | 'S'
690 | 't'
691 | 'u'
692 | 'v'
693 | 'w'
694 | 'x'
695 | 'z'
696 | 'G'
697 | 'N'
698 | 'O'
699 )
700}
701
702fn parse_binary_op(s: &str) -> Option<CondType> {
703 Some(match s {
704 "=" | "==" => CondType::StrEq,
705 "!=" => CondType::StrNeq,
706 "<" => CondType::StrLt,
707 ">" => CondType::StrGt,
708 "-eq" => CondType::Eq,
709 "-ne" => CondType::Ne,
710 "-lt" => CondType::Lt,
711 "-gt" => CondType::Gt,
712 "-le" => CondType::Le,
713 "-ge" => CondType::Ge,
714 "-nt" => CondType::Nt,
715 "-ot" => CondType::Ot,
716 "-ef" => CondType::Ef,
717 "=~" => CondType::Regex,
718 _ => return None,
719 })
720}
721
722pub fn eval_test(
724 args: &[&str],
725 options: &HashMap<String, bool>,
726 variables: &HashMap<String, String>,
727 posix_mode: bool,
728) -> i32 {
729 if args.is_empty() {
731 return 1; }
733
734 let args: Vec<&str> = args
736 .iter()
737 .filter(|&s| *s != "[" && *s != "]" && *s != "[[" && *s != "]]")
738 .copied()
739 .collect();
740
741 if args.is_empty() {
742 return 1;
743 }
744
745 let mut parser = CondParser::new(args, posix_mode);
746 match parser.parse() {
747 Ok(expr) => {
748 let evaluator = CondEval::new(options, variables).with_posix_mode(posix_mode);
749 evaluator.eval(&expr).to_exit_code()
750 }
751 Err(_) => 2, }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use std::fs::File;
759 use tempfile::TempDir;
760
761 fn empty_maps() -> (HashMap<String, bool>, HashMap<String, String>) {
762 (HashMap::new(), HashMap::new())
763 }
764
765 #[test]
766 fn test_string_empty() {
767 let (opts, vars) = empty_maps();
768 assert_eq!(eval_test(&["-z", ""], &opts, &vars, true), 0);
769 assert_eq!(eval_test(&["-z", "hello"], &opts, &vars, true), 1);
770 assert_eq!(eval_test(&["-n", "hello"], &opts, &vars, true), 0);
771 assert_eq!(eval_test(&["-n", ""], &opts, &vars, true), 1);
772 }
773
774 #[test]
775 fn test_string_compare() {
776 let (opts, vars) = empty_maps();
777 assert_eq!(eval_test(&["hello", "=", "hello"], &opts, &vars, true), 0);
778 assert_eq!(eval_test(&["hello", "!=", "world"], &opts, &vars, true), 0);
779 assert_eq!(eval_test(&["abc", "<", "def"], &opts, &vars, true), 0);
780 assert_eq!(eval_test(&["xyz", ">", "abc"], &opts, &vars, true), 0);
781 }
782
783 #[test]
784 fn test_numeric_compare() {
785 let (opts, vars) = empty_maps();
786 assert_eq!(eval_test(&["5", "-eq", "5"], &opts, &vars, true), 0);
787 assert_eq!(eval_test(&["5", "-ne", "3"], &opts, &vars, true), 0);
788 assert_eq!(eval_test(&["3", "-lt", "5"], &opts, &vars, true), 0);
789 assert_eq!(eval_test(&["5", "-gt", "3"], &opts, &vars, true), 0);
790 assert_eq!(eval_test(&["5", "-le", "5"], &opts, &vars, true), 0);
791 assert_eq!(eval_test(&["5", "-ge", "5"], &opts, &vars, true), 0);
792 }
793
794 #[test]
795 fn test_file_exists() {
796 let dir = TempDir::new().unwrap();
797 let file_path = dir.path().join("testfile");
798 File::create(&file_path).unwrap();
799
800 let (opts, vars) = empty_maps();
801 let path_str = file_path.to_str().unwrap();
802
803 assert_eq!(eval_test(&["-e", path_str], &opts, &vars, true), 0);
804 assert_eq!(eval_test(&["-f", path_str], &opts, &vars, true), 0);
805 assert_eq!(eval_test(&["-d", path_str], &opts, &vars, true), 1);
806 }
807
808 #[test]
809 fn test_directory() {
810 let dir = TempDir::new().unwrap();
811 let (opts, vars) = empty_maps();
812 let path_str = dir.path().to_str().unwrap();
813
814 assert_eq!(eval_test(&["-d", path_str], &opts, &vars, true), 0);
815 assert_eq!(eval_test(&["-f", path_str], &opts, &vars, true), 1);
816 }
817
818 #[test]
819 fn test_logical_not() {
820 let (opts, vars) = empty_maps();
821 assert_eq!(eval_test(&["!", "-z", "hello"], &opts, &vars, true), 0);
822 assert_eq!(eval_test(&["!", "-n", ""], &opts, &vars, true), 0);
823 }
824
825 #[test]
826 fn test_logical_and() {
827 let (opts, vars) = empty_maps();
828 assert_eq!(
829 eval_test(&["-n", "a", "-a", "-n", "b"], &opts, &vars, true),
830 0
831 );
832 assert_eq!(
833 eval_test(&["-n", "a", "-a", "-z", "b"], &opts, &vars, true),
834 1
835 );
836 }
837
838 #[test]
839 fn test_logical_or() {
840 let (opts, vars) = empty_maps();
841 assert_eq!(
842 eval_test(&["-z", "a", "-o", "-n", "b"], &opts, &vars, true),
843 0
844 );
845 assert_eq!(
846 eval_test(&["-z", "a", "-o", "-z", "b"], &opts, &vars, true),
847 1
848 );
849 }
850
851 #[test]
852 fn test_variable_exists() {
853 let opts = HashMap::new();
854 let mut vars = HashMap::new();
855 vars.insert("MYVAR".to_string(), "value".to_string());
856
857 assert_eq!(eval_test(&["-v", "MYVAR"], &opts, &vars, true), 0);
858 assert_eq!(eval_test(&["-v", "NOTEXIST"], &opts, &vars, true), 1);
859 }
860}