Repository: xd009642/tarpaulin
Pull request #1359 (2023-08-26 15:37:13)

Migrate clap to version 4 with derives
<!--- When contributing to tarpaulin all contributions should branch off the -->
<!--- develop branch as master is only used for releases. --> 

<!--- Check CONTRIBUTING.md for more information-->

<!--- Please describe the changes in the PR and them motivation below -->

# Motivation:

Current app uses an old version of `clap` crate with the builder convention, which adds a lot of additional runtime uncertainty and makes it difficult to separate the arguments into multiple apps/subcommands. Using the currently available derive convention will make it easier to group arguments together and make use of compile time checks.

# Changes:

- Update the `clap` crate to v4
- Migrate arguments to structs with derives

MiniaczQ: I think this is the least changes I could possibly do.
On the way I ran into some questionable code, but I decided to keep it as to not make it harder to review this already monstrous PR.
MiniaczQ: One test does not pass after the refactor: `exclude_paths_directory_separators`.
It uses what I think is an invalid convention:
```rs
let args = TarpaulinCli::parse_from(vec![
            "tarpaulin",
            "--exclude-files",
            "src/foo/*",
            // No `--exclude-files` before the next argument
            "src\\bar\\*",
        ]);
```
MiniaczQ: I wanna just ignore it and add the missing `--exclude-files`, what do you think?
xd009642: in clap 3 I think `multiple_values=true` worked for getting the same behaviour - maybe it exists in clap 4? Just on my way out so I'll try and think about it more and reply tomorrow or monday but I remember personally finding doing `--exclude-files` multiple times a bit annoying but if it's a convention I suppose I could shift to it
MiniaczQ: I'll check again, it could be one of those builder-only things
MiniaczQ: Yeah, so it exists in clap 3 and disappears in clap 4, either they renamed it or it's totally gone
MiniaczQ: I found it, extremely obscure and undocumented, even in the source code I couldn't find it:
https://github.com/clap-rs/clap/discussions/4998#discussioncomment-6681330

I'll patch it in a second, just wanna write an issue on `clap` for this
MiniaczQ: The next step would be to separate arguments for building tests, running tests and processing results.
I think we can also perform this separation on `Config` class with the `#[serde(flatten)]` without breaking compatibility yet.
MiniaczQ: Oh yeah, about the convention of multiple values per flag,
https://docs.rs/clap/latest/clap/struct.Arg.html#method.allow_hyphen_values
describes how this option can lead to problems, I guess that's why it's rarely done that way
MiniaczQ: Should I work on the next changes in this PR or a new one?
xd009642: A new one please, I'm trying to find some time to review this one as it is (part way through it). And when that's done I'll merge it in. More changes come in it'll slow down the review a bit :sweat_smile: 
xd009642: there are some conflicts to resolve though - before merge
xd009642: Getting closer, it actually shows some printout from tarpaulin not cargo

```
cargo tarpaulin --help
error: unexpected argument 'tarpaulin' found

Usage: cargo-tarpaulin [OPTIONS] [-- <ARGS>...]

For more information, try '--help'.
```
MiniaczQ: I tried a few thing but nothing worked like it should so I posted a question on clap's discussion forum
https://github.com/clap-rs/clap/discussions/5107

hopefully I missed something
MiniaczQ: Very quick response from the clap team, there are 2 working solutions, I went with the less bloaty one, felt more in line with what I originally tried.
xd009642: LGTM thanks for the hard work! Once CI passes I'll merge this in :rocket: 

Patch:
diff --git a/.gitignore b/.gitignore
index 82553c0..f4874f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,6 @@
 .idea
 tarpaulin*.json
 tarpaulin-report.html
+# Generated by `cargo test`
+cobertura.xml
+lcov.info
diff --git a/Cargo.lock b/Cargo.lock
index 3c7f6b7..b2470b5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -48,22 +48,59 @@ dependencies = [
 ]
 
 [[package]]
-name = "anyhow"
-version = "1.0.65"
+name = "anstream"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
 
 [[package]]
-name = "atty"
-version = "0.2.14"
+name = "anstyle"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
 dependencies = [
- "hermit-abi 0.1.19",
- "libc",
- "winapi",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -121,6 +158,7 @@ dependencies = [
  "chrono",
  "clap",
  "coveralls-api",
+ "enum-display",
  "fallible-iterator",
  "gimli",
  "git2",
@@ -197,17 +235,58 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "2.34.0"
+version = "4.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+checksum = "1d5f1946157a96594eb2d2c10eb7ad9a2b27518cb3000209dec700c35df9197d"
 dependencies = [
- "ansi_term",
- "atty",
- "bitflags 1.3.2",
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
  "strsim",
- "textwrap",
- "unicode-width",
- "vec_map",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.28",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
 ]
 
 [[package]]
@@ -280,6 +359,26 @@ dependencies = [
  "gzip-header",
 ]
 
+[[package]]
+name = "enum-display"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d4df33d54dd1959d177a0e2c2f4e5a8637a3054aa56861ed7e173ad2043fe2"
+dependencies = [
+ "enum-display-macro",
+]
+
+[[package]]
+name = "enum-display-macro"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0ce3a36047ede676eb0d2721d065beed8410cf4f113f489604d2971331cb378"
+dependencies = [
+ "convert_case",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "equivalent"
 version = "1.0.0"
@@ -402,13 +501,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
 
 [[package]]
-name = "hermit-abi"
-version = "0.1.19"
+name = "heck"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
-dependencies = [
- "libc",
-]
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
 name = "hermit-abi"
@@ -703,7 +799,7 @@ version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
 dependencies = [
- "hermit-abi 0.3.1",
+ "hermit-abi",
  "libc",
 ]
 
@@ -1056,9 +1152,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[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"
@@ -1096,15 +1192,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "textwrap"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
-
 [[package]]
 name = "thiserror"
 version = "1.0.32"
@@ -1308,10 +1395,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "unicode-width"
-version = "0.1.9"
+name = "unicode-segmentation"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
 
 [[package]]
 name = "url"
@@ -1325,6 +1412,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
 [[package]]
 name = "valuable"
 version = "0.1.0"
@@ -1337,12 +1430,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 = "wait-timeout"
 version = "0.2.0"
@@ -1481,6 +1568,15 @@ dependencies = [
  "windows_x86_64_msvc 0.42.0",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.48.5"
diff --git a/Cargo.toml b/Cargo.toml
index 6717bc2..f7aeaa8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,7 @@ path = "tests/mod.rs"
 cargo_metadata = "0.17"
 cfg-if = "1.0.0"
 chrono = "0.4"
-clap = "2.34.0"
+clap = { version = "4.4.0", features = ["derive"] }
 coveralls-api = "0.5.0"
 fallible-iterator = "0.3.0"
 gimli = "0.28.0"
@@ -44,9 +44,17 @@ serde_json = "1.0"
 syn = { version = "1.0", features = ["full"] }
 toml = "0.7"
 tracing = { version = "0.1", default-features = false }
-tracing-subscriber = { version = "0.2.25", default-features = false, features = ["env-filter", "fmt", "chrono", "ansi", "smallvec", "tracing-log"] }
+tracing-subscriber = { version = "0.2.25", default-features = false, features = [
+    "env-filter",
+    "fmt",
+    "chrono",
+    "ansi",
+    "smallvec",
+    "tracing-log",
+] }
 walkdir = "2.3.3"
 glob = "0.3.1"
+enum-display = "0.1.3"
 
 [target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
 libc = "0.2.94"
diff --git a/README.md b/README.md
index fb39e5a..f4a0a7d 100644
--- a/README.md
+++ b/README.md
@@ -30,101 +30,83 @@ available:
 
 ```
 cargo-tarpaulin version: 0.26.1
-Tool to analyse test coverage of cargo projects
-
-USAGE:
-    cargo tarpaulin [FLAGS] [OPTIONS] [-- <args>...]
-
-FLAGS:
-        --all                      Alias for --workspace (deprecated)
-        --all-features             Build all available features
-        --all-targets              Test all targets (excluding doctests)
-        --avoid-cfg-tarpaulin      Remove --cfg=tarpaulin from the RUSTFLAG
-        --benches                  Test all benches
-        --bins                     Test all binaries
-    -b, --branch                   Branch coverage: NOT IMPLEMENTED
-        --count                    Counts the number of hits during coverage
-        --debug                    Show debug output - this is used for diagnosing issues with tarpaulin
-        --doc                      Test only this library's documentation
-        --dump-traces              Log tracing events and save to a json file. Also, enabled when --debug is used
-        --examples                 Test all examples
-        --follow-exec              Follow executed processes capturing coverage information if they're part of your
-                                   project.
-        --force-clean              Adds a clean stage to work around cargo bugs that may affect coverage results
-    -f, --forward                  Forwards unexpected signals to test. This is now the default behaviour
-        --frozen                   Do not update Cargo.lock or any caches
-    -h, --help                     Prints help information
-        --ignore-config            Ignore any project config files
-        --ignore-panics            Ignore panic macros in tests
-        --ignore-tests             Ignore lines of test functions when collecting coverage (default)
-    -i, --ignored                  Run ignored tests as well
-        --implicit-test-threads    Don't supply an explicit `--test-threads` argument to test executable. By default
-                                   tarpaulin will infer the default rustc would pick if not ran via tarpaulin and set it
-        --include-tests            Include lines of test functions when collecting coverage
-        --lib                      Test only this package's library unit tests
-    -l, --line                     Line coverage
-        --locked                   Do not update Cargo.lock
-        --no-dead-code             Stops tarpaulin from building projects with -Clink-dead-code
-        --no-default-features      Do not include default features
-        --no-fail-fast             Run all tests regardless of failure
-        --no-run                   Compile tests but don't run coverage
-        --offline                  Run without accessing the network
-        --print-rust-flags         Print the RUSTFLAGS options that tarpaulin will compile your program with and exit
-        --print-rustdoc-flags      Print the RUSTDOCFLAGS options that tarpaulin will compile any doctests with and exit
-        --release                  Build in release mode.
-        --skip-clean               The opposite of --force-clean
-        --tests                    Test all tests
-    -V, --version                  Prints version information
-    -v, --verbose                  Show extra output
-        --workspace                Test all packages in the workspace
-
-OPTIONS:
-    -Z <FEATURES>...                   List of unstable nightly only flags
-        --bench <NAME>...              Test only the specified bench target
-        --bin <NAME>...                Test only the specified binary
-        --ciserver <SERVICE>           Name of service, supported services are:
-                                       travis-ci, travis-pro, circle-ci, semaphore, jenkins and codeship.
-                                       If you are interfacing with coveralls.io or another site you can also specify a
-                                       name that they will recognise. Refer to their documentation for this.
-        --color <WHEN>                 Coloring: auto, always, never [possible values: Auto, Always, Never]
-        --command <CMD>                cargo subcommand to run. So far only test and build are supported [possible
-                                       values: Test, Build]
-        --config <FILE>                Path to a toml file specifying a list of options this will override any other
-                                       options set
-        --coveralls <KEY>              Coveralls key, either the repo token, or if you're using travis use
-                                       $TRAVIS_JOB_ID and specify travis-{ci|pro} in --ciserver
-        --engine <ENGINE>              Coverage tracing backend to use [possible values: Auto, Ptrace, Llvm]
-        --example <NAME>...            Test only the specified example
-    -e, --exclude <PACKAGE>...         Package id specifications to exclude from coverage. See cargo help pkgid for more
-                                       info
-        --exclude-files <FILE>...      Exclude given files from coverage results has * wildcard
-        --fail-under <PERCENTAGE>      Sets a percentage threshold for failure ranging from 0-100, if coverage is below
-                                       exit with a non-zero code
-        --features <FEATURES>...       Features to be included in the target project
-    -j, --jobs <N>                     Number of parallel jobs, defaults to # of CPUs
-        --manifest-path <PATH>         Path to Cargo.toml
-        --objects <objects>...         Other object files to load which contain information for llvm coverage - must
-                                       have been compiled with llvm coverage instrumentation (ignored for ptrace)
-    -o, --out <FMT>...                 Output format of coverage report [possible values: Json, Stdout, Xml, Html, Lcov]
-        --output-dir <PATH>            Specify a custom directory to write report files
-    -p, --packages <PACKAGE>...        Package id specifications for which package should be build. See cargo help pkgid
-                                       for more info
-        --post-test-delay <SECONDS>    Delay after test to collect coverage profiles
-        --profile <NAME>               Build artefacts with the specified profile
-        --report-uri <URI>             URI to send report to, only used if the option --coveralls is used
-    -r, --root <DIR>                   Calculates relative paths to root directory. If --manifest-path isn't specified
-                                       it will look for a Cargo.toml in root
-        --run-types <TYPE>...          Type of the coverage run [possible values: Tests, Doctests, Benchmarks, Examples,
-                                       Lib, Bins, AllTargets]
-        --rustflags <FLAGS>            rustflags to add when building project (can also be set via RUSTFLAGS env var)
-        --target <TRIPLE>              Compilation target triple
-        --target-dir <DIR>             Directory for all generated artifacts
-        --test <NAME>...               Test only the specified test target
-    -t, --timeout <SECONDS>            Integer for the maximum time in seconds without response from test before timeout
-                                       (default is 1 minute).
-
-ARGS:
-    <args>...    Arguments to be passed to the test executables can be used to filter or skip certain tests
+Cargo-Tarpaulin is a tool to determine code coverage achieved via tests
+
+Usage: cargo [OPTIONS] [-- <ARGS>...]
+
+Arguments:
+  [ARGS]...  Arguments to be passed to the test executables can be used to filter or skip certain tests
+
+Options:
+      --print-rust-flags           Print the RUSTFLAGS options that tarpaulin will compile your program with and exit
+      --print-rustdoc-flags        Print the RUSTDOCFLAGS options that tarpaulin will compile any doctests with and exit
+      --color <WHEN>               Coloring: auto, always, never [possible values: auto, always, never]
+      --debug                      Show debug output - this is used for diagnosing issues with tarpaulin
+      --verbose                    Show extra output
+      --dump-traces                Log tracing events and save to a json file. Also, enabled when --debug is used
+      --run-types <TYPE>           Type of the coverage run [possible values: tests, doctests, benchmarks, examples, lib, bins, all-targets]
+      --benches                    Test all benches
+      --doc                        Test only this library's documentation
+      --all-targets                Test all targets (excluding doctests)
+      --lib                        Test only this package's library unit tests
+      --bins                       Test all binaries
+      --examples                   Test all examples
+      --tests                      Test all tests
+      --config <FILE>              Path to a toml file specifying a list of options this will override any other options set
+      --ignore-config              Ignore any project config files
+      --bin [<NAME>...]            Test only the specified binary
+      --example [<NAME>...]        Test only the specified example
+      --test [<NAME>...]           Test only the specified test target
+      --bench [<NAME>...]          Test only the specified bench target
+      --no-fail-fast               Run all tests regardless of failure
+      --profile <NAME>             Build artefacts with the specified profile
+      --ignore-tests               Ignore lines of test functions when collecting coverage (default)
+      --no-dead-code               Stops tarpaulin from building projects with -Clink-dead-code
+      --include-tests              Include lines of test functions when collecting coverage
+      --ignore-panics              Ignore panic macros in tests
+      --count                      Counts the number of hits during coverage
+  -i, --ignored                    Run ignored tests as well
+  -l, --line                       Line coverage
+      --skip-clean                 The opposite of --force-clean
+      --force-clean                Adds a clean stage to work around cargo bugs that may affect coverage results
+      --fail-under <PERCENTAGE>    Sets a percentage threshold for failure ranging from 0-100, if coverage is below exit with a non-zero code
+  -b, --branch                     Branch coverage: NOT IMPLEMENTED
+  -f, --forward                    Forwards unexpected signals to test. This is now the default behaviour
+      --coveralls <KEY>            Coveralls key, either the repo token, or if you're using travis use $TRAVIS_JOB_ID and specify travis-{ci|pro} in --ciserver
+      --report-uri <URI>           URI to send report to, only used if the option --coveralls is used
+      --no-default-features        Do not include default features
+      --features [<FEATURES>...]   Features to be included in the target project
+      --all-features               Build all available features
+      --all                        Alias for --workspace (deprecated)
+      --workspace                  Test all packages in the workspace
+  -p, --packages [<PACKAGE>...]    Package id specifications for which package should be build. See cargo help pkgid for more info
+  -e, --exclude [<PACKAGE>...]     Package id specifications to exclude from coverage. See cargo help pkgid for more info
+      --exclude-files [<FILE>...]  Exclude given files from coverage results has * wildcard
+  -t, --timeout <SECONDS>          Integer for the maximum time in seconds without response from test before timeout (default is 1 minute)
+      --post-test-delay <SECONDS>  Delay after test to collect coverage profiles
+      --follow-exec                Follow executed processes capturing coverage information if they're part of your project
+      --release                    Build in release mode
+      --no-run                     Compile tests but don't run coverage
+      --implicit-test-threads      'Don't supply an explicit `--test-threads` argument to test executable. By default tarpaulin will infer the default rustc would pick if not ran via tarpaulin and set it
+      --locked                     Do not update Cargo.lock
+      --frozen                     Do not update Cargo.lock or any caches
+      --target <TRIPLE>            Compilation target triple
+      --target-dir <DIR>           Directory for all generated artifacts
+      --offline                    Run without accessing the network
+      --avoid-cfg-tarpaulin        Remove --cfg=tarpaulin from the RUSTFLAG
+  -j, --jobs <N>                   Number of parallel jobs, defaults to # of CPUs
+      --rustflags <FLAGS>          Rustflags to add when building project (can also be set via RUSTFLAGS env var)
+      --objects [<objects>...]     Other object files to load which contain information for llvm coverage - must have been compiled with llvm coverage instrumentation (ignored for ptrace)
+  -Z [<FEATURES>...]               List of unstable nightly only flags
+  -o, --out [<FMT>...]             Output format of coverage report [possible values: json, stdout, xml, html, lcov]
+      --engine <ENGINE>            Coverage tracing backend to use [possible values: auto, ptrace, llvm]
+      --output-dir <PATH>          Specify a custom directory to write report files
+      --command <CMD>              cargo subcommand to run. So far only test and build are supported [possible values: test, build]
+  -r, --root <DIR>                 Calculates relative paths to root directory. If --manifest-path isn't specified it will look for a Cargo.toml in root
+      --manifest-path <PATH>       Path to Cargo.toml
+      --ciserver <SERVICE>         CI server being used, if unspecified tarpaulin may automatically infer for coveralls uploads
+  -h, --help                       Print help
+  -V, --version                    Print version
 ```
 
 ### Note on tests using signals
diff --git a/src/args.rs b/src/args.rs
new file mode 100644
index 0000000..9c48607
--- /dev/null
+++ b/src/args.rs
@@ -0,0 +1,308 @@
+use std::path::PathBuf;
+
+use clap::{Args, Parser};
+use glob::Pattern;
+
+use crate::config::{Ci, Color, Mode, OutputFile, RunType, TraceEngine};
+
+#[derive(Debug, Parser)]
+#[command(name = "cargo-tarpaulin")]
+#[command(bin_name = "cargo")]
+#[command(author, version, about, long_about = None)]
+pub enum CargoTarpaulinCli {
+    Tarpaulin(TarpaulinCli),
+}
+
+impl CargoTarpaulinCli {
+    pub fn from_args() -> TarpaulinCli {
+        match TarpaulinCli::try_parse() {
+            Ok(tarpaulin_cli) => tarpaulin_cli,
+            Err(err) if !err.use_stderr() => err.exit(),
+            Err(_) => match CargoTarpaulinCli::parse() {
+                CargoTarpaulinCli::Tarpaulin(tarpaulin_cli) => tarpaulin_cli,
+            },
+        }
+    }
+}
+
+#[derive(Debug, Parser)]
+#[command(name = "tarpaulin")]
+#[command(author, version, about, long_about = None)]
+pub struct TarpaulinCli {
+    #[clap(flatten)]
+    pub print_flags: PrintFlagsArgs,
+    #[clap(flatten)]
+    pub config: ConfigArgs,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ConfigArgs {
+    #[clap(flatten)]
+    pub logging: LoggingArgs,
+    #[clap(flatten)]
+    pub run_types: RunTypesArgs,
+
+    /// Path to a toml file specifying a list of options this will override any other options set
+    #[arg(long, value_name = "FILE")]
+    pub config: Option<PathBuf>,
+    /// Ignore any project config files
+    #[arg(long)]
+    pub ignore_config: bool,
+    /// Test only the specified binary
+    #[arg(long, value_name = "NAME", num_args = 0..)]
+    pub bin: Vec<String>,
+    /// Test only the specified example
+    #[arg(long, value_name = "NAME", num_args = 0..)]
+    pub example: Vec<String>,
+    /// Test only the specified test target
+    #[arg(long, value_name = "NAME", num_args = 0..)]
+    pub test: Vec<String>,
+    /// Test only the specified bench target
+    #[arg(long, value_name = "NAME", num_args = 0..)]
+    pub bench: Vec<String>,
+    /// Run all tests regardless of failure
+    #[arg(long)]
+    pub no_fail_fast: bool,
+    /// Build artefacts with the specified profile
+    #[arg(long, value_name = "NAME")]
+    pub profile: Option<String>,
+    /// Ignore lines of test functions when collecting coverage (default)
+    #[arg(long)]
+    pub ignore_tests: bool,
+    /// Stops tarpaulin from building projects with -Clink-dead-code
+    #[arg(long)]
+    pub no_dead_code: bool,
+    /// Include lines of test functions when collecting coverage
+    #[arg(long)]
+    pub include_tests: bool,
+    /// Ignore panic macros in tests
+    #[arg(long)]
+    pub ignore_panics: bool,
+    /// Counts the number of hits during coverage
+    #[arg(long)]
+    pub count: bool,
+    /// Run ignored tests as well
+    #[arg(long, short)]
+    pub ignored: bool,
+    /// Line coverage
+    #[arg(long, short)]
+    pub line: bool,
+    /// The opposite of --force-clean
+    #[arg(long)]
+    pub skip_clean: bool,
+    /// Adds a clean stage to work around cargo bugs that may affect coverage results
+    #[arg(long)]
+    pub force_clean: bool,
+    /// Sets a percentage threshold for failure ranging from 0-100, if coverage is below exit with a non-zero code
+    #[arg(long, value_name = "PERCENTAGE")]
+    pub fail_under: Option<f64>,
+    /// Branch coverage: NOT IMPLEMENTED
+    #[arg(long, short)]
+    pub branch: bool,
+    /// Forwards unexpected signals to test. This is now the default behaviour
+    #[arg(long, short)]
+    pub forward: bool,
+    /// Coveralls key, either the repo token, or if you're using travis use $TRAVIS_JOB_ID and specify travis-{ci|pro} in --ciserver
+    #[arg(long, value_name = "KEY")]
+    pub coveralls: Option<String>,
+    /// URI to send report to, only used if the option --coveralls is used
+    #[arg(long, value_name = "URI")]
+    pub report_uri: Option<String>,
+    /// Do not include default features
+    #[arg(long)]
+    pub no_default_features: bool,
+    /// Features to be included in the target project
+    #[arg(long, value_name = "FEATURES", num_args = 0..)]
+    pub features: Vec<String>,
+    /// Build all available features
+    #[arg(long)]
+    pub all_features: bool,
+    /// Alias for --workspace (deprecated)
+    #[arg(long)]
+    pub all: bool,
+    /// Test all packages in the workspace
+    #[arg(long)]
+    pub workspace: bool,
+    /// Package id specifications for which package should be build. See cargo help pkgid for more info
+    #[arg(long, short, value_name = "PACKAGE", num_args = 0..)]
+    pub packages: Vec<String>,
+    /// Package id specifications to exclude from coverage. See cargo help pkgid for more info
+    #[arg(long, short, value_name = "PACKAGE", num_args = 0..)]
+    pub exclude: Vec<String>,
+    /// Exclude given files from coverage results has * wildcard
+    #[arg(long, value_name = "FILE", num_args = 0..)]
+    pub exclude_files: Vec<Pattern>,
+    /// Integer for the maximum time in seconds without response from test before timeout (default is 1 minute).
+    #[arg(long, short, value_name = "SECONDS")]
+    pub timeout: Option<u64>,
+    /// Delay after test to collect coverage profiles
+    #[arg(long, value_name = "SECONDS")]
+    pub post_test_delay: Option<u64>,
+    /// Follow executed processes capturing coverage information if they're part of your project.
+    #[arg(long)]
+    pub follow_exec: bool,
+    /// Build in release mode.
+    #[arg(long)]
+    pub release: bool,
+    /// Compile tests but don't run coverage
+    #[arg(long)]
+    pub no_run: bool,
+    /// 'Don't supply an explicit `--test-threads` argument to test executable. By default tarpaulin will infer the default rustc would pick if not ran via tarpaulin and set it
+    #[arg(long)]
+    pub implicit_test_threads: bool,
+    /// Do not update Cargo.lock
+    #[arg(long)]
+    pub locked: bool,
+    /// Do not update Cargo.lock or any caches
+    #[arg(long)]
+    pub frozen: bool,
+    /// Compilation target triple
+    #[arg(long, value_name = "TRIPLE")]
+    pub target: Option<String>,
+    /// Directory for all generated artifacts
+    #[arg(long, value_name = "DIR")]
+    pub target_dir: Option<PathBuf>,
+    /// Run without accessing the network
+    #[arg(long)]
+    pub offline: bool,
+    /// Remove --cfg=tarpaulin from the RUSTFLAG
+    #[arg(long)]
+    pub avoid_cfg_tarpaulin: bool,
+    /// Number of parallel jobs, defaults to # of CPUs
+    #[arg(long, short, value_name = "N")]
+    pub jobs: Option<usize>,
+    /// Rustflags to add when building project (can also be set via RUSTFLAGS env var)
+    #[arg(long, value_name = "FLAGS")]
+    pub rustflags: Option<String>,
+    /// Other object files to load which contain information for llvm coverage - must have been compiled with llvm coverage instrumentation (ignored for ptrace)
+    #[arg(long, value_name = "objects", num_args = 0..)]
+    pub objects: Vec<PathBuf>,
+    /// List of unstable nightly only flags
+    #[arg(short = 'Z', value_name = "FEATURES", num_args = 0..)]
+    pub unstable_features: Vec<String>,
+    /// Output format of coverage report
+    #[arg(long, short, value_enum, value_name = "FMT", num_args = 0..)]
+    pub out: Vec<OutputFile>,
+    /// Coverage tracing backend to use
+    #[arg(long, value_enum, value_name = "ENGINE")]
+    pub engine: Option<TraceEngine>,
+    /// Specify a custom directory to write report files
+    #[arg(long, value_name = "PATH")]
+    pub output_dir: Option<PathBuf>,
+    /// cargo subcommand to run. So far only test and build are supported
+    #[arg(long, value_enum, value_name = "CMD")]
+    pub command: Option<Mode>,
+    /// Calculates relative paths to root directory. If --manifest-path isn't specified it will look for a Cargo.toml in root
+    #[arg(long, short, value_name = "DIR")]
+    pub root: Option<PathBuf>,
+    /// Path to Cargo.toml
+    #[arg(long, value_name = "PATH")]
+    pub manifest_path: Option<PathBuf>,
+    /// CI server being used, if unspecified tarpaulin may automatically infer for coveralls uploads
+    #[arg(long, value_name = "SERVICE")]
+    pub ciserver: Option<Ci>,
+    /// Arguments to be passed to the test executables can be used to filter or skip certain tests
+    #[arg(last = true)]
+    pub args: Vec<String>,
+}
+
+#[derive(Debug, Clone, Copy, Args)]
+pub struct LoggingArgs {
+    /// Coloring: auto, always, never
+    #[arg(long, value_enum, value_name = "WHEN")]
+    pub color: Option<Color>,
+    /// Show debug output - this is used for diagnosing issues with tarpaulin
+    #[arg(long)]
+    pub debug: bool,
+    /// Show extra output
+    #[arg(long)]
+    pub verbose: bool,
+    /// Log tracing events and save to a json file. Also, enabled when --debug is used
+    #[arg(long)]
+    pub dump_traces: bool,
+}
+
+#[derive(Debug, Clone, Copy, Args)]
+pub struct PrintFlagsArgs {
+    /// Print the RUSTFLAGS options that tarpaulin will compile your program with and exit
+    #[arg(long)]
+    pub print_rust_flags: bool,
+    /// Print the RUSTDOCFLAGS options that tarpaulin will compile any doctests with and exit
+    #[arg(long)]
+    pub print_rustdoc_flags: bool,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct RunTypesArgs {
+    /// Type of the coverage run
+    #[arg(long, value_enum, value_name = "TYPE")]
+    pub run_types: Vec<RunType>,
+    /// Test all benches
+    #[arg(long)]
+    pub benches: bool,
+    /// Test only this library's documentation
+    #[arg(long)]
+    pub doc: bool,
+    /// Test all targets (excluding doctests)
+    #[arg(long)]
+    pub all_targets: bool,
+    /// Test only this package's library unit tests
+    #[arg(long)]
+    pub lib: bool,
+    /// Test all binaries
+    #[arg(long)]
+    pub bins: bool,
+    /// Test all examples
+    #[arg(long)]
+    pub examples: bool,
+    /// Test all tests
+    #[arg(long)]
+    pub tests: bool,
+}
+
+impl RunTypesArgs {
+    pub fn collect(self) -> Vec<RunType> {
+        let mut run_types = self.run_types;
+        if self.lib && !run_types.contains(&RunType::Lib) {
+            run_types.push(RunType::Lib);
+        }
+        if self.all_targets && !run_types.contains(&RunType::AllTargets) {
+            run_types.push(RunType::AllTargets);
+        }
+        if self.benches && !run_types.contains(&RunType::Benchmarks) {
+            run_types.push(RunType::Benchmarks);
+        }
+        if self.bins && !run_types.contains(&RunType::Bins) {
+            run_types.push(RunType::Bins);
+        }
+        if self.examples && !run_types.contains(&RunType::Examples) {
+            run_types.push(RunType::Examples);
+        }
+        if self.doc && !run_types.contains(&RunType::Doctests) {
+            run_types.push(RunType::Doctests);
+        }
+        if self.tests && !run_types.contains(&RunType::Tests) {
+            run_types.push(RunType::Tests);
+        }
+        run_types
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use clap::CommandFactory;
+
+    use super::CargoTarpaulinCli;
+
+    #[test]
+    fn verify_args() {
+        CargoTarpaulinCli::command().debug_assert()
+    }
+
+    #[test]
+    #[ignore = "Manual use only"]
+    fn show_help() {
+        let help = CargoTarpaulinCli::command().render_help();
+        println!("{help}");
+    }
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 5a68496..7bcf6a1 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -1,10 +1,10 @@
 use self::parse::*;
 pub use self::types::*;
-use crate::cargo::supports_llvm_coverage;
 use crate::path_utils::fix_unc_path;
+use crate::{args::ConfigArgs, cargo::supports_llvm_coverage};
 use cargo_metadata::{Metadata, MetadataCommand, Package};
-use clap::{value_t, ArgMatches};
 use coveralls_api::CiService;
+use glob::Pattern;
 use humantime_serde::deserialize as humantime_serde;
 use indexmap::IndexMap;
 use serde::{Deserialize, Serialize};
@@ -259,24 +259,18 @@ impl Default for Config {
     }
 }
 
-impl<'a> From<&'a ArgMatches<'a>> for ConfigWrapper {
-    fn from(args: &'a ArgMatches<'a>) -> Self {
+impl From<ConfigArgs> for ConfigWrapper {
+    fn from(args: ConfigArgs) -> Self {
         info!("Creating config");
-        let debug = args.is_present("debug");
-        let dump_traces = debug || args.is_present("dump-traces");
-        let verbose = args.is_present("verbose") || debug;
-        let excluded_files = get_excluded(args);
-        let excluded_files_raw = get_list(args, "exclude-files");
-        let features = get_list(args, "features");
+
+        let features = args.features;
         let features = if features.is_empty() {
             None
         } else {
             Some(features.join(" "))
         };
-        let force_clean = match (
-            args.is_present("force-clean"),
-            args.is_present("skip-clean"),
-        ) {
+
+        let force_clean = match (args.force_clean, args.skip_clean) {
             (true, false) | (false, false) => true,
             (false, true) => false,
             _ => {
@@ -285,74 +279,71 @@ impl<'a> From<&'a ArgMatches<'a>> for ConfigWrapper {
             }
         };
 
-        let engine = value_t!(args.value_of("engine"), TraceEngine).unwrap_or_default();
-
         let args_config = Config {
             name: String::new(),
-            manifest: get_manifest(args),
+            manifest: process_manifest(args.manifest_path, args.root.clone()),
             config: None,
-            root: get_root(args),
-            engine: RefCell::new(engine),
-            command: value_t!(args.value_of("command"), Mode).unwrap_or(Mode::Test),
-            color: value_t!(args.value_of("color"), Color).unwrap_or(Color::Auto),
-            run_types: get_run_types(args),
-            run_ignored: args.is_present("ignored"),
-            include_tests: args.is_present("include-tests"),
-            ignore_panics: args.is_present("ignore-panics"),
-            no_dead_code: args.is_present("no-dead-code"),
+            root: args.root,
+            engine: RefCell::new(args.engine.unwrap_or_default()),
+            command: args.command.unwrap_or(Mode::Test),
+            verbose: args.logging.verbose || args.logging.debug,
+            debug: args.logging.debug,
+            dump_traces: args.logging.debug || args.logging.dump_traces,
+            color: args.logging.color.unwrap_or(Color::Auto),
+            run_types: args.run_types.collect(),
+            run_ignored: args.ignored,
+            include_tests: args.include_tests,
+            ignore_panics: args.ignore_panics,
+            no_dead_code: args.no_dead_code,
             force_clean,
             skip_clean: !force_clean,
-            no_fail_fast: args.is_present("no-fail-fast"),
-            follow_exec: args.is_present("follow-exec"),
-            verbose,
-            debug,
-            dump_traces,
-            count: args.is_present("count"),
-            line_coverage: get_line_cov(args),
-            branch_coverage: get_branch_cov(args),
-            generate: get_outputs(args),
-            output_directory: get_output_directory(args),
-            coveralls: get_coveralls(args),
-            ci_tool: get_ci(args),
-            report_uri: get_report_uri(args),
+            no_fail_fast: args.no_fail_fast,
+            follow_exec: args.follow_exec,
+            count: args.count,
+            line_coverage: args.line || !args.branch,
+            branch_coverage: args.branch || !args.line,
+            generate: args.out,
+            output_directory: args.output_dir,
+            coveralls: args.coveralls,
+            ci_tool: args.ciserver.map(|c| c.0),
+            report_uri: args.report_uri,
             forward_signals: true, // No longer an option
-            all_features: args.is_present("all-features"),
-            no_default_features: args.is_present("no-default-features"),
+            all_features: args.all_features,
+            no_default_features: args.no_default_features,
             features,
-            unstable_features: get_list(args, "Z"),
-            all: args.is_present("all") | args.is_present("workspace"),
-            packages: get_list(args, "packages"),
-            exclude: get_list(args, "exclude"),
-            excluded_files: RefCell::new(excluded_files),
-            excluded_files_raw,
-            varargs: get_list(args, "args"),
-            test_timeout: get_timeout(args),
-            release: args.is_present("release"),
-            no_run: args.is_present("no-run"),
-            locked: args.is_present("locked"),
-            frozen: args.is_present("frozen"),
-            target: get_target(args),
-            target_dir: get_target_dir(args),
-            offline: args.is_present("offline"),
-            test_names: get_list(args, "test").iter().cloned().collect(),
-            bin_names: get_list(args, "bin").iter().cloned().collect(),
-            bench_names: get_list(args, "bench").iter().cloned().collect(),
-            example_names: get_list(args, "example").iter().cloned().collect(),
-            fail_under: value_t!(args.value_of("fail-under"), f64).ok(),
-            jobs: value_t!(args.value_of("jobs"), usize).ok(),
-            profile: get_profile(args),
+            unstable_features: args.unstable_features,
+            all: args.all | args.workspace,
+            packages: args.packages,
+            exclude: args.exclude,
+            excluded_files_raw: args.exclude_files.iter().map(Pattern::to_string).collect(),
+            excluded_files: RefCell::new(args.exclude_files),
+            varargs: args.args,
+            test_timeout: Duration::from_secs(args.timeout.unwrap_or(60)),
+            release: args.release,
+            no_run: args.no_run,
+            locked: args.locked,
+            frozen: args.frozen,
+            target: args.target,
+            target_dir: process_target_dir(args.target_dir),
+            offline: args.offline,
+            test_names: args.test.into_iter().collect(),
+            bin_names: args.bin.into_iter().collect(),
+            bench_names: args.bench.into_iter().collect(),
+            example_names: args.example.into_iter().collect(),
+            fail_under: args.fail_under,
+            jobs: args.jobs,
+            profile: args.profile,
             metadata: RefCell::new(None),
-            avoid_cfg_tarpaulin: args.is_present("avoid-cfg-tarpaulin"),
-            implicit_test_threads: args.is_present("implicit-test-threads"),
-            rustflags: get_rustflags(args),
-            post_test_delay: get_post_test_delay(args),
-            objects: get_objects(args),
+            avoid_cfg_tarpaulin: args.avoid_cfg_tarpaulin,
+            implicit_test_threads: args.implicit_test_threads,
+            rustflags: args.rustflags,
+            post_test_delay: args.post_test_delay.map(Duration::from_secs),
+            objects: canonicalize_paths(args.objects),
             profraw_folder: PathBuf::from("profraws"),
         };
-        if args.is_present("ignore-config") {
+        if args.ignore_config {
             Self(vec![args_config])
-        } else if args.is_present("config") {
-            let mut path = PathBuf::from(args.value_of("config").unwrap());
+        } else if let Some(mut path) = args.config {
             if path.is_relative() {
                 path = env::current_dir()
                     .unwrap()
@@ -558,7 +549,7 @@ impl Config {
             None => PathBuf::new(),
         };
         if let Ok(cfs) = res.as_mut() {
-            for mut c in cfs.iter_mut() {
+            for c in cfs.iter_mut() {
                 c.config = Some(file.as_ref().to_path_buf());
                 c.manifest = make_absolute_with_parent(&c.manifest, &parent);
                 if let Some(root) = c.root.as_mut() {
@@ -582,7 +573,7 @@ impl Config {
         })?;
 
         let mut result = Vec::new();
-        for (name, mut conf) in map.iter_mut() {
+        for (name, conf) in map.iter_mut() {
             conf.name = name.to_string();
             result.push(conf.clone());
         }
@@ -899,48 +890,37 @@ pub fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
 
 #[cfg(test)]
 mod tests {
+    use clap::Parser;
+
+    use crate::args::TarpaulinCli;
+
     use super::*;
-    use clap::App;
 
     #[test]
     fn features_args() {
-        let matches = App::new("tarpaulin")
-            .args_from_usage(
-                "--features [FEATURES]... 'Features to be included in the target project'
-                             --ignore-config 'Ignore any project config files'",
-            )
-            .get_matches_from_safe(vec![
-                "tarpaulin",
-                "--ignore-config",
-                "--features",
-                "a",
-                "--features",
-                "b",
-            ])
-            .unwrap();
-        let conf = ConfigWrapper::from(&matches).0;
+        let args = TarpaulinCli::parse_from(vec![
+            "tarpaulin",
+            "--ignore-config",
+            "--features",
+            "a",
+            "--features",
+            "b",
+        ]);
+        let conf = ConfigWrapper::from(args.config).0;
         assert_eq!(conf.len(), 1);
         assert_eq!(conf[0].features, Some("a b".to_string()));
 
-        let matches = App::new("tarpaulin")
-            .args_from_usage(
-                "--features [FEATURES]... 'Features to be included in the target project'
-                             --ignore-config 'Ignore any project config files'",
-            )
-            .get_matches_from_safe(vec!["tarpaulin", "--ignore-config", "--features", "a b"])
-            .unwrap();
-        let conf = ConfigWrapper::from(&matches).0;
+        let args =
+            TarpaulinCli::parse_from(vec!["tarpaulin", "--ignore-config", "--features", "a b"]);
+        let conf = ConfigWrapper::from(args.config).0;
         assert_eq!(conf.len(), 1);
         assert_eq!(conf[0].features, Some("a b".to_string()));
     }
 
     #[test]
     fn exclude_paths() {
-        let matches = App::new("tarpaulin")
-            .args_from_usage("--exclude-files [FILE]... 'Exclude given files from coverage results has * wildcard'")
-            .get_matches_from_safe(vec!["tarpaulin", "--exclude-files", "*module*"])
-            .unwrap();
-        let conf = ConfigWrapper::from(&matches).0;
+        let args = TarpaulinCli::parse_from(vec!["tarpaulin", "--exclude-files", "*module*"]);
+        let conf = ConfigWrapper::from(args.config).0;
         assert_eq!(conf.len(), 1);
         assert!(conf[0].exclude_path(Path::new("src/module/file.rs")));
         assert!(!conf[0].exclude_path(Path::new("src/mod.rs")));
@@ -950,11 +930,13 @@ mod tests {
 
     #[test]
     fn exclude_paths_directory_separators() {
-        let matches = App::new("tarpaulin")
-            .args_from_usage("--exclude-files [FILE]... 'Exclude given files from coverage results has * wildcard'")
-            .get_matches_from_safe(vec!["tarpaulin", "--exclude-files", "src/foo/*", "src\\bar\\*"])
-            .unwrap();
-        let conf = ConfigWrapper::from(&matches).0;
+        let args = TarpaulinCli::parse_from(vec![
+            "tarpaulin",
+            "--exclude-files",
+            "src/foo/*",
+            "src\\bar\\*",
+        ]);
+        let conf = ConfigWrapper::from(args.config).0;
         assert_eq!(conf.len(), 1);
         assert!(conf[0].exclude_path(Path::new("src/foo/file.rs")));
         assert!(conf[0].exclude_path(Path::new("src\\bar\\file.rs")));
@@ -972,11 +954,8 @@ mod tests {
 
     #[test]
     fn no_exclusions() {
-        let matches = App::new("tarpaulin")
-            .args_from_usage("--exclude-files [FILE]... 'Exclude given files from coverage results has * wildcard'")
-            .get_matches_from_safe(vec!["tarpaulin"])
-            .unwrap();
-        let conf = ConfigWrapper::from(&matches).0;
+        let args = TarpaulinCli::parse_from(vec!["tarpaulin"]);
+        let conf = ConfigWrapper::from(args.config).0;
         assert_eq!(conf.len(), 1);
         assert!(!conf[0].exclude_path(Path::new("src/module/file.rs")));
         assert!(!conf[0].exclude_path(Path::new("src/mod.rs")));
@@ -986,11 +965,8 @@ mod tests {
 
     #[test]
     fn exclude_exact_file() {
-        let matches = App::new("tarpaulin")
-            .args_from_usage("--exclude-files [FILE]... 'Exclude given files from coverage results has * wildcard'")
-            .get_matches_from_safe(vec!["tarpaulin", "--exclude-files", "*/lib.rs"])
-            .unwrap();
-        let conf = ConfigWrapper::from(&matches).0;
+        let args = TarpaulinCli::parse_from(vec!["tarpaulin", "--exclude-files", "*/lib.rs"]);
+        let conf = ConfigWrapper::from(args.config).0;
         assert_eq!(conf.len(), 1);
         assert!(conf[0].exclude_path(Path::new("src/lib.rs")));
         assert!(!conf[0].exclude_path(Path::new("src/mod.rs")));
diff --git a/src/config/parse.rs b/src/config/parse.rs
index 030dc2b..b1a5726 100644
--- a/src/config/parse.rs
+++ b/src/config/parse.rs
@@ -1,6 +1,5 @@
 use crate::config::types::*;
 use crate::path_utils::fix_unc_path;
-use clap::{value_t, values_t, ArgMatches};
 use coveralls_api::CiService;
 use serde::de::{self, Deserializer};
 use std::env;
@@ -8,48 +7,34 @@ use std::fmt;
 use std::fs::create_dir_all;
 use std::path::PathBuf;
 use std::str::FromStr;
-use std::time::Duration;
 use tracing::error;
 
-pub(super) fn get_list(args: &ArgMatches, key: &str) -> Vec<String> {
-    args.values_of_lossy(key).unwrap_or_default()
-}
-
-pub(super) fn get_line_cov(args: &ArgMatches) -> bool {
-    let cover_lines = args.is_present("line");
-    let cover_branches = args.is_present("branch");
-
-    cover_lines || !cover_branches
-}
-
-pub(super) fn get_branch_cov(args: &ArgMatches) -> bool {
-    let cover_lines = args.is_present("line");
-    let cover_branches = args.is_present("branch");
-
-    cover_branches || !cover_lines
+pub(super) fn globs_from_excluded(strs: &[String]) -> Vec<glob::Pattern> {
+    let mut files = vec![];
+    for temp_str in strs {
+        if let Ok(glob) = glob::Pattern::new(temp_str) {
+            files.push(glob);
+        } else {
+            error!("Ignoring invalid glob pattern: '{}'", temp_str);
+        }
+    }
+    files
 }
 
-pub(super) fn get_manifest(args: &ArgMatches) -> PathBuf {
-    if let Some(path) = args.value_of("manifest-path") {
-        let path = PathBuf::from(path);
-        if path.is_relative() {
-            return env::current_dir()
-                .unwrap()
-                .join(path)
-                .canonicalize()
-                .unwrap();
-        }
-        return fix_unc_path(&path);
+pub(super) fn process_manifest(
+    opt_manifest_path: Option<PathBuf>,
+    opt_root: Option<PathBuf>,
+) -> PathBuf {
+    if let Some(path) = opt_manifest_path {
+        return canonicalize_path(path);
     }
 
     let mut manifest = env::current_dir().unwrap();
-
-    if let Some(path) = args.value_of("root") {
+    if let Some(path) = opt_root {
         manifest.push(path);
     }
-
     manifest.push("Cargo.toml");
-    fix_unc_path(&manifest.canonicalize().unwrap_or(manifest))
+    canonicalize_path(manifest)
 }
 
 pub(super) fn default_manifest() -> PathBuf {
@@ -58,17 +43,9 @@ pub(super) fn default_manifest() -> PathBuf {
     fix_unc_path(&manifest.canonicalize().unwrap_or(manifest))
 }
 
-pub(super) fn get_target(args: &ArgMatches) -> Option<String> {
-    args.value_of("target").map(String::from)
-}
-
-pub(super) fn get_rustflags(args: &ArgMatches) -> Option<String> {
-    args.value_of("rustflags").map(String::from)
-}
-
-pub(super) fn get_target_dir(args: &ArgMatches) -> Option<PathBuf> {
-    let path = if let Some(path) = args.value_of("target-dir") {
-        PathBuf::from(path)
+pub(super) fn process_target_dir(opt_path: Option<PathBuf>) -> Option<PathBuf> {
+    let path = if let Some(path) = opt_path {
+        path
     } else if let Some(envvar) = env::var_os("CARGO_TARPAULIN_TARGET_DIR") {
         PathBuf::from(envvar)
     } else {
@@ -78,122 +55,23 @@ pub(super) fn get_target_dir(args: &ArgMatches) -> Option<PathBuf> {
     if !path.exists() {
         let _ = create_dir_all(&path);
     }
-    let path = if path.is_relative() {
-        env::current_dir()
-            .unwrap()
-            .join(path)
-            .canonicalize()
-            .unwrap()
-    } else {
-        path
-    };
-    Some(fix_unc_path(&path))
-}
-
-pub(super) fn get_root(args: &ArgMatches) -> Option<PathBuf> {
-    args.value_of("root").map(PathBuf::from)
-}
-
-pub(super) fn get_ci(args: &ArgMatches) -> Option<CiService> {
-    value_t!(args, "ciserver", Ci).map(|x| x.0).ok()
+    Some(canonicalize_path(path))
 }
 
-pub(super) fn get_coveralls(args: &ArgMatches) -> Option<String> {
-    args.value_of("coveralls").map(ToString::to_string)
+pub(super) fn canonicalize_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
+    paths.into_iter().map(canonicalize_path).collect()
 }
 
-pub(super) fn get_report_uri(args: &ArgMatches) -> Option<String> {
-    args.value_of("report-uri").map(ToString::to_string)
-}
-
-pub(super) fn get_profile(args: &ArgMatches) -> Option<String> {
-    args.value_of("profile").map(ToString::to_string)
-}
-
-pub(super) fn get_outputs(args: &ArgMatches) -> Vec<OutputFile> {
-    values_t!(args.values_of("out"), OutputFile).unwrap_or_else(|_| vec![])
-}
-
-pub(super) fn get_output_directory(args: &ArgMatches) -> Option<PathBuf> {
-    args.value_of("output-dir").map(PathBuf::from)
-}
-
-pub(super) fn get_objects(args: &ArgMatches) -> Vec<PathBuf> {
-    let mut objs = values_t!(args.values_of("objects"), PathBuf).unwrap_or_else(|_| vec![]);
-
-    for obj in objs.iter_mut() {
-        if obj.is_relative() {
-            *obj = env::current_dir()
-                .unwrap()
-                .canonicalize()
-                .unwrap()
-                .join(&obj);
-            *obj = fix_unc_path(obj);
-        }
-    }
-    objs
-}
-
-pub(super) fn get_run_types(args: &ArgMatches) -> Vec<RunType> {
-    let mut res = values_t!(args.values_of("run-types"), RunType).unwrap_or_else(|_| vec![]);
-    if args.is_present("lib") && !res.contains(&RunType::Lib) {
-        res.push(RunType::Lib);
-    }
-    if args.is_present("all-targets") && !res.contains(&RunType::AllTargets) {
-        res.push(RunType::AllTargets);
-    }
-    if args.is_present("benches") && !res.contains(&RunType::Benchmarks) {
-        res.push(RunType::Benchmarks);
-    }
-    if args.is_present("bins") && !res.contains(&RunType::Bins) {
-        res.push(RunType::Bins);
-    }
-    if args.is_present("examples") && !res.contains(&RunType::Examples) {
-        res.push(RunType::Examples);
-    }
-    if args.is_present("doc") && !res.contains(&RunType::Doctests) {
-        res.push(RunType::Doctests);
-    }
-    if args.is_present("tests") && !res.contains(&RunType::Tests) {
-        res.push(RunType::Tests);
-    }
-    res
-}
-
-pub(super) fn get_excluded(args: &ArgMatches) -> Vec<glob::Pattern> {
-    globs_from_excluded(&get_list(args, "exclude-files"))
-}
-
-pub(super) fn globs_from_excluded(strs: &[String]) -> Vec<glob::Pattern> {
-    let mut files = vec![];
-    for temp_str in strs {
-        if let Ok(glob) = glob::Pattern::new(temp_str) {
-            files.push(glob);
-        } else {
-            error!("Ignoring invalid glob pattern: '{}'", temp_str);
-        }
-    }
-    files
-}
-
-pub(super) fn get_timeout(args: &ArgMatches) -> Duration {
-    if args.is_present("timeout") {
-        let duration = value_t!(args.value_of("timeout"), u64).unwrap_or(60);
-        Duration::from_secs(duration)
-    } else {
-        Duration::from_secs(60)
-    }
-}
-
-pub(super) fn get_post_test_delay(args: &ArgMatches) -> Option<Duration> {
-    if args.is_present("post-test-delay") {
-        value_t!(args.value_of("post-test-delay"), u64)
-            .map(Duration::from_secs)
-            .ok()
-            .or_else(|| Some(Duration::from_secs(1)))
-    } else {
-        None
+pub(super) fn canonicalize_path(mut path: PathBuf) -> PathBuf {
+    if path.is_relative() {
+        path = env::current_dir()
+            .unwrap()
+            .canonicalize()
+            .unwrap()
+            .join(&path);
+        path = fix_unc_path(&path);
     }
+    path
 }
 
 pub fn deserialize_ci_server<'de, D>(d: D) -> Result<Option<CiService>, D::Error>
diff --git a/src/config/types.rs b/src/config/types.rs
index 72207ea..b207d45 100644
--- a/src/config/types.rs
+++ b/src/config/types.rs
@@ -1,24 +1,36 @@
-use clap::arg_enum;
+use clap::ValueEnum;
 use coveralls_api::CiService;
+use enum_display::EnumDisplay;
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
 
-arg_enum! {
-    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
-    pub enum Color {
-        Auto,
-        Always,
-        Never,
-    }
+#[derive(
+    Debug,
+    Copy,
+    Clone,
+    PartialEq,
+    Eq,
+    Hash,
+    Ord,
+    PartialOrd,
+    Deserialize,
+    Serialize,
+    ValueEnum,
+    EnumDisplay,
+)]
+pub enum Color {
+    Auto,
+    Always,
+    Never,
 }
 
-arg_enum! {
-    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
-    pub enum TraceEngine {
-        Auto,
-        Ptrace,
-        Llvm,
-    }
+#[derive(
+    Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize, ValueEnum,
+)]
+pub enum TraceEngine {
+    Auto,
+    Ptrace,
+    Llvm,
 }
 
 impl TraceEngine {
@@ -45,37 +57,37 @@ impl Default for TraceEngine {
     }
 }
 
-arg_enum! {
-    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
-    pub enum Mode {
-        Test,
-        Build
-    }
+#[derive(
+    Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize, ValueEnum,
+)]
+pub enum Mode {
+    Test,
+    Build,
 }
 
-arg_enum! {
-    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
-    pub enum RunType {
-        Tests,
-        Doctests,
-        Benchmarks,
-        Examples,
-        Lib,
-        Bins,
-        AllTargets,
-    }
+#[derive(
+    Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize, ValueEnum,
+)]
+pub enum RunType {
+    Tests,
+    Doctests,
+    Benchmarks,
+    Examples,
+    Lib,
+    Bins,
+    AllTargets,
 }
 
-arg_enum! {
-    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
-    #[non_exhaustive]
-    pub enum OutputFile {
-        Json,
-        Stdout,
-        Xml,
-        Html,
-        Lcov,
-    }
+#[derive(
+    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, ValueEnum,
+)]
+#[non_exhaustive]
+pub enum OutputFile {
+    Json,
+    Stdout,
+    Xml,
+    Html,
+    Lcov,
 }
 
 impl Default for OutputFile {
@@ -85,7 +97,7 @@ impl Default for OutputFile {
     }
 }
 
-#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
 pub struct Ci(pub CiService);
 
 impl FromStr for Ci {
diff --git a/src/lib.rs b/src/lib.rs
index bd16147..cf6106c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,6 +13,7 @@ use std::fs::{create_dir_all, remove_dir_all};
 use tracing::{debug, error, info, warn};
 use tracing_subscriber::{filter::LevelFilter, EnvFilter};
 
+pub mod args;
 pub mod branching;
 pub mod cargo;
 pub mod config;
diff --git a/src/main.rs b/src/main.rs
index bfd4680..72bd1a9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,22 +1,11 @@
 #![cfg(not(tarpaulin_include))]
+use cargo_tarpaulin::args::CargoTarpaulinCli;
 use cargo_tarpaulin::cargo::{rust_flags, rustdoc_flags};
-use cargo_tarpaulin::config::{
-    Color, Config, ConfigWrapper, Mode, OutputFile, RunType, TraceEngine,
-};
+use cargo_tarpaulin::config::{Color, Config, ConfigWrapper};
 use cargo_tarpaulin::{run, setup_logging};
-use clap::{crate_version, value_t, App, Arg, ArgMatches, ArgSettings, SubCommand};
 use std::collections::HashMap;
-use std::path::Path;
 use tracing::{info, trace};
 
-fn is_dir(d: String) -> Result<(), String> {
-    if Path::new(&d).is_dir() {
-        Ok(())
-    } else {
-        Err(String::from("root must be a directory"))
-    }
-}
-
 fn print_env(seen_rustflags: HashMap<String, Vec<String>>, prefix: &str, default_val: &str) {
     info!("Printing `{}`", prefix);
     if seen_rustflags.is_empty() {
@@ -33,30 +22,27 @@ fn print_env(seen_rustflags: HashMap<String, Vec<String>>, prefix: &str, default
     }
 }
 
-const CI_SERVER_HELP: &str = "Name of service, supported services are:
-travis-ci, travis-pro, circle-ci, semaphore, jenkins and codeship.
-If you are interfacing with coveralls.io or another site you can \
-also specify a name that they will recognise. Refer to their documentation for this.";
-
 fn main() -> Result<(), String> {
-    let args = from_args();
+    let args = CargoTarpaulinCli::from_args();
 
+    let logging_args = args.config.logging;
     setup_logging(
-        value_t!(args.value_of("color"), Color).unwrap_or(Color::Auto),
-        args.is_present("debug"),
-        args.is_present("verbose"),
+        logging_args.color.unwrap_or(Color::Auto),
+        logging_args.debug,
+        logging_args.verbose,
     );
 
-    let config = ConfigWrapper::from(&args);
+    let config = ConfigWrapper::from(args.config);
 
     trace!("Config vector: {:#?}", config);
 
-    if args.is_present("print-rust-flags") {
+    let print_flags_args = args.print_flags;
+    if print_flags_args.print_rust_flags {
         print_flags(&config, rust_flags, "RUSTFLAGS");
         return Ok(());
     }
 
-    if args.is_present("print-rustdoc-flags") {
+    if print_flags_args.print_rustdoc_flags {
         print_flags(&config, rustdoc_flags, "RUSTDOCFLAGS");
         return Ok(());
     }
@@ -68,111 +54,6 @@ fn main() -> Result<(), String> {
     run(&config.0).map_err(|e| e.to_string())
 }
 
-fn from_args() -> ArgMatches<'static> {
-    let args = App::new("cargo-tarpaulin")
-        .author("Daniel McKenna, <danielmckenna93@gmail.com>")
-        .about("Tool to analyse test coverage of cargo projects")
-        .version(concat!("version: ", crate_version!()))
-        .bin_name("cargo")
-        .subcommand(SubCommand::with_name("tarpaulin")
-            .about("Tool to analyse test coverage of cargo projects")
-            .version(concat!("version: ", crate_version!()))
-            .args_from_usage(
-                 "--config [FILE] 'Path to a toml file specifying a list of options this will override any other options set'
-                 --ignore-config 'Ignore any project config files'
-                 --lib 'Test only this package's library unit tests'
-                 --bin [NAME]... 'Test only the specified binary`
-                 --bins 'Test all binaries'
-                 --example [NAME]... 'Test only the specified example'
-                 --examples 'Test all examples'
-                 --test [NAME]... 'Test only the specified test target'
-                 --tests 'Test all tests'
-                 --bench [NAME]... 'Test only the specified bench target'
-                 --benches 'Test all benches'
-                 --doc 'Test only this library's documentation'
-                 --all-targets 'Test all targets (excluding doctests)'
-                 --no-fail-fast 'Run all tests regardless of failure'
-                 --profile [NAME] 'Build artefacts with the specified profile'
-                 --debug 'Show debug output - this is used for diagnosing issues with tarpaulin'
-                 --dump-traces 'Log tracing events and save to a json file. Also, enabled when --debug is used'
-                 --verbose -v 'Show extra output'
-                 --ignore-tests 'Ignore lines of test functions when collecting coverage (default)'
-                 --no-dead-code 'Stops tarpaulin from building projects with -Clink-dead-code'
-                 --include-tests 'Include lines of test functions when collecting coverage'
-                 --ignore-panics 'Ignore panic macros in tests'
-                 --count   'Counts the number of hits during coverage'
-                 --ignored -i 'Run ignored tests as well'
-                 --line -l    'Line coverage'
-                 --skip-clean 'The opposite of --force-clean'
-                 --force-clean 'Adds a clean stage to work around cargo bugs that may affect coverage results'
-                 --fail-under [PERCENTAGE] 'Sets a percentage threshold for failure ranging from 0-100, if coverage is below exit with a non-zero code'
-                 --branch -b  'Branch coverage: NOT IMPLEMENTED'
-                 --forward -f 'Forwards unexpected signals to test. This is now the default behaviour'
-                 --coveralls [KEY]  'Coveralls key, either the repo token, or if you're using travis use $TRAVIS_JOB_ID and specify travis-{ci|pro} in --ciserver'
-                 --report-uri [URI] 'URI to send report to, only used if the option --coveralls is used'
-                 --no-default-features 'Do not include default features'
-                 --features [FEATURES]... 'Features to be included in the target project'
-                 --all-features 'Build all available features'
-                 --all        'Alias for --workspace (deprecated)'
-                 --workspace 'Test all packages in the workspace'
-                 --packages -p [PACKAGE]... 'Package id specifications for which package should be build. See cargo help pkgid for more info'
-                 --exclude -e [PACKAGE]... 'Package id specifications to exclude from coverage. See cargo help pkgid for more info'
-                 --exclude-files [FILE]... 'Exclude given files from coverage results has * wildcard'
-                 --timeout -t [SECONDS] 'Integer for the maximum time in seconds without response from test before timeout (default is 1 minute).'
-                 --post-test-delay [SECONDS] 'Delay after test to collect coverage profiles'
-                 --follow-exec 'Follow executed processes capturing coverage information if they're part of your project.'
-                 --release   'Build in release mode.'
-                 --no-run 'Compile tests but don't run coverage'
-                 --implicit-test-threads 'Don't supply an explicit `--test-threads` argument to test executable. By default tarpaulin will infer the default rustc would pick if not ran via tarpaulin and set it'
-                 --locked 'Do not update Cargo.lock'
-                 --frozen 'Do not update Cargo.lock or any caches'
-                 --target [TRIPLE] 'Compilation target triple'
-                 --target-dir [DIR] 'Directory for all generated artifacts'
-                 --offline 'Run without accessing the network'
-                 --print-rust-flags 'Print the RUSTFLAGS options that tarpaulin will compile your program with and exit'
-                 --print-rustdoc-flags 'Print the RUSTDOCFLAGS options that tarpaulin will compile any doctests with and exit'
-                 --avoid-cfg-tarpaulin 'Remove --cfg=tarpaulin from the RUSTFLAG'
-                 -j --jobs [N] 'Number of parallel jobs, defaults to # of CPUs'
-                 --rustflags [FLAGS] 'rustflags to add when building project (can also be set via RUSTFLAGS env var)'
-                --objects [objects]...   'Other object files to load which contain information for llvm coverage - must have been compiled with llvm coverage instrumentation (ignored for ptrace)'
-                 -Z [FEATURES]...   'List of unstable nightly only flags'")
-            .args(&[
-                Arg::from_usage("--out -o [FMT]   'Output format of coverage report'")
-                    .possible_values(&OutputFile::variants())
-                    .case_insensitive(true)
-                    .multiple(true),
-                Arg::from_usage("--engine [ENGINE] 'Coverage tracing backend to use'")
-                    .possible_values(&TraceEngine::variants())
-                    .case_insensitive(true)
-                    .multiple(false),
-                Arg::from_usage("--output-dir [PATH] 'Specify a custom directory to write report files'"),
-                Arg::from_usage("--run-types [TYPE]... 'Type of the coverage run'")
-                    .possible_values(&RunType::variants())
-                    .case_insensitive(true)
-                    .multiple(true),
-                Arg::from_usage("--color [WHEN] 'Coloring: auto, always, never'")
-                    .case_insensitive(true)
-                    .possible_values(&Color::variants()),
-                Arg::from_usage("--command [CMD] 'cargo subcommand to run. So far only test and build are supported'")
-                    .case_insensitive(true)
-                    .possible_values(&Mode::variants()),
-                Arg::from_usage("--root -r [DIR]  'Calculates relative paths to root directory. If --manifest-path isn't specified it will look for a Cargo.toml in root'")
-                    .validator(is_dir),
-                Arg::from_usage("--manifest-path [PATH] 'Path to Cargo.toml'"),
-                Arg::from_usage("--ciserver [SERVICE] 'CI server being used, if unspecified tarpaulin may automatically infer for coveralls uploads'")
-                    .help(CI_SERVER_HELP),
-                Arg::with_name("args")
-                    .set(ArgSettings::Last)
-                    .multiple(true)
-                    .help("Arguments to be passed to the test executables can be used to filter or skip certain tests")
-            ]))
-        .get_matches();
-
-    let args = args.subcommand_matches("tarpaulin").unwrap_or(&args);
-
-    args.clone()
-}
-
 fn print_flags<F>(config: &ConfigWrapper, flags_fn: F, prefix: &str)
 where
     F: Fn(&Config) -> String,


Test patch:
diff --git a/tests/data/arrays/Cargo.lock b/tests/data/arrays/Cargo.lock
index 68e3c59..d7d22de 100644
--- a/tests/data/arrays/Cargo.lock
+++ b/tests/data/arrays/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "arrays"
 version = "0.1.0"
-
diff --git a/tests/data/assigns/Cargo.lock b/tests/data/assigns/Cargo.lock
index 70d78dc..41279ca 100644
--- a/tests/data/assigns/Cargo.lock
+++ b/tests/data/assigns/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "assigns"
 version = "0.1.0"
-
diff --git a/tests/data/boxes/Cargo.lock b/tests/data/boxes/Cargo.lock
index a201713..adaaf6d 100644
--- a/tests/data/boxes/Cargo.lock
+++ b/tests/data/boxes/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "boxes"
 version = "0.1.0"
-
diff --git a/tests/data/breaks/Cargo.lock b/tests/data/breaks/Cargo.lock
index 94d9e6c..ca1a595 100644
--- a/tests/data/breaks/Cargo.lock
+++ b/tests/data/breaks/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "breaks"
 version = "0.1.0"
-
diff --git a/tests/data/compile_fail/Cargo.lock b/tests/data/compile_fail/Cargo.lock
index 5f6dcd3..c41e423 100644
--- a/tests/data/compile_fail/Cargo.lock
+++ b/tests/data/compile_fail/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "compile_fail"
 version = "0.1.0"
-
diff --git a/tests/data/configs/Cargo.lock b/tests/data/configs/Cargo.lock
index 829bd4e..1e9ef30 100644
--- a/tests/data/configs/Cargo.lock
+++ b/tests/data/configs/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "configs"
 version = "0.1.0"
-
diff --git a/tests/data/continues/Cargo.lock b/tests/data/continues/Cargo.lock
index 7c0439c..b7b766d 100644
--- a/tests/data/continues/Cargo.lock
+++ b/tests/data/continues/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "continues"
 version = "0.1.0"
-
diff --git a/tests/data/doc_coverage/Cargo.lock b/tests/data/doc_coverage/Cargo.lock
index 32b776a..142bb7e 100644
--- a/tests/data/doc_coverage/Cargo.lock
+++ b/tests/data/doc_coverage/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "doc_coverage"
 version = "0.1.0"
-
diff --git a/tests/data/ifelse/Cargo.lock b/tests/data/ifelse/Cargo.lock
index 4ed2e1c..8d38e09 100644
--- a/tests/data/ifelse/Cargo.lock
+++ b/tests/data/ifelse/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "ifelse"
 version = "0.1.0"
-
diff --git a/tests/data/lets/Cargo.lock b/tests/data/lets/Cargo.lock
index a9acae9..0405fb3 100644
--- a/tests/data/lets/Cargo.lock
+++ b/tests/data/lets/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "lets"
 version = "0.1.0"
-
diff --git a/tests/data/loops/Cargo.lock b/tests/data/loops/Cargo.lock
index 9942b36..4d2ad7f 100644
--- a/tests/data/loops/Cargo.lock
+++ b/tests/data/loops/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "loops"
 version = "0.1.0"
-
diff --git a/tests/data/matches/Cargo.lock b/tests/data/matches/Cargo.lock
index d52d76f..1c1fa00 100644
--- a/tests/data/matches/Cargo.lock
+++ b/tests/data/matches/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "matches"
 version = "0.1.0"
-
diff --git a/tests/data/paths/Cargo.lock b/tests/data/paths/Cargo.lock
index 31de912..5ad5b8d 100644
--- a/tests/data/paths/Cargo.lock
+++ b/tests/data/paths/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "paths"
 version = "0.1.0"
-
diff --git a/tests/data/returns/Cargo.lock b/tests/data/returns/Cargo.lock
index 065c6cf..c952079 100644
--- a/tests/data/returns/Cargo.lock
+++ b/tests/data/returns/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "returns"
 version = "0.1.0"
-
diff --git a/tests/data/simple_project/Cargo.lock b/tests/data/simple_project/Cargo.lock
index d95c2bd..3a6c1ad 100644
--- a/tests/data/simple_project/Cargo.lock
+++ b/tests/data/simple_project/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "simple_project"
 version = "0.1.0"
-
diff --git a/tests/data/structs/Cargo.lock b/tests/data/structs/Cargo.lock
index bede081..fb30ed9 100644
--- a/tests/data/structs/Cargo.lock
+++ b/tests/data/structs/Cargo.lock
@@ -1,6 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "structs"
 version = "0.1.0"
-
diff --git a/tests/mod.rs b/tests/mod.rs
index 2e95285..a176ac7 100755
--- a/tests/mod.rs
+++ b/tests/mod.rs
@@ -1,12 +1,13 @@
 use crate::utils::get_test_path;
-use cargo_tarpaulin::config::{
-    Color, Config, ConfigWrapper, Mode, OutputFile, RunType, TraceEngine,
-};
 use cargo_tarpaulin::event_log::EventLog;
 use cargo_tarpaulin::path_utils::*;
 use cargo_tarpaulin::traces::TraceMap;
+use cargo_tarpaulin::{
+    args::TarpaulinCli,
+    config::{Color, Config, ConfigWrapper, Mode, OutputFile, RunType, TraceEngine},
+};
 use cargo_tarpaulin::{launch_tarpaulin, run, setup_logging};
-use clap::{App, Arg};
+use clap::Parser;
 #[cfg(windows)]
 use regex::Regex;
 use rusty_fork::rusty_fork_test;
@@ -35,33 +36,9 @@ pub fn check_percentage_with_cli_args(
 ) -> TraceMap {
     setup_logging(Color::Never, false, false);
     let restore_dir = env::current_dir().unwrap();
-    let matches = App::new("tarpaulin")
-        .args_from_usage(
-             "--config [FILE] 'Path to a toml file specifying a list of options this will override any other options set'
-             --ignore-config 'Ignore any project config files'
-             --debug 'Show debug output - this is used for diagnosing issues with tarpaulin'
-             --verbose -v 'Show extra output'
-             --root -r [DIR] 'directory'
-             --include-tests 'include tests in your tests'
-             --post-test-delay [SECONDS] 'Delay after test to collect coverage profiles'
-             --implicit-test-threads 'Don't supply an explicit `--test-threads` argument to tarpaulin. By default tarpaulin will infer the default rustc would pick if not ran via tarpaulin and set it'"
-        ).args(&[
-                Arg::from_usage("--out -o [FMT]   'Output format of coverage report'")
-                    .possible_values(&OutputFile::variants())
-                    .case_insensitive(true)
-                    .multiple(true),
-                Arg::from_usage("--engine [ENGINE] 'Coverage tracing backend to use'")
-                    .possible_values(&TraceEngine::variants())
-                    .case_insensitive(true)
-                    .multiple(false),
-                Arg::from_usage("--output-dir [PATH] 'Specify a custom directory to write report files'"),
-                Arg::from_usage("--color [WHEN] 'Coloring: auto, always, never'")
-                    .case_insensitive(true)
-                    .possible_values(&Color::variants()),
-        ])
-        .get_matches_from(args);
-
-    let mut configs = ConfigWrapper::from(&matches).0;
+    let args = TarpaulinCli::parse_from(args);
+
+    let mut configs = ConfigWrapper::from(args.config).0;
     let mut res = TraceMap::new();
     for config in &mut configs {
         config.set_clean(false);