ffind @ a8b6ed833a30

Remove --stdin for now.
author Steve Losh <steve@stevelosh.com>
date Sat, 22 Sep 2012 15:40:00 -0400
parents f393d2ad42d7
children d6c518c8ab39
#!/usr/bin/env python
# -*- coding: utf8 -*-

#    ____  ____  __  ____  __ _  ____  __    _  _     ____  __  __ _  ____
#   (  __)(  _ \(  )(  __)(  ( \(    \(  )  ( \/ )___(  __)(  )(  ( \(    \
#    ) _)  )   / )(  ) _) /    / ) D (/ (_/\ )  /(___)) _)  )( /    / ) D (
#   (__)  (__\_)(__)(____)\_)__)(____/\____/(__/     (__)  (__)\_)__)(____/
#
#                         The friendlier file finder.

import os
import optparse
import string
import sys
from optparse import OptionParser, OptionGroup


# Constants -------------------------------------------------------------------
CASE_SENSITIVE = 1
CASE_INSENSITIVE = 2
CASE_SMART = 3

VCS_DIRS = ['.hg', '.git', '.svn']

# Global Options --------------------------------------------------------------
# (it's a prototype, shut up)
options = None

# Output ----------------------------------------------------------------------
def out(s, line_ending='\n'):
    sys.stdout.write(s + line_ending)

def err(s):
    sys.stderr.write(s + '\n')

def die(s, exitcode=1):
    err('error: ' + s)
    sys.exit(exitcode)


# Searching! ------------------------------------------------------------------
def should_ignore(basename, path):
    if basename in VCS_DIRS:
        return True

    return False

def match(query, path):
    def _match():
        if options.case == CASE_INSENSITIVE:
            return query.lower() in path.lower()
        else:
            return query in path

    result = _match()
    return not result if options.invert else result

def search(query, dir='.', depth=0):
    contents = os.listdir(dir)

    next = []
    for item in contents:
        path = os.path.join(dir, item)
        if not should_ignore(item, path):
            if match(query, path):
                out(path, '\0' if options.zero else '\n')

            is_dir = os.path.isdir(path)
            if is_dir:
                next.append(path)

    if depth < options.depth:
        for d in next:
            search(query, d, depth + 1)


# Option Parsing and Main -----------------------------------------------------
def build_option_parser():
    p = OptionParser("usage: %prog [options] PATTERN")

    # Main options
    p.add_option('-d', '--dir', default='.',
                 help='root the search in DIR (default .)',
                 metavar='DIR')
    p.add_option('-D', '--depth', default='25',
                 help='search at most N directories deep (default 25)',
                 metavar='N')
    p.add_option('-f', '--follow',
                 action='store_true', default=False,
                 help='follow symlinked directories and search their contents')
    p.add_option('-F', '--no-follow',
                 dest='follow', action='store_false',
                 help="don't follow symlinked directories (default)")
    p.add_option('-0', '--print0', dest='zero',
                 action='store_true', default=False,
                 help='separate matches with a null byte in output')
    p.add_option('-l', '--literal',
                 action='store_true', default=False,
                 help='force literal search, even if it looks like a regex')
    p.add_option('-v', '--invert',
                 action='store_true', default=False,
                 help='invert match')

    # Case sensitivity
    g = OptionGroup(p, "Configuring Case Sensitivity")
    g.add_option('-s', '--case-sensitive',
                 dest='case', action='store_const', const=CASE_SENSITIVE,
                 default=CASE_SENSITIVE,
                 help='case sensitive matching (default)')
    g.add_option('-i', '--case-insensitive',
                 dest='case', action='store_const', const=CASE_INSENSITIVE,
                 help='case insensitive matching')
    g.add_option('-S', '--case-smart',
                 dest='case', action='store_const', const=CASE_SMART,
                 help='smart case matching (sensitive if any uppercase chars '
                      'are in the pattern, insensitive otherwise)')
    p.add_option_group(g)

    # Ignoring
    g = OptionGroup(p, "Configuring Ignoring")
    g.add_option('-b', '--binary',
                 dest='binary', action='store_true', default=True,
                 help="allow binary files (default)")
    g.add_option('-B', '--no-binary',
                 dest='binary', action='store_false',
                 help='ignore binary files')
    g.add_option('-r', '--restricted',
                 action='store_true', default=False,
                 help="restricted search (skip VCS directories, parse all ignore files) (default)")
    g.add_option('-q', '--semi-restricted',
                 action='store_true', default=False,
                 help="semi-restricted search (don't parse VCS ignore files, but still skip VCS directories and parse .ffignore)")
    g.add_option('-u', '--unrestricted',
                 action='store_true', default=False,
                 help="unrestricted search (don't parse ignore files, but still skip VCS directories)")
    g.add_option('-a', '--all',
                 action='store_true', default=False,
                 help="don't ignore anything (ALL files can match)")
    g.add_option('-I', '--ignore', metavar='PATTERN',
                 action='append',
                 help="add a pattern to be ignored (can be given multiple times)")
    p.add_option_group(g)

    # Time filtering
    g = OptionGroup(p, "Time Filtering")
    g.add_option('--before',
                 help='match files modified < TIME',
                 metavar='TIME')
    g.add_option('--after',
                 help='match files modified > TIME',
                 metavar='TIME')
    g.add_option('--until',
                 help='match files modified <= TIME',
                 metavar='TIME')
    g.add_option('--since',
                 help='match files modified >= TIME',
                 metavar='TIME')
    g.add_option('--at',
                 help='match files modified at TIME',
                 metavar='TIME')
    g.add_option('--created-before',
                 help='match files created < TIME',
                 metavar='TIME')
    g.add_option('--created-after',
                 help='match files created > TIME',
                 metavar='TIME')
    g.add_option('--created-until',
                 help='match files created <= TIME',
                 metavar='TIME')
    g.add_option('--created-since',
                 help='match files created >= TIME',
                 metavar='TIME')
    g.add_option('--created-at',
                 help='match files created at TIME',
                 metavar='TIME')
    p.add_option_group(g)

    # Size filtering
    g = OptionGroup(p, "Size Filtering",
                    "Sizes can be given as a number followed by a prefix.  Some examples: "
                    "1k, 5kb, 1.5gb, 2g, 1024b")
    g.add_option('--larger-than',
                 help='match files larger than SIZE (inclusive)',
                 metavar='SIZE')
    g.add_option('--bigger-than', dest='larger_than',
                 help=optparse.SUPPRESS_HELP)
    g.add_option('--smaller-than',
                 help='match files smaller than SIZE (inclusive)',
                 metavar='SIZE')
    p.add_option_group(g)

    # Type filtering
    g = OptionGroup(p, "Type Filtering",
                    "Possible types are "
                    "a (all), "
                    "f (files), "
                    "d (dirs), "
                    "r (real), "
                    "s (symlinked), "
                    "e (real files), "
                    "c (real dirs), "
                    "x (symlinked files), "
                    "y (symlinked dirs). "
                    "If multiple types are given they will be unioned together:  "
                    "--type 'es' would match real files and all symlinks.")
    g.add_option('-t', '--type',
                 action='store', default=False, metavar='TYPE(S)',
                 help='match only specific types of things (files, dirs, non-symlinks, symlinks)')
    p.add_option_group(g)

    return p


def main():
    global options

    (options, args) = build_option_parser().parse_args()

    # PATTERN
    if len(args) > 1:
        die("only one search pattern can be given")
        sys.exit(1)

    query = args[0] if args else ''

    # --dir
    if options.dir:
        try:
            os.chdir(options.dir)
        except OSError:
            die('could not change to directory "%s"' % options.dir)

    # --depth
    try:
        options.depth = int(options.depth)
    except ValueError:
        die('depth must be a non-negative integer (got "%s")' % options.depth)

    # --case-*
    if options.case == CASE_SMART:
        if any(c in string.uppercase for c in query):
            options.case = CASE_SENSITIVE
        else:
            options.case = CASE_INSENSITIVE

    # Go!
    search(query)


if __name__ == '__main__':
    main()