├── .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
2 |
3 |
4 |
5 | 
About
It allows quick and easy recursive search for files in the Unix command line.

Basically, replaces find . -name '*FILE_PATTERN*'
with ffind.py FILE_PATTERN
(and a few more niceties)
6 |
7 | - Input filename may be a full regex
8 | - Search recursively on current directory by default.
9 | - If the FILE_PATTERN is all in lowercase, the search will be case insensitive, unless a flag is set.
10 | - Regex can affect only the filename (default) or the full path.
11 | - Will print colorized output in glamorous red (default), except on redirected output.
12 | - Ignores hidden directories and files (starting with
.
) by default
13 | - Can ignore source control common directories and files, like
.gitignore
or RCS/
. Typically not needed as hidden
14 | are ignored by default.
15 | - Follow symlinks by default, but that can be deactivated if necessary to avoid recursion problems
16 | - Can delete matched files
17 | - Can execute a command on matched files
18 | - Experimental fuzzy search
19 |
20 | Common uses:
21 |
22 | ffind txt
to return all text files in current dir
23 | ffind ../other_dir txt
to return all text files under dir ../otherdir (or `ffind.py txt -d ../otherdir`)
24 |
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 |
91 | - FFIND_SORT: Return the results sorted. This is slower, and is mainly thought to ensure
92 | consistency on the tests, as some filesystems may order files differently
93 | - FFINDCASESENSITIVE: Search is case sensitive. Equivalent to
-c
flag
94 | - FFINDCASEINSENSITIVE: Search is case insensitive. Equivalent to
-i
flag.
95 | - FFINDSEARCHHIDDEN: Search in hidden directories and files. Equivalent to
--hidden
flag.
96 | - FFINDSEARCHPATH: Search in the whole path. Equivalent to
-p
flag.
97 | - FFINDIGNOREVCS: Ignore paths in version control. Equivalent to
--ignore-vcs
98 | - FFINDNOSYMLINK: Do not follow symlinks. Equivalent to
--nosymlinks
flag.
99 | - FFINDNOCOLOR: Do not show colors. Equivalent to
--nocolor
flag.
100 | - FFINDFUZZYSEARCH: Enable fuzzy search. Equivalent to
-f
flag.
101 |
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 |
--------------------------------------------------------------------------------