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

Issue:
`ArgMathces::args_present` behaviour with `ArgAction::SetTrue`
### 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

rustc 1.85.0-nightly

### Clap Version

4.5.23

### Minimal reproducible code

```rust
fn main() {
    let cli = Command::new("myapp").arg(
        Arg::new("show")
            .short('s')
            .long("show")
            .action(ArgAction::SetTrue),
    );
    let matches = cli.get_matches_from(["myapp"]);
    assert!(!matches.args_present());
}
```

This code panic with message
```
assertion failed: !matches.args_present()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

No args were provided, but `args_present()` returns true, which I think counterintuitive

### Steps to reproduce the bug with the above code

cargo run

### Actual Behaviour

`matches.args_present()` returns true

### Expected Behaviour

`matches.args_present()` returns false if no arguments were provided despite `Arg::new("show").action(ArgAction::SetTrue)`

### Additional Context

_No response_

### Debug Output

_No response_


Discussion / hints:
The documentation says "present on command line" but the behavior is if any ids are present, like `contains_id`
https://github.com/clap-rs/clap/blob/master/clap_builder%2Fsrc%2Fparser%2Fmatches%2Farg_matches.rs#L578

With `SetTrue`, `false` is a default which makes it so it contains the id.

The `value_source` is the defínitive way to know.

The question is whether we should change the docs or the behavior.
Well, personally, I think it's very important to be able to check whether some args were provided via command line or not. If we change the docs, the behavior of `args_present` remains the same. And if so, `args_present` is not useful for any CLI setup, which uses `action(ArgAction::SetTrue)`, because it will always return `true` regardless of whether args were provided or not. So my personal suggestion is that `args_present` should return presence of arguments despite any `Command` setup. 

I'm not very familiar with clap yet, so I'm sorry if I'm saying nonsense things
In #3382, we added `args_present()` as a mirror to `is_present(id)`.

In clap 3, flag's only set a value when passed in.  With #3786, the new `SetTrue` and `SetFalse` actions started setting defaults.   In these cases, we could no longer conflate "present in the structure" with "present on the command line".

In #3814, we renamed `is_present(id)` to `contains_id(id)` to better fit with the new actions.

We also have the [`IsPresent`](https://docs.rs/clap/latest/clap/builder/enum.ArgPredicate.html#variant.IsPresent) predicate which does check the `ValueSource`.
I see, thank you for the explanation. 

But I still deeply convinced that checking if "any arg presented on the command line" is important feature (at least for my purposes). [`IsPresent`](https://docs.rs/clap/latest/clap/builder/enum.ArgPredicate.html#variant.IsPresent) can be used for only one concrete arg, and if there was similar functionality for `ArgMathces`, that would be great and very useful
Note that wasn't necessarily a comment about what should be done for this issue but notes I took as I researched how we got here and how the term "present" is used.  This is important background for understanding where to go from here.

For instance
- The original intent was to communicate if arguments are present on the command-line which we are no longer upholding
- The term "present" in the name matches that original intent
- We've been in this situation for 2.5 years and no one has noticed until now
I think for my initial purpose to check whether any argument was provided on the command line or not the next code can be used:
```rust
// no args provided
if std::env::args() == 1 {
    // do some stuff ...
}
```

But the current doc of `args_presnt()` states:
- `Check if any args were present on the command line`

And I find it confusing, because it's not true it we use `Arg::new("somearg").action(ArgAction::SetTrue)`

So my suggestion is just change its documentation. For checking presence of arguments on the command line the above simple code can be used
> I think for my initial purpose to check whether any argument was provided on the command line or not the next code can be used:
> 
> // no args provided
> if std::env::args() == 1 {
>     // do some stuff ...
> }
>
> ... 
>
> So my suggestion is just change its documentation. For checking presence of arguments on the command line the above simple code can be used

That works for the root command but not any subcommands.

If we changed the documentation, we'd need to also deprecate and make a new name available because `present` has a specific meaning in clap.
Doing a quick browse on [github](https://github.com/search?q=args_present+language%3ARust&type=code&l=Rust)
- kou: [intended as documented](https://github.com/reinacchi/kou/blob/b00c3e700f453448a15a55c4b861b60c3d7293f7/src/commands.rs#L106) **with flags**, subject to this issue
- simple-ftp-client: [intended as documented](https://github.com/wiresock/simple-ftp-client/blob/237da6d151f2fdf393862bcab113df9c9be74a86/src/main.rs#L289) **with flags**, subject to this issue
- ~~erdtree: [intended as documented](https://github.com/fawni/erdtree/blob/9905785526bb42bc870a61d052b2d3739361b306/src/render/context/mod.rs#L136) **with flags**, subject to this issue~~ (out of date fork)
- solana-did-method: [unsure](https://github.com/hashblock/solana-did-method/blob/d84a6bf713839212d6acb9a2da91caa08cf3d082/soldid/src/clparse.rs#L139)
- Chads-Advent-of-Code: [unsure](https://github.com/ChadRogers0010/Chads-Advent-of-Code/blob/9b13ebdda2aa9fbe52f29352258b49719b05da3d/rust-cli/src/main/main.rs#L16)
- zk-avail-da/da-sender: [unsure](https://github.com/lambdaclass/zk-avail-da/blob/697776aa7c01e36e35d9e9cc8d206bd39ebe793f/da-sender/src/main.rs#L32)
- kmerutils: [assumes subcommands are checked](https://github.com/jean-pierreBoth/kmerutils/blob/f04e9eafbeb28842221eada821fba2cb454e567a/src/bin/parsefastq.rs#L178) (shouldn't this be broken?
- my_rust: [intended as documented](https://github.com/nestorwheelock/my_rust/blob/748dc07258699ac6ab9a8dd064a4b2e6d02ca2e1/src/main.rs#L174) and with what is intended to be a flag but a bug is in the code otherwise they'd run into this issue
- Rust-CLI/unique: [intention is unclear](https://github.com/uninumber/Rust-CLI/blob/acb9a7c90a3c8a6c5c4e8f36e6eb41f29a3bce0b/uniq/src/uniq.rs#L44)
- kaivm: [intended as documented](https://github.com/agallardol/kaivm/blob/61f6e02edda3fca71422ac13a0fd163bd1d23628/src/command_handlers/env.rs#L17) but no flags used
- beancount-language: [intended as documented](https://github.com/polarmutex/beancount-language-server/blob/2582df1b303600f742e079c686064ce30ca49341/crates/lsp/src/main.rs#L18), with a flag, but in a way that this issue doesn't change behavior
- BlinkSync: [intended as documented, I think?](https://github.com/Vernoxvernax/BlinkSync/blob/913a39a5f04995ecc1527b050052fb022df3a045/src/main.rs#L170) but the shirt-circuit is dead because of required arguments
- lotr: [intended as documented](https://github.com/TomaTechnology/lotr/blob/0e8a6609a0d6a2583e55314f122c4c3aa9e51958/src/main.rs#L374) with no flags
- MovieChaptersPLS: [intent unclear](https://github.com/Vernoxvernax/MovieChaptersPLS/blob/7701fef47ce124a024880a4b2c83183d269dd630/src/main.rs#L86), used with flags and will panic if false but some arguments are required making it so the panic shouldn't be hit
- bat: on `cache` subcommand, [intended as documented](https://github.com/sharkdp/bat/blob/94d3dc254edb2e3364dd11af0acd0c70e7648a30/src/bin/bat/main.rs#L371) but its dead code because [some arguments are required](https://github.com/sharkdp/bat/blob/94d3dc254edb2e3364dd11af0acd0c70e7648a30/src/bin/bat/clap_app.rs#L680-L684)
- rosu-tracker: [intended as documented](https://github.com/Chiffario/rosu-tracker/blob/9b6c241bdd010624425681b4d0c316c14257c0c1/src/main.rs#L65) but no flags
- nisq: [intended as documented](https://github.com/NoHaxito/nisq/blob/f00d0e4a6ec3124b84a8fa298d80cf8f206361bd/src/main.rs#L28), no flags
- cim: [intended as documented](https://github.com/feraxhp/cim/blob/4f011846aac5f0771f7d26a5426b2502acc05f18/src/main.rs#L23), no flags
- mdbook-embeddify: **[unclear intent](https://github.com/MR-Addict/mdbook-embedify/blob/449cac73cdcbe8b55fd957bc4b0e34ec62dbee37/src/cli.rs#L24)**, maybe thinking it includes subcommands, like `args_required_else_help`?
- cxl-fabric-amanger: [dead code](https://github.com/computexpresslink/cxl-fabric-manager/blob/cff5d531079ee682129b89d9e0f966d4c74a6f53/fm_cli/src/multi_headed_device.rs#L37)
- qpass: [intended as documented](https://github.com/mdmmn378/qpass/blob/74b88f639c54bd004362178f407e3f5f6cc4ecdc/src/main.rs#L59), I think?  No flags
- kvs: [intended as documented](https://github.com/jporwal05/kvs/blob/f1f7b03e1c01fafde875cd9467e6ca53e7ba937c/src/bin/kvs.rs#L12), no flags
- RustLeetCode!Q: [unsure](https://github.com/egehandogan35/RustLeetCodeQ/blob/88de7084a383ccb087cf534db69e4ee2b8537dfe/src/fileHash.rs#L24) but its redundant
- wordle_yvaniak: [unsure](https://github.com/Yvaniak/wordle_yvaniak/blob/843f26074fa482b6230fcc6f5f7d751421ff2852/src/config.rs#L54) but redundant
- grp: [intended as documented](https://github.com/feraxhp/grp/blob/9bba86c8d9b3fffb4742adfcd49e177ee9e1510c/src/main.rs#L50) but redundant

Fix patch:
diff --git a/clap_builder/src/parser/matches/arg_matches.rs b/clap_builder/src/parser/matches/arg_matches.rs
index 5e4d9dcd48e..78e6c758a2b 100644
--- a/clap_builder/src/parser/matches/arg_matches.rs
+++ b/clap_builder/src/parser/matches/arg_matches.rs
@@ -554,7 +554,10 @@ impl ArgMatches {
         }
     }
 
-    /// Check if any args were present on the command line
+    /// Check if any [`Arg`][crate::Arg]s were present on the command line
+    ///
+    /// See [`ArgMatches::subcommand_name()`] or [`ArgMatches::subcommand()`] to check if a
+    /// subcommand was present on the command line.
     ///
     /// # Examples
     ///
@@ -575,7 +578,9 @@ impl ArgMatches {
     ///     .unwrap();
     /// assert!(! m.args_present());
     pub fn args_present(&self) -> bool {
-        !self.args.is_empty()
+        self.args
+            .values()
+            .any(|v| v.source().map(|s| s.is_explicit()).unwrap_or(false))
     }
 
     /// Report where argument value came from
diff --git a/clap_builder/src/util/flat_map.rs b/clap_builder/src/util/flat_map.rs
index d739b922211..3dce096ef26 100644
--- a/clap_builder/src/util/flat_map.rs
+++ b/clap_builder/src/util/flat_map.rs
@@ -118,6 +118,10 @@ impl<K: PartialEq + Eq, V> FlatMap<K, V> {
         self.keys.iter()
     }
 
+    pub(crate) fn values(&self) -> std::slice::Iter<'_, V> {
+        self.values.iter()
+    }
+
     pub(crate) fn iter(&self) -> Iter<'_, K, V> {
         Iter {
             keys: self.keys.iter(),


Test patch:
diff --git a/tests/builder/arg_matches.rs b/tests/builder/arg_matches.rs
index c6d16df0eea..cfbb0731453 100644
--- a/tests/builder/arg_matches.rs
+++ b/tests/builder/arg_matches.rs
@@ -1,5 +1,4 @@
 use clap::{arg, value_parser, Command};
-#[cfg(debug_assertions)]
 use clap::{Arg, ArgAction};
 
 #[test]
@@ -83,3 +82,36 @@ fn arg_matches_subcommand_matches_wrong_sub() {
     assert!(m.subcommand_matches("speed").is_some());
     m.subcommand_matches("seed");
 }
+
+#[test]
+fn args_present_positional() {
+    let c = Command::new("test").arg(Arg::new("positional"));
+
+    let m = c.clone().try_get_matches_from(["test"]).unwrap();
+    assert!(!m.args_present());
+
+    let m = c.clone().try_get_matches_from(["test", "value"]).unwrap();
+    assert!(m.args_present());
+}
+
+#[test]
+fn args_present_flag() {
+    let c = Command::new("test").arg(Arg::new("flag").long("flag").action(ArgAction::SetTrue));
+
+    let m = c.clone().try_get_matches_from(["test"]).unwrap();
+    assert!(!m.args_present());
+
+    let m = c.clone().try_get_matches_from(["test", "--flag"]).unwrap();
+    assert!(m.args_present());
+}
+
+#[test]
+fn args_present_subcommand() {
+    let c = Command::new("test").subcommand(Command::new("sub"));
+
+    let m = c.clone().try_get_matches_from(["test"]).unwrap();
+    assert!(!m.args_present());
+
+    let m = c.clone().try_get_matches_from(["test", "sub"]).unwrap();
+    assert!(!m.args_present());
+}