Feelings of White   recycle rerinse rerepeat
Me, looking like Hot Sex inc. with my cool shades, a beer and my bountiful chest hair displayed for all to see
  • all
  • curator's pick
  • funny
  • narcissism
  • technical
  • the arts
  • the void
  • violent aggression
  • writing

Command Line Parsing using Attribute Decoration in .NET

The Tease

At some point, you'll have to write an application accepting command line arguments.  Typically a console application, but GUI apps can accept command lines too.  Its features and options will grow and you will become frustrated with .Net's default abilities.  You'll desire a robust parsing ability, and you'll wish to configure it easily, automatically and in an extensible way.  Welcome to today's topic

    screenshot of command-line program, demonstrating different usage patterns supported by the framework, described later in the document

Big whup, eh?  But let's see how simple it was to create: just decorate a class' properties with attributes to describe the mapping.

public class ProgramHelloArguments
{
    public enum GreetingType
        { Standard, Friendly, Familiar }
    private int? _age;
    private string _email;

    [EnumArgument("say"), HelpText("Specify alternate greeting")]
    public GreetingType Greeting = GreetingType.Standard;

    [CommandLineArgument("name"), Required, Position(1)]
    public string Name;

    [SwitchArgument("kissAss")]
    public bool ComplimentUser;

    [CommandLineArgument("email"), Position(2)]
    public string Email
    {   get { return _email; }
        set
        {   _email = value;
            if (!String.IsNullOrEmpty(_email))
                _email = "mailto:" + _email;
        }
    }

    [ValidDirectoryArgument("homeDir")]
    public string HomeDir;

    [IntArgument("age"), HelpText("Optionally specify your age")]
    public int? Age
    {   get { return _age; }
        set
        {   if (value < 0)
                throw new ArgumentParseException("Age cannot be less than zero");
            _age = value;
        }
    }
}
Goofy lookin' guy on a magic carpet

Join me on a magic carpet ride into simplicity as I further describe some of the interesting highlights of how to use it, how it works. and how you can steal the code, make it your own and add the quirky bits unique to your situation.  The full Visual Studio 2005 project code is available.

Admit it, you can tell how its supposed to work just from reading the property declaration, right? Features worth mentioning listed below. (or let me draw you a picture [in a new window], if you prefer)

  • Both fields and properties are supported.  As are booleans (Switch), enums and nullable types
  • Re-usable validation (e.g. ValidDirectory, IntArgument), I'll show how easy it is to plug in your attributes for custom validation
  • Ad-hoc validation in property setters (e.g. Age), or ad-hoc re-interpretation of arguments (e.g. Email)
  • Required parameter validation
  • Use Position to pass parameters with out specifying the token (e.g. "CommandLineParsing.exe James"), or use tokens to pass parameters in any order
  • Use HelpText for auto-generating parameter help files (e.g. "-?")
  • Decorate properties of any class.  Imagine your own hierarchy of parameter classes which could be re-used across many programs

Note: The code's a tad too verbose to dump full contents into this article.  I'll omit the boring shit and provide full code for download.  The full code is better commented, but within this article I'll reduce or omit inline comments for brevity and discuss in paragraph form.

Y'all Know What Time It Is?

The .Net libraries provide only limited parsing of arguments.  It turns the command line into an array of strings, and in my experience parsing command lines is done differently in every situation, copy + pasted around and done half-assed except when required.  I first had the idea of using attributes to describe the desired parsing a few years back, started coding a super-generic solution, then stopped.  My current job has around 40+ batch exes, and when discussion recently turned to how we could improve the horrid inconsistent parsing situation, I revived my old idea, it took over a chunk of my free time, and ended here.

I've decided that a one-sized-fits-all parse is impossible.  There are a frazillion variations on command line parsing, conflicting conventions, and perhaps you've already got a situation that this won't handle.  For instance, we use "Exe name:value name:value" at my current job,  cmd's attrib command accepts things like "attrib -r +hs /s", and in fact, I hate how .net parses strings into it's array and BAH HUMBUG!  Forget it!!  No fucking one-size-fits-all!!!  I'll show you later where to swap out the parsing layer into something of your own choosing, but keep the binding and attributes layer, which is the interesting part anyways.

Invocation is simple

If you're only interested in using the code I've already written, this is to show you there's nothing to fear. You've already seen an example of an interesting Arguments class. Let's see how simple the my sample program is. You are not required to use this pattern, it's just how I chose to use the Arguments class.

public class ProgramHelloWorld
{
    public class ProgramHelloArguments
        { /* See above for Arguments Class */ }

    public ProgramHelloArguments Arguments = new ProgramHelloArguments();

    public void Run()
    {
        StringBuilder sb = new StringBuilder();
        if (Arguments.Greeting == ProgramHelloArguments.GreetingType.Friendly)
            sb.Append("Hi ");
        else if (Arguments.Greeting == ProgramHelloArguments.GreetingType.Familiar)
            sb.Append("What's up ");
        else
            sb.Append("Hello ");
        sb.Append(Arguments.Name);
        if (!String.IsNullOrEmpty(Arguments.Email))
            sb.Append(" (" + Arguments.Email + ')');
        if (Arguments.ComplimentUser)
            sb.Append(", you are looking exceptionally good today!");
        Console.WriteLine(sb.ToString());
        // more code in the full example, but you get the idea
    }
}

Next let's see the boilerplate to invoke it that program. The highlights are the ArgumentBindings and the ArgumentParser. The rest is mere error trapping, plumbing and niceties. Notice there's a binding phase that's seperate from the parsing phase.

  • Binding uses reflection to analyze your custom Arguments object and it's properties;
    • errors are the fault of the developer (e.g. decorating a non-boolean field with a a SwitchArgument).
  • Parsing interprets the command line parameters and attempts to assign values to properties of the Arguments object;
    • errors are the fault of the user (e.g. omitting a required field)

internal class ExecutionEntryPoint
{
    private static void Main(string[] args)
    {
        ProgramHelloWorld program = new ProgramHelloWorld();
        try
        {
            //Binding uses reflection to create a hash table of relevant information
            ArgumentBindings bindings = new ArgumentBindings(program.Arguments);

            //Parsing inspects the command line (args) assigns values to fields/properties
            ArgumentParser parser = new ArgumentParser(bindings);
            if (parser.HelpSwitchSpecified(args))
                bindings.WriteHelpText();
            else
            {
                parser.ParseAndAssign(args);
                program.Run();
            }
        }
        catch (ArgumentParseException ex)
        {
            Console.WriteLine(String.Format("Error: {0}", ex.Message));
            Console.WriteLine(String.Format("Use {0} to get usage parameters", ArgumentParser.HelpSwitch));
        }
        catch (Exception ex)
        {
            Console.WriteLine(String.Format("Error: {0} [{1}]", ex.Message, ex.GetType().Name));
        }
    }
}

So that's it, that's how to use this unholy beast. Why are you showing me this boring junk you scream? To stress the simplicity of usage. Spend your time marking up properties instead. After all, command line parsing should be simple.

Arguing with Attributes

I won't be dicussing the HelpText, Required and Position attributes, which contain no logic. Let's jump instead right into the CommandLineArgument. It is meant to decorate a string field (or property). It is also the base-class from which all other attributes descend.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class CommandLineArgumentAttribute : Attribute
{
    private readonly string _argumentToken;

    public CommandLineArgumentAttribute(string argumentToken)
    {
        _argumentToken = argumentToken;
    }

    // The token used when parsing the command line.  
    //   eg. my.exe -token {value} -token {value}
    public string Token
        { get { return _argumentToken; } }

    // Called during the bind process (when inspecting the reflected properties)
    public virtual void ValidateBind(MemberInfoAdapter member)
    {
        if (!member.Type.IsAssignableFrom(typeof(string)))
            throw new ArgumentBindException(String.Format("{0} '{1}' can only be applied to a property or field of type string", this.GetType().Name, this.Token));
    }

    // Should the parser interpret the subsequent command line as a value
    public virtual bool ExpectsValue(MemberInfoAdapter member)
    {
        return true;
    }

    // called during the parse phase when a command line is encountered
    public virtual void SetValue(MemberInfoAdapter member, object obj, string value)
    {
        member.SetValue(obj, value);
    }

    /// displayed to user during help text generation.  descendants can override to be more descriptive
    public virtual string GetHelpTextValue(MemberInfoAdapter member)
        { return "<value>"; }
}

What the heck is a MemberInfoAdapter? (seen referenced in SetValue)

  • It's an uninteresting class allowing us to do things like SetValue, or obtain the Type.
  • Works for both FieldInfo or PropertyInfo object.
    • The two classes both support such operations, but do not share enough ancestory, or a common interface.
  • The adapter simplifies the Argument classes, allowing them to easily support both fields and properties.

As discussed in the Invocation section, there is a serpate binding phase. The ValidateBind method provides a way for argument classes to validate they've been applied to a field of the correct type, or any other validation you can think of, based on the reflection information. The CommandLineArgument, displayed above, provides a good example of a ValidateBind implementation.

Let's examine the EnumArgument's ValidateBind for a bit of contrast.

  • It's meant to be applied to any enumerated type, used to restrict values accepted from user input.
  • SetValue translates a string to the proper enumerated value
  • GetHelpTextValue, returns a descriptive string, displayed during "my.exe -?" type operations
public class EnumArgumentAttribute : CommandLineArgumentAttribute
{
    public EnumArgumentAttribute(string argumentToken) : base(argumentToken) { }

    public override void ValidateBind(MemberInfoAdapter member)
    {
        if (!member.Type.IsEnum)
            throw new ArgumentBindException(String.Format("{0} '{1}' can only be applied to an enumerated property or field", this.GetType().Name, this.Token));
    }

    public override void SetValue(MemberInfoAdapter member, object obj, string value)
    {
        object enumvalue;
        try
        {
            enumvalue = Enum.Parse(member.Type, value, true);
        }
        catch (ArgumentException ex)
        {
            throw new ArgumentParseException(String.Format("'{0}' is not valid, must be one of {1}", value, GetHelpValue(member)), ex);
        }
        member.SetValue(obj, enumvalue);
    }

    public override string GetHelpTextValue(MemberInfoAdapter member)
    {
        StringBuilder sb = new StringBuilder("{");
        bool isFirst = true;
        foreach (string memberName in Enum.GetNames(member.Type))
        {
            if (!isFirst)
                sb.Append(", ");
            isFirst = false;
            sb.Append(memberName);
        }
        sb.Append("}");
        return sb.ToString();
    }
}

If all properties successfully bind, we move onto the parsing phase. The SetValue method will be called with details the user entered on the command line. This is where value validation or re-interpretation can occur. For example, you might ensure a numeric UserId actually exists in a database before assigning it the property. Taking it further, you might accept a UserId but assign a UserName to the object's property.

Here's a descendant property that ensures the user specifies a valid directory:

public class ValidDirectoryArgument : CommandLineArgumentAttribute
{
    public ValidDirectoryArgument(string argumentToken) : base(argumentToken) { }

    public override void SetValue(MemberInfoAdapter member, object obj, string value)
    {
        if (!System.IO.Directory.Exists(value))
            throw new ArgumentParseException(String.Format("'{0}' is not a valid directory", value));
        member.SetValue(obj, value);
    }
}

Finally, the ExpectsValue is meant primarily for the SwitchArgument, or switch-like arguments. It allows "my.exe -recursive" instead of a clunkier "my.exe -recursive true". It allows helps the parser determine wether "my.exe -nice -6", means "token nice has a value of -6" or "token nice exists; token 6 exists" (or "token nice exists; unamed value of -6 exists, use positional logic). Likely the SwitchArgument's bool support is all you'll ever need, but perhaps you can imagine arguments that do something else within SetValue?

   Visual repetition of the possible variable interpretations

public class SwitchArgumentAttribute : CommandLineArgumentAttribute
{
    public SwitchArgumentAttribute(string argumentToken) : base(argumentToken) { }

    public override bool ExpectsValue(MemberInfoAdapter member)
    {
        return false;
    }

    public override void SetValue(MemberInfoAdapter member, object obj, string value)
    {
        member.SetValue(obj, true);
    }

    public override void ValidateBind(MemberInfoAdapter member)
    {
        if (!member.Type.IsAssignableFrom(typeof(bool)))
            throw new ArgumentBindException(String.Format("{0} '{1}' can only be applied to a property or field of type bool", this.GetType().Name, this.Token));
    }
}

Final Thoughts & Download

That's all that's required to use it. If you curious about the the technical workings of Binding & Parsing, read through the appendix. If you have customized parsing requirements or are integrating into an existing framework you'll want to take a look for sure. This code is very much a receipe, feel free to download, tweak and customize it to your heart's content. I've also listed included a Future Enhancements appendix, with some ideas of how to build on the command line parsing that I've alredy written.

I've tried to be brief, and there are much more comments in the included code. Feel free to use it, modify it, etc. Just include a link back to this webpage in the source, and maybe drop me a comment to let me know you found this useful. There's a lot here, and we've found it useful base at my current work. Its unlikely you have a command line project now, but one day you will. Next time you're faced with a command line, remember Feelings of White, come back and grab the code that will save you mountains of time.

Download full source: CommandLineParsing.zip

Appendix A: Future Enhancements

  • As mentioned in Appendix B, the ArgumentParser is most likely the bit you'll need to improve/replace, to fit your unique situation.  It might be that some of the simpler choices I've made won't fit your need. 
    • You might need to allow Argument Attributes to participate in the parsing process themselves.  While this makes a tighter coupling of parse & attributes, it also allows things like both "-r42" and "-r 42", allowing the attribute to determine dynamically whether it expects a value.  The waters have already been muddied by the ExpectsValue and GetHelpTextValue
    • You might have a radically different parse algorithm (e.g. "name:value" format), values mixed with attributes (e.g. "-r42"), multiple switches being set within a single string (e.g. "+rh -s"), or any other things you can think of.
  • There are many generic CommandLineArgument descendants that could be written, left as an exercise for the reader.  Some ideas: Date Parsing, Float parsing, allowing comma,seperate,lists to be written to a List<string> property as well as your own domain specific attributes (the CustomerID attribute, etc)
  • A better automated help text generation system could be written.  I included it both for simple convenience, but also as an example of what else you could do with the Binding metadata gathered.

Appendix B: Binding & Parsing

You may be curious about what translates the argument attributes into useful calls to the custom arguments class.  Let's take a look at the binding and parsing phase in more depth.  I've removed quite a bit of extra code from the ArgumentBindings excerpt below, to better focus on the BindMember method (you'll have to scroll down a bit).  In essence, it's doing a few System.Reflection  calls to determine the attributes decorated from a _argumentObject, our custom parameter object, and parsing them into a Dictionary to make the information a little easier to deal with. 

public class ArgumentBindings : IArgumentAssignment
{
    private readonly bool _ignoreCase;
    private readonly object _argumentObject;
    private readonly Dictionary<string, ArgumentBindingInfo> _argumentHash = new Dictionary<string, ArgumentBindingInfo>();
    private readonly SortedList<int, ArgumentBindingInfo> _positionalArguments = new SortedList<int, ArgumentBindingInfo>();

    private class ArgumentBindingInfo
    {
        public CommandLineArgumentAttribute ArgumentAttribute;
        public bool Required;
        public bool Specified = false;
        public string HelpText;
        public MemberInfoAdapter Adapter;
        public int? PositionIndex;
    }

    // called by constructor (ctor omitted from this snippet)
    private void Bind()
    {
        foreach (MemberInfo memberInfo in GetFieldsAndProperties(_argumentObject.GetType()))
        {
            CommandLineArgumentAttribute argumentAttribute;
            if (GetSingleAttribute(memberInfo, out argumentAttribute))
                BindMember(memberInfo, argumentAttribute);
        }
    }

    private void BindMember(MemberInfo memberInfo, CommandLineArgumentAttribute argumentAttribute)
    {
        ArgumentBindingInfo argument = new ArgumentBindingInfo();
        argument.Adapter = MemberInfoAdapter.Create(memberInfo);
        argument.ArgumentAttribute = argumentAttribute;
        argument.ArgumentAttribute.ValidateBind(argument.Adapter);
        argument.Required = memberInfo.IsDefined(typeof(RequiredAttribute), false);

        PositionAttribute positionAttribute;
        if (GetSingleAttribute(memberInfo, out positionAttribute))
            argument.PositionIndex = positionAttribute.Index;

        if (argument.PositionIndex != null)
        {
            if (!argument.ArgumentAttribute.ExpectsValue(argument.Adapter))
                throw new ArgumentBindException(String.Format("'{0}' cannot have a position, because {1} does accept values",
                                                              argument.Adapter.Name,
                                                              argument.ArgumentAttribute.GetType().Name));
            ArgumentBindingInfo existing;
            if (_positionalArguments.TryGetValue(argument.PositionIndex.Value, out existing))
                throw new ArgumentBindException(
                    String.Format("Position '{0}' registered to both '{1}' and '{2}'",
                                  argument.PositionIndex,
                                  existing.Adapter.Name,
                                  argument.Adapter.Name));
            _positionalArguments.Add(argument.PositionIndex.Value, argument);
        }

        HelpTextAttribute helpTextAttribute;
        if (GetSingleAttribute(memberInfo, out helpTextAttribute))
            argument.HelpText = helpTextAttribute.Text;

        string token = argument.ArgumentAttribute.Token;
        if (_ignoreCase)
            token = token.ToLower();
        _argumentHash.Add(token, argument);
    }

    private static bool GetSingleAttribute<T>(MemberInfo memberInfo, out T match) where T : class
    {
        //If you're curious: 
        //  if AttributeUsage's AllowMultiple=True, Child's attribute is 0, Base's attributes is Length-1
        //  if AllowMultiple=False [default], Subsequent usages of an attribute (eg HelpText) replace previous ones
        //  For non-sealed classes (eg CommandlineArgumentAttribute), it is possible to have multiple
        //  returned, as attribute descendants of the attribute can be applied.  
        T[] attributes = (T[])memberInfo.GetCustomAttributes(typeof(T), true);
        switch (attributes.Length)
        {
            case 0:
                match = null;
                return false;
            case 1:
                match = attributes[0];
                return true;
            default:
                throw new ArgumentBindException(
                    String.Format("Member '{0}' has multiple {1} attributes or descendants",
                                  memberInfo.Name, typeof(T).Name));
        }
    }

    private static IEnumerable<MemberInfo> GetFieldsAndProperties(Type type)
    {
        foreach (FieldInfo info in type.GetFields())
            yield return info;
        foreach (PropertyInfo info in type.GetProperties())
            yield return info;
    }

    // Note: Many methods omitted for brevity
}

As I mentioned, there are many more methods omitted from this article which appear in the full code.  Chiefly I've skipped the WriteHelpText method mentioned during the invocation boilerplate (it loops through the _argumentHash and displays the strings captured by HelpText).  Also skipped is the implementation details of the IArgumentAssignment:

public enum ArgumentAssignmentType
    {  Unknown, Switch, NameValue  }

public interface IArgumentAssignment
{
    ArgumentAssignmentType GetArgumentType(string token);
    void SetArgumentValue(string token, string value);
    void SetPositionalArgument(string value);
    void FinishedParsing();
}

The IArgumentAssignment is the contract by which the Parser interacts with the Binding information.  Calls to GetArgumentType use the hash and CommandLineArgument's ExpectValue to determine the result, the next two are passthroughs to the SetValue, while FinishedParsing just ensures that all Required fields are specified. 

I don't feel this article could be complete without including the parsing code that shows the interaction between command line and the bindings.  But at the same time it's merely Yet Another String Parsing Problem.  I'll leave discussion on it's future potential to another day's session.  Feel free to skip the next code block, unless the nitty gritty of the argument parse are your cup of tea.  It's also the most likely candidate for your customization and/or replacement by you.

// Strings are expected to be in the form:
///   -name1 {value} -switchA -name2 {value} -name3 {value}
public class ArgumentParser
{
    public const string SwitchPrefix = "-";
    public const string HelpSwitch = SwitchPrefix + "?";
    private readonly IArgumentAssignment _argumentAssignment;

    public void ParseAndAssign(string[] args)
    {
        Queue<string> positional = new Queue<string>();
        int i = 0;
        while (i < args.Length)
        {
            string token = null;
            ArgumentAssignmentType argumentAssignmentType = ArgumentAssignmentType.Unknown;
            if (args[i].Substring(0, 1) == SwitchPrefix)
            {
                token = args[i].Substring(1);
                argumentAssignmentType = _argumentAssignment.GetArgumentType(token);
            }
            switch (argumentAssignmentType)
            {
                case ArgumentAssignmentType.Switch:
                    _argumentAssignment.SetArgumentValue(token, null);
                    break;
                case ArgumentAssignmentType.NameValue:
                    if (i + 1 == args.Length)
                        throw new ArgumentParseException(
                            String.Format("Argument '{0}' requires a value", token));
                    else
                    {
                        _argumentAssignment.SetArgumentValue(token, args[i + 1]);
                        i++;
                    }
                    break;
                case ArgumentAssignmentType.Unknown:
                    positional.Enqueue(args[i]);
                    break;
            }
            i++;
        }
        while (positional.Count > 0)
            _argumentAssignment.SetPositionalArgument(positional.Dequeue());
        _argumentAssignment.FinishedParsing();
    }

    //ctor and HelpSwitchSpecified are omitted
}
2008 Jul 16 11:48 pm; Filed under technical and tagged c#, command-line, library.
« We Are Metallica « before «
» after » Betty the Gas-Whore [Tamdhu Stories 1.1] »
Posting your comment.
Follow responses with this post's comment feed.

Leave a reply

Subscribe

Recent Awesomeness

  • More to follow
  • Ken Melnichuk
  • Scary fucking shit; the pictures
  • Scary fucking shit
  • Unsold Pilot
  • VA#5 Violent Aggression vs. The Racist Candy
  • FlashForward Brain Breaker
  • VA#4 Violent Aggression & The Legend of the F-Bomb
  • A piece of Ryan Turner’s desk is embedded in my hand
  • VA#3 Violent Aggression and the Onslaught of Destiny
  • VA#2 Violent Aggression Versus The Seal Meat
  • Behind Closed Doors
  • VA#1 Violent Aggression Is Advised
  • Lost in Thought, Wibbly Wobby Timey Wimey Narratives
  • Top 100 TV Series Of All Time, Ever

Find things tagged

4400 Battlestar Galactica Battlestar Galactica cliff comics curation depression erron family fiction game janine job kelly kyle liam lost manifesto meta mlp music nathan passionate diatribes plug poem politics powershell Really Dumb Story relationships review revisionism sam sermon software spirituality star trek Star Trek Deep Space Nine suicide tamdhu testpoint the process travels video vlad wtf

Other Opinions

  • Tammy on Ken Melnichuk
  • Kyle on Scary fucking shit
  • Erron on Scary fucking shit
  • Tammy on Scary fucking shit
  • Morpheus on Scary fucking shit
  • Cliff on Scary fucking shit
  • mike on Scary fucking shit
  • Kyle on Unsold Pilot
  • legion on FlashForward Brain Breaker
  • Liam on FlashForward Brain Breaker
  • Growlie on BSG 4×20 – Daybreak [Part 2 & 3]
  • legion on BSG 4×20 – Daybreak [Part 2 & 3]
  • legion on Obama vs. Adama
  • MD on Obama vs. Adama
  • Growlie on BSG 4×20 – Daybreak [Part 2 & 3]

Friends of White

  • The Ack Attack is bringing Lost & BSG awesomeness direct to your brain!
  • Peer Pressure Works to bring you football, rants, politics and kittens
  • What's Alan Watching is your television addiction
  • Jammer's Reviews provides insight into BSG and Star Trek
  • The Grind is a pixelrific exploration of WoW addiction and more
  • In The Now touched me inappropriately with its writing
  • The Angry Scotsman contains a powerfully cranky football
  • Analog Coast amalgamates many blogs into one and lately some original content has washed ashore

What was I doing in..

  • January 2010 (1)
  • December 2009 (4)
  • November 2009 (2)
  • October 2009 (1)
  • August 2009 (2)
  • July 2009 (2)
  • June 2009 (1)
  • May 2009 (1)
  • April 2009 (3)
  • March 2009 (11)
  • February 2009 (6)
  • January 2009 (10)
  • December 2008 (3)
  • October 2008 (1)
  • August 2008 (2)
  • July 2008 (3)
  • June 2008 (1)
  • May 2008 (11)
  • April 2008 (7)
  • March 2008 (3)
  • February 2008 (1)
  • January 2008 (2)
  • December 2007 (1)
  • October 2007 (1)
  • September 2007 (3)
  • August 2007 (1)
  • June 2007 (3)
  • May 2007 (2)
  • March 2007 (5)
  • February 2007 (5)
  • January 2007 (13)
  • September 2006 (1)
  • June 2001 (3)
  • May 2001 (2)
  • April 2001 (2)
  • March 2001 (2)
  • February 2001 (1)
  • January 2001 (1)
  • November 2000 (5)
  • May 2000 (3)
  • April 2000 (5)
  • March 2000 (3)
  • February 2000 (3)
  • January 2000 (6)
  • December 1999 (17)

Copyright © 2009 Feelings of White | Powered by WordPress | Original site design by Stephen Reinhardt; tweaked by me