Index: django/core/management/commands/query.py
===================================================================
--- django/core/management/commands/query.py	(revision 0)
+++ django/core/management/commands/query.py	(revision 0)
@@ -0,0 +1,120 @@
+from django.core.exceptions import FieldError
+from django.core.management import BaseCommand
+from django.db.models import Q
+from django.template import loader, Template, Context, TemplateSyntaxError, TemplateDoesNotExist
+from optparse import make_option
+import os.path
+import sys
+
+usage="""The django ORM will be queried with the filters on the commandline.
+Records will be separated with newlines, fields with the specified separator
+(the default is a comma). Alternatively, a template can be specified which will
+be passed the result of the query as the 'objects' variable
+
+Query key/value pairs can be prefixed with a '!' or ~ to negate the query,
+internally this uses a Q object.
+
+Examples:
+ - Display username and email for all users
+   %prog query -a django.contrib.auth -m User -f username
+ - Show who can delete sites
+   %prog query -a django.contrib.auth -m User -f username groups__permissions__codename=delete_site
+ - Use comma-separated values for __in lookups
+   %prog query -a django.contrib.auth -m User -f username groups__permissions__codename__in=delete_site,change_site
+ - Use a template to format the information
+   %prog query -a django.contrib.auth -m User -t '{% for o in objects %}{{ o.get_full_name }} has joined on {{ o.date_joined }}
+{% endfor %}'
+"""
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('-a', '--application', dest="application",
+                    default=os.environ.get("DJANGO_QUERY_DEFAULT_APPLICATION", None),
+                    help="Use this application", metavar="APP"),
+        make_option('-m', '--model', dest="model",
+                    default=os.environ.get("DJANGO_QUERY_DEFAULT_MODEL", None),
+                    help="Query this model"),
+        make_option('-o', '--order', dest="order", default=None,
+                    help="Order by this field"),
+        make_option('-f', '--fields', dest="fields", default=None,
+                    help="Give these fields"),
+        make_option('-s', '--separator', dest="separator", default=",",
+                    help="Output separator"),
+        make_option('-t', '--template', dest="template", default='',
+                    help="Inline template in django syntax"),
+        make_option('-T', '--template-file', dest="template_file", default=None,
+                    help="File containing the template (abs/rel path or loader path)")
+    )
+    help = usage
+    args = 'filter [filter ...]'
+
+    def handle(self, *args, **options):
+        if not options['application']:
+            print "You must specify which application to use"
+            sys.exit(1)
+        if not options['model']:
+            print "You must specify which model to use"
+            sys.exit(1)
+        if not options['fields'] and not options['template'] and not options['template_file']:
+            print "You must specify a list of fields or a template"
+            sys.exit(1)
+
+        # Import the model
+        models = options['application'] + '.models'
+        __import__(models)
+        models = sys.modules[models]
+        model = getattr(models, options['model'])
+
+        # Create queryset
+        qargs = []
+        for x in args:
+            if '=' not in args:
+                print "Invalid filter '%s' - should be in attribute=value format" % x
+                sys.exit(1)
+            key, val = x.split('=',1)
+            # Accecpt comma-separated values for __in lookups
+            if key.endswith('__in'):
+                val = val.split(',')
+            if key.startswith('!') or key.startswith('~'):
+                qargs.append(~Q(**{key[1:]: val}))
+            else:
+                qargs.append(Q(**{key: val}))
+        try:
+            queryset = model.objects.filter(*qargs).distinct()
+            if options['order']:
+                queryset = queryset.order_by(options['order'])
+            # Force coercion into list to trap illegal values for options['order']
+            queryset = list(queryset)
+        except FieldError, e:
+            print e
+            sys.exit(1)
+
+        # Generate output
+        if options['template']:
+            # Inline template
+            template = options['template']
+        elif options['template_file']:
+            # Template file, 3 options: stdin, existing path, or loader
+            tf = options['template_file']
+            if tf == '-':
+                template = sys.stdin.read()
+            elif tf and os.path.exists(tf):
+                template = open(tf).read()
+            elif tf:
+                try:
+                    template = loader.get_template(tf)
+                except TemplateDoesNotExist:
+                    print "Template %s does not exist" % tf
+                    sys.exit(1)
+        else:
+            # Build a (c)sv template
+            template = "{% for obj in objects %}"
+            for field in options['fields'].split(','):
+                template += '{{ obj.' + field + ' }}' + options['separator']
+            template = template[:-len(options['separator'])] + "\n{% endfor %}"
+        try:
+            template = Template(template)
+        except TemplateSyntaxError, e:
+            print "Template syntax error: " + str(e)
+            sys.exit(1)
+        print template.render(Context({'objects': queryset}))
Index: docs/ref/django-admin.txt
===================================================================
--- docs/ref/django-admin.txt	(revision 11211)
+++ docs/ref/django-admin.txt	(working copy)
@@ -252,6 +252,75 @@
     you sure?" confirmation messages. This is useful if ``django-admin.py`` is
     being executed as an unattended, automated script.
 
+query
+-----
+
+.. django-admin:: query -a application -m model [filters] [output options]
+
+Query the django ORM from the commandline. You need to specify the application
+and model to query and optionally a list of filters. The default output is csv
+format, but you can change the separator to something else or use a template to
+format the output.
+
+Query options
+~~~~~~~~~~~~~
+
+..django-admin-option:: --application
+Query a model in this application.
+
+..django-admin-option:: --model
+Use -m or --model to specify which model in the application to query
+
+..django-admin-option:: --order
+Order by this field
+
+Output options
+~~~~~~~~~~~~~~~~
+
+..django-admin-option:: --fields
+A comma-separated list of fields to output in CSV format
+
+..django-admin-option:: --separator
+Use another separator than the comma in the output
+
+..django-admin-option:: --template
+Inline template to use for rendering the data. The data will be available in
+the ``objects`` variable.
+
+..django-admin-option:: --template-file
+Use a template from a file. If you specify ``-`` as template file, the template
+will be read from stdin. If the path you specify is an absolute/relative path
+to an existing file it will be used. For other values, the template will be
+loaded with ``template.loader``.
+
+Query filters
+~~~~~~~~~~~~~
+The remaining arguments will be interpreted as filters, very similar to the
+standard query syntax. There are a few syntactic changes to accomodate
+negations and ``__in`` lookups.
+
+    * ``~Q(key=value)`` can be written as ``~key=value`` or ``!key=value``
+    * ``key__in=(value1,value2,value3)`` can be written as ``key__in=value1,value2,value3``
+
+Examples
+~~~~~~~~
+Display username and email for all users::
+
+    django-admin.py query -a django.contrib.auth -m User -f username
+
+Show who can delete sites::
+
+    django-admin.py query -a django.contrib.auth -m User -f username groups__permissions__codename=delete_site
+   
+Use comma-separated values for __in lookups::
+
+    django-admin.py query -a django.contrib.auth -m User -f username groups__permissions__codename__in=delete_site,change_site
+
+Use a template to format the information::
+
+    django-admin.py query -a django.contrib.auth -m User -t '{% for o in objects %}{{ o.get_full_name }} has joined on {{ o.date_joined }}
+    {% endfor %}'
+
 inspectdb
 ---------
 
