├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST ├── MANIFEST.in ├── Makefile ├── README.md ├── RELEASE_NOTES.md ├── build.py ├── codecov.yml ├── docs ├── ALL_ARGUMENTS.md └── ENV_VARIABLES.md ├── ffind.gif ├── man_pages ├── README ├── ffind.1 ├── ffind.html └── install.sh ├── pyproject.toml ├── setup.cfg ├── src └── ffind │ ├── __init__.py │ └── ffind.py └── tests ├── README ├── basic.t ├── caps.t ├── case_sensitive.t ├── command.t ├── delete.t ├── execute.t ├── fuzzy.t ├── hidden.t ├── ignore_vcs.t ├── module.t ├── path.t ├── regex.t ├── regex2.t ├── return_results.t ├── setup.sh └── wrong_pattern.t /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | tags: 10 | - v* 11 | pull_request: 12 | branches: [ master ] 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9", "pypy-3.10"] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | python -m pip install pyflakes cram python-coveralls codecov build hatchling 33 | - name: Get package version 34 | id: get_version 35 | run: | 36 | VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') 37 | echo "version=$VERSION" >> $GITHUB_OUTPUT 38 | - name: Lint with pyflakes 39 | run: | 40 | pyflakes *.py src/ffind/*.py 41 | - name: Build package 42 | run: | 43 | hatchling build 44 | - name: Install wheel 45 | run: | 46 | pip install dist/ffind-${{ steps.get_version.outputs.version }}-py3-none-any.whl 47 | - name: Run ffind with pyflakes 48 | run: | 49 | ffind --ignore-vcs . '(? man_pages/ffind.1 4 | md2man-html README.md > man_pages/ffind.html 5 | 6 | There is not a easy, cross-platform way to install man pages through the distutils. 7 | 8 | The install script copies the man file to the first directory on manpath 9 | To check the MANPATH, use man --path or manpath command 10 | 11 | TODO: Render the man files automatically 12 | -------------------------------------------------------------------------------- /man_pages/ffind.1: -------------------------------------------------------------------------------- 1 | .TH ffind v1.6.1 \- A sane replacement for command line file search 2 | .PP 3 | \fIInfo:\fP An utility to search files recursively on a dir. 4 | .PP 5 | \fIAuthor:\fP Jaime Buelta 6 | .PP 7 | [Build Status](https://travis\-ci.org/jaimebuelta/ffind.svg?branch=master) \[la]https://travis-ci.org/jaimebuelta/ffind\[ra] 8 | [Coverage Status](https://coveralls.io/repos/github/jaimebuelta/ffind/badge.svg?branch=master) \[la]https://coveralls.io/github/jaimebuelta/ffind?branch=master\[ra] 9 | [Requirements Status](https://requires.io/github/jaimebuelta/ffind/requirements.svg?branch=master) \[la]https://requires.io/github/jaimebuelta/ffind/requirements/?branch=master\[ra] 10 | [PyPI version](https://badge.fury.io/py/ffind.svg) \[la]https://badge.fury.io/py/ffind\[ra] 11 | [codecov](https://codecov.io/gh/jaimebuelta/ffind/branch/master/graph/badge.svg) \[la]https://codecov.io/gh/jaimebuelta/ffind\[ra] 12 | .SH About 13 | .PP 14 | It allows quick and easy recursive search for files in the Unix command line. 15 | .PP 16 | [Demo](\[la]https://github.com/jaimebuelta/ffind/blob/master/ffind.gif\[ra]) 17 | .PP 18 | Basically, replaces \fB\fCfind . \-name '*FILE_PATTERN*'\fR with \fB\fCffind.py FILE_PATTERN\fR (and a few more niceties) 19 | .RS 20 | .IP \(bu 2 21 | Input filename may be a full regex 22 | .IP \(bu 2 23 | Search recursively on current directory by default. 24 | .IP \(bu 2 25 | If the FILE_PATTERN is all in lowercase, the search will be case insensitive, unless a flag is set. 26 | .IP \(bu 2 27 | Regex can affect only the filename (default) or the full path. 28 | .IP \(bu 2 29 | Will print colorized output in glamorous red (default), except on redirected output. 30 | .IP \(bu 2 31 | Ignores hidden directories and files (starting with \fB\fC\&.\fR) by default 32 | .IP \(bu 2 33 | Can ignore source control common directories and files, like \fB\fC\&.gitignore\fR or \fB\fCRCS/\fR\&. Typically not needed as hidden 34 | are ignored by default. 35 | .IP \(bu 2 36 | Follow symlinks by default, but that can be deactivated if necessary to avoid recursion problems 37 | .IP \(bu 2 38 | Works in python2.7 and python3. 39 | .IP \(bu 2 40 | Can delete matched files 41 | .IP \(bu 2 42 | Can execute a command on matched files 43 | .IP \(bu 2 44 | Experimental fuzzy search 45 | .RE 46 | .PP 47 | Common uses: 48 | .RS 49 | .IP \(bu 2 50 | \fB\fCffind txt\fR to return all text files in current dir 51 | .IP \(bu 2 52 | \fB\fCffind ../other_dir txt\fR to return all text files under dir ../other\fIdir (or `ffind.py txt \-d ../other\fPdir`) 53 | .RE 54 | .SH Install 55 | .PP 56 | Requires pip \[la]https://pip.pypa.io/en/stable/installing/\[ra], the tool for installing Python packages. 57 | .PP 58 | .RS 59 | .nf 60 | pip install ffind 61 | .fi 62 | .RE 63 | .SH All options 64 | .PP 65 | .RS 66 | .nf 67 | usage: ffind.py [\-h] [\-p] [\-\-nocolor] [\-\-nosymlinks] [\-\-hidden] [\-c] [\-i] 68 | [\-\-delete | \-\-exec "command" | \-\-module "module_name args" | \-\-command "program"] 69 | [\-\-ignore\-vcs] [\-f] [\-\-version] 70 | [dir] filepattern 71 | 72 | Search file name in directory tree 73 | 74 | positional arguments: 75 | dir Directory to search 76 | filepattern 77 | 78 | optional arguments: 79 | \-h, \-\-help show this help message and exit 80 | \-p Match whole path, not only name of files. Set env 81 | variable FFIND_SEARCH_PATH to set this automatically 82 | \-\-nocolor Do not display color. Set env variable FFIND_NO_COLOR 83 | to set this automatically 84 | \-\-nosymlinks Do not follow symlinks (following symlinks can lead to 85 | infinite recursion) Set env variable FFIND_NO_SYMLINK 86 | to set this automatically 87 | \-\-hidden Do not ignore hidden directories and files. Set env 88 | variable FFIND_SEARCH_HIDDEN to set this automatically 89 | \-c Force case sensitive. By default, all lowercase 90 | patterns are case insensitive. Set env variable 91 | FFIND_CASE_SENSITIVE to set this automatically 92 | \-i Force case insensitive. This allows case insensitive 93 | for patterns with uppercase. If both \-i and \-c are 94 | set, the search will be case sensitive.Set env 95 | variable FFIND_CASE_INSENSITIVE to set this 96 | automatically 97 | \-\-delete Delete files found 98 | \-\-exec "command" Execute the given command with the file found as 99 | argument. The string '{}' will be replaced with the 100 | current file name being processed. If this option is 101 | used, ffind will return a status code of 0 if all the 102 | executions return 0, and 1 otherwise 103 | \-\-module "module_name args" 104 | Execute the given module with the file found as 105 | argument. The string '{}' will be replaced with the 106 | current file name being processed. If this option is 107 | used, ffind will return a status code of 0 if all the 108 | executions return 0, and 1 otherwise. Only SystemExit 109 | is caught 110 | \-\-command "program" Execute the given python program with the file found 111 | placed in local variable 'filename'. If this option is 112 | used, ffind will return a status code of 1 if any 113 | exceptions occur, and 0 otherwise. SystemExit is not 114 | caught 115 | \-\-ignore\-vcs Ignore version control system files and directories. 116 | Set env variable FFIND_IGNORE_VCS to set this 117 | automatically 118 | \-f Experimental fuzzy search. Increases the matches, use 119 | with care. Combining it with regex may give crazy 120 | results 121 | \-\-return\-results For testing purposes only. Please ignore 122 | \-\-version show program's version number and exit 123 | .fi 124 | .RE 125 | .SH Environment variables 126 | .PP 127 | Setting these environment variables, you'll set options by default. For example: 128 | .PP 129 | .RS 130 | .nf 131 | export FFIND_CASE_SENSITIVE=1 132 | # equivalent to ffind \-c something 133 | ffind something 134 | FFIND_CASE_SENSITIVE=1 ffind something 135 | .fi 136 | .RE 137 | .RS 138 | .IP \(bu 2 139 | FFIND_SORT: Return the results sorted. This is slower, and is mainly thought to ensure 140 | consistency on the tests, as some filesystems may order files differently 141 | .IP \(bu 2 142 | FFIND\fICASE\fPSENSITIVE: Search is case sensitive. Equivalent to \fB\fC\-c\fR flag 143 | .IP \(bu 2 144 | FFIND\fICASE\fPINSENSITIVE: Search is case insensitive. Equivalent to \fB\fC\-i\fR flag. 145 | .IP \(bu 2 146 | FFIND\fISEARCH\fPHIDDEN: Search in hidden directories and files. Equivalent to \fB\fC\-\-hidden\fR flag. 147 | .IP \(bu 2 148 | FFIND\fISEARCH\fPPATH: Search in the whole path. Equivalent to \fB\fC\-p\fR flag. 149 | .IP \(bu 2 150 | FFIND\fIIGNORE\fPVCS: Ignore paths in version control. Equivalent to \fB\fC\-\-ignore\-vcs\fR 151 | .IP \(bu 2 152 | FFIND\fINO\fPSYMLINK: Do not follow symlinks. Equivalent to \fB\fC\-\-nosymlinks\fR flag. 153 | .IP \(bu 2 154 | FFIND\fINO\fPCOLOR: Do not show colors. Equivalent to \fB\fC\-\-nocolor\fR flag. 155 | .IP \(bu 2 156 | FFIND\fIFUZZY\fPSEARCH: Enable fuzzy search. Equivalent to \fB\fC\-f\fR flag. 157 | .RE 158 | .PP 159 | If an environment variable is present, when calling \fB\fCffind \-h\fR, the option will display [SET] at the end. 160 | .SH Manual Install 161 | .PP 162 | From the source code directory 163 | \fB\fC 164 | python setup.py install 165 | \fR 166 | .SH Test 167 | .PP 168 | It requires to install cram \[la]https://bitheap.org/cram/\[ra] (it can be installed with \fB\fCpip install cram\fR) 169 | .PP 170 | To run all the tests, run \fB\fCmake test\fR\&. This runs the tests on both Python 2 and Python 3. Running just 171 | \fB\fCmake\fR runs the test for Python 3. 172 | .PP 173 | The tests are under the \fB\fCtests\fR directory, more tests are welcome. 174 | -------------------------------------------------------------------------------- /man_pages/ffind.html: -------------------------------------------------------------------------------- 1 |

ffind v1.4.1 - A sane replacement for command line file search

Info: An utility to search files recursively on a dir.

Author: Jaime Buelta

Build Status 2 | Coverage Status 3 | Requirements Status 4 | PyPI version 5 | codecov

About

It allows quick and easy recursive search for files in the Unix command line.

Demo

Basically, replaces find . -name '*FILE_PATTERN*' with ffind.py FILE_PATTERN (and a few more niceties)

6 | 20 |

Common uses:

21 | 25 |

Install

Requires pip, the tool for installing Python packages.

pip install ffind
 26 | 
27 |

All options

usage: ffind.py [-h] [-p] [--nocolor] [--nosymlinks] [--hidden] [-c]  [-i]
 28 |             [--delete | --exec "command" | --module "module_name args" | --command "program"]
 29 |             [--ignore-vcs] [-f] [--version]
 30 |             [dir] filepattern
 31 | 
 32 | Search file name in directory tree
 33 | 
 34 | positional arguments:
 35 |   dir                   Directory to search
 36 |   filepattern
 37 | 
 38 | optional arguments:
 39 |   -h, --help            show this help message and exit
 40 |   -p                    Match whole path, not only name of files. Set env
 41 |                         variable FFIND_SEARCH_PATH to set this automatically
 42 |   --nocolor             Do not display color. Set env variable FFIND_NO_COLOR
 43 |                         to set this automatically
 44 |   --nosymlinks          Do not follow symlinks (following symlinks can lead to
 45 |                         infinite recursion) Set env variable FFIND_NO_SYMLINK
 46 |                         to set this automatically
 47 |   --hidden              Do not ignore hidden directories and files. Set env
 48 |                         variable FFIND_SEARCH_HIDDEN to set this automatically
 49 |   -c                    Force case sensitive. By default, all lowercase
 50 |                         patterns are case insensitive. Set env variable
 51 |                         FFIND_CASE_SENSITIVE to set this automatically
 52 |   -i                    Force case insensitive. This allows case insensitive
 53 |                         for patterns with uppercase. If both -i and -c are
 54 |                         set, the search will be case sensitive.Set env
 55 |                         variable FFIND_CASE_INSENSITIVE to set this
 56 |                         automatically
 57 |   --delete              Delete files found
 58 |   --exec "command"      Execute the given command with the file found as
 59 |                         argument. The string '{}' will be replaced with the
 60 |                         current file name being processed. If this option is
 61 |                         used, ffind will return a status code of 0 if all the
 62 |                         executions return 0, and 1 otherwise
 63 |   --module "module_name args"
 64 |                         Execute the given module with the file found as
 65 |                         argument. The string '{}' will be replaced with the
 66 |                         current file name being processed. If this option is
 67 |                         used, ffind will return a status code of 0 if all the
 68 |                         executions return 0, and 1 otherwise. Only SystemExit
 69 |                         is caught
 70 |   --command "program"   Execute the given python program with the file found
 71 |                         placed in local variable 'filename'. If this option is
 72 |                         used, ffind will return a status code of 1 if any
 73 |                         exceptions occur, and 0 otherwise. SystemExit is not
 74 |                         caught
 75 |   --ignore-vcs          Ignore version control system files and directories.
 76 |                         Set env variable FFIND_IGNORE_VCS to set this
 77 |                         automatically
 78 |   -f                    Experimental fuzzy search. Increases the matches, use
 79 |                         with care. Combining it with regex may give crazy
 80 |                         results
 81 |   --return-results      For testing purposes only. Please ignore
 82 |   --version             show program's version number and exit
 83 | 
84 |

Environment variables

Setting these environment variables, you'll set options by default. For example:

export FFIND_CASE_SENSITIVE=1
 85 | # equivalent to ffind -c something
 86 | ffind something 
 87 | FFIND_CASE_SENSITIVE=1 ffind something
 88 | 
89 | 90 | 102 |

If an environment variable is present, when calling ffind -h, the option will display [SET] at the end.

Manual Install

From the source code directory 103 | 104 | python setup.py install 105 |

Test

It requires to install cram (it can be installed with pip install cram)

To run all the tests, run make test. This runs the tests on both Python 2 and Python 3. Running just 106 | make runs the test for Python 3.

The tests are under the tests directory, more tests are welcome.

107 | -------------------------------------------------------------------------------- /man_pages/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Set the first element of the manpath 4 | MAN_PREFIX=`manpath | tr ":" "\n" | head -1` 5 | 6 | for i in *.1 ; do 7 | section=`echo ${i}|sed 's/.*\([0-9]\).*/\1/'` 8 | #echo ${section} 9 | target="${MAN_PREFIX}/man${section}" 10 | # check if man directory exists 11 | if ! test -d ${target}; then 12 | mkdir -p ${target} 13 | fi 14 | echo "${i} --> ${target}" 15 | install -m 444 ${i} ${target} 16 | done 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "ffind" 7 | version = "1.6.1" 8 | description = "Sane replacement for command line file search" 9 | readme = "README.md" 10 | requires-python = ">=3.9,<4" 11 | license = "MIT" 12 | authors = [ 13 | { name = "Jaime Buelta", email = "jaime.buelta@gmail.com" } 14 | ] 15 | keywords = ["searching", "file system"] 16 | classifiers = [ 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | ] 24 | 25 | [project.urls] 26 | Homepage = "https://github.com/jaimebuelta/ffind" 27 | Download = "https://github.com/jaimebuelta/ffind/tarball/v1.6.1" 28 | 29 | [project.scripts] 30 | ffind = "ffind.ffind:run" 31 | 32 | [tool.hatch.build] 33 | artifacts = [ 34 | "man_pages/ffind.1", 35 | ] 36 | 37 | [tool.hatch.build.targets.wheel] 38 | packages = ["src/ffind"] 39 | artifacts = [ 40 | "man_pages/ffind.1", 41 | ] 42 | 43 | [tool.hatch.build.targets.sdist] 44 | include = [ 45 | "src/ffind/**/*.py", 46 | "man_pages/**/*", 47 | ] 48 | 49 | [tool.hatch.build.hooks.custom] 50 | path = "build.py" 51 | hooks = ["ManPageHook"] -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /src/ffind/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaimebuelta/ffind/3cc1c87bce818298a6774c2d3e1267c97f44d1f6/src/ffind/__init__.py -------------------------------------------------------------------------------- /src/ffind/ffind.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' Search for a file name in the specified dir (default current one) ''' 3 | from __future__ import print_function 4 | import os 5 | import sys 6 | import re 7 | import runpy 8 | import itertools 9 | from copy import copy 10 | 11 | import argparse 12 | try: 13 | from importlib.metadata import version 14 | VERSION = version('ffind') 15 | except ImportError: # pragma: no cover 16 | # Default if not installed yet. Exclude from 17 | # coverage 18 | VERSION = '1.6.1' 19 | 20 | # Define colors 21 | RED_CHARACTER = '\x1b[31m' 22 | GREEN_CHARACTER = '\x1b[32m' 23 | YELLOW_CHARACTER = '\x1b[33m' 24 | BLUE_CHARACTER = '\x1b[36m' 25 | PURPLE_CHARACTER = '\x1b[35m' 26 | NO_COLOR = '\x1b[0m' 27 | 28 | VCS_DIRS = ('CVS', 29 | 'RCS', 30 | 'SCCS', 31 | '.git', 32 | '.svn', 33 | '.arch-ids', 34 | '{arch}') 35 | 36 | VCS_FILES = ('=RELEASE-ID', 37 | '=meta-update', 38 | '=update', 39 | '.bzr', 40 | '.bzrignore', 41 | '.bzrtags', 42 | '.hg', 43 | '.hgignore', 44 | '.hgrags', 45 | '_darcs', 46 | '.cvsignore', 47 | '.gitignore',) 48 | 49 | SORT_RESULT = bool(os.environ.get('FFIND_SORT', '0')) 50 | 51 | 52 | class WrongPattern(Exception): 53 | pass 54 | 55 | 56 | def create_comparison(file_pattern, ignore_case, fuzzy): 57 | ''' Return the adequate comparison (regex or simple) ''' 58 | if fuzzy: 59 | # Generate a pattern to fuzzy match 60 | file_pattern = '.*?'.join(f for f in file_pattern) 61 | 62 | try: 63 | if ignore_case: 64 | pattern = re.compile(file_pattern, re.IGNORECASE) 65 | else: 66 | pattern = re.compile(file_pattern) 67 | except re.error: 68 | msg = ( 69 | '{red}Sorry, the expression {pattern} is incorrect.{no_color}\n' 70 | 'Remember that this should be a regular expression.\n' 71 | '(https://docs.python.org/howto/regex.html)\n' 72 | 'If you are trying something "*py" for "Everyfile that ends with py"\n' 73 | 'you can use just "py"(if you can tolerate .pyc) or "py$"\n' 74 | ) 75 | raise WrongPattern(msg.format(pattern=file_pattern, 76 | red=RED_CHARACTER, 77 | no_color=NO_COLOR)) 78 | 79 | def regex_compare(to_match): 80 | match = re.search(pattern, to_match) 81 | if match: 82 | smatch = [to_match[:match.start()], 83 | to_match[match.start(): match.end()], 84 | to_match[match.end():]] 85 | return smatch 86 | 87 | # Check if is a proper regex (contains a char different from [a-zA-Z0-9]) 88 | if fuzzy or re.search(r'[^a-zA-Z0-9]', file_pattern): 89 | return regex_compare 90 | 91 | # We can go with a simplified comparison 92 | if ignore_case: 93 | file_pattern = file_pattern.lower() 94 | 95 | def simple_compare_case_insensitive(to_match): 96 | if file_pattern in to_match.lower(): 97 | return regex_compare(to_match) 98 | return simple_compare_case_insensitive 99 | 100 | def simple_compare(to_match): 101 | if file_pattern in to_match: 102 | return regex_compare(to_match) 103 | 104 | return simple_compare 105 | 106 | 107 | def filtered_subfolders(sub_folders, ignore_hidden, ignore_vcs): 108 | ''' 109 | Create a generator to return subfolders to search, removing the 110 | ones to not search from the original list, to avoid keep 111 | walking them 112 | ''' 113 | 114 | # Create a copy to iterate to avoid iteration problems 115 | for folder in copy(sub_folders): 116 | if ignore_hidden and folder.startswith('.'): 117 | sub_folders.remove(folder) 118 | elif ignore_vcs and folder in VCS_DIRS: 119 | sub_folders.remove(folder) 120 | else: 121 | yield folder 122 | 123 | 124 | def filtered_files(files, ignore_hidden, ignore_vcs): 125 | ''' 126 | Create a generator to return the filtered files 127 | ''' 128 | for f in files: 129 | if ignore_hidden and f.startswith('.'): 130 | continue 131 | if ignore_vcs and f in VCS_FILES: 132 | continue 133 | 134 | yield f 135 | 136 | 137 | def search(directory, file_pattern, path_match, 138 | follow_symlinks=True, output=False, colored=False, 139 | ignore_hidden=True, delete=False, exec_command=False, 140 | ignore_case=False, ignore_vcs=False, return_results=True, 141 | fuzzy=False, return_exec_result=False, run_module_command=False, 142 | program=False): 143 | ''' 144 | Search the files matching the pattern. 145 | The files will be returned as a list, and can be optionally printed 146 | 147 | if return_exec_result is True in no return_results are specified, 148 | the function will return 1 if any execution has been wrong 149 | ''' 150 | 151 | # Create the compare function 152 | compare = create_comparison(file_pattern, ignore_case, fuzzy) 153 | 154 | if return_results: 155 | results = [] 156 | 157 | exec_result = 0 158 | 159 | for root, sub_folders, files in os.walk(directory, topdown=True, 160 | followlinks=follow_symlinks): 161 | if SORT_RESULT: 162 | sub_folders.sort() 163 | files.sort() 164 | # Ignore hidden and VCS directories. 165 | fsubfolders = filtered_subfolders(sub_folders, ignore_hidden, 166 | ignore_vcs) 167 | 168 | ffiles = filtered_files(files, ignore_hidden, ignore_vcs) 169 | 170 | # Search in files and subfolders 171 | for filename in itertools.chain(ffiles, fsubfolders): 172 | to_match = os.path.join(root, filename) if path_match else filename 173 | smatch = compare(to_match) 174 | if smatch: 175 | if not path_match: 176 | # Add the fullpath to the prefix 177 | smatch[0] = os.path.join(root, smatch[0]) 178 | 179 | full_filename = os.path.join(root, filename) 180 | 181 | if delete: 182 | if delete_file(full_filename): 183 | exec_result = 1 184 | 185 | elif exec_command: 186 | if execute_command(exec_command[0], full_filename): 187 | exec_result = 1 188 | 189 | elif run_module_command: 190 | if run_module(run_module_command, full_filename): 191 | exec_result = 1 192 | 193 | elif program: 194 | if execute_python_string(program, full_filename): 195 | exec_result = 1 196 | 197 | elif output: 198 | print_match(smatch, colored) 199 | 200 | if return_results: 201 | results.append(full_filename) 202 | 203 | if return_results: 204 | return results 205 | elif return_exec_result: 206 | return exec_result 207 | 208 | 209 | def print_match(splitted_match, colored, color=RED_CHARACTER): 210 | ''' Output a match on the console ''' 211 | if colored: # pragma: no cover 212 | a, b, c = splitted_match 213 | colored_output = (a, color, b, NO_COLOR, c) 214 | else: 215 | colored_output = splitted_match 216 | 217 | print(''.join(colored_output)) 218 | 219 | 220 | def delete_file(full_filename): 221 | try: 222 | if os.path.isdir(full_filename): 223 | os.removedirs(full_filename) 224 | else: 225 | os.remove(full_filename) 226 | except Exception as err: 227 | print("cannot delete: {error}".format(error=err)) 228 | return 1 229 | 230 | 231 | def execute_command(command_template, full_filename): 232 | if command_template.count('{}') > 0: 233 | command = command_template.replace('{}', full_filename) 234 | else: 235 | command = command_template + " " + full_filename 236 | 237 | result = os.system(command) 238 | if result: 239 | return 1 240 | return 0 241 | 242 | 243 | def execute_python_string(program, full_filename): 244 | try: 245 | exec(program, {}, {'filename': full_filename}) 246 | result = 0 247 | except Exception as e: 248 | print(e) 249 | result = 1 250 | 251 | return result 252 | 253 | 254 | def run_module(module_invocation, full_filename): 255 | old_argv = sys.argv 256 | result = 0 257 | 258 | args = module_invocation.split() 259 | module_name, args = args[0], args[1:] 260 | 261 | if args: 262 | args = [arg.replace('{}', full_filename) for arg in args] 263 | else: 264 | args = [full_filename] 265 | 266 | sys.argv = [module_name] + args 267 | 268 | try: 269 | runpy.run_module(module_name, run_name='__main__') 270 | except SystemExit as e: 271 | result = e.code 272 | except Exception as e: 273 | print(e) 274 | result = 1 275 | 276 | sys.argv = old_argv 277 | 278 | return result 279 | 280 | 281 | def parse_params_and_search(): 282 | ''' 283 | Process all cli parameters and call the search 284 | ''' 285 | parser = argparse.ArgumentParser( 286 | description='Search file name in directory tree' 287 | ) 288 | 289 | env_var = bool(os.environ.get('FFIND_SEARCH_PATH')) 290 | parser.add_argument('-p', 291 | action='store_true', 292 | help='Match whole path, not only name of files. ' 293 | 'Set env variable FFIND_SEARCH_PATH to set ' 294 | 'this automatically' 295 | '{0}'.format('[SET]' if env_var else ''), 296 | dest='path_match', 297 | # default False 298 | default=env_var) 299 | 300 | env_var = bool(os.environ.get('FFIND_NO_COLOR')) 301 | parser.add_argument('--nocolor', 302 | action='store_false', 303 | dest='colored', 304 | help='Do not display color. ' 305 | 'Set env variable FFIND_NO_COLOR to set ' 306 | 'this automatically' 307 | '{0}'.format('[SET]' if env_var else ''), 308 | # default True 309 | default=not env_var) 310 | 311 | env_var = bool(os.environ.get('FFIND_NO_SYMLINK')) 312 | parser.add_argument('--nosymlinks', 313 | action='store_false', 314 | dest='follow_symlinks', 315 | help='Do not follow symlinks' 316 | ' (following symlinks can lead to ' 317 | 'infinite recursion) ' 318 | 'Set env variable FFIND_NO_SYMLINK to set ' 319 | 'this automatically' 320 | '{0}'.format('[SET]' if env_var else ''), 321 | # default True 322 | default=not env_var) 323 | 324 | env_var = bool(os.environ.get('FFIND_SEARCH_HIDDEN')) 325 | parser.add_argument('--hidden', 326 | action='store_false', 327 | dest='ignore_hidden', 328 | help='Do not ignore hidden directories and files. ' 329 | 'Set env variable FFIND_SEARCH_HIDDEN to set ' 330 | 'this automatically' 331 | '{0}'.format('[SET]' if env_var else ''), 332 | # default is True 333 | default=not env_var) 334 | 335 | env_var = bool(os.environ.get('FFIND_CASE_SENSITIVE')) 336 | parser.add_argument('-c', 337 | action='store_true', 338 | dest='case_sensitive', 339 | help='Force case sensitive. By default, all lowercase ' 340 | 'patterns are case insensitive. ' 341 | 'Set env variable FFIND_CASE_SENSITIVE to set ' 342 | 'this automatically' 343 | '{0}'.format('[SET]' if env_var else ''), 344 | # default is False 345 | default=env_var) 346 | 347 | env_var = bool(os.environ.get('FFIND_CASE_INSENSITIVE')) 348 | parser.add_argument('-i', 349 | action='store_true', 350 | dest='case_insensitive', 351 | help='Force case insensitive. This allows case ' 352 | 'insensitive for patterns with uppercase. ' 353 | 'If both -i and -c are set, the search will be ' 354 | 'case sensitive.' 355 | 'Set env variable FFIND_CASE_INSENSITIVE to set ' 356 | 'this automatically' 357 | '{0}'.format('[SET]' if env_var else ''), 358 | # default is False 359 | default=env_var) 360 | 361 | action = parser.add_mutually_exclusive_group() 362 | 363 | action.add_argument('--delete', 364 | action='store_true', 365 | dest='delete', 366 | help='Delete files found', 367 | default=False) 368 | 369 | action.add_argument('--exec', 370 | dest='exec_command', 371 | nargs=1, 372 | metavar=('"command"'), 373 | help='Execute the given command with the file found ' 374 | "as argument. The string '{}' will be replaced " 375 | 'with the current file name being processed. ' 376 | 'If this option is used, ffind will return a ' 377 | 'status code of 0 if all the executions return ' 378 | '0, and 1 otherwise', 379 | default=False) 380 | 381 | action.add_argument('--module', 382 | dest='run_module', 383 | metavar=('"module_name args"'), 384 | help='Execute the given module with the file found ' 385 | "as argument. The string '{}' will be replaced " 386 | 'with the current file name being processed. ' 387 | 'If this option is used, ffind will return a ' 388 | 'status code of 0 if all the executions return ' 389 | '0, and 1 otherwise. Only SystemExit is caught', 390 | default=False) 391 | 392 | action.add_argument('--command', 393 | dest='program', 394 | metavar=('"program"'), 395 | help='Execute the given python program with the file ' 396 | "found placed in local variable 'filename'. " 397 | 'If this option is used, ffind will return a ' 398 | 'status code of 1 if any exceptions occur, ' 399 | 'and 0 otherwise. SystemExit is not caught', 400 | default=False) 401 | 402 | env_var = bool(os.environ.get('FFIND_IGNORE_VCS')) 403 | parser.add_argument('--ignore-vcs', 404 | action='store_true', 405 | dest='ignore_vcs', 406 | help='Ignore version control system files and ' 407 | 'directories. ' 408 | 'Set env variable FFIND_IGNORE_VCS to set ' 409 | 'this automatically' 410 | '{0}'.format('[SET]' if env_var else ''), 411 | # Default False 412 | default=env_var) 413 | 414 | env_var = bool(os.environ.get('FFIND_FUZZY_SEARCH')) 415 | parser.add_argument('-f', 416 | action='store_true', 417 | dest='fuzzy', 418 | help='Experimental fuzzy search. ' 419 | 'Increases the matches, use with care. ' 420 | 'Combining it with regex may give crazy results ' 421 | '{0}'.format('[SET]' if env_var else ''), 422 | # Default False 423 | default=env_var) 424 | 425 | parser.add_argument('--return-results', 426 | action='store_true', 427 | dest='return_results', 428 | help='For testing purposes only. Please ignore', 429 | default=False) 430 | 431 | parser.add_argument('--version', action='version', 432 | version='%(prog)s {version}'.format(version=VERSION)) 433 | 434 | parser.add_argument('dir', nargs='?', 435 | help='Directory to search', default='.') 436 | parser.add_argument('filepattern') 437 | args = parser.parse_args() 438 | 439 | # If output is redirected, deactivate color 440 | if not sys.stdout.isatty(): 441 | args.colored = False 442 | 443 | lowercase_pattern = args.filepattern == args.filepattern.lower() 444 | if args.case_sensitive: 445 | ignore_case = False 446 | elif args.case_insensitive: 447 | ignore_case = True 448 | else: 449 | ignore_case = lowercase_pattern 450 | 451 | exec_result = search(directory=args.dir, 452 | file_pattern=args.filepattern, 453 | path_match=args.path_match, 454 | output=True, 455 | colored=args.colored, 456 | follow_symlinks=args.follow_symlinks, 457 | ignore_hidden=args.ignore_hidden, 458 | delete=args.delete, 459 | ignore_case=ignore_case, 460 | exec_command=args.exec_command, 461 | return_exec_result=True, 462 | ignore_vcs=args.ignore_vcs, 463 | fuzzy=args.fuzzy, 464 | return_results=args.return_results, 465 | run_module_command=args.run_module, 466 | program=args.program) 467 | 468 | if args.return_results: 469 | exit(0) 470 | 471 | exit(exec_result) 472 | 473 | 474 | def run(): 475 | try: 476 | parse_params_and_search() 477 | except KeyboardInterrupt: # pragma: no cover 478 | pass 479 | except WrongPattern as err: 480 | print(err) 481 | exit(1) 482 | 483 | 484 | if __name__ == '__main__': 485 | run() 486 | -------------------------------------------------------------------------------- /tests/README: -------------------------------------------------------------------------------- 1 | cram tests are very easy, as they replicate the usual run of ffind. 2 | 3 | The current state, it sets up some files in the setup.sh script and then run an ffind command 4 | with an expected result. Finally, there's a clean up stage removing the files. 5 | 6 | Feel free to increase the test coverage, it's very easy to copy and adapt one of the current 7 | tests. More coverage will be greatly appreciated! 8 | -------------------------------------------------------------------------------- /tests/basic.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD test1 8 | ./test_dir/test1.py 9 | ./test_dir/second_level/stest1.py 10 | -------------------------------------------------------------------------------- /tests/caps.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD Test 8 | ./test_dir/Test2.py 9 | ./test_dir/second_level/sTest2.py 10 | -------------------------------------------------------------------------------- /tests/case_sensitive.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD test2 8 | ./test_dir/Test2.py 9 | ./test_dir/second_level/sTest2.py 10 | 11 | $ $FFIND_CMD -c Test2 12 | ./test_dir/Test2.py 13 | ./test_dir/second_level/sTest2.py 14 | 15 | $ $FFIND_CMD -c test2 16 | 17 | $ $FFIND_CMD -i STEST1 18 | ./test_dir/second_level/stest1.py 19 | 20 | $ $FFIND_CMD -i -c Test2 21 | ./test_dir/Test2.py 22 | ./test_dir/second_level/sTest2.py 23 | 24 | 25 | Run the tests with environment variables 26 | 27 | $ $FFIND_CMD test2 28 | ./test_dir/Test2.py 29 | ./test_dir/second_level/sTest2.py 30 | 31 | $ FFIND_CASE_SENSITIVE=1 $FFIND_CMD Test2 32 | ./test_dir/Test2.py 33 | ./test_dir/second_level/sTest2.py 34 | 35 | $ FFIND_CASE_SENSITIVE=1 $FFIND_CMD test2 36 | 37 | $ FFIND_CASE_INSENSITIVE=1 $FFIND_CMD STEST1 38 | ./test_dir/second_level/stest1.py 39 | 40 | $ FFIND_CASE_SENSITIVE=1 FFIND_CASE_INSENSITIVE=1 $FFIND_CMD Test2 41 | ./test_dir/Test2.py 42 | ./test_dir/second_level/sTest2.py 43 | -------------------------------------------------------------------------------- /tests/command.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test (we know the error is converting implicitly int to str, but the error is different 6 | in different versions of python) 7 | 8 | $ $FFIND_CMD --command "print('file:' + filename)" stest1 9 | file:./test_dir/second_level/stest1.py 10 | $ $FFIND_CMD --command "print('file:' + filename + 1)" stest1 11 | *str* (glob) 12 | [1] 13 | -------------------------------------------------------------------------------- /tests/delete.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD test1 8 | ./test_dir/test1.py 9 | ./test_dir/second_level/stest1.py 10 | $ $FFIND_CMD --delete stest1 11 | $ $FFIND_CMD test1 12 | ./test_dir/test1.py 13 | $ $FFIND_CMD third_level 14 | ./test_dir/second_level/third_level 15 | $ $FFIND_CMD --delete third_level 16 | $ $FFIND_CMD third_level 17 | $ $FFIND_CMD --delete second_level 18 | cannot delete: [Errno *] Directory not empty: './test_dir/second_level' (glob) 19 | [1] 20 | -------------------------------------------------------------------------------- /tests/execute.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD --exec "cat" stest1 8 | inside of stest1 9 | $ $FFIND_CMD --exec "cat {}{}" stest1 10 | cat: ./test_dir/second_level/stest1.py./test_dir/second_level/stest1.py: No such file or directory 11 | [1] 12 | -------------------------------------------------------------------------------- /tests/fuzzy.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD e1 8 | $ $FFIND_CMD -f e1 9 | ./test_dir/test1.py 10 | ./test_dir/second_level/stest1.py 11 | 12 | Using environment variable 13 | 14 | $ FFIND_FUZZY_SEARCH=1 $FFIND_CMD -f e1 15 | ./test_dir/test1.py 16 | ./test_dir/second_level/stest1.py 17 | -------------------------------------------------------------------------------- /tests/hidden.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD --hidden config 8 | ./test_dir/.git/config 9 | ./test_dir/second_level/config 10 | $ $FFIND_CMD config 11 | ./test_dir/second_level/config 12 | $ $FFIND_CMD lib 13 | 14 | Using environment variable 15 | 16 | $ FFIND_SEARCH_HIDDEN=1 $FFIND_CMD config 17 | ./test_dir/.git/config 18 | ./test_dir/second_level/config 19 | -------------------------------------------------------------------------------- /tests/ignore_vcs.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD --hidden lib 8 | ./test_dir/.git/library 9 | ./test_dir/.venv/library 10 | $ $FFIND_CMD --hidden --ignore-vcs lib 11 | ./test_dir/.venv/library 12 | $ $FFIND_CMD lib 13 | 14 | $ $FFIND_CMD --hidden gitignore 15 | ./test_dir/.gitignore 16 | $ $FFIND_CMD --ignore-vcs gitignore 17 | 18 | $ $FFIND_CMD --hidden --ignore-vcs gitignore 19 | 20 | Env variable 21 | 22 | $ FFIND_SEARCH_HIDDEN=1 $FFIND_CMD lib 23 | ./test_dir/.git/library 24 | ./test_dir/.venv/library 25 | $ FFIND_SEARCH_HIDDEN=1 FFIND_IGNORE_VCS=1 $FFIND_CMD lib 26 | ./test_dir/.venv/library 27 | $ FFIND_SEARCH_HIDDEN=1 $FFIND_CMD gitignore 28 | ./test_dir/.gitignore 29 | $ FFIND_IGNORE_VCS=1 $FFIND_CMD gitignore 30 | -------------------------------------------------------------------------------- /tests/module.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD sTest2 8 | ./test_dir/second_level/sTest2.py 9 | $ $FFIND_CMD --module "py_compile {}" sTest2 10 | 11 | Search for compiled file. Needs wildcard as python3 compiled file if different 12 | $ $FFIND_CMD sTest2 13 | ./test_dir/second_level/sTest2.py 14 | *sTest2*pyc (glob) 15 | 16 | Error in the module 17 | $ $FFIND_CMD --module "badmodule" stest3 18 | No module named badmodule 19 | [1] 20 | -------------------------------------------------------------------------------- /tests/path.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD second 8 | ./test_dir/second_level 9 | 10 | $ $FFIND_CMD -p second 11 | ./test_dir/second_level 12 | ./test_dir/second_level/CVS 13 | ./test_dir/second_level/config 14 | ./test_dir/second_level/sTest2.py 15 | ./test_dir/second_level/stest1.py 16 | ./test_dir/second_level/stest3.sh 17 | ./test_dir/second_level/third_level 18 | 19 | Using env variable 20 | 21 | $ FFIND_SEARCH_PATH=1 $FFIND_CMD second 22 | ./test_dir/second_level 23 | ./test_dir/second_level/CVS 24 | ./test_dir/second_level/config 25 | ./test_dir/second_level/sTest2.py 26 | ./test_dir/second_level/stest1.py 27 | ./test_dir/second_level/stest3.sh 28 | ./test_dir/second_level/third_level 29 | -------------------------------------------------------------------------------- /tests/regex.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD py$ 8 | ./test_dir/Test2.py 9 | ./test_dir/test1.py 10 | ./test_dir/second_level/sTest2.py 11 | ./test_dir/second_level/stest1.py 12 | -------------------------------------------------------------------------------- /tests/regex2.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD .test..py 8 | ./test_dir/second_level/sTest2.py 9 | ./test_dir/second_level/stest1.py 10 | -------------------------------------------------------------------------------- /tests/return_results.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD --return-results test1 8 | ./test_dir/test1.py 9 | ./test_dir/second_level/stest1.py 10 | -------------------------------------------------------------------------------- /tests/setup.sh: -------------------------------------------------------------------------------- 1 | # The ffind script 2 | FFIND_PY=$TESTDIR/../src/ffind/ffind.py 3 | # The complete ffind command, by python interpreter 4 | FFIND_CMD="$PYTHON $FFIND_PY" 5 | 6 | # Set a directory with different files to test search capabilities 7 | TEST_DIR=./test_dir 8 | mkdir $TEST_DIR 9 | touch $TEST_DIR/test1.py 10 | touch $TEST_DIR/Test2.py 11 | touch $TEST_DIR/test3.sh 12 | mkdir $TEST_DIR/second_level 13 | echo 'inside of stest1' > $TEST_DIR/second_level/stest1.py 14 | echo 'print("sTest2")' > $TEST_DIR/second_level/sTest2.py 15 | touch $TEST_DIR/second_level/stest3.sh 16 | touch $TEST_DIR/second_level/config 17 | touch $TEST_DIR/second_level/.hidden 18 | touch $TEST_DIR/second_level/CVS 19 | mkdir $TEST_DIR/second_level/third_level 20 | mkdir $TEST_DIR/.git 21 | touch $TEST_DIR/.gitignore 22 | touch $TEST_DIR/.git/config 23 | touch $TEST_DIR/.git/library 24 | mkdir $TEST_DIR/.venv 25 | mkdir $TEST_DIR/.venv/library 26 | 27 | FFIND_SORT=1 28 | -------------------------------------------------------------------------------- /tests/wrong_pattern.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . $TESTDIR/setup.sh 4 | 5 | Run test 6 | 7 | $ $FFIND_CMD *.t 8 | \x1b[31mSorry, the expression *.t is incorrect.\x1b[0m (esc) 9 | Remember that this should be a regular expression. 10 | (https://docs.python.org/howto/regex.html) 11 | If you are trying something "*py" for "Everyfile that ends with py" 12 | you can use just "py"(if you can tolerate .pyc) or "py$" 13 | 14 | [1] 15 | --------------------------------------------------------------------------------