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