Repository: sharkdp/bat (language: rust)

Issue:
Upgrade to clap 3
I think everyone agrees we need/should upgrade to clap 3 at some point. This ticket tracks that work.

I mostly create this ticket as a reminder for us to turn Dependabot updates back on for clap 3.x when we have done that work. I will turn off Dependabot updates for clap 3.x versions now.



Discussion / hints:
This might also allow us to automatically generate shell completion files again. Assuming they are as good as the hand-written ones.

See also: https://github.com/sharkdp/bat/blob/312c8ef01f1a80f4a806629a7dacd32508b267a5/build.rs#L1-L2
I have now migrated three other projects to clap 3. I would suggest that we first do a ["normal" migration](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md#migrating) by still using the `App`/`Arg` structs. We can later think about using the new (`structopt`) `derive` functionality, which might simplify our code quite a bit. See also: #998
Sounds good. Also related: https://github.com/sharkdp/bat/pull/1281 that initially depended on clap 3 mechanisms that was later removed from the API, but
* there might we a new way to achieve the same thing
* we might be successful in requesting clap to add the API again

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index f41c859701..5d974115e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -193,18 +193,28 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "clap"
-version = "2.34.0"
+version = "3.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd"
 dependencies = [
- "ansi_term",
  "atty",
  "bitflags",
+ "clap_lex",
+ "indexmap",
+ "once_cell",
  "strsim",
- "term_size",
+ "termcolor",
+ "terminal_size",
  "textwrap",
- "unicode-width",
- "vec_map",
+]
+
+[[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]]
@@ -715,6 +725,12 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "os_str_bytes"
+version = "6.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
+
 [[package]]
 name = "parking_lot"
 version = "0.11.2"
@@ -1045,9 +1061,9 @@ checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe"
 
 [[package]]
 name = "strsim"
-version = "0.8.0"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
 [[package]]
 name = "syn"
@@ -1108,16 +1124,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "term_size"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9"
-dependencies = [
- "libc",
- "winapi",
-]
-
 [[package]]
 name = "termcolor"
 version = "1.1.2"
@@ -1145,12 +1151,11 @@ checksum = "13a4ec180a2de59b57434704ccfad967f789b12737738798fa08798cd5824c16"
 
 [[package]]
 name = "textwrap"
-version = "0.11.0"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
 dependencies = [
- "term_size",
- "unicode-width",
+ "terminal_size",
 ]
 
 [[package]]
@@ -1243,12 +1248,6 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
-[[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
 [[package]]
 name = "version_check"
 version = "0.9.4"
diff --git a/Cargo.toml b/Cargo.toml
index 73cefea6f7..e14f3cfa73 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -77,10 +77,10 @@ default-features = false
 features = ["parsing"]
 
 [dependencies.clap]
-version = "2.34"
+version = "3.2.20"
 optional = true
 default-features = false
-features = ["suggestions", "color", "wrap_help"]
+features = ["std", "suggestions", "color", "wrap_help", "cargo"]
 
 [dev-dependencies]
 assert_cmd = "2.0.4"
@@ -93,7 +93,7 @@ tempfile = "3.3.0"
 nix = { version = "0.24.2", default-features = false, features = ["term"] }
 
 [build-dependencies]
-clap = { version = "2.34", optional = true }
+clap = { version = "3.2.20", optional = true }
 
 [profile.release]
 lto = true
diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs
index 78a0df69d7..7bab26c384 100644
--- a/src/bin/bat/app.rs
+++ b/src/bin/bat/app.rs
@@ -1,6 +1,6 @@
 use std::collections::HashSet;
 use std::env;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::str::FromStr;
 
 use atty::{self, Stream};
@@ -32,7 +32,7 @@ fn is_truecolor_terminal() -> bool {
 }
 
 pub struct App {
-    pub matches: ArgMatches<'static>,
+    pub matches: ArgMatches,
     interactive_output: bool,
 }
 
@@ -49,7 +49,7 @@ impl App {
         })
     }
 
-    fn matches(interactive_output: bool) -> Result<ArgMatches<'static>> {
+    fn matches(interactive_output: bool) -> Result<ArgMatches> {
         let args = if wild::args_os().nth(1) == Some("cache".into())
             || wild::args_os().any(|arg| arg == "--no-config")
         {
@@ -79,13 +79,13 @@ impl App {
     pub fn config(&self, inputs: &[Input]) -> Result<Config> {
         let style_components = self.style_components()?;
 
-        let paging_mode = match self.matches.value_of("paging") {
+        let paging_mode = match self.matches.get_one::<String>("paging").map(|s| s.as_str()) {
             Some("always") => PagingMode::Always,
             Some("never") => PagingMode::Never,
             Some("auto") | None => {
                 // If we have -pp as an option when in auto mode, the pager should be disabled.
-                let extra_plain = self.matches.occurrences_of("plain") > 1;
-                if extra_plain || self.matches.is_present("no-paging") {
+                let extra_plain = self.matches.get_count("plain") > 1;
+                if extra_plain || self.matches.get_flag("no-paging") {
                     PagingMode::Never
                 } else if inputs.iter().any(Input::is_stdin) {
                     // If we are reading from stdin, only enable paging if we write to an
@@ -107,13 +107,13 @@ impl App {
 
         let mut syntax_mapping = SyntaxMapping::builtin();
 
-        if let Some(values) = self.matches.values_of("ignored-suffix") {
+        if let Some(values) = self.matches.get_many::<String>("ignored-suffix") {
             for suffix in values {
                 syntax_mapping.insert_ignored_suffix(suffix);
             }
         }
 
-        if let Some(values) = self.matches.values_of("map-syntax") {
+        if let Some(values) = self.matches.get_many::<String>("map-syntax") {
             for from_to in values {
                 let parts: Vec<_> = from_to.split(':').collect();
 
@@ -125,36 +125,43 @@ impl App {
             }
         }
 
-        let maybe_term_width = self.matches.value_of("terminal-width").and_then(|w| {
-            if w.starts_with('+') || w.starts_with('-') {
-                // Treat argument as a delta to the current terminal width
-                w.parse().ok().map(|delta: i16| {
-                    let old_width: u16 = Term::stdout().size().1;
-                    let new_width: i32 = i32::from(old_width) + i32::from(delta);
-
-                    if new_width <= 0 {
-                        old_width as usize
-                    } else {
-                        new_width as usize
-                    }
-                })
-            } else {
-                w.parse().ok()
-            }
-        });
+        let maybe_term_width = self
+            .matches
+            .get_one::<String>("terminal-width")
+            .and_then(|w| {
+                if w.starts_with('+') || w.starts_with('-') {
+                    // Treat argument as a delta to the current terminal width
+                    w.parse().ok().map(|delta: i16| {
+                        let old_width: u16 = Term::stdout().size().1;
+                        let new_width: i32 = i32::from(old_width) + i32::from(delta);
+
+                        if new_width <= 0 {
+                            old_width as usize
+                        } else {
+                            new_width as usize
+                        }
+                    })
+                } else {
+                    w.parse().ok()
+                }
+            });
 
         Ok(Config {
             true_color: is_truecolor_terminal(),
-            language: self.matches.value_of("language").or_else(|| {
-                if self.matches.is_present("show-all") {
-                    Some("show-nonprintable")
-                } else {
-                    None
-                }
-            }),
-            show_nonprintable: self.matches.is_present("show-all"),
+            language: self
+                .matches
+                .get_one::<String>("language")
+                .map(|s| s.as_str())
+                .or_else(|| {
+                    if self.matches.get_flag("show-all") {
+                        Some("show-nonprintable")
+                    } else {
+                        None
+                    }
+                }),
+            show_nonprintable: self.matches.get_flag("show-all"),
             wrapping_mode: if self.interactive_output || maybe_term_width.is_some() {
-                match self.matches.value_of("wrap") {
+                match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) {
                     Some("character") => WrappingMode::Character,
                     Some("never") => WrappingMode::NoWrapping(true),
                     Some("auto") | None => {
@@ -171,8 +178,8 @@ impl App {
                 // There's no point in wrapping when this is the case.
                 WrappingMode::NoWrapping(false)
             },
-            colored_output: self.matches.is_present("force-colorization")
-                || match self.matches.value_of("color") {
+            colored_output: self.matches.get_flag("force-colorization")
+                || match self.matches.get_one::<String>("color").map(|s| s.as_str()) {
                     Some("always") => true,
                     Some("never") => false,
                     Some("auto") => env::var_os("NO_COLOR").is_none() && self.interactive_output,
@@ -181,12 +188,16 @@ impl App {
             paging_mode,
             term_width: maybe_term_width.unwrap_or(Term::stdout().size().1 as usize),
             loop_through: !(self.interactive_output
-                || self.matches.value_of("color") == Some("always")
-                || self.matches.value_of("decorations") == Some("always")
-                || self.matches.is_present("force-colorization")),
+                || self.matches.get_one::<String>("color").map(|s| s.as_str()) == Some("always")
+                || self
+                    .matches
+                    .get_one::<String>("decorations")
+                    .map(|s| s.as_str())
+                    == Some("always")
+                || self.matches.get_flag("force-colorization")),
             tab_width: self
                 .matches
-                .value_of("tabs")
+                .get_one::<String>("tabs")
                 .map(String::from)
                 .or_else(|| env::var("BAT_TABS").ok())
                 .and_then(|t| t.parse().ok())
@@ -199,7 +210,7 @@ impl App {
                 ),
             theme: self
                 .matches
-                .value_of("theme")
+                .get_one::<String>("theme")
                 .map(String::from)
                 .or_else(|| env::var("BAT_THEME").ok())
                 .map(|s| {
@@ -210,19 +221,19 @@ impl App {
                     }
                 })
                 .unwrap_or_else(|| String::from(HighlightingAssets::default_theme())),
-            visible_lines: match self.matches.is_present("diff") {
+            visible_lines: match self.matches.contains_id("diff") && self.matches.get_flag("diff") {
                 #[cfg(feature = "git")]
                 true => VisibleLines::DiffContext(
                     self.matches
-                        .value_of("diff-context")
+                        .get_one::<String>("diff-context")
                         .and_then(|t| t.parse().ok())
                         .unwrap_or(2),
                 ),
 
                 _ => VisibleLines::Ranges(
                     self.matches
-                        .values_of("line-range")
-                        .map(|vs| vs.map(LineRange::from).collect())
+                        .get_many::<String>("line-range")
+                        .map(|vs| vs.map(|s| LineRange::from(s.as_str())).collect())
                         .transpose()?
                         .map(LineRanges::from)
                         .unwrap_or_default(),
@@ -230,45 +241,47 @@ impl App {
             },
             style_components,
             syntax_mapping,
-            pager: self.matches.value_of("pager"),
-            use_italic_text: self.matches.value_of("italic-text") == Some("always"),
+            pager: self.matches.get_one::<String>("pager").map(|s| s.as_str()),
+            use_italic_text: self
+                .matches
+                .get_one::<String>("italic-text")
+                .map(|s| s.as_str())
+                == Some("always"),
             highlighted_lines: self
                 .matches
-                .values_of("highlight-line")
-                .map(|ws| ws.map(LineRange::from).collect())
+                .get_many::<String>("highlight-line")
+                .map(|ws| ws.map(|s| LineRange::from(s.as_str())).collect())
                 .transpose()?
                 .map(LineRanges::from)
                 .map(HighlightedLineRanges)
                 .unwrap_or_default(),
-            use_custom_assets: !self.matches.is_present("no-custom-assets"),
+            use_custom_assets: !self.matches.get_flag("no-custom-assets"),
         })
     }
 
     pub fn inputs(&self) -> Result<Vec<Input>> {
-        // verify equal length of file-names and input FILEs
-        match self.matches.values_of("file-name") {
-            Some(ref filenames)
-                if self.matches.values_of_os("FILE").is_some()
-                    && filenames.len() != self.matches.values_of_os("FILE").unwrap().len() =>
-            {
-                return Err("Must be one file name per input type.".into());
-            }
-            _ => {}
-        }
         let filenames: Option<Vec<&Path>> = self
             .matches
-            .values_of_os("file-name")
-            .map(|values| values.map(Path::new).collect());
+            .get_many::<PathBuf>("file-name")
+            .map(|vs| vs.map(|p| p.as_path()).collect::<Vec<_>>());
+
+        let files: Option<Vec<&Path>> = self
+            .matches
+            .get_many::<PathBuf>("FILE")
+            .map(|vs| vs.map(|p| p.as_path()).collect::<Vec<_>>());
+
+        // verify equal length of file-names and input FILEs
+        if filenames.is_some()
+            && files.is_some()
+            && filenames.as_ref().map(|v| v.len()) != files.as_ref().map(|v| v.len())
+        {
+            return Err("Must be one file name per input type.".into());
+        }
 
         let mut filenames_or_none: Box<dyn Iterator<Item = Option<&Path>>> = match filenames {
             Some(filenames) => Box::new(filenames.into_iter().map(Some)),
             None => Box::new(std::iter::repeat(None)),
         };
-        let files: Option<Vec<&Path>> = self
-            .matches
-            .values_of_os("FILE")
-            .map(|vs| vs.map(Path::new).collect());
-
         if files.is_none() {
             return Ok(vec![new_stdin_input(
                 filenames_or_none.next().unwrap_or(None),
@@ -294,12 +307,12 @@ impl App {
 
     fn style_components(&self) -> Result<StyleComponents> {
         let matches = &self.matches;
-        let mut styled_components =
-            StyleComponents(if matches.value_of("decorations") == Some("never") {
+        let mut styled_components = StyleComponents(
+            if matches.get_one::<String>("decorations").map(|s| s.as_str()) == Some("never") {
                 HashSet::new()
-            } else if matches.is_present("number") {
+            } else if matches.get_flag("number") {
                 [StyleComponent::LineNumbers].iter().cloned().collect()
-            } else if matches.is_present("plain") {
+            } else if 0 < matches.get_count("plain") {
                 [StyleComponent::Plain].iter().cloned().collect()
             } else {
                 let env_style_components: Option<Vec<StyleComponent>> = env::var("BAT_STYLE")
@@ -313,7 +326,7 @@ impl App {
                     .transpose()?;
 
                 matches
-                    .value_of("style")
+                    .get_one::<String>("style")
                     .map(|styles| {
                         styles
                             .split(',')
@@ -329,7 +342,8 @@ impl App {
                         acc.extend(components.iter().cloned());
                         acc
                     })
-            });
+            },
+        );
 
         // If `grid` is set, remove `rule` as it is a subset of `grid`, and print a warning.
         if styled_components.grid() && styled_components.0.remove(&StyleComponent::Rule) {
diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs
index 9e6f4a035f..f96e9e9842 100644
--- a/src/bin/bat/clap_app.rs
+++ b/src/bin/bat/clap_app.rs
@@ -1,7 +1,10 @@
-use clap::{crate_name, crate_version, App as ClapApp, AppSettings, Arg, ArgGroup, SubCommand};
+use clap::{
+    crate_name, crate_version, value_parser, AppSettings, Arg, ArgAction, ArgGroup, ColorChoice,
+    Command,
+};
 use once_cell::sync::Lazy;
 use std::env;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 
 static VERSION: Lazy<String> = Lazy::new(|| {
     #[cfg(feature = "bugreport")]
@@ -16,23 +19,21 @@ static VERSION: Lazy<String> = Lazy::new(|| {
     }
 });
 
-pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
-    let clap_color_setting = if interactive_output && env::var_os("NO_COLOR").is_none() {
-        AppSettings::ColoredHelp
+pub fn build_app(interactive_output: bool) -> Command<'static> {
+    let color_when = if interactive_output && env::var_os("NO_COLOR").is_none() {
+        ColorChoice::Auto
     } else {
-        AppSettings::ColorNever
+        ColorChoice::Never
     };
 
-    let mut app = ClapApp::new(crate_name!())
+    let mut app = Command::new(crate_name!())
         .version(VERSION.as_str())
-        .global_setting(clap_color_setting)
+        .color(color_when)
         .global_setting(AppSettings::DeriveDisplayOrder)
-        .global_setting(AppSettings::UnifiedHelpMessage)
-        .global_setting(AppSettings::HidePossibleValuesInHelp)
-        .setting(AppSettings::ArgsNegateSubcommands)
-        .setting(AppSettings::AllowExternalSubcommands)
-        .setting(AppSettings::DisableHelpSubcommand)
-        .setting(AppSettings::VersionlessSubcommands)
+        .hide_possible_values(true)
+        .args_conflicts_with_subcommands(true)
+        .allow_external_subcommands(true)
+        .disable_help_subcommand(true)
         .max_term_width(100)
         .about(
             "A cat(1) clone with wings.\n\n\
@@ -44,20 +45,22 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
         )
         .long_about("A cat(1) clone with syntax highlighting and Git integration.")
         .arg(
-            Arg::with_name("FILE")
+            Arg::new("FILE")
                 .help("File(s) to print / concatenate. Use '-' for standard input.")
                 .long_help(
                     "File(s) to print / concatenate. Use a dash ('-') or no argument at all \
                      to read from standard input.",
                 )
-                .multiple(true)
-                .empty_values(false),
+                .takes_value(true)
+                .multiple_values(true)
+                .value_parser(value_parser!(PathBuf)),
         )
         .arg(
-            Arg::with_name("show-all")
+            Arg::new("show-all")
                 .long("show-all")
                 .alias("show-nonprintable")
-                .short("A")
+                .short('A')
+                .action(ArgAction::SetTrue)
                 .conflicts_with("language")
                 .help("Show non-printable characters (space, tab, newline, ..).")
                 .long_help(
@@ -67,12 +70,12 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("plain")
+            Arg::new("plain")
                 .overrides_with("plain")
                 .overrides_with("number")
-                .short("p")
+                .short('p')
                 .long("plain")
-                .multiple(true)
+                .action(ArgAction::Count)
                 .help("Show plain style (alias for '--style=plain').")
                 .long_help(
                     "Only show plain style, no decorations. This is an alias for \
@@ -81,8 +84,8 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("language")
-                .short("l")
+            Arg::new("language")
+                .short('l')
                 .long("language")
                 .overrides_with("language")
                 .help("Set the language for syntax highlighting.")
@@ -95,12 +98,11 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 .takes_value(true),
         )
         .arg(
-            Arg::with_name("highlight-line")
+            Arg::new("highlight-line")
                 .long("highlight-line")
-                .short("H")
+                .short('H')
                 .takes_value(true)
-                .number_of_values(1)
-                .multiple(true)
+                .action(ArgAction::Append)
                 .value_name("N:M")
                 .help("Highlight lines N through M.")
                 .long_help(
@@ -114,12 +116,12 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("file-name")
+            Arg::new("file-name")
                 .long("file-name")
                 .takes_value(true)
-                .number_of_values(1)
-                .multiple(true)
+                .action(ArgAction::Append)
                 .value_name("name")
+                .value_parser(value_parser!(PathBuf))
                 .help("Specify the name to display for a file.")
                 .long_help(
                     "Specify the name to display for a file. Useful when piping \
@@ -133,9 +135,11 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
     {
         app = app
                 .arg(
-                    Arg::with_name("diff")
+                    Arg::new("diff")
                         .long("diff")
-                        .short("d")
+                        .short('d')
+                        .action(ArgAction::SetTrue)
+                        .conflicts_with("line-range")
                         .help("Only show lines that have been added/removed/modified.")
                         .long_help(
                             "Only show lines that have been added/removed/modified with respect \
@@ -143,20 +147,20 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                         ),
                 )
                 .arg(
-                    Arg::with_name("diff-context")
+                    Arg::new("diff-context")
                         .long("diff-context")
                         .overrides_with("diff-context")
                         .takes_value(true)
                         .value_name("N")
-                        .validator(
-                            |n| {
+                        .value_parser(
+                            |n: &str| {
                                 n.parse::<usize>()
                                     .map_err(|_| "must be a number")
-                                    .map(|_| ()) // Convert to Result<(), &str>
+                                    .map(|_| n.to_owned()) // Convert to Result<String, &str>
                                     .map_err(|e| e.to_string())
                             }, // Convert to Result<(), String>
                         )
-                        .hidden_short_help(true)
+                        .hide_short_help(true)
                         .long_help(
                             "Include N lines of context around added/removed/modified lines when using '--diff'.",
                         ),
@@ -164,16 +168,16 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
     }
 
     app = app.arg(
-        Arg::with_name("tabs")
+        Arg::new("tabs")
             .long("tabs")
             .overrides_with("tabs")
             .takes_value(true)
             .value_name("T")
-            .validator(
-                |t| {
+            .value_parser(
+                |t: &str| {
                     t.parse::<u32>()
                         .map_err(|_t| "must be a number")
-                        .map(|_t| ()) // Convert to Result<(), &str>
+                        .map(|_t| t.to_owned()) // Convert to Result<String, &str>
                         .map_err(|e| e.to_string())
                 }, // Convert to Result<(), String>
             )
@@ -184,12 +188,12 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
             ),
     )
         .arg(
-            Arg::with_name("wrap")
+            Arg::new("wrap")
                 .long("wrap")
                 .overrides_with("wrap")
                 .takes_value(true)
                 .value_name("mode")
-                .possible_values(&["auto", "never", "character"])
+                .value_parser(["auto", "never", "character"])
                 .default_value("auto")
                 .hide_default_value(true)
                 .help("Specify the text-wrapping mode (*auto*, never, character).")
@@ -198,21 +202,21 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                            control the output width."),
         )
         .arg(
-            Arg::with_name("terminal-width")
+            Arg::new("terminal-width")
                 .long("terminal-width")
                 .takes_value(true)
                 .value_name("width")
-                .hidden_short_help(true)
+                .hide_short_help(true)
                 .allow_hyphen_values(true)
-                .validator(
-                    |t| {
+                .value_parser(
+                    |t: &str| {
                         let is_offset = t.starts_with('+') || t.starts_with('-');
                         t.parse::<i32>()
                             .map_err(|_e| "must be an offset or number")
                             .and_then(|v| if v == 0 && !is_offset {
                                 Err("terminal width cannot be zero")
                             } else {
-                                Ok(())
+                                Ok(t.to_owned())
                             })
                             .map_err(|e| e.to_string())
                     })
@@ -223,10 +227,11 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("number")
+            Arg::new("number")
                 .long("number")
                 .overrides_with("number")
-                .short("n")
+                .short('n')
+                .action(ArgAction::SetTrue)
                 .help("Show line numbers (alias for '--style=numbers').")
                 .long_help(
                     "Only show line numbers, no other decorations. This is an alias for \
@@ -234,12 +239,12 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("color")
+            Arg::new("color")
                 .long("color")
                 .overrides_with("color")
                 .takes_value(true)
                 .value_name("when")
-                .possible_values(&["auto", "never", "always"])
+                .value_parser(["auto", "never", "always"])
                 .hide_default_value(true)
                 .default_value("auto")
                 .help("When to use colors (*auto*, never, always).")
@@ -251,23 +256,23 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("italic-text")
+            Arg::new("italic-text")
                 .long("italic-text")
                 .takes_value(true)
                 .value_name("when")
-                .possible_values(&["always", "never"])
+                .value_parser(["always", "never"])
                 .default_value("never")
                 .hide_default_value(true)
                 .help("Use italics in output (always, *never*)")
                 .long_help("Specify when to use ANSI sequences for italic text in the output. Possible values: always, *never*."),
         )
         .arg(
-            Arg::with_name("decorations")
+            Arg::new("decorations")
                 .long("decorations")
                 .overrides_with("decorations")
                 .takes_value(true)
                 .value_name("when")
-                .possible_values(&["auto", "never", "always"])
+                .value_parser(["auto", "never", "always"])
                 .default_value("auto")
                 .hide_default_value(true)
                 .help("When to show the decorations (*auto*, never, always).")
@@ -278,25 +283,26 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("force-colorization")
+            Arg::new("force-colorization")
                 .long("force-colorization")
-                .short("f")
+                .short('f')
+                .action(ArgAction::SetTrue)
                 .conflicts_with("color")
                 .conflicts_with("decorations")
                 .overrides_with("force-colorization")
-                .hidden_short_help(true)
+                .hide_short_help(true)
                 .long_help("Alias for '--decorations=always --color=always'. This is useful \
                         if the output of bat is piped to another program, but you want \
                         to keep the colorization/decorations.")
         )
         .arg(
-            Arg::with_name("paging")
+            Arg::new("paging")
                 .long("paging")
                 .overrides_with("paging")
                 .overrides_with("no-paging")
                 .takes_value(true)
                 .value_name("when")
-                .possible_values(&["auto", "never", "always"])
+                .value_parser(["auto", "never", "always"])
                 .default_value("auto")
                 .hide_default_value(true)
                 .help("Specify when to use the pager, or use `-P` to disable (*auto*, never, always).")
@@ -308,22 +314,23 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("no-paging")
-                .short("P")
+            Arg::new("no-paging")
+                .short('P')
                 .long("no-paging")
                 .alias("no-pager")
+                .action(ArgAction::SetTrue)
                 .overrides_with("no-paging")
-                .hidden(true)
-                .hidden_short_help(true)
+                .hide(true)
+                .hide_short_help(true)
                 .help("Alias for '--paging=never'")
             )
         .arg(
-            Arg::with_name("pager")
+            Arg::new("pager")
                 .long("pager")
                 .overrides_with("pager")
                 .takes_value(true)
                 .value_name("command")
-                .hidden_short_help(true)
+                .hide_short_help(true)
                 .help("Determine which pager to use.")
                 .long_help(
                     "Determine which pager is used. This option will override the \
@@ -333,12 +340,11 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("map-syntax")
-                .short("m")
+            Arg::new("map-syntax")
+                .short('m')
                 .long("map-syntax")
-                .multiple(true)
+                .action(ArgAction::Append)
                 .takes_value(true)
-                .number_of_values(1)
                 .value_name("glob:syntax")
                 .help("Use the specified syntax for files matching the glob pattern ('*.cpp:C++').")
                 .long_help(
@@ -351,19 +357,18 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 .takes_value(true),
         )
         .arg(
-            Arg::with_name("ignored-suffix")
-                .number_of_values(1)
-                .multiple(true)
+            Arg::new("ignored-suffix")
+                .action(ArgAction::Append)
                 .takes_value(true)
                 .long("ignored-suffix")
-                .hidden_short_help(true)
+                .hide_short_help(true)
                 .help(
                     "Ignore extension. For example:\n  \
                     'bat --ignored-suffix \".dev\" my_file.json.dev' will use JSON syntax, and ignore '.dev'"
                 )
         )
         .arg(
-            Arg::with_name("theme")
+            Arg::new("theme")
                 .long("theme")
                 .overrides_with("theme")
                 .takes_value(true)
@@ -377,24 +382,25 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("list-themes")
+            Arg::new("list-themes")
                 .long("list-themes")
+                .action(ArgAction::SetTrue)
                 .help("Display all supported highlighting themes.")
                 .long_help("Display a list of supported themes for syntax highlighting."),
         )
         .arg(
-            Arg::with_name("style")
+            Arg::new("style")
                 .long("style")
                 .value_name("components")
                 // Need to turn this off for overrides_with to work as we want. See the bottom most
                 // example at https://docs.rs/clap/2.32.0/clap/struct.Arg.html#method.overrides_with
-                .use_delimiter(false)
+                .use_value_delimiter(false)
                 .takes_value(true)
                 .overrides_with("style")
                 .overrides_with("plain")
                 .overrides_with("number")
                 // Cannot use claps built in validation because we have to turn off clap's delimiters
-                .validator(|val| {
+                .value_parser(|val: &str| {
                     let mut invalid_vals = val.split(',').filter(|style| {
                         !&[
                             "auto",
@@ -416,7 +422,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                     if let Some(invalid) = invalid_vals.next() {
                         Err(format!("Unknown style, '{}'", invalid))
                     } else {
-                        Ok(())
+                        Ok(val.to_owned())
                     }
                 })
                 .help(
@@ -448,14 +454,12 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("line-range")
+            Arg::new("line-range")
                 .long("line-range")
-                .short("r")
-                .multiple(true)
+                .short('r')
+                .action(ArgAction::Append)
                 .takes_value(true)
-                .number_of_values(1)
                 .value_name("N:M")
-                .conflicts_with("diff")
                 .help("Only print the lines from N to M.")
                 .long_help(
                     "Only print the specified range of lines for each file. \
@@ -468,18 +472,19 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("list-languages")
+            Arg::new("list-languages")
                 .long("list-languages")
-                .short("L")
+                .short('L')
+                .action(ArgAction::SetTrue)
                 .conflicts_with("list-themes")
                 .help("Display all supported languages.")
                 .long_help("Display a list of supported languages for syntax highlighting."),
         )
         .arg(
-            Arg::with_name("unbuffered")
-                .short("u")
+            Arg::new("unbuffered")
+                .short('u')
                 .long("unbuffered")
-                .hidden_short_help(true)
+                .hide_short_help(true)
                 .long_help(
                     "This option exists for POSIX-compliance reasons ('u' is for \
                      'unbuffered'). The output is always unbuffered - this option \
@@ -487,60 +492,66 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                 ),
         )
         .arg(
-            Arg::with_name("no-config")
+            Arg::new("no-config")
                 .long("no-config")
-                .hidden(true)
+                .hide(true)
                 .help("Do not use the configuration file"),
         )
         .arg(
-            Arg::with_name("no-custom-assets")
+            Arg::new("no-custom-assets")
                 .long("no-custom-assets")
-                .hidden(true)
+                .action(ArgAction::SetTrue)
+                .hide(true)
                 .help("Do not load custom assets"),
         )
         .arg(
-            Arg::with_name("config-file")
+            Arg::new("config-file")
                 .long("config-file")
+                .action(ArgAction::SetTrue)
                 .conflicts_with("list-languages")
                 .conflicts_with("list-themes")
-                .hidden(true)
+                .hide(true)
                 .help("Show path to the configuration file."),
         )
         .arg(
-            Arg::with_name("generate-config-file")
+            Arg::new("generate-config-file")
                 .long("generate-config-file")
+                .action(ArgAction::SetTrue)
                 .conflicts_with("list-languages")
                 .conflicts_with("list-themes")
-                .hidden(true)
+                .hide(true)
                 .help("Generates a default configuration file."),
         )
         .arg(
-            Arg::with_name("config-dir")
+            Arg::new("config-dir")
                 .long("config-dir")
-                .hidden(true)
+                .action(ArgAction::SetTrue)
+                .hide(true)
                 .help("Show bat's configuration directory."),
         )
         .arg(
-            Arg::with_name("cache-dir")
+            Arg::new("cache-dir")
                 .long("cache-dir")
-                .hidden(true)
+                .action(ArgAction::SetTrue)
+                .hide(true)
                 .help("Show bat's cache directory."),
         )
         .arg(
-            Arg::with_name("diagnostic")
+            Arg::new("diagnostic")
                 .long("diagnostic")
                 .alias("diagnostics")
-                .hidden_short_help(true)
+                .action(ArgAction::SetTrue)
+                .hide_short_help(true)
                 .help("Show diagnostic information for bug reports.")
         )
         .arg(
-            Arg::with_name("acknowledgements")
+            Arg::new("acknowledgements")
                 .long("acknowledgements")
-                .hidden_short_help(true)
+                .action(ArgAction::SetTrue)
+                .hide_short_help(true)
                 .help("Show acknowledgements."),
         )
-        .help_message("Print this help message.")
-        .version_message("Show version information.");
+        .mut_arg("help", |arg| arg.help("Print this help message."));
 
     // Check if the current directory contains a file name cache. Otherwise,
     // enable the 'bat cache' subcommand.
@@ -548,12 +559,13 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
         app
     } else {
         app.subcommand(
-            SubCommand::with_name("cache")
+            Command::new("cache")
                 .about("Modify the syntax-definition and theme cache")
                 .arg(
-                    Arg::with_name("build")
+                    Arg::new("build")
                         .long("build")
-                        .short("b")
+                        .short('b')
+                        .action(ArgAction::SetTrue)
                         .help("Initialize (or update) the syntax/theme cache.")
                         .long_help(
                             "Initialize (or update) the syntax/theme cache by loading from \
@@ -561,18 +573,19 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                         ),
                 )
                 .arg(
-                    Arg::with_name("clear")
+                    Arg::new("clear")
                         .long("clear")
-                        .short("c")
+                        .short('c')
+                        .action(ArgAction::SetTrue)
                         .help("Remove the cached syntax definitions and themes."),
                 )
                 .group(
-                    ArgGroup::with_name("cache-actions")
+                    ArgGroup::new("cache-actions")
                         .args(&["build", "clear"])
                         .required(true),
                 )
                 .arg(
-                    Arg::with_name("source")
+                    Arg::new("source")
                         .long("source")
                         .requires("build")
                         .takes_value(true)
@@ -580,7 +593,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                         .help("Use a different directory to load syntaxes and themes from."),
                 )
                 .arg(
-                    Arg::with_name("target")
+                    Arg::new("target")
                         .long("target")
                         .requires("build")
                         .takes_value(true)
@@ -590,8 +603,9 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                         ),
                 )
                 .arg(
-                    Arg::with_name("blank")
+                    Arg::new("blank")
                         .long("blank")
+                        .action(ArgAction::SetTrue)
                         .requires("build")
                         .help(
                             "Create completely new syntax and theme sets \
@@ -599,11 +613,17 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
                         ),
                 )
                 .arg(
-                    Arg::with_name("acknowledgements")
+                    Arg::new("acknowledgements")
                         .long("acknowledgements")
+                        .action(ArgAction::SetTrue)
                         .requires("build")
                         .help("Build acknowledgements.bin."),
                 ),
         )
     }
 }
+
+#[test]
+fn verify_app() {
+    build_app(false).debug_assert();
+}
diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs
index 6b2daa2b84..a6424e401b 100644
--- a/src/bin/bat/main.rs
+++ b/src/bin/bat/main.rs
@@ -39,30 +39,30 @@ const THEME_PREVIEW_DATA: &[u8] = include_bytes!("../../../assets/theme_preview.
 #[cfg(feature = "build-assets")]
 fn build_assets(matches: &clap::ArgMatches) -> Result<()> {
     let source_dir = matches
-        .value_of("source")
+        .get_one::<String>("source")
         .map(Path::new)
         .unwrap_or_else(|| PROJECT_DIRS.config_dir());
     let target_dir = matches
-        .value_of("target")
+        .get_one::<String>("target")
         .map(Path::new)
         .unwrap_or_else(|| PROJECT_DIRS.cache_dir());
 
     bat::assets::build(
         source_dir,
-        !matches.is_present("blank"),
-        matches.is_present("acknowledgements"),
+        !matches.get_flag("blank"),
+        matches.get_flag("acknowledgements"),
         target_dir,
         clap::crate_version!(),
     )
 }
 
 fn run_cache_subcommand(matches: &clap::ArgMatches) -> Result<()> {
-    if matches.is_present("build") {
+    if matches.get_flag("build") {
         #[cfg(feature = "build-assets")]
         build_assets(matches)?;
         #[cfg(not(feature = "build-assets"))]
         println!("bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.");
-    } else if matches.is_present("clear") {
+    } else if matches.get_flag("clear") {
         clear_assets();
     }
 
@@ -227,8 +227,10 @@ fn run_controller(inputs: Vec<Input>, config: &Config) -> Result<bool> {
 #[cfg(feature = "bugreport")]
 fn invoke_bugreport(app: &App) {
     use bugreport::{bugreport, collector::*, format::Markdown};
-    let pager = bat::config::get_pager_executable(app.matches.value_of("pager"))
-        .unwrap_or_else(|| "less".to_owned()); // FIXME: Avoid non-canonical path to "less".
+    let pager = bat::config::get_pager_executable(
+        app.matches.get_one::<String>("pager").map(|s| s.as_str()),
+    )
+    .unwrap_or_else(|| "less".to_owned()); // FIXME: Avoid non-canonical path to "less".
 
     let mut custom_assets_metadata = PROJECT_DIRS.cache_dir().to_path_buf();
     custom_assets_metadata.push("metadata.yaml");
@@ -284,7 +286,7 @@ fn invoke_bugreport(app: &App) {
 fn run() -> Result<bool> {
     let app = App::new()?;
 
-    if app.matches.is_present("diagnostic") {
+    if app.matches.get_flag("diagnostic") {
         #[cfg(feature = "bugreport")]
         invoke_bugreport(&app);
         #[cfg(not(feature = "bugreport"))]
@@ -293,11 +295,11 @@ fn run() -> Result<bool> {
     }
 
     match app.matches.subcommand() {
-        ("cache", Some(cache_matches)) => {
+        Some(("cache", cache_matches)) => {
             // If there is a file named 'cache' in the current working directory,
             // arguments for subcommand 'cache' are not mandatory.
             // If there are non-zero arguments, execute the subcommand cache, else, open the file cache.
-            if !cache_matches.args.is_empty() {
+            if cache_matches.args_present() {
                 run_cache_subcommand(cache_matches)?;
                 Ok(true)
             } else {
@@ -311,7 +313,7 @@ fn run() -> Result<bool> {
             let inputs = app.inputs()?;
             let config = app.config(&inputs)?;
 
-            if app.matches.is_present("list-languages") {
+            if app.matches.get_flag("list-languages") {
                 let languages: String = get_languages(&config)?;
                 let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))];
                 let plain_config = Config {
@@ -320,22 +322,22 @@ fn run() -> Result<bool> {
                     ..Default::default()
                 };
                 run_controller(inputs, &plain_config)
-            } else if app.matches.is_present("list-themes") {
+            } else if app.matches.get_flag("list-themes") {
                 list_themes(&config)?;
                 Ok(true)
-            } else if app.matches.is_present("config-file") {
+            } else if app.matches.get_flag("config-file") {
                 println!("{}", config_file().to_string_lossy());
                 Ok(true)
-            } else if app.matches.is_present("generate-config-file") {
+            } else if app.matches.get_flag("generate-config-file") {
                 generate_config_file()?;
                 Ok(true)
-            } else if app.matches.is_present("config-dir") {
+            } else if app.matches.get_flag("config-dir") {
                 writeln!(io::stdout(), "{}", config_dir())?;
                 Ok(true)
-            } else if app.matches.is_present("cache-dir") {
+            } else if app.matches.get_flag("cache-dir") {
                 writeln!(io::stdout(), "{}", cache_dir())?;
                 Ok(true)
-            } else if app.matches.is_present("acknowledgements") {
+            } else if app.matches.get_flag("acknowledgements") {
                 writeln!(io::stdout(), "{}", bat::assets::get_acknowledgements())?;
                 Ok(true)
             } else {


Test patch: