#! /usr/bin/env python
#
# Part of the A-A-P project: File type detection module
# Copyright (C) 2002 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here:
http://www.a-a-p.org/COPYING
# This module detects the type of a file.
# It can be run as a separate program or called from Python.
# Many types are recognized by default. More types can be added dynamically.
# See filetype.txt for an explanation.
#
#
# EXTERNAL INTERFACE:
#
# ft_detect(fname) Detects the type of file "fname".
#
# ft_check_dir(dir [, errmsg)]
# Scan directory "dir" for "*.afd" files, which are
# loaded with ft_read_file().
#
# ft_read_file(fname) Read file "fname" for detection rules.
#
# ft_add_rules(str) Add file type detection rules from "str". See
# "filetype.txt" for the syntax.
#
import string
import os.path
import re
from Util import *
# Set to non-zero when run as a program.
_run_as_program = 0
#
# The default list of detected file types.
# NOTE: since "append" isn't used, the order of checking is last one first!
#
_def_detect_list = """
suffix c c
suffix h cpp
suffix hh cpp
suffix H cpp
suffix hxx cpp
suffix hpp cpp
suffix cpp cpp
suffix cc cpp
suffix C cpp
suffix c++ cpp
suffix cxx cpp
suffix moc cpp
suffix tcc cpp
suffix inl cpp
suffix py python
suffix pl perl
suffix sh sh
suffix aap aap
suffix afd afd
suffix html html
suffix htm html
suffix Z ignore
suffix gz ignore
suffix bz2 ignore
suffix bak ignore
regexp .*enlightenment/.*.cfg$ c
regexp .*vimrc$ vim
regexp .*\\bconfigure$ sh
script .*\\bpython python
script .*\\bperl perl
script .*csh\\b csh
script .*\\bbash sh
"""
# List of _Ft_py objects: Python code executed to detect file type.
# Used first.
_py_list_before = []
# Dictionary used to map file name extension to file type.
_suffix_dict = {}
# List of _Ft_re objects; a match of the RE with the file name defines the file
# type.
_regexp_list = []
# List of _Ft_re objects: a match of the RE with the script in the first line
# of the file defines the file type.
_script_list = []
# List of _Ft_py objects: Python code executed to detect file type.
# Used after everything else didn't detect the type.
_py_list_after = []
_did_init = 0 # non-zero when __init__() did its work
def __init__():
global _suffix_dict, _regexp_list, _script_list
global _py_list_before, _py_list_after
global _did_init
# this only needs to be done once
if _did_init:
return
_did_init = 1
_py_list_before = []
_suffix_dict = {}
_regexp_list = []
_script_list = []
_py_list_after = []
# Load the built-in detection rules.
ft_add_rules(_def_detect_list)
# Load detection rules from system and user *.afd files.
ft_check_dir("/usr/local/share/aap/afd")
ft_check_dir(os.path.expanduser("~/.aap/afd"))
class DetectError(Exception):
"""Error for something gone wrong."""
def __init__(self, args = None):
self.args = args
def ft_check_dir(dir, errmsg = 0):
"""Check directory "dir" for *.afd files and load them.
When "errmsg" is non-zero give an error message when the directory
doesn't exist."""
if os.path.exists(dir) and os.path.isdir(dir):
for f in glob(os.path.join(dir, "*.afd")):
try:
ft_read_file(f)
except DetectError, e:
if _run_as_program:
print str(e)
else:
from Message import msg_error
msg_error(str(e))
elif errmsg:
e = _('Directory does not exist: "%s"') % dir
if _run_as_program:
print e
else:
from Message import msg_error
msg_error(e)
def ft_read_file(fname):
"""Read file "fname" for file type detection rules."""
try:
file = open(fname)
except IOError, e:
raise DetectError, (_('Cannot open "%s": ') % fname) + str(e)
try:
str = file.read()
except IOError, e:
raise DetectError, (_('Cannot read "%s": ') % fname) + str(e)
file.close()
ft_add_rules(str)
def ft_add_rules(str):
"""Add file type detection rules from string "str"."""
# Always load the default rules first (skipped when done already).
__init__()
# Split the string into individual lines.
lines = string.split(str, '\n')
# Loop over all the lines (may use more than one for python items).
line_idx = 0
line_count = len(lines)
while line_idx < line_count:
line = lines[line_idx]
line_len = len(line)
# isolate first word: type of detection.
ds = skip_white(line, 0) # detection start
# ignore empty and comment lines
if ds == line_len or line[ds] == '#':
line_idx = line_idx + 1
continue
de = skip_to_white(line, ds) # detection end
item = line[ds:de]
as = skip_white(line, de) # argument start
# isolate first argument, which may be in quotes
if as < line_len:
if line[as] == '"' or line[as] == "'":
quote = line[as]
as = as + 1
ae = as
while ae < line_len and line[ae] != quote:
ae = ae + 1
if ae == line_len:
raise DetectError, _('Missing quote in "%s"') % line
n = ae + 1
else:
ae = as
while ae < line_len and line[ae] != ' ' and line[ae] != '\t':
ae = ae + 1
n = ae
arg1 = line[as:ae]
n = skip_white(line, n)
else:
arg1 = ''
n = line_len
# Isolate further arguments (no quotes!).
# A superfluous argument is silently ignore (could be a comment).
args = string.split(line[n:])
if len(args) >= 1:
arg2 = args[0]
else:
arg2 = ''
if len(args) >= 2:
arg3 = args[1]
else:
arg3 = ''
if item == "suffix":
if not arg2:
raise DetectError, _('Missing argument in "%s"') % line
_add_suffix(arg1, arg2)
elif item == "regexp":
if not arg2:
raise DetectError, _('Missing argument in "%s"') % line
_add_regexp(arg1, arg2, arg3 and arg3 == "append")
elif item == "script":
if not arg2:
raise DetectError, _('Missing argument in "%s"') % line
_add_script(arg1, arg2, arg3 and arg3 == "append")
elif item == "python":
append = 0
after = 0
for arg in [arg1, arg2]:
if arg:
if arg == "append":
append = 1
elif arg == "after":
after = 1
else:
raise DetectError, _('Illegal argument in "%s"') % line
start_indent = get_indent(line)
line_idx = line_idx + 1
start_line_idx = line_idx
cmds = ""
while line_idx < line_len:
line = lines[line_idx]
if get_indent(line) <= start_indent:
line_idx = line_idx - 1 # this line has next item
break
cmds = cmds + line + '\n'
line_idx = line_idx + 1
if not cmds:
raise DetectError, _('Python commands missing')
_add_python(cmds, _("filetype detection line %d") % start_line_idx,
after, append)
else:
raise (DetectError,
_("Illegal item %s in argument to ft_add_rules()") % item)
line_idx = line_idx + 1
class _Ft_re:
"""Class used to store pairs of RE and file type."""
def __init__(self, re, type):
self.re = re
self.type = type
class _Ft_py:
"""Class used to store Python code for detecting a file type."""
def __init__(self, code, error_msg):
self.code = code # the Python code
self.error_msg = error_msg # ar message used for errors
def _add_suffix(suf, type):
"""Add detection of "type" by file name extension "suf".
When "type" is "ignore" it means the suffix is removed and further
detection done on the rest.
When "type" is "remove" an existing detection for "suf" is removed."""
if type == 'remove':
if _suffix_dict.has_key(suf):
del _suffix_dict[suf]
else:
_suffix_dict[suf] = type
def _add_regexp(re, type, append):
"""Add detection of "type" by matching the file name with Python regular
expression "re".
When append is non-zero, add to the end of the regexp rules.
When "type" is "remove" an existing detection for "re" is removed."""
if type == 'remove':
for r in _regexp_list:
if r.re == re:
_regexp_list.remove(r)
else:
f = _Ft_re(re, type)
if append:
_regexp_list.append(f)
else:
_regexp_list.insert(0, f)
def _add_script(re, type, append):
"""Add detection of "type" by matching the script name in the first line of
the file with Python regular expression "re".
When append is non-zero, add to the end of the script rules.
When "type" is "remove" an existing detection for "re" is removed."""
if type == 'remove':
for r in _script_list:
if r.re == re:
_script_list.remove(r)
else:
f = _Ft_re(re, type)
if append:
_script_list.append(f)
else:
_script_list.insert(0, f)
def _add_python(code, error_msg, after, append):
"""Add detection of "type" by using Python code "code".
Each line in "code" must end in a '\n'.
"error_msg" is printed when executing the code results in an error.
When "after" is non-zero use this rule after suffix, regexp and script
rules.
When append is non-zero, add to the end of the python rules."""
p = _Ft_py(code, error_msg)
if after:
list = _py_list_after
else:
list = _py_list_before
if append:
list.append(p)
else:
list.insert(0, p)
def _exec_py(fname, item):
"""Execute the code defined with _add_python()."""
# Make a completely fresh globals dictionary.
new_globals = {"fname" : fname}
# Prepend "if 1:" to get the indenting right.
if item.code[0] == ' ' or item.code[0] == '\t':
code = "if 1:\n" + item.code
else:
code = item.code
try:
exec code in new_globals, new_globals
except StandardError, e:
raise DetectError, _(item.error_msg) + str(e)
if new_globals.has_key("type"):
return new_globals["type"]
return None
def ft_detect(fname):
"""Detect the file type for file "fname".
Returns the type as a string or None."""
# Initialize (will skip when done already)
__init__()
# On non-Posix systems we ignore case differences by making the name lower
# case.
if os.name != 'posix':
fname = string.lower(fname)
# Do the python code checks.
for p in _py_list_before:
type = _exec_py(fname, p)
if type:
return type
# Try the extension, this is fastest.
# When "fname" has several extensions, try with all of them first, then
# try by removing the first ones: "f.html.c": "html.c" then ".c".
bn = os.path.basename(fname)
i = string.find(bn, ".")
while i > 0 and i + 1 < len(bn):
# Found a dot that's not the first or last character.
if _suffix_dict.has_key(bn[i + 1:]):
ft = _suffix_dict[bn[i + 1:]]
if ft != "ignore":
return ft
# remove an ignored extension
fname = fname[:-(len(bn[i:]))]
bn = bn[:i]
i = 0
i = string.find(bn, ".", i + 1)
# match all defined REs with the file name.
# TODO: handle "/" in RE and fname.
for r in _regexp_list:
if re.match(r.re, fname):
return r.type
# match all defined REs with the script name in the first line of the
# file.
try:
f = open(fname)
line = f.readline()
f.close()
except:
# Errors for files that can't be read are ignored.
pass
else:
if len(line) > 2 and line[:2] == "#!":
# TODO: remove "env VAR=val" and script arguments from line
for r in _script_list:
if re.match(r.re, line[2:]):
return r.type
# Do the python code checks.
for p in _py_list_after:
type = _exec_py(fname, p)
if type:
return type
return None
# When executed as a program, detect the type of the specified file.
if __name__ == '__main__':
import sys
# Internationalisation inits: setlocale and gettext.
i18n_init()
items = []
checkfile = None
_run_as_program = 1
# Check for any "-Idir", "-I dir", "-ffile" and "-f file" arguments.
next_is_dir = 0
next_is_file = 0
for arg in sys.argv[1:]:
if next_is_dir:
items.extend({"dir" : arg})
next_is_dir = 0
elif next_is_file:
items.extend({"file" : arg})
next_is_file = 0
elif len(arg) >= 2 and arg[:2] == "-I":
if len(arg) > 2:
items.extend({"dir" : arg[2:]})
else:
next_is_dir = 1
elif len(arg) >= 2 and arg[:2] == "-f":
if len(arg) > 2:
items.extend({"file" : arg[2:]})
else:
next_is_file = 1
else:
if checkfile:
print _("Can only check one file")
sys.exit(1)
checkfile = arg
if next_is_dir:
print _("-I argument must be followed by a directory name")
sys.exit(1)
if next_is_file:
print _("-f argument must be followed by a file name")
sys.exit(1)
if not checkfile:
print _("Usage: %s [-I ruledir] [-f rulefile] filename") % sys.argv[0]
sys.exit(1)
# load the built-in default rules
__init__()
# Check specified directories for *.afd files and read specified files.
for item in items:
if item.has_key("dir"):
ft_check_dir(item["dir"])
else:
try:
ft_read_file(item["file"])
except DetectError, e:
print e
print ft_detect(sys.argv[1])
# vim: set sw=4 sts=4 tw=79 fo+=l: