Repository: fujiapple852/trippy
Pull request #932 (2024-01-20 09:25:35)

test: add command line tests


Patch:
diff --git a/src/config.rs b/src/config.rs
index 3ea94c6..eb7ad87 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1045,10 +1045,13 @@ fn validate_bindings(bindings: &TuiBindings) -> anyhow::Result<()> {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::net::{Ipv4Addr, Ipv6Addr};
+    use test_case::test_case;
+    use trippy::tracing::Port;
 
     #[test]
     fn test_config_default() {
-        let args = args(&["trip", "example.com"]);
+        let args = args(&["trip", "example.com"]).unwrap();
         let cfg_file = ConfigFile::default();
         let platform = Platform::dummy_for_test();
         let config = TrippyConfig::build_config(args, cfg_file, &platform).unwrap();
@@ -1061,7 +1064,7 @@ mod tests {
 
     #[test]
     fn test_config_sample() {
-        let args = args(&["trip", "example.com"]);
+        let args = args(&["trip", "example.com"]).unwrap();
         let cfg_file: ConfigFile =
             toml::from_str(include_str!("../trippy-config-sample.toml")).unwrap();
         let platform = Platform::dummy_for_test();
@@ -1073,8 +1076,428 @@ mod tests {
         pretty_assertions::assert_eq!(expected, config);
     }
 
-    fn args(args: &[&str]) -> Args {
+    #[test_case("trip", Err(anyhow!(include_str!("../test_resources/usage_short.txt"))); "show default help")]
+    #[test_case("trip -h", Err(anyhow!(include_str!("../test_resources/usage_short.txt"))); "show short help")]
+    #[test_case("trip --help", Err(anyhow!(include_str!("../test_resources/usage_long.txt"))); "show long help")]
+    #[test_case("trip -V", Err(anyhow!(format!("trip {}", clap::crate_version!()))); "show version")]
+    #[test_case("trip example.com --config-file trippy.toml", Ok(cfg().build()); "custom config file")]
+    #[test_case("trip example.com -c trippy.toml", Ok(cfg().build()); "custom config file short")]
+    #[test_case("trip example.com", Ok(cfg().build()); "single target")]
+    #[test_case("trip example.com foo.com bar.com", Ok(cfg_multi().build()); "multiple targets")]
+    #[test_case("trip example.com -U 20", Ok(cfg().max_inflight(20).build()); "single target before args")]
+    #[test_case("trip -U 20 example.com", Ok(cfg().max_inflight(20).build()); "single target after args")]
+    #[test_case("trip example.com foo.com bar.com -U 20", Ok(cfg_multi().max_inflight(20).build()); "multiple targets before args")]
+    #[test_case("trip -U 20 example.com foo.com bar.com", Ok(cfg_multi().max_inflight(20).build()); "multiple targets after args")]
+    #[test_case("trip example.com -U 20 foo.com -Q 255 bar.com", Ok(cfg_multi().max_inflight(20).tos(255).build()); "multiple targets between args")]
+    #[test_case("trip example.com --dummy", Err(anyhow!("error: unexpected argument found")); "invalid argument")]
+    #[test_case("trip example.com", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); "default strategy")]
+    #[test_case("trip example.com --multipath-strategy classic", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); "classic strategy")]
+    #[test_case("trip example.com --multipath-strategy paris --udp", Ok(cfg().multipath_strategy(MultipathStrategy::Paris).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "paris strategy")]
+    #[test_case("trip example.com --multipath-strategy dublin --udp", Ok(cfg().multipath_strategy(MultipathStrategy::Dublin).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "dublin strategy")]
+    #[test_case("trip example.com --multipath-strategy tokyo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid strategy")]
+    #[test_case("trip example.com", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "default protocol")]
+    #[test_case("trip example.com --protocol icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol")]
+    #[test_case("trip example.com --protocol udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol")]
+    #[test_case("trip example.com --protocol tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol")]
+    #[test_case("trip example.com --protocol foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid protocol")]
+    #[test_case("trip example.com -p icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol short")]
+    #[test_case("trip example.com -p udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol short")]
+    #[test_case("trip example.com -p tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol short")]
+    #[test_case("trip example.com -p foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid protocol short")]
+    #[test_case("trip example.com --icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol shortcut")]
+    #[test_case("trip example.com --udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol shortcut")]
+    #[test_case("trip example.com --tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol shortcut")]
+    #[test_case("trip example.com --udp --source-port 2222", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(2222))).build()); "udp protocol custom src port")]
+    #[test_case("trip example.com --udp --target-port 8888", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedDest(Port(8888))).build()); "udp protocol custom target port")]
+    #[test_case("trip example.com --udp --source-port 123", Err(anyhow!("source-port (123) must be >= 1024")); "udp protocol invalid src port")]
+    #[test_case("trip example.com --tcp --source-port 3333", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedSrc(Port(3333))).build()); "tcp protocol custom src port")]
+    #[test_case("trip example.com --tcp --target-port 7777", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(7777))).build()); "tcp protocol custom target port")]
+    #[test_case("trip example.com --udp --multipath-strategy paris", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol paris strategy default ports")]
+    #[test_case("trip example.com --udp --multipath-strategy paris --source-port 33000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedSrc(Port(33000))).build()); "udp protocol paris strategy custom src port")]
+    #[test_case("trip example.com --udp --multipath-strategy paris --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedDest(Port(5000))).build()); "udp protocol paris strategy custom target port")]
+    #[test_case("trip example.com --udp --multipath-strategy paris --source-port 33000 --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedBoth(Port(33000), Port(5000))).build()); "udp protocol paris strategy custom both ports")]
+    #[test_case("trip example.com --udp --multipath-strategy dublin", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol dublin strategy default ports")]
+    #[test_case("trip example.com --udp --multipath-strategy dublin --source-port 33000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedSrc(Port(33000))).build()); "udp protocol dublin strategy custom src port")]
+    #[test_case("trip example.com --udp --multipath-strategy dublin --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedDest(Port(5000))).build()); "udp protocol dublin strategy custom target port")]
+    #[test_case("trip example.com --udp --multipath-strategy dublin --source-port 33000 --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedBoth(Port(33000), Port(5000))).build()); "udp protocol dublin strategy custom both ports")]
+    #[test_case("trip example.com --icmp --multipath-strategy paris", Err(anyhow!("Paris multipath strategy not support for icmp")); "paris with invalid protocol icmp")]
+    #[test_case("trip example.com --icmp --multipath-strategy dublin", Err(anyhow!("Dublin multipath strategy not support for icmp")); "dublin with invalid protocol icmp")]
+    #[test_case("trip example.com --tcp --multipath-strategy paris", Err(anyhow!("Paris multipath strategy not yet supported for tcp")); "paris with invalid protocol tcp")]
+    #[test_case("trip example.com --tcp --multipath-strategy dublin", Err(anyhow!("Dublin multipath strategy not yet supported for tcp")); "dublin with invalid protocol tcp")]
+    #[test_case("trip example.com --udp --source-port 33000 --target-port 5000", Err(anyhow!("only one of source-port and target-port may be fixed (except IPv4/udp protocol with dublin or paris strategy)")); "udp protocol custom both ports with invalid strategy")]
+    #[test_case("trip example.com", Ok(cfg().addr_family(AddrFamily::Ipv4).build()); "default address family shortcut")]
+    #[test_case("trip example.com -4", Ok(cfg().addr_family(AddrFamily::Ipv4).build()); "ipv4 address family shortcut")]
+    #[test_case("trip example.com -6", Ok(cfg().addr_family(AddrFamily::Ipv6).build()); "ipv6 address family shortcut")]
+    #[test_case("trip example.com -5", Err(anyhow!("error: unexpected argument found")); "invalid address family shortcut")]
+    #[test_case("trip example.com", Ok(cfg().first_ttl(1).build()); "default first ttl")]
+    #[test_case("trip example.com --first-ttl 5", Ok(cfg().first_ttl(5).build()); "custom first ttl")]
+    #[test_case("trip example.com -f 5", Ok(cfg().first_ttl(5).build()); "custom first ttl short")]
+    #[test_case("trip example.com --first-ttl 0", Err(anyhow!("first-ttl (0) must be in the range 1..255")); "invalid low first ttl")]
+    #[test_case("trip example.com --first-ttl 500", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high first ttl")]
+    #[test_case("trip example.com", Ok(cfg().first_ttl(1).build()); "default max ttl")]
+    #[test_case("trip example.com --max-ttl 5", Ok(cfg().max_ttl(5).build()); "custom max ttl")]
+    #[test_case("trip example.com -t 5", Ok(cfg().max_ttl(5).build()); "custom max ttl short")]
+    #[test_case("trip example.com --max-ttl 0", Err(anyhow!("max-ttl (0) must be in the range 1..255")); "invalid low max ttl")]
+    #[test_case("trip example.com --max-ttl 500", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high max ttl")]
+    #[test_case("trip example.com --first-ttl 3 --max-ttl 2", Err(anyhow!("first-ttl (3) must be less than or equal to max-ttl (2)")); "first ttl higher than max ttl")]
+    #[test_case("trip example.com --first-ttl 5 --max-ttl 5", Ok(cfg().first_ttl(5).max_ttl(5).build()); "custom first and max ttl")]
+    #[test_case("trip example.com", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); "default min round duration")]
+    #[test_case("trip example.com --min-round-duration 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); "custom min round duration")]
+    #[test_case("trip example.com -i 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); "custom min round duration short")]
+    #[test_case("trip example.com --min-round-duration 0", Err(anyhow!("time unit needed, for example 0sec or 0ms")); "invalid format min round duration")]
+    #[test_case("trip example.com", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); "default max round duration")]
+    #[test_case("trip example.com --max-round-duration 1250ms", Ok(cfg().max_round_duration(Duration::from_millis(1250)).build()); "custom max round duration")]
+    #[test_case("trip example.com -T 2s", Ok(cfg().max_round_duration(Duration::from_millis(2000)).build()); "custom max round duration short")]
+    #[test_case("trip example.com --min-round-duration 0", Err(anyhow!("time unit needed, for example 0sec or 0ms")); "invalid format max round duration")]
+    #[test_case("trip example.com -i 250ms -T 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).max_round_duration(Duration::from_millis(250)).build()); "custom min and max round duration")]
+    #[test_case("trip example.com -i 300ms -T 250ms", Err(anyhow!("max-round-duration (250ms) must not be less than min-round-duration (300ms)")); "min round duration greater than max")]
+    #[test_case("trip example.com", Ok(cfg().grace_duration(Duration::from_millis(100)).build()); "default grace duration")]
+    #[test_case("trip example.com --grace-duration 10ms", Ok(cfg().grace_duration(Duration::from_millis(10)).build()); "custom grace duration")]
+    #[test_case("trip example.com -g 50ms", Ok(cfg().grace_duration(Duration::from_millis(50)).build()); "custom grace duration short")]
+    #[test_case("trip example.com --grace-duration 0", Err(anyhow!("time unit needed, for example 0sec or 0ms")); "invalid format grace duration")]
+    #[test_case("trip example.com --grace-duration 9ms", Err(anyhow!("grace-duration (9ms) must be between 10ms and 1s inclusive")); "invalid low grace duration")]
+    #[test_case("trip example.com --grace-duration 1001ms", Err(anyhow!("grace-duration (1.001s) must be between 10ms and 1s inclusive")); "invalid high grace duration")]
+    #[test_case("trip example.com", Ok(cfg().max_inflight(24).build()); "default max inflight")]
+    #[test_case("trip example.com --max-inflight 12", Ok(cfg().max_inflight(12).build()); "custom max inflight")]
+    #[test_case("trip example.com -U 20", Ok(cfg().max_inflight(20).build()); "custom max inflight short")]
+    #[test_case("trip example.com --max-inflight foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format max inflight")]
+    #[test_case("trip example.com --max-inflight 0", Err(anyhow!("max-inflight (0) must be greater than zero")); "invalid low max inflight")]
+    #[test_case("trip example.com --max-inflight 300", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high max inflight")]
+    #[test_case("trip example.com", Ok(cfg().initial_sequence(33000).build()); "default initial sequence")]
+    #[test_case("trip example.com --initial-sequence 5000", Ok(cfg().initial_sequence(5000).build()); "custom initial sequence")]
+    #[test_case("trip example.com --initial-sequence foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format initial sequence")]
+    #[test_case("trip example.com --initial-sequence 100000", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high initial sequence")]
+    #[test_case("trip example.com", Ok(cfg().tos(0).build()); "default tos")]
+    #[test_case("trip example.com --tos 255", Ok(cfg().tos(0xFF).build()); "custom tos")]
+    #[test_case("trip example.com -Q 255", Ok(cfg().tos(0xFF).build()); "custom tos short")]
+    #[test_case("trip example.com --tos foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format tos")]
+    #[test_case("trip example.com --tos 300", Err(anyhow!("error: invalid value for one of the arguments")); "invalid high tos")]
+    #[test_case("trip example.com", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Disabled).build()); "default icmp extensions")]
+    #[test_case("trip example.com --icmp-extensions", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled).build()); "enabled icmp extensions")]
+    #[test_case("trip example.com -e", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled).build()); "enabled icmp extensions short")]
+    #[test_case("trip example.com", Ok(cfg().read_timeout(Duration::from_millis(10)).build()); "default read timeout")]
+    #[test_case("trip example.com --read-timeout 20ms", Ok(cfg().read_timeout(Duration::from_millis(20)).build()); "custom read timeout")]
+    #[test_case("trip example.com --read-timeout 20", Err(anyhow!("time unit needed, for example 20sec or 20ms")); "invalid custom read timeout")]
+    #[test_case("trip example.com --read-timeout 9ms", Err(anyhow!("read-timeout (9ms) must be between 10ms and 100ms inclusive")); "invalid low custom read timeout")]
+    #[test_case("trip example.com --read-timeout 101ms", Err(anyhow!("read-timeout (101ms) must be between 10ms and 100ms inclusive")); "invalid high custom read timeout")]
+    #[test_case("trip example.com", Ok(cfg().packet_size(84).build()); "default packet size")]
+    #[test_case("trip example.com --packet-size 120", Ok(cfg().packet_size(120).build()); "custom packet size")]
+    #[test_case("trip example.com --packet-size foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format packet size")]
+    #[test_case("trip example.com --packet-size 27", Err(anyhow!("packet-size (27) must be between 28 and 1024 inclusive")); "invalid low packet size")]
+    #[test_case("trip example.com --packet-size 1025", Err(anyhow!("packet-size (1025) must be between 28 and 1024 inclusive")); "invalid high packet size")]
+    #[test_case("trip example.com --packet-size 100000", Err(anyhow!("error: invalid value for one of the arguments")); "invalid out of range packet size")]
+    #[test_case("trip example.com", Ok(cfg().payload_pattern(0).build()); "default payload pattern size")]
+    #[test_case("trip example.com --payload-pattern 255", Ok(cfg().payload_pattern(0xFF).build()); "custom payload pattern")]
+    #[test_case("trip example.com --payload-pattern foo", Err(anyhow!("error: invalid value for one of the arguments")); "invalid format payload pattern")]
+    #[test_case("trip example.com --payload-pattern 256", Err(anyhow!("error: invalid value for one of the arguments")); "invalid out of range payload pattern")]
+    #[test_case("trip example.com", Ok(cfg().source_addr(None).build()); "default source address")]
+    #[test_case("trip example.com --source-address 10.0.0.1", Ok(cfg().source_addr(Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))).build()); "custom ipv4 source address")]
+    #[test_case("trip example.com --source-address 2404:6800:4005:81a::200e", Ok(cfg().source_addr(Some(IpAddr::V6(Ipv6Addr::from_str("2404:6800:4005:81a::200e").unwrap()))).build()); "custom ipv6 source address")]
+    #[test_case("trip example.com -A 10.0.0.1", Ok(cfg().source_addr(Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))).build()); "custom ipv4 source address short")]
+    #[test_case("trip example.com --source-address foobar", Err(anyhow!("invalid source IP address format: foobar")); "invalid source address")]
+    #[test_case("trip example.com", Ok(cfg().interface(None).build()); "default interface")]
+    #[test_case("trip example.com --interface en0", Ok(cfg().interface(Some(String::from("en0"))).build()); "custom interface")]
+    #[test_case("trip example.com -I tun0", Ok(cfg().interface(Some(String::from("tun0"))).build()); "custom interface short")]
+    #[test_case("trip example.com", Ok(cfg().dns_timeout(Duration::from_millis(5000)).build()); "default dns timeout")]
+    #[test_case("trip example.com --dns-timeout 20ms", Ok(cfg().dns_timeout(Duration::from_millis(20)).build()); "custom dns timeout")]
+    #[test_case("trip example.com --dns-timeout 20", Err(anyhow!("time unit needed, for example 20sec or 20ms")); "invalid custom dns timeout")]
+    #[test_case("trip example.com", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); "default resolve method")]
+    #[test_case("trip example.com --dns-resolve-method system", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); "custom resolve method system")]
+    #[test_case("trip example.com -r system", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); "custom resolve method system short")]
+    #[test_case("trip example.com --dns-resolve-method google", Ok(cfg().dns_resolve_method(ResolveMethod::Google).build()); "custom resolve method google")]
+    #[test_case("trip example.com --dns-resolve-method cloudflare", Ok(cfg().dns_resolve_method(ResolveMethod::Cloudflare).build()); "custom resolve method cloudflare")]
+    #[test_case("trip example.com --dns-resolve-method resolv", Ok(cfg().dns_resolve_method(ResolveMethod::Resolv).build()); "custom resolve method resolv")]
+    #[test_case("trip example.com --dns-resolve-method foobar", Err(anyhow!("error: one of the values isn't valid for an argument")); "invalid resolve method")]
+    #[test_case("trip example.com", Ok(cfg().dns_lookup_as_info(false).build()); "default dns lookup as info")]
+    #[test_case("trip example.com --dns-lookup-as-info -r resolv", Ok(cfg().dns_lookup_as_info(true).dns_resolve_method(ResolveMethod::Resolv).build()); "custom dns lookup as info")]
+    #[test_case("trip example.com -z -r resolv", Ok(cfg().dns_lookup_as_info(true).dns_resolve_method(ResolveMethod::Resolv).build()); "custom dns lookup as info short")]
+    #[test_case("trip example.com --dns-lookup-as-info", Err(anyhow!("AS lookup not supported by resolver `system` (use '-r' to choose another resolver)")); "invalid resolve method for as info")]
+    fn test_commands(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
+        compare(parse_config(cmd), expected);
+    }
+
+    #[test_case("trip --print-config-template", Ok(TrippyAction::PrintConfigTemplate); "print config template")]
+    #[test_case("trip --print-tui-binding-commands", Ok(TrippyAction::PrintTuiBindingCommands); "print the tui binding commands")]
+    #[test_case("trip --print-tui-theme-items", Ok(TrippyAction::PrintTuiThemeItems); "print the tui theme items")]
+    #[test_case("trip --generate elvish", Ok(TrippyAction::PrintShellCompletions(Shell::Elvish)); "generate elvish shell completions")]
+    #[test_case("trip --generate fish", Ok(TrippyAction::PrintShellCompletions(Shell::Fish)); "generate fish shell completions")]
+    #[test_case("trip --generate powershell", Ok(TrippyAction::PrintShellCompletions(Shell::PowerShell)); "generate powershell shell completions")]
+    #[test_case("trip --generate zsh", Ok(TrippyAction::PrintShellCompletions(Shell::Zsh)); "generate zsh shell completions")]
+    #[test_case("trip --generate bash", Ok(TrippyAction::PrintShellCompletions(Shell::Bash)); "generate bash shell completions")]
+    #[test_case("trip --generate foo", Err(anyhow!("error: one of the values isn't valid for an argument")); "generate invalid shell completions")]
+    fn test_action(cmd: &str, expected: anyhow::Result<TrippyAction>) {
+        compare(parse_action(cmd), expected);
+    }
+
+    fn parse_action(cmd: &str) -> anyhow::Result<TrippyAction> {
+        TrippyAction::from(parse(cmd)?, &Platform::dummy_for_test())
+    }
+
+    fn parse_config(cmd: &str) -> anyhow::Result<TrippyConfig> {
+        let args = parse(cmd)?;
+        let cfg_file = ConfigFile::default();
+        let platform = Platform::dummy_for_test();
+        TrippyConfig::build_config(args, cfg_file, &platform)
+    }
+
+    fn parse(cmd: &str) -> anyhow::Result<Args> {
         use clap::Parser;
-        Args::parse_from(args.iter().map(std::ffi::OsString::from))
+        Ok(Args::try_parse_from(
+            cmd.split(' ').map(std::ffi::OsString::from),
+        )?)
+    }
+
+    fn compare<T: PartialEq + Eq + std::fmt::Debug>(
+        actual: anyhow::Result<T>,
+        expected: anyhow::Result<T>,
+    ) {
+        match (actual, expected) {
+            (Ok(cfg), Ok(exp)) => {
+                pretty_assertions::assert_eq!(cfg, exp);
+            }
+            (Err(err), Err(exp_err)) => {
+                if remove_whitespace(err.to_string()) != remove_whitespace(exp_err.to_string()) {
+                    pretty_assertions::assert_eq!(err.to_string(), exp_err.to_string());
+                }
+            }
+            (Ok(_), Err(exp_err)) => {
+                panic!("expected err {}", exp_err.to_string().trim());
+            }
+            (Err(err), Ok(_)) => {
+                panic!("unexpected err {}", err.to_string().trim());
+            }
+        }
+    }
+
+    fn cfg() -> TrippyConfigBuilder {
+        TrippyConfigBuilder::new(vec![String::from("example.com")])
+    }
+
+    fn cfg_multi() -> TrippyConfigBuilder {
+        TrippyConfigBuilder::new(vec![
+            String::from("example.com"),
+            String::from("foo.com"),
+            String::from("bar.com"),
+        ])
+    }
+
+    fn args(args: &[&str]) -> anyhow::Result<Args> {
+        use clap::Parser;
+        Ok(Args::try_parse_from(
+            args.iter().map(std::ffi::OsString::from),
+        )?)
+    }
+
+    fn remove_whitespace(mut s: String) -> String {
+        s.retain(|c| !c.is_whitespace());
+        s
+    }
+
+    pub struct TrippyConfigBuilder {
+        config: TrippyConfig,
+    }
+
+    impl TrippyConfigBuilder {
+        pub fn new(targets: Vec<String>) -> Self {
+            Self {
+                config: TrippyConfig {
+                    targets,
+                    ..TrippyConfig::default()
+                },
+            }
+        }
+
+        pub fn protocol(self, protocol: Protocol) -> Self {
+            Self {
+                config: TrippyConfig {
+                    protocol,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn addr_family(self, addr_family: AddrFamily) -> Self {
+            Self {
+                config: TrippyConfig {
+                    addr_family,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn first_ttl(self, first_ttl: u8) -> Self {
+            Self {
+                config: TrippyConfig {
+                    first_ttl,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn max_ttl(self, max_ttl: u8) -> Self {
+            Self {
+                config: TrippyConfig {
+                    max_ttl,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn min_round_duration(self, min_round_duration: Duration) -> Self {
+            Self {
+                config: TrippyConfig {
+                    min_round_duration,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn max_round_duration(self, max_round_duration: Duration) -> Self {
+            Self {
+                config: TrippyConfig {
+                    max_round_duration,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn grace_duration(self, grace_duration: Duration) -> Self {
+            Self {
+                config: TrippyConfig {
+                    grace_duration,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn max_inflight(self, max_inflight: u8) -> Self {
+            Self {
+                config: TrippyConfig {
+                    max_inflight,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn initial_sequence(self, initial_sequence: u16) -> Self {
+            Self {
+                config: TrippyConfig {
+                    initial_sequence,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn tos(self, tos: u8) -> Self {
+            Self {
+                config: TrippyConfig { tos, ..self.config },
+            }
+        }
+
+        pub fn icmp_extension_parse_mode(
+            self,
+            icmp_extension_parse_mode: IcmpExtensionParseMode,
+        ) -> Self {
+            Self {
+                config: TrippyConfig {
+                    icmp_extension_parse_mode,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn read_timeout(self, read_timeout: Duration) -> Self {
+            Self {
+                config: TrippyConfig {
+                    read_timeout,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn packet_size(self, packet_size: u16) -> Self {
+            Self {
+                config: TrippyConfig {
+                    packet_size,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn payload_pattern(self, payload_pattern: u8) -> Self {
+            Self {
+                config: TrippyConfig {
+                    payload_pattern,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn source_addr(self, source_addr: Option<IpAddr>) -> Self {
+            Self {
+                config: TrippyConfig {
+                    source_addr,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn interface(self, interface: Option<String>) -> Self {
+            Self {
+                config: TrippyConfig {
+                    interface,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn port_direction(self, port_direction: PortDirection) -> Self {
+            Self {
+                config: TrippyConfig {
+                    port_direction,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn multipath_strategy(self, multipath_strategy: MultipathStrategy) -> Self {
+            Self {
+                config: TrippyConfig {
+                    multipath_strategy,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn dns_timeout(self, dns_timeout: Duration) -> Self {
+            Self {
+                config: TrippyConfig {
+                    dns_timeout,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn dns_resolve_method(self, dns_resolve_method: ResolveMethod) -> Self {
+            Self {
+                config: TrippyConfig {
+                    dns_resolve_method,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn dns_lookup_as_info(self, dns_lookup_as_info: bool) -> Self {
+            Self {
+                config: TrippyConfig {
+                    dns_lookup_as_info,
+                    ..self.config
+                },
+            }
+        }
+
+        pub fn build(self) -> TrippyConfig {
+            self.config
+        }
     }
 }
diff --git a/src/main.rs b/src/main.rs
index 543efc2..8ba1ab9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -59,7 +59,7 @@ fn main() -> anyhow::Result<()> {
         TrippyAction::PrintTuiThemeItems => print_tui_theme_items(),
         TrippyAction::PrintTuiBindingCommands => print_tui_binding_commands(),
         TrippyAction::PrintConfigTemplate => print_config_template(),
-        TrippyAction::PrintShellCompletions(shell) => print_shell_completions(shell),
+        TrippyAction::PrintShellCompletions(shell) => print_shell_completions(shell)?,
     }
     Ok(())
 }
@@ -82,18 +82,12 @@ fn run_trippy(cfg: &TrippyConfig, platform: &Platform) -> anyhow::Result<()> {
 }
 
 fn print_tui_theme_items() {
-    println!(
-        "TUI theme color items: {}",
-        TuiThemeItem::VARIANTS.join(", ")
-    );
+    println!("{}", tui_theme_items());
     process::exit(0);
 }
 
 fn print_tui_binding_commands() {
-    println!(
-        "TUI binding commands: {}",
-        TuiCommandItem::VARIANTS.join(", ")
-    );
+    println!("{}", tui_binding_commands());
     process::exit(0);
 }
 
@@ -102,10 +96,8 @@ fn print_config_template() {
     process::exit(0);
 }
 
-fn print_shell_completions(shell: Shell) {
-    let mut cmd = Args::command();
-    let name = cmd.get_name().to_string();
-    clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
+fn print_shell_completions(shell: Shell) -> anyhow::Result<()> {
+    println!("{}", shell_completions(shell)?);
     process::exit(0);
 }
 
@@ -451,3 +443,49 @@ impl TraceInfo {
         }
     }
 }
+
+fn tui_theme_items() -> String {
+    format!(
+        "TUI theme color items: {}",
+        TuiThemeItem::VARIANTS.join(", ")
+    )
+}
+
+fn tui_binding_commands() -> String {
+    format!(
+        "TUI binding commands: {}",
+        TuiCommandItem::VARIANTS.join(", ")
+    )
+}
+
+fn shell_completions(shell: Shell) -> anyhow::Result<String> {
+    let mut cmd = Args::command();
+    let name = cmd.get_name().to_string();
+    let mut buffer: Vec<u8> = vec![];
+    clap_complete::generate(shell, &mut cmd, name, &mut buffer);
+    Ok(String::from_utf8(buffer)?)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use test_case::test_case;
+
+    #[test_case(include_str!("../test_resources/tui_theme_items.txt"), &tui_theme_items(); "tui theme items match")]
+    #[test_case(include_str!("../test_resources/tui_binding_commands.txt"), &tui_binding_commands(); "tui binding commands match")]
+    #[test_case(include_str!("../test_resources/completions_bash.txt"), &shell_completions(Shell::Bash).unwrap(); "generate bash shell completions")]
+    #[test_case(include_str!("../test_resources/completions_elvish.txt"), &shell_completions(Shell::Elvish).unwrap(); "generate elvish shell completions")]
+    #[test_case(include_str!("../test_resources/completions_fish.txt"), &shell_completions(Shell::Fish).unwrap(); "generate fish shell completions")]
+    #[test_case(include_str!("../test_resources/completions_powershell.txt"), &shell_completions(Shell::PowerShell).unwrap(); "generate powershell shell completions")]
+    #[test_case(include_str!("../test_resources/completions_zsh.txt"), &shell_completions(Shell::Zsh).unwrap(); "generate zsh shell completions")]
+    fn test_output(expected: &str, actual: &str) {
+        if remove_whitespace(actual.to_string()) != remove_whitespace(expected.to_string()) {
+            pretty_assertions::assert_eq!(actual, expected);
+        }
+    }
+
+    fn remove_whitespace(mut s: String) -> String {
+        s.retain(|c| !c.is_whitespace());
+        s
+    }
+}


Test patch:
diff --git a/test_resources/completions_bash.txt b/test_resources/completions_bash.txt
new file mode 100644
index 0000000..29f2192
--- /dev/null
+++ b/test_resources/completions_bash.txt
@@ -0,0 +1,282 @@
+_trip() {
+    local i cur prev opts cmd
+    COMPREPLY=()
+    cur="${COMP_WORDS[COMP_CWORD]}"
+    prev="${COMP_WORDS[COMP_CWORD-1]}"
+    cmd=""
+    opts=""
+
+    for i in ${COMP_WORDS[@]}
+    do
+        case "${cmd},${i}" in
+            ",$1")
+                cmd="trip"
+                ;;
+            *)
+                ;;
+        esac
+    done
+
+    case "${cmd}" in
+        trip)
+            opts="-c -m -u -p -4 -6 -P -S -A -I -i -T -g -R -U -f -t -Q -e -r -y -z -a -M -s -C -G -v -h -V --config-file --mode --unprivileged --protocol --udp --tcp --icmp --ipv4 --ipv6 --target-port --source-port --source-address --interface --min-round-duration --max-round-duration --grace-duration --initial-sequence --multipath-strategy --max-inflight --first-ttl --max-ttl --packet-size --payload-pattern --tos --icmp-extensions --read-timeout --dns-resolve-method --dns-resolve-all --dns-timeout --dns-lookup-as-info --tui-address-mode --tui-as-mode --tui-custom-columns --tui-icmp-extension-mode --tui-geoip-mode --tui-max-addrs --tui-max-samples --tui-max-flows --tui-preserve-screen --tui-refresh-rate --tui-privacy-max-ttl --tui-theme-colors --print-tui-theme-items --tui-key-bindings --print-tui-binding-commands --report-cycles --geoip-mmdb-file --generate --print-config-template --log-format --log-filter --log-span-events --verbose --help --version [TARGETS]..."
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                --config-file)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -c)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --mode)
+                    COMPREPLY=($(compgen -W "tui stream pretty markdown csv json dot flows silent" -- "${cur}"))
+                    return 0
+                    ;;
+                -m)
+                    COMPREPLY=($(compgen -W "tui stream pretty markdown csv json dot flows silent" -- "${cur}"))
+                    return 0
+                    ;;
+                --protocol)
+                    COMPREPLY=($(compgen -W "icmp udp tcp" -- "${cur}"))
+                    return 0
+                    ;;
+                -p)
+                    COMPREPLY=($(compgen -W "icmp udp tcp" -- "${cur}"))
+                    return 0
+                    ;;
+                --target-port)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -P)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --source-port)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -S)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --source-address)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -A)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --interface)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -I)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --min-round-duration)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -i)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --max-round-duration)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -T)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --grace-duration)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -g)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --initial-sequence)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --multipath-strategy)
+                    COMPREPLY=($(compgen -W "classic paris dublin" -- "${cur}"))
+                    return 0
+                    ;;
+                -R)
+                    COMPREPLY=($(compgen -W "classic paris dublin" -- "${cur}"))
+                    return 0
+                    ;;
+                --max-inflight)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -U)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --first-ttl)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -f)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --max-ttl)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -t)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --packet-size)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --payload-pattern)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tos)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -Q)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --read-timeout)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --dns-resolve-method)
+                    COMPREPLY=($(compgen -W "system resolv google cloudflare" -- "${cur}"))
+                    return 0
+                    ;;
+                -r)
+                    COMPREPLY=($(compgen -W "system resolv google cloudflare" -- "${cur}"))
+                    return 0
+                    ;;
+                --dns-timeout)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-address-mode)
+                    COMPREPLY=($(compgen -W "ip host both" -- "${cur}"))
+                    return 0
+                    ;;
+                -a)
+                    COMPREPLY=($(compgen -W "ip host both" -- "${cur}"))
+                    return 0
+                    ;;
+                --tui-as-mode)
+                    COMPREPLY=($(compgen -W "asn prefix country-code registry allocated name" -- "${cur}"))
+                    return 0
+                    ;;
+                --tui-custom-columns)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-icmp-extension-mode)
+                    COMPREPLY=($(compgen -W "off mpls full all" -- "${cur}"))
+                    return 0
+                    ;;
+                --tui-geoip-mode)
+                    COMPREPLY=($(compgen -W "off short long location" -- "${cur}"))
+                    return 0
+                    ;;
+                --tui-max-addrs)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -M)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-max-samples)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -s)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-max-flows)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-refresh-rate)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-privacy-max-ttl)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-theme-colors)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --tui-key-bindings)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --report-cycles)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -C)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --geoip-mmdb-file)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -G)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --generate)
+                    COMPREPLY=($(compgen -W "bash elvish fish powershell zsh" -- "${cur}"))
+                    return 0
+                    ;;
+                --log-format)
+                    COMPREPLY=($(compgen -W "compact pretty json chrome" -- "${cur}"))
+                    return 0
+                    ;;
+                --log-filter)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --log-span-events)
+                    COMPREPLY=($(compgen -W "off active full" -- "${cur}"))
+                    return 0
+                    ;;
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+    esac
+}
+
+if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
+    complete -F _trip -o nosort -o bashdefault -o default trip
+else
+    complete -F _trip -o bashdefault -o default trip
+fi
diff --git a/test_resources/completions_elvish.txt b/test_resources/completions_elvish.txt
new file mode 100644
index 0000000..e9aa582
--- /dev/null
+++ b/test_resources/completions_elvish.txt
@@ -0,0 +1,109 @@
+
+use builtin;
+use str;
+
+set edit:completion:arg-completer[trip] = {|@words|
+    fn spaces {|n|
+        builtin:repeat $n ' ' | str:join ''
+    }
+    fn cand {|text desc|
+        edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
+    }
+    var command = 'trip'
+    for word $words[1..-1] {
+        if (str:has-prefix $word '-') {
+            break
+        }
+        set command = $command';'$word
+    }
+    var completions = [
+        &'trip'= {
+            cand -c 'Config file'
+            cand --config-file 'Config file'
+            cand -m 'Output mode [default: tui]'
+            cand --mode 'Output mode [default: tui]'
+            cand -p 'Tracing protocol [default: icmp]'
+            cand --protocol 'Tracing protocol [default: icmp]'
+            cand -P 'The target port (TCP & UDP only) [default: 80]'
+            cand --target-port 'The target port (TCP & UDP only) [default: 80]'
+            cand -S 'The source port (TCP & UDP only) [default: auto]'
+            cand --source-port 'The source port (TCP & UDP only) [default: auto]'
+            cand -A 'The source IP address [default: auto]'
+            cand --source-address 'The source IP address [default: auto]'
+            cand -I 'The network interface [default: auto]'
+            cand --interface 'The network interface [default: auto]'
+            cand -i 'The minimum duration of every round [default: 1s]'
+            cand --min-round-duration 'The minimum duration of every round [default: 1s]'
+            cand -T 'The maximum duration of every round [default: 1s]'
+            cand --max-round-duration 'The maximum duration of every round [default: 1s]'
+            cand -g 'The period of time to wait for additional ICMP responses after the target has responded [default: 100ms]'
+            cand --grace-duration 'The period of time to wait for additional ICMP responses after the target has responded [default: 100ms]'
+            cand --initial-sequence 'The initial sequence number [default: 33000]'
+            cand -R 'The Equal-cost Multi-Path routing strategy (UDP only) [default: classic]'
+            cand --multipath-strategy 'The Equal-cost Multi-Path routing strategy (UDP only) [default: classic]'
+            cand -U 'The maximum number of in-flight ICMP echo requests [default: 24]'
+            cand --max-inflight 'The maximum number of in-flight ICMP echo requests [default: 24]'
+            cand -f 'The TTL to start from [default: 1]'
+            cand --first-ttl 'The TTL to start from [default: 1]'
+            cand -t 'The maximum number of TTL hops [default: 64]'
+            cand --max-ttl 'The maximum number of TTL hops [default: 64]'
+            cand --packet-size 'The size of IP packet to send (IP header + ICMP header + payload) [default: 84]'
+            cand --payload-pattern 'The repeating pattern in the payload of the ICMP packet [default: 0]'
+            cand -Q 'The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]'
+            cand --tos 'The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]'
+            cand --read-timeout 'The socket read timeout [default: 10ms]'
+            cand -r 'How to perform DNS queries [default: system]'
+            cand --dns-resolve-method 'How to perform DNS queries [default: system]'
+            cand --dns-timeout 'The maximum time to wait to perform DNS queries [default: 5s]'
+            cand -a 'How to render addresses [default: host]'
+            cand --tui-address-mode 'How to render addresses [default: host]'
+            cand --tui-as-mode 'How to render AS information [default: asn]'
+            cand --tui-custom-columns 'Custom columns to be displayed in the TUI hops table [default: HOLSRAVBWDT]'
+            cand --tui-icmp-extension-mode 'How to render ICMP extensions [default: off]'
+            cand --tui-geoip-mode 'How to render GeoIp information [default: short]'
+            cand -M 'The maximum number of addresses to show per hop [default: auto]'
+            cand --tui-max-addrs 'The maximum number of addresses to show per hop [default: auto]'
+            cand -s 'The maximum number of samples to record per hop [default: 256]'
+            cand --tui-max-samples 'The maximum number of samples to record per hop [default: 256]'
+            cand --tui-max-flows 'The maximum number of flows to show [default: 64]'
+            cand --tui-refresh-rate 'The Tui refresh rate [default: 100ms]'
+            cand --tui-privacy-max-ttl 'The maximum ttl of hops which will be masked for privacy [default: 0]'
+            cand --tui-theme-colors 'The TUI theme colors [item=color,item=color,..]'
+            cand --tui-key-bindings 'The TUI key bindings [command=key,command=key,..]'
+            cand -C 'The number of report cycles to run [default: 10]'
+            cand --report-cycles 'The number of report cycles to run [default: 10]'
+            cand -G 'The MaxMind City GeoLite2 mmdb file'
+            cand --geoip-mmdb-file 'The MaxMind City GeoLite2 mmdb file'
+            cand --generate 'Generate shell completion'
+            cand --log-format 'The debug log format [default: pretty]'
+            cand --log-filter 'The debug log filter [default: trippy=debug]'
+            cand --log-span-events 'The debug log format [default: off]'
+            cand -u 'Trace without requiring elevated privileges on supported platforms [default: false]'
+            cand --unprivileged 'Trace without requiring elevated privileges on supported platforms [default: false]'
+            cand --udp 'Trace using the UDP protocol'
+            cand --tcp 'Trace using the TCP protocol'
+            cand --icmp 'Trace using the ICMP protocol'
+            cand -4 'Use IPv4 only'
+            cand --ipv4 'Use IPv4 only'
+            cand -6 'Use IPv6 only'
+            cand --ipv6 'Use IPv6 only'
+            cand -e 'Parse ICMP extensions'
+            cand --icmp-extensions 'Parse ICMP extensions'
+            cand -y 'Trace to all IPs resolved from DNS lookup [default: false]'
+            cand --dns-resolve-all 'Trace to all IPs resolved from DNS lookup [default: false]'
+            cand -z 'Lookup autonomous system (AS) information during DNS queries [default: false]'
+            cand --dns-lookup-as-info 'Lookup autonomous system (AS) information during DNS queries [default: false]'
+            cand --tui-preserve-screen 'Preserve the screen on exit [default: false]'
+            cand --print-tui-theme-items 'Print all TUI theme items and exit'
+            cand --print-tui-binding-commands 'Print all TUI commands that can be bound and exit'
+            cand --print-config-template 'Print a template toml config file and exit'
+            cand -v 'Enable verbose debug logging'
+            cand --verbose 'Enable verbose debug logging'
+            cand -h 'Print help (see more with ''--help'')'
+            cand --help 'Print help (see more with ''--help'')'
+            cand -V 'Print version'
+            cand --version 'Print version'
+        }
+    ]
+    $completions[$command]
+}
diff --git a/test_resources/completions_fish.txt b/test_resources/completions_fish.txt
new file mode 100644
index 0000000..d8da083
--- /dev/null
+++ b/test_resources/completions_fish.txt
@@ -0,0 +1,55 @@
+complete -c trip -s c -l config-file -d 'Config file' -r -F
+complete -c trip -s m -l mode -d 'Output mode [default: tui]' -r -f -a "{tui    'Display interactive TUI',stream        'Display a continuous stream of tracing data',pretty    'Generate an pretty text table report for N cycles',markdown    'Generate a markdown text table report for N cycles',csv        'Generate a CSV report for N cycles',json       'Generate a JSON report for N cycles',dot       'Generate a Graphviz DOT file for N cycles',flows       'Display all flows',silent      'Do not generate any tracing output for N cycles'}"
+complete -c trip -s p -l protocol -d 'Tracing protocol [default: icmp]' -r -f -a "{icmp 'Internet Control Message Protocol',udp 'User Datagram Protocol',tcp    'Transmission Control Protocol'}"
+complete -c trip -s P -l target-port -d 'The target port (TCP & UDP only) [default: 80]' -r
+complete -c trip -s S -l source-port -d 'The source port (TCP & UDP only) [default: auto]' -r
+complete -c trip -s A -l source-address -d 'The source IP address [default: auto]' -r
+complete -c trip -s I -l interface -d 'The network interface [default: auto]' -r
+complete -c trip -s i -l min-round-duration -d 'The minimum duration of every round [default: 1s]' -r
+complete -c trip -s T -l max-round-duration -d 'The maximum duration of every round [default: 1s]' -r
+complete -c trip -s g -l grace-duration -d 'The period of time to wait for additional ICMP responses after the target has responded [default: 100ms]' -r
+complete -c trip -l initial-sequence -d 'The initial sequence number [default: 33000]' -r
+complete -c trip -s R -l multipath-strategy -d 'The Equal-cost Multi-Path routing strategy (UDP only) [default: classic]' -r -f -a "{classic    'The src or dest port is used to store the sequence number',paris       'The UDP `checksum` field is used to store the sequence number',dublin  'The IP `identifier` field is used to store the sequence number'}"
+complete -c trip -s U -l max-inflight -d 'The maximum number of in-flight ICMP echo requests [default: 24]' -r
+complete -c trip -s f -l first-ttl -d 'The TTL to start from [default: 1]' -r
+complete -c trip -s t -l max-ttl -d 'The maximum number of TTL hops [default: 64]' -r
+complete -c trip -l packet-size -d 'The size of IP packet to send (IP header + ICMP header + payload) [default: 84]' -r
+complete -c trip -l payload-pattern -d 'The repeating pattern in the payload of the ICMP packet [default: 0]' -r
+complete -c trip -s Q -l tos -d 'The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]' -r
+complete -c trip -l read-timeout -d 'The socket read timeout [default: 10ms]' -r
+complete -c trip -s r -l dns-resolve-method -d 'How to perform DNS queries [default: system]' -r -f -a "{system 'Resolve using the OS resolver',resolv  'Resolve using the `/etc/resolv.conf` DNS configuration',google 'Resolve using the Google `8.8.8.8` DNS service',cloudflare     'Resolve using the Cloudflare `1.1.1.1` DNS service'}"
+complete -c trip -l dns-timeout -d 'The maximum time to wait to perform DNS queries [default: 5s]' -r
+complete -c trip -s a -l tui-address-mode -d 'How to render addresses [default: host]' -r -f -a "{ip    'Show IP address only',host     'Show reverse-lookup DNS hostname only',both    'Show both IP address and reverse-lookup DNS hostname'}"
+complete -c trip -l tui-as-mode -d 'How to render AS information [default: asn]' -r -f -a "{asn 'Show the ASN',prefix   'Display the AS prefix',country-code    'Display the country code',registry     'Display the registry name',allocated   'Display the allocated date',name       'Display the AS name'}"
+complete -c trip -l tui-custom-columns -d 'Custom columns to be displayed in the TUI hops table [default: HOLSRAVBWDT]' -r
+complete -c trip -l tui-icmp-extension-mode -d 'How to render ICMP extensions [default: off]' -r -f -a "{off    'Do not show `icmp` extensions',mpls    'Show MPLS label(s) only',full  'Show full `icmp` extension data for all known extensions',all  'Show full `icmp` extension data for all classes'}"
+complete -c trip -l tui-geoip-mode -d 'How to render GeoIp information [default: short]' -r -f -a "{off 'Do not display GeoIp data',short       'Show short format',long        'Show long format',location     'Show latitude and Longitude format'}"
+complete -c trip -s M -l tui-max-addrs -d 'The maximum number of addresses to show per hop [default: auto]' -r
+complete -c trip -s s -l tui-max-samples -d 'The maximum number of samples to record per hop [default: 256]' -r
+complete -c trip -l tui-max-flows -d 'The maximum number of flows to show [default: 64]' -r
+complete -c trip -l tui-refresh-rate -d 'The Tui refresh rate [default: 100ms]' -r
+complete -c trip -l tui-privacy-max-ttl -d 'The maximum ttl of hops which will be masked for privacy [default: 0]' -r
+complete -c trip -l tui-theme-colors -d 'The TUI theme colors [item=color,item=color,..]' -r
+complete -c trip -l tui-key-bindings -d 'The TUI key bindings [command=key,command=key,..]' -r
+complete -c trip -s C -l report-cycles -d 'The number of report cycles to run [default: 10]' -r
+complete -c trip -s G -l geoip-mmdb-file -d 'The MaxMind City GeoLite2 mmdb file' -r -F
+complete -c trip -l generate -d 'Generate shell completion' -r -f -a "{bash     '',elvish       '',fish '',powershell   '',zsh  ''}"
+complete -c trip -l log-format -d 'The debug log format [default: pretty]' -r -f -a "{compact   'Display log data in a compact format',pretty   'Display log data in a pretty format',json      'Display log data in a json format',chrome      'Display log data in Chrome trace format'}"
+complete -c trip -l log-filter -d 'The debug log filter [default: trippy=debug]' -r
+complete -c trip -l log-span-events -d 'The debug log format [default: off]' -r -f -a "{off     'Do not display event spans',active     'Display enter and exit event spans',full       'Display all event spans'}"
+complete -c trip -s u -l unprivileged -d 'Trace without requiring elevated privileges on supported platforms [default: false]'
+complete -c trip -l udp -d 'Trace using the UDP protocol'
+complete -c trip -l tcp -d 'Trace using the TCP protocol'
+complete -c trip -l icmp -d 'Trace using the ICMP protocol'
+complete -c trip -s 4 -l ipv4 -d 'Use IPv4 only'
+complete -c trip -s 6 -l ipv6 -d 'Use IPv6 only'
+complete -c trip -s e -l icmp-extensions -d 'Parse ICMP extensions'
+complete -c trip -s y -l dns-resolve-all -d 'Trace to all IPs resolved from DNS lookup [default: false]'
+complete -c trip -s z -l dns-lookup-as-info -d 'Lookup autonomous system (AS) information during DNS queries [default: false]'
+complete -c trip -l tui-preserve-screen -d 'Preserve the screen on exit [default: false]'
+complete -c trip -l print-tui-theme-items -d 'Print all TUI theme items and exit'
+complete -c trip -l print-tui-binding-commands -d 'Print all TUI commands that can be bound and exit'
+complete -c trip -l print-config-template -d 'Print a template toml config file and exit'
+complete -c trip -s v -l verbose -d 'Enable verbose debug logging'
+complete -c trip -s h -l help -d 'Print help (see more with \'--help\')'
+complete -c trip -s V -l version -d 'Print version'
\ No newline at end of file
diff --git a/test_resources/completions_powershell.txt b/test_resources/completions_powershell.txt
new file mode 100644
index 0000000..c5e6595
--- /dev/null
+++ b/test_resources/completions_powershell.txt
@@ -0,0 +1,115 @@
+
+using namespace System.Management.Automation
+using namespace System.Management.Automation.Language
+
+Register-ArgumentCompleter -Native -CommandName 'trip' -ScriptBlock {
+    param($wordToComplete, $commandAst, $cursorPosition)
+
+    $commandElements = $commandAst.CommandElements
+    $command = @(
+        'trip'
+        for ($i = 1; $i -lt $commandElements.Count; $i++) {
+            $element = $commandElements[$i]
+            if ($element -isnot [StringConstantExpressionAst] -or
+                $element.StringConstantType -ne [StringConstantType]::BareWord -or
+                $element.Value.StartsWith('-') -or
+                $element.Value -eq $wordToComplete) {
+                break
+        }
+        $element.Value
+    }) -join ';'
+
+    $completions = @(switch ($command) {
+        'trip' {
+            [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Config file')
+            [CompletionResult]::new('--config-file', 'config-file', [CompletionResultType]::ParameterName, 'Config file')
+            [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Output mode [default: tui]')
+            [CompletionResult]::new('--mode', 'mode', [CompletionResultType]::ParameterName, 'Output mode [default: tui]')
+            [CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Tracing protocol [default: icmp]')
+            [CompletionResult]::new('--protocol', 'protocol', [CompletionResultType]::ParameterName, 'Tracing protocol [default: icmp]')
+            [CompletionResult]::new('-P', 'P ', [CompletionResultType]::ParameterName, 'The target port (TCP & UDP only) [default: 80]')
+            [CompletionResult]::new('--target-port', 'target-port', [CompletionResultType]::ParameterName, 'The target port (TCP & UDP only) [default: 80]')
+            [CompletionResult]::new('-S', 'S ', [CompletionResultType]::ParameterName, 'The source port (TCP & UDP only) [default: auto]')
+            [CompletionResult]::new('--source-port', 'source-port', [CompletionResultType]::ParameterName, 'The source port (TCP & UDP only) [default: auto]')
+            [CompletionResult]::new('-A', 'A ', [CompletionResultType]::ParameterName, 'The source IP address [default: auto]')
+            [CompletionResult]::new('--source-address', 'source-address', [CompletionResultType]::ParameterName, 'The source IP address [default: auto]')
+            [CompletionResult]::new('-I', 'I ', [CompletionResultType]::ParameterName, 'The network interface [default: auto]')
+            [CompletionResult]::new('--interface', 'interface', [CompletionResultType]::ParameterName, 'The network interface [default: auto]')
+            [CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'The minimum duration of every round [default: 1s]')
+            [CompletionResult]::new('--min-round-duration', 'min-round-duration', [CompletionResultType]::ParameterName, 'The minimum duration of every round [default: 1s]')
+            [CompletionResult]::new('-T', 'T ', [CompletionResultType]::ParameterName, 'The maximum duration of every round [default: 1s]')
+            [CompletionResult]::new('--max-round-duration', 'max-round-duration', [CompletionResultType]::ParameterName, 'The maximum duration of every round [default: 1s]')
+            [CompletionResult]::new('-g', 'g', [CompletionResultType]::ParameterName, 'The period of time to wait for additional ICMP responses after the target has responded [default: 100ms]')
+            [CompletionResult]::new('--grace-duration', 'grace-duration', [CompletionResultType]::ParameterName, 'The period of time to wait for additional ICMP responses after the target has responded [default: 100ms]')
+            [CompletionResult]::new('--initial-sequence', 'initial-sequence', [CompletionResultType]::ParameterName, 'The initial sequence number [default: 33000]')
+            [CompletionResult]::new('-R', 'R ', [CompletionResultType]::ParameterName, 'The Equal-cost Multi-Path routing strategy (UDP only) [default: classic]')
+            [CompletionResult]::new('--multipath-strategy', 'multipath-strategy', [CompletionResultType]::ParameterName, 'The Equal-cost Multi-Path routing strategy (UDP only) [default: classic]')
+            [CompletionResult]::new('-U', 'U ', [CompletionResultType]::ParameterName, 'The maximum number of in-flight ICMP echo requests [default: 24]')
+            [CompletionResult]::new('--max-inflight', 'max-inflight', [CompletionResultType]::ParameterName, 'The maximum number of in-flight ICMP echo requests [default: 24]')
+            [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'The TTL to start from [default: 1]')
+            [CompletionResult]::new('--first-ttl', 'first-ttl', [CompletionResultType]::ParameterName, 'The TTL to start from [default: 1]')
+            [CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'The maximum number of TTL hops [default: 64]')
+            [CompletionResult]::new('--max-ttl', 'max-ttl', [CompletionResultType]::ParameterName, 'The maximum number of TTL hops [default: 64]')
+            [CompletionResult]::new('--packet-size', 'packet-size', [CompletionResultType]::ParameterName, 'The size of IP packet to send (IP header + ICMP header + payload) [default: 84]')
+            [CompletionResult]::new('--payload-pattern', 'payload-pattern', [CompletionResultType]::ParameterName, 'The repeating pattern in the payload of the ICMP packet [default: 0]')
+            [CompletionResult]::new('-Q', 'Q ', [CompletionResultType]::ParameterName, 'The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]')
+            [CompletionResult]::new('--tos', 'tos', [CompletionResultType]::ParameterName, 'The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]')
+            [CompletionResult]::new('--read-timeout', 'read-timeout', [CompletionResultType]::ParameterName, 'The socket read timeout [default: 10ms]')
+            [CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'How to perform DNS queries [default: system]')
+            [CompletionResult]::new('--dns-resolve-method', 'dns-resolve-method', [CompletionResultType]::ParameterName, 'How to perform DNS queries [default: system]')
+            [CompletionResult]::new('--dns-timeout', 'dns-timeout', [CompletionResultType]::ParameterName, 'The maximum time to wait to perform DNS queries [default: 5s]')
+            [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'How to render addresses [default: host]')
+            [CompletionResult]::new('--tui-address-mode', 'tui-address-mode', [CompletionResultType]::ParameterName, 'How to render addresses [default: host]')
+            [CompletionResult]::new('--tui-as-mode', 'tui-as-mode', [CompletionResultType]::ParameterName, 'How to render AS information [default: asn]')
+            [CompletionResult]::new('--tui-custom-columns', 'tui-custom-columns', [CompletionResultType]::ParameterName, 'Custom columns to be displayed in the TUI hops table [default: HOLSRAVBWDT]')
+            [CompletionResult]::new('--tui-icmp-extension-mode', 'tui-icmp-extension-mode', [CompletionResultType]::ParameterName, 'How to render ICMP extensions [default: off]')
+            [CompletionResult]::new('--tui-geoip-mode', 'tui-geoip-mode', [CompletionResultType]::ParameterName, 'How to render GeoIp information [default: short]')
+            [CompletionResult]::new('-M', 'M ', [CompletionResultType]::ParameterName, 'The maximum number of addresses to show per hop [default: auto]')
+            [CompletionResult]::new('--tui-max-addrs', 'tui-max-addrs', [CompletionResultType]::ParameterName, 'The maximum number of addresses to show per hop [default: auto]')
+            [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'The maximum number of samples to record per hop [default: 256]')
+            [CompletionResult]::new('--tui-max-samples', 'tui-max-samples', [CompletionResultType]::ParameterName, 'The maximum number of samples to record per hop [default: 256]')
+            [CompletionResult]::new('--tui-max-flows', 'tui-max-flows', [CompletionResultType]::ParameterName, 'The maximum number of flows to show [default: 64]')
+            [CompletionResult]::new('--tui-refresh-rate', 'tui-refresh-rate', [CompletionResultType]::ParameterName, 'The Tui refresh rate [default: 100ms]')
+            [CompletionResult]::new('--tui-privacy-max-ttl', 'tui-privacy-max-ttl', [CompletionResultType]::ParameterName, 'The maximum ttl of hops which will be masked for privacy [default: 0]')
+            [CompletionResult]::new('--tui-theme-colors', 'tui-theme-colors', [CompletionResultType]::ParameterName, 'The TUI theme colors [item=color,item=color,..]')
+            [CompletionResult]::new('--tui-key-bindings', 'tui-key-bindings', [CompletionResultType]::ParameterName, 'The TUI key bindings [command=key,command=key,..]')
+            [CompletionResult]::new('-C', 'C ', [CompletionResultType]::ParameterName, 'The number of report cycles to run [default: 10]')
+            [CompletionResult]::new('--report-cycles', 'report-cycles', [CompletionResultType]::ParameterName, 'The number of report cycles to run [default: 10]')
+            [CompletionResult]::new('-G', 'G ', [CompletionResultType]::ParameterName, 'The MaxMind City GeoLite2 mmdb file')
+            [CompletionResult]::new('--geoip-mmdb-file', 'geoip-mmdb-file', [CompletionResultType]::ParameterName, 'The MaxMind City GeoLite2 mmdb file')
+            [CompletionResult]::new('--generate', 'generate', [CompletionResultType]::ParameterName, 'Generate shell completion')
+            [CompletionResult]::new('--log-format', 'log-format', [CompletionResultType]::ParameterName, 'The debug log format [default: pretty]')
+            [CompletionResult]::new('--log-filter', 'log-filter', [CompletionResultType]::ParameterName, 'The debug log filter [default: trippy=debug]')
+            [CompletionResult]::new('--log-span-events', 'log-span-events', [CompletionResultType]::ParameterName, 'The debug log format [default: off]')
+            [CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'Trace without requiring elevated privileges on supported platforms [default: false]')
+            [CompletionResult]::new('--unprivileged', 'unprivileged', [CompletionResultType]::ParameterName, 'Trace without requiring elevated privileges on supported platforms [default: false]')
+            [CompletionResult]::new('--udp', 'udp', [CompletionResultType]::ParameterName, 'Trace using the UDP protocol')
+            [CompletionResult]::new('--tcp', 'tcp', [CompletionResultType]::ParameterName, 'Trace using the TCP protocol')
+            [CompletionResult]::new('--icmp', 'icmp', [CompletionResultType]::ParameterName, 'Trace using the ICMP protocol')
+            [CompletionResult]::new('-4', '4', [CompletionResultType]::ParameterName, 'Use IPv4 only')
+            [CompletionResult]::new('--ipv4', 'ipv4', [CompletionResultType]::ParameterName, 'Use IPv4 only')
+            [CompletionResult]::new('-6', '6', [CompletionResultType]::ParameterName, 'Use IPv6 only')
+            [CompletionResult]::new('--ipv6', 'ipv6', [CompletionResultType]::ParameterName, 'Use IPv6 only')
+            [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Parse ICMP extensions')
+            [CompletionResult]::new('--icmp-extensions', 'icmp-extensions', [CompletionResultType]::ParameterName, 'Parse ICMP extensions')
+            [CompletionResult]::new('-y', 'y', [CompletionResultType]::ParameterName, 'Trace to all IPs resolved from DNS lookup [default: false]')
+            [CompletionResult]::new('--dns-resolve-all', 'dns-resolve-all', [CompletionResultType]::ParameterName, 'Trace to all IPs resolved from DNS lookup [default: false]')
+            [CompletionResult]::new('-z', 'z', [CompletionResultType]::ParameterName, 'Lookup autonomous system (AS) information during DNS queries [default: false]')
+            [CompletionResult]::new('--dns-lookup-as-info', 'dns-lookup-as-info', [CompletionResultType]::ParameterName, 'Lookup autonomous system (AS) information during DNS queries [default: false]')
+            [CompletionResult]::new('--tui-preserve-screen', 'tui-preserve-screen', [CompletionResultType]::ParameterName, 'Preserve the screen on exit [default: false]')
+            [CompletionResult]::new('--print-tui-theme-items', 'print-tui-theme-items', [CompletionResultType]::ParameterName, 'Print all TUI theme items and exit')
+            [CompletionResult]::new('--print-tui-binding-commands', 'print-tui-binding-commands', [CompletionResultType]::ParameterName, 'Print all TUI commands that can be bound and exit')
+            [CompletionResult]::new('--print-config-template', 'print-config-template', [CompletionResultType]::ParameterName, 'Print a template toml config file and exit')
+            [CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Enable verbose debug logging')
+            [CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Enable verbose debug logging')
+            [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
+            [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
+            [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
+            [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
+            break
+        }
+    })
+
+    $completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
+        Sort-Object -Property ListItemText
+}
diff --git a/test_resources/completions_zsh.txt b/test_resources/completions_zsh.txt
new file mode 100644
index 0000000..3b977a8
--- /dev/null
+++ b/test_resources/completions_zsh.txt
@@ -0,0 +1,167 @@
+#compdef trip
+
+autoload -U is-at-least
+
+_trip() {
+    typeset -A opt_args
+    typeset -a _arguments_options
+    local ret=1
+
+    if is-at-least 5.2; then
+        _arguments_options=(-s -S -C)
+    else
+        _arguments_options=(-s -C)
+    fi
+
+    local context curcontext="$curcontext" state line
+    _arguments "${_arguments_options[@]}" \
+'-c+[Config file]:CONFIG_FILE:_files' \
+'--config-file=[Config file]:CONFIG_FILE:_files' \
+'-m+[Output mode \[default\: tui\]]:MODE:((tui\:"Display interactive TUI"
+stream\:"Display a continuous stream of tracing data"
+pretty\:"Generate an pretty text table report for N cycles"
+markdown\:"Generate a markdown text table report for N cycles"
+csv\:"Generate a CSV report for N cycles"
+json\:"Generate a JSON report for N cycles"
+dot\:"Generate a Graphviz DOT file for N cycles"
+flows\:"Display all flows"
+silent\:"Do not generate any tracing output for N cycles"))' \
+'--mode=[Output mode \[default\: tui\]]:MODE:((tui\:"Display interactive TUI"
+stream\:"Display a continuous stream of tracing data"
+pretty\:"Generate an pretty text table report for N cycles"
+markdown\:"Generate a markdown text table report for N cycles"
+csv\:"Generate a CSV report for N cycles"
+json\:"Generate a JSON report for N cycles"
+dot\:"Generate a Graphviz DOT file for N cycles"
+flows\:"Display all flows"
+silent\:"Do not generate any tracing output for N cycles"))' \
+'-p+[Tracing protocol \[default\: icmp\]]:PROTOCOL:((icmp\:"Internet Control Message Protocol"
+udp\:"User Datagram Protocol"
+tcp\:"Transmission Control Protocol"))' \
+'--protocol=[Tracing protocol \[default\: icmp\]]:PROTOCOL:((icmp\:"Internet Control Message Protocol"
+udp\:"User Datagram Protocol"
+tcp\:"Transmission Control Protocol"))' \
+'-P+[The target port (TCP & UDP only) \[default\: 80\]]:TARGET_PORT: ' \
+'--target-port=[The target port (TCP & UDP only) \[default\: 80\]]:TARGET_PORT: ' \
+'-S+[The source port (TCP & UDP only) \[default\: auto\]]:SOURCE_PORT: ' \
+'--source-port=[The source port (TCP & UDP only) \[default\: auto\]]:SOURCE_PORT: ' \
+'(-I --interface)-A+[The source IP address \[default\: auto\]]:SOURCE_ADDRESS: ' \
+'(-I --interface)--source-address=[The source IP address \[default\: auto\]]:SOURCE_ADDRESS: ' \
+'-I+[The network interface \[default\: auto\]]:INTERFACE: ' \
+'--interface=[The network interface \[default\: auto\]]:INTERFACE: ' \
+'-i+[The minimum duration of every round \[default\: 1s\]]:MIN_ROUND_DURATION: ' \
+'--min-round-duration=[The minimum duration of every round \[default\: 1s\]]:MIN_ROUND_DURATION: ' \
+'-T+[The maximum duration of every round \[default\: 1s\]]:MAX_ROUND_DURATION: ' \
+'--max-round-duration=[The maximum duration of every round \[default\: 1s\]]:MAX_ROUND_DURATION: ' \
+'-g+[The period of time to wait for additional ICMP responses after the target has responded \[default\: 100ms\]]:GRACE_DURATION: ' \
+'--grace-duration=[The period of time to wait for additional ICMP responses after the target has responded \[default\: 100ms\]]:GRACE_DURATION: ' \
+'--initial-sequence=[The initial sequence number \[default\: 33000\]]:INITIAL_SEQUENCE: ' \
+'-R+[The Equal-cost Multi-Path routing strategy (UDP only) \[default\: classic\]]:MULTIPATH_STRATEGY:((classic\:"The src or dest port is used to store the sequence number"
+paris\:"The UDP \`checksum\` field is used to store the sequence number"
+dublin\:"The IP \`identifier\` field is used to store the sequence number"))' \
+'--multipath-strategy=[The Equal-cost Multi-Path routing strategy (UDP only) \[default\: classic\]]:MULTIPATH_STRATEGY:((classic\:"The src or dest port is used to store the sequence number"
+paris\:"The UDP \`checksum\` field is used to store the sequence number"
+dublin\:"The IP \`identifier\` field is used to store the sequence number"))' \
+'-U+[The maximum number of in-flight ICMP echo requests \[default\: 24\]]:MAX_INFLIGHT: ' \
+'--max-inflight=[The maximum number of in-flight ICMP echo requests \[default\: 24\]]:MAX_INFLIGHT: ' \
+'-f+[The TTL to start from \[default\: 1\]]:FIRST_TTL: ' \
+'--first-ttl=[The TTL to start from \[default\: 1\]]:FIRST_TTL: ' \
+'-t+[The maximum number of TTL hops \[default\: 64\]]:MAX_TTL: ' \
+'--max-ttl=[The maximum number of TTL hops \[default\: 64\]]:MAX_TTL: ' \
+'--packet-size=[The size of IP packet to send (IP header + ICMP header + payload) \[default\: 84\]]:PACKET_SIZE: ' \
+'--payload-pattern=[The repeating pattern in the payload of the ICMP packet \[default\: 0\]]:PAYLOAD_PATTERN: ' \
+'-Q+[The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) \[default\: 0\]]:TOS: ' \
+'--tos=[The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) \[default\: 0\]]:TOS: ' \
+'--read-timeout=[The socket read timeout \[default\: 10ms\]]:READ_TIMEOUT: ' \
+'-r+[How to perform DNS queries \[default\: system\]]:DNS_RESOLVE_METHOD:((system\:"Resolve using the OS resolver"
+resolv\:"Resolve using the \`/etc/resolv.conf\` DNS configuration"
+google\:"Resolve using the Google \`8.8.8.8\` DNS service"
+cloudflare\:"Resolve using the Cloudflare \`1.1.1.1\` DNS service"))' \
+'--dns-resolve-method=[How to perform DNS queries \[default\: system\]]:DNS_RESOLVE_METHOD:((system\:"Resolve using the OS resolver"
+resolv\:"Resolve using the \`/etc/resolv.conf\` DNS configuration"
+google\:"Resolve using the Google \`8.8.8.8\` DNS service"
+cloudflare\:"Resolve using the Cloudflare \`1.1.1.1\` DNS service"))' \
+'--dns-timeout=[The maximum time to wait to perform DNS queries \[default\: 5s\]]:DNS_TIMEOUT: ' \
+'-a+[How to render addresses \[default\: host\]]:TUI_ADDRESS_MODE:((ip\:"Show IP address only"
+host\:"Show reverse-lookup DNS hostname only"
+both\:"Show both IP address and reverse-lookup DNS hostname"))' \
+'--tui-address-mode=[How to render addresses \[default\: host\]]:TUI_ADDRESS_MODE:((ip\:"Show IP address only"
+host\:"Show reverse-lookup DNS hostname only"
+both\:"Show both IP address and reverse-lookup DNS hostname"))' \
+'--tui-as-mode=[How to render AS information \[default\: asn\]]:TUI_AS_MODE:((asn\:"Show the ASN"
+prefix\:"Display the AS prefix"
+country-code\:"Display the country code"
+registry\:"Display the registry name"
+allocated\:"Display the allocated date"
+name\:"Display the AS name"))' \
+'--tui-custom-columns=[Custom columns to be displayed in the TUI hops table \[default\: HOLSRAVBWDT\]]:TUI_CUSTOM_COLUMNS: ' \
+'--tui-icmp-extension-mode=[How to render ICMP extensions \[default\: off\]]:TUI_ICMP_EXTENSION_MODE:((off\:"Do not show \`icmp\` extensions"
+mpls\:"Show MPLS label(s) only"
+full\:"Show full \`icmp\` extension data for all known extensions"
+all\:"Show full \`icmp\` extension data for all classes"))' \
+'--tui-geoip-mode=[How to render GeoIp information \[default\: short\]]:TUI_GEOIP_MODE:((off\:"Do not display GeoIp data"
+short\:"Show short format"
+long\:"Show long format"
+location\:"Show latitude and Longitude format"))' \
+'-M+[The maximum number of addresses to show per hop \[default\: auto\]]:TUI_MAX_ADDRS: ' \
+'--tui-max-addrs=[The maximum number of addresses to show per hop \[default\: auto\]]:TUI_MAX_ADDRS: ' \
+'-s+[The maximum number of samples to record per hop \[default\: 256\]]:TUI_MAX_SAMPLES: ' \
+'--tui-max-samples=[The maximum number of samples to record per hop \[default\: 256\]]:TUI_MAX_SAMPLES: ' \
+'--tui-max-flows=[The maximum number of flows to show \[default\: 64\]]:TUI_MAX_FLOWS: ' \
+'--tui-refresh-rate=[The Tui refresh rate \[default\: 100ms\]]:TUI_REFRESH_RATE: ' \
+'--tui-privacy-max-ttl=[The maximum ttl of hops which will be masked for privacy \[default\: 0\]]:TUI_PRIVACY_MAX_TTL: ' \
+'*--tui-theme-colors=[The TUI theme colors \[item=color,item=color,..\]]:TUI_THEME_COLORS: ' \
+'*--tui-key-bindings=[The TUI key bindings \[command=key,command=key,..\]]:TUI_KEY_BINDINGS: ' \
+'-C+[The number of report cycles to run \[default\: 10\]]:REPORT_CYCLES: ' \
+'--report-cycles=[The number of report cycles to run \[default\: 10\]]:REPORT_CYCLES: ' \
+'-G+[The MaxMind City GeoLite2 mmdb file]:GEOIP_MMDB_FILE:_files' \
+'--geoip-mmdb-file=[The MaxMind City GeoLite2 mmdb file]:GEOIP_MMDB_FILE:_files' \
+'--generate=[Generate shell completion]:GENERATE:(bash elvish fish powershell zsh)' \
+'--log-format=[The debug log format \[default\: pretty\]]:LOG_FORMAT:((compact\:"Display log data in a compact format"
+pretty\:"Display log data in a pretty format"
+json\:"Display log data in a json format"
+chrome\:"Display log data in Chrome trace format"))' \
+'--log-filter=[The debug log filter \[default\: trippy=debug\]]:LOG_FILTER: ' \
+'--log-span-events=[The debug log format \[default\: off\]]:LOG_SPAN_EVENTS:((off\:"Do not display event spans"
+active\:"Display enter and exit event spans"
+full\:"Display all event spans"))' \
+'-u[Trace without requiring elevated privileges on supported platforms \[default\: false\]]' \
+'--unprivileged[Trace without requiring elevated privileges on supported platforms \[default\: false\]]' \
+'(-p --protocol --tcp --icmp)--udp[Trace using the UDP protocol]' \
+'(-p --protocol --udp --icmp)--tcp[Trace using the TCP protocol]' \
+'(-p --protocol --udp --tcp)--icmp[Trace using the ICMP protocol]' \
+'(-6 --ipv6)-4[Use IPv4 only]' \
+'(-6 --ipv6)--ipv4[Use IPv4 only]' \
+'(-4 --ipv4)-6[Use IPv6 only]' \
+'(-4 --ipv4)--ipv6[Use IPv6 only]' \
+'-e[Parse ICMP extensions]' \
+'--icmp-extensions[Parse ICMP extensions]' \
+'-y[Trace to all IPs resolved from DNS lookup \[default\: false\]]' \
+'--dns-resolve-all[Trace to all IPs resolved from DNS lookup \[default\: false\]]' \
+'-z[Lookup autonomous system (AS) information during DNS queries \[default\: false\]]' \
+'--dns-lookup-as-info[Lookup autonomous system (AS) information during DNS queries \[default\: false\]]' \
+'--tui-preserve-screen[Preserve the screen on exit \[default\: false\]]' \
+'--print-tui-theme-items[Print all TUI theme items and exit]' \
+'--print-tui-binding-commands[Print all TUI commands that can be bound and exit]' \
+'--print-config-template[Print a template toml config file and exit]' \
+'-v[Enable verbose debug logging]' \
+'--verbose[Enable verbose debug logging]' \
+'-h[Print help (see more with '\''--help'\'')]' \
+'--help[Print help (see more with '\''--help'\'')]' \
+'-V[Print version]' \
+'--version[Print version]' \
+'*::targets -- A space delimited list of hostnames and IPs to trace:' \
+&& ret=0
+}
+
+(( $+functions[_trip_commands] )) ||
+_trip_commands() {
+    local commands; commands=()
+    _describe -t commands 'trip commands' commands "$@"
+}
+
+if [ "$funcstack[1]" = "_trip" ]; then
+    _trip "$@"
+else
+    compdef _trip trip
+fi
diff --git a/test_resources/tui_binding_commands.txt b/test_resources/tui_binding_commands.txt
new file mode 100644
index 0000000..97d00a8
--- /dev/null
+++ b/test_resources/tui_binding_commands.txt
@@ -0,0 +1,1 @@
+TUI binding commands: toggle-help, toggle-help-alt, toggle-settings, next-hop, previous-hop, next-trace, previous-trace, next-hop-address, previous-hop-address, address-mode-ip, address-mode-host, address-mode-both, toggle-freeze, toggle-chart, toggle-map, toggle-flows, toggle-privacy, expand-hosts, expand-hosts-max, contract-hosts, contract-hosts-min, chart-zoom-in, chart-zoom-out, clear-trace-data, clear-dns-cache, clear-selection, toggle-as-info, toggle-hop-details, quit
diff --git a/test_resources/tui_theme_items.txt b/test_resources/tui_theme_items.txt
new file mode 100644
index 0000000..2462205
--- /dev/null
+++ b/test_resources/tui_theme_items.txt
@@ -0,0 +1,1 @@
+TUI theme color items: bg-color, border-color, text-color, tab-text-color, hops-table-header-bg-color, hops-table-header-text-color, hops-table-row-active-text-color, hops-table-row-inactive-text-color, hops-chart-selected-color, hops-chart-unselected-color, hops-chart-axis-color, frequency-chart-bar-color, frequency-chart-text-color, flows-chart-bar-selected-color, flows-chart-bar-unselected-color, flows-chart-text-current-color, flows-chart-text-non-current-color, samples-chart-color, help-dialog-bg-color, help-dialog-text-color, settings-tab-text-color, settings-dialog-bg-color, settings-table-header-text-color, settings-table-header-bg-color, settings-table-row-text-color, map-world-color, map-radius-color, map-selected-color, map-info-panel-border-color, map-info-panel-bg-color, map-info-panel-text-color
diff --git a/test_resources/usage_long.txt b/test_resources/usage_long.txt
new file mode 100644
index 0000000..d5cad83
--- /dev/null
+++ b/test_resources/usage_long.txt
@@ -0,0 +1,238 @@
+A network diagnostic tool
+
+Usage: trip [OPTIONS] [TARGETS]...
+
+Arguments:
+  [TARGETS]...
+          A space delimited list of hostnames and IPs to trace
+
+Options:
+  -c, --config-file <CONFIG_FILE>
+          Config file
+
+  -m, --mode <MODE>
+          Output mode [default: tui]
+
+          Possible values:
+          - tui:      Display interactive TUI
+          - stream:   Display a continuous stream of tracing data
+          - pretty:   Generate an pretty text table report for N cycles
+          - markdown: Generate a markdown text table report for N cycles
+          - csv:      Generate a CSV report for N cycles
+          - json:     Generate a JSON report for N cycles
+          - dot:      Generate a Graphviz DOT file for N cycles
+          - flows:    Display all flows
+          - silent:   Do not generate any tracing output for N cycles
+
+  -u, --unprivileged
+          Trace without requiring elevated privileges on supported platforms [default: false]
+
+  -p, --protocol <PROTOCOL>
+          Tracing protocol [default: icmp]
+
+          Possible values:
+          - icmp: Internet Control Message Protocol
+          - udp:  User Datagram Protocol
+          - tcp:  Transmission Control Protocol
+
+      --udp
+          Trace using the UDP protocol
+
+      --tcp
+          Trace using the TCP protocol
+
+      --icmp
+          Trace using the ICMP protocol
+
+  -4, --ipv4
+          Use IPv4 only
+
+  -6, --ipv6
+          Use IPv6 only
+
+  -P, --target-port <TARGET_PORT>
+          The target port (TCP & UDP only) [default: 80]
+
+  -S, --source-port <SOURCE_PORT>
+          The source port (TCP & UDP only) [default: auto]
+
+  -A, --source-address <SOURCE_ADDRESS>
+          The source IP address [default: auto]
+
+  -I, --interface <INTERFACE>
+          The network interface [default: auto]
+
+  -i, --min-round-duration <MIN_ROUND_DURATION>
+          The minimum duration of every round [default: 1s]
+
+  -T, --max-round-duration <MAX_ROUND_DURATION>
+          The maximum duration of every round [default: 1s]
+
+  -g, --grace-duration <GRACE_DURATION>
+          The period of time to wait for additional ICMP responses after the target has responded [default: 100ms]
+
+      --initial-sequence <INITIAL_SEQUENCE>
+          The initial sequence number [default: 33000]
+
+  -R, --multipath-strategy <MULTIPATH_STRATEGY>
+          The Equal-cost Multi-Path routing strategy (UDP only) [default: classic]
+
+          Possible values:
+          - classic: The src or dest port is used to store the sequence number
+          - paris:   The UDP `checksum` field is used to store the sequence number
+          - dublin:  The IP `identifier` field is used to store the sequence number
+
+  -U, --max-inflight <MAX_INFLIGHT>
+          The maximum number of in-flight ICMP echo requests [default: 24]
+
+  -f, --first-ttl <FIRST_TTL>
+          The TTL to start from [default: 1]
+
+  -t, --max-ttl <MAX_TTL>
+          The maximum number of TTL hops [default: 64]
+
+      --packet-size <PACKET_SIZE>
+          The size of IP packet to send (IP header + ICMP header + payload) [default: 84]
+
+      --payload-pattern <PAYLOAD_PATTERN>
+          The repeating pattern in the payload of the ICMP packet [default: 0]
+
+  -Q, --tos <TOS>
+          The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]
+
+  -e, --icmp-extensions
+          Parse ICMP extensions
+
+      --read-timeout <READ_TIMEOUT>
+          The socket read timeout [default: 10ms]
+
+  -r, --dns-resolve-method <DNS_RESOLVE_METHOD>
+          How to perform DNS queries [default: system]
+
+          Possible values:
+          - system:     Resolve using the OS resolver
+          - resolv:     Resolve using the `/etc/resolv.conf` DNS configuration
+          - google:     Resolve using the Google `8.8.8.8` DNS service
+          - cloudflare: Resolve using the Cloudflare `1.1.1.1` DNS service
+
+  -y, --dns-resolve-all
+          Trace to all IPs resolved from DNS lookup [default: false]
+
+      --dns-timeout <DNS_TIMEOUT>
+          The maximum time to wait to perform DNS queries [default: 5s]
+
+  -z, --dns-lookup-as-info
+          Lookup autonomous system (AS) information during DNS queries [default: false]
+
+  -a, --tui-address-mode <TUI_ADDRESS_MODE>
+          How to render addresses [default: host]
+
+          Possible values:
+          - ip:   Show IP address only
+          - host: Show reverse-lookup DNS hostname only
+          - both: Show both IP address and reverse-lookup DNS hostname
+
+      --tui-as-mode <TUI_AS_MODE>
+          How to render AS information [default: asn]
+
+          Possible values:
+          - asn:          Show the ASN
+          - prefix:       Display the AS prefix
+          - country-code: Display the country code
+          - registry:     Display the registry name
+          - allocated:    Display the allocated date
+          - name:         Display the AS name
+
+      --tui-custom-columns <TUI_CUSTOM_COLUMNS>
+          Custom columns to be displayed in the TUI hops table [default: HOLSRAVBWDT]
+
+      --tui-icmp-extension-mode <TUI_ICMP_EXTENSION_MODE>
+          How to render ICMP extensions [default: off]
+
+          Possible values:
+          - off:  Do not show `icmp` extensions
+          - mpls: Show MPLS label(s) only
+          - full: Show full `icmp` extension data for all known extensions
+          - all:  Show full `icmp` extension data for all classes
+
+      --tui-geoip-mode <TUI_GEOIP_MODE>
+          How to render GeoIp information [default: short]
+
+          Possible values:
+          - off:      Do not display GeoIp data
+          - short:    Show short format
+          - long:     Show long format
+          - location: Show latitude and Longitude format
+
+  -M, --tui-max-addrs <TUI_MAX_ADDRS>
+          The maximum number of addresses to show per hop [default: auto]
+
+  -s, --tui-max-samples <TUI_MAX_SAMPLES>
+          The maximum number of samples to record per hop [default: 256]
+
+      --tui-max-flows <TUI_MAX_FLOWS>
+          The maximum number of flows to show [default: 64]
+
+      --tui-preserve-screen
+          Preserve the screen on exit [default: false]
+
+      --tui-refresh-rate <TUI_REFRESH_RATE>
+          The Tui refresh rate [default: 100ms]
+
+      --tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>
+          The maximum ttl of hops which will be masked for privacy [default: 0]
+
+      --tui-theme-colors <TUI_THEME_COLORS>
+          The TUI theme colors [item=color,item=color,..]
+
+      --print-tui-theme-items
+          Print all TUI theme items and exit
+
+      --tui-key-bindings <TUI_KEY_BINDINGS>
+          The TUI key bindings [command=key,command=key,..]
+
+      --print-tui-binding-commands
+          Print all TUI commands that can be bound and exit
+
+  -C, --report-cycles <REPORT_CYCLES>
+          The number of report cycles to run [default: 10]
+
+  -G, --geoip-mmdb-file <GEOIP_MMDB_FILE>
+          The MaxMind City GeoLite2 mmdb file
+
+      --generate <GENERATE>
+          Generate shell completion
+          
+          [possible values: bash, elvish, fish, powershell, zsh]
+
+      --print-config-template
+          Print a template toml config file and exit
+
+      --log-format <LOG_FORMAT>
+          The debug log format [default: pretty]
+
+          Possible values:
+          - compact: Display log data in a compact format
+          - pretty:  Display log data in a pretty format
+          - json:    Display log data in a json format
+          - chrome:  Display log data in Chrome trace format
+
+      --log-filter <LOG_FILTER>
+          The debug log filter [default: trippy=debug]
+
+      --log-span-events <LOG_SPAN_EVENTS>
+          The debug log format [default: off]
+
+          Possible values:
+          - off:    Do not display event spans
+          - active: Display enter and exit event spans
+          - full:   Display all event spans
+
+  -v, --verbose
+          Enable verbose debug logging
+
+  -h, --help
+          Print help (see a summary with '-h')
+
+  -V, --version
+          Print version
diff --git a/test_resources/usage_short.txt b/test_resources/usage_short.txt
new file mode 100644
index 0000000..f884494
--- /dev/null
+++ b/test_resources/usage_short.txt
@@ -0,0 +1,63 @@
+A network diagnostic tool
+
+Usage: trip [OPTIONS] [TARGETS]...
+
+Arguments:
+  [TARGETS]...  A space delimited list of hostnames and IPs to trace
+
+Options:
+  -c, --config-file <CONFIG_FILE>                          Config file
+  -m, --mode <MODE>                                        Output mode [default: tui] [possible values: tui, stream, pretty, markdown, csv, json, dot, flows, silent]
+  -u, --unprivileged                                       Trace without requiring elevated privileges on supported platforms [default: false]
+  -p, --protocol <PROTOCOL>                                Tracing protocol [default: icmp] [possible values: icmp, udp, tcp]
+      --udp                                                Trace using the UDP protocol
+      --tcp                                                Trace using the TCP protocol
+      --icmp                                               Trace using the ICMP protocol
+  -4, --ipv4                                               Use IPv4 only
+  -6, --ipv6                                               Use IPv6 only
+  -P, --target-port <TARGET_PORT>                          The target port (TCP & UDP only) [default: 80]
+  -S, --source-port <SOURCE_PORT>                          The source port (TCP & UDP only) [default: auto]
+  -A, --source-address <SOURCE_ADDRESS>                    The source IP address [default: auto]
+  -I, --interface <INTERFACE>                              The network interface [default: auto]
+  -i, --min-round-duration <MIN_ROUND_DURATION>            The minimum duration of every round [default: 1s]
+  -T, --max-round-duration <MAX_ROUND_DURATION>            The maximum duration of every round [default: 1s]
+  -g, --grace-duration <GRACE_DURATION>                    The period of time to wait for additional ICMP responses after the target has responded [default: 100ms]
+      --initial-sequence <INITIAL_SEQUENCE>                The initial sequence number [default: 33000]
+  -R, --multipath-strategy <MULTIPATH_STRATEGY>            The Equal-cost Multi-Path routing strategy (UDP only) [default: classic] [possible values: classic, paris, dublin]
+  -U, --max-inflight <MAX_INFLIGHT>                        The maximum number of in-flight ICMP echo requests [default: 24]
+  -f, --first-ttl <FIRST_TTL>                              The TTL to start from [default: 1]
+  -t, --max-ttl <MAX_TTL>                                  The maximum number of TTL hops [default: 64]
+      --packet-size <PACKET_SIZE>                          The size of IP packet to send (IP header + ICMP header + payload) [default: 84]
+      --payload-pattern <PAYLOAD_PATTERN>                  The repeating pattern in the payload of the ICMP packet [default: 0]
+  -Q, --tos <TOS>                                          The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]
+  -e, --icmp-extensions                                    Parse ICMP extensions
+      --read-timeout <READ_TIMEOUT>                        The socket read timeout [default: 10ms]
+  -r, --dns-resolve-method <DNS_RESOLVE_METHOD>            How to perform DNS queries [default: system] [possible values: system, resolv, google, cloudflare]
+  -y, --dns-resolve-all                                    Trace to all IPs resolved from DNS lookup [default: false]
+      --dns-timeout <DNS_TIMEOUT>                          The maximum time to wait to perform DNS queries [default: 5s]
+  -z, --dns-lookup-as-info                                 Lookup autonomous system (AS) information during DNS queries [default: false]
+  -a, --tui-address-mode <TUI_ADDRESS_MODE>                How to render addresses [default: host] [possible values: ip, host, both]
+      --tui-as-mode <TUI_AS_MODE>                          How to render AS information [default: asn] [possible values: asn, prefix, country-code, registry, allocated, name]
+      --tui-custom-columns <TUI_CUSTOM_COLUMNS>            Custom columns to be displayed in the TUI hops table [default: HOLSRAVBWDT]
+      --tui-icmp-extension-mode <TUI_ICMP_EXTENSION_MODE>  How to render ICMP extensions [default: off] [possible values: off, mpls, full, all]
+      --tui-geoip-mode <TUI_GEOIP_MODE>                    How to render GeoIp information [default: short] [possible values: off, short, long, location]
+  -M, --tui-max-addrs <TUI_MAX_ADDRS>                      The maximum number of addresses to show per hop [default: auto]
+  -s, --tui-max-samples <TUI_MAX_SAMPLES>                  The maximum number of samples to record per hop [default: 256]
+      --tui-max-flows <TUI_MAX_FLOWS>                      The maximum number of flows to show [default: 64]
+      --tui-preserve-screen                                Preserve the screen on exit [default: false]
+      --tui-refresh-rate <TUI_REFRESH_RATE>                The Tui refresh rate [default: 100ms]
+      --tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>          The maximum ttl of hops which will be masked for privacy [default: 0]
+      --tui-theme-colors <TUI_THEME_COLORS>                The TUI theme colors [item=color,item=color,..]
+      --print-tui-theme-items                              Print all TUI theme items and exit
+      --tui-key-bindings <TUI_KEY_BINDINGS>                The TUI key bindings [command=key,command=key,..]
+      --print-tui-binding-commands                         Print all TUI commands that can be bound and exit
+  -C, --report-cycles <REPORT_CYCLES>                      The number of report cycles to run [default: 10]
+  -G, --geoip-mmdb-file <GEOIP_MMDB_FILE>                  The MaxMind City GeoLite2 mmdb file
+      --generate <GENERATE>                                Generate shell completion [possible values: bash, elvish, fish, powershell, zsh]
+      --print-config-template                              Print a template toml config file and exit
+      --log-format <LOG_FORMAT>                            The debug log format [default: pretty] [possible values: compact, pretty, json, chrome]
+      --log-filter <LOG_FILTER>                            The debug log filter [default: trippy=debug]
+      --log-span-events <LOG_SPAN_EVENTS>                  The debug log format [default: off] [possible values: off, active, full]
+  -v, --verbose                                            Enable verbose debug logging
+  -h, --help                                               Print help (see more with '--help')
+  -V, --version                                            Print version