Skip to content

Commit

Permalink
feat(tui)!: hide source address when tui-privacy-max-ttl is set (#1365
Browse files Browse the repository at this point in the history
)

BREAKING CHANGE: the default vaue for `tui-privacy-max-ttl` has changed
from `Some(0)` to `None`.
  • Loading branch information
fujiapple852 committed Oct 27, 2024
1 parent 3659ff7 commit 0f62ff2
Show file tree
Hide file tree
Showing 20 changed files with 47 additions and 39 deletions.
17 changes: 7 additions & 10 deletions crates/trippy-tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub struct TrippyConfig {
pub max_flows: usize,
pub tui_preserve_screen: bool,
pub tui_refresh_rate: Duration,
pub tui_privacy_max_ttl: u8,
pub tui_privacy_max_ttl: Option<u8>,
pub tui_address_mode: AddressMode,
pub tui_as_mode: AsMode,
pub tui_custom_columns: TuiColumns,
Expand Down Expand Up @@ -505,11 +505,8 @@ impl TrippyConfig {
cfg_file_tui.tui_refresh_rate,
constants::DEFAULT_TUI_REFRESH_RATE,
);
let tui_privacy_max_ttl = cfg_layer(
args.tui_privacy_max_ttl,
cfg_file_tui.tui_privacy_max_ttl,
constants::DEFAULT_TUI_PRIVACY_MAX_TTL,
);
let tui_privacy_max_ttl =
cfg_layer_opt(args.tui_privacy_max_ttl, cfg_file_tui.tui_privacy_max_ttl);
let tui_address_mode = cfg_layer(
args.tui_address_mode,
cfg_file_tui.tui_address_mode,
Expand Down Expand Up @@ -750,7 +747,7 @@ impl Default for TrippyConfig {
max_flows: defaults::DEFAULT_MAX_FLOWS,
tui_preserve_screen: constants::DEFAULT_TUI_PRESERVE_SCREEN,
tui_refresh_rate: constants::DEFAULT_TUI_REFRESH_RATE,
tui_privacy_max_ttl: constants::DEFAULT_TUI_PRIVACY_MAX_TTL,
tui_privacy_max_ttl: None,
tui_address_mode: constants::DEFAULT_TUI_ADDRESS_MODE,
tui_as_mode: constants::DEFAULT_TUI_AS_MODE,
tui_icmp_extension_mode: constants::DEFAULT_TUI_ICMP_EXTENSION_MODE,
Expand Down Expand Up @@ -1471,8 +1468,8 @@ mod tests {
compare(parse_config(cmd), expected);
}

#[test_case("trip example.com", Ok(cfg().tui_privacy_max_ttl(0).build()); "default tui privacy max ttl")]
#[test_case("trip example.com --tui-privacy-max-ttl 4", Ok(cfg().tui_privacy_max_ttl(4).build()); "custom tui privacy max ttl")]
#[test_case("trip example.com", Ok(cfg().tui_privacy_max_ttl(None).build()); "default tui privacy max ttl")]
#[test_case("trip example.com --tui-privacy-max-ttl 4", Ok(cfg().tui_privacy_max_ttl(Some(4)).build()); "custom tui privacy max ttl")]
#[test_case("trip example.com --tui-privacy-max-ttl foo", Err(anyhow!("error: invalid value 'foo' for '--tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>': invalid digit found in string For more information, try '--help'.")); "invalid tui privacy max ttl")]
fn test_tui_privacy_max_ttl(cmd: &str, expected: anyhow::Result<TrippyConfig>) {
compare(parse_config(cmd), expected);
Expand Down Expand Up @@ -2052,7 +2049,7 @@ mod tests {
}
}

pub fn tui_privacy_max_ttl(self, tui_privacy_max_ttl: u8) -> Self {
pub fn tui_privacy_max_ttl(self, tui_privacy_max_ttl: Option<u8>) -> Self {
Self {
config: TrippyConfig {
tui_privacy_max_ttl,
Expand Down
4 changes: 3 additions & 1 deletion crates/trippy-tui/src/config/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ pub struct Args {
#[arg(long, value_parser = parse_duration)]
pub tui_refresh_rate: Option<Duration>,

/// The maximum ttl of hops which will be masked for privacy [default: 0]
/// The maximum ttl of hops which will be masked for privacy [default: none]
///
/// If set, the source IP address and hostname will also be hidden.
#[arg(long)]
pub tui_privacy_max_ttl: Option<u8>,

Expand Down
3 changes: 0 additions & 3 deletions crates/trippy-tui/src/config/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ pub const DEFAULT_TUI_ADDRESS_MODE: AddressMode = AddressMode::Host;
/// The default value for `tui-refresh-rate`.
pub const DEFAULT_TUI_REFRESH_RATE: Duration = Duration::from_millis(100);

/// The default value for `tui-privacy-max-ttl`.
pub const DEFAULT_TUI_PRIVACY_MAX_TTL: u8 = 0;

/// The default value for `dns-resolve-method`.
pub const DEFAULT_DNS_RESOLVE_METHOD: DnsResolveMethodConfig = DnsResolveMethodConfig::System;

Expand Down
2 changes: 1 addition & 1 deletion crates/trippy-tui/src/config/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ impl Default for ConfigTui {
Self {
tui_preserve_screen: Some(super::constants::DEFAULT_TUI_PRESERVE_SCREEN),
tui_refresh_rate: Some(super::constants::DEFAULT_TUI_REFRESH_RATE),
tui_privacy_max_ttl: Some(super::constants::DEFAULT_TUI_PRIVACY_MAX_TTL),
tui_privacy_max_ttl: None,
tui_address_mode: Some(super::constants::DEFAULT_TUI_ADDRESS_MODE),
tui_as_mode: Some(super::constants::DEFAULT_TUI_AS_MODE),
tui_custom_columns: Some(String::from(super::constants::DEFAULT_CUSTOM_COLUMNS)),
Expand Down
4 changes: 2 additions & 2 deletions crates/trippy-tui/src/frontend/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct TuiConfig {
/// Refresh rate.
pub refresh_rate: Duration,
/// The maximum ttl of hops which will be masked for privacy.
pub privacy_max_ttl: u8,
pub privacy_max_ttl: Option<u8>,
/// Preserve screen on exit.
pub preserve_screen: bool,
/// How to render addresses.
Expand Down Expand Up @@ -42,7 +42,7 @@ impl TuiConfig {
#[allow(clippy::too_many_arguments)]
pub fn new(
refresh_rate: Duration,
privacy_max_ttl: u8,
privacy_max_ttl: Option<u8>,
preserve_screen: bool,
address_mode: AddressMode,
lookup_as_info: bool,
Expand Down
4 changes: 2 additions & 2 deletions crates/trippy-tui/src/frontend/render/bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) {
Span::raw("%: -")
};

let privacy = if app.tui_config.privacy_max_ttl > 0 {
Span::raw(format!(:{:2}", app.tui_config.privacy_max_ttl))
let privacy = if let Some(ttl) = app.tui_config.privacy_max_ttl {
Span::raw(format!(:{ttl:2}"))
} else {
Span::raw("»: -")
};
Expand Down
4 changes: 3 additions & 1 deletion crates/trippy-tui/src/frontend/render/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ fn render_source(app: &TuiApp) -> String {
}
}
}
if let Some(addr) = app.tracer_config().data.source_addr() {
if app.tui_config.privacy_max_ttl.is_some() {
format!("**{}**", t!("hidden"))
} else if let Some(addr) = app.tracer_config().data.source_addr() {
let entry = app.resolver.lazy_reverse_lookup_with_asinfo(addr);
if let Some(hostname) = entry.hostnames().next() {
format_both(app, hostname, addr)
Expand Down
4 changes: 3 additions & 1 deletion crates/trippy-tui/src/frontend/render/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ fn format_tui_settings(app: &TuiApp) -> Vec<SettingsItem> {
),
SettingsItem::new(
"tui-privacy-max-ttl",
format!("{}", app.tui_config.privacy_max_ttl),
app.tui_config
.privacy_max_ttl
.map_or_else(|| t!("off").to_string(), |m| m.to_string()),
),
SettingsItem::new(
"tui-address-mode",
Expand Down
4 changes: 2 additions & 2 deletions crates/trippy-tui/src/frontend/render/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ fn render_hostname(
geoip_lookup: &GeoIpLookup,
) -> (Cell<'static>, u16) {
let (hostname, count) = if hop.total_recv() > 0 {
if app.tui_config.privacy_max_ttl >= hop.ttl() {
if app.tui_config.privacy_max_ttl >= Some(hop.ttl()) {
(format!("**{}**", t!("hidden")), 1)
} else {
match app.tui_config.max_addrs {
Expand Down Expand Up @@ -512,7 +512,7 @@ fn render_hostname_with_details(
config: &TuiConfig,
) -> (Cell<'static>, u16) {
let rendered = if hop.total_recv() > 0 {
if config.privacy_max_ttl >= hop.ttl() {
if config.privacy_max_ttl >= Some(hop.ttl()) {
format!("**{}**", t!("hidden"))
} else {
let index = app.selected_hop_address;
Expand Down
4 changes: 2 additions & 2 deletions crates/trippy-tui/src/frontend/render/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fn render_map_canvas(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[Map
let any_show = entry
.hops
.iter()
.any(|hop| *hop > app.tui_config.privacy_max_ttl);
.any(|hop| Some(*hop) >= app.tui_config.privacy_max_ttl);
if any_show {
render_map_canvas_pin(ctx, entry);
render_map_canvas_radius(ctx, entry, theme.map_radius);
Expand Down Expand Up @@ -146,7 +146,7 @@ fn render_map_info_panel(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &
}
})
.collect::<Vec<_>>();
let info = if app.tui_config.privacy_max_ttl >= selected_hop.ttl() {
let info = if app.tui_config.privacy_max_ttl >= Some(selected_hop.ttl()) {
format!("**{}**", t!("hidden"))
} else {
match locations.as_slice() {
Expand Down
16 changes: 12 additions & 4 deletions crates/trippy-tui/src/frontend/tui_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,14 +381,22 @@ impl TuiApp {

pub fn expand_privacy(&mut self) {
let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len();
if usize::from(self.tui_config.privacy_max_ttl) < hop_count {
self.tui_config.privacy_max_ttl += 1;
if let Some(privacy_max_ttl) = self.tui_config.privacy_max_ttl {
if usize::from(privacy_max_ttl) < hop_count {
self.tui_config.privacy_max_ttl = Some(privacy_max_ttl + 1);
}
} else {
self.tui_config.privacy_max_ttl = Some(0);
}
}

pub fn contract_privacy(&mut self) {
if self.tui_config.privacy_max_ttl > 0 {
self.tui_config.privacy_max_ttl -= 1;
if let Some(privacy_max_ttl) = self.tui_config.privacy_max_ttl {
if privacy_max_ttl > 0 {
self.tui_config.privacy_max_ttl = Some(privacy_max_ttl - 1);
} else {
self.tui_config.privacy_max_ttl = None;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion ...rippy-tui/tests/resources/snapshots/[email protected]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
source: crates/trippy-tui/src/config.rs
---
AnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotraceOptions:-c,--config-file<CONFIG_FILE>Configfile-m,--mode<MODE>Outputmode[default:tui][possiblevalues:tui,stream,pretty,markdown,csv,json,dot,flows,silent]-u,--unprivilegedTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false]-p,--protocol<PROTOCOL>Tracingprotocol[default:icmp][possiblevalues:icmp,udp,tcp]--udpTraceusingtheUDPprotocol--tcpTraceusingtheTCPprotocol--icmpTraceusingtheICMPprotocol-F,--addr-family<ADDR_FAMILY>Theaddressfamily[default:Ipv4thenIpv6][possiblevalues:ipv4,ipv6,ipv6-then-ipv4,ipv4-then-ipv6]-4,--ipv4UseIPv4only-6,--ipv6UseIPv6only-P,--target-port<TARGET_PORT>Thetargetport(TCP&UDPonly)[default:80]-S,--source-port<SOURCE_PORT>Thesourceport(TCP&UDPonly)[default:auto]-A,--source-address<SOURCE_ADDRESS>ThesourceIPaddress[default:auto]-I,--interface<INTERFACE>Thenetworkinterface[default:auto]-i,--min-round-duration<MIN_ROUND_DURATION>Theminimumdurationofeveryround[default:1s]-T,--max-round-duration<MAX_ROUND_DURATION>Themaximumdurationofeveryround[default:1s]-g,--grace-duration<GRACE_DURATION>TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms]--initial-sequence<INITIAL_SEQUENCE>Theinitialsequencenumber[default:33434]-R,--multipath-strategy<MULTIPATH_STRATEGY>TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic][possiblevalues:classic,paris,dublin]-U,--max-inflight<MAX_INFLIGHT>Themaximumnumberofin-flightICMPechorequests[default:24]-f,--first-ttl<FIRST_TTL>TheTTLtostartfrom[default:1]-t,--max-ttl<MAX_TTL>ThemaximumnumberofTTLhops[default:64]--packet-size<PACKET_SIZE>ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84]--payload-pattern<PAYLOAD_PATTERN>TherepeatingpatterninthepayloadoftheICMPpacket[default:0]-Q,--tos<TOS>TheTOS(i.e.DSCP+ECN)IPheadervalue(TCPandUDPonly)[default:0]-e,--icmp-extensionsParseICMPextensions--read-timeout<READ_TIMEOUT>Thesocketreadtimeout[default:10ms]-r,--dns-resolve-method<DNS_RESOLVE_METHOD>HowtoperformDNSqueries[default:system][possiblevalues:system,resolv,google,cloudflare]-y,--dns-resolve-allTracetoallIPsresolvedfromDNSlookup[default:false]--dns-timeout<DNS_TIMEOUT>ThemaximumtimetowaittoperformDNSqueries[default:5s]--dns-ttl<DNS_TTL>Thetime-to-live(TTL)ofDNSentries[default:300s]-z,--dns-lookup-as-infoLookupautonomoussystem(AS)informationduringDNSqueries[default:false]-s,--max-samples<MAX_SAMPLES>Themaximumnumberofsamplestorecordperhop[default:256]--max-flows<MAX_FLOWS>Themaximumnumberofflowstorecord[default:64]-a,--tui-address-mode<TUI_ADDRESS_MODE>Howtorenderaddresses[default:host][possiblevalues:ip,host,both]--tui-as-mode<TUI_AS_MODE>Howtorenderautonomoussystem(AS)information[default:asn][possiblevalues:asn,prefix,country-code,registry,allocated,name]--tui-custom-columns<TUI_CUSTOM_COLUMNS>CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt]--tui-icmp-extension-mode<TUI_ICMP_EXTENSION_MODE>HowtorenderICMPextensions[default:off][possiblevalues:off,mpls,full,all]--tui-geoip-mode<TUI_GEOIP_MODE>HowtorenderGeoIpinformation[default:short][possiblevalues:off,short,long,location]-M,--tui-max-addrs<TUI_MAX_ADDRS>Themaximumnumberofaddressestoshowperhop[default:auto]--tui-preserve-screenPreservethescreenonexit[default:false]--tui-refresh-rate<TUI_REFRESH_RATE>TheTUIrefreshrate[default:100ms]--tui-privacy-max-ttl<TUI_PRIVACY_MAX_TTL>Themaximumttlofhopswhichwillbemaskedforprivacy[default:0]--tui-locale<TUI_LOCALE>ThelocaletousefortheTUI[default:auto]--tui-theme-colors<TUI_THEME_COLORS>TheTUIthemecolors[item=color,item=color,..]--print-tui-theme-itemsPrintallTUIthemeitemsandexit--tui-key-bindings<TUI_KEY_BINDINGS>TheTUIkeybindings[command=key,command=key,..]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit-C,--report-cycles<REPORT_CYCLES>Thenumberofreportcyclestorun[default:10]-G,--geoip-mmdb-file<GEOIP_MMDB_FILE>ThesupportedMaxMindorIPinfoGeoIpmmdbfile--generate<GENERATE>Generateshellcompletion[possiblevalues:bash,elvish,fish,powershell,zsh]--generate-manGenerateROFFmanpage--print-config-templatePrintatemplatetomlconfigfileandexit--print-localesPrintallavailableTUIlocalesandexit--log-format<LOG_FORMAT>Thedebuglogformat[default:pretty][possiblevalues:compact,pretty,json,chrome]--log-filter<LOG_FILTER>Thedebuglogfilter[default:trippy=debug]--log-span-events<LOG_SPAN_EVENTS>Thedebuglogformat[default:off][possiblevalues:off,active,full]-v,--verboseEnableverbosedebuglogging-h,--helpPrinthelp(seemorewith'--help')-V,--versionPrintversion
AnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotraceOptions:-c,--config-file<CONFIG_FILE>Configfile-m,--mode<MODE>Outputmode[default:tui][possiblevalues:tui,stream,pretty,markdown,csv,json,dot,flows,silent]-u,--unprivilegedTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false]-p,--protocol<PROTOCOL>Tracingprotocol[default:icmp][possiblevalues:icmp,udp,tcp]--udpTraceusingtheUDPprotocol--tcpTraceusingtheTCPprotocol--icmpTraceusingtheICMPprotocol-F,--addr-family<ADDR_FAMILY>Theaddressfamily[default:Ipv4thenIpv6][possiblevalues:ipv4,ipv6,ipv6-then-ipv4,ipv4-then-ipv6]-4,--ipv4UseIPv4only-6,--ipv6UseIPv6only-P,--target-port<TARGET_PORT>Thetargetport(TCP&UDPonly)[default:80]-S,--source-port<SOURCE_PORT>Thesourceport(TCP&UDPonly)[default:auto]-A,--source-address<SOURCE_ADDRESS>ThesourceIPaddress[default:auto]-I,--interface<INTERFACE>Thenetworkinterface[default:auto]-i,--min-round-duration<MIN_ROUND_DURATION>Theminimumdurationofeveryround[default:1s]-T,--max-round-duration<MAX_ROUND_DURATION>Themaximumdurationofeveryround[default:1s]-g,--grace-duration<GRACE_DURATION>TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms]--initial-sequence<INITIAL_SEQUENCE>Theinitialsequencenumber[default:33434]-R,--multipath-strategy<MULTIPATH_STRATEGY>TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic][possiblevalues:classic,paris,dublin]-U,--max-inflight<MAX_INFLIGHT>Themaximumnumberofin-flightICMPechorequests[default:24]-f,--first-ttl<FIRST_TTL>TheTTLtostartfrom[default:1]-t,--max-ttl<MAX_TTL>ThemaximumnumberofTTLhops[default:64]--packet-size<PACKET_SIZE>ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84]--payload-pattern<PAYLOAD_PATTERN>TherepeatingpatterninthepayloadoftheICMPpacket[default:0]-Q,--tos<TOS>TheTOS(i.e.DSCP+ECN)IPheadervalue(TCPandUDPonly)[default:0]-e,--icmp-extensionsParseICMPextensions--read-timeout<READ_TIMEOUT>Thesocketreadtimeout[default:10ms]-r,--dns-resolve-method<DNS_RESOLVE_METHOD>HowtoperformDNSqueries[default:system][possiblevalues:system,resolv,google,cloudflare]-y,--dns-resolve-allTracetoallIPsresolvedfromDNSlookup[default:false]--dns-timeout<DNS_TIMEOUT>ThemaximumtimetowaittoperformDNSqueries[default:5s]--dns-ttl<DNS_TTL>Thetime-to-live(TTL)ofDNSentries[default:300s]-z,--dns-lookup-as-infoLookupautonomoussystem(AS)informationduringDNSqueries[default:false]-s,--max-samples<MAX_SAMPLES>Themaximumnumberofsamplestorecordperhop[default:256]--max-flows<MAX_FLOWS>Themaximumnumberofflowstorecord[default:64]-a,--tui-address-mode<TUI_ADDRESS_MODE>Howtorenderaddresses[default:host][possiblevalues:ip,host,both]--tui-as-mode<TUI_AS_MODE>Howtorenderautonomoussystem(AS)information[default:asn][possiblevalues:asn,prefix,country-code,registry,allocated,name]--tui-custom-columns<TUI_CUSTOM_COLUMNS>CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt]--tui-icmp-extension-mode<TUI_ICMP_EXTENSION_MODE>HowtorenderICMPextensions[default:off][possiblevalues:off,mpls,full,all]--tui-geoip-mode<TUI_GEOIP_MODE>HowtorenderGeoIpinformation[default:short][possiblevalues:off,short,long,location]-M,--tui-max-addrs<TUI_MAX_ADDRS>Themaximumnumberofaddressestoshowperhop[default:auto]--tui-preserve-screenPreservethescreenonexit[default:false]--tui-refresh-rate<TUI_REFRESH_RATE>TheTUIrefreshrate[default:100ms]--tui-privacy-max-ttl<TUI_PRIVACY_MAX_TTL>Themaximumttlofhopswhichwillbemaskedforprivacy[default:none]--tui-locale<TUI_LOCALE>ThelocaletousefortheTUI[default:auto]--tui-theme-colors<TUI_THEME_COLORS>TheTUIthemecolors[item=color,item=color,..]--print-tui-theme-itemsPrintallTUIthemeitemsandexit--tui-key-bindings<TUI_KEY_BINDINGS>TheTUIkeybindings[command=key,command=key,..]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit-C,--report-cycles<REPORT_CYCLES>Thenumberofreportcyclestorun[default:10]-G,--geoip-mmdb-file<GEOIP_MMDB_FILE>ThesupportedMaxMindorIPinfoGeoIpmmdbfile--generate<GENERATE>Generateshellcompletion[possiblevalues:bash,elvish,fish,powershell,zsh]--generate-manGenerateROFFmanpage--print-config-templatePrintatemplatetomlconfigfileandexit--print-localesPrintallavailableTUIlocalesandexit--log-format<LOG_FORMAT>Thedebuglogformat[default:pretty][possiblevalues:compact,pretty,json,chrome]--log-filter<LOG_FILTER>Thedebuglogfilter[default:trippy=debug]--log-span-events<LOG_SPAN_EVENTS>Thedebuglogformat[default:off][possiblevalues:off,active,full]-v,--verboseEnableverbosedebuglogging-h,--helpPrinthelp(seemorewith'--help')-V,--versionPrintversion
Loading

0 comments on commit 0f62ff2

Please sign in to comment.