Repository: static-web-server/static-web-server (language: rust)

Issue:
Boolean options without a value (--http2) are silently ignored
**Describe the bug**

Boolean options like `--http2` and `--security-headers` only work if you add `=true` to the end, which is kinda non-standard. Seems the blame lies with [clap's peculiar decision](https://github.com/clap-rs/clap/issues/1649) to ignore decades of option-parsing precedence by default. But also it seems that clap v2.0 (which comes from structopt) does not require the user to supply the `=true` and instead will just silently ignore the flag.

**To Reproduce**

```
$ static-web-server --port 8000 --log-level=trace --security-headers --root=./ 2>&1 | grep "security headers"
2023-05-29T16:19:43.950269Z  INFO static_web_server::server: security headers: enabled=false

$ static-web-server --port 8000 --log-level=trace --security-headers=true --root=./ 2>&1 | grep "security headers"
2023-05-29T16:21:45.744096Z  INFO static_web_server::server: security headers: enabled=true

$ static-web-server --port 8000 --log-level=trace --http2 --root=./ 2>&1 | grep "security headers"
2023-05-29T16:21:04.107193Z  INFO static_web_server::server: security headers: enabled=false
(also should have crashed since no tls cert flags were provided)
```

**Expected behavior**
* `--http2` should imply "true", or `--http2` should throw an error that a value is required.

**Environment and Specs**
 - static-web-server: 2.16.0 (built from commit 0f66443)
 - OS: Linux

**Additional context**
If we migrate from structopt to clapv3 (which includes structopt), then it will log `error: The argument '--http2 <HTTP2>' requires a value but none was supplied`. But I'm struggling to implement the "--http2 implies true, no value required" behavior.

I have started down this path of migrating to clapv3 here: https://github.com/static-web-server/static-web-server/compare/master...mac-chaffee:static-web-server:clap-migration



Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index da52844f..1b19c2ea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -78,6 +78,17 @@ dependencies = [
  "zstd-safe",
 ]
 
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -222,13 +233,41 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "2.34.0"
+version = "3.2.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
 dependencies = [
+ "atty",
  "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "indexmap",
+ "once_cell",
+ "strsim",
+ "termcolor",
  "textwrap",
- "unicode-width",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
 ]
 
 [[package]]
@@ -434,11 +473,17 @@ dependencies = [
 
 [[package]]
 name = "heck"
-version = "0.3.3"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
 dependencies = [
- "unicode-segmentation",
+ "libc",
 ]
 
 [[package]]
@@ -714,7 +759,7 @@ version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.2.6",
  "libc",
 ]
 
@@ -724,6 +769,12 @@ version = "1.17.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
 
+[[package]]
+name = "os_str_bytes"
+version = "6.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -1076,6 +1127,7 @@ dependencies = [
  "bcrypt",
  "bytes",
  "chrono",
+ "clap",
  "form_urlencoded",
  "futures-util",
  "globset",
@@ -1096,7 +1148,6 @@ dependencies = [
  "serde_repr",
  "signal-hook",
  "signal-hook-tokio",
- "structopt",
  "tikv-jemallocator",
  "tokio",
  "tokio-rustls",
@@ -1108,28 +1159,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "structopt"
-version = "0.3.26"
+name = "strsim"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
-dependencies = [
- "clap",
- "lazy_static",
- "structopt-derive",
-]
-
-[[package]]
-name = "structopt-derive"
-version = "0.4.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
-dependencies = [
- "heck",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
 [[package]]
 name = "subtle"
@@ -1160,14 +1193,20 @@ dependencies = [
 ]
 
 [[package]]
-name = "textwrap"
-version = "0.11.0"
+name = "termcolor"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
 dependencies = [
- "unicode-width",
+ "winapi-util",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+
 [[package]]
 name = "thread_local"
 version = "1.1.7"
@@ -1340,18 +1379,6 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
 
-[[package]]
-name = "unicode-segmentation"
-version = "1.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
-
 [[package]]
 name = "untrusted"
 version = "0.7.1"
@@ -1478,6 +1505,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index 5a2c8478..3387d6ed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -70,7 +70,7 @@ rustls-pemfile = { version = "1.0", optional = true }
 serde = { version = "1.0", default-features = false, features = ["derive"] }
 serde_ignored = "0.1"
 serde_repr = "0.1"
-structopt = { version = "0.3", default-features = false }
+clap = { version = "3.0", features = ["derive", "env"] }
 chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
 tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"] }
 tokio-rustls = { version = "0.24", optional = true }
diff --git a/src/directory_listing.rs b/src/directory_listing.rs
index 47753a1f..952db874 100644
--- a/src/directory_listing.rs
+++ b/src/directory_listing.rs
@@ -9,6 +9,7 @@
 #![allow(missing_docs)]
 
 use chrono::{DateTime, Local, NaiveDateTime, Utc};
+use clap::arg_enum;
 use futures_util::future::Either;
 use futures_util::{future, FutureExt};
 use headers::{ContentLength, ContentType, HeaderMapExt};
@@ -21,7 +22,6 @@ use std::future::Future;
 use std::io;
 use std::path::Path;
 use std::time::{SystemTime, UNIX_EPOCH};
-use structopt::clap::arg_enum;
 
 use crate::{exts::http::MethodExt, Context, Result};
 
diff --git a/src/settings/cli.rs b/src/settings/cli.rs
index 2c686d93..fc41286f 100644
--- a/src/settings/cli.rs
+++ b/src/settings/cli.rs
@@ -5,8 +5,8 @@
 
 //! The server CLI options
 
+use clap::StructOpt;
 use std::path::PathBuf;
-use structopt::StructOpt;
 
 use crate::directory_listing::DirListFmt;
 
@@ -14,19 +14,19 @@ use crate::directory_listing::DirListFmt;
 #[derive(Debug, StructOpt)]
 #[structopt(about, author)]
 pub struct General {
-    #[structopt(long, short = "a", default_value = "::", env = "SERVER_HOST")]
+    #[structopt(long, short = 'a', default_value = "::", env = "SERVER_HOST")]
     /// Host address (E.g 127.0.0.1 or ::1)
     pub host: String,
 
-    #[structopt(long, short = "p", default_value = "80", env = "SERVER_PORT")]
+    #[structopt(long, short = 'p', default_value = "80", env = "SERVER_PORT")]
     /// Host port
     pub port: u16,
 
     #[structopt(
         long,
-        short = "f",
+        short = 'f',
         env = "SERVER_LISTEN_FD",
-        conflicts_with_all(&["host", "port", "https_redirect"])
+        conflicts_with_all(&["host", "port", "https-redirect"])
     )]
     /// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP
     /// socket listener on the specified file descriptor number (usually zero). Requires that the
@@ -41,7 +41,7 @@ pub struct General {
         not(wasm),
         structopt(
             long,
-            short = "n",
+            short = 'n',
             default_value = "1",
             env = "SERVER_THREADS_MULTIPLIER"
         )
@@ -50,7 +50,7 @@ pub struct General {
         wasm,
         structopt(
             long,
-            short = "n",
+            short = 'n',
             default_value = "2",
             env = "SERVER_THREADS_MULTIPLIER"
         )
@@ -65,7 +65,7 @@ pub struct General {
         not(wasm),
         structopt(
             long,
-            short = "b",
+            short = 'b',
             default_value = "512",
             env = "SERVER_MAX_BLOCKING_THREADS"
         )
@@ -74,7 +74,7 @@ pub struct General {
         wasm,
         structopt(
             long,
-            short = "b",
+            short = 'b',
             default_value = "20",
             env = "SERVER_MAX_BLOCKING_THREADS"
         )
@@ -82,7 +82,7 @@ pub struct General {
     /// Maximum number of blocking threads
     pub max_blocking_threads: usize,
 
-    #[structopt(long, short = "d", default_value = "./public", env = "SERVER_ROOT")]
+    #[structopt(long, short = 'd', default_value = "./public", env = "SERVER_ROOT")]
     /// Root directory path of static files.
     pub root: PathBuf,
 
@@ -106,13 +106,13 @@ pub struct General {
     /// HTML file path that is used for GET requests when the requested path doesn't exist. The fallback page is served with a 200 status code, useful when using client routers. If the path is not specified or simply doesn't exist then this feature will not be active.
     pub page_fallback: Option<PathBuf>,
 
-    #[structopt(long, short = "g", default_value = "error", env = "SERVER_LOG_LEVEL")]
+    #[structopt(long, short = 'g', default_value = "error", env = "SERVER_LOG_LEVEL")]
     /// Specify a logging level in lower case. Values: error, warn, info, debug or trace
     pub log_level: String,
 
     #[structopt(
         long,
-        short = "c",
+        short = 'c',
         default_value = "",
         env = "SERVER_CORS_ALLOW_ORIGINS"
     )]
@@ -121,7 +121,7 @@ pub struct General {
 
     #[structopt(
         long,
-        short = "j",
+        short = 'j',
         default_value = "origin, content-type",
         env = "SERVER_CORS_ALLOW_HEADERS"
     )]
@@ -138,7 +138,7 @@ pub struct General {
 
     #[structopt(
         long,
-        short = "t",
+        short = 't',
         parse(try_from_str),
         default_value = "false",
         env = "SERVER_HTTP2_TLS"
@@ -148,13 +148,13 @@ pub struct General {
     /// Enable HTTP/2 with TLS support.
     pub http2: bool,
 
-    #[structopt(long, required_if("http2", "true"), env = "SERVER_HTTP2_TLS_CERT")]
+    #[structopt(long, required_if_eq("http2", "true"), env = "SERVER_HTTP2_TLS_CERT")]
     #[cfg(feature = "http2")]
     #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
     /// Specify the file path to read the certificate.
     pub http2_tls_cert: Option<PathBuf>,
 
-    #[structopt(long, required_if("http2", "true"), env = "SERVER_HTTP2_TLS_KEY")]
+    #[structopt(long, required_if_eq("http2", "true"), env = "SERVER_HTTP2_TLS_KEY")]
     #[cfg(feature = "http2")]
     #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
     /// Specify the file path to read the private key.
@@ -162,42 +162,46 @@ pub struct General {
 
     #[structopt(
         long,
-        required_if("http2", "true"),
+        requires_if("true", "http2"),
         parse(try_from_str),
         default_value = "false",
         env = "SERVER_HTTPS_REDIRECT"
     )]
     #[cfg(feature = "http2")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
     /// Redirect all requests with scheme "http" to "https" for the current server instance. It depends on "http2" to be enabled.
     pub https_redirect: bool,
 
     #[structopt(
         long,
-        required_if("https_redirect", "true"),
+        requires_if("true", "https-redirect"),
         default_value = "localhost",
         env = "HTTPS_REDIRECT_HOST"
     )]
     #[cfg(feature = "http2")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
     /// Canonical host name or IP of the HTTPS (HTTPS/2) server. It depends on "https_redirect" to be enabled.
     pub https_redirect_host: String,
 
     #[structopt(
         long,
-        required_if("https_redirect", "true"),
+        requires_if("true", "https-redirect"),
         default_value = "80",
         env = "HTTPS_REDIRECT_FROM_PORT"
     )]
     #[cfg(feature = "http2")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
     /// HTTP host port where the redirect server will listen for requests to redirect them to HTTPS. It depends on "https_redirect" to be enabled.
     pub https_redirect_from_port: u16,
 
     #[structopt(
         long,
-        required_if("https_redirect", "true"),
+        requires_if("true", "https-redirect"),
         default_value = "localhost",
         env = "HTTPS_REDIRECT_FROM_HOSTS"
     )]
     #[cfg(feature = "http2")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
     /// List of host names or IPs allowed to redirect from. HTTP requests must contain the HTTP 'Host' header and match against this list. It depends on "https_redirect" to be enabled.
     pub https_redirect_from_hosts: String,
 
@@ -205,7 +209,7 @@ pub struct General {
     #[cfg_attr(docsrs, doc(cfg(feature = "compression")))]
     #[structopt(
         long,
-        short = "x",
+        short = 'x',
         parse(try_from_str),
         default_value = "true",
         env = "SERVER_COMPRESSION"
@@ -227,7 +231,7 @@ pub struct General {
 
     #[structopt(
         long,
-        short = "z",
+        short = 'z',
         parse(try_from_str),
         default_value = "false",
         env = "SERVER_DIRECTORY_LISTING"
@@ -237,7 +241,7 @@ pub struct General {
 
     #[structopt(
         long,
-        required_if("directory_listing", "true"),
+        required_if_eq("directory-listing", "true"),
         default_value = "6",
         env = "SERVER_DIRECTORY_LISTING_ORDER"
     )]
@@ -246,8 +250,7 @@ pub struct General {
 
     #[structopt(
         long,
-        required_if("directory_listing", "true"),
-        possible_values = &DirListFmt::variants(),
+        required_if_eq("directory-listing", "true"),
         default_value = "html",
         env = "SERVER_DIRECTORY_LISTING_FORMAT",
         case_insensitive = true
@@ -258,8 +261,7 @@ pub struct General {
     #[structopt(
         long,
         parse(try_from_str),
-        required_if("http2", "true"),
-        default_value_if("http2", Some("true"), "true"),
+        default_value_if("http2", Some("true"), Some("true")),
         default_value = "false",
         env = "SERVER_SECURITY_HEADERS"
     )]
@@ -270,7 +272,7 @@ pub struct General {
 
     #[structopt(
         long,
-        short = "e",
+        short = 'e',
         parse(try_from_str),
         default_value = "true",
         env = "SERVER_CACHE_CONTROL_HEADERS"
@@ -282,11 +284,11 @@ pub struct General {
     #[structopt(long, default_value = "", env = "SERVER_BASIC_AUTH")]
     pub basic_auth: String,
 
-    #[structopt(long, short = "q", default_value = "0", env = "SERVER_GRACE_PERIOD")]
+    #[structopt(long, short = 'q', default_value = "0", env = "SERVER_GRACE_PERIOD")]
     /// Defines a grace period in seconds after a `SIGTERM` signal is caught which will delay the server before to shut it down gracefully. The maximum value is 255 seconds.
     pub grace_period: u8,
 
-    #[structopt(long, short = "w", env = "SERVER_CONFIG_FILE")]
+    #[structopt(long, short = 'w', env = "SERVER_CONFIG_FILE")]
     /// Server TOML configuration file path.
     pub config_file: Option<PathBuf>,
 
@@ -323,7 +325,7 @@ pub struct General {
     #[cfg(windows)]
     #[structopt(
         long,
-        short = "s",
+        short = 's',
         parse(try_from_str),
         default_value = "false",
         env = "SERVER_WINDOWS_SERVICE"
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 51cacaf0..bf94bd8f 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -6,10 +6,10 @@
 //! Module that provides all settings of SWS.
 //!
 
+use clap::StructOpt;
 use globset::{Glob, GlobMatcher};
 use headers::HeaderMap;
 use hyper::StatusCode;
-use structopt::StructOpt;
 
 use crate::{Context, Result};
 


Test patch: