Repository: sharkdp/fd (language: rust)

Issue:
Opposing/negating command-line options (eg. --no-hidden)
Background
-------------
First off, let me just say that I love your projects, `bat` and `fd` are great and I use them both regularly.

What I was looking for was something similar to the `--no-hidden` option as it exists in ripgrep. As in fd, in ripgrep hidden files are not searched by default. However with ripgrep I can create a global config that effectively changes it's behavior to search hidden files by default, and then still be able to pass a `--no-hidden` option in order to turn it back off on a case by case basis when desired. The logic that parses the command-line options would simply let subsequent opposing command-line options override prior ones. When I was looking around to see if this was possible, I found https://github.com/sharkdp/fd/issues/362 where a global config file that was previously proposed, and I agree that aliases adequately satisfy the same use case.

Proposal
----------
If so-called "opposing" command-line options such as `--no-hidden` were supported. Then it would be possible to define aliases such as

```bash
alias fd='fd --hidden'
```
which would effectively change the default options to search hidden files, and then if you wanted to ignore hidden files you could run something like:

```bash
fd --no-hidden
```

Similarly other "opposing" command-line options could be added where applicable, for example the currently supported `--no-ignore` could be complemented with a new `--ignore` option. 

Just briefly looking at the currently supported options, here are some of the ones I feel could possibly be useful:

Currently Supported Option  | Potential "Opposing" Option                
:---:                                          | :---:                                   
--hidden                                  | --no-hidden    
--no-ignore                             | --ignore
--no-ignore-vcs                     | --ignore-vcs
--follow                                    | --no-follow
--fixed-strings                        | --no-fixed-strings
--show-errors                         | --no-show-errors
--absolute-path                     | --relative-path
--full-path                               | --no-full-path


Note that I haven't tested it, but for some currently supported options, this may or may not already be possible:

Currently Supported Option  | "Opposing" Supported Option       
:---:                                          | :---:                                   
--case-sensitive                     | --ignore-case
--glob foo*                              | --exclude foo*

How do you feel about this? If you would be interested in supporting anything like this in fd that would be awesome. If it helped, I could offer to write an initial implementation. I do have a full-time day job so depending on how involved it ended up being I couldn't guarantee how quickly I could finish it, but I would definitely find some time to do it sooner or later. I would expect that the necessary changes would be mostly, if not fully contained within the command-line parsing logic, but I haven't actually looked into it so you never know.


Discussion / hints:
Thank you for the feedback and for the detailed feature request. I agree with all points. To keep the `--help` text from exploding, we could maybe hide most/all of these negating command-line options and instead mention them in the respective "positive" option help text?

> --glob foo* 	--exclude foo*

`--exclude` is not really the opposite of `--glob`. Both can be used at the same time:
```bash
fd --exclude '*.bkp' --glob 'example*'
```

However, `--glob` is kind of opposite to `--regex` (which is the default). The search pattern can either be interpreted as a regex or a glob. So users can already do this:
```
alias fd="fd --glob"

fd --regex '^example'
```
`--exclude` can be used with both `--glob` and `--regex`.
May be another possibility is to have `true` and `false` values for all boolean switches, with `true` as default. 

For example `--hidden` would be the same as `--hidden=true` and putting `--hidden=false` will negate a previous `--hidden` flag.  

In this way you could use variable as values, like `--hidden=$1`.
I think we could start with these:

Currently Supported Option  | "Opposing" Option                
:---:                                          | :---:                                   
--hidden                                  | --no-hidden    
--no-ignore                             | --ignore
--no-ignore-vcs                     | --ignore-vcs
--follow                                    | --no-follow
--absolute-path                     | --relative-path

> May be another possibility is to have `true` and `false` values for all boolean switches, with `true` as default.
> 
> For example `--hidden` would be the same as `--hidden=true` and putting `--hidden=false` will negate a previous `--hidden` flag.

I don't think that's possible without breaking changes. If we accept `--hidden=true/false`, we also need to accept `--hidden true/false`. This is due to the way `clap` (argument parser) works. And it's also a good policy in general, IMO. But this would mean that we can not make the `true` argument the default. Because that would result in ambiguities:
```bash
fd --hidden false # searching for a file called `false` or searching
                  # without pattern and `hidden=false`?
```
I'd like to have a go at tackling this issue if no one else has started already; sounds okay?

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d016699f8..bb6f8e0e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@
 
 - Add new `--batch-size` flag, see #410 (@devonhollowood)
 
+- Add opposing command-line options, see #595 (@Asha20)
+
 ## Bugfixes
 
 - Set default path separator to `/` in MSYS, see #537 and #730 (@aswild)
diff --git a/Cargo.lock b/Cargo.lock
index c3bfee79b..5c4918b84 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -181,6 +181,7 @@ dependencies = [
  "regex",
  "regex-syntax",
  "tempdir",
+ "test-case",
  "users",
  "version_check",
 ]
@@ -395,6 +396,24 @@ version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
 
+[[package]]
+name = "proc-macro2"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+dependencies = [
+ "proc-macro2",
+]
+
 [[package]]
 name = "rand"
 version = "0.4.6"
@@ -492,6 +511,17 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
 
+[[package]]
+name = "syn"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
 [[package]]
 name = "tempdir"
 version = "0.3.7"
@@ -512,6 +542,19 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "test-case"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7cad0a06f9a61e94355aa3b3dc92d85ab9c83406722b1ca5e918d4297c12c23"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
 [[package]]
 name = "textwrap"
 version = "0.11.0"
@@ -547,6 +590,12 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
 
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
 [[package]]
 name = "users"
 version = "0.11.0"
diff --git a/Cargo.toml b/Cargo.toml
index c71509137..c89b2e97a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -70,7 +70,8 @@ jemallocator = "0.3.0"
 [dev-dependencies]
 diff = "0.1"
 tempdir = "0.3"
-filetime = "0.2.15"
+filetime = "0.2"
+test-case = "1.2"
 
 [profile.release]
 lto = true
diff --git a/doc/fd.1 b/doc/fd.1
index 66413cc3d..51d0c034a 100644
--- a/doc/fd.1
+++ b/doc/fd.1
@@ -28,7 +28,7 @@ is a simple, fast and user-friendly alternative to
 .TP
 .B \-H, \-\-hidden
 Include hidden files and directories in the search results
-(default: hidden files and directories are skipped).
+(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'.
 .TP
 .B \-I, \-\-no\-ignore
 Show search results from files and directories that would otherwise be ignored by
@@ -49,6 +49,8 @@ The global fd ignore file (usually
 .I $HOME/.config/fd/ignore
 )
 .RE
+.IP
+The flag can be overridden with '--ignore'.
 .TP
 .B \-u, \-\-unrestricted
 Alias for '--no-ignore'. Can be repeated; '-uu' is an alias for '--no-ignore --hidden'.
@@ -62,6 +64,7 @@ and the global gitignore configuration
 .RI ( core.excludesFile
 git setting, which defaults to
 .IR $HOME/.config/git/ignore ).
+The flag can be overridden with '--ignore-vcs'.
 .TP
 .B \-s, \-\-case\-sensitive
 Perform a case-sensitive search. By default, fd uses case-insensitive searches, unless the
@@ -84,6 +87,7 @@ performs substring comparison. If you want to match on an exact filename, consid
 .TP
 .B \-a, \-\-absolute\-path
 Shows the full path starting from the root as opposed to relative paths.
+The flag can be overridden with '--relative-path'.
 .TP
 .B \-l, \-\-list\-details
 Use a detailed listing format like 'ls -l'. This is basically an alias
@@ -93,7 +97,7 @@ sort order.
 .TP
 .B \-L, \-\-follow
 By default, fd does not descend into symlinked directories. Using this flag, symbolic links are
-also traversed.
+also traversed. The flag can be overridden with '--no-follow'.
 .TP
 .B \-p, \-\-full\-path
 By default, the search pattern is only matched against the filename (or directory name). Using
diff --git a/src/app.rs b/src/app.rs
index efd11d766..4c17dca16 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -25,7 +25,17 @@ pub fn build_app() -> App<'static, 'static> {
                 .long_help(
                     "Include hidden directories and files in the search results (default: \
                          hidden files and directories are skipped). Files and directories are \
-                         considered to be hidden if their name starts with a `.` sign (dot).",
+                         considered to be hidden if their name starts with a `.` sign (dot). \
+                         The flag can be overridden with --no-hidden.",
+                ),
+        )
+        .arg(
+            Arg::with_name("no-hidden")
+                .long("no-hidden")
+                .overrides_with("hidden")
+                .hidden(true)
+                .long_help(
+                    "Overrides --hidden.",
                 ),
         )
         .arg(
@@ -36,7 +46,17 @@ pub fn build_app() -> App<'static, 'static> {
                 .help("Do not respect .(git|fd)ignore files")
                 .long_help(
                     "Show search results from files and directories that would otherwise be \
-                         ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file.",
+                         ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file. \
+                         The flag can be overridden with --ignore.",
+                ),
+        )
+        .arg(
+            Arg::with_name("ignore")
+                .long("ignore")
+                .overrides_with("no-ignore")
+                .hidden(true)
+                .long_help(
+                    "Overrides --no-ignore.",
                 ),
         )
         .arg(
@@ -47,7 +67,16 @@ pub fn build_app() -> App<'static, 'static> {
                 .help("Do not respect .gitignore files")
                 .long_help(
                     "Show search results from files and directories that would otherwise be \
-                         ignored by '.gitignore' files.",
+                         ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs.",
+                ),
+        )
+        .arg(
+            Arg::with_name("ignore-vcs")
+                .long("ignore-vcs")
+                .overrides_with("no-ignore-vcs")
+                .hidden(true)
+                .long_help(
+                    "Overrides --no-ignore-vcs.",
                 ),
         )
         .arg(
@@ -72,6 +101,7 @@ pub fn build_app() -> App<'static, 'static> {
             Arg::with_name("rg-alias-hidden-ignore")
                 .short("u")
                 .long("unrestricted")
+                .overrides_with_all(&["ignore", "no-hidden"])
                 .multiple(true)
                 .hidden_short_help(true)
                 .help("Alias for '--no-ignore', and '--hidden' when given twice")
@@ -145,7 +175,17 @@ pub fn build_app() -> App<'static, 'static> {
                 .overrides_with("absolute-path")
                 .help("Show absolute instead of relative paths")
                 .long_help(
-                    "Shows the full path starting from the root as opposed to relative paths.",
+                    "Shows the full path starting from the root as opposed to relative paths. \
+                     The flag can be overridden with --relative-path.",
+                ),
+        )
+        .arg(
+            Arg::with_name("relative-path")
+                .long("relative-path")
+                .overrides_with("absolute-path")
+                .hidden(true)
+                .long_help(
+                    "Overrides --absolute-path.",
                 ),
         )
         .arg(
@@ -170,7 +210,17 @@ pub fn build_app() -> App<'static, 'static> {
                 .help("Follow symbolic links")
                 .long_help(
                     "By default, fd does not descend into symlinked directories. Using this \
-                         flag, symbolic links are also traversed.",
+                         flag, symbolic links are also traversed. \
+                         Flag can be overriden with --no-follow.",
+                ),
+        )
+        .arg(
+            Arg::with_name("no-follow")
+                .long("no-follow")
+                .overrides_with("follow")
+                .hidden(true)
+                .long_help(
+                    "Overrides --follow.",
                 ),
         )
         .arg(


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index 1baf15ed6..54b4ecce4 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -4,6 +4,7 @@ use std::fs;
 use std::io::Write;
 use std::path::Path;
 use std::time::{Duration, SystemTime};
+use test_case::test_case;
 
 use normpath::PathExt;
 use regex::escape;
@@ -1929,6 +1930,30 @@ fn test_number_parsing_errors() {
     te.assert_failure(&["--max-results=a"]);
 }
 
+#[test_case("--hidden", &["--no-hidden"] ; "hidden")]
+#[test_case("--no-ignore", &["--ignore"] ; "no-ignore")]
+#[test_case("--no-ignore-vcs", &["--ignore-vcs"] ; "no-ignore-vcs")]
+#[test_case("--follow", &["--no-follow"] ; "follow")]
+#[test_case("--absolute-path", &["--relative-path"] ; "absolute-path")]
+#[test_case("-u", &["--ignore"] ; "u")]
+#[test_case("-uu", &["--ignore", "--no-hidden"] ; "uu")]
+fn test_opposing(flag: &str, opposing_flags: &[&str]) {
+    let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
+
+    let mut flags = vec![flag];
+    flags.extend_from_slice(opposing_flags);
+    let out_no_flags = te.assert_success_and_get_output(".", &[]);
+    let out_opposing_flags = te.assert_success_and_get_output(".", &flags);
+
+    assert_eq!(
+        out_no_flags,
+        out_opposing_flags,
+        "{} should override {}",
+        opposing_flags.join(" "),
+        flag
+    );
+}
+
 /// Print error if search pattern starts with a dot and --hidden is not set
 /// (Unix only, hidden files on Windows work differently)
 #[test]