TeraBIThia

16/05/08

Arguments Handler

Archivado en: General — crisfervil @ 11:34 pm

Llevo varios días queriendo escribir este post. La verdad es que cuando se escriben habitualmente aplicaciones de consola, tener que lidiar cada vez con la manera en que gestionamos los parámetros que admitimos en la línea de comandos es un poco engorroso.

Por eso, en su día escribí una función genérica que me ahorraba todo el trabajo.

Esta mañana, me encontré con este post de mi vecino virtual ;) . En él, Valeriano nos hace una propuesta para tratar los argumentos de la línea de comandos.

Yo quisiera pues, dar otro enfoque, que no es mejor ni peor, ya que que tiene por contra que es menos flexible y más complejo de implementar, pero me parece un poco más cómodo de usar.

A mí me gustaría poder definir una clase, que represente todos los parámetros que admite mi programa. Una propiedad por cada parámetro. Con su tipo correspondiente (esto me ahorraría validaciones sencillas). Si el tipo de la propiedad es un bool, lo trataremos como modificador.

Por ejemplo, para el comando

c:\ConsoleApplication9.exe FileName="c:\Program Files\Test.txt" Overwrite ShowProgress

Tenemos:
El parámetro FileName, con valor c:\Program Files\Test.txt
El parámetro Overwrite, que es un modificador.
El parámetro ShowProgress, que es un modificador.

Además, mi aplicación admitirá otros parámetros. Definiremos por lo tanto un clase que los contemple a todos.

image

 Command: Como la aplicación admite varios parámetros, establecemos una propiedad que nos indicará cuál ejecutar. Los parámetros admitidos vienen definidos en el Enum Commands. Pueden ser Get, Set y Delete.

FileName: Un string cualquiera.

Format: Otro Enum. En este caso para los formatos de salida del comando. Al igual que Command, se definen los valores posibles en un Enum.

Overwrite: Es un modificador. Es decir, no tiene valor. La presencia de este parámetro dentro de los argumentos se indicaría con un true en esta propiedad.

Password: Otro string cualquiera.

ShowProgress: Otro modificador. Igual que Overwrite.

StartDate: Un valor de tipo fecha. La propiedad debería ser un nullable, para que cuando no se haya especificado valor, lo podamos saber.

UserName: Otro string cualquiera.

Necesitaremos, una función que, a partir del array de string que recibimos en el Main, obtenga una instancia de MyArguments con todas las propiedades rellenas correctamente. Debería además ser genérica para no tener que andar realizando conversiones.

class Program
{
    static void Main(string[] args)
    {
        MyArguments myArgs = ArgumentsHandler.Get<MyArguments>(args);
    }
}

Y cómo se cocina esto?… pues, allá vamos.

En primer lugar, hemos de saber que cada elemento del parámetro args representa cada uno de los parámetros de la línea de comandos. Y cada parámetro viene delimitado por espacios en la cadena obtenida en System.Environment.CommandLine . Para incluir un espacio como parte de un parámetro debemos delimitarlo por comillas. Y los items de args no incluirán estas comillas.
Si tomamos como ejemplo la línea:

c:\ConsoleApplication9.exe FileName="c:\Program Files\Test.txt" Overwrite ShowProgress pepe juan

args[0] será igual a FileName=c:\Program Files\Test.txt
args[1] será igual a Overwrite
args[2] será igual a ShowProgress
args[3] será igual a pepe
args[4] será igual a juan

Como primer paso, crearemos la clase ArgumentsHandler con un método estático Get, que además sea genérico. Este método admitirá el array de string que contendrá los parámertros recibidos en el Main.

public class ArgumentsHandler
{
    public static T Get<T>(string[] args) where T : new()
    {
    }
}

Necesitamos una manera de obtener la referencia a una propiedad de T, a partir del nombre. No tendremos en cuenta las mayúsculas/minúsculas. Lo haremos por reflexión. La propiedad debe ser pública y ser una propiedad de instancia (no estática).

private static PropertyInfo GetPropertyInfo(string propertyName, Type argumentsEntityType)
{
    PropertyInfo retVal = null;
    retVal = argumentsEntityType.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
    return retVal;
}

Si propertyName no es propiedad de T, este método devolverá null. Pero qué haremos? lanzamos una Exception? Mostramos un mensaje? Lo haremos configurable.

private static void CheckPropertyInfo(string propertyName, PropertyInfo propertyInfo, bool throwExceptions)
{
    if (propertyInfo == null)
    {
        if (throwExceptions)
        {
            throw new Exception(String.Format("Argumento {0} no reconocido", propertyName));
        }
        else
        {
            Console.WriteLine("Argumento {0} no reconocido", propertyName);
        }
    }
}

También, para el caso de parámetros que deban admitir un valor, necesitaremos obtener, por un lado nombre, y por otro el valor de la propiedad.

private static string[] SplitArgument(string argument)
{
    string[] argumentParts = new string[] { argument, argument };

    if (argument.Contains("="))
    {
        argumentParts = argument.Split('=');
    }

    return argumentParts;
}

Este método, devolverá un string con 2 posiciones en la que la primera corresponderá al nombre del argumento, y la segunda al valor. El separador nombre/valor es el signo igual. Si no se especifica este nombre=valor, devolveremos el nombre en las dos posiciones.

Queda convertir una string al tipo de la propiedad.

private static object GetValue(PropertyInfo propertyInfo, string value)
{
    // Instanciamos el valor de retorno
    Object returnValue = null;
    if (propertyInfo.PropertyType.IsEnum)
    {
        // Si el tipo de la propiedad es un enum, lo parseamos, ignorando el casing 
        returnValue = Enum.Parse(propertyInfo.PropertyType, value, true);
    }
    else if (propertyInfo.PropertyType == typeof(bool))
    {
        // Si el tipo de la propiedad es un bool, será true siempre.
        // Este es el caso de los modificadores
        // Si el usuario lo ha mencionado en la línea de comandos es porque quiere el modificador
        returnValue = true;
    }
    else if (propertyInfo.PropertyType == typeof(DateTime?))
    {
        // Si es de tipo fecha, lo parseamos
        returnValue = (object)DateTime.Parse(value);
    }
    else
    {
        // Si no es ninguno de esos tipos, intentamos una conversión directa
        returnValue = value;
    }
    // Devolvemos.
    // Si hubieran habido problemas de formato del valor, se habría disparado 
    // una excepción en la conversión
    return returnValue;
}

Asignamos el valor convertido a la propiedad

private static void SetPropertyValue(object objectInstance, PropertyInfo propertyInfo, string value)
{
    Object castedValue = GetValue(propertyInfo, value);
    propertyInfo.SetValue(objectInstance, castedValue, null);
}

Y finalmente, lo montamos todo.

Un requerimiento de última hora (nunca faltan). Si mi aplicación es de las de “una aplicación varios comandos” no vamos a escribir MiAplicacion.exe command=Get
El método debería ser lo bastante listo para saber que el primer parámetro corresponde al comando que queremos ejecutar y debe ser asignado a la propiedad Command.

// Instanciamos el valor de retorno
T retVal = new T();
// Obtenemos el tipo de retorno
Type argumentsEntityType = typeof(T);
// Iteramos sobre los elementos de args
for (int i = 0; i < args.Length; i++)
{
    // Separamos el nombre/valor
    string[] argumentParts = SplitArgument(args[i]);
    if (i == 0 && firstArgumentMustBeCommand)
    {
        // Si hemos indicado que el primer parámetro debe ser el comando, 
        // diremos que su nombre es Command
        argumentParts[0] = "Command";
    }
    // Obtenemos la PropertyInfo correspondiente
    PropertyInfo propInfo = GetPropertyInfo(argumentParts[0], argumentsEntityType);
    // Mostramos mensaje o lanzamos excepción si no la hemos podido obtener
    CheckPropertyInfo(argumentParts[0], propInfo, throwExceptions);
    // Si hemos podido obtenerla
    if (propInfo != null)
    {
        // Le establecemos el valor
        SetPropertyValue(retVal, propInfo, argumentParts[1]);
    }
}
return retVal;

 

Y voilá. Ya lo tenemos. ¿Cómo se usa?

class Program
{
    static void Main(string[] args)
    {
        MyArguments myArgs = ArgumentsHandler.Get<MyArguments>(args);
        Console.WriteLine(Environment.CommandLine);
        Console.WriteLine("Command: {0}", myArgs.Command.ToString());
        Console.WriteLine("UserName: {0}", myArgs.UserName);
        Console.WriteLine("Password: {0}", myArgs.Password);
        Console.WriteLine("FileName: {0}", myArgs.FileName);
        Console.WriteLine("Format: {0}", myArgs.Format.ToString());
        Console.WriteLine("StartDate: {0}", myArgs.StartDate.ToString());

        Console.WriteLine("ShowProgress: {0}", myArgs.ShowProgress.ToString());
        Console.WriteLine("Overwrite: {0}", myArgs.Overwrite.ToString());
    }
}

 

Para una línea de comandos del tipo
Set UserName=crisfervil qwerty PASSWORD=pepe SHOWPROGRESS FileName=”c:\Program Files\Test.txt” Format=doc StartDate=31/03/2007 asdf

La salida será

image

Tengo previsto añadirle mejoras, como la posibilidad de definir atributos a las propiedades para identificar sinónimos. Por ejemplo, poder escribir -p en lugar de showprogress. Pero eso, para la v 2.0

Crossposted from crisfervil.com

Aún no hay comentarios »

Aún no hay comentarios.

Canal RSS de los comentarios de la entrada. URI para TrackBack.

Deja un comentario

Blog de WordPress.com.