Repository: clap-rs/clap (language: rust)

Issue:
assert_defaults assumes parsers are stateless (e.g. don't check file system for a files existence)
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the [open](https://github.com/clap-rs/clap/issues) and [rejected](https://github.com/clap-rs/clap/issues?q=is%3Aissue+label%3AS-wont-fix+is%3Aclosed) issues

### Rust Version

1.66.0

### Clap Version

4.1.1

### Minimal reproducible code

```
use clap::{self, Arg, Command, ValueHint};
use std::path::Path;

fn main() -> () {
    let app = cli();
    let matches = app.get_matches();
    let wordlist = matches.get_one::<String>("wordlist").unwrap().to_string();
}

fn cli() -> Command {
    let app = clap::Command::new(clap::crate_name!())
        .version(clap::crate_version!())
        .author(clap::crate_authors!())
        .about(clap::crate_description!());

    app.arg(
        Arg::new("wordlist")
            .short('w')
            .value_name("WORDLIST")
            .num_args(1)
            .value_hint(ValueHint::FilePath)
            .default_value("/usr/share/wordlists/seclists/raft-medium-directories.txt")
            .value_parser(clap::builder::ValueParser::new(wordlist_parse_wrap))
            .help("Wordlist for web discovery"),
    )
}

fn wordlist_parse_wrap(wordlist: &str) -> Result<String, clap::Error> {
    if !Path::new(&wordlist).exists() {
        return Err(clap::Error::raw(
            clap::error::ErrorKind::ValueValidation,
            "File does not exist",
        ));
    }
    Ok(wordlist.to_string())
}

```

### Steps to reproduce the bug with the above code

cargo run

### Actual Behaviour

thread 'main' panicked at 'Argument `wordlist`'s default_value="/usr/share/wordlists/seclists/raft-medium-directories.txt" failed validation: error: invalid value '/usr/share/wordlists/seclists/raft-medium-directories.txt' for '-w <WORDLIST>': error: File does not exist

### Expected Behaviour

error: invalid value '/usr/share/wordlists/seclists/raft-medium-directories.txt' for '-w <WORDLIST>': error: File does not exist

### Additional Context

I know the [documentation](https://docs.rs/clap/latest/clap/builder/struct.Command.html#method.debug_assert) states

> Most error states are handled as asserts under the assumption they are programming mistake and not something to handle at runtime

But in this case it isn't a programmer mistake. The default value may or may not be valid in this case, but programmer mistakes should be catchable at compile time rather than runtime. Obviously the validity of the arg isn't knowable at compile time and can't be checked, but as a result a normal failure is more appropriate than a panic.

The two potential solutions that come to mind are:

1. Completely remove the [assert_default function](https://github.com/clap-rs/clap/blob/master/src/builder/debug_asserts.rs#L855)
2. Create a new flag that can disable its use

Option 1 seems like less work, but there may be valid uses of the function (I haven't come across one but I'm sure there's good reason for its existence). Option 2 would probably be more work, but it is more explicit and allows for the programmer to opt in to the unvalidated defaults.

### Debug Output

_No response_


Discussion / hints:
I've clarified the title to what I see as the root issue.

The old title implied a third solution of having `Command::build` return an error which would not help in your situation as it still prevents you from parsing.

> but programmer mistakes should be catchable at compile time rather than runtime. Obviously the validity of the arg isn't knowable at compile time and can't be checked, but as a result a normal failure is more appropriate than a panic.

Not all programmer mistakes can be.  We also regularly encourage people to [add a test to help catch these asserts](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#testing) so they are caught at test-time rather than in-prod.

However, seems irrelevant to the actual problem at hand.  Your parser is dependent on the state of the system and so asserting on the status of the parser can fail.

>  there may be valid uses of the function (I haven't come across one but I'm sure there's good reason for its existence).

When a user sets a raw default (ie a string) it might not be valid.  To catch this, the user needs to exhaustively test their application.  By adding an assert, we guaranteed that people will catch these bugs so long as they activate the debug asserts in a test.  See #3202.

> Create a new flag that can disable its use

Flags come with API complexity and extra build time, both things that we are working on reducing.  

#1695 is another route to resolving this.

Another option is for the application's file existence check to not be done within clap but to be done afterwards.

In deciding what route to go, something we should keep in mind is that this will also affect #4074.
That all makes sense, thanks for the explanations!

Specific to the following, I admit I hadn't considered the obvious, but as I think of it, surely I'm not the only one who has done something like this before. How do others usually tackle the problem of accepting a file as an argument? If there's a best practice around that I certainly don't know it but would like to!

> Another option is for the application's file existence check to not be done within clap but to be done afterwards.
> How do others usually tackle the problem of accepting a file as an argument? If there's a best practice around that I certainly don't know it but would like to!

In my applications I pass the Patrh around and let the failure happen naturally on access.  For better error messages, some people use crates like [fs-err](https://crates.io/crates/fs-err)
It feels a little weird to be able to validate _some_ but not _all_ arguments though doesn't it? 
I doubt clap  will ever support validation of every case.  For example, an argument might depend on the value of another argument.
Reviewing this issue, I think it may be miscategorized. The real issue here is that a default argument that doesn't pass validation is _panicking_ when it should more gracefully exit through https://github.com/clap-rs/clap/blob/master/src/error/format.rs#L313.

The valueparser shouldn't be responsible for managing state as you've mentioned, but it also shouldn't cause a panic when the mechanism for explaining that a value doesn't pass validation already exists
This is categorized correctly

We were previously deferring all default parse errors to parse-time which made error reporting dependent on a specific command line and for the developer/user to be able to distinguish this as a development-time error.  We found people were mistyping their defaults and wanted to move this from "test the right combination of command lines" to a part of the process that will always catch this, our debug asserts.  This is what #3202 is about which I linked earlier.

Maybe you meant that programmer errors should pass up as errors, rather than panic.
- This won't help in your case because it will prevent parsing the command line
- This would require having a failable builder.  Currently, for large trees of subcommands, we build only what is needed.  With a failable builder, we could force building all, slowing down those applications and hurting ergonomics by requiring an explicit build step, or we could mix it into the user error when doing a parse.  The derive API does this in some cases and is a frequent source of support issues.
> Maybe you meant that programmer errors should pass up as errors, rather than panic.

That is correct, and what I said in the original post. If that's not a change you intend to make that's fine, but the intuitiveness of clap suffers greatly as a result
From experience in supporting clap users, we've had to support people more from problems with reporting developer errors through `Result` than where we do it through `panic`.

Even this issue, so far I've seen nothing that would have been helped by reporting this through `Result`.  
The difference is the user experience of a panic vs a graceful exit as if they had typed in any other non-valid value. If the default is a file they don't have on their machine because they prefer to use another they shouldn't get a panic without the opportunity to provide their own value
> The difference is the user experience of a panic vs a graceful exit as if they had typed in any other non-valid value.

From my experience in my own apps and supporting other people's apps, our panics are a better user experience for development-time bugs.

> If the default is a file they don't have on their machine because they prefer to use another they shouldn't get a panic without the opportunity to provide their own value

Yes, I recognize that our current approach does not support parsers that are dependent on state, like the filesystem.  That is why this issue is still open.  I was not objecting to that but to trying to re-categegorize this.
I ran into this with [clio](https://github.com/aj-bagwell/clio) where I was trying to set the default directory to './' which was panicing when the current working directory did not exist.

After I worked out that the asserts are only included in debug builds so won't affect real users, I decided that it was not that big an issue for me.
But it means users of clio wouldn't be able to run / test debug builds.

In weighing this vs #3202, I'm considering reverting the fix for #3202 and re-opening it.  We offer higher-level ways of doing most things that prevent bugs like that happening (`default_value_t`, `#[derive(ValueEum)]`) 

Fix patch:
diff --git a/clap_builder/src/builder/debug_asserts.rs b/clap_builder/src/builder/debug_asserts.rs
index ef970cdaad0..29008be9f5e 100644
--- a/clap_builder/src/builder/debug_asserts.rs
+++ b/clap_builder/src/builder/debug_asserts.rs
@@ -1,8 +1,5 @@
 use std::cmp::Ordering;
 
-use clap_lex::OsStrExt as _;
-
-use crate::builder::OsStr;
 use crate::builder::ValueRange;
 use crate::mkeymap::KeyType;
 use crate::util::FlatSet;
@@ -809,20 +806,6 @@ fn assert_arg(arg: &Arg) {
     }
 
     assert_arg_flags(arg);
-
-    assert_defaults(arg, "default_value", arg.default_vals.iter());
-    assert_defaults(
-        arg,
-        "default_missing_value",
-        arg.default_missing_vals.iter(),
-    );
-    assert_defaults(
-        arg,
-        "default_value_if",
-        arg.default_vals_ifs
-            .iter()
-            .filter_map(|(_, _, default)| default.as_ref()),
-    );
 }
 
 fn assert_arg_flags(arg: &Arg) {
@@ -854,37 +837,3 @@ fn assert_arg_flags(arg: &Arg) {
     checker!(is_multiple_values_set requires is_takes_value_set);
     checker!(is_ignore_case_set requires is_takes_value_set);
 }
-
-fn assert_defaults<'d>(
-    arg: &Arg,
-    field: &'static str,
-    defaults: impl IntoIterator<Item = &'d OsStr>,
-) {
-    for default_os in defaults {
-        let value_parser = arg.get_value_parser();
-        let assert_cmd = Command::new("assert");
-        if let Some(val_delim) = arg.get_value_delimiter() {
-            let mut val_delim_buffer = [0; 4];
-            let val_delim = val_delim.encode_utf8(&mut val_delim_buffer);
-            for part in default_os.split(val_delim) {
-                if let Err(err) = value_parser.parse_ref(&assert_cmd, Some(arg), part) {
-                    panic!(
-                        "Argument `{}`'s {}={:?} failed validation: {}",
-                        arg.get_id(),
-                        field,
-                        part.to_string_lossy(),
-                        err
-                    );
-                }
-            }
-        } else if let Err(err) = value_parser.parse_ref(&assert_cmd, Some(arg), default_os) {
-            panic!(
-                "Argument `{}`'s {}={:?} failed validation: {}",
-                arg.get_id(),
-                field,
-                default_os,
-                err
-            );
-        }
-    }
-}


Test patch:
diff --git a/tests/builder/default_missing_vals.rs b/tests/builder/default_missing_vals.rs
index da34c28b7c8..98efafcd8b3 100644
--- a/tests/builder/default_missing_vals.rs
+++ b/tests/builder/default_missing_vals.rs
@@ -258,38 +258,6 @@ fn delimited_missing_value() {
     );
 }
 
-#[cfg(debug_assertions)]
-#[test]
-#[cfg(feature = "error-context")]
-#[should_panic = "Argument `arg`'s default_missing_value=\"value\" failed validation: error: invalid value 'value' for '[arg]'"]
-fn default_missing_values_are_possible_values() {
-    use clap::{Arg, Command};
-
-    let _ = Command::new("test")
-        .arg(
-            Arg::new("arg")
-                .value_parser(["one", "two"])
-                .default_missing_value("value"),
-        )
-        .try_get_matches();
-}
-
-#[cfg(debug_assertions)]
-#[test]
-#[cfg(feature = "error-context")]
-#[should_panic = "Argument `arg`'s default_missing_value=\"value\" failed validation: error: invalid value 'value' for '[arg]"]
-fn default_missing_values_are_valid() {
-    use clap::{Arg, Command};
-
-    let _ = Command::new("test")
-        .arg(
-            Arg::new("arg")
-                .value_parser(clap::value_parser!(u32))
-                .default_missing_value("value"),
-        )
-        .try_get_matches();
-}
-
 #[test]
 fn valid_index() {
     let m = Command::new("df")
diff --git a/tests/builder/default_vals.rs b/tests/builder/default_vals.rs
index e8ddf7c1e12..c10ddedb775 100644
--- a/tests/builder/default_vals.rs
+++ b/tests/builder/default_vals.rs
@@ -793,38 +793,6 @@ fn required_args_with_default_values() {
     assert!(m.contains_id("arg"));
 }
 
-#[cfg(debug_assertions)]
-#[test]
-#[cfg(feature = "error-context")]
-#[should_panic = "Argument `arg`'s default_value=\"value\" failed validation: error: invalid value 'value' for '[arg]'"]
-fn default_values_are_possible_values() {
-    use clap::{Arg, Command};
-
-    let _ = Command::new("test")
-        .arg(
-            Arg::new("arg")
-                .value_parser(["one", "two"])
-                .default_value("value"),
-        )
-        .try_get_matches();
-}
-
-#[cfg(debug_assertions)]
-#[test]
-#[cfg(feature = "error-context")]
-#[should_panic = "Argument `arg`'s default_value=\"one\" failed validation: error: invalid value 'one' for '[arg]"]
-fn invalid_default_values() {
-    use clap::{Arg, Command};
-
-    let _ = Command::new("test")
-        .arg(
-            Arg::new("arg")
-                .value_parser(clap::value_parser!(u32))
-                .default_value("one"),
-        )
-        .try_get_matches();
-}
-
 #[test]
 fn valid_delimited_default_values() {
     use clap::{Arg, Command};
@@ -839,23 +807,6 @@ fn valid_delimited_default_values() {
         .debug_assert();
 }
 
-#[cfg(debug_assertions)]
-#[test]
-#[cfg(feature = "error-context")]
-#[should_panic = "Argument `arg`'s default_value=\"one\" failed validation: error: invalid value 'one' for '[arg]"]
-fn invalid_delimited_default_values() {
-    use clap::{Arg, Command};
-
-    Command::new("test")
-        .arg(
-            Arg::new("arg")
-                .value_parser(clap::value_parser!(u32))
-                .value_delimiter(',')
-                .default_value("one,two"),
-        )
-        .debug_assert();
-}
-
 #[test]
 fn with_value_delimiter() {
     let cmd = Command::new("multiple_values").arg(