From a2130bb68d93288ddfcca27b9c6aecd08884a48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20=F0=9F=91=A8=F0=9F=8F=BD=E2=80=8D=F0=9F=92=BB=20Copl?= =?UTF-8?q?an?= Date: Fri, 13 Jan 2023 13:52:34 -0800 Subject: [PATCH] =?UTF-8?q?Draft:=20=E2=9C=A8=20Add=20FD=5FCONFIG=5FPATH?= =?UTF-8?q?=20environment=20variable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Add FD_CONFIG_PATH environment variable which points to a file with config options that are prepended to any `fd` command. This has the same behavior as `RG_CONFIG_PATH` from ripgrep Test Plan: `cargo test` TODO: - add flag `--no-config` to not respect FD_CONFIG_PATH, `.fdignore`, or global - add documentation fd ignore - make parsing config file blazingly fast - if FD_CONFIG_PATH not set, or file does not exist, just use `args_os()` directly - fix `test_invalid_cwd` --- src/main.rs | 38 ++++++++++++++++++++++++++++++++++++-- tests/testenv/mod.rs | 8 ++++++++ tests/tests.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 567058c..0041cd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ use std::time; use anyhow::{anyhow, bail, Context, Result}; use atty::Stream; -use clap::{CommandFactory, Parser}; +use clap::{CommandFactory, FromArgMatches}; use globset::GlobBuilder; use lscolors::LsColors; use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder}; @@ -67,7 +67,7 @@ fn main() { } fn run() -> Result { - let opts = Opts::parse(); + let opts = parse_opts(); #[cfg(feature = "completions")] if let Some(shell) = opts.gen_completions()? { @@ -105,6 +105,40 @@ fn run() -> Result { walk::scan(&search_paths, Arc::new(regexps), Arc::new(config)) } +fn parse_opts() -> Opts { + let mut cli_args = std::env::args_os(); // [/usr/bin/fd, --hidden, --etc] + let mut args: Vec = args_from_config_file(); // [--no-require-git] + + // TODO(vegerot): If `args` is empty we can just skip the next two lines and not pay the cost + // of moving things around + args.insert(0, cli_args.next().unwrap()); // [/usr/bin/fd, --no-require-git] + + // this way, CLI arguments override config-file arguments + args.extend(cli_args.skip(1)); // [/usr/bin/fd, --no-require-git, --hidden, --etc] + + let mut matches = Opts::command().get_matches_from(args); + let res = + Opts::from_arg_matches_mut(&mut matches).map_err(|err| err.format(&mut Opts::command())); + + match res { + Ok(s) => s, + Err(e) => e.exit(), + } +} + +fn args_from_config_file() -> Vec { + let config_path = match std::env::var_os("FD_CONFIG_PATH") { + Some(path) => path, + None => return vec![], + }; + + // TODO(vegerot): 🚀 + match std::fs::read_to_string(config_path) { + Ok(config) => config.split_whitespace().map(|conf| conf.into()).collect(), + Err(_) => vec![], + } +} + #[cfg(feature = "completions")] #[cold] fn print_completions(shell: clap_complete::Shell) -> Result { diff --git a/tests/testenv/mod.rs b/tests/testenv/mod.rs index d4bfad2..8b8542a 100644 --- a/tests/testenv/mod.rs +++ b/tests/testenv/mod.rs @@ -161,6 +161,14 @@ impl TestEnv { } } + pub fn set_environment_variable(&self, key: &str, value: &str) { + env::set_var(key, value); + } + + pub fn remove_environment_variable(&self, key: &str) { + env::remove_var(key); + } + /// Create a broken symlink at the given path in the temp_dir. pub fn create_broken_symlink>( &mut self, diff --git a/tests/tests.rs b/tests/tests.rs index a2bc4d9..8efe758 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2500,3 +2500,32 @@ fn test_invalid_cwd() { panic!("{:?}", output); } } + +/// Read config file from FD_CONFIG_PATH environment variable if present +#[test] +#[cfg(unix)] +fn test_fd_config_path() { + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); + + te.set_environment_variable("FD_CONFIG_PATH", "fd.conf"); + + fs::File::create(te.test_root().join("fd.conf")) + .unwrap() + // test flags separated by spaces and newlines + .write_all( + b"--hidden --ignore-case + --no-ignore-vcs + --maxdepth=2", + ) + .unwrap(); + + te.assert_output( + &["foo"], + ".hidden.foo + a.foo + gitignored.foo + one/b.foo", + ); + + te.remove_environment_variable("FD_CONFIG_PATH"); +}