1use std::collections::BTreeSet;
2use std::io::IsTerminal;
3
4use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser};
5
6#[must_use]
18pub fn detect_interactive() -> bool {
19 std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum InteractivityMode {
29 Interactive,
32 NonInteractive,
35}
36
37impl InteractivityMode {
38 #[must_use]
40 pub fn is_interactive(self) -> bool {
41 self == Self::Interactive
42 }
43}
44
45impl From<bool> for InteractivityMode {
46 fn from(interactive: bool) -> Self {
47 if interactive {
48 Self::Interactive
49 } else {
50 Self::NonInteractive
51 }
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
60pub struct GlobalFlags {
61 pub output_format: String,
63 pub verbose: String,
65 pub dry_run: bool,
67 pub fields: String,
69 pub filter: String,
71 pub expr: String,
73 pub schema: bool,
75 pub reason: String,
77 pub timeout: String,
79 pub debug: String,
81 pub credential_store: Option<crate::config::CredentialStore>,
83 pub interactive: bool,
86}
87
88impl Default for GlobalFlags {
89 fn default() -> Self {
90 Self {
91 output_format: "json".to_owned(),
92 verbose: String::new(),
93 dry_run: false,
94 fields: String::new(),
95 filter: String::new(),
96 expr: String::new(),
97 schema: false,
98 reason: String::new(),
99 timeout: "0s".to_owned(),
100 debug: String::new(),
101 credential_store: None,
102 interactive: detect_interactive(),
103 }
104 }
105}
106
107pub(crate) mod global_flag_order {
143 pub(crate) const HELP: usize = 1000;
144 pub(crate) const OUTPUT: usize = 1001;
145 pub(crate) const VERBOSE: usize = 1002;
146 pub(crate) const DRY_RUN: usize = 1003;
147 pub(crate) const FIELDS: usize = 1004;
148 pub(crate) const FILTER: usize = 1005;
149 pub(crate) const EXPR: usize = 1006;
150 pub(crate) const LIMIT: usize = 1007;
151 pub(crate) const OFFSET: usize = 1008;
152 pub(crate) const SCHEMA: usize = 1009;
153 pub(crate) const TIMEOUT: usize = 1010;
154 pub(crate) const DEBUG: usize = 1011;
155 pub(crate) const CREDENTIAL_STORE: usize = 1012;
156 pub(crate) const JSON: usize = 1013;
157 pub(crate) const TOON: usize = 1014;
158 pub(crate) const HUMAN: usize = 1015;
159 pub(crate) const INTERACTIVE: usize = 1016;
160 pub(crate) const REASON: usize = 1017;
161 pub(crate) const ENV: usize = 1018;
162}
163
164pub fn register_global_flags(command: Command) -> Command {
166 command
167 .disable_help_flag(true)
168 .arg(
169 Arg::new("help")
174 .short('h')
175 .long("help")
176 .action(ArgAction::HelpLong)
177 .global(true)
178 .display_order(global_flag_order::HELP)
179 .help("Print help"),
180 )
181 .arg(
182 Arg::new("output")
183 .long("output")
184 .short('o')
185 .global(true)
186 .display_order(global_flag_order::OUTPUT)
187 .value_name("FORMAT")
188 .default_value(if std::io::stdout().is_terminal() {
201 "human"
202 } else {
203 "json"
204 })
205 .conflicts_with_all(["json", "toon", "human"])
209 .help(
210 "Output format: toon|json|human (shorthand: --json, --toon, --human); \
211 defaults to human in an interactive terminal, json otherwise",
212 ),
213 )
214 .arg(
215 Arg::new("verbose")
216 .long("verbose")
217 .global(true)
218 .num_args(0..=1)
219 .default_missing_value("all")
220 .value_name("FIELDS")
221 .display_order(global_flag_order::VERBOSE)
222 .help("Include metadata in output (all, or comma-separated: system,duration,args,env,identity,command,effective_args,timestamp)"),
223 )
224 .arg(
225 Arg::new("dry-run")
226 .long("dry-run")
227 .global(true)
228 .num_args(0..=1)
229 .require_equals(true)
230 .default_missing_value("true")
231 .default_value("false")
232 .value_parser(compat_bool_value_parser())
233 .display_order(global_flag_order::DRY_RUN)
234 .help("Preview mutations without executing"),
235 )
236 .arg(
237 Arg::new("fields")
238 .long("fields")
239 .global(true)
240 .value_name("FIELDS")
241 .display_order(global_flag_order::FIELDS)
242 .help("Comma-separated fields to include in output (use 'all' or '*' for everything)"),
243 )
244 .arg(
245 Arg::new("filter")
246 .long("filter")
247 .global(true)
248 .value_name("EXPR")
249 .display_order(global_flag_order::FILTER)
250 .help("Per-item JMESPath predicate for list data"),
251 )
252 .arg(
253 Arg::new("expr")
254 .long("expr")
255 .global(true)
256 .value_name("EXPR")
257 .display_order(global_flag_order::EXPR)
258 .help("JMESPath query applied to the whole result"),
259 )
260 .arg(
261 Arg::new("schema")
262 .long("schema")
263 .global(true)
264 .num_args(0..=1)
265 .require_equals(true)
266 .default_missing_value("true")
267 .default_value("false")
268 .value_parser(compat_bool_value_parser())
269 .display_order(global_flag_order::SCHEMA)
270 .help("Dump output field metadata instead of running the command"),
271 )
272 .arg(
273 Arg::new("timeout")
274 .long("timeout")
275 .global(true)
276 .allow_hyphen_values(true)
277 .default_value("0s")
278 .value_name("DURATION")
279 .display_order(global_flag_order::TIMEOUT)
280 .help("Overall command timeout (e.g. 60s, 5m); default 0s = no timeout"),
281 )
282 .arg(
283 Arg::new("debug")
284 .long("debug")
285 .global(true)
286 .num_args(0..=1)
287 .default_missing_value("*")
288 .value_name("PATTERN")
289 .display_order(global_flag_order::DEBUG)
290 .help("Enable debug logging (comma-separated component patterns, e.g. *, transport, *,-auth)"),
291 )
292 .arg(
293 Arg::new("credential-store")
294 .long("credential-store")
295 .display_order(global_flag_order::CREDENTIAL_STORE)
296 .global(true)
297 .value_name("MODE")
298 .value_parser(|s: &str| s.parse::<crate::config::CredentialStore>())
299 .help("Credential storage: auto|keyring|file (overrides env and config)"),
300 )
301 .arg(
302 Arg::new("interactive")
303 .long("interactive")
304 .short('i')
305 .global(true)
306 .action(ArgAction::SetTrue)
307 .conflicts_with("non-interactive")
308 .display_order(global_flag_order::INTERACTIVE)
309 .help("Force interactive prompts for missing inputs (default when TTY is detected)"),
310 )
311 .arg(
312 Arg::new("non-interactive")
313 .long("non-interactive")
314 .global(true)
315 .action(ArgAction::SetTrue)
316 .conflicts_with("interactive")
317 .hide(true)
318 .display_order(global_flag_order::INTERACTIVE)
319 .help("Disable interactive prompts; fail on missing required inputs"),
320 )
321 .arg(
322 Arg::new("json")
323 .long("json")
324 .global(true)
325 .action(ArgAction::SetTrue)
326 .conflicts_with_all(["toon", "human"])
330 .hide(true)
333 .display_order(global_flag_order::JSON)
334 .help("Shorthand for --output json"),
335 )
336 .arg(
337 Arg::new("toon")
338 .long("toon")
339 .global(true)
340 .action(ArgAction::SetTrue)
341 .conflicts_with_all(["json", "human"])
342 .hide(true)
343 .display_order(global_flag_order::TOON)
344 .help("Shorthand for --output toon"),
345 )
346 .arg(
347 Arg::new("human")
348 .long("human")
349 .global(true)
350 .action(ArgAction::SetTrue)
351 .conflicts_with_all(["json", "toon"])
352 .hide(true)
353 .display_order(global_flag_order::HUMAN)
354 .help("Shorthand for --output human"),
355 )
356}
357
358pub fn register_reason_flag(command: Command) -> Command {
371 command.arg(
372 Arg::new("reason")
373 .long("reason")
374 .global(true)
375 .value_name("TEXT")
376 .display_order(global_flag_order::REASON)
377 .help("Short explanation of why this command is being run (forwarded to your authorizer, auditor, or activity emitter)"),
378 )
379}
380
381pub(crate) fn apply_pagination_args(
385 command: Command,
386 default_limit: i64,
387 max_limit: i64,
388) -> Command {
389 command
390 .arg(
391 Arg::new("limit")
392 .long("limit")
393 .value_parser(pagination_limit_value_parser(max_limit))
394 .allow_hyphen_values(true)
395 .default_value(default_limit.to_string())
396 .display_order(global_flag_order::LIMIT)
397 .help(pagination_limit_help(default_limit, max_limit)),
398 )
399 .arg(
400 Arg::new("offset")
401 .long("offset")
402 .value_parser(pagination_offset_value_parser())
403 .allow_hyphen_values(true)
404 .default_value("0")
405 .display_order(global_flag_order::OFFSET)
406 .help("Skip N items before applying limit"),
407 )
408}
409
410fn pagination_limit_help(default_limit: i64, max_limit: i64) -> String {
411 let mut help = format!("Max items to return (client-side, 0=all, default {default_limit}");
412 if max_limit > 0 {
413 help.push_str(&format!(", max {max_limit}"));
414 }
415 help.push(')');
416 help
417}
418
419fn pagination_limit_value_parser(max_limit: i64) -> ValueParser {
420 ValueParser::new(move |raw: &str| -> Result<i64, String> {
421 let value = raw
422 .parse::<i64>()
423 .map_err(|_| format!("invalid limit value {raw:?}"))?;
424 if max_limit > 0 && value > max_limit {
425 return Err(format!("limit {value} exceeds the maximum of {max_limit}"));
426 }
427 Ok(value)
428 })
429}
430
431fn pagination_offset_value_parser() -> ValueParser {
435 ValueParser::new(|raw: &str| -> Result<i64, String> {
436 let value = raw
437 .parse::<i64>()
438 .map_err(|_| format!("invalid offset value {raw:?}"))?;
439 if value < 0 {
440 return Err(format!("offset {value} must be non-negative"));
441 }
442 Ok(value)
443 })
444}
445
446#[must_use]
454pub fn resolve_default_output_format(
455 env_override: Option<&str>,
456 config_override: Option<&str>,
457 is_tty: bool,
458) -> String {
459 for candidate in [env_override, config_override].into_iter().flatten() {
464 let normalized = candidate.trim().to_ascii_lowercase();
465 if crate::output::is_valid_output_format(&normalized) {
466 return normalized;
467 }
468 }
469 if is_tty { "human" } else { "json" }.to_owned()
470}
471
472#[must_use]
480pub fn app_id_env_prefix(app_id: &str) -> String {
481 app_id
482 .chars()
483 .map(|c| {
484 if c.is_ascii_alphanumeric() {
485 c.to_ascii_uppercase()
486 } else {
487 '_'
488 }
489 })
490 .collect()
491}
492
493#[must_use]
496pub fn output_env_var(app_id: &str) -> String {
497 format!("{}_OUTPUT", app_id_env_prefix(app_id))
498}
499
500#[must_use]
503pub fn min_stage_env_var(app_id: &str) -> String {
504 format!("{}_MIN_STAGE", app_id_env_prefix(app_id))
505}
506
507#[must_use]
520pub fn default_output_format(app_id: &str) -> String {
521 let env = std::env::var(output_env_var(app_id)).ok();
522 let file = crate::config::load(app_id);
523 resolve_default_output_format(
524 env.as_deref(),
525 file.output.format.as_deref(),
526 std::io::stdout().is_terminal(),
527 )
528}
529
530#[must_use]
531pub fn global_flags_from_matches(
534 matches: &ArgMatches,
535 default_format: &str,
536 auto_interactive: bool,
537) -> GlobalFlags {
538 let output_format = if matches.get_flag("toon") {
539 "toon".to_owned()
540 } else if matches.get_flag("human") {
541 "human".to_owned()
542 } else if matches.get_flag("json") {
543 "json".to_owned()
544 } else if matches.value_source("output") == Some(clap::parser::ValueSource::CommandLine) {
545 matches
546 .get_one::<String>("output")
547 .cloned()
548 .unwrap_or_else(|| default_format.to_owned())
549 } else {
550 default_format.to_owned()
551 };
552
553 GlobalFlags {
554 output_format,
555 verbose: matches
556 .get_one::<String>("verbose")
557 .cloned()
558 .unwrap_or_default(),
559 dry_run: matches.get_one::<bool>("dry-run").copied().unwrap_or(false),
560 fields: matches
561 .get_one::<String>("fields")
562 .cloned()
563 .unwrap_or_default(),
564 filter: matches
565 .get_one::<String>("filter")
566 .cloned()
567 .unwrap_or_default(),
568 expr: matches
569 .get_one::<String>("expr")
570 .cloned()
571 .unwrap_or_default(),
572 schema: matches.get_one::<bool>("schema").copied().unwrap_or(false),
573 reason: matches
576 .try_get_one::<String>("reason")
577 .ok()
578 .flatten()
579 .cloned()
580 .unwrap_or_default(),
581 timeout: matches
582 .get_one::<String>("timeout")
583 .cloned()
584 .unwrap_or_else(|| "0s".to_owned()),
585 debug: matches
586 .get_one::<String>("debug")
587 .cloned()
588 .unwrap_or_default(),
589 credential_store: matches
590 .get_one::<crate::config::CredentialStore>("credential-store")
591 .copied(),
592 interactive: if matches.get_flag("non-interactive") {
593 false
594 } else if matches.get_flag("interactive") {
595 true
596 } else if auto_interactive {
597 detect_interactive()
598 } else {
599 false
600 },
601 }
602}
603
604#[must_use]
605pub fn extract_output_format(args: &[impl AsRef<str>], default_format: &str) -> String {
611 for index in 0..args.len() {
612 let arg = args[index].as_ref();
613 if arg == "--output" || arg == "-o" {
614 return args.get(index + 1).map_or_else(
615 || default_format.to_owned(),
616 |value| value.as_ref().to_owned(),
617 );
618 }
619 if let Some(value) = arg.strip_prefix("--output=") {
620 return value.to_owned();
621 }
622 if arg == "--json" {
623 return "json".to_owned();
624 }
625 if arg == "--toon" {
626 return "toon".to_owned();
627 }
628 if arg == "--human" {
629 return "human".to_owned();
630 }
631 }
632 default_format.to_owned()
633}
634
635#[must_use]
636pub fn extract_command_path(
638 args: &[impl AsRef<str>],
639 bool_flags: &BTreeSet<String>,
640 value_flags: &BTreeSet<String>,
641) -> String {
642 let mut parts = Vec::new();
643 let mut index = 1;
644 while index < args.len() {
645 let arg = args[index].as_ref();
646 if arg == "--schema" {
647 index += 1;
648 continue;
649 }
650 if arg.starts_with('-') {
651 if bool_flags.contains(arg) || arg.contains('=') {
652 index += 1;
653 continue;
654 }
655 if value_flags.contains(arg)
656 || (index + 1 < args.len() && !args[index + 1].as_ref().starts_with('-'))
657 {
658 index += 2;
659 continue;
660 }
661 index += 1;
662 continue;
663 }
664 parts.push(arg.to_owned());
665 index += 1;
666 }
667 parts.join(":")
668}
669
670#[must_use]
671pub fn has_true_schema_flag(args: &[impl AsRef<str>]) -> bool {
673 for arg in args {
674 let arg = arg.as_ref();
675 if arg == "--schema" {
676 return true;
677 }
678 if let Some(value) = arg.strip_prefix("--schema=") {
679 return parse_compat_bool(value).unwrap_or(false);
680 }
681 }
682 false
683}
684
685pub(crate) fn compat_bool_value_parser() -> ValueParser {
686 ValueParser::new(parse_compat_bool)
687}
688
689fn parse_compat_bool(raw: &str) -> Result<bool, String> {
690 match raw {
691 "1" | "t" | "T" | "TRUE" | "true" | "True" => Ok(true),
692 "0" | "f" | "F" | "FALSE" | "false" | "False" => Ok(false),
693 _ => Err(format!("invalid boolean value {raw:?}")),
694 }
695}
696
697#[must_use]
698pub fn derive_bool_flags(command: &Command) -> BTreeSet<String> {
700 let mut flags = BTreeSet::from([
701 "--help".to_owned(),
702 "-h".to_owned(),
703 "--verbose".to_owned(),
704 "--debug".to_owned(),
705 ]);
706 collect_flag_names(command, &mut |arg, name| {
707 if !arg_requires_value(arg) {
708 flags.insert(name);
709 }
710 });
711 flags
712}
713
714#[must_use]
715pub fn derive_value_flags(command: &Command) -> BTreeSet<String> {
717 let mut flags = BTreeSet::new();
718 collect_flag_names(command, &mut |arg, name| {
719 if arg_requires_value(arg) {
720 flags.insert(name);
721 }
722 });
723 flags
724}
725
726fn collect_flag_names(command: &Command, visit: &mut impl FnMut(&Arg, String)) {
727 for arg in command.get_arguments() {
728 if arg.is_positional() {
729 continue;
730 }
731 if let Some(long) = arg.get_long() {
732 visit(arg, format!("--{long}"));
733 }
734 if let Some(short) = arg.get_short() {
735 visit(arg, format!("-{short}"));
736 }
737 }
738 for child in command.get_subcommands() {
739 collect_flag_names(child, visit);
740 }
741}
742
743#[must_use]
767pub fn debug_component_enabled(pattern: &str, component: &str) -> bool {
768 let component = component.trim().to_ascii_lowercase();
769 if component.is_empty() {
771 return false;
772 }
773 let mut enabled = false;
774 for raw in pattern.split(',') {
775 let token = raw.trim();
776 if token.is_empty() {
777 continue;
778 }
779 let (negated, name) = token
780 .strip_prefix('-')
781 .map_or((false, token), |rest| (true, rest));
782 let name = name.trim().to_ascii_lowercase();
783 if name == "*" || name == component {
784 enabled = !negated;
785 }
786 }
787 enabled
788}
789
790fn arg_requires_value(arg: &Arg) -> bool {
791 match arg.get_action() {
792 ArgAction::Set | ArgAction::Append => arg
793 .get_num_args()
794 .is_none_or(|range| range.takes_values() && range.min_values() > 0),
795 ArgAction::SetTrue
796 | ArgAction::SetFalse
797 | ArgAction::Count
798 | ArgAction::Help
799 | ArgAction::HelpShort
800 | ArgAction::HelpLong
801 | ArgAction::Version => false,
802 _ => arg
803 .get_num_args()
804 .is_some_and(|range| range.takes_values() && range.min_values() > 0),
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use clap::Command;
811
812 use super::{
813 debug_component_enabled, min_stage_env_var, output_env_var, register_global_flags,
814 resolve_default_output_format,
815 };
816
817 #[test]
818 fn debug_component_matcher_handles_wildcards_and_negation() {
819 assert!(!debug_component_enabled("", "transport"));
821 assert!(debug_component_enabled("*", "transport"));
823 assert!(debug_component_enabled("*", "auth"));
824 assert!(debug_component_enabled("transport", "transport"));
826 assert!(!debug_component_enabled("transport", "auth"));
827 assert!(!debug_component_enabled("*,-transport", "transport"));
829 assert!(debug_component_enabled("*,-auth", "transport"));
830 assert!(!debug_component_enabled("*,-*", "transport"));
832 assert!(debug_component_enabled("-*,transport", "transport"));
833 assert!(debug_component_enabled(" Transport , -auth ", "transport"));
835 assert!(!debug_component_enabled("*", ""));
837 assert!(!debug_component_enabled("*", " "));
838 }
839
840 #[test]
841 fn default_output_format_follows_env_override_then_tty() {
842 assert_eq!(resolve_default_output_format(None, None, true), "human");
844 assert_eq!(resolve_default_output_format(None, None, false), "json");
845 assert_eq!(
847 resolve_default_output_format(Some("json"), None, true),
848 "json"
849 );
850 assert_eq!(
851 resolve_default_output_format(Some("human"), None, false),
852 "human"
853 );
854 assert_eq!(
856 resolve_default_output_format(Some("JSON"), None, true),
857 "json"
858 );
859 assert_eq!(
860 resolve_default_output_format(Some(" Human "), None, false),
861 "human"
862 );
863 assert_eq!(
865 resolve_default_output_format(Some(" "), None, false),
866 "json"
867 );
868 assert_eq!(resolve_default_output_format(Some(""), None, true), "human");
869 assert_eq!(
870 resolve_default_output_format(Some("yaml"), None, false),
871 "json"
872 );
873 assert_eq!(
874 resolve_default_output_format(Some("yaml"), None, true),
875 "human"
876 );
877 }
878
879 #[test]
880 fn default_output_format_config_override_wins_over_tty_but_not_env() {
881 assert_eq!(
883 resolve_default_output_format(None, Some("json"), true),
884 "json"
885 );
886 assert_eq!(
887 resolve_default_output_format(None, Some("human"), false),
888 "human"
889 );
890 assert_eq!(
892 resolve_default_output_format(Some("human"), Some("json"), false),
893 "human"
894 );
895 assert_eq!(
897 resolve_default_output_format(None, Some("yaml"), true),
898 "human"
899 );
900 assert_eq!(
901 resolve_default_output_format(None, Some("yaml"), false),
902 "json"
903 );
904 }
905
906 #[test]
907 fn output_env_var_is_derived_from_app_id() {
908 assert_eq!(output_env_var("godaddy"), "GODADDY_OUTPUT");
909 assert_eq!(output_env_var("gdx"), "GDX_OUTPUT");
910 assert_eq!(output_env_var("my-cli"), "MY_CLI_OUTPUT");
911 }
912
913 #[test]
914 fn min_stage_env_var_is_derived_from_app_id() {
915 assert_eq!(min_stage_env_var("godaddy"), "GODADDY_MIN_STAGE");
916 assert_eq!(min_stage_env_var("gdx"), "GDX_MIN_STAGE");
917 assert_eq!(min_stage_env_var("my-cli"), "MY_CLI_MIN_STAGE");
918 }
919
920 #[test]
921 fn short_and_long_help_flags_render_identical_output() {
922 let build = || {
923 register_global_flags(Command::new("testcli"))
924 .subcommand(Command::new("sub").about("A subcommand"))
925 };
926 let help_text = |args: &[&str]| {
927 build()
928 .try_get_matches_from(args)
929 .expect_err("help action short-circuits parsing")
930 .to_string()
931 };
932
933 assert_eq!(
934 help_text(&["testcli", "-h"]),
935 help_text(&["testcli", "--help"])
936 );
937 assert_eq!(
938 help_text(&["testcli", "sub", "-h"]),
939 help_text(&["testcli", "sub", "--help"])
940 );
941 }
942
943 #[test]
944 fn interactivity_mode_from_bool() {
945 use super::InteractivityMode;
946 assert_eq!(
947 InteractivityMode::from(true),
948 InteractivityMode::Interactive
949 );
950 assert_eq!(
951 InteractivityMode::from(false),
952 InteractivityMode::NonInteractive
953 );
954 assert!(InteractivityMode::Interactive.is_interactive());
955 assert!(!InteractivityMode::NonInteractive.is_interactive());
956 }
957
958 #[test]
959 fn interactive_flag_parsing_explicit_interactive() {
960 use super::global_flags_from_matches;
961 let cmd = register_global_flags(Command::new("test"));
962 let matches = cmd
963 .try_get_matches_from(["test", "--interactive"])
964 .expect("should parse");
965 let flags = global_flags_from_matches(&matches, "json", false);
967 assert!(flags.interactive);
968 }
969
970 #[test]
971 fn interactive_flag_parsing_explicit_non_interactive() {
972 use super::global_flags_from_matches;
973 let cmd = register_global_flags(Command::new("test"));
974 let matches = cmd
975 .try_get_matches_from(["test", "--non-interactive"])
976 .expect("should parse");
977 let flags = global_flags_from_matches(&matches, "json", true);
979 assert!(!flags.interactive);
980 }
981
982 #[test]
983 fn interactive_defaults_off_without_auto_interactive() {
984 use super::global_flags_from_matches;
985 let cmd = register_global_flags(Command::new("test"));
986 let matches = cmd.try_get_matches_from(["test"]).expect("should parse");
987 let flags = global_flags_from_matches(&matches, "json", false);
989 assert!(!flags.interactive);
990 }
991
992 #[test]
993 fn interactive_flag_conflicts() {
994 let cmd = register_global_flags(Command::new("test"));
995 let result = cmd.try_get_matches_from(["test", "--interactive", "--non-interactive"]);
996 assert!(result.is_err());
997 }
998
999 #[test]
1000 fn detect_interactive_is_consistent_with_tty_state() {
1001 let result = super::detect_interactive();
1006 let stdin_tty = std::io::IsTerminal::is_terminal(&std::io::stdin());
1007 let stderr_tty = std::io::IsTerminal::is_terminal(&std::io::stderr());
1008 assert_eq!(result, stdin_tty && stderr_tty);
1009 }
1010}